Reference
Everything with a name, in tables. The two commands, the instance directory, the configuration keys, the sixteen tools and what they take, the events a session receives, the rules an instance is born with, the log, the plugin contract, and what it needs to run.
openovai · ovai · the instance directory · openovai.json · the typing hold · tools · server events · seeded rules · runtime.log · plugin contract · requirements
openovai — the user-level command
One POSIX shell script at ~/.local/share/openovai/openovai, linked as
~/.local/bin/openovai. Read from a pipe —
curl -fsSL
— it installs itself there and says so; if ~/.local/bin is not on
PATH it prints the line to add to your shell profile and never edits one. Run
from its place it resolves a release, prints it, downloads it once under
~/.local/share/openovai/releases/<version>/ and hands over to that release's
installer. It knows one option of its own:
| form | what |
|---|---|
openovai | the newest release on GitHub at that moment, printed as openovai <version> (latest) before anything is done; with no other argument the release's installer asks its questions, and the version question is the only one openovai asks itself, offering latest and the versions already downloaded |
openovai <version> …openovai --version <version> … | that release |
openovai [version] | every argument after the version is passed through unchanged to the release's installer; all given, nothing is asked. --force installs over an instance already in place, as an update of it. The options are on the install page |
The same installer is also published on npm as @openovai/ovai, for a machine with
Node.js 24 or newer: npx @openovai/ovai@latest takes every
option above except the version, which is named as @openovai/ovai@<version>.
The package is the release, so the installer needs no download and no openovai
command is left behind; the Node.js and Claude Code it pins are fetched at the first
ovai start, with the tools in the requirements below.
@latest is written out because npx reuses a copy it has cached for a name given
with no version. npm refuses it on any system but Linux and macOS.
ovai — the instance's command
<instance>/bin/ovai, one per instance. Bare ovai prints the help.
| command | what |
|---|---|
ovai help | the usage; --help and -h the same |
ovai status | running at <url> (pid, since), or not running; reads runtime.json and then tries the address, so a record a killed server left behind is found out by asking |
ovai configuration | what this instance is: version (a release, or the 8-character commit of a build), who works here, on which models, the port, how it signs in, whether it has a credential (unchecked), when it was installed, every desk with the model it resolves to, and the runtime row: the pinned Node.js and Claude Code and the directory they live in; last, under claude variables, what the sessions get of each Claude Code variable, read from claude-variables as the server last started, never from the shell it is typed in |
ovai start | starts the server in the background and prints a one-time link to the page at http://127.0.0.1:<port>, good once and for ten minutes, and, under Claude Code variables (values masked):, one line per Claude Code variable sorted by name: (removed), (overridden: <value>), (overridden) or (passed on) for one found in the shell, and the value alone for one ovai sets that the shell had not; a taken port is named with the process holding it; a server already running is reported and left alone. The server and every seat it starts drop what Claude Code put on the session it was typed in — its id, pid, entry point, effort, messaging socket and token — and the shell's PWD and OLDPWD |
ovai url | a new one-time link to the running page, for another browser; it replaces the link before it, so at most one is ever pending; not running when there is no server |
ovai stop | parks the room — every Worker is closed and the Leader writes its desk and stops, with park.timeout as the deadline — then closes the server; it waits for the sessions and nothing else, and anything else a seat left running is named with one line that ends all of them; not running is not a failure. kill or kill -HUP of the server's pid reach the same handler |
ovai restart | stop, then start; prints the new address |
ovai plugin <name> | writes plugins/<name>.mjs from the scaffold; refuses a name a tool cannot have, a built-in name, and a name whose file is already there |
ovai login | under --auth login, opens a browser once and keeps the credential inside the instance; refuses under inherit and says so |
ovai claude | open Claude Code on this instance, to reach its own commands: a real Claude Code session on the instance's own configuration, at your terminal, so /mcp, /plugin and whatever Claude Code gains next write this instance's account rather than yours. Not a seat: no desk, no name in the room, no way to speak to anybody here, and every question it has it asks you. Quit it when you are done; a seat reads its configuration when it starts, so ovai restart is what makes a change live for the people working here |
ovai update | takes the latest GitHub release when it is newer: bin/ and lib/ replaced whole, then finished by the version it brought in, with bin/ovai update --finish from the new payload — which fetches the new runtime, takes away what the new version retires, seeds once a file a fresh install would seed and this instance has not got, wires the hooks and turns attribution off — everything of yours left alone. A release is taken only signed: its signature checked against the keys in the instance's own lib/RELEASE_KEYS, its package against SHA256SUMS, and the lib/VERSION inside against the tag, before anything is replaced; a release without them is refused, and GitHub's own archive of a tag is never taken. A rule a fresh instance starts with and this one lacks is not added: the update prints one line, and the server asks for each such rule on the Leader's panel at every start, with what ovai recommends and Ignore (remembered in .claude/ignored-defaults.json). On the version the instance is already on it runs the same finish. Refuses for a session — a process running the code the update replaces — naming the kill line for each, and goes on past anything else a seat left running; refuses to go backwards without --downgrade. --from a directory that is a git checkout takes a build: only on main with nothing git status --porcelain lists, else refused in one line that says which; the instance is then on the first 8 characters of that commit's hash, the version ovai configuration, the page and the server's health show. A build has no order: a release is always taken onto it, another commit always, and the same commit replaces nothing and runs the finish. A directory that is not a checkout, an unpacked release, is taken as a release, with no signature check. Only an http or https address is fetched; anything else is a directory, and one that does not exist, cannot be read or is not a directory is named with the reason. Going back with --downgrade to a version before 0.20.0 says that the hooks in .claude/settings.json name lib/hooks/run.sh, which that version does not have, and must be rewired by hand |
| exit | when |
|---|---|
0 | done |
1 | the server, a plugin or a release said no: the message, and no usage under it |
2 | the command line or the instance: an unknown command, an argument too many, a directory that is not an instance; the message with the usage under it |
3 | ovai status, ovai url: not running |
The instance directory
This, and nothing else ever lands in it.
| path | whose | what |
|---|---|---|
bin/ovai | the product | the one command |
lib/ | the product | everything else the release ships, its version in lib/VERSION and the runtimes it pins in lib/RUNTIME; replaced whole on update |
openovai.json | the instance | its description of itself; the keys |
instructions.json | the instance | the settings document every session is started with: the instruction files above the instance it is not to read; written at every start |
runtime.json | the instance | the running server: url, pid, since |
runtime.log | the instance | what the server said, one row per line, every run appended; the columns |
page-session, page-link | the instance | the page's two keys: the one a signed-in browser's cookie carries, made at the first start and kept, and the pending one-time link, deleted when it is used; delete page-session to sign every browser out. Refused to the Read tool and the tools that write files in a new instance, and to the file view at any depth |
introduced | the instance | the version the introduction was last shown for on the Leader's panel; delete it to see the introduction again at the next start |
claude-variables | the instance | what the sessions get of the shell's Claude Code variables, values masked; written at each start |
plugins/ | the instance | tools it serves itself, one file each; yours, kept across updates |
knowledge/ | the instance | what the workspace knows: one Markdown note per topic, written by every seat, and one directory, files/, which index and validate pass over; the note format |
customization/ | the instance | what you add to the personas: common.md, leader.md, worker.md; never touched |
desks/<Name>/ | the people | one directory per person: STATE.md, persona.md, conversation.json, MODEL when one was named, mcp.json for the moment a session starts (it hands the session its secret, and is removed when the session first uses it), and whatever that person keeps there; on the Leader's desk also STATUS.md, its status, written by the Leader with the file tools, and status.json, what the panel shows of it: the text and the time of the last confirm_status, and how many things have reached the Leader since; STATE-<time>.md, a whole copy of the desk file saved each time it grows past the desk limit; the listing is the roster |
archive/ | the people | retired desks, moved whole: <day>-<Name>-<slug of the final title>/; appears at the first retire |
reference/ | your material | kept to look at: documents, sources, clones for analysis; never worked on |
projects/ | your material | worked on; a reference that needs edits is cloned fresh here, never moved |
temp/ | your material | scratch, deletable by anyone at any time |
.claude/ | Claude Code | the project settings for the instance: settings.json with the rules, allowed.md with one line per rule granted from a permission popup, ignored-defaults.json with the new default rules you pressed Ignore on |
.local/ | Claude Code | its config dir for the instance: account, transcripts, memory (off) |
A fresh instance directory is exactly that list without runtime.json,
runtime.log, the page's keys, introduced,
claude-variables and archive/, which appear at the first start and the
first retire. The directory is open to its owner alone, and so are the files that hold its
address and keys.
openovai.json
Read when the server starts. The customization page says what each one does; the defaults page has every value that is not a key.
| key | default |
|---|---|
user, leader, models.leader, models.worker, port, auth | written by the installer; no default |
schema, createdAt | written by the installer |
quota | { "5h": [90, 95], "7d": [97, 99] } — percent |
idle | { "fyi": [10, 50], "force": 55, "all": 1 } — minutes |
context | { "warning": 200000, "step": 20000, "error": 300000 } — tokens; step is the Leader's own, a Worker's size reaching the Leader at the warning and the error only |
park | { "deadline": 1800, "timeout": 20, "margin": 15 } — seconds |
typing | { "quiet": 10, "cap": 30 } — seconds; the typing hold |
desk | { "limit": 131072 } — bytes; a desk file past it is saved whole beside itself and its seat told once |
The typing hold
A seat waits for you while you type to it. When something arrives for a seat — a Worker's message, a server event — while you are typing in that seat's box, the seat does not start on it straight away: it waits until you send your line or stop typing, so what you were about to say goes in with it instead of landing behind a turn spent on something else.
What is held, and what is not:
- Only a turn made of Worker messages and server events, and only before it begins. A turn already under way runs on: the server cannot pause a session between its own tool calls.
- Your own line is never held. Pressing Enter takes whatever is waiting in with it, as one turn, in the order it all arrived.
- Only the seat whose box you are typing in. Every other seat carries on as before.
- A keystroke counts, text sitting in the box does not: a box you walked away from with words still in it holds nothing once the quiet has run out.
Two settings, under typing in openovai.json, both in seconds:
"typing": { "quiet": 10, "cap": 30 }
| key | default | what |
|---|---|---|
typing.quiet | 10 | how long after your last keystroke you still count as typing. The page tells the server about a keystroke at most once every two seconds, so the quiet is counted from the last keystroke the server heard, which can be up to two seconds before your real last one. A quiet under two seconds therefore holds only in patches |
typing.cap | 30 | the longest a waiting turn is held, counted from when it started waiting (the moment it would otherwise have begun), however long you keep typing. Once it goes in and its turn ends, the next thing that arrives while you are still typing can be held again, for a fresh cap |
0 in either one turns the hold off. A value that is not a number, or is below 0,
is taken as the default. The server reads openovai.json when it starts: a change
takes effect at ovai restart.
A timeline with the defaults, on the Leader's panel:
- Typing, a message arrives, Enter. You start typing at 14:00:00. At 14:00:04 a Worker's message arrives and the Leader, idle, does not start on it. At 14:00:09 you press Enter: your line and the message go in together as one turn, the message first because it arrived first.
- Typing stops without sending. You type from 14:00:00 to 14:00:03 and stop, the words left in the box. At 14:00:04 a Worker's message arrives and is held. Ten seconds after the last keystroke the server heard, at about 14:00:13, it goes in on its own.
- Typing past the cap. You type without a pause from 14:00:00. At 14:00:04 a Worker's message arrives and is held. At 14:00:34 the cap is reached and it goes in while you are still typing. When you send your line it reaches that turn the way any line of yours does: at the turn's next tool call, or after the turn if none is left.
What runtime.log shows: one row when a hold starts, and the usual
wrote row when it ends, with how long it held and what ended it —
user (your line), quiet or cap:
held <seat> - typing, 1 waiting wrote <seat> - queue x2 (#41..#42: message, user), 0 waiting, after typing held 5.0s (user)
The sixteen tools
Served over POST /mcp/<secret>, one secret per process, and named
mcp__openovai__<tool> to the session. 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.
| tool | who | arguments | refused when |
|---|---|---|---|
message | everyone | to (who), text (what); both required. The Leader's alone: checkpoint (a whole number, at least 1), on an order to a Worker — after that many tool calls the server tells the Worker to report where it stands and carry on, and tells the Leader; it replaces a checkpoint still pending; urgent (boolean) — the message reaches a Worker mid-turn, at its next tool call, rather than after the turn | the other seat has no process; a seat held by the quota gate has it and reads it at the reset; a checkpoint or urgent from a Worker, or a checkpoint that is not a whole number of at least 1 |
room | everyone | none | — |
index | everyone | tags (left out: the header — how many notes, when the directory last changed, every tag with its count, and any file it could not read; given: the notes carrying those tags, the ones carrying more of what was asked first) | —; a file it cannot read is named in the header rather than failing the call |
validate | everyone | none | —; every problem is one line of the answer, naming the rule it breaks |
write_desk | everyone | title (one line, up to 120 characters), status (one line, up to 80, no |); both required; writes the header line alone, and the body below it is edited in place | an empty title or status, a newline in either, a | in status, any other key |
done | Worker | note (one line, up to 200 characters, optional); the Leader is told with a done event and decides whether the Worker stops; it ends nothing, and the report still goes as a message | a note of more than one line or 200 characters; the Leader calling it |
confirm_status | Leader | none; reads desks/<Leader>/STATUS.md as it stands at the call and shows it on the Leader's panel, stamped with the time of the call, until the next call; an edit not confirmed shows nothing | no STATUS.md, or an empty one; a Worker calling it |
restart_session | Leader | none; ends the Leader's own process and starts a successor on its desk | the desk was not written since the event that asked, or since the turn began; a Worker calling it |
stop_session | Leader | none; ends the Leader's own process | the same |
stop_worker | Leader | name (required), interrupt (boolean, default false: cut the turn the Worker is on first), deadline (seconds, at least 5; default 5 minutes after an interrupt, park.deadline otherwise). The Worker is told its session is closing, writes its desk, and goes when that turn is over; with no desk by the deadline, it goes with its desk as last written. Answers at once; the outcome comes as a stopped event | a name that is not one; the Leader's own name; a name with no desk; a Worker not running or already ending; while the room is parking; while the server is stopping; a deadline that is not a whole number of at least 5 |
restart_worker | Leader | as stop_worker; then a successor starts on the desk with whatever was queued for the Worker | as stop_worker |
park | Leader | interrupt (boolean, default false), deadline (seconds, at least 5; default park.deadline); every running Worker is closed | a Worker calling it |
hire | Leader | name (somebody who has a desk and stopped; left out, the roster names a new Worker), model (model or model/effort, such as opus or claude-opus-5/low; when not the usual one; on a desk a Worker had, it runs on that model and the model is kept for the desk from then on, and without one the desk's own stands) | the quota gate holds hires; the Leader's own name; a name whose directory is under desks/ without a desk file in it |
retire | Leader | name (required): a Worker that has stopped | a name that is not one; the Leader's own; a name with no desk; a Worker still running; the server stopping |
permission | Leader | rule (as Claude Code's reference spells it: Bash(word:*), Bash(the whole command), Edit(/dir/**), Read(/dir/**), Edit(/dir/file) or Read(/dir/file) for one file, Read(//dir/**), WebFetch(domain:host), WebSearch, mcp__server__tool, Agent(Name), or a bare tool name), why (what the User reads beside it); with neither, answers what the instance holds and what is pending; a rule already held comes on a popup that says where it stands now, and the press moves it | a rule Claude Code would not read; a rule already asked on the panel; no why; moving one of the rules that keep the file tools away from the instance's own settings, login and page keys |
remove_rule | Leader | rule (exactly as the settings hold it), why (what the User reads beside it); a popup with Remove and Keep, the list holding it and its line in .claude/allowed.md | a rule in none of the lists; a rule already asked on the panel; no why; one of the rules that keep the file tools away from the instance's own settings, login and page keys |
The roster hire names from is a fixed list of thirty first names — Paul,
Jane, Jack, Pete, Anna, Mark, Lucy, Tom, Eva, Sam, Nora, Ben, Mia, Leo, Zoe, Max, Ivy, Finn,
Ada, Noah, Ella, Owen, Ruby, Hugo, Iris, Otto, Lena, Axel, Nina, Theo — and the next
name is the least recently used free one: never used first, in that order, then the one whose
holder left longest ago, read from the desk file under archive/; a name is taken
while desks/<Name>/ exists. With nobody in the pool free, Dev
and three digits, and the hire's answer says so, with how many of the desks are stopped
Workers to retire once nothing open names them.
Server events
A frame the server sets itself, <server-event type="…">, queued like a
turn and ahead of the rest when it has to be. Nothing inside a message can pose as one. There
are nineteen.
| type | attributes | to | what the session does |
|---|---|---|---|
context | stage (warning | error), the size reached, and all three sizes | the Leader, when its own conversation as sent passed the warning size, and again at every step above it; measured at every request of a turn | below the error size: carry on, keep the desk current, plan its own restart. From the error size: write_desk, restart_session. Nothing is enforced at any size |
context | who, stage, the size reached, the error size | the Leader, about a Worker, at the warning size and at the error size only; the Worker is not told | at the error size, restart_worker at a moment that suits the Worker's work |
restarted | — | the successor, as its first frame, behind whatever was waiting for the seat and in the same turn as it | goes on from what the desk says to do next, and never redoes what it says is done; a successor with nothing in flight answers <noop/>, and the panel shows nothing |
quota-low | stage (warning | critical), window (5h | 7d | 7d-fable), resets; model="fable" for fable's own window | every running seat at the first stage, seats on that model only for fable's window; the Leader at the second | a Worker keeps its desk current and carries on; the Leader tells the User, stops the Workers whose work can wait, and stays. At the second stage every Worker on the window is interrupted and closed instead, and the Leader's turns are held |
quota-reset | window; model when it is one model's own | the Leader, when a window that reached a stage has reset; a Leader not running is started by it. A park forgets the resets to come, and a server restart before the reset loses them | brings back the Workers that stopped on the window, with hire on their desks |
idle | who, minutes; at 50 also the cold-in time and the context | the Leader, about a silent Worker, at 10 and 50 minutes | read its desk, message it if a word moves it, or stop_worker it; at 55 the server closes it |
idle | stage="critical" | the Leader at 55 minutes | write_desk, stop_session; ended at 60 either way, whatever else it is told in between |
all-idle | workers (how many); the body names them | the Leader, off a turn, when every running Worker has been idle idle.all minutes with nothing waiting for it; once, and again only after a Worker has taken a turn. Not with no Worker running; a Worker on a permission popup is on a turn | usually a report or a done it has not acted on: acts on it |
park | interrupted="true" when the turn was stopped for it | the Leader, from ovai stop or a signal | write_desk, stop_session, by the deadline |
closing | why (stop | restart | idle | quota | park); for a spent window also window and resets; interrupted="true" when the turn was cut for it; deadline in seconds when it has one | a Worker, from stop_worker, restart_worker, park, at 55 minutes idle, and when its window is spent. Read at the start of a turn of its own, never into one under way | writes its desk with write_desk as the last thing in that turn; the session ends when the turn is over. With no desk by the deadline — counted from the turn that read it, and never cutting a turn — it ends with the desk as last written: 5 minutes for idle and a spent window, the park's own for a park. A later message does not take a close back; a word to a Worker closed for idling does. A close whose turn the service refuses ends at once |
done | who; the note, when there is one, is the body | the Leader, when a Worker calls done | gives the Worker the next thing, or stop_worker when nothing more is wanted |
unanswered | none; the body names the time of the User's line | the Leader, at once, when it read what the User typed and ended the turn with no words on its panel: nothing, only <noop/>, or only calls. Once per such turn, and never after a turn that read one, a turn that failed or one the page stopped | answers that line in words |
died | who; the service's words are the body | the Leader, when the service refused a turn of a Worker that has worked before; once, and not again until the Worker has answered a turn | the Worker keeps running; a failure that passes goes away at the next message, one that stays is the User's to hear |
admin-closed | ended, changed (how many of the configuration files the session watched changed); the body names those files and what moved in them — marketplaces, plugins, skills, MCP servers, connectors, each permission rule (masked), and any other settings key by name — and nothing of what the files hold | the Leader, within one 5-second tick of an ovai claude session ending while the server runs, or at the server's next start when it was not running | told, not asked; nothing waits on it. Reads what matters before handing work out: a permission rule that moved changes what a Worker may do, at its next call; the rest reaches the seats at ovai restart. The telling is the server's, never the admin session's |
checkpoint | calls; to the Leader also who | a Worker, once it has made the number of tool calls the Leader set on its order with checkpoint; the Leader at the same moment | the Worker reports to the Leader where the work stands, done or not, what is left and how many more calls it expects, then carries on unless told otherwise; the Leader is told, not asked |
undelivered | to, and the words as they were sent | the sender of a message whose addressee's session ended before reading it; a Leader not running is started by it | says it again, to somebody else, or later |
status-check | none; the body names the User and the Leader's STATUS.md | the Leader, when the User clicks the stamp of its status; not before its first confirm_status, and a second click while one waits asks nothing more | edits STATUS.md if it is out of date, calls confirm_status with or without an edit, and answers <noop/> |
user-typed | who | the Leader, when the User types on a Worker's panel | told, not asked: acts on it if it needs the Leader, does not answer for the Worker |
overheard | from, to | the Leader, when one Worker messages another | heard, not asked |
stopped | who, why: stop (stop_worker), stop-deadline (the same, with no desk in time), restart (restart_worker; the successor is running), idle, idle-forced, quota, first-turn-failed (the service refused the Worker's very first turn; its words are the body) | the Leader, when a Worker has gone | notes it; hire brings it back when the work is still wanted. After first-turn-failed, tells the User what the service said before hiring on that desk again |
permission | who, minutes, and the call as the body; for a settled rule decision and who, and the rule | the Leader, when a Worker's permission popup timed out — minutes is 10, or 2 when one of the Worker's subagents made the call; also when a rule the Leader asked for was settled, or a removal it asked for was pressed: decision remove or keep | tells the User what the Worker asked for, or gives the work to somebody else |
A change to customization/ or to knowledge/common.md reaches the
sessions started after it and no others: there is no frame that hands a running session a
change to its standing instructions. When something has to reach a session that is running,
tell it in words, or restart it: restart_worker for a Worker.
The seeded permission rules
.claude/settings.json, written once at install. Every path rule starts with
/, anchored at the instance directory. An update adds no rule; it wires the subagent hook
into a file that lacks it, takes out the shell hook versions before 0.26.0 ran, and turns commit attribution off in one that says nothing about
it.
| list | rule | what |
|---|---|---|
| allow | mcp__openovai | the tool server: the sixteen tools and every plugin |
| allow | AskUserQuestion | a session asking you a question: the question is itself a popup on its panel, answered there |
| allow | Read(/**) | reading any file |
| allow | Edit(/reference/**), Edit(/projects/**), Edit(/temp/**) | writes under the three trees that are yours; Edit(…) governs every tool that writes a file, Write included — a Write(…) rule matches nothing |
| allow | Edit(/knowledge/**) | every seat writes what the workspace knows, where it found it out |
| allow | Edit(/desks/<Leader>/**) | the Leader's desk directory; every Worker hired gets the same pair for theirs, withdrawn at retire |
| allow | one Bash(…:*) each for git, mkdir, cd, cp, mv, rm, ls, cat, tar, diff, cmp, sha256sum, shasum, grep, find, sed, awk, head, tail, wc, echo, chmod, touch, curl, npm | a seat's ordinary shell commands, matched by the command from its first character; no interpreter (node, bash, sh): a script is the classifier's to judge in auto mode, or the User's on a popup in Manual |
| ask | Edit(/customization/**) | a session that would rewrite what every session here is told stops for your press — every seat, the Leader with the rest, since one settings file serves them all |
| ask | Edit(/knowledge/common.md) | narrower than the allow above: the one note handed to every session at its start, while the rest of knowledge/ stays open to write |
| deny | the instance's own account files | under .local/ |
| deny | git push, sudo, ssh | a push is yours; a refusal wins over the git rule that would let it through, in either mode and inside a chain of commands too |
Also seeded: attribution with an empty commit and pr
and sessionUrl false, so a commit says what its message says; and the
hook lib/hooks/subagent.mjs when a subagent starts, which hands it
customization/common.md in the frame a seat reads it in, and nothing when there
is none. It is named as sh "${CLAUDE_PROJECT_DIR}/lib/hooks/run.sh" subagent,
which runs it on the Node.js the instance's version pins. No hook runs before a shell
command. The instance's Claude Code home turns the built-in memory off.
runtime.log
Every row has the same four columns, single spaces, then the text. No request is ever logged.
Every row is masked as it is written: a key, a token, a password in a URL, an
Authorization header, a --token-style flag or a
SECRET=-style assignment is written as ***. The rows of every panel
are masked the same way, before they are kept.
<moment> <event> <seat> <id> <text…> 2026-09-20T02:15:33.123+02:00 started - - serving /home/ana/workspace at http://127.0.0.1:7357 pid 41200 host box user ana version 0.21.0 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 2026-09-20T03:40:00.117+02:00 stopped - - SIGTERM
| column | what |
|---|---|
| moment | ISO 8601 to the millisecond with the offset of the machine's zone — the clock the page and the desks keep |
| event | one word, and the event is the level. The whole set: started, error, plugins, parking, parked, hired, retired, called, tool, failed, checkpoint, died, closed, queued, wrote, unwritten, self-started, unpaired, interrupt, unread, dropped, redelivered, asked, answered, mode, blocked, restart, held, released, quota, untold, stop, stopped, leftovers, usage, unopened. A row with an event outside the set is a mistake in the code and is refused there |
| seat | the session the row is about; - for the server itself |
| id | the tool-use id on called, tool and failed, else -; grep toolu_… runtime.log is a call's whole life, and the three rows name the tool in full, as Claude Code does |
The pid, the host, the user and the version are on the started row and on no
other. A session's process gone is one stopped row after the last row of its
calls, saying what it ended as — stop, restart,
idle, quota, park, idle-forced,
stop-deadline, park-deadline, first-turn-failed
— or, for one that ended on its own, how: exit 1, SIGKILL. The
server's own going out is the last row of its run, stopped - - SIGTERM.
A day can be read back from the file. hired says a Worker was started on a desk,
what it runs on, and whether the desk was new or one it had. retired says a desk
was filed away, the model that round ran on, and where the desk went. restart
says how full the session that restarted was, in tokens, so "it filled up" is a number rather
than a story. quota says which usage window crossed a threshold, the percentage
it stood at, when it resets, and the model when the window is one model's own.
held says a turn waits, for a quota window or while you type on the seat's
panel (the typing hold), and released that it was let go.
checkpoint says a Worker reached the number of calls the Leader set.
died says the service refused a turn, in the service's own words, and
closed that a close reached its deadline with no desk written, and when the desk
last was.
leftovers says what a seat left running was ended with its session.
The plugin contract
plugins/<name>.mjs; the name is the tool's. Read once, when the server starts.
| export | what |
|---|---|
description | a string: what it does and what it refuses; a session reads it before calling |
inputSchema | a JSON schema object for the call's arguments |
run(args, { seat, role, root, config }) | the work, sync or async; seat from the secret, role Leader | Worker, root the instance's absolute path, config openovai.json. Answers { text } or { refused }; anything else, or a throw, reaches the caller as a refusal naming the file |
| not served when |
|---|
| the name is not a letter followed by letters, digits and hyphens, up to 32 in all |
| the name is one of the sixteen the server serves itself |
| the file does not load |
| the file loads and lacks one of the three exports |
Each is one plugins row in runtime.log naming the file and the reason. The plugins page walks through one.
Requirements
| what | for |
|---|---|
Linux or macOS, x86_64 or arm64. macOS is built for and checked with stand-ins on Linux, but ovai has not yet been run on a Mac. WSL 2 has not been tried, and native Windows is not supported; to try WSL 2, keep the instance in the Linux file system and off /mnt/c | running an instance: the page is opened in your browser on 127.0.0.1 |
sh, curl or wget, tar, sha256sum or shasum, uname | the openovai command and the release installer: fetching the release, and the Node.js and Claude Code it pins, and checking them; an instance made with npx @openovai/ovai needs them too, at its first ovai start, where bin/ovai fetches those two through lib/runtime.sh |
| Node.js 24 or newer | only the npx @openovai/ovai way in, which runs the installer on it; an instance never runs on it, and the way in still needs the row above at the first start |
| an Anthropic account | signing the instance in, with --auth login or a token under --auth inherit |
Node.js, npm and Claude Code are not requirements: each release pins the exact versions it was
tested with, in lib/RUNTIME, and the installer fetches them under
~/.local/share/openovai/, per user, shared between instances, used by absolute
path. This release pins Node.js 24.21.0 and Claude Code 2.1.314. Nothing global, no
sudo.