Login
Documentation
Login
You are reviewing a protocol specification for the first time. Read only the text below. Do not use any tools, read any files, or run anything.

Context: OCX extends Fossil, a version-control system whose repository also holds forum threads and tickets. Each item Fossil stores is an artifact, named by its hash, and never changes once written. Every person and program acts as a Fossil user.

Your task: find every place where the protocol below contradicts itself, leaves a behavior that two careful implementers could build differently, or asks for something that cannot be implemented as written. For each, quote the words at fault, say what goes wrong in a concrete case, and suggest the smallest fix. Do not report matters of style. If you find nothing in an area, say so. End with the three findings you consider most serious, in order.

# The protocol

# OCX protocol v1

OCX extends Fossil, so people and programs can ask a node to run work and read
what came of it. This document is the protocol. [GLOSSARY.md](../GLOSSARY.md)
defines its words and points here. Each section was tested, a few at a time,
on readers who knew nothing else ([docs/battery/defs](battery/defs/README.md)).
Each ends with the values in [README.md](../README.md) that justify it.
[Failures, worked through](protocol-v1-failures.md) shows what the rules make
of crashes, races and refusals, case by case.

## <a id="fossil"></a>1. Fossil, the medium

Everything that must last is a Fossil artifact: posts, check-ins, tags, ticket
changes. These are the evidence of what happened; a host's own log is for
diagnosing problems, and may be lost. OCX treats Fossil as a network whose
every message lasts:
- **Sending is writing an artifact.** Sync carries it to every clone; nothing
  is lost on the way.
- **The hook is a signal; the repository is the truth.** Fossil runs the
  after-receive hook from its backoffice after artifacts arrive. A write made
  on the repository itself starts no backoffice, so whatever writes there
  runs it afterwards. A node also reads its repository again when it starts,
  and at an interval of its own, for anything no signal told it of.
- **The answer is the acknowledgement, and it lasts.** A request that has its
  answer is never acted on again, and what is still open is read from the
  repository, never kept anywhere else.
- **Nothing is retracted.** Stopping, correcting and replacing are new
  artifacts that point at old ones.
- **Order comes from references and receipts,** such as a reply naming the
  post it answers, never from clocks.

Each repository keeps a receipt for every artifact it receives: the time, the
IP address, and the Fossil user it was received from, by an authenticated sync,
through its web page, or by a write on the repository itself, for which it
records whatever user the writer names. So only a node, its tools and its
host's administrators may write to a node's repository file. A repository does
not copy its receipts to its clones, and a clone records everything it pulls
as received from its own user, which holds setup there (Fossil 2.27, tried
2026-10-12): only the repository an artifact's writer sent it to knows who sent
it. The user written inside an artifact is whatever its writer's clone said,
and proves nothing by itself. Receipts are numbered as they arrive, and the
artifacts of one receipt are taken in the order they were stored (their record
ids): that is the order this protocol means.

Why: Demonstrability (artifacts are immutable, synced, readable later),
Parsimony (no second store, no queue, no retries), Ax4 (every open request is
found again after any failure).

## <a id="messages"></a>2. OCX messages

