LearnNewsExamplesServices
Frontmatter
id16429
titleNeural Link bridge spawn uses the wrong cwd, and its failure is fatal
stateClosed
labels
bugaiarchitecture
assigneesneo-opus-grace
createdAtAug 3, 2026, 10:45 AM
updatedAtAug 12, 2026, 9:00 AM
githubUrlhttps://github.com/neomjs/neo/issues/16429
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 12, 2026, 9:00 AM

Neural Link bridge spawn uses the wrong cwd, and its failure is fatal

Closed Backlog/active-chunk-12 bugaiarchitecture
neo-opus-ada
neo-opus-ada commented on Aug 3, 2026, 10:45 AM

Context

Observed 2026-08-03 on the one-machine local Docker Agent OS, after a host restart left the Neural Link Bridge unstarted. The neo-mjs-neural-link MCP server connected its transport, answered tools/list, then exited entirely ~2s later:

[INFO] Neural Link MCP Server transport connected
... method="tools/list" id=1 → result
[WARN] Disconnected from Neural Link Bridge
Error: connect ECONNREFUSED 127.0.0.1:8081
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1705:16)
node:internal/process/promises:332  triggerUncaughtException(err, true /* fromPromise */);
[error] Server disconnected.

The seat lost every Neural Link tool for the whole session. Observation and inference are separated below.

The Problem

ConnectionService already has a self-heal path: ensureBridgeAndConnect() connects to an existing Bridge, and spawns one via npm run ai:server-neural-link when that fails. It ran, and the spawn failed. From the bridge stdio log (<planeDataRoot>/logs/neural-link-bridge-stdio.log), stamped inside the crash window (npm debug logs 08:26:09.175Z, 08:26:09.413Z):

npm error path /package.json
npm error enoent Could not read package.json: ENOENT: no such file or directory, open '/package.json'

The spawn ran with cwd = the filesystem root. spawnBridge() resolves cwd: this.cwd || process.cwd(); a GUI-launched MCP server has process.cwd() === '/', so npm run finds no package.json and the Bridge never starts.

--cwd is supplied by the seat config, and Server.mjs does assign it — but the singleton is constructed at module import, so initAsync()'s auto-connect can begin before that assignment executes. Two consequences fall out of the same race:

  1. spawnBridge() reads a still-null this.cwd and silently substitutes /.
  2. The resulting rejection is not inside the try/catch that was written to keep the server alive — so the process dies instead of degrading.

Measured, not inferred: the Bridge binds 498 ms after npm run ai:server-neural-link, comfortably inside spawnBridge()'s fixed 2000 ms wait. Startup latency is not the cause.

Note also that this.cwd || process.cwd() is a hidden-default fallback: it silently substitutes a wrong value where it should fail loudly.

The Architectural Reality

  • ai/services/neural-link/ConnectionService.mjs:740spawnBridge(), cwd: this.cwd || process.cwd(), plus a setTimeout(resolve, 2000) that ignores its own startupDelayMs parameter.
  • ai/services/neural-link/ConnectionService.mjs:215-221initAsync() calls ensureBridgeAndConnect() when !Neo.config.unitTestMode && aiConfig.autoConnect.
  • ai/services/neural-link/ConnectionService.mjs:150-176, :799singleton: true, export default Neo.setupClass(ConnectionService): constructed at import.
  • ai/services/neural-link/ConnectionService.mjs:370-375ws.on('close') rejects; // Optional: Auto-reconnect logic could go here.
  • ai/mcp/server/neural-link/Server.mjs:183-191 — assigns ConnectionService.cwd = this.bridgeCwd, then await ConnectionService.ready(), inside a try/catch whose comment reads "Do not throw — server stays alive to report health errors via MCP healthcheck."
  • ai/mcp/server/neural-link/mcp-server.mjs:23, :44-w, --cwd <path>bridgeCwd.

The ordering inside start() is correct. The defect is that import-time construction can outrun it.

The Fix

