Summary
src/index.ts registers a process.on('exit', ...) handler that calls shutdownBifrost() — an async function that terminates the Bifrost Go child process. However, the 'exit' event is synchronous; the Node.js event loop is already torn down, meaning the returned Promise is never awaited and the SIGTERM sent inside shutdownBifrost() is never delivered. The Bifrost subprocess continues running as an orphaned daemon after Probus exits.
Panel verdict: P0, VERIFIED, EXISTING_DEFECT, unanimous consensus.
Affected Files
| File |
Lines |
Issue |
src/index.ts |
L24 |
void shutdownBifrost() inside synchronous 'exit' handler |
src/bifrost.ts |
L125–L133 |
shutdownBifrost() is async — calls await p internally |
Root Cause — Code Evidence
src/index.ts, line 24:
process.on('exit', () => { void shutdownBifrost(); });
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 'exit' fires synchronously; the Promise returned by shutdownBifrost()
// is created but the event loop is gone — the async body never executes.
src/bifrost.ts, lines 125–133:
export async function shutdownBifrost(): Promise<void> {
const p = bifrostPromise;
bifrostPromise = null;
if (!p) return;
try {
const h = await p; // ← this 'await' never resumes in 'exit' context
h.close(); // ← h.close() / SIGTERM are never called
} catch { /* ignore */ }
}
Contrast with the SIGINT/SIGTERM handler (lines 19–23) — which does work:
const shutdown = () => {
shutdownBifrost().catch(() => {}).finally(() => process.exit(0));
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// ↑ Correct: the Promise chain runs before process.exit(0) is called.
// The 'exit' handler below is broken because the event loop is already dead.
Impact
- Bifrost (a Go HTTP proxy binary) keeps running with the developer's OpenAI API key in memory after Probus exits.
- If Probus is restarted the
getFreePort() call may re-use the same port the orphan is listening on, causing a EADDRINUSE error or silent proxy routing to the stale process.
- Over multiple restart cycles, multiple orphan Bifrost processes accumulate, consuming ports and memory.
Steps to Reproduce
- Start Probus with an OpenAI provider:
OPENAI_API_KEY=sk-... npm run dev
- Initiate a scan so Bifrost is spawned.
- Kill Probus with
Ctrl+C (SIGINT) or kill <pid> (SIGTERM).
- After Probus exits, run:
ps aux | grep bifrost
- Observe the Bifrost process is still alive.
Remediation
Option A (preferred) — Store the child handle and kill synchronously:
In src/bifrost.ts, expose the child process handle:
let _child: ChildProcess | null = null;
// inside the spawn block:
_child = child;
export function shutdownBifrostSync(): void {
if (_child) {
try { _child.kill('SIGKILL'); } catch { /* ignore */ }
_child = null;
}
bifrostPromise = null;
}
In src/index.ts, replace the async call with the sync version:
// Remove:
process.on('exit', () => { void shutdownBifrost(); });
// Add:
process.on('exit', () => { shutdownBifrostSync(); });
Option B — Remove the 'exit' handler entirely:
The SIGINT/SIGTERM handlers already call process.exit(0) inside a proper async chain. The synchronous 'exit' handler is redundant and broken — simply delete line 24.
References
Summary
src/index.tsregisters aprocess.on('exit', ...)handler that callsshutdownBifrost()— an async function that terminates the Bifrost Go child process. However, the'exit'event is synchronous; the Node.js event loop is already torn down, meaning the returned Promise is never awaited and theSIGTERMsent insideshutdownBifrost()is never delivered. The Bifrost subprocess continues running as an orphaned daemon after Probus exits.Panel verdict: P0, VERIFIED, EXISTING_DEFECT, unanimous consensus.
Affected Files
src/index.tsvoid shutdownBifrost()inside synchronous'exit'handlersrc/bifrost.tsshutdownBifrost()is async — callsawait pinternallyRoot Cause — Code Evidence
src/index.ts, line 24:src/bifrost.ts, lines 125–133:Contrast with the SIGINT/SIGTERM handler (lines 19–23) — which does work:
Impact
getFreePort()call may re-use the same port the orphan is listening on, causing aEADDRINUSEerror or silent proxy routing to the stale process.Steps to Reproduce
OPENAI_API_KEY=sk-... npm run devCtrl+C(SIGINT) orkill <pid>(SIGTERM).ps aux | grep bifrostRemediation
Option A (preferred) — Store the child handle and kill synchronously:
In
src/bifrost.ts, expose the child process handle:In
src/index.ts, replace the async call with the sync version:Option B — Remove the 'exit' handler entirely:
The SIGINT/SIGTERM handlers already call
process.exit(0)inside a proper async chain. The synchronous'exit'handler is redundant and broken — simply delete line 24.References