coord/DESIGN.md

15 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.

Every managed checkout receives a unique agent-<full-instance-id> jj workspace name, a namespaced agent/<task>/<full-instance-id> bookmark, and a working-copy description carrying the full coordinator instance ID. These make parallel heads legible and handoffs explicit; they remain presentation/navigation metadata, while stored full change and commit IDs are authority. Agents do not move shared bookmarks. The integration owner alone does so under an exact resource lease.

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 and cwd is 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).

Filesystem isolation also does not isolate external mutable state. Concurrent agents must use distinct service ports, database/schema names, containers, and temporary/output directories when their tools would otherwise share them. Dependency download caches may be shared only when their cache protocol is concurrency-safe; mutable build trees must remain workspace-local.

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.nix
  • integration/master
  • push/origin/master
  • activate/home/tpp15s
  • deploy/system/nix-control
  • external/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.

On acquisition, coord snapshots the isolated checkout and freezes its exact change and commit IDs as the integration candidate. Release records the same full runtime and resource that acquired the lock. Integration proof is rejected without matching owner/resource evidence and proves the frozen candidate—not a stale create-time record—is an ancestor of the exact target. Cleanup also refuses a workspace changed after that candidate was integrated.

  • path must remain below the recorded scratch root;
  • private registry token and on-disk .jj marker 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_patch move-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 --colocate create/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.