From 0a8d9b7440caf13d80a99da74751bc63556672a4 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:51:41 -0800 Subject: [PATCH] fix: add missing timeout to Daytona runServer/runServerCapture (#1945) Daytona was the only cloud provider without process timeouts in runServer() and runServerCapture(). All other providers (AWS, Fly, Hetzner, DigitalOcean, GCP) implement setTimeout + killWithTimeout to prevent the CLI from hanging forever on stalled remote commands. This adds the same timeout pattern: default 300s, configurable via the timeoutSecs parameter that the CloudRunner interface already declares but Daytona was silently ignoring. Agent: code-health Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 --- packages/cli/src/daytona/daytona.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/daytona/daytona.ts b/packages/cli/src/daytona/daytona.ts index 44c16569..aef3c0de 100644 --- a/packages/cli/src/daytona/daytona.ts +++ b/packages/cli/src/daytona/daytona.ts @@ -20,7 +20,7 @@ import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../sh import { parseJsonWith, parseJsonRaw, isString, toObjectArray, toRecord } from "@openrouter/spawn-shared"; import * as v from "valibot"; import { saveVmConnection } from "../history.js"; -import { sleep, spawnInteractive } from "../shared/ssh"; +import { sleep, spawnInteractive, killWithTimeout } from "../shared/ssh"; const DAYTONA_API_BASE = "https://app.daytona.io/api"; const DAYTONA_DASHBOARD_URL = "https://app.daytona.io/"; @@ -395,7 +395,7 @@ export async function createServer(name: string, sandboxSize?: SandboxSize): Pro * Run a command on the remote sandbox via SSH. * Adds a brief sleep after each call to let Daytona's gateway release the connection slot. */ -export async function runServer(cmd: string): Promise { +export async function runServer(cmd: string, timeoutSecs?: number): Promise { const fullCmd = `export PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH" && ${cmd}`; const args = [ ...sshBaseArgs(), @@ -419,7 +419,10 @@ export async function runServer(cmd: string): Promise { } catch { /* already closed */ } + const timeout = (timeoutSecs || 300) * 1000; + const timer = setTimeout(() => killWithTimeout(proc), timeout); const exitCode = await proc.exited; + clearTimeout(timer); // Brief sleep to let gateway release connection slot await sleep(1000); @@ -430,7 +433,7 @@ export async function runServer(cmd: string): Promise { } /** Run a command and capture stdout. */ -export async function runServerCapture(cmd: string): Promise { +export async function runServerCapture(cmd: string, timeoutSecs?: number): Promise { const fullCmd = `export PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH" && ${cmd}`; const args = [ ...sshBaseArgs(), @@ -453,12 +456,15 @@ export async function runServerCapture(cmd: string): Promise { } catch { /* already closed */ } + const timeout = (timeoutSecs || 300) * 1000; + const timer = setTimeout(() => killWithTimeout(proc), timeout); // Drain both pipes before awaiting exit to prevent pipe buffer deadlock const [stdout] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), ]); const exitCode = await proc.exited; + clearTimeout(timer); await sleep(1000);