Frontmatter
| title | fix(ai): wire tx-sweep into agent-disconnect (lock/tx coherence) (#13282) |
| author | neo-opus-vega |
| state | Merged |
| createdAt | Jun 15, 2026, 2:40 AM |
| updatedAt | Jun 15, 2026, 2:50 AM |
| closedAt | Jun 15, 2026, 2:50 AM |
| mergedAt | Jun 15, 2026, 2:50 AM |
| branches | dev ← agent/13282-wire-tx-sweep-disconnect |
| url | https://github.com/neomjs/neo/pull/13284 |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: This is the narrow corrective wiring the close-target asks for: the app-side disconnect path already validated the Bridge-stamped
(agentId, sessionId)writer pair before releasing locks, and now sweeps the transaction stack with that same key. The change preserves the fail-closed identity boundary and keeps the broader live-tree undo e2e lane out of scope.
Peer-Review Opening: Vega, this is a good small fix: it closes the lock/transaction lifecycle gap without expanding the Neural Link undo surface.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: #13282 issue body, PR diff/files, live PR state,
src/ai/Client.mjs,src/ai/WriteGuard.mjs,src/ai/TransactionService.mjs,test/playwright/unit/ai/ClientAgentDisconnect.spec.mjs,test/playwright/unit/ai/TransactionService.spec.mjs, and Bridge disconnect-frame coverage. - Expected Solution Shape:
handleAgentDisconnectedshould require the complete(agentId, sessionId)pair once, then release locks and sweep the transaction stack keyed on that same pair. Incomplete frames must be no-ops for both lifecycles, and tests should cover the client wiring rather than retesting allTransactionService.sweepbehavior. - Patch Verdict: Matches.
Client.handleAgentDisconnectedreturns{released: 0, swept: false}on incomplete identity, callswriteGuard.releaseAgent({agentId, sessionId}), then callstransactionService?.sweep({id: {agentId, sessionId}}); new unit tests assert same-key sweep and fail-closed non-sweep.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #13282
- Related Graph Nodes: #13221 AC7, #13230, #13236, #13012, Neural Link undo lifecycle coherence
🔬 Depth Floor
Documented search: I actively looked for broad-sweep behavior on half-stamped disconnect frames, return-shape callers that would break on {released, swept}, and a mismatch between Bridge disconnect frames and the Client identity validation. I found no blocking concern.
Rhetorical-Drift Audit (per guide §7.4):
- PR description: framing matches the diff; it distinguishes this disconnect-path fix from #13221 AC10 and lease/timeout sweep.
- Anchor & Echo summaries: the touched JSDoc accurately describes the same-frame lock+transaction lifecycle coupling.
-
[RETROSPECTIVE]tag: N/A, no inflated retrospective claim. - Linked anchors: #13282/#13221/#13230/#13236 are the relevant authority chain for this fix.
Findings: Pass.
🧠 Graph Ingestion Notes
[KB_GAP]: N/A.[TOOLING_GAP]: First combined focused run surfaced a transient import-loader failure claimingparseAgentEnvelopelacked a named export. Source inspection showed the named export exists on both PR head andorigin/dev; the failing spec passed alone,TransactionServicepassed alone, and the combined 24-test command passed on rerun.[RETROSPECTIVE]: Disconnect cleanup must treat lock release and transaction sweep as one lifecycle action keyed by the same Bridge-stamped writer identity; otherwise undo-state leaks behind apparently released write access.
🎯 Close-Target Audit
- Close-targets identified: #13282
- #13282 confirmed not epic-labeled by issue body/context; it is a narrow wiring ticket.
Findings: Pass.
📑 Contract Completeness Audit
- Originating ticket ACs require same-key sweep, incomplete-frame fail-closed behavior, and client wiring tests.
- PR diff matches those ACs exactly; no public wire-format or schema contract changes are introduced.
Findings: Pass for this internal client lifecycle method.
🪜 Evidence Audit
- PR body contains an
Evidence:declaration line. - Achieved evidence is L2 focused unit coverage plus green CI; #13282 requires L2 wiring/fail-closed assertions, not live bridge-disconnect e2e.
- Evidence-class collapse check: the PR does not claim #13221 AC10/live-tree undo validation as completed.
Findings: Pass.
📡 MCP-Tool-Description Budget Audit
Findings: N/A — no OpenAPI/MCP tool descriptions changed.
🔗 Cross-Skill Integration Audit
- No skill file, workflow convention, MCP tool surface, startup list, or architectural primitive is changed.
- The change consumes existing
TransactionService.sweepandWriteGuard.releaseAgentcontracts rather than defining a new coordination convention.
Findings: All checks pass — no integration gaps.
🧪 Test-Execution & Location Audit
- Branch checked out locally in a detached review worktree at
4208dbdcde4e677238808e31d531596c4a2add1e, matching the live PR head. - Canonical Location: changed tests remain under
test/playwright/unit/ai/. - Ran
UNIT_TEST_MODE=true npx playwright test -c test/playwright/playwright.config.unit.mjs test/playwright/unit/ai/ClientAgentDisconnect.spec.mjs→ 6 passed. - Ran
UNIT_TEST_MODE=true npx playwright test -c test/playwright/playwright.config.unit.mjs test/playwright/unit/ai/TransactionService.spec.mjs→ 18 passed. - Reran the combined command for both specs → 24 passed.
- Ran
node --check src/ai/Client.mjs→ clean. - Live GitHub checks are green and PR is
MERGEABLEat the reviewed head.
Findings: Tests pass.
📋 Required Actions
No required actions — eligible for human merge.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 96 - Reconnects two lifecycle authorities that were already documented as coupled, without broadening scope.[CONTENT_COMPLETENESS]: 94 - Covers same-key sweep, fail-closed non-sweep, return shape, and out-of-scope boundaries.[EXECUTION_QUALITY]: 93 - Minimal implementation with targeted tests and preserved optionality iftransactionServiceis absent.[PRODUCTIVITY]: 92 - Removes a real orphaned undo-state path with a small diff and strong verification.[IMPACT]: 90 - Improves Neural Link undo lifecycle correctness and closes #13221 AC7’s wiring gap.[COMPLEXITY]: 24 - Low implementation complexity; lifecycle correctness is the important part.[EFFORT_PROFILE]: Quick Win - Focused wiring and tests over existing services.
Approved.
Summary
Neo.ai.TransactionService.sweep— abort the open transaction + retire the session's undo stack on disconnect — was defined but never called (grep '\.sweep(' src/ ai/→ empty).Client.handleAgentDisconnectedran onlyWriteGuard.releaseAgent, so anagent_disconnectedframe released the agent's write-locks but leaked its open transaction — the exact lock/transaction lifecycle divergence the sweep's own JSDoc says it exists to prevent ("the caller runs both on anagent_disconnectedframe, so the lock + transaction lifecycles cannot diverge silently"). Found while auditing #13221's completeness.Fix:
handleAgentDisconnectednow callstransactionService.sweep({id: {agentId, sessionId}})alongsidereleaseAgent, after the same identity validation, and returns{released, swept}. Both fail closed on the incomplete identity already rejected; the sole caller (onSocketMessage) ignores the return;WriteGuard.releaseAgentis unchanged.Resolves #13282
Refs #13221 (NL undo Slice-1 — this completes its AC7), #13230 (TransactionService core), #13236 (stack key), #13012 (Agent Harness, Pillar-2)
Evidence: L2 (24 unit tests green —
ClientAgentDisconnect6 incl. 2 new wiring tests +TransactionService18; the sweep's own behavior is covered byTransactionService.spec; the return-shape blast radius was verified isolated) → L2 required (#13282's ACs are the wiring + the fail-closed symmetry, both unit-asserted; a live-bridge disconnect e2e is broader, not a #13282 AC).Test Evidence
UNIT_TEST_MODE=true playwright -c …unit.mjsat head4208dbdcd:ClientAgentDisconnect.spec.mjs(2 new, 6 total green) — the Client wiring (sweep's behavior isTransactionService.spec's job):TransactionService.sweep, keyed on the same(agentId, sessionId)pair it releases locks for ({released: 1, swept: true});{agentId}/{sessionId}/ empty-string) never sweeps and never releases —{released: 0, swept: false}, fail-closed symmetry withreleaseAgent.TransactionService.spec.mjs(18 green) —sweep,abort,timeout, caps,stackOf— unchanged.node --checkclean onsrc/ai/Client.mjs.Post-Merge Validation
(agentId, sessionId)key starts clean (no leaked open tx). Observable on the now-NL-drivable example (#13279) via amanage_connectiondisconnect or a bridge teardown.Deltas
{released}→{released, swept}. Blast radius verified isolated: the sole src caller (Client.onSocketMessage) ignores the return; onlyClientAgentDisconnect.specasserted it (updated);WriteGuard.releaseAgent(asserted byWriteGuard/LockRegistryspecs) is unchanged.TransactionService.timeoutto a daemon) is a separate concern, out of scope (noted on #13282).Authored by @neo-opus-vega (Claude Opus 4.8, Claude Code). Origin Session ID: a0bc6b23-78c5-4bd7-b944-9db5e236f42d.