From 68c3239c39acb9d0690a45bc6bc409c7f96519d5 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+Gricha@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:15:27 +0000 Subject: [PATCH 1/2] Fix agent shutdown hanging on Ctrl+C --- src/agent/run.ts | 24 +++++++++++++++++++++++- src/docker/eager-pull.ts | 24 +++++++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/agent/run.ts b/src/agent/run.ts index 5dd9f3c1..dffe3b23 100644 --- a/src/agent/run.ts +++ b/src/agent/run.ts @@ -6,7 +6,7 @@ import { HOST_WORKSPACE_NAME } from '../shared/client-types'; import { DEFAULT_AGENT_PORT } from '../shared/constants'; import { WorkspaceManager } from '../workspace/manager'; import { containerRunning, getContainerName } from '../docker'; -import { startEagerImagePull } from '../docker/eager-pull'; +import { startEagerImagePull, stopEagerImagePull } from '../docker/eager-pull'; import { TerminalWebSocketServer } from '../terminal/websocket'; import { ChatWebSocketServer } from '../chat/websocket'; import { OpencodeWebSocketServer } from '../chat/opencode-websocket'; @@ -297,17 +297,39 @@ export async function startAgent(options: StartAgentOptions = {}): Promise startEagerImagePull(); }); + let isShuttingDown = false; + const shutdown = async () => { + if (isShuttingDown) { + console.log('[agent] Force exit'); + process.exit(0); + } + isShuttingDown = true; + console.log('[agent] Shutting down...'); + + stopEagerImagePull(); fileWatcher.stop(); + if (tailscaleServeActive) { console.log('[agent] Stopping Tailscale Serve...'); await stopTailscaleServe(); } + chatServer.close(); opencodeServer.close(); terminalServer.close(); + + server.closeAllConnections(); + + const forceExitTimeout = setTimeout(() => { + console.log('[agent] Force exit after timeout'); + process.exit(0); + }, 3000); + forceExitTimeout.unref(); + server.close(() => { + clearTimeout(forceExitTimeout); console.log('[agent] Server closed'); process.exit(0); }); diff --git a/src/docker/eager-pull.ts b/src/docker/eager-pull.ts index f3839e0c..91bc3032 100644 --- a/src/docker/eager-pull.ts +++ b/src/docker/eager-pull.ts @@ -7,6 +7,7 @@ const MAX_RETRIES = 10; let pullInProgress = false; let pullComplete = false; +let abortController: AbortController | null = null; async function isDockerAvailable(): Promise { try { @@ -44,8 +45,15 @@ export async function startEagerImagePull(): Promise { } pullInProgress = true; + abortController = new AbortController(); + const signal = abortController.signal; const attemptPull = async (attempt: number): Promise => { + if (signal.aborted) { + pullInProgress = false; + return; + } + if (attempt > MAX_RETRIES) { console.log('[agent] Max retries reached for image pull - giving up background pull'); pullInProgress = false; @@ -58,7 +66,8 @@ export async function startEagerImagePull(): Promise { if (attempt === 1) { console.log('[agent] Docker not available - will retry in background'); } - setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS); + const timer = setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS); + timer.unref(); return; } @@ -67,14 +76,23 @@ export async function startEagerImagePull(): Promise { if (success) { pullComplete = true; pullInProgress = false; - } else { - setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS); + } else if (!signal.aborted) { + const timer = setTimeout(() => attemptPull(attempt + 1), RETRY_INTERVAL_MS); + timer.unref(); } }; attemptPull(1); } +export function stopEagerImagePull(): void { + if (abortController) { + abortController.abort(); + abortController = null; + } + pullInProgress = false; +} + export function isImagePullComplete(): boolean { return pullComplete; } From 766134daa0f0ca2f4ec6a24ff566cfb9cdc85884 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+Gricha@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:52:22 +0000 Subject: [PATCH 2/2] Move force-exit timeout before async operations --- src/agent/run.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/agent/run.ts b/src/agent/run.ts index dffe3b23..5fd53ecc 100644 --- a/src/agent/run.ts +++ b/src/agent/run.ts @@ -308,6 +308,12 @@ export async function startAgent(options: StartAgentOptions = {}): Promise console.log('[agent] Shutting down...'); + const forceExitTimeout = setTimeout(() => { + console.log('[agent] Force exit after timeout'); + process.exit(0); + }, 3000); + forceExitTimeout.unref(); + stopEagerImagePull(); fileWatcher.stop(); @@ -322,12 +328,6 @@ export async function startAgent(options: StartAgentOptions = {}): Promise server.closeAllConnections(); - const forceExitTimeout = setTimeout(() => { - console.log('[agent] Force exit after timeout'); - process.exit(0); - }, 3000); - forceExitTimeout.unref(); - server.close(() => { clearTimeout(forceExitTimeout); console.log('[agent] Server closed');