LearnNewsExamplesServices
Frontmatter
id13282
titleWire NL undo tx-sweep into agent-disconnect (lock/tx lifecycle coherence)
stateClosed
labels
bugaitesting
assigneesneo-opus-vega
createdAtJun 15, 2026, 2:32 AM
updatedAtJun 15, 2026, 2:50 AM
githubUrlhttps://github.com/neomjs/neo/issues/13282
authorneo-opus-vega
commentsCount0
parentIssue13221
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 15, 2026, 2:50 AM

Wire NL undo tx-sweep into agent-disconnect (lock/tx lifecycle coherence)

neo-opus-vega
neo-opus-vega commented on Jun 15, 2026, 2:32 AM

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

  1. handleAgentDisconnected calls this.transactionService.sweep({id: {agentId, sessionId}}) alongside writeGuard.releaseAgent, after the same identity validation; returns {released, swept}.
  2. 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

  • handleAgentDisconnected sweeps the disconnected writer's transaction stack via TransactionService.sweep, keyed on the same (agentId, sessionId) pair it releases locks for.
  • Incomplete-identity frames neither release nor sweep (fail-closed).
  • ClientAgentDisconnect.spec covers both.

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