OpenOv AI

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 https://raw.githubusercontent.com/nilcaream/openovai/main/openovai | sh — 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:

formwhat
openovaithe 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] --root … --user … --leader … --leader-model … --worker-model … --port … --auth …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 --root … 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.

commandwhat
ovai helpthe usage; --help and -h the same
ovai statusrunning 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 configurationwhat 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 startstarts 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 urla 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 stopparks 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 restartstop, 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 loginunder --auth login, opens a browser once and keeps the credential inside the instance; refuses under inherit and says so
ovai claudeopen 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 [--downgrade] [--from <url or directory>]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
exitwhen
0done
1the server, a plugin or a release said no: the message, and no usage under it
2the 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
3ovai status, ovai url: not running

The instance directory

This, and nothing else ever lands in it.

pathwhosewhat
bin/ovaithe productthe one command
lib/the producteverything else the release ships, its version in lib/VERSION and the runtimes it pins in lib/RUNTIME; replaced whole on update
openovai.jsonthe instanceits description of itself; the keys
instructions.jsonthe instancethe settings document every session is started with: the instruction files above the instance it is not to read; written at every start
runtime.jsonthe instancethe running server: url, pid, since
runtime.logthe instancewhat the server said, one row per line, every run appended; the columns
page-session, page-linkthe instancethe 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
introducedthe instancethe version the introduction was last shown for on the Leader's panel; delete it to see the introduction again at the next start
claude-variablesthe instancewhat the sessions get of the shell's Claude Code variables, values masked; written at each start
plugins/the instancetools it serves itself, one file each; yours, kept across updates
knowledge/the instancewhat 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 instancewhat you add to the personas: common.md, leader.md, worker.md; never touched
desks/<Name>/the peopleone 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 peopleretired desks, moved whole: <day>-<Name>-<slug of the final title>/; appears at the first retire
reference/your materialkept to look at: documents, sources, clones for analysis; never worked on
projects/your materialworked on; a reference that needs edits is cloned fresh here, never moved
temp/your materialscratch, deletable by anyone at any time
.claude/Claude Codethe 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 Codeits 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.

keydefault
user, leader, models.leader, models.worker, port, authwritten by the installer; no default
schema, createdAtwritten 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:

Two settings, under typing in openovai.json, both in seconds:

"typing": { "quiet": 10, "cap": 30 }
keydefaultwhat
typing.quiet10how 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.cap30the 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:

  1. 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.
  2. 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.
  3. 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.

toolwhoargumentsrefused when
messageeveryoneto (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 turnthe 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
roomeveryonenone—
indexeveryonetags (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
validateeveryonenone—; every problem is one line of the answer, naming the rule it breaks
write_deskeveryonetitle (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 placean empty title or status, a newline in either, a | in status, any other key
doneWorkernote (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 messagea note of more than one line or 200 characters; the Leader calling it
confirm_statusLeadernone; 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 nothingno STATUS.md, or an empty one; a Worker calling it
restart_sessionLeadernone; ends the Leader's own process and starts a successor on its deskthe desk was not written since the event that asked, or since the turn began; a Worker calling it
stop_sessionLeadernone; ends the Leader's own processthe same
stop_workerLeadername (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 eventa 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_workerLeaderas stop_worker; then a successor starts on the desk with whatever was queued for the Workeras stop_worker
parkLeaderinterrupt (boolean, default false), deadline (seconds, at least 5; default park.deadline); every running Worker is closeda Worker calling it
hireLeadername (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
retireLeadername (required): a Worker that has stoppeda name that is not one; the Leader's own; a name with no desk; a Worker still running; the server stopping
permissionLeaderrule (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 ita 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_ruleLeaderrule (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.mda 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.

typeattributestowhat the session does
contextstage (warning | error), the size reached, and all three sizesthe Leader, when its own conversation as sent passed the warning size, and again at every step above it; measured at every request of a turnbelow 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
contextwho, stage, the size reached, the error sizethe Leader, about a Worker, at the warning size and at the error size only; the Worker is not toldat 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 itgoes 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-lowstage (warning | critical), window (5h | 7d | 7d-fable), resets; model="fable" for fable's own windowevery running seat at the first stage, seats on that model only for fable's window; the Leader at the seconda 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-resetwindow; model when it is one model's ownthe 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 thembrings back the Workers that stopped on the window, with hire on their desks
idlewho, minutes; at 50 also the cold-in time and the contextthe Leader, about a silent Worker, at 10 and 50 minutesread its desk, message it if a word moves it, or stop_worker it; at 55 the server closes it
idlestage="critical"the Leader at 55 minuteswrite_desk, stop_session; ended at 60 either way, whatever else it is told in between
all-idleworkers (how many); the body names themthe 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 turnusually a report or a done it has not acted on: acts on it
parkinterrupted="true" when the turn was stopped for itthe Leader, from ovai stop or a signalwrite_desk, stop_session, by the deadline
closingwhy (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 onea 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 waywrites 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
donewho; the note, when there is one, is the bodythe Leader, when a Worker calls donegives the Worker the next thing, or stop_worker when nothing more is wanted
unanswerednone; the body names the time of the User's linethe 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 stoppedanswers that line in words
diedwho; the service's words are the bodythe Leader, when the service refused a turn of a Worker that has worked before; once, and not again until the Worker has answered a turnthe Worker keeps running; a failure that passes goes away at the next message, one that stays is the User's to hear
admin-closedended, 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 holdthe 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 runningtold, 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
checkpointcalls; to the Leader also whoa Worker, once it has made the number of tool calls the Leader set on its order with checkpoint; the Leader at the same momentthe 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
undeliveredto, and the words as they were sentthe sender of a message whose addressee's session ended before reading it; a Leader not running is started by itsays it again, to somebody else, or later
status-checknone; the body names the User and the Leader's STATUS.mdthe 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 moreedits STATUS.md if it is out of date, calls confirm_status with or without an edit, and answers <noop/>
user-typedwhothe Leader, when the User types on a Worker's paneltold, not asked: acts on it if it needs the Leader, does not answer for the Worker
overheardfrom, tothe Leader, when one Worker messages anotherheard, not asked
stoppedwho, 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 gonenotes 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
permissionwho, minutes, and the call as the body; for a settled rule decision and who, and the rulethe 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 keeptells 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.

listrulewhat
allowmcp__openovaithe tool server: the sixteen tools and every plugin
allowAskUserQuestiona session asking you a question: the question is itself a popup on its panel, answered there
allowRead(/**)reading any file
allowEdit(/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
allowEdit(/knowledge/**)every seat writes what the workspace knows, where it found it out
allowEdit(/desks/<Leader>/**)the Leader's desk directory; every Worker hired gets the same pair for theirs, withdrawn at retire
allowone 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, npma 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
askEdit(/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
askEdit(/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
denythe instance's own account filesunder .local/
denygit push, sudo, ssha 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
columnwhat
momentISO 8601 to the millisecond with the offset of the machine's zone — the clock the page and the desks keep
eventone 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
seatthe session the row is about; - for the server itself
idthe 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.

exportwhat
descriptiona string: what it does and what it refuses; a session reads it before calling
inputSchemaa 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

whatfor
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/crunning an instance: the page is opened in your browser on 127.0.0.1
sh, curl or wget, tar, sha256sum or shasum, unamethe 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 neweronly 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 accountsigning 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.