Login
Protocol version 2, draft: one owner for each release root, woken by events
Login

Written against the hook and poll design, which the daemon replaced (How a post reaches an agent); what it says of today's code is not current.

Part of the proposal, for version 2's node layer. Nothing of it is built. Revised after an adversarial review (three reviewers, 46 findings merged into 18) of its first text. It answers the backlog's "Who owns a run and its repository on a host" (docs/backlog.md): the races fixed on 2026-10-18 each came from two short-lived processes owning one decision or one write. The maintainer, 2026-10-18: "i never opposed the idempotent daemon", and "do try to keep event driven scheduling, rather than polling, for any event that has a clear trigger. try to keep clear layers and data-driven design." The daemon here is the one their earlier direction named: "No automatic relaunch loop, the daemon started from the CGI or the hook under Fossil's sandbox" (HISTORY.md, the list kept).

The invariant

On each host, for each release root (ROOT), at most one process decides about the runs of every repository that root acts on, and writes OCX's answers to them: the root's owner, OCX (layers 2 and 3 of 2.0). A repository is acted on from one release root, as hook's status already requires. Everything else wakes it. A tool still writes its caller's own message or post, as decision 39 has it, and takes turns with the owner there, one fossil command, or the commands of one forum write, at a time (Fossil.turn).

Why one for each root, not for each repository: refusal 7 checks a directory against every run in OCX's care, whatever its repository, and ROOT/care belongs to one account.

Why a lock, when the record already settles which started counts (2.4, "the first that counts, counts")? The record settles it only after both are written: two owners could each launch a process before either started reached the server. The lock stops the second launch.

Part Owns Never
Fossil (layer 1) storage, the server's order, receipts, sync decides about a run
The owner every decision about the root's runs; every answer OCX writes to them; each run in its care, its workspace steps and its process, as that process's parent, until its last publication runs as any account but the root's
A run's process its command, in its sandbox and limits holds the owner's lock, inbox or notes
A trigger (Fossil's hook, poll, and each tool that writes: create, post, stop, publish, start and the rest) its caller's message or post, or poll's pull; then one wake decides about a run; waits for the owner

Events, each with its trigger

Nothing polls where an event has a trigger. A wake is one line, written in one write of at most 4096 bytes (Linux's PIPE_BUF, so lines from many triggers never mix): the repository, by its path with links followed, and the first and last receipt its trigger acts on; or check and the repository. The owner keeps in memory the receipts it has acted on, and acts on each receipt once, so a receipt two lines name (a tool's own write, which Fossil's next hook lists again; two polls whose pulls overlap) is decided and given once. It decides by the record, read in the server's order, and never reads a receipt that no line names (decision 39).

Event Its trigger Its line The owner then
artifacts arrive at the server's repository Fossil's after-receive hook the receipts of the artifacts Fossil listed reads them through the envelope (layer 2) and decides each by a run's life
a tool writes on the server's repository itself that tool, after its write (HISTORY.md, decision 39) its write's receipt the same
artifacts arrive at a clone poll, after its pull; Fossil cannot push to a clone, so this trigger is whoever runs poll: a person, or a timer the host's owner sets the receipts its pull made the same
a person starts a manual run start, writing a start message (2.3) where the run is its receipt the same: the run decided as one that is not manual, from the start's sender
a person asks the deliberate check hook --login on the server's host, poll --all on a clone's, and every accept or rollback check reads every open run naming the repository's clone and decides each by 2.13
a run's process exits, by itself or past a limit the kernel: the owner, its parent, reads its exit status none writes ended, then the run's publications
a stop, a publish or a delivery arrives as artifacts arrive as they do ends the run's process, publishes again, or writes the delivery to the process's input

Starting, idempotently

Publication

The owner writes ended when a run's process is over, with outputs (the hash of each file the run will publish), then each publication. A run stays in its care until the last one is written; a publish for it meanwhile does nothing and says so. If the owner dies first, 2.3 stands: nothing publishes by itself, and a publish has the owner publish again from the kept workspace.

What OCX keeps on the host

Kept Where For
the owner's lock, naming its pid ROOT/care/owner.lock one owner for each release root; accept and rollback end it
its inbox ROOT/care/inbox, a pipe, mode 0600 wakes
each run's note: the process group and pid the owner launched, and the run's workspace ROOT/care/RUN.note, written before launch, removed after its last publication or by the deliberate check the deliberate check, to end what a dead owner left before answering lost; refusal 7, which counts every note's workspace
the log ROOT/hook.log, one line for each wake, as Hook::Run status
each run's workspace as 2.15 what the run made, read and published again
the passwords file, on a clone's host as 2.15 the clone's user's password

What goes

Today Why it goes
the round's lock one owner decides
each run's lock and each user's, and the probes of them (take, held?, care, busy?) the owner knows the runs in its care
poll.lock the owner acts on each receipt once, whatever lines name it
the watcher as a process of its own (--watch, Watcher.hand, Watcher.start), and its deciding again what the round decided (decided, the second decision in prepared, halted, following) the owner runs each run's steps and process itself
each session's inbox (RUN.inbox, Relay) the owner writes a delivery to its process's input
stop_program, TERM to a watcher the owner ends its own child
RUN.workspace, and the pids in RUN.lock the run's note
run's decision by hand (DecideRun, --by-hand) a start message, decided like any arrival
2.3's watcher, keeping a run in OCX's care to its last publication under the run's lock and OCX's own the owner keeps it

Kept: Fossil.turn, its wait made the kernel's; Acting.end_left, run by the deliberate check from the run's note; Acting.mend, which indexes each post a failed write left unindexed and on a clone sends what a failed push left, run by the deliberate check alone; Session.fifo, without its delete, for the inbox.

What comes

New Its cost
the owner's loop reading the inbox, parsing a line, the receipts acted on, one task for each run, the retry of a write that met another writer
a trigger's wake open without waiting and write one line, or start the owner detached; one call, in the hook, poll and each writing tool
the owner's start make and open the inbox, write its line, try the lock
start, a message kind (NEW TERM) a row in 2.3 and in 2.13's table, its check in layer 3, the start tool writing it
the run's note written before launch; read by refusal 7 and the deliberate check
accept and rollback end an owner with no run in its care, by TERM to the pid its lock names, then run the deliberate check

What callers lose

Caller Today Then
create, stop answer what the node did at once (Hook::Acted) answer that the message is written and the owner woken; what the owner did is on the record and in ROOT/hook.log
run, now start refuses at once with the node's reason writes start; a refusal is the run's ended on the record
poll answers what it answered, handed, ended, gave and could not give answers what its pull brought, and that the owner was woken
status reads hook.log's last line the same, written by the owner

Demonstrations owed

Open