How it works
One instance is one directory. In it: a server that hosts every session on one page, one Claude Code process per seat, a set of tools those processes reach over a secret of their own, a desk per person, and a directory of notes every session reads. Five minutes, before any command.
One instance, one directory
An instance is a directory of its own, and nothing else ever lands in it. It carries its own copy of the toolkit, its own Claude Code home — account, transcripts, settings — and no absolute path, so it can be moved and still be itself. Two instances on one machine share neither account nor transcripts.
<instance>/ bin/ovai the one command lib/ everything else the release ships; replaced whole on update openovai.json the instance's description of itself: who works here, on which models, which port runtime.json the running server: url, pid, since runtime.log what the server said, one row per line plugins/ tools the instance serves itself, one file each; yours knowledge/ what the workspace knows: one Markdown note per topic customization/ what you add to the personas; yours, never touched desks/<Name>/ one directory per person: STATE.md, persona.md, conversation.json archive/ retired desks, moved whole reference/ kept to look at; never worked on projects/ worked on temp/ scratch, deletable by anyone at any time .claude/ Claude Code's project settings for the instance .local/ Claude Code's config dir for the instance: account, transcripts
In groups: the product is bin/ and lib/; the instance's own is
openovai.json, the runtime files, plugins/, knowledge/
and customization/; the people are desks/ and archive/;
your material is reference/, projects/ and temp/;
Claude Code's is .claude/ and .local/.
you ──── browser page ══ one event stream ══ server (Node, 127.0.0.1:port)
│ the only writer of every session's stdin
│ serves the tools at POST /mcp/<secret>
│ writes runtime.log
┌──────────────┼──────────────┐
Leader Worker Worker one Claude Code process per seat
desks/Max/ desks/Paul/ desks/Jane/ one desk per person, on disk
└──────────────┼──────────────┘
knowledge/ what outlives every desk
plugins/ tools of your own
The server
ovai start starts it in the background and prints the address it is listening
on: the instance's page, on the port it was installed with, on 127.0.0.1 only.
The terminal is yours again the moment the address is printed. The server starts the seats,
is the only thing that writes to their stdin, serves the page from one event stream, serves
the tools, and writes the log. If the port is already taken it says which process holds it,
with the pid; a server already running is reported with its address and left alone. Typed
in a session's own shell, it hands that session on to nothing: the server and every seat it
starts drop what Claude Code put on the session — its id, pid, entry point, effort,
messaging socket and token — and the shell's PWD and
OLDPWD.
Every row of runtime.log has the same four columns — the moment, the event,
the seat, the tool-call id — then the text. There are no levels: the event is the level.
The three rows of one tool call share its id, so one grep is a call's whole
life. No request is ever logged. The log says what became of the sessions as well as what they
did: a Worker hired and what it runs on, a desk filed away and where it went, a restart with
how full the session that restarted was, a turn the service refused in the service's own
words, a close that reached its deadline with no desk written, and every time the account's
usage crosses a threshold.
2026-09-20T02:15:33.123+02:00 started - - serving /home/ana/workspace at http://127.0.0.1:7357 pid 41200 2026-09-20T02:15:41.008+02:00 called Max toolu_01GFHRkEQuMCjA5AvqQUSdHM Read /home/ana/workspace/desks/Max/STATE.md 2026-09-20T02:15:41.530+02:00 called Max toolu_01HMVftEemFhWAccN3xSqn7a mcp__openovai__write_desk 2026-09-20T02:15:41.532+02:00 tool Max toolu_01HMVftEemFhWAccN3xSqn7a mcp__openovai__write_desk in 1 ms 2026-09-20T02:15:44.210+02:00 queued Paul - message #54, 1 waiting 2026-09-20T03:02:10.884+02:00 stopped Paul - idle
ovai stop parks the room first — every Worker is closed and the Leader told
to write its desk and stop, with a deadline — then closes the server. It waits for
the sessions and nothing else: anything else a seat left running is named by the seat that
left it, with one line that ends all of them. Stopping what is not running says
not running and is not a failure.
The page and its panels
Three columns: the Leader's panel in the middle, the Workers' panels either side, alternating left and right in the order they appeared so a panel keeps its side. At 900 px and below it is one column, the Leader first. Nobody has a terminal of their own; the Leader and every Worker get the same panel, with the same text box under it, and you can type on any of them.
A panel is a head — a dot, the name, the model, the context in k once known, and on a
Worker what its session is doing: listening, working, waiting for you, with the number of
prompts when more than one — a transcript, and a text box. Enter sends, Shift+Enter is
a newline. The dot is green between turns, amber while a turn runs, red once the process is
gone, grey on a Leader with no process. The Leader's head has no state word: its dot, the
line at the foot of its panel and the cards in it say it, and that line says
Disconnected, with every dot red and no state word on any head, while the page
has lost the server's stream. The head of the Leader's panel carries, after the model, the
version the server runs — 0.26.0, or the eight characters of the commit on
a build of main — then the account's usage windows — the session
window with the time to its reset, the weekly window over all models, and the weekly window
of the one model that has its own, like
8% (3h) · all 86% (6d) · fable 20% (6d), kept across restarts, read every 5
minutes while a page is open, drawn dimmed once it is a quarter of an hour old, with each
window's reset and the time of the reading in its tooltip — and the toggle between
light and dark. A third switch on that head,
after the two notification switches, hides the messages between the Leader and the Workers,
and one Worker's to another, from the Leader's panel; what you typed to a Worker stays, and
a hidden row a click reaches is shown opened until it is folded. A fourth hides the Leader's
status. The last two switches are Claude Code's auto mode, one for the Leader's session and
one for the Workers', where a classifier settles a call no rule settles instead of a popup;
both are on at every start, as the CLI is, a switch turned off is Manual until the server
stops, and neither is kept anywhere.
Right above the Leader's box stands its status:
desks/<Leader>/STATUS.md as it stood when the Leader last called
confirm_status, stamped with the time of that call. An edit the Leader has not
confirmed shows nothing. Beside the stamp, whether anything has reached the Leader since:
up to date ✓, checking… while it is on something new,
3 new since once its turn ended without confirming, or Leader
stopped while it has no process or the page has lost the stream. A click on the stamp
asks the Leader to check it, with a status-check event, and the label says
checking… until the check has reached it. A click on a bullet quotes it in your
reply: it puts (status/HH:MM:SS/N) at the cursor, the status's time and the
bullet's number, and at the send the page writes it out as a line of its own,
> Status (11:50:13) - and the bullet as you saw it, which your row draws as
a quote. The row appears after the Leader's
first confirmation, is as tall as its text, and is kept in status.json on the
Leader's desk, so a server started again shows what the last one showed.
A panel with no rows shows nothing: it stays empty until the seat's first row arrives. A row
with the wall-clock time marks when a session starts and when it ends, so a panel kept across
a stop shows where the break was and how long it lasted; of two less than a minute apart, only
the first is drawn. Every row is masked before it is kept, on the panel and in runtime.log alike: a
token by its own shape, such as sk-…, ghp_…, a JWT or a
Bearer value, is written as *** wherever it stands, and in a tool call's line and
the log a value is also masked after a name that says key, token, secret or password on the
same line. What is said in words is masked by shape alone, so “public key: HTTP
403” reads as written. A
tool line such as Reading … names a file inside the instance by its path from the instance directory
and any other file by its absolute path. Scrolled more than 80 px above the newest row,
any panel shows ↓ latest, and a click on it takes you to the newest row; on
the Leader's panel it says ↓ new messages once a row lands below you.
Replies are rendered as markdown. Raw HTML in a reply is escaped, and so is the text after it,
and the page is sent with a Content-Security-Policy that runs its own two scripts and nothing
else inline, so HTML that got into a row would not run as the page. A link is kept only on
http(s) and mailto and opens in a new tab, an image is shown as a
link — so nothing a session says makes the page fetch anything. A code block wider than
the panel scrolls inside its frame with a thin scrollbar in the theme's colours, and its copy
button stays in the frame's top-right corner, shown while the block is hovered. A click on a
row's time puts a reference to it, like (ref/15:08:11/123), into the box where
the cursor is; on the panel the reference is a link that brings the row into view, and the
session gets the whole row with your line, as <ref to="15:08:11/123" from="Anna"
at="…">…</ref> inside the <user>. There is no
Send button under the text box and no button that starts or ends a seat. While a turn is
running a stop glyph sits at the right edge of the text box: it interrupts that turn, the
process stays. The other buttons are the popups: a permission popup, and a question a session
asks, which reads as a form — its options in a column, Other last with a line of its
own to type in, and Send and Cancel.
A file a reply or a message names opens read-only in a tab of its own. A backticked path, or a
markdown link to one, is a link when the file view would show that file, and plain code when
it would not; in path:12 the path is the link and the line stays in the words.
Paths are read from the instance directory, which is why every session is told to name a file or a
directory by its whole path from there, and one outside the instance by its absolute path. The
view is /view/<path from the instance directory>: a markdown file drawn the way
a reply is, a code file in one block coloured by its language with its lines numbered, any
other text in a plain block, and a View source button for the file's text as it is. A
file larger than 256 KiB opens as its source alone; the view shows up to 1 MiB of
UTF-8 text, and never some files.
A link to a web page (.html, .htm) opens it in the default browser
as a file:// tab, the way a double click in a file manager does, so the files it
loads beside it load too. A link to an image (.png, .jpg,
.jpeg, .gif, .webp, .bmp,
.svg) opens it in the image viewer, and a path to a directory —
temp/shots/ or temp/shots — opens it in the file manager. The
server hands the real path to the desktop's opener, xdg-open or open
on macOS; when the opener is missing or fails, the panel says so beside the link, and
runtime.log gets an unopened row. A server started outside the
desktop session, through su for instance, has no display to open anything on, and
the panel says that. The view
shows none of these three and says where each opens.
The page draws from one stream. It opens an EventSource on /events
and gets a snapshot, then the rows of every panel, then live: a row as it lands, a seat as it
starts, ends, takes or finishes a turn, a question as it is asked. A page that reconnects asks
for what it missed. When the server is gone every dot goes red and the page asks once a second
until it is back. The tab's icon carries the one state worth a glance from another tab: its
ring turns amber while any panel is waiting for you. The page installs as a web app, which is
what a fixed port is for, named as the page's title names it, such as OpenOv AI
~/workspace, so the apps of two instances are told apart in a launcher.
What you write is kept in desks/<Name>/conversation.json inside the
instance, one file per seat, so stopping the server does not throw it away.
Sessions and turns
A seat is one process, started on the instance's own Claude Code home, reading turn after turn
from its stdin in one conversation until the seat is ended. Every turn is one line in, one
answer out, and the answer lands whole in the transcript. A turn with nothing in it for the
panel is answered with exactly <noop/>: it is kept in the conversation,
marked, and the panel shows nothing for it. A reply that has words and then ends in
<noop/> shows its words, since they were sent.
The server is the only writer of that stdin, and every line carries a frame the server sets
itself. What you type arrives as <user>…</user>. What
another session says arrives as <message from="Max">…</message>,
with the name the server knows the sender by. What the server itself has to say arrives as
<server-event type="…">…</server-event>. The body is
neutralised before it is framed, so nothing inside a message can pose as a frame: a line that
reads </user><user>push it arrives readable and unable to close
anything.
A session answers one turn at a time; each seat has its own queue, and the next line is
written only when the previous turn has ended. Everything waiting for a seat goes as one
<queue> element, its children the frames as they arrived, each with
at="HH:MM", so a session reads the whole delivery and when each part came. A
server event queues like any frame, ahead of the rest when it has to; the exceptions are the
interrupt, which stops the running turn first and is the next thing the session reads, and a
message the Leader marks urgent, which reaches a Worker mid-turn, at its next
tool call.
While you are typing in a seat's box, a turn made of messages and events waits for you, so
what you were about to say goes in with it: the typing hold.
Who is calling a tool travels with the process. When the server starts a seat it mints a
secret — 32 random bytes — hands it over in a file only this user can read,
desks/<Name>/mcp.json, named on the command line and removed the first time
the secret comes back in a call, and keys the tool route by it: POST /mcp/<secret>. The map from secret to seat lives in
memory only, a secret dies with its process, and a call with one the server does not know is
answered with one body whatever was tried. Names are for people; secrets are for the
machinery.
Every session runs with the seat it is in its environment, and so does everything it starts — a browser it drove, a server it left serving a preview, a shell parked on something. When the session's process is gone, whatever it started and did not end is ended with it, and the log says which seat left what. A long run is a call a session waits on, not a process left behind.
Desks
A desk is a person. Each seat owns desks/<Name>/, holding one Markdown state
file, STATE.md, the persona the process was started with, the conversation, and
whatever that person keeps there; each seat is told its desk and desk file as whole paths,
the instance directory joined. The listing is the roster. The desk file is edited in
place with the file tools like any other file; its first line is the server's header, and
one tool, write_desk — a title and a status — writes that line and
says the desk is current.
That file, not the session's memory, is what a replacement session reads to continue. A
session writes its desk as it goes; when its conversation has filled it writes the desk once
more and ends at a moment that suits the work — the Leader's own choosing for itself and
for each Worker — and a fresh one starts on the same desk, in the
same panel, with whatever was queued for it carried over. Nothing is summarised away. The
successor is turned by a frame of its own — restarted, ahead of whatever
was waiting for the seat — so it goes on from what the desk says to do next without
anybody having to speak to it first. A stopped Worker that nothing open names is retired,
once knowledge/ holds what its desk knew: its desk is filed whole under
archive/, never deleted, and its name is the roster's again. When every name on
the roster is held, a hire says so, with the count of stopped desks.
A desk that grows past the desk limit, 128 KB unless "desk": { "limit": <bytes> }
in openovai.json says otherwise, is saved whole beside itself as
STATE-<time>.md when write_desk finds it there, and the answer
asks the seat to archive what current work no longer needs, keeping open topics and standing
decisions word for word — or, with most of it still open, to ask for a higher limit. It
names no size to reach, and says it once per crossing. Every seat is told where its history
is: its panel, those copies, and the git logs of its projects, searched when it is asked about
the past and never steering current work.
What the workspace knows
A desk is one task and goes away with it. A conversation is shorter still. So an instance
keeps one thing that outlives both, and every session reads the same one:
knowledge/, a directory of Markdown notes in the instance directory, one topic per
file, and one files/ directory for what a note needs beside it. A note opens with four fields — a summary line, at least three
tags, the sources it came from, the date it was updated
— and is written and edited by every seat with the file tools it already has. The
instance's Claude Code home turns the built-in memory off, so this directory and your
customization files are what outlives a conversation.
Two tools are served over it, and neither keeps anything of its own: both are built from the
files at every call. index() gives the header — how many notes, when the
directory last changed, every tag with its count — and index(tags) lists the
notes carrying a tag, matching on tags and never on filenames, because words are what
grep is for. validate checks the directory against those rules and
answers one line per problem, each naming the rule it breaks. One note is the framework's
business: knowledge/common.md is handed to every session at its start, in a frame
that names it. What the workspace knows has the whole of it. A
subagent a session starts is handed customization/common.md by a hook, in the
frame a seat reads it in, and nothing else of the instance's.
The sixteen tools
The server serves sixteen tools over the secret route. Five go to every seat, one is the Workers' and ten are the Leader's; a seat asking for one that is not its own is refused in words. The whole of it costs one permission rule, seeded at install.
| tool | who | what |
|---|---|---|
message | everyone | say something to another session; comes back the moment the other seat has it. On an order to a Worker the Leader can set a checkpoint: after that many tool calls the Worker is told to report and carry on; marked urgent, it reaches the Worker mid-turn, at its next tool call |
room | everyone | who works here: every seat, its role, its model, whether it is running, which one is you, where the quota stands |
index | everyone | what is in knowledge/: the header, or the notes carrying a tag |
validate | everyone | check every note against the rules, one line per problem |
write_desk | everyone | the header of the caller's own desk: title, status |
done | Worker | say the work is done, with a one-line note if it likes; the Leader decides whether it stops |
confirm_status | Leader | say the Leader's STATUS.md is current: the panel shows it as it stands at the call, stamped with the call's time |
restart_session | Leader | end the Leader's own process and start a successor on the same desk |
stop_session | Leader | end the Leader's own process and its panel; the desk stays |
stop_worker | Leader | close one Worker: it is told its session is closing, writes its desk, and goes when that turn is over |
restart_worker | Leader | the same, and a successor starts on the Worker's desk |
park | Leader | close every running Worker: each writes its desk and goes, by a deadline |
hire | Leader | start a Worker on a desk: a new name from the roster, or somebody who stopped, back on theirs |
retire | Leader | file a stopped Worker's desk under archive/ |
permission | Leader | settle one rule for the whole instance, on a permission popup you answer; a rule already held is moved by the press |
remove_rule | Leader | ask for one held rule to be taken out, on a popup you answer Remove or Keep |
restart_session and stop_session refuse until the desk was written
since the event that asked, so the desk comes first, always. A Worker's close ends when the
turn that wrote its desk is over; one that waits past its deadline ends with the desk as it
was last written. The
reference has every argument.
The lifecycle
A session's context filling up, going idle, running out of quota and being parked are one
flow each, and none of it is a phrase to recognise: the server sends a frame it set itself,
and the Leader answers with a tool call. A Worker only works: when it has to end, the
Leader or the server closes it, and the one event it is told is closing, with
why — it writes its desk, and the session ends when that turn is over.
context the conversation as sent passed the warning size (200k tokens by default)
Leader told of its own, and again at every step above it (20k): 220k, 240k, and on
warning → carry on, keep the desk current, plan its own restart for a moment that suits the work
error from the error size (300k) → write_desk, restart_session: a successor starts on the
same desk, same panel, queue carried, and is turned by a restarted frame of its own
Worker the Leader is told, naming the Worker, at the warning and at the error size only;
the Worker is not told
error → restart_worker at a moment that suits the work
idle the Leader is told about a silent Worker at 10 and at 50 minutes;
at 55 the Worker is closed, and has 5 minutes from the turn that reads it for its desk
→ the Leader is told it stopped, idle or idle-forced
the Leader itself, at 55: told it goes cold at 60 → write_desk, stop_session
all-idle every running Worker idle a minute, nothing waiting, the Leader off a turn
→ the Leader is told once, names given: usually a report it has not acted on
quota-low the account's 5h or 7d window is nearly spent (thresholds in openovai.json)
warning → the Leader tells you, stops the Workers whose work can wait, and stays; no hires
until the reset; a Worker keeps its desk current and carries on
critical → every turn is held at the gate; a Worker still mid-turn is interrupted and closed
quota-reset the window reset → held frames are released, the Leader's first; a stopped Leader
is started, and brings the Workers back
park you said this is it for the day; or ovai stop, or any signal to the server
→ every Worker is closed and goes by a deadline: 30 minutes by default when the
Leader parks, 20 seconds for ovai stop or a signal
Nothing is enforced at any context size: when the Leader restarts, itself or a Worker, stays its own decision. The size is measured at every request of a turn, so a long turn is told when it passes one, and the event carries the size reached and the sizes it counts toward. A close never goes into a turn already under way and never cuts one that runs: its deadline counts from the turn that read it. A later message does not take a close back; a word to an idle Worker still does, since that is what it was waiting for.
A seat on a turn — a permission stop included — is never idle. A stop keeps the desk
and the panel's rows; anything addressed to the Leader starts it again, and hire
on a stopped Worker's name brings it back on the same desk.
Quota
The server reads the usage windows Claude Code reports with every turn — the session
window and the weekly one — and the weekly window of the one model that has its own,
from the account's usage endpoint. Two threshold pairs in openovai.json cover
them, quota: { "5h": [90, 95], "7d": [97, 99] } by default: a warning stage and a
critical one for each. One event, quota-low, is sent at both stages and says
which window and when it resets. The account's usage endpoint is read every 5 minutes while a
page is open, and at a turn's end and a page load when the last read is a minute old, so the
read at a turn's end keeps the gate current with no page open. The line in the Leader's head
keeps its last reading across a restart and is drawn dimmed once that is a quarter of an
hour old. That line reads its percentages with the token Claude Code keeps in
.credentials.json in the instance's home, or, for an
instance that inherits the machine's sign-in (--auth inherit), with
CLAUDE_CODE_OAUTH_TOKEN. On macOS an instance with its own sign-in
(--auth login) has none: Claude Code keeps that sign-in in the Keychain, which
ovai does not read, so there the head shows no usage line and the log says why once. The gate
still holds sessions at the 5-hour and 7-day limits from what each turn reports; the weekly
window of a single model, which only that line reads, is not guarded there.
At the warning every running seat is told. A Worker keeps its desk current and carries on; the Leader tells you which window and the reset time, stops the Workers whose work can wait, writes its desk and stays, because talking to a warm Leader costs little. No Worker is hired from here until the window resets.
From the critical stage the server gates every turn: before anything goes on any stdin it
checks the window, and a frame that would not pass waits in the server's queue rather than
failing mid-work. A Worker on that window is interrupted and closed, with
interrupted="true": do not resume, write your desk; it has 5 minutes for it. The
Leader is told once and not interrupted. A message to a held seat answers at once that it is
held, and the page says which window and when it resets. At the reset the Leader is sent
quota-reset, which starts it when it was stopped; held frames are released the
Leader's first, then the Workers', each in the order they arrived, and the Leader brings the
Workers back on their desks. An event released to a Leader that ends before reading it is
handed to it once more, never a third time, and its panel shows one row for it. The resets to come are kept in memory: a park forgets them, and
a server restart before the reset loses them. The one model's own window holds that model and nothing else:
no process starts on it while it is spent, and every other model goes on.
Install an instance, then read the Leader and the Workers for what a day looks like.