Both changes live in ai/services/neural-link/ + ai/mcp/server/neural-link/:

  1. Resolve the working directory instead of racing it. The Bridge spawn must read the entrypoint-provided cwd. An unresolved cwd must fail loudly with a named error rather than falling back to process.cwd().
  2. A missing Bridge must be survivable. The auto-connect rejection must be handled on the path that actually runs it, so the server stays alive and reports the failure through healthcheck — the behaviour Server.mjs:190 already declares but does not currently get.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
ConnectionService.cwd (ConnectionService.mjs:160) NL MCP entrypoint --cwd Resolved before any spawn can run None — fail loudly; remove the || process.cwd() substitution JSDoc on the member bridge stdio log ENOENT above
spawnBridge({startupDelayMs}) (:740) same module Honor the parameter, or replace the fixed wait with a readiness probe n/a JSDoc measured 498 ms bind
NL healthcheck tool ai/mcp/server/neural-link/Server.mjs Reports bridge-unreachable while the server stays alive n/a openapi.yaml crash log: today the server exits instead

Decision Record impact

aligned-with ADR 0019 — this is not an AiConfig leaf, but this.cwd || process.cwd() is exactly the hidden-default-fallback pattern its §3 catalog forbids, and the fix should not introduce one.

Acceptance Criteria

  • With no Bridge running and --cwd supplied, starting the NL MCP server yields a running Bridge and a connected agent — witnessed from a live run, not inferred.
  • A Bridge spawn with an unresolved working directory fails with a named error; process.cwd() is no longer a silent fallback.
  • With the Bridge unspawnable (e.g. deliberately broken script path) the MCP server stays alive and healthcheck names the failure; the process does not exit.
  • Regression witness that fails RED against today's code: an unreachable 127.0.0.1:8081 must not terminate the server process.
  • spawnBridge() either honors startupDelayMs or replaces the fixed wait with a readiness probe. (Contract correctness — measured bind is 498 ms, so this is not a latency fix.)

Out of Scope

  • Who owns/starts the Bridge in the containerized topology — ai/deploy/hostEdgeProfile.mjs:113 explicitly un-elects NEO_ORCHESTRATOR_NL_BRIDGE_ENABLED. That is a topology decision, not this defect.
  • Per-seat / per-worktree Bridge port allocation for multiple peers on one host.
  • The dev-server lane (NEO_ORCHESTRATOR_DEV_SERVER_ENABLED).

Avoided Traps

  • Do not "fix" this by widening the 2 s spawn delay. Measured: the Bridge binds in 498 ms. Treating a wrong-cwd failure as a timing problem buries it.
  • Do not swallow the rejection silently. The server must stay alive and surface the fault via healthcheck; a bare catch recreates the catch-22 where an agent cannot observe the degradation it is subject to.
  • Do not assume --cwd was missing. It is supplied and assigned; the bug is ordering, so a config-plumbing "fix" would leave the race intact.

Related

  • #16008 — Neural Link Bridge stdio must fail without the injected logPath (CLOSED). Same spawnBridge() function; adjacent hardening precedent.
  • #14124 / #12972 — the embed-canary catch-22. Same "must stay observable while degraded" principle, applied here to the NL server's own liveness.

Origin Session ID: 6fbb7047-4b3f-4842-af7d-0aa5949dc392

Retrieval Hint: "Neural Link Bridge spawn cwd filesystem root ENOENT package.json" Retrieval Hint: "ConnectionService singleton import-time initAsync beats bridgeCwd assignment"

Live latest-open sweep: checked the latest 20 open issues at 2026-08-03T08:43:35Z; A2A in-flight claim sweep over 30 messages (all read-states) at the same time. No equivalent found.

tobiu referenced in commit a9ae5fd - "fix(neural-link): an unresolved bridge cwd fails loudly instead of spawning at / (#16429) (#16983) on Aug 12, 2026, 9:00 AM
tobiu closed this issue on Aug 12, 2026, 9:00 AM