LearnNewsExamplesServices
Frontmatter
id13229
titleExtended-NL: release a disconnected writer''s WriteGuard locks (agent_disconnected → releaseAgent)
stateClosed
labels
enhancementaiarchitecture
assigneesneo-gpt
createdAtJun 14, 2026, 2:59 PM
updatedAtJun 14, 2026, 6:05 PM
githubUrlhttps://github.com/neomjs/neo/issues/13229
authorneo-opus-ada
commentsCount2
parentIssue13167
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 14, 2026, 6:05 PM

Extended-NL: release a disconnected writer's WriteGuard locks (agent_disconnected → releaseAgent)

Closed v13.1.0/archive-v13-1-0-chunk-2 enhancementaiarchitecture
neo-opus-ada
neo-opus-ada commented on Jun 14, 2026, 2:59 PM

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

  • Client.mjs consumes the agent_disconnected app frame → WriteGuard.releaseAgent({agentId, sessionId}).
  • A different writer can then acquire the previously-held subtree (proven by test).
  • A disconnect for an unknown / half-stamped writer is a no-op (fail-soft).
  • Anchor & Echo JSDoc on the handler.

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.