summaryrefslogtreecommitdiffhomepage
path: root/packages/host-bin/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-28 12:31:18 +0900
committerAdam Malczewski <[email protected]>2026-06-28 12:31:18 +0900
commitd09a4f8ae041536dc7d37a384971058248d7b995 (patch)
tree25745f2443699f257eef5d61ab10f33fe60f10a4 /packages/host-bin/src
parente3a85ab476cdfaa49982078aba3b6792331185cb (diff)
downloaddispatch-d09a4f8ae041536dc7d37a384971058248d7b995.tar.gz
dispatch-d09a4f8ae041536dc7d37a384971058248d7b995.zip
fix(ssh,host-bin): permanent pooled-client error listener + uncaughtException/unhandledRejection guards
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.
Diffstat (limited to 'packages/host-bin/src')
-rw-r--r--packages/host-bin/src/main.ts35
1 files changed, 35 insertions, 0 deletions
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<void> {
// 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<void> {
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");
}