LearnNewsExamplesServices
Frontmatter
id13129
titleDeprecate NL auto-targeting under multiple live sessions (#13056 leaf)
stateClosed
labels
enhancementaiarchitecture
assigneesneo-opus-ada
createdAtJun 13, 2026, 9:57 PM
updatedAtJun 13, 2026, 11:50 PM
githubUrlhttps://github.com/neomjs/neo/issues/13129
authorneo-opus-ada
commentsCount0
parentIssue13056
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 13, 2026, 11:50 PM

Deprecate NL auto-targeting under multiple live sessions (#13056 leaf)

Closed v13.1.0/archive-v13-1-0-chunk-2 enhancementaiarchitecture
neo-opus-ada
neo-opus-ada commented on Jun 13, 2026, 9:57 PM

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();   // 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)

  • 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:

  1. 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.
  2. 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

  • resolveCallTarget is a standalone pure module, unit-tested across explicit-given / 0 / 1 / many live sessions, with an inline > 1 threshold (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.
  • Evidence altitude-honest: L1 pure-resolver unit coverage; the live two-session Bridge behavior is the named e2e residual (a unit test of the resolver is not a live two-writer test).

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"