A Session Can Outlive Its Own Authority
The adapter that follows a chat is a child process on office. It can be restarted, replaced, or left behind when an enrollment ends and the same chat is later enrolled again. Through all of that the chat's native id stays the same, so identity alone cannot tell the current adapter from a stale one still holding an old envelope. Coop adds a number for exactly that: the session's generation.
Generation as a Fencing Token
A session row starts at generation 1, and it goes up in two places. When register enrolls a retired session again, the code says why in a comment: it "starts a new generation, which fences anything still acting for the old one." And when a chat that took work through the CLI is adopted into a managed enrollment, adopt_take bumps it too.
Everything the engine hands out is stamped with the generation it was issued under: dispatches, bindings, outbox envelopes. Everything the adapter asks for names a generation: accept, decline, report, delivery receipts, status observations. And one small helper, _session, which accept, decline, report, notes and deliveries all pass through, refuses any call whose generation is not current: "Stale or exited session generation." Status observations make the same comparison on their own. A report goes one step further and also requires the binding itself to belong to this session and generation and still be active. The architecture's failure table puts it plainly: an old generation's report is a conflict, and the current owner's state is preserved.
The fence works from both sides. The adapter only delivers envelopes of its own generation ("Host queue does not own this envelope"), and re-enrollment rewrites its adapter file with the new number, "or the runtime would keep refusing the stale one." When the engine starts, cancel_fenced cancels any pending envelope stamped with an older generation than its session's current one, recording the reason fenced-generation: such a row can never be sent, and left alone it would read as "still being delivered" for ever and hold the enrollment open. Unread replies get a fresh review at the current generation instead. Adoption is the deliberate exception: there, envelopes queued while nobody watched the chat move forward to the new generation, because they still belong to that same chat.
This is Martin Kleppmann's fencing token. A process that holds a lock can pause, lose its lease without knowing, wake up and write as if nothing happened. The fix is a number that grows every time the right is granted, checked by the storage, which rejects a write carrying an older number. Coop's generation is that number and the engine is that storage. The point is where the check lives: in the store, not in the stale process's good manners.
Two Sessions, One Checkout
Dad routinely runs several sessions in one repository on different items. On 2026-09-30 (todo #285, commit 43ddac9) a new Claude chat in one checkout answered READY and sat idle. Discovery had found it in six seconds, but prepare refused the dispatch, because a Codex session assigned #284 three minutes earlier held the same checkout as its write scope, and the failure reached only Telegram. Every sequential launch into one repository after the first failed the same way.
The fix flipped the rule: an overlapping write scope between sessions is disclosed, not refused. At accept, shared_checkout lists every other session whose live dispatch declares a write scope overlapping this one (either path inside the other), with its vendor, actor, status, scope and the titles of its items. Its docstring is the whole policy: "A disclosure, not a lock." A dispatch from the same session and generation is skipped, since one native owner cannot collide with itself, and a duplicate dispatch of the same item is still refused. When the list is non-empty, the work envelope adds this instruction:
"Other sessions are working in this same checkout right now (shared_checkout lists them and their tasks): never revert, stash, reset, clean or commit their changes; stage and commit only files you changed, by explicit path; if a file you need is also being changed by another session, ask Dad before touching it."
The real guards stay where they were: the family's git discipline and executor-owned locks, such as Firelink's deploy lock.
No Chat Waits at READY for Nothing
The same commit added a new envelope kind, the notice. When an assignment fails after its chat was connected, the chat is told: "Coop could not hand this chat its task (…): <reason> Nothing was claimed, and Dad has been notified. Wait for Dad's instruction; do not start the task on your own." The adapter frames it as "A notice from cwkCoop; it asks for nothing but an acknowledgement." A notice owes nothing, so it never holds an enrollment open, and any still pending is cancelled when the enrollment retires.