Context
The release half of the multi-writer enforcement, follow-up to #13224 / PR #13226 (the InstanceService write-path WriteGuard enforcement, merged). That PR acquires-and-holds subtree write-locks; this frees a disconnected writer's locks. Architecture: ADR 0021.
V-B-A — live source (re-grounded against origin/dev; corrects a stale-checkout over-scope of mine)
⚠️ I briefly over-scoped this ticket from a stale local checkout (92 commits behind on Bridge.mjs). Re-grounded against origin/dev ai/mcp/server/neural-link/Bridge.mjs:213-237, the Bridge half is already done on dev:
ws.sessionId = crypto.randomUUID() (line 220) — minted per connection, "so the app-side write-guard can key locks on the (agentId, sessionId) pair".
- On agent disconnect, the Bridge already calls
broadcastToApps({type:'agent_disconnected', agentId, sessionId}) (lines 232-237) — to app sockets, carrying sessionId, explicitly "Lets the app-side write-guard release locks held by the now-dead writer; idempotent so a blanket app broadcast is safe." Bridge.spec covers it.
The only gap: src/ai/Client.mjs has no agent_disconnected consumer. So this is a small Client-side lane — exactly as @neo-gpt's intake scoped it (his origin/dev read was correct).
The Fix
src/ai/Client.mjs: handle the inbound agent_disconnected app frame → this.writeGuard.releaseAgent({agentId, sessionId}) (already exists + tested, #13134).
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
agent_disconnected app frame (consumed) |
Bridge.mjs:232-237 (EXISTING on dev — broadcastToApps({agent_disconnected, agentId, sessionId})) |
Client reads {agentId, sessionId} as the dropped writer pair. |
Frame missing agentId/sessionId → no-op (never release on a half pair). |
n/a (existing wire frame) |
Bridge.spec (existing). |
Client.mjs agent_disconnected handler |
This issue + ADR 0021 |
On the frame, call WriteGuard.releaseAgent({agentId, sessionId}), freeing that writer's held locks. No re-render. |
Unknown / never-held / half-stamped writer → releaseAgent returns {released: 0} (fail-soft no-op). |
JSDoc on the handler. |
Unit: handler releases the right writer's locks; freed subtree grantable to a different writer. |
WriteGuard.releaseAgent({agentId, sessionId}) |
#13134 (merged) |
Sweeps the pair's held locks; returns {released: N}. |
Covered. |
Existing. |
Existing WriteGuard.spec. |
Acceptance Criteria
Out of Scope
- The Bridge
agent_disconnected app-broadcast + sessionId minting — already on dev (Bridge.mjs:213-237).
- Cross-harness session-id canonicalization (#12984).
- The live two-agent disconnect→regrant whitebox-e2e — rides the ADR 0021 / #13167 umbrella e2e.
Related
Parent: #13167. Consumes #13134 + the existing Bridge app-broadcast. Sibling to merged #13224. Architecture: ADR 0021. Claimed by @neo-gpt; scope confirmed Client-only against origin/dev Bridge.mjs.
Authored by @neo-opus-ada; re-scoped Client-only (correcting a stale-checkout over-scope) 2026-06-14.
Context
The release half of the multi-writer enforcement, follow-up to #13224 / PR #13226 (the InstanceService write-path WriteGuard enforcement, merged). That PR acquires-and-holds subtree write-locks; this frees a disconnected writer's locks. Architecture: ADR 0021.
V-B-A — live source (re-grounded against origin/dev; corrects a stale-checkout over-scope of mine)
⚠️ I briefly over-scoped this ticket from a stale local checkout (92 commits behind on
Bridge.mjs). Re-grounded against origin/devai/mcp/server/neural-link/Bridge.mjs:213-237, the Bridge half is already done on dev:ws.sessionId = crypto.randomUUID()(line 220) — minted per connection, "so the app-side write-guard can key locks on the (agentId, sessionId) pair".broadcastToApps({type:'agent_disconnected', agentId, sessionId})(lines 232-237) — to app sockets, carryingsessionId, explicitly "Lets the app-side write-guard release locks held by the now-dead writer; idempotent so a blanket app broadcast is safe." Bridge.spec covers it.The only gap:
src/ai/Client.mjshas noagent_disconnectedconsumer. So this is a small Client-side lane — exactly as @neo-gpt's intake scoped it (his origin/dev read was correct).The Fix
src/ai/Client.mjs: handle the inboundagent_disconnectedapp frame →this.writeGuard.releaseAgent({agentId, sessionId})(already exists + tested, #13134).Contract Ledger Matrix
agent_disconnectedapp frame (consumed)Bridge.mjs:232-237(EXISTING on dev —broadcastToApps({agent_disconnected, agentId, sessionId})){agentId, sessionId}as the dropped writer pair.agentId/sessionId→ no-op (never release on a half pair).Client.mjsagent_disconnectedhandlerWriteGuard.releaseAgent({agentId, sessionId}), freeing that writer's held locks. No re-render.releaseAgentreturns{released: 0}(fail-soft no-op).WriteGuard.releaseAgent({agentId, sessionId}){released: N}.WriteGuard.spec.Acceptance Criteria
Client.mjsconsumes theagent_disconnectedapp frame →WriteGuard.releaseAgent({agentId, sessionId}).Out of Scope
agent_disconnectedapp-broadcast +sessionIdminting — already on dev (Bridge.mjs:213-237).Related
Parent: #13167. Consumes #13134 + the existing Bridge app-broadcast. Sibling to merged #13224. Architecture: ADR 0021. Claimed by @neo-gpt; scope confirmed Client-only against origin/dev
Bridge.mjs.Authored by @neo-opus-ada; re-scoped Client-only (correcting a stale-checkout over-scope) 2026-06-14.