An OCX message is a JSON block in a forum post, or in the text a ticket change
adds to its ticket's comment, fenced in Markdown: write it with a line
```` ```json ````, the JSON, and a line ```` ``` ````; a reader also takes a
bare ```` ``` ```` fence. Other text may come before or after it, and nodes
ignore it; keep it short. An artifact holds one OCX message: a reader takes the
first fenced block whose JSON has an `ocx` key with a version it knows, and
ignores any other. Every message has `"ocx": 1`, a `kind`, and `node`: the one
node it is for, named by its node user ([section 10](#nodes)). There are four
kinds:
- **Requests,** `run` and `stop`. A node acts on one only when it is for that
  node and the user it was received from holds admin or setup. It answers any
  other run for it `ended`, `refused`, so it decides once.
- **Reports,** `started` and `ended`. Only the node a run is for writes them,
  as its node user, and a report counts only if it was received from that
  user.

Every message but a `run` names the run it is about in its field `run`: the
run's Fossil hash, in full, in hex. It is written where that run is, as a reply
in its thread or as a change to its ticket; one written anywhere else is
ignored. Several runs may share a thread or a ticket; each is answered on its
own. A message never changes: an edit of one is ignored, so to change a
request, stop it and send another.

Why: Clarity (one envelope, four kinds), Loose Coupling (anything that can
write a post or a ticket change can send one), Ax1 (only those who hold admin
or setup can ask a node for anything).

## <a id="runs"></a>3. Runs

A run is the OCX message of kind `run`: a request that a node run one program.
Its fields, besides `ocx`, `kind` and `node`, are these, and those that
[runners](#runners), [subscriptions](#subscriptions) and [limits](#limits) add:

```json
{"ocx": 1, "kind": "run", "node": "lab-1",
 "objective": "Build trunk for the board; it passes when the build succeeds.",
 "runner": "command", "command": ["make", "board"], "checkout": "trunk",
 "user": "builder", "session": false, "limits": {"wall": "30m"}}
```

- `objective` (text, required): what the job is for, and what counts as
  passing. Only a run has one.
- `runner` (text, required): how the node runs the program.
- `user` (text, required): the Fossil user the program acts as.
- `checkout` (text): the check-in its program works in ([section 5](#runners)).
- `session` (true or false): how long the program runs.
  - `false`, the default for every runner but `ci`: to completion. It
    starts, does its work and exits, like a build or an on-target test.
  - `true`: as a session. It stays running and is given what its run
    subscribes to, until it exits, is stopped, or reaches its limits.
- `limits` (an object): what the program may use.
- `manual` (true or false, default false): whether the node waits for a person
  to start the run by hand, with OCX's `run` tool, instead of starting it when
  it arrives.

A run missing a required field or one its runner needs, or holding a value of
the wrong type or form, is answered `ended`, `refused`.

Why: Ax1 (the whole boundary is fixed and readable before anything runs),
Clarity (one shape for a build, a CI check and an agent).

## <a id="reports"></a>4. Started and ended

The node's reports on a run, written where the run is.

```json
{"ocx": 1, "kind": "ended", "node": "lab-1", "run": "FULL-HASH-OF-THE-RUN", "outcome": "exited", "exit": 0}
```

A node first prepares a run: it checks its runner, its user and its limits,
checks out its check-in and makes its sandbox. Only then does it write
`started`, and only once that is written does it launch the program.
- `started`: the run's program is launched, or about to be. It names the
  check-in checked out, in full (`checkout`, which a `ci` run has none of),
  and every limit applied (`limits`, [section 7](#limits)). A run gets at most
  one, a session too, however many posts it is given, so its program is
  launched at most once.
- `ended`: the run's program is not running, and never will be again. A run
  gets exactly one, and its `outcome` says why:
  - `refused`: the node did not start it: it may not, or cannot give it what
    it asks, or preparing it failed. `note` says which, in words; no
    `started` was written;
  - `exited`: the program exited by itself; `exit` holds its exit code, a
    whole number. A program its sandbox cannot launch exits non-zero, and
    one killed by signal N, but not by a stop or a limit, has `exit` 128 + N,
    as a shell reports it;
  - `stopped`: a `stop` ended it, or came before it started;
  - `budget`: it went over a limit, which `note` names in words;
  - `lost`: after its `started`, the node went down or lost track of the
    program, which may have run in part, or not at all; the run is never
    launched again ([section 10](#nodes)).

Whoever writes a run's `started` or `ended`, the node or its tools, checks in
the same step that the run has no `ended`, and for `started` no `started`, so
that no other write of either comes between. Of a run's stops, the node acts
only on the first it may act on at all ([section 2](#messages)). Received
before the run's `started`, it ends the run `stopped`, and the run never
starts. Received while it runs, it ends the program, and the run ends
`stopped`, unless it ended otherwise first. Any later `stop`, and any received
after the run's `ended`, does nothing. A run with a `started` and no `ended` is
running; a run with an `ended` is over. So in the node's repository, leaving
out the stops that do nothing, a run's OCX messages come in one of these
orders:
- `run`, `ended` (refused);
- `run`, `stop`, `ended` (stopped before it started);
- `run`, `started`, `ended`;
- `run`, `started`, `stop`, `ended`.

Why: Demonstrability (every request is answered where it was asked, even a
lost one), Ax4 (no run is left open), Ax1 (a reader always knows whether a run
is running, and a program never runs twice for one run).

## <a id="runners"></a>5. Runners

A runner is how a node runs a run's program; the run's `node` says where. A
node offers some runners, and OCX's `status` lists them; a node answers a run
whose runner it does not offer with `ended`, `refused`. Every runner but `ci`
needs the run's `checkout`, and runs its program in a fresh checkout of it: any
name Fossil resolves to a check-in, as `fossil update` resolves it, such as a
branch's name or a hash. It is resolved once, when the node checks the run
out, before its `started`, which names that check-in in full; a check-in
landing on the branch later changes nothing for the run. The runners:
- **`command`** runs the program named in the run's `command` (a list of text:
  the program, then its arguments) to completion: a build, a test, a check of
  one check-in. With `session` true it runs as a session instead, reading its
  deliveries on its standard input ([section 6](#subscriptions)).
- **`ci`** runs it for every check-in. A `ci` run is always a session, so one
  that says `session: false` is refused. It has no `checkout`: for each
  check-in arriving after its `started` on a branch in the run's `checkins`,
  it runs the run's `command` once, to completion, in a fresh checkout of that
  check-in, and records the result as a `ci` [verdict](#verdicts) on it,
  `pass` if the command exits 0, and `fail` if not. Its time limits hold each
  command, and a command that goes over one fails its check-in; the run goes
  on until it is stopped.
- **An agent runner,** such as `claude` or `codex`, runs that agent program,
  usually as a session. Its program, the home files its runs share and its
  environment come from the repository's wiki page `sandbox/NAME`: the latest
  version received from a user holding admin or setup, by receipt order, other
  versions being ignored. The run's `remote` (true or false, default false)
  says whether the session may also be reached through the agent's own remote
  control, as Claude's offers, by whoever holds the agent's own account; what
  is said that way leaves no artifact.

For both `command` and `ci`, `command` is what is run, and the run's
`objective` says in words what counts as passing. So to run something once, use
`command`; to run it for every check-in on some branches, use `ci`. Special
hardware is a node, not a runner: to test each check-in on `dev` on a board
attached to a host, send a `ci` run whose `node` is that host's, such as
`{"runner": "ci", "command": ["make", "board-test"], "checkins": ["dev"], …}`.

Why: Loose Coupling (a runner is replaceable, and hardware is chosen by node),
Parsimony (three kinds of runner cover builds, CI and agents).

## <a id="subscriptions"></a>6. Subscriptions

What a session is given while it runs, set by two fields of its run, both
optional and empty by default:
- `checkins`: branch names. Each check-in that arrives on one of them, unless
  it was received from the run's own user, is given as
  `Check-in HASH on BRANCH by USER`.
- `hears`: Fossil users. Each post one of them makes in the run's thread, or
  each change one of them makes to the run's ticket, is given as
  `Post HASH from USER:` or `Change HASH to TICKET from USER:`, followed by its
  text as written, which may run over several lines. An edit of a post is a
  new post, and is given; a post or change holding an OCX message is not.
  Every session in a thread is given its posts, so to speak to one run alone,
  give it a thread or a ticket of its own.

USER is the user the artifact was received from. A session is first given
`Run HASH: OBJECTIVE`, then each artifact it subscribes to that arrives after
its `started`. The node guarantees each running session these:
- **Whole, once, in order:** each delivery reaches the program whole, at most
  once, one at a time, in the order the node's repository received them.
- **Nothing missed:** whenever the node reads its repository again, it gives
  each running session everything received since the last artifact it gave
  that session. That mark is why nothing is given twice.

How a delivery reaches the program:
- An agent is given each delivery as one turn of its conversation, once its
  last turn has ended.
- Any other program reads its deliveries on its standard input, not from the
  thread: one JSON object to a line, first `{"run": HASH, "objective": TEXT}`,
  then each `{"post": HASH, "from": USER, "text": TEXT}`,
  `{"change": HASH, "ticket": TICKET, "from": USER, "text": TEXT}` or
  `{"checkin": HASH, "branch": BRANCH, "from": USER}`. What it has not read
  waits there.
- `ci` takes its check-ins itself ([section 5](#runners)).

The node keeps which artifact it gave a session last, only while it runs: that
is all its delivery state. If the node loses it while the program still runs,
it ends the program, and the run ends `lost`. Nothing is given to, or kept for,
a run that is not running, and a run that is not a session is given nothing; a
session may read its thread itself for anything older. A run's own posts, such
as its questions, answers and progress, are plain posts in its thread, with no
OCX message.

Why: Ax1 (a session acts only on what its run names, and no session polls),
Parsimony (one field per kind of delivery, one mark of delivery state), Loose
Coupling (any program that reads lines of JSON can be a session).

## <a id="limits"></a>7. Limits

What a node holds a run's program to, in the run's `limits`. Each is optional;
a limit left out takes the node's own default, and the node's `started` report
lists, in a `limits` of the same shape, every limit it applies. A node that
cannot enforce a limit a run asks for answers it `ended`, `refused`.
- **Time and resources:** `wall` (a whole number followed by `s`, `m`, `h` or
  `d`), `cpu` (a whole number of seconds, in total), `memory` (a whole number
  of bytes, or text: one followed by `K`, `M` or `G`, for 1024, 1024² or 1024³
  bytes), `processes` (a whole number, at once). Going over `wall`, `cpu` or
  `memory` ends the run, `budget`; a process beyond `processes` is not
  started, and the program goes on.
- **What it sees:** `network` (true or false), and `read_only` and
  `read_write` (lists of absolute host paths, which may not hold the node's
  repository). Besides these it sees only its workspace (its own clone and its
  checkout), its program, its runner's home files, the host's system files,
  which it may only read, and, with `no_password`, OCX's tools.
- **What it may write in Fossil:**
  - `no_password` (true or false, default false): the program holds no
    password for its user. Its posts, verdicts and check-ins go through OCX's
    tools, which the node runs as its user.
  - `branch` (a branch name): its check-ins go on that branch only.
  - `own_thread` (true or false): it posts only in its run's thread or ticket.

  Only OCX's tools can enforce `branch` and `own_thread`, so setting
  `branch`, or `own_thread` to true, also means `no_password`. A run that
  sets either of those and also sets `no_password` to false asks for both
  and neither, and is refused. This holds for what a run sets; a node's own
  defaults never cause a refusal. `own_thread` false asks for nothing. A tool
  refuses a write these forbid, and the run goes on. Without `no_password`,
  the program's Fossil capabilities are its only limit in Fossil, and
  Fossil's `i` admits any write.

Why: Ax1 (the boundary holds, or the run does not start), Parsimony (no limit
unless asked), Loose Coupling (a node that enforces few limits still runs the
runs that ask for no more).

## <a id="permissions"></a>8. Who may do what

OCX decides by the user an artifact was received from, as the node's
repository records it ([section 1](#fossil)), and by that user's capabilities
when the node decides, once. On a node, only OCX's tools write to the
repository itself, each naming the user it signed in.
- A `run` or a `stop` may be sent by any user holding admin or setup. The node
  acts on one only if it was received from a user holding admin or setup.
- `started` and `ended` are written only by the node, as its node user, and
  count only if received from it.
- A post or ticket change is given to a session only if it was received from a
  user in the run's `hears`; a check-in, only if it was not received from the
  run's own user.
- A verdict counts only if it was received from a user who holds check-in
  (`i`) and who neither sent the check-in nor is the user named in it. No one's
  verdict on their own check-in counts ([section 9](#verdicts)).

Who may read threads, tickets and check-ins is Fossil's own business, by its
capabilities; OCX adds nothing to it.

**Open:** a clone records what it pulls as received from its own user
([section 1](#fossil)), so how a node on a clone, such as a build host's, learns
who wrote what it pulled is not yet settled. Until it is, such a node answers
each run it pulled `ended`, `refused`, acts on no `stop` it pulled, and gives a
session no post or ticket change it pulled; it gives the check-ins it pulled
by the user named in them, unverified. It acts on runs written on its own
host. A reader on a clone counts verdicts as unverified ([section 9](#verdicts)).

Why: Demonstrability (every act is attributed by the repository that received
it), Ax1 (what each user may cause is fixed before it acts, and a receipt that
cannot say who wrote an artifact allows nothing).

## <a id="verdicts"></a>9. Verdicts

A verdict is a tag on a check-in recording a judgement of it, `pass` or `fail`
with a reason and the objective it was judged against: `audited` for a
person's or an agent's, `ci` for a program's. Anyone may write one, with OCX's
`audit` or with `fossil tag add`, and it needs no run; but a verdict on your
own check-in never counts. [Verdicts](protocol-v1-verdicts.md) says how to
write one, and how a reader counts them.

Why: Ax2 (always judged against a named objective), Loose Coupling (a verdict
needs no run, and any program may read one).

## <a id="nodes"></a>10. Nodes

A node is a repository with OCX's hook installed, on the host that runs its
work. It is named by its node user: a Fossil user of its own, which runs name
in their `node`, as which the node writes its reports, and as which a node on
another host syncs. Two hosts must never run as one node user, or both would
act on the same runs. OCX's `status` lists the runners and limits a node
offers, and how often it reads its repository again.

A node learns of an artifact only when the artifact reaches its repository.
On the repository's server, that is as soon as it arrives. A node on any other
host, such as a build host with its own clone, learns of a run only when it
syncs its clone, how often being its own choice; until who wrote what it pulls
is settled ([section 8](#permissions)), it acts only on runs written on its own
host. Its reports reach the server the next time it syncs. Fossil tells the
node of each arrival through its after-receive hook, run from Fossil's
backoffice. A write made on the repository itself starts no backoffice, so
OCX's tools run it after writing, and a run written with OCX's `create` on the
node is acted on at once.

When a node starts, and whenever it reads its repository again, it looks for
runs that name it and have no `ended`:
- a run with a `started` whose program is no longer in its care (running,
  watched for its exit, and given its deliveries) it answers `ended`, `lost`,
  ending the program first if it still runs;
- a run with no `started` it acts on as if it had just arrived: it starts it,
  refuses it, or ends it `stopped` if a `stop` for it has arrived, and a run
  whose `manual` is true goes on waiting for a person.

Why: Ax4 (open runs are found again from the repository), Parsimony (the
repository is the only state, and one user names a node and signs its
reports), Loose Coupling (a build host joins by syncing a clone).

## <a id="tools"></a>11. Tools

OCX's tools write these artifacts, and perform runs by hand, for people and
programs, as a convenience, never a requirement: anyone may write the same
artifacts by other means, such as a verdict with `fossil tag add` or a run as
a forum post, and they count exactly the same. [OCX's tools](protocol-v1-tools.md)
are `create`, which writes a run; `run`, which performs one by hand, starting a
`manual` run or trying a definition without writing anything; and `attach`,
`stop`, `post`, `audit`, `push` and `status`.

Why: Loose Coupling (the artifacts are the standard, and no tool is required).

## <a id="versions"></a>12. Versions

Every OCX message carries `"ocx": 1`. Version 1 may gain new keys and stay
version 1, and a key never changes its meaning or its name. An unknown key in
`limits` may be a limit the node cannot enforce, so the node answers the run
`ended`, `refused`. An unknown key anywhere else is ignored: the message is
acted on as if the key were not there. A node also refuses a run whose runner
it does not offer, and ignores a message whose `kind` it does not know. A
change that breaks these rules makes version 2. A reader takes the first block
with a version it knows ([section 2](#messages)), so one post may carry a
block for each version.

Why: Loose Coupling (other programs can rely on the formats), Ax4 (meanings
cannot drift, and no limit is silently dropped).

# OCX's tools for protocol v1

[Protocol v1](protocol-v1.md) is the standard: its artifacts count however
they are written. OCX's tools write them, and perform runs by hand, for people
and programs, as a convenience, never a requirement. Anyone may write the same
artifacts by other means, such as a verdict with `fossil tag add` or a run as a
forum post, and they count exactly the same.

## <a id="tools"></a>The tools

- `create` writes a `run`: in a new thread, in reply to a post (`to`), or on a
  ticket (`ticket`). It writes the run's keys as given, known to it or not,
  since a node ignores the keys it does not know (section 12).
- `run` performs a run by hand, at the node, from its definition: the same
  checkout, sandbox and limits the node would give it. Only a user holding
  admin or setup may use it. From the command line it runs in your shell, its
  program's terminal yours until it ends, or until you leave a session. From
  the web page it starts detached, for `attach` to join. It has two uses:
  - starting a run whose `manual` is true, which the node does not start
    itself. The tool hands the launch to the node, so the node writes
    `started` and then `ended` as its node user, as for any run. If several
    start it at once, the first to write `started` starts it, and each of the
    others is refused, writing nothing; so is anyone starting a run already
    `ended`;
  - a trial, `run --trial`: it performs any run's definition, or one not yet
    sent, and writes nothing to Fossil, no `started` and no `ended`: its
    program holds no password, and OCX's tools refuse its writes. It prints
    the outcome instead, and for a `ci` run, which it tries once on the latest
    check-in of each branch in `checkins`, the verdicts it would write.
- `attach` joins a running session's terminal from a shell at the node, and
  leaves it running when you go. Only a user holding admin or setup may use
  it, and each attachment leaves a post in the run's thread, or a change to
  its ticket.
- `stop` writes a `stop` for a run.
- `post` writes a plain post in reply to a post, or a change to a ticket.
- `audit` writes a verdict.
- `push` copies a run's check-ins from its clone to the node's repository, for
  a run whose limits ask for `no_password`.
- `status` writes nothing: it says who and where you are, and what this node
  offers.

## <a id="answers"></a>Where they are offered, and what they answer

The command line offers every tool; the web page and MCP offer every tool but
`attach`, which needs a terminal at the node. Each tool checks its input before
anything happens, and naming a field it does not know is refused. Every tool
takes `dry_run`, which checks everything and writes nothing, and every tool
answers in one shape: `{"tool", "dry_run", "log", "result", "stopped"}`.
`result` is the tool's output, once it has run to the end:
- for `create`, `stop`, `post` and `audit`, the hash of what it wrote;
- for `push`, the hashes of the check-ins it copied;
- for `run`, the outcome as `ended` gives it, a trial's too;
- for `attach`, the hash of the post it left;
- for `status`, what it says.

`stopped` says why it did not run to the end, naming the field at fault.

Why: Clarity (typed input, one answer shape), Loose Coupling (the same tools
from every place, and none required), Demonstrability (the dry run, and the
trial).

# OCX protocol v1: verdicts

A verdict is a tag on a check-in that records a judgement of it: `audited`
for one made by a person or an agent, `ci` for a program's check. Runs of
[protocol v1](protocol-v1.md) write them, but a verdict needs no run: anyone
may write one.

## <a id="writing"></a>Writing a verdict

Write it as a raw tag (Fossil's `--raw`), so it never becomes a name for the
check-in, and on that check-in alone, never propagating to its descendants. A
reader reads older `sym-audited` and `sym-ci` tags as verdicts too. Its value
is `pass: ` or `fail: `, then the reason on one line, then ` (against X)`,
where X names the objective it was judged against: a run's full hash, or the
path of a file that states the objective, such as `PLAN.md`, read as it stands
in the check-in judged. X is everything after the value's last ` (against `,
up to its final `)`. Two examples:
- a reviewer's judgement: tag `audited`, value
  `fail: the new command has no tests (against PLAN.md)`;
- a `ci` run's check: tag `ci`, value `fail: 2 tests failed (against RUNHASH)`.

Anyone may write an `audited` verdict, with OCX's `audit` tool or with
Fossil's own `fossil tag add`; it needs no run. But a verdict on your own
check-in never counts: the author of a check-in cannot pass it. A `ci` run
writes a `ci` verdict, as the run's user, on each check-in it checks, against
the run itself.

## <a id="reading"></a>Reading verdicts

A verdict counts only if it was received from a user who holds check-in (`i`)
when it is read, and who neither sent the check-in nor is the user named in
it. A run named in it must be about the check-in: the check-in its `started`
names, or, for a `ci` run, one on a branch in its `checkins`.

Fossil shows only a tag's latest value on a check-in, but keeps every tag
change in its `event` table (type `g`). So a reader takes the tag changes from
there and counts, for each user a verdict was received from, that user's
latest, by receipt order: the times written in tag changes are their writers'
clocks, and decide nothing. Cancelling a tag (`fossil tag cancel`) writes no
verdict, and leaves the verdicts before it standing: nothing is retracted.

How to combine the verdicts that count is the reader's choice: any fail fails,
a majority, one named reviewer. OCX's `queue` and `activity`, CI checks and
merge gates are all readers. A reader on the repository a verdict was sent to
counts by receipts; a reader on a clone can count only by the user named in
each tag change, and says its count is unverified.

Why: Ax2 (always judged against a named objective), Demonstrability
(attributed by receipts, and kept), Loose Coupling (a verdict needs no run, and
any program may read one).

# OCX protocol v1: failures, worked through

Each case is a failure or a race, and what [protocol v1](protocol-v1.md) says
comes of it, artifact by artifact, with the section that decides it. Together
they pin down what the rules might otherwise leave to a reader, as codex's
review of c28028fbd5 asked (stone, 2026-10-12).

## <a id="crash"></a>Crashes

1. **The node goes down before `started`.** It had checked the run out, and
   written nothing. When it starts again it finds the run with no `started`,
   and acts on it as if it had just arrived: it prepares it again, writes
   `started` and launches it. The program had never been launched, so it is
   launched once ([4](protocol-v1.md#reports), [10](protocol-v1.md#nodes)).
2. **The node goes down after `started`,** whether before launching the
   program or while it ran. When it starts again it finds the run with a
   `started`, no `ended`, and no program in its care, and writes `ended`,
   `lost`. It never launches the run again, so no program runs twice for one
   run, and `lost` says it may have run in part, or not at all
   ([4](protocol-v1.md#reports)).
3. **The node loses a session's delivery mark** while its program runs on.
   The node ends the program and writes `ended`, `lost`; it does not guess
   where delivery stopped ([6](protocol-v1.md#subscriptions)).
4. **A manual run is waiting when the node goes down.** When it starts again,
   the run has no `started`, and its `manual` is true: it goes on waiting for a
   person ([10](protocol-v1.md#nodes)).

## <a id="starting"></a>Starting

5. **The checkout fails.** A run names `"checkout": "release-9"`, a branch the
   node's repository does not have, perhaps not yet synced. The node writes
   `ended`, `refused`, with a `note` saying so, and no `started`. To try again,
   send a new run ([4](protocol-v1.md#reports)).
6. **The program cannot be launched.** `command` names `make`, which the
   sandbox does not have. `started` is written, the sandbox exits non-zero,
   and the run ends `exited` with that code ([4](protocol-v1.md#reports)).
7. **A branch moves after the run starts.** A run names `"checkout": "dev"`.
   The node resolves `dev` to a check-in, writes `started` naming it in full,
   and launches. A check-in landing on `dev` a minute later changes nothing:
   the program works on the check-in its `started` names ([5](protocol-v1.md#runners)).
8. **Two people start one manual run at once,** each with OCX's `run` tool at
   the node. One `started` is written and one program launched; the other
   tool finds the `started` and is refused, writing nothing
   ([tools](protocol-v1-tools.md#tools)).

## <a id="stopping"></a>Stopping

9. **Stopped before it starts.** stone sends a run with `manual` true, then a
   `stop` for it. The node writes `ended`, `stopped`, and no `started`: the
   thread holds `run`, `stop`, `ended`. A later `run` tool on it is refused,
   since it has an `ended` ([4](protocol-v1.md#reports)).
10. **Two stops.** While a run runs, two admins each send a `stop`. The node
    acts on the first it received: it ends the program and writes `ended`,
    `stopped`. The second does nothing, and nothing answers it.
11. **The program exits as a `stop` arrives.** Whichever the node meets first
    decides: `ended`, `exited`, the stop then doing nothing; or `ended`,
    `stopped`. Never both, since a run gets one `ended`.

## <a id="delivery"></a>Delivery

12. **A busy session.** Three posts from users in its `hears` arrive while
    its agent is in a turn. They wait; when the turn ends the agent is given
    the first as one turn, then the second and the third, each once the last
    turn has ended. A program that is not an agent finds them in its standard
    input, one JSON object to a line, and reads them when it is ready
    ([6](protocol-v1.md#subscriptions)).
13. **A missed signal.** The hook does not run for a post a session should be
    given. The next time the node reads its repository, it gives the session
    everything received since the last artifact it gave it, in the order
    received: the missed post, and nothing twice.

## <a id="refusing"></a>Refusing

14. **Contradictory limits.** A run asks `{"own_thread": true, "no_password": false}`.
    Only OCX's tools can enforce `own_thread`, which means `no_password`, so
    the run asks for both and neither: `ended`, `refused`, with a note. A run
    asking `{"own_thread": false}` asks for nothing ([7](protocol-v1.md#limits)).
15. **`ci` told it is not a session.** A run says `"runner": "ci"` and
    `"session": false`: `ended`, `refused`, since `ci` is always a session
    ([5](protocol-v1.md#runners)).
16. **A forged run.** A user holding only check-in (`i`) pushes a forum post
    whose U card says `stone`, holding a run for the server's node. The
    server's repository received it from that user, not from stone, so the
    node answers it `ended`, `refused`, and never decides it again. A node on
    a clone pulled it, received from its own user, and cannot tell who wrote
    it: until that is settled, it answers every run it pulled `ended`,
    `refused` ([2](protocol-v1.md#messages), [8](protocol-v1.md#permissions)).
17. **A forged report.** A user who may post replies to a waiting run with an
    `ended`, `exited`, `exit` 0. It was not received from the run's node user,
    so it does not count: the node still starts the run, and readers still
    see it waiting ([2](protocol-v1.md#messages)).
18. **A sandboxed agent asks for a trial.** Through MCP, it asks `run --trial`
    for a definition naming `stone` and the network. The agent's user holds
    neither admin nor setup, so the tool refuses, and nothing runs
    ([tools](protocol-v1-tools.md#tools)).