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
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.
Context
Wiring the #13221 Slice-1 capture-hook onto the merged #13231
TransactionServicecore (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}, butneuralLinkSessionIdhas no source in the live Neural Link flow.The Problem
The capture-hook (and
sweep/undo/begin…) must construct the stack-keyidfrom the live request context. But the live writer identity is the 2-tuple(agentId, sessionId), V-B-A'd from source ondev:parseAgentEnvelopeyieldscontext: {agentId, sessionId}— nothing more.Bridge.mjs:217-220mintsws.sessionIdper 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.sameWriterkeys on exactly(agentId, sessionId).agent_disconnectedframe (Bridge.mjs:233-235) carries only{agentId, sessionId}— soTransactionService.sweep(the tx-side counterpart toWriteGuard.releaseAgent) has only the 2-tuple to reconcile on.So
neuralLinkSessionIdwas over-design: there is no live NL-session id distinct from the(agentId, sessionId)writer (oneClient/heap = one NL connection; the per-heapTransactionServiceinstance 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
TransactionServicestack key to the(agentId, sessionId)2-tuple, matchingWriteGuard+ theagent_disconnectedframe — so the paired lock+tx lifecycles key identically andsweepmaps straight from the disconnect frame.stackKey({agentId, sessionId})→ fail-closednullon either field missing.idparam + JSDoc (begin/record/commit/undo/abort/timeout/sweep/stackOf).ID/ID2) + assertions to the 2-tuple (per-writer isolation still asserted on(agentId, sessionId)).Acceptance Criteria
TransactionServicestack key is{agentId, sessionId};stackKeyfails closed (null) on either field missing.id.(agentId, sessionId).TransactionServicerow reads the 2-tuple key.Out of Scope
undotool wiring (the rest of #13221).Avoided Traps
neuralLinkSessionId = appWorkerIdto keep the 3-tuple — rejected: threads anappWorkerIdmapping at every call +sweepsite 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
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.