Context
Leaf of #13056 (Extended-NL coordination), implementing the auto-targeting deprecation the epic names under its Intended-solution shape: "explicit target becomes mandatory when more than one session is live." The threshold is inlined here (decoupled from #13125 — see the Implementation Note); single-sourcing it through #13125's LockRegistry.requiresExplicitTarget is a named post-merge follow-up.
Release classification: boardless — harness coordination line (board 13), post-v13, not release-gating.
The Problem
ConnectionService.call() (ai/services/neural-link/ConnectionService.mjs:103-112) silently auto-targets the most recent session when no sessionId is passed:
if (!sessionId) {
if (this.sessionData.size > 0) {
sessionId = Array.from(this.sessionData.keys()).pop();
logger.warn(`No sessionId provided. Defaulting to ${sessionId}`);
} else {
throw new Error('No active App Worker sessions found.');
}
}This is the #13056 audit finding #2 — correct for a single writer, a silent cross-writer footgun once two agents share the Bridge: a call meant for session A can land on whichever session connected last.
Scope (this leaf)
- Add a standalone pure
resolveCallTarget(sessionId, liveSessionIds) module (ai/services/neural-link/resolveCallTarget.mjs):
- explicit
sessionId given → return it unchanged (always honored);
- none + zero live sessions → throw the existing "No active App Worker sessions found." (behavior preserved);
- none + more than one live session → throw an explicit-target-required error, using an inline
liveSessionIds.length > 1 threshold;
- none + exactly one live session → return that session (back-compatible implicit path).
call() delegates its target resolution to resolveCallTarget(sessionId, [...this.sessionData.keys()]) — the only behavioral change is that the multi-session implicit case now throws instead of guessing.
Out of Scope (named follow-up slices)
- In-heap lock enforcement (intercepting write-class ops against the lock table) — the larger #13056 enforcement slice.
- Bridge-side advisory mirroring; the NL-mutation→
MESSAGE observability glue.
- Threading a harness-native canonical session id (#12984) — consumed opaquely.
- Single-sourcing the
> 1 threshold through #13125's LockRegistry.requiresExplicitTarget — a post-merge follow-up once LockRegistry lands on dev.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Evidence |
resolveCallTarget (standalone module) |
this leaf (new) |
explicit honored; 0 live → no-sessions throw; >1 live → explicit-target-required throw; 1 live → implicit |
single-session implicit path unchanged (back-compat) |
unit: 0/1/N sessions + explicit-given |
ConnectionService.call() target resolution |
#13056 audit finding #2 |
delegates to resolveCallTarget; multi-session implicit now throws |
single/zero-session callers unaffected |
unit (pure resolver) + e2e residual |
| explicit-target threshold |
this leaf (inline liveSessionIds.length > 1) |
>1 ⇒ explicit required |
n/a |
unit: 1 implicit, 2+ explicit-required |
Implementation Note (truth-sync per #13130 cross-family review)
The shipped PR (#13130) diverges from this ticket's first draft in two deliberate, documented ways:
- Standalone pure module, not a method on the singleton. Importing the Bridge-connected
ConnectionService singleton in a unit test auto-connects (and in CI, with no Bridge, would spawn one). Extracting the resolver into its own side-effect-free module makes it testable without waking the Bridge.
- Inline
> 1 threshold, not LockRegistry.requiresExplicitTarget. LockRegistry (#13125 / PR #13126) is not yet on dev, so importing it would make this PR un-mergeable independently. The threshold is inlined; single-sourcing against the predicate is listed above as a post-merge follow-up.
Decision Record impact
aligned-with ADR 0020 (the co-habitation / multi-writer guardrail). No ADR amendment.
Acceptance Criteria
Related
Parent: #13056. Refs #13012, #13125 (related — threshold single-sourcing is a post-merge follow-up, not a build dependency), ai/services/neural-link/ConnectionService.mjs:103.
Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e
Retrieval Hint: "neural link auto-targeting deprecation explicit target resolveCallTarget standalone module inline threshold multiple sessions"
Context
Leaf of #13056 (Extended-NL coordination), implementing the auto-targeting deprecation the epic names under its Intended-solution shape: "explicit target becomes mandatory when more than one session is live." The threshold is inlined here (decoupled from #13125 — see the Implementation Note); single-sourcing it through #13125's
LockRegistry.requiresExplicitTargetis a named post-merge follow-up.Release classification:boardless — harness coordination line (board 13), post-v13, not release-gating.The Problem
ConnectionService.call()(ai/services/neural-link/ConnectionService.mjs:103-112) silently auto-targets the most recent session when nosessionIdis passed:if (!sessionId) { if (this.sessionData.size > 0) { sessionId = Array.from(this.sessionData.keys()).pop(); // most-recent logger.warn(`No sessionId provided. Defaulting to ${sessionId}`); } else { throw new Error('No active App Worker sessions found.'); } }This is the #13056 audit finding #2 — correct for a single writer, a silent cross-writer footgun once two agents share the Bridge: a call meant for session A can land on whichever session connected last.
Scope (this leaf)
resolveCallTarget(sessionId, liveSessionIds)module (ai/services/neural-link/resolveCallTarget.mjs):sessionIdgiven → return it unchanged (always honored);liveSessionIds.length > 1threshold;call()delegates its target resolution toresolveCallTarget(sessionId, [...this.sessionData.keys()])— the only behavioral change is that the multi-session implicit case now throws instead of guessing.Out of Scope (named follow-up slices)
MESSAGEobservability glue.> 1threshold through #13125'sLockRegistry.requiresExplicitTarget— a post-merge follow-up once LockRegistry lands ondev.Contract Ledger
resolveCallTarget(standalone module)ConnectionService.call()target resolutionresolveCallTarget; multi-session implicit now throwsliveSessionIds.length > 1)Implementation Note (truth-sync per #13130 cross-family review)
The shipped PR (#13130) diverges from this ticket's first draft in two deliberate, documented ways:
ConnectionServicesingleton in a unit test auto-connects (and in CI, with no Bridge, would spawn one). Extracting the resolver into its own side-effect-free module makes it testable without waking the Bridge.> 1threshold, notLockRegistry.requiresExplicitTarget.LockRegistry(#13125 / PR #13126) is not yet ondev, so importing it would make this PR un-mergeable independently. The threshold is inlined; single-sourcing against the predicate is listed above as a post-merge follow-up.Decision Record impact
aligned-with ADR 0020 (the co-habitation / multi-writer guardrail). No ADR amendment.
Acceptance Criteria
resolveCallTargetis a standalone pure module, unit-tested across explicit-given / 0 / 1 / many live sessions, with an inline> 1threshold (no Bridge import in the test).call()delegates to it; a single live session still resolves implicitly (back-compat), zero still throws the existing message.Related
Parent: #13056. Refs #13012, #13125 (related — threshold single-sourcing is a post-merge follow-up, not a build dependency),
ai/services/neural-link/ConnectionService.mjs:103.Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e Retrieval Hint: "neural link auto-targeting deprecation explicit target resolveCallTarget standalone module inline threshold multiple sessions"