Context
#13294 (Resolves #13167, merged) proved the WriteGuard deny-while-held path live: two distinct raw role=agent writers, writer-1 holds a subtree lock, writer-2's overlapping write is DENIED. #13229 (closed) implemented the disconnect-release: a disconnected writer's held locks are swept. Vega's #13294 review follow-up-1 flagged that the release → re-admit half of the lock lifecycle is unit-covered (#13226 / #13284) but not e2e-proven live. This ticket adds that live proof — the symmetric counterpart to #13294.
The Problem
The lock lifecycle has two halves. The deny-while-held half is live-proven (#13294). The release-on-disconnect → re-admit half is implemented (#13229) and unit-covered, but no live e2e exercises the full transport→sweep→re-admit path. Without it, a regression in the Bridge's app-side agent_disconnected emission or the client sweep would pass CI (which doesn't run e2e) and unit tests, yet break real two-agent lock handoff.
The Architectural Reality
The live path: Bridge agent ws close → broadcastToApps({type:'agent_disconnected', agentId, sessionId}) (ai/mcp/server/neural-link/Bridge.mjs:232, the app-side broadcast includes sessionId — the agent-side broadcast at :226 omits it) → Client.onSocketMessage (src/ai/Client.mjs:313) → handleAgentDisconnected → writeGuard.releaseAgent({agentId, sessionId}) (src/ai/Client.mjs:301) + transactionService.sweep. The e2e is the live-app counterpart to the unit coverage, the same shape as #13294.
The Fix
A sibling spec test/playwright/e2e/WriteGuardDisconnectReleaseNL.spec.mjs: writer-1 (raw ws) acquires + holds a button's subtree; a distinct writer-2 (raw ws) overlapping write → DENIED; writer-1 disconnects → its lock is swept; writer-2 → ADMITTED.
Critical harness ordering (proven empirically): writer-1 must be fully established and holding the lock BEFORE writer-2 opens. Opening both sockets back-to-back races the lock acquisition and yields non-deterministic deny/admit — establishing the holder first is rock-solid (proven deterministic 2/2 in a throwaway: deny-while-held, then admit-after-disconnect).
Acceptance Criteria
Out of Scope
- The deny-while-held proof — #13294 / #13167 (merged).
- The NL undo/redo e2e proofs — #13286 / #13306 (Vega's lane; undo ≠ lock-release).
- The disconnect-release implementation — #13229 (closed).
Decision Record impact
none — e2e verification of an existing, shipped behavior; aligned-with the #13056 topological-locking model.
Related
- #13056 (epic parent — Extended-NL coordination: locking) / #13229 (the disconnect-release impl this proves)
- #13294 / #13167 (the deny-while-held e2e sibling) / #13226 / #13284 (unit coverage)
Release classification: post-release (verification proof of shipped behavior — non-blocking).
Origin Session ID: 2d993feb-ea2f-4468-8fbd-c53e62365f4d
Retrieval Hint: "WriteGuard disconnect release e2e agent_disconnected sweep re-admit lock lifecycle"
Authored by Claude Opus 4.8 (1M context), @neo-opus-ada (Ada).
Context
#13294 (Resolves #13167, merged) proved the WriteGuard deny-while-held path live: two distinct raw
role=agentwriters, writer-1 holds a subtree lock, writer-2's overlapping write is DENIED. #13229 (closed) implemented the disconnect-release: a disconnected writer's held locks are swept. Vega's #13294 review follow-up-1 flagged that the release → re-admit half of the lock lifecycle is unit-covered (#13226 / #13284) but not e2e-proven live. This ticket adds that live proof — the symmetric counterpart to #13294.The Problem
The lock lifecycle has two halves. The deny-while-held half is live-proven (#13294). The release-on-disconnect → re-admit half is implemented (#13229) and unit-covered, but no live e2e exercises the full transport→sweep→re-admit path. Without it, a regression in the Bridge's app-side
agent_disconnectedemission or the client sweep would pass CI (which doesn't run e2e) and unit tests, yet break real two-agent lock handoff.The Architectural Reality
The live path: Bridge agent ws
close→broadcastToApps({type:'agent_disconnected', agentId, sessionId})(ai/mcp/server/neural-link/Bridge.mjs:232, the app-side broadcast includes sessionId — the agent-side broadcast at :226 omits it) →Client.onSocketMessage(src/ai/Client.mjs:313) →handleAgentDisconnected→writeGuard.releaseAgent({agentId, sessionId})(src/ai/Client.mjs:301) +transactionService.sweep. The e2e is the live-app counterpart to the unit coverage, the same shape as #13294.The Fix
A sibling spec
test/playwright/e2e/WriteGuardDisconnectReleaseNL.spec.mjs: writer-1 (rawws) acquires + holds a button's subtree; a distinct writer-2 (rawws) overlapping write → DENIED; writer-1 disconnects → its lock is swept; writer-2 → ADMITTED.Critical harness ordering (proven empirically): writer-1 must be fully established and holding the lock BEFORE writer-2 opens. Opening both sockets back-to-back races the lock acquisition and yields non-deterministic deny/admit — establishing the holder first is rock-solid (proven deterministic 2/2 in a throwaway: deny-while-held, then admit-after-disconnect).
Acceptance Criteria
WriteGuardDisconnectReleaseNL.spec.mjs: established writer-1 holds A; writer-2 overlapping → DENIED (conflict naming writer-1'sagentId/sessionId); writer-1 disconnect → writer-2 retry → ADMITTED.Out of Scope
Decision Record impact
none— e2e verification of an existing, shipped behavior; aligned-with the #13056 topological-locking model.Related
Release classification:post-release (verification proof of shipped behavior — non-blocking).Origin Session ID: 2d993feb-ea2f-4468-8fbd-c53e62365f4d
Retrieval Hint: "WriteGuard disconnect release e2e agent_disconnected sweep re-admit lock lifecycle"
Authored by Claude Opus 4.8 (1M context), @neo-opus-ada (Ada).