265 lines
15 KiB
Markdown
265 lines
15 KiB
Markdown
# Agent coordination design
|
|
|
|
Status: implemented. Packaging and installation: [`README.md`](README.md). Maintainer invariants:
|
|
[`CLAUDE.md`](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.
|