From d09a4f8ae041536dc7d37a384971058248d7b995 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sun, 28 Jun 2026 12:31:18 +0900 Subject: fix(ssh,host-bin): permanent pooled-client error listener + uncaughtException/unhandledRejection guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the live production crash (exit-1 'Timed out while waiting for handshake'): the pooled ssh2.Client had no permanent 'error' listener after connect, so a post-connect ssh2 error escaped as an uncaught EventEmitter 'error' with no process-level guard. See notes/crash-investigation-findings.md ยง1. - packages/ssh/src/pool.ts: attach a permanent 'error' listener to the pooled client in buildConnection that sets state=error, logs (alias, message, level), and does not throw; cleanup() no longer removes it. - packages/host-bin/src/main.ts: add process.on('uncaughtException') (graceful shutdown after logging) and process.on('unhandledRejection') (log + continue), both logging message/stack, memory snapshot, activeConversations count, and timestamp so the failure site is observable. --- packages/host-bin/src/main.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) (limited to 'packages/host-bin/src') diff --git a/packages/host-bin/src/main.ts b/packages/host-bin/src/main.ts index 29e402e..70d1cb2 100644 --- a/packages/host-bin/src/main.ts +++ b/packages/host-bin/src/main.ts @@ -31,6 +31,7 @@ import { extension as providerOpenaiCompatExt } from "@dispatch/provider-openai- import { extension as providerUmansExt } from "@dispatch/provider-umans"; import { type MemorySample, + memorySampleAttributes, extension as sessionOrchestratorExt, sessionOrchestratorHandle, } from "@dispatch/session-orchestrator"; @@ -243,8 +244,10 @@ async function boot(): Promise { // owns the PERIODIC baseline. All effects are injected (no ambient state); // stop() is cleared on shutdown so timers never leak across a restart. let memoryTelemetry: { stop: () => void } | undefined; + let activeConvCountFn: (() => number) | undefined; try { const orchestrator = host.getHostAPI().getService(sessionOrchestratorHandle); + activeConvCountFn = () => orchestrator.getActiveConversationCount(); memoryTelemetry = startMemoryTelemetry({ logger: logger.child({ extensionId: "mem-telemetry" }), sampleMemory: (): MemorySample => { @@ -280,6 +283,38 @@ async function boot(): Promise { process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); + const memorySnapshot = () => { + const m = process.memoryUsage(); + return memorySampleAttributes({ + rss: m.rss, + heapUsed: m.heapUsed, + heapTotal: m.heapTotal, + external: m.external, + arrayBuffers: m.arrayBuffers, + }); + }; + + process.on("unhandledRejection", (reason) => { + logger.error("unhandledRejection", { + err: reason, + handler: "unhandledRejection", + activeConversations: activeConvCountFn?.() ?? "unavailable", + timestamp: new Date().toISOString(), + ...memorySnapshot(), + }); + }); + + process.on("uncaughtException", (err) => { + logger.error("uncaughtException", { + err, + handler: "uncaughtException", + activeConversations: activeConvCountFn?.() ?? "unavailable", + timestamp: new Date().toISOString(), + ...memorySnapshot(), + }); + void shutdown(); + }); + logger.info("Dispatch booted"); console.info("Dispatch booted"); } -- cgit v1.2.3