LearnNewsExamplesServices
Frontmatter
id13236
titleAlign TransactionService stack key to the (agentId, sessionId) writer pair
stateClosed
labels
aiarchitecture
assigneesneo-opus-vega
createdAtJun 14, 2026, 4:57 PM
updatedAtJun 14, 2026, 6:41 PM
githubUrlhttps://github.com/neomjs/neo/issues/13236
authorneo-opus-vega
commentsCount0
parentIssue13221
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 14, 2026, 6:41 PM

Align TransactionService stack key to the (agentId, sessionId) writer pair

neo-opus-vega
neo-opus-vega commented on Jun 14, 2026, 4:57 PM

Context

Wiring the #13221 Slice-1 capture-hook onto the merged #13231 TransactionService core (src/ai/TransactionService.mjs) surfaced an integration gap — the pure-core-then-wiring pattern validating the core against the live model. The merged core's per-session stack key is the 3-tuple {neuralLinkSessionId, requesterAgentId, requesterSessionId}, but neuralLinkSessionId has no source in the live Neural Link flow.

The Problem

The capture-hook (and sweep / undo / begin …) must construct the stack-key id from the live request context. But the live writer identity is the 2-tuple (agentId, sessionId), V-B-A'd from source on dev:

  • parseAgentEnvelope yields context: {agentId, sessionId} — nothing more.
  • Bridge.mjs:217-220 mints ws.sessionId per agent connection "so the app-side write-guard can key locks on the (agentId, sessionId) pair" — the writer identity is explicitly the 2-tuple.
  • WriteGuard.sameWriter keys on exactly (agentId, sessionId).
  • The agent_disconnected frame (Bridge.mjs:233-235) carries only {agentId, sessionId} — so TransactionService.sweep (the tx-side counterpart to WriteGuard.releaseAgent) has only the 2-tuple to reconcile on.

So neuralLinkSessionId was over-design: there is no live NL-session id distinct from the (agentId, sessionId) writer (one Client/heap = one NL connection; the per-heap TransactionService instance already is the NL-session scope). The 3-tuple key cannot be populated from the live flow — blocking the capture-hook.

The Fix

Align the TransactionService stack key to the (agentId, sessionId) 2-tuple, matching WriteGuard + the agent_disconnected frame — so the paired lock+tx lifecycles key identically and sweep maps straight from the disconnect frame.

  • stackKey({agentId, sessionId}) → fail-closed null on either field missing.
  • Update every lifecycle method id param + JSDoc (begin / record / commit / undo / abort / timeout / sweep / stackOf).
  • Update the unit spec fixtures (ID / ID2) + assertions to the 2-tuple (per-writer isolation still asserted on (agentId, sessionId)).
  • Reconcile the #13221 Contract Ledger row.

Acceptance Criteria

  • TransactionService stack key is {agentId, sessionId}; stackKey fails closed (null) on either field missing.
  • All lifecycle methods + JSDoc reflect the 2-tuple id.
  • The unit spec passes with 2-tuple fixtures; per-writer isolation asserted on (agentId, sessionId).
  • The #13221 Contract Ledger TransactionService row reads the 2-tuple key.

Out of Scope

  • The capture-hook + undo tool wiring (the rest of #13221).
  • Multi-NL-session keying — a future slice may add a session-scoping field with real semantics; YAGNI for Slice-1 (same-session).

Avoided Traps

  • Mapping neuralLinkSessionId = appWorkerId to keep the 3-tuple — rejected: threads an appWorkerId mapping at every call + sweep site for a distinction Slice-1 doesn't need; the per-heap instance already scopes the NL session. Add a session-scoping field with real semantics if/when a multi-NL-session slice needs it.

Related

  • Parent: #13221 (NL mutation undo Slice-1). Corrects the merged core #13230 (PR #13231). Writer model: WriteGuard / parseAgentEnvelope / Bridge.mjs (#13167). Surfaced by the #13221 wiring finding (issue comment).

Release classification: post-release (Agent Harness Pillar-2; not v13-release-blocking) — boardless.

Authored by @neo-opus-vega (Claude Opus 4.8, Claude Code). Origin Session ID: 4cc428e3-cf36-4324-8646-1b96cb23fa4a.