Development record
Coordination between headless agents is bounded by deadlines, fenced by the claim's tip, version-checked, and done under each session's own credential
Context
The coordination intent (itd-2610170634394993) replaces the messages sessions sent each other on 2026-10-16 with records on the shared remote that any session reads and acts on. Its decomposition, confirmed by the product thinker on 2026-10-17 (the calibration note under that date), routed four rules to an ADR rather than to the intent, because each one is a boundary other work must not cross, not a feature:
- That evening the load gate deadlocked between two builds of its own queue rule (iss-2610161634043273) and no wait timed out for eight hours.
- A claim taken over from a dead holder can be acted on by that holder if it
comes back, unless a later act re-checks that the claim is still its own
(Chubby's sequencers, Kubernetes leases; the research note
2026-10-17-parallel-headless-agent-coordination.md). - Two sessions on different abcd versions read each other's records; an older reader that guesses at a newer record can wait on it forever.
- Every session on the machine used the person's own forge login, which holds admin rights, so any session could change a setting meant for the person alone (the reservation cap, captured on 2026-10-17). The product thinker ruled the same day that autonomous sessions never work under that login, and on 2026-10-22 that unattended runs happen only where the login is absent (C1) and that each session's own App token ships with the coordination (C3).
The planning prep of 2026-10-18 settled the mechanics each rule rests on (dcn-2610180720366282, dcn-2610180720397970, dcn-2610180720379765, dcn-2610180723332655): the generation number is the claim branch's tip, every expiry is derived when the records are read, and the credential is a GitHub App token minted for the person.
Decision
Four rules bind every coordination act abcd makes, by any verb, in any session:
- Every wait has a deadline. A wait on a slot, a dependency, a claim, a merge or another session ends by a stated time, and a wait that reaches it ends in a refusal that names what it waited on and for how long. No wait is open-ended, and no deadline depends on a process staying alive: each is derived from the records when they are read.
- A claim is fenced by its tip. A push of the work branch or a tag that abcd makes under a claim travels in one atomic push with a renewal of the claim branch, so if the claim's tip moved the forge refuses the whole push. A queue entry cannot share a push: abcd re-reads the claim's tip immediately before it enqueues and names the pull request's head it expects, which leaves a window of one call that this rule states rather than claims closed. A push made outside abcd is not fenced, and the rule does not claim it. A claim whose holder stopped renewing is retaken only after its expiry plus a fixed delay.
- A reader refuses a coordination record newer than it knows. From the release that adds a version to the coordination records, a reader meeting a version above its own refuses the record with a message naming both versions, and never waits on it or guesses its meaning. Readers released before that ignore the field, which is why the rule starts there.
- A build or drain lane abcd launches on its own never acts under the person's own forge login. It pushes, opens pull requests and reads the forge with a short-lived token issued through abcd's GitHub App for that lane, limited to the repository and the acts its work needs. The token reaches only the git and forge commands abcd runs for the session, through a credential channel of abcd's own, never the forge CLI's token variables, and the key or refresh token that issues it is never readable by a session. Such a lane starts only where the person's saved login cannot be reached (a cloud session, a container or a separate user account). The person's own sessions, those working while the person is away included, keep running under their login on their own computer, and a drain the person starts and watches may count as attended (narrowed on 2026-10-23, dcn-2610230710469123). Before such a lane starts, and before its token is issued, abcd reads the signals that carry no secret (the forge CLI's login status by exit code, whether the token variables are set, whether a stored-credential file names the host) and refuses to start, naming where it found a login. It never asks a credential helper, so a login kept only in a keychain is out of its sight, and the refusal says that rather than calling the machine clean.
Alternatives Considered
- These four as one trust rule (chosen). They are the four ways coordination by records fails silently (a wait that never ends, a stale holder acting, a reader guessing, a session holding more than it needs), and each is checkable at the act.
- A coordinator process holding locks (etcd, a Temporal server). Rejected in the state-of-the-art pass: out of scale for one repository, and a process that must stay alive is the failure 2026-10-16 showed.
- Leave deadlines and fencing to each verb. Rejected: the load gate's eight-hour deadlock was exactly a verb that had none.
- Say plainly that the person's login is still within reach, rather than run where it is absent. Offered to the product thinker on 2026-10-22 and not chosen: a caveat is not a guarantee.
Consequences
- Every coordination verb (land, build, drain, status, reserve, the load gate) carries a stated deadline and a named refusal at its end; a test per verb watches it end.
- abcd's pushes and tags carry the claim's renewal atomically, and its queue entries a tip re-check; a session whose claim was taken over cannot push or tag under it through abcd, and can enqueue only inside the one-call window.
- The coordination records gain a version field in the release that ships this; the refusal binds from then on.
- abcd's own build and drain lanes need a setup the person does once: the App registered in the browser (C3), and a place to run them without the person's login. abcd states that as a step only the person can take.
- Accepted with the product thinker's sign-off of the intent's plan on 2026-10-23 (dcn-2610230757556283), and stated in the brief as invariant 25, which binds the coordination intent's build until it is enforced.
Frontmatter
| id | adr-2610222026262980 |
|---|---|
| type | adr |
| status | accepted |
| date | 2026-10-22 |
.abcd/development/decisions/adrs/2610222026262980-coordination-between-headless-agents-is.md
open on GitHub ↗ · commit history ↗
References4
- adr-2610222026265719A session's checklist names a credential by its kind, scope and expiry, never its value or anything that retrieves itrelated
- itd-2609091034175565Before an agent starts on an item, it reserves it on the shared remote, and everyone who works on the project sees the reservationrelated
- itd-2609151838312703Sessions under any account, on any machine or in the cloud announce what they are doing through short fixed-form announcements on the shared remote, a person reads everyone's status with one command, and what is really a hold stays a reservationrelated
- itd-2610170634394993Headless agents in parallel coordinate through the shared remote, not conversationrelated
mentions10
- adr-2610222222326104abcd drives agents with no host session only on the person's own invocation, on the person's own account behind a wall, and hands back what it cannot finish
- dcn-26101807203662822026-10-18 — The coordination pass runs on demand (facilitator-on-delegation, itd-2610170634394993 planning prep): land, build, drain and status run the pass from the records they have just fetched before they act; there is no timer lease and no process that must stay alive, and every deadline and expiry is derived when the records are read, as a reservation's release already is (grounds: the draft's research note on controllers; kubernetes.io/docs/concepts/architecture/controller; brief invariant 7; itd-2609151838312703 decision 14; itd-2609091034175565 decision 13)
- dcn-26101807203797652026-10-18 — An autonomous session's credential is a GitHub App token (facilitator-on-delegation, itd-2610170634394993 planning prep): each session gets a token the repository's App mints for it, short-lived and scoped by repository and permission; a fine-grained personal token is not used, because none can be created through an API and none expires with the session (grounds: docs.github.com authenticating-as-a-github-app-installation; docs.github.com managing-your-personal-access-tokens)
- dcn-26101807203979702026-10-18 — The claim's generation number is its tip commit (facilitator-on-delegation, itd-2610170634394993 planning prep): every act goes through a fast-forward push, which is already a compare-and-swap, and a take-over moves the tip, so no separate counter is added; the forge enforces the fence only on a push, so for queueing and tagging the criterion says abcd refuses the act, and whether one atomic push can carry the work branch and the renewal together is left to probe at planning (grounds: internal/core/claim/claim.go; internal/core/implement/loop/reservation.go; Chubby section 2.4; git-scm.com/docs/git-push --atomic)
- dcn-26101807233326552026-10-18 — An autonomous session acts on the forge as the person, through the App (facilitator-on-delegation, itd-2610170634394993 planning prep): every commit stays authored by the person, since the attribution gate refuses a machine as author or committer; the session pushes and opens pull requests with a token the repository's App mints for the person, limited to the permissions both the App and the person hold and expiring after eight hours, so its work shows the person's name marked as made through the App; the App's own bot identity is not used, because the forge's who-am-I endpoint does not answer for it, so a cloud session could no longer resolve its reservation holder; this meets the product thinker's ruling of a weaker credential of the session's own, which concerns the credential, not the name (grounds: the draft's decision 5; AGENTS.md attribution; scripts/check-attribution.sh; brief invariant 10; docs.github.com generating-a-user-access-token-for-a-github-app; docs.github.com endpoints-available-for-github-app-installation-access-tokens)
- dcn-26102220304779272026-10-22 — abcd looks for the person's forge login before it starts an unattended run, and refuses to start where it finds one (facilitator, on state-of-the-art grounds, applying the product thinker's 2026-10-22 ruling C1 on itd-2610170634394993): before an away-mode run, build or drain starts unattended, and before abcd issues the session a token of its own, abcd checks only signals that carry no secret: the forge CLI's login status for the host by exit code alone, with its output discarded (the CLI prints no token unless asked with --show-token); whether the token variables the forge CLI and git read (GH_TOKEN, GITHUB_TOKEN and their enterprise forms) are set, never their values; and whether the stored-credential files hold an entry for the host (the forge CLI's hosts file and ~/.git-credentials), by the host line alone. It never asks a git credential helper for the host, since a helper answers with the token itself and on macOS may raise a keychain prompt that hangs an unattended start; so a login kept only in a keychain is out of the check's sight, and the refusal says so rather than claiming the machine is clean (brief invariant 16). It refuses to start, naming each place it found a login, and never removes or hides one; the guarantee rests on where the run is started (a cloud session, a container or a separate user account), which abcd states as the person's setup step. The session's own token, issued after the check, never travels in those variables (grounds: cli.github.com/manual/gh_auth_status; cli.github.com/manual/gh_help_environment; git-scm.com/docs/gitcredentials; adr-2610222026262980 rule 4)
- dcn-26102307104691232026-10-23 — The product thinker's ruling of 2026-10-23, restated, narrowing their 2026-10-22 C1 answer (dcn-2610222025104969): only the helpers abcd launches on its own, the build and drain lanes, must run away from the person's GitHub login. The person's own sessions working while they are away keep running on their own computer under their login, and a drain the person starts and watches may count as attended. It settles choice 1 of the coordination intent (itd-2610170634394993) and choice 2 of the terminal-drain intent (itd-2610222215485265), and the login check before an unattended run (dcn-2610222030477927) covers abcd's own launches only.
- dcn-26102307575562832026-10-23 — The product thinker's walk and sign-off of itd-2610170634394993 on 2026-10-23 (about 09:50 BST, in abcd-ae's session), restated. The shared model budget is per computer: sessions on one computer share one pool, and a session on another computer or account draws on that computer's own pool, which narrows the draft's scope condition that the budget spans every account of an organisation. The ten person-visible stories are kept, the budget story in its per-computer form. The eight-hour key of a cloud run is kept: asked why eight, the product thinker was told that GitHub sets eight hours for an app's user access token and that its renewal stays on the person's computer, and kept it. The technical list is accepted in full. The press release, the mechanism and its grounds, the scope conditions and the reason for doing it now are signed off as written. Choice 1 had been ruled earlier (dcn-2610230710469123). The intent is planned on this sign-off.
- iss-2610161634043273The load gate's queue orders waiters by pid, so a waiter with a high pid starves once the machine's process ids wrap. machineload.QueueAhead counts another waiter as ahead when its pid is lower than this one's. After a reboot on 2026-10-16 the ids wrapped: waiters started between 16:07 and 17:26, with pids 50460 to 71218, all queued ahead of one started at 16:04 with pid 96274. That waiter could only wait out its timeout while lower pids kept arriving. The docs and the LOAD rule promise one waiter at a time in queue order.
- spc-2610230758581403several-headless-agents-work-one