Context
Found during a completeness audit of #13221 (NL undo Slice-1) before considering it closeable. Neo.ai.TransactionService.sweep — the transaction-side teardown that "aborts any open transaction and retires the session's stack" — is defined but never called anywhere (grep '\.sweep(' src/ ai/ → empty). Its own JSDoc states the contract: "The transaction-side counterpart to WriteGuard.releaseAgent (the caller runs both on an agent_disconnected frame, so the lock + transaction lifecycles cannot diverge silently)." But the only caller — Neo.ai.Client.handleAgentDisconnected — runs only writeGuard.releaseAgent.
The Problem
On an agent_disconnected frame the agent's held write-locks are released, but its open transaction + undo stack are not swept — the exact lifecycle divergence the sweep exists to prevent. A disconnected (or worker-restarted) agent leaks its session entry in TransactionService.sessions; the lock and transaction lifecycles diverge silently. This is #13221's AC7 ("disconnect / releaseAgent / timeout sweeps the requester's open tx ... lifecycles can't diverge") — partially undelivered: the mechanism shipped (#13230), the wiring did not.
The Architectural Reality
Neo.ai.Client.handleAgentDisconnected (src/ai/Client.mjs:284) — validates the (agentId, sessionId) pair, then return writeGuard.releaseAgent({agentId, sessionId}). No transactionService call.
Neo.ai.TransactionService.sweep({id}) (src/ai/TransactionService.mjs:332) — deletes the (agentId, sessionId) session entry; fails closed on incomplete identity; already unit-tested in TransactionService.spec.
Neo.ai.Client holds this.transactionService (src/ai/Client.mjs:100).
The Fix
handleAgentDisconnected calls this.transactionService.sweep({id: {agentId, sessionId}}) alongside writeGuard.releaseAgent, after the same identity validation; returns {released, swept}.
- Extend
test/playwright/unit/ai/ClientAgentDisconnect.spec.mjs: the sweep is invoked with the exact writer pair on a complete frame, and a half-stamped frame never sweeps (fail-closed symmetry with releaseAgent).
Acceptance Criteria
Out of Scope
- #13221 AC10 (a live-tree e2e undo round-trip) — a separate, larger sub-lane, now enabled by the NL-drivable example (#13279).
- Lease/timeout-driven sweep (
TransactionService.timeout wiring to a daemon) — distinct from the disconnect path.
Related
- Completes #13221 AC7 (NL undo Slice-1). Mechanism delivered by #13230 (TransactionService core) + #13236 (stack key).
- Epic: #13012 (Agent Harness, Pillar-2).
- Decision Record impact: none (wiring + test of shipped mechanisms; no ADR).
Release classification: boardless (post-release Pillar-2 coherence fix — not v13-release-blocking).
Retrieval Hint: "TransactionService sweep orphaned not wired agent_disconnected handleAgentDisconnected lock tx lifecycle divergence"
Origin Session ID: a0bc6b23-78c5-4bd7-b944-9db5e236f42d
Context
Found during a completeness audit of #13221 (NL undo Slice-1) before considering it closeable.
Neo.ai.TransactionService.sweep— the transaction-side teardown that "aborts any open transaction and retires the session's stack" — is defined but never called anywhere (grep '\.sweep(' src/ ai/→ empty). Its own JSDoc states the contract: "The transaction-side counterpart toWriteGuard.releaseAgent(the caller runs both on anagent_disconnectedframe, so the lock + transaction lifecycles cannot diverge silently)." But the only caller —Neo.ai.Client.handleAgentDisconnected— runs onlywriteGuard.releaseAgent.The Problem
On an
agent_disconnectedframe the agent's held write-locks are released, but its open transaction + undo stack are not swept — the exact lifecycle divergence the sweep exists to prevent. A disconnected (or worker-restarted) agent leaks its session entry inTransactionService.sessions; the lock and transaction lifecycles diverge silently. This is #13221's AC7 ("disconnect /releaseAgent/ timeout sweeps the requester's open tx ... lifecycles can't diverge") — partially undelivered: the mechanism shipped (#13230), the wiring did not.The Architectural Reality
Neo.ai.Client.handleAgentDisconnected(src/ai/Client.mjs:284) — validates the(agentId, sessionId)pair, thenreturn writeGuard.releaseAgent({agentId, sessionId}). NotransactionServicecall.Neo.ai.TransactionService.sweep({id})(src/ai/TransactionService.mjs:332) — deletes the(agentId, sessionId)session entry; fails closed on incomplete identity; already unit-tested inTransactionService.spec.Neo.ai.Clientholdsthis.transactionService(src/ai/Client.mjs:100).The Fix
handleAgentDisconnectedcallsthis.transactionService.sweep({id: {agentId, sessionId}})alongsidewriteGuard.releaseAgent, after the same identity validation; returns{released, swept}.test/playwright/unit/ai/ClientAgentDisconnect.spec.mjs: the sweep is invoked with the exact writer pair on a complete frame, and a half-stamped frame never sweeps (fail-closed symmetry withreleaseAgent).Acceptance Criteria
handleAgentDisconnectedsweeps the disconnected writer's transaction stack viaTransactionService.sweep, keyed on the same(agentId, sessionId)pair it releases locks for.ClientAgentDisconnect.speccovers both.Out of Scope
TransactionService.timeoutwiring to a daemon) — distinct from the disconnect path.Related
Release classification: boardless (post-release Pillar-2 coherence fix — not v13-release-blocking).
Retrieval Hint: "TransactionService sweep orphaned not wired agent_disconnected handleAgentDisconnected lock tx lifecycle divergence"
Origin Session ID: a0bc6b23-78c5-4bd7-b944-9db5e236f42d