14 KiB
Agent coordination design
Status: implemented. Packaging and installation: README.md. Maintainer invariants:
CLAUDE.md.
Boundary that actually isolates work
The shared Jujutsu working copy is also @. Every jj command snapshots all peer filesystem
edits into whichever change currently owns that checkout. File leases cannot prevent this because
the absorbing session did not write those files. This caused duplicate work, foreign features
inside unrelated commits, and one temporary loss/recovery incident.
Writable main agents therefore use independent colocated jj git clone instances. They have
independent operation logs, working-copy commits and Git backends, and ordinary Git-aware Nix
flake discovery works in them. The canonical checkout is for read-only inspection and one
explicit integration owner.
Native jj workspace add remains available behind --native-workspace. It is useful when sharing
one repository is intentional, but it shares the operation log/object store and is not the
default isolation boundary. In this repository its secondary directory also lacks an ordinary
.git, so nix flake metadata . does not discover the colocated Git flake normally.
The compatibility default for managed clones is /var/tmp/fleet-audit, the large
non-snapshotted scratch dataset in the original deployment. Other installations should select a
safe local dataset with --scratch-root; avoid any location whose capacity, snapshots, backup,
or mount semantics make disposable clones expensive.
Identity
- A conversation ID is durable across resume.
- A runtime ID is a fresh UUID for one live attachment to that conversation.
- Human/LLM-facing runtime and workspace identities are exact five-word handles derived from the full opaque ID with a domain-separated SHA-256 mapping over 128 common words (35 display bits). Exact handle matches carry authority; prefixes never do, and an unlikely collision is refused rather than guessed.
- Full UUIDs remain canonical in SQLite, tmux metadata, launcher environments and deterministic audit exports. Handles are aliases, not secrets and not replacements for internal entropy.
- Managed launches export the exact workspace record as
COORD_WORK_ID. It may default only low-blast-radius workspace-local operations after proving that the live runtime owns the record andcwdis inside its registered path. Resource scopes and destructive targets stay explicit. - The launcher stores full IDs in tmux user options. Window names are presentation only:
<short-task>-<sid4>.
The distinction prevents a stale pre-resume process from renewing or releasing the resumed process's resources.
Transactional state
Runtime state is a private SQLite WAL database at
$XDG_STATE_HOME/fleet-agent-coord/<project-id>.sqlite3 (override with COORD_DB). It is outside
every agent-writable clone, directory mode 0700 and database/WAL mode 0600.
This database is deliberately host-local. It coordinates Claude and Codex processes sharing one machine; it does not serialize work performed from different fleet nodes. Cross-host deployment still needs an explicit human/integration owner (or a future authenticated service).
All conflict-check-plus-claim operations use BEGIN IMMEDIATE. Resource names are exact,
repository-relative hierarchical names. Equal names and ancestor/descendant names overlap.
The compatibility matrix is:
| Existing | Requested append | Requested exclusive |
|---|---|---|
| append | allowed | refused |
| exclusive | refused | refused |
Leases cover files and global operations alike, for example:
tests/checks.nixintegration/masterpush/origin/masteractivate/home/tpp15sdeploy/system/nix-controlexternal/fleet-dotfiles
Heartbeats renew both runtime presence and held leases. Expired/dead runtime leases are reaped transactionally. Partial release removes only the named resources.
Messages have monotonic integer IDs. A broadcast snapshots its recipient set at send time.
Delivery transitions queued → delivered → acked. Delivered context repeats at hook boundaries
until the receiving agent explicitly runs coord ack <message-id>. The first delivery carries the
body; later boundaries carry the message ID, sender handle, kind and body digest plus an explicit
coord inbox --peek recovery command. A rejected/lost harness response therefore cannot silently
consume it, while a long body is not reinjected on every tool call.
Audit events remain append-like in SQLite and can be exported deterministically as JSONL with
coord audit.
Action composition
coord batch ACTION : ACTION... reduces agent/tool round trips without inventing a second command
language: every action is parsed by the ordinary CLI parser and executed by its ordinary handler.
It runs sequentially, stops on the first nonzero result, and can return compact human output or
one JSON envelope. The separator is a standalone token, so quoted message punctuation is not
special.
A batch is intentionally not atomic and performs no rollback. Each state-changing action retains its own transaction, validation and audit event. The allowlist excludes clone creation/removal, tmux process replacement and integration queueing. Generic composition must not become a way to hide destructive scope or ambiguous external side effects.
Every nested action inherits the outer batch's already-resolved exact runtime. Identity flags are rejected inside action token streams and the resolved nested runtime is asserted equal before dispatch, preventing one batch from mixing or impersonating audit/lease authority.
Remote publication, integration, activation and deployment need purpose-built workflows with explicit resources and expected object IDs. In particular, a future publish workflow must acquire the exact push lease, authoritatively read the network remote, refuse an unexpected base, avoid force, reread the new remote head, and retain the lease with recovery instructions after an ambiguous mutation result.
Enforcement and degradation
The shared hook recognizes Claude Edit/Write paths and every Codex apply_patch source and
Move to: destination. It canonicalizes against the hook payload's working directory and blocks
a peer's exclusive overlapping lease.
This is scoped enforcement, not a shell parser. Shell writers, formatters, generators and VCS operations remain advisory. Isolation is what makes that honest limitation safe. A private clone can continue working if the coordinator is unavailable.
The hook and lifecycle bridge fail open on load, parse or database failure. A positively
recognized conflicting write fails closed. Denial and additionalContext are separate branches
because Codex rejects a response containing both.
Claude subagents receive their own deterministic runtime UUID, derived from the full parent
conversation ID and hook agent_id. They therefore conflict, message, and lease independently
instead of inheriting the parent's authority. Writable subagents should still use their own
managed clone; the distinct identity makes accidental writes in the integration checkout fail
closed unless that subagent explicitly owns the resource.
Workspace lifecycle and cleanup
Each record contains the full task/conversation/runtime identity, source and actual path, exact base/current Jujutsu IDs, dependencies, planned/actual resources, gate/integration/lifecycle states, immutable tmux IDs and an ownership token.
The default CLI view deliberately omits those machine fields. It prints handles, task/state, and
only the path needed to enter a newly created clone. --verbose, --json, and audit export are
explicit diagnostic/machine surfaces. Cleanup tokens remain private by default; normal removal
requires repeating the exact work handle and then compares the registry token with the on-disk
marker.
Lifecycle states support active, parked, resumed, archived, queued/integrating/lock-released/ integrated and removed work. Lock release is not integration evidence: a separate proof verifies that the exact source commit is an ancestor of the exact target commit. Cleanup never guesses:
The current “queue” is an integration lock plus an auditable state machine, not a fair scheduler: enqueue order and recorded dependencies are visible but do not automatically grant the next turn. The integration owner adjudicates readiness and stale bases.
- path must remain below the recorded scratch root;
- private registry token and on-disk
.jjmarker must agree; - working copy must be clean;
- an advanced change must be released/integrated;
- automatic cleanup requires a passed or explicitly waived gate.
Missing, dirty, ungated or identity-mismatched directories remain recovery candidates.
Known independent clones can be recovered after database loss with explicit work adopt. Adoption
requires both .jj/ and .git/, a path beneath the configured scratch root, and a source distinct
from the candidate. It validates exact Jujutsu IDs before rotating the ownership marker. There is
deliberately no directory-name scan or automatic adoption.
If older shared-checkout work was lost, recover read-only with
jj --at-op=<op> --ignore-working-copy file show <path>, then merge the recovered content into a
fresh isolated clone. Never restore a whole stale file over newer work.
Tmux rules
Managed sessions discover project ownership through tmux user options and mutate objects only by
immutable $session, @window and %pane IDs returned by tmux. Names are never lookup targets.
Automatic window renaming is disabled. Codex resumes with codex-direct resume <conversation>;
Claude resumes with claude-direct --resume <conversation>.
work close revalidates those IDs and kills the exact window. It refuses when called from any
pane in the target window, because killing that window would terminate the coordinator before its
registry update. If the operator is attached elsewhere in the target session's last window, it
first switches to another managed session and otherwise refuses, preventing the surprising drop
to a plain outer shell.
The ordinary codex/claude helper also persists interactive resume invocations and forwards
their arguments to the session runner. Automation (codex exec, claude -p, pipes and non-TTY
calls) remains direct and status-faithful.
Proved failure modes
Behavioral checks cover:
- eight-way exclusive race: exactly one winner;
- six simultaneous messages: no loss and monotonic IDs;
- exclusive/append matrix, hierarchical conflicts, renewal/expiry and partial release;
- truncated identity refusal;
apply_patchmove-destination blocking;- repeated delivery until explicit acknowledgement;
- independent clone metadata and explicit native-workspace limitations;
- dirty, ungated, advanced and ownership-mismatched cleanup refusal;
- tmux name collision handling and immutable-ID-only mutations;
- distinct Claude subagent authority, explicit orphan adoption and exact-window close;
- real disposable
jj git clone --colocatecreate/inspect/remove.
Implementation language and versioning
Each standalone release carries a semantic VERSION, vendored into .coord/VERSION and exposed
by coord --version. The installer doctor reports both source and installed versions but still
compares every managed byte and executable mode; equal version strings are not integrity evidence.
Legacy installations without the marker are explicitly unversioned and can be repaired through
the same preflighted atomic plan as any upgrade. This adds observability without coupling runtime
SQLite schema state to package release numbering.
Python plus the standard-library SQLite driver remains the current best fit: hooks can run before a Nix activation or build bootstrap, deployment has no compiled-artifact handoff, and the transaction boundary lives in SQLite rather than process memory. A Go rewrite would improve single-binary distribution, startup predictability and static typing, but would not improve the isolation or locking model by itself. Those benefits do not currently justify a second implementation and migration surface.
Interface and context budget
Coordinator output crosses an unusually expensive boundary: lifecycle context and command output are repeatedly injected into model context. The interface therefore follows these rules:
- managed environments infer caller identity; ordinary commands do not repeat it;
- required operands are positional (
claim integration/master,work create cache-review); - readable long options remain for genuinely optional semantics;
- default listings use exact word handles and suppress full IDs, conversations, paths and workspace rows not requested by the caller;
- JSON is compact and explicit, and ownership secrets are redacted unless a private recovery export opts in;
- full Jujutsu/Git IDs are stored as evidence but presentation output uses a sufficient display prefix only where no later command consumes it as authority.
This is a larger saving than removing the two dash characters from every option. Cryptic flags can increase correction turns and negate their tiny lexical saving.
The executable hook source is cooperative infrastructure, not a security boundary: a writable clone contains a writable copy, and hook trust is hash-pinned by each harness. Before deploying incompatible database schemas, add explicit versioned migrations with backup/rollback and package one immutable coordinator runtime through Home Manager. That packaging can use Python or Go; it does not require a language rewrite.
Third-party adjudication
The current system is intentionally small and local. MCP Agent Mail may later add a searchable
mail UI, and tmux-agent-status may add an operator sidebar, but neither is authoritative for
leases. Beads adds a useful task DAG at the cost of Dolt; Gas Town and Agent of Empires are
Git-worktree-oriented and too invasive as the coordination substrate. Any pilot must compose with,
not replace, the transactional resource and isolated-clone boundary.