Summary
The Brain Console can remain on an old in-memory server indefinitely after the installed Console files have been updated.
Closing and reopening Codex or Claude Code does not correct it because the Console is deliberately launched as a background process. Once its host/tool process exits, the long-running Node server is reparented and is no longer owned by the next host session. The current launcher then probes only GET / and the text RuvNet Brain; any old Console that can render the page is treated as current and reused.
This produced a mixed-generation Console on a supported dual-host install:
- the browser received the current UI assets from disk;
- the Node process still had its pre-update API router in memory;
- the UI called
/api/capabilities;
- the old server returned 404;
- invoking the Console again printed “already running” and reopened the same incompatible process;
- restarting Codex did not replace it.
There is a second activation gap: npx ruvnet-brain --update updates the bundle and host shells, but returns through runUpdate() before the full-install path calls installConsoleRuntime(). The persistent .console-runtime is therefore not part of that update transaction.
The Console needs a versioned, owned runtime-instance contract. A host restart is neither a safe nor a sufficient process supervisor.
Exact evidence
Verified on 2026-08-01 against public main at 12f31ce1bbfb785938573e350a09dad8da0542f0 and the supported installed runtime.
Mixed-generation process
The listener was:
pid 38770
ppid 1
started 2026-07-18 10:15:50
listen 127.0.0.1:7411
command node ~/.claude/plugins/marketplaces/ruvnet-brain/scripts/onboarding-console.mjs --serve --open
The current marketplace and persistent Console runtime files were byte-identical and contained the /api/capabilities handler:
9080fb98cc52c084ec9a3c66f96f2aef22e860cac7160d0e4fd9ca2e02bfb2fe
~/.claude/plugins/marketplaces/ruvnet-brain/scripts/onboarding-console.mjs
~/.cache/ruvnet-brain/kb/.console-runtime/scripts/onboarding-console.mjs
But the live listener returned:
GET /api/capabilities HTTP/1.1
HTTP/1.1 404 Not Found
content-type: text/plain
not found
This is not an on-disk installation mystery. Node loaded the old module and route table on 18 July; replacing the file later did not hot-reload the running process.
The result is especially deceptive because serveStatic() reads the frontend files from disk per request. The old process can therefore serve the new console/app.js while retaining its old in-memory HTTP handlers: new page, old server, one apparently healthy port.
The existing-server check cannot distinguish generations
Current scripts/onboarding-console.mjs (lines 2707-2725 on the observed build) does this:
const req = http.get({ host: '127.0.0.1', port, path: '/', timeout: 800 }, (res) => {
let b = '';
res.on('data', (c) => { b += c; if (b.length > 4096) res.destroy(); });
res.on('end', () => resolve(res.statusCode === 200 && /RuvNet Brain/.test(b)));
});
If that weak probe passes, it prints “already running” and opens the old URL. There is no API-contract version, runtime/source digest, process receipt, PID ownership proof, or graceful shutdown channel.
The native Claude command and Codex skill both explicitly launch the Console in the background and state that an already-running server is success. That is why its lifetime is independent of the host session.
The supported update path does not refresh the persistent Console runtime
bin/install.mjs has a correct atomic installConsoleRuntime(cacheDir) implementation, but it is reached only in the full install path near line 3946.
The top-level dispatcher sends --update to runUpdate(). That function:
- updates the bundle;
- runs
syncHostsAfterUpdate() for Claude/Codex and the Stable Spine;
- exits with
process.exit(updateStatus).
It never calls installConsoleRuntime(). Therefore an update can converge hooks/MCP/host payloads while the official runtime used by /rvbc and $ruvnet-brain:rvbc stays on an older generation.
Root cause
There are four connected failures:
- Unowned lifetime assumption — the Console is a background service, but it has no explicit instance ownership/lifecycle contract.
- Host-restart assumption — Codex/Claude restart does not and should not be assumed to reap an independently running user-level server, especially when several sessions may coexist.
- Weak compatibility probe —
GET / plus a product-name regex proves branding, not API or source compatibility.
- Incomplete update transaction — the persistent Console runtime is outside
runUpdate() / host convergence, so current host code does not imply current Console code.
Required upstream implementation
The durable solution should stay user-level and host-neutral.
1. Put the persistent Console runtime in the update transaction
The same exact candidate that activates the bundle, Stable Spine, Claude plugin, and Codex plugin must atomically stage and verify .console-runtime.
- Stage it from the exact candidate source.
- Gate its syntax and required files before activation.
- Record its version and source digest in the convergence receipt.
- Preserve/restore the previous runtime on failure.
- Do not report host convergence while the persistent Console runtime is still from another candidate.
This is the Console-specific completion of the broader release invariant in #77.
2. Give each Console instance a verifiable identity
On successful bind, atomically write a mode-0600 instance receipt under ~/.cache/ruvnet-brain containing at least:
- product/schema identifier;
- PID;
- bound port;
- start time;
- project cwd/scope;
- canonical script realpath;
- Brain/runtime version;
- source or build SHA-256;
- a random instance/control token.
Expose a small read-only endpoint such as GET /api/runtime returning the non-secret identity fields. The endpoint must be cheap and must not depend on the heavy Console caches.
3. Reuse only an exact compatible instance
Before printing “already running,” the launcher must compare the running instance with the script it is about to execute.
Compatible means, at minimum:
- expected product and runtime schema;
- required API-contract version;
- exact current source/build digest;
- appropriate project scope if Console state is project-scoped.
A root page title is not compatibility evidence.
4. Replace only a receipt-proven owned stale instance
If a listener is a recognized Brain Console but its generation/digest is stale:
- authenticate a graceful shutdown with the receipt’s control token;
- wait for the owned listener to release the port;
- start the current runtime;
- atomically replace the receipt.
If the listener has no valid receipt, is a legacy Console, or is a foreign service, never kill it merely because it owns port 7411. Start the current Console on a free port, record that instance, and surface the legacy/foreign listener plainly. Subsequent launches should consult the receipt and reuse that current instance instead of spawning another random-port server.
On update, either restart a receipt-proven running Console safely or persist pending-console-restart and make the next Console launch perform the replacement. Do not claim that restarting Codex or Claude is sufficient.
5. Make doctor observe the running generation
--doctor should compare:
- candidate/persistent Console runtime version and digest;
- instance receipt;
- live
/api/runtime response;
- PID/liveness;
- required API contract.
It should report current, stale-running, legacy-unowned, foreign-port, or pending-console-restart rather than treating a branded root page as healthy.
Acceptance criteria
Relationship to #77
#77 is the release-identity transaction: one immutable candidate across GitHub, npm, bundle, Stable Spine, both host plugins, and Console. This issue is the narrower long-running-process activation contract after bytes have changed, plus the concrete omission of .console-runtime from runUpdate().
Both need to hold:
Non-goals
- Do not couple Console lifetime to one Codex or Claude session; multiple sessions may coexist.
- Do not add a per-project Brain installation.
- Do not kill arbitrary listeners by port/PID heuristics.
- Do not make a downstream updater, cache mirror, pin, or version-specific entrypoint.
- Do not treat “the browser page loaded” as proof that its API server matches it.
Summary
The Brain Console can remain on an old in-memory server indefinitely after the installed Console files have been updated.
Closing and reopening Codex or Claude Code does not correct it because the Console is deliberately launched as a background process. Once its host/tool process exits, the long-running Node server is reparented and is no longer owned by the next host session. The current launcher then probes only
GET /and the textRuvNet Brain; any old Console that can render the page is treated as current and reused.This produced a mixed-generation Console on a supported dual-host install:
/api/capabilities;There is a second activation gap:
npx ruvnet-brain --updateupdates the bundle and host shells, but returns throughrunUpdate()before the full-install path callsinstallConsoleRuntime(). The persistent.console-runtimeis therefore not part of that update transaction.The Console needs a versioned, owned runtime-instance contract. A host restart is neither a safe nor a sufficient process supervisor.
Exact evidence
Verified on 2026-08-01 against public
mainat12f31ce1bbfb785938573e350a09dad8da0542f0and the supported installed runtime.Mixed-generation process
The listener was:
The current marketplace and persistent Console runtime files were byte-identical and contained the
/api/capabilitieshandler:But the live listener returned:
This is not an on-disk installation mystery. Node loaded the old module and route table on 18 July; replacing the file later did not hot-reload the running process.
The result is especially deceptive because
serveStatic()reads the frontend files from disk per request. The old process can therefore serve the newconsole/app.jswhile retaining its old in-memory HTTP handlers: new page, old server, one apparently healthy port.The existing-server check cannot distinguish generations
Current
scripts/onboarding-console.mjs(lines 2707-2725 on the observed build) does this:If that weak probe passes, it prints “already running” and opens the old URL. There is no API-contract version, runtime/source digest, process receipt, PID ownership proof, or graceful shutdown channel.
The native Claude command and Codex skill both explicitly launch the Console in the background and state that an already-running server is success. That is why its lifetime is independent of the host session.
The supported update path does not refresh the persistent Console runtime
bin/install.mjshas a correct atomicinstallConsoleRuntime(cacheDir)implementation, but it is reached only in the full install path near line 3946.The top-level dispatcher sends
--updatetorunUpdate(). That function:syncHostsAfterUpdate()for Claude/Codex and the Stable Spine;process.exit(updateStatus).It never calls
installConsoleRuntime(). Therefore an update can converge hooks/MCP/host payloads while the official runtime used by/rvbcand$ruvnet-brain:rvbcstays on an older generation.Root cause
There are four connected failures:
GET /plus a product-name regex proves branding, not API or source compatibility.runUpdate()/ host convergence, so current host code does not imply current Console code.Required upstream implementation
The durable solution should stay user-level and host-neutral.
1. Put the persistent Console runtime in the update transaction
The same exact candidate that activates the bundle, Stable Spine, Claude plugin, and Codex plugin must atomically stage and verify
.console-runtime.This is the Console-specific completion of the broader release invariant in #77.
2. Give each Console instance a verifiable identity
On successful bind, atomically write a mode-0600 instance receipt under
~/.cache/ruvnet-braincontaining at least:Expose a small read-only endpoint such as
GET /api/runtimereturning the non-secret identity fields. The endpoint must be cheap and must not depend on the heavy Console caches.3. Reuse only an exact compatible instance
Before printing “already running,” the launcher must compare the running instance with the script it is about to execute.
Compatible means, at minimum:
A root page title is not compatibility evidence.
4. Replace only a receipt-proven owned stale instance
If a listener is a recognized Brain Console but its generation/digest is stale:
If the listener has no valid receipt, is a legacy Console, or is a foreign service, never kill it merely because it owns port 7411. Start the current Console on a free port, record that instance, and surface the legacy/foreign listener plainly. Subsequent launches should consult the receipt and reuse that current instance instead of spawning another random-port server.
On update, either restart a receipt-proven running Console safely or persist
pending-console-restartand make the next Console launch perform the replacement. Do not claim that restarting Codex or Claude is sufficient.5. Make doctor observe the running generation
--doctorshould compare:/api/runtimeresponse;It should report
current,stale-running,legacy-unowned,foreign-port, orpending-console-restartrather than treating a branded root page as healthy.Acceptance criteria
npx ruvnet-brain --updatestages and verifies.console-runtimefrom the same exact candidate as the bundle and host payloads.GET /api/capabilities -> 404is included.Relationship to #77
#77 is the release-identity transaction: one immutable candidate across GitHub, npm, bundle, Stable Spine, both host plugins, and Console. This issue is the narrower long-running-process activation contract after bytes have changed, plus the concrete omission of
.console-runtimefromrunUpdate().Both need to hold:
Non-goals