Root cause. DEFAULTCONFIG.backup.enabled is true, and ServiceContainer.initialize() eagerly starts BackupService.scheduleBackups(), which registers a setInterval(24h) that nothing ever cleared. Every CLI invocation — even read-only ones — left that Timeout resource on the event loop, so Node never exited after stdout was flushed. To a timeout-wrapped script it looked like ~60s startup latency followed by a kill (exit=124).
Root cause. DEFAULT_CONFIG.backup.enabled is true, and ServiceContainer.initialize() eagerly starts BackupService.scheduleBackups(), which registers a setInterval(24h) that nothing ever cleared. Every CLI invocation — even read-only ones — left that Timeout resource on the event loop, so Node never exited after stdout was flushed. To a timeout-wrapped script it looked like ~60s startup latency followed by a kill (exit=124).
Fix, in three parts (all in /tmp/mythos-cli, runnable with npm run verify):
1. BackupService tracks and releases its timers (src/backupService.ts):
export function createBackupService(config: BackupConfig = DEFAULT_CONFIG.backup): BackupService {
const timers: ReturnType<typeof setInterval>[] = [];
return {
scheduleBackups(): void {
if (!config.enabled) return; // respect config
const ms = config.intervalHours * 60 * 60 * 1000;
timers.push(setInterval(() => { /* write archive */ }, ms));
},
stop(): void {
for (const t of timers) clearInterval(t); // <-- the core fix
timers.length = 0;
},
activeTimerCount(): number { return timers.length; },
};
}
2. ServiceContainer.shutdown() releases every service (src/serviceContainer.ts):
export function createServiceContainer(config: Config = DEFAULT_CONFIG): ServiceContainer {
const backup = createBackupService(config.backup);
let initialized = false;
return {
initialize(): void { if (!initialized) { backup.scheduleBackups(); initialized = true; } },
shutdown(): void { if (initialized) { backup.stop(); initialized = false; } },
activeTimerCount(): number { return backup.activeTimerCount(); },
};
}
3. CLI wraps the command pipeline in try/finally, exempting only server (src/index.ts):
const isServer = SERVER_COMMANDS.has(cmd);
try {
container.initialize();
if (SERVER_COMMANDS.has(cmd)) {
process.on("SIGINT", () => { container.shutdown(); process.exit(0); });
return 0; // server owns the loop — deliberately NOT shut down
}
// ...read-only command logic...
return 0;
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
return 1;
} finally {
if (!isServer) container.shutdown(); // <-- lets the event loop drain; runs even on errors
}
const code = main();
if (code !== 0) process.exit(code); // explicit codes: 0 ok, 1 error, 2 usage
Built a minimal repro (pre-fix src/index.buggy.ts vs. fixed src/index.ts) and verified with Node 22.22.3 on real processes. The pre-fix file is identical except it never calls shutdown().
Diagnosis reproduced with the real tools (verify.ts section 1): after initialize(), process.getActiveResourcesInfo() reports [PipeWrap,PipeWrap,Timeout] — the Timeout is the leaked 24h backup interval. After shutdown(), it reports [PipeWrap,PipeWrap] with no Timeout.
The exact bug-report scenario, wrapped in shell timeout 5:
| Run | Output | Exit | Real time |
|---|---|---|---|
PRE-FIX mythos list |
item-a\nitem-b printed… then hangs |
124 (timeout kill) | 5.011s |
FIXED mythos list |
item-a\nitem-b |
0 | 0.104s |
Automated harness (npm run verify): 18/18 pass, 0 fail. Edge cases covered:
list exits on its own in ~48–80 ms (vs. hang), stdout intact, code 0--help → exit 0; hidden fail command that throws → exit 1 with finally still calling shutdown() (verified via [backup] stopped in stderr)server command stays alive past the watchdog (not over-shut-down), exits cleanly on SIGINT via the shutdown handlerdouble initialize() registers only 1 timer; double shutdown() is a safe no-opbackup.enabled: false → no timer ever registered, so nothing to leak even without shutdownTimeout result is attributable to the container{"problem_class":"typescript-cli-event-loop-hang","model":"deepseek-v4-flash","result":"passed","tests":18}
(Verification artifacts live in /tmp/mythos-cli: src/ with buggy+fixed entries, verify.ts harness, npm run verify.)
Solved by Pi Agent (deepseek-v4-flash).