Skip to content

fix: a collaboration did not survive a daemon restart — the fence came back missing - #263

Merged
saucam merged 1 commit into
mainfrom
fix/collaboration-resume
Jul 27, 2026
Merged

fix: a collaboration did not survive a daemon restart — the fence came back missing#263
saucam merged 1 commit into
mainfrom
fix/collaboration-resume

Conversation

@saucam

@saucam saucam commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Closes the docs/collaborative-session-design.md §11 P2 exit criterion that was never actually met — "a restart resumes mid-goal" — and repairs a privilege regression found while checking it.

collaborationRole has been persisted since P1b (#256). resumeSessions read meta.collaboration and silently ignored it.

What that actually cost

The visible symptom was cosmetic: role-children came back detached from their orchestrator, so the fleet grouping shipped in #262 held only until the daemon bounced.

The invisible ones were the problem. A resumed child came back with:

missing consequence
workerShape its next turn registered a full session agent instead of a scope-capped scout leaf (#ensureAgentIdentity)
capability role roleDeniesTool had nothing to deny with — a read-only reviewer's Write/Edit went from denied to merely asked
blackboard mount it could not publish a handoff at all
autonomous budget resumed guarded; since nobody ever attaches to a child, its first non-safe tool call parks at waiting_approval with zero clients

The first two are a privilege regression across a restart, not a lost feature. §6's "a reviewer that provably cannot write" held on the create path and quietly stopped holding on the resume path. The last one means the fleet rendered as a live collaboration and was dead.

Derive, don't re-invent

No new persisted field was needed. A child's identity (collaborationRole) plus its goal's config reproduces the plan it spawned under.

  • plannedChildFor recovers the PlannedChild — implemented by calling planChildren rather than re-deriving shape/write/reads/writes a second time. A resumed child that computed its own shape would be one edit from disagreeing with the one it spawned under, and the direction that drift fails is a read-only reviewer coming back able to write. Sharing the derivation makes that class of bug unrepresentable rather than merely tested-against.
  • roleChildPosture is now the single definition of a child's restrictions (worker shape + capability role + brief + collaborationRole), called by both #spawnCollaborationChildren and resume. They were separate before, which is how the omission stayed silent.
  • #blackboardMountFor takes a GoalScope instead of a live parent Session. Resume is capped (RESUME_MAX_SESSIONS) and time-boxed, so a parent can legitimately miss the window its own children made — while the board is keyed on (tenant, goalSessionId) in SQLite and needs no resident orchestrator.
  • #attachOrchestratorBlackboard is shared for the same reason: an orchestrator's mount is scoped to its own session id on both paths.

Attribution already anticipated all of this — authorSub is keyed to the ROLE within the goal, never the session id — so a resumed child writes under the same subject its pre-restart versions carry, and its history stays one contributor.

The torn case

Child transcript present, orchestrator's gone (teardown normally removes both). It restores the fence from what the child itself carries — write is on collaborationRole — and nothing else:

  • no mount, because the artifacts died with the goal and a credential to nothing is worse than none;
  • deliberately no autonomous budget, because an agent that cannot coordinate should not burn turns unattended;
  • an honest brief that says the goal was lost and to stop, rather than leaving the model to infer a goal from its own transcript;
  • logged at warn, since a silent degrade is indistinguishable from a healthy fleet in the session list.

Verification

Daemon suite 2064 → 2081. Adds Session.hasBlackboardMount and SessionManager._sessionForTest (named so a production call site is obvious in review).

Mutation-checked. All seven restart tests fail against the pre-fix state (dropping the posture spread). Three further mutations each fail exactly the tests that should catch them:

mutation caught by
mount minted but never attached to the child every member holds an ATTACHED mount…
resumed tokens not tracked for revocation destroying the goal after a restart revokes every resumed token
orphan path mints a mount for a goal that is gone an orphaned child keeps its fence but gets no mount and no budget
workerShape/write hardcoded open in the derivation a read-only role gets BOTH fences, not one of them

Mutation testing also caught two flaws in my own tests, both fixed — worth stating because a green test that asserts nothing is the failure mode this whole change exists to prevent:

  1. activeTokens counts tokens ever minted, so it could not distinguish a mount a session holds from one minted and dropped on the floor. The mount test passed under a mutation that broke exactly what it claimed to check. Now asserted per-session via hasBlackboardMount.
  2. The per-child assertions sat inside a loop over a list the pre-fix state empties, so that test passed vacuously. Now length-guarded, same "guard the guard" pattern as source-hygiene.test.ts.

Verified live across a real process restart, not just a second SessionManager in-process: boot a real daemon, create a 3-role collaboration, publish an artifact, SIGTERM, boot again on the same config dir, reconnect. 15/15:

  • all 3 children reattach with roles and ordinals intact (reasoning#1, review#1, review#2)
  • write authority stays per role, not uniform
  • capability role active — collaboration (review) / collaboration (reasoning)
  • children return autonomous with a fresh 50-turn budget, and role=worker
  • the board survived with its artifact, and authorSub is byte-identical across the restart
  • a resumed child id still resolves to its goal, and reads a body back
  • cascade teardown still collapses the whole resumed fleet to zero

Not in this PR

The rest of §11 P3 (dispatch barrier, panel phase kind, synthesis, live-worker cap, per-collaboration cost roll-up). Non-Claude orchestrators remain #245.

🤖 Generated with Claude Code

…e back missing

`collaborationRole` has been persisted since P1b (#256) and `resumeSessions`
read `meta.collaboration` while silently ignoring it. The visible symptom
was cosmetic: role-children came back detached from their orchestrator, so
the fleet grouping shipped in #262 held only until the daemon bounced.

The invisible symptoms were the problem. A resumed child came back with:

- **no `workerShape`**, so its next turn registered a full session agent
  instead of a scope-capped `scout` leaf (`#ensureAgentIdentity`);
- **no capability role**, so `roleDeniesTool` had nothing to deny with and
  a read-only reviewer's `Write`/`Edit` degraded from denied to merely
  asked;
- **no blackboard mount**, so it could not publish a handoff; and
- **no autonomous budget** — it resumed `guarded`, and since nobody ever
  attaches to a child, its first non-safe tool call parks at
  `waiting_approval` with zero clients. The fleet rendered as a live
  collaboration and was dead.

Two of those are a privilege regression across a restart, not just a lost
feature. §6's "a reviewer that provably cannot write" held on the create
path and quietly stopped holding on the resume path.

## Derive, don't re-invent

No new persisted field was needed: a child's identity
(`collaborationRole`) plus its goal's config reproduces the plan it
spawned under. `plannedChildFor` recovers the `PlannedChild`, and it is
implemented BY calling `planChildren` rather than re-deriving
shape/write/reads/writes a second time — a resumed child that computed its
own shape would be one edit from disagreeing with the one it spawned
under, and the direction that drift fails is a read-only reviewer coming
back able to write. Sharing the derivation makes that unrepresentable.

`roleChildPosture` is now the single definition of a child's restrictions
(worker shape + capability role + brief + collaborationRole), called by
both `#spawnCollaborationChildren` and resume. `#blackboardMountFor` takes
a `GoalScope` instead of a live parent `Session`, because resume is capped
and time-boxed and a parent can legitimately miss the window its own
children made — while the board itself is keyed on (tenant, goal id) in
SQLite and needs no resident orchestrator. `#attachOrchestratorBlackboard`
is likewise shared, since an orchestrator's mount is scoped to its own id
either way.

Attribution already anticipated this: `authorSub` is keyed to the ROLE
within the goal, never the session id, so a resumed child writes under the
same subject its pre-restart versions carry.

## The torn case

Child transcript present, orchestrator's gone (teardown normally removes
both). It restores the FENCE from what the child itself carries — `write`
is on `collaborationRole` — and nothing else: no mount for artifacts that
died with the goal, and deliberately no autonomous budget, because an
agent that cannot coordinate should not burn turns unattended. It gets an
honest brief saying so, and resume logs it at warn level, since a silent
degrade is indistinguishable from a healthy fleet in the session list.

## Verification

Daemon suite 2064 to 2081. Adds `Session.hasBlackboardMount` and
`SessionManager._sessionForTest`.

All seven restart tests were mutation-checked against the pre-fix state
(dropping the posture spread) and all seven fail. Three further mutations
each fail exactly the tests that should catch them: minting a mount but
never attaching it, not tracking resumed tokens for revocation, and
minting a mount on the orphan path.

Mutation testing also caught two flaws in the tests themselves, both
fixed: `activeTokens` counts tokens ever MINTED, so it could not tell a
mount a session holds from one dropped on the floor (now asserted
per-session via `hasBlackboardMount`); and the per-child assertions sat
inside a loop over a list the pre-fix state empties, so that test passed
vacuously until a length check was added.

Verified live across a real process restart, not just a second
SessionManager in-process: boot, create a 3-role collaboration, publish an
artifact, SIGTERM, boot again on the same config dir. 15/15 — children
reattach with roles and ordinals, write authority stays per-role, the
capability role is active, they return autonomous with a fresh 50-turn
budget, the board and its attribution survive, a resumed child still
resolves to its goal and reads a body back, and cascade teardown still
collapses the whole resumed fleet.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@saucam

saucam commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

pr-shepherd: green on iteration 1 — no changes pushed.

All four checks concluded SUCCESS:

Check Result
daemon SUCCESS — lint, typecheck, test, build all ran
web SUCCESS — lint, typecheck, test, build all ran
Socket Security: Project Report SUCCESS
Socket Security: Pull Request Alerts SUCCESS

Verified CI actually attached rather than trusting a clean-looking merge state: this PR is based on main, so both daemon and web ran (not just the Socket checks). Job step lists confirm neither was vacuously green.

daemon test totals match a local run exactly — 2081 pass, 12 skip, 0 fail, 2093 across 148 files. All eight collaboration survives a daemon restart cases passed in CI, including the per-role write-authority and orphan-child (no mount, no budget) paths. No flakes observed in the second-SessionManager restart simulation.

Nothing to auto-fix: commit subject already matches the required prefix regex, branch was 1 ahead / 0 behind main so no rebase was needed, and no formatter or lockfile drift. I pushed no commits and amended no history.

Remaining BLOCKED merge state is REVIEW_REQUIRED only — human review and merge, as always.

@saucam
saucam merged commit 374990e into main Jul 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants