From fc6741e8525096af9455d4813f58c51996db84bb Mon Sep 17 00:00:00 2001 From: Joe Lombrozo Date: Thu, 13 Nov 2025 18:05:14 -0800 Subject: [PATCH 1/2] only retry on network level errors, not http errors --- packages/cli/src/terminal.ts | 54 +++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/terminal.ts b/packages/cli/src/terminal.ts index 713314e638..bd4f5706bd 100644 --- a/packages/cli/src/terminal.ts +++ b/packages/cli/src/terminal.ts @@ -9,6 +9,37 @@ function getStdoutSize() { } } +function isRetryableError(err: unknown): boolean { + // Retry on SDK TimeoutError + if (err instanceof (e2b as any).TimeoutError) return true + + // Some environments throw AbortError for aborted/timeout fetches + if (err && typeof err === 'object' && (err as any).name === 'AbortError') return true + + // Network/system-level transient errors commonly exposed via code property + const code = (err as any)?.code ?? (err as any)?.cause?.code + const retryableCodes = new Set([ + 'ECONNRESET', + 'ECONNREFUSED', + 'ECONNABORTED', + 'EPIPE', + 'ETIMEDOUT', + 'ENOTFOUND', + 'EAI_AGAIN', + 'EHOSTUNREACH', + 'EADDRINUSE', + ]) + if (typeof code === 'string' && retryableCodes.has(code)) return true + + // Undici/Fetch may surface as TypeError: fetch failed with nested cause + if ((err as any) instanceof TypeError) { + const msg = String((err as any).message || '').toLowerCase() + if (msg.includes('fetch failed') || msg.includes('network error')) return true + } + + return false +} + export async function spawnConnectedTerminal(sandbox: e2b.Sandbox) { // Clear local terminal emulator before starting terminal // process.stdout.write('\x1b[2J\x1b[0f') @@ -26,11 +57,25 @@ export async function spawnConnectedTerminal(sandbox: e2b.Sandbox) { const inputQueue = new BatchedQueue(async (batch) => { const combined = Buffer.concat(batch) - await sandbox.pty.sendInput(terminalSession.pid, combined) + + const maxRetries = 3 + let retry = 0 + do { + try { + await sandbox.pty.sendInput(terminalSession.pid, combined) + break + } catch (err) { + if (!isRetryableError(err)) { + // Do not retry on errors that come with valid HTTP/gRPC responses + throw err + } + retry++ + } + } while (retry < maxRetries) }, FLUSH_INPUT_INTERVAL_MS) const resizeListener = process.stdout.on('resize', () => - sandbox.pty.resize(terminalSession.pid, getStdoutSize()) + sandbox.pty.resize(terminalSession.pid, getStdoutSize()), ) const stdinListener = process.stdin.on('data', (data) => { inputQueue.push(data) @@ -69,8 +114,9 @@ class BatchedQueue { constructor( private flushHandler: (batch: T[]) => Promise, - private flushIntervalMs: number - ) {} + private flushIntervalMs: number, + ) { + } push(item: T) { this.queue.push(item) From 66ff8e03ffc279b52a4295ca7ec2f324f4f7d51b Mon Sep 17 00:00:00 2001 From: Joe Lombrozo Date: Thu, 13 Nov 2025 18:10:34 -0800 Subject: [PATCH 2/2] linting --- packages/cli/src/terminal.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/terminal.ts b/packages/cli/src/terminal.ts index bd4f5706bd..7baf06046d 100644 --- a/packages/cli/src/terminal.ts +++ b/packages/cli/src/terminal.ts @@ -14,7 +14,8 @@ function isRetryableError(err: unknown): boolean { if (err instanceof (e2b as any).TimeoutError) return true // Some environments throw AbortError for aborted/timeout fetches - if (err && typeof err === 'object' && (err as any).name === 'AbortError') return true + if (err && typeof err === 'object' && (err as any).name === 'AbortError') + return true // Network/system-level transient errors commonly exposed via code property const code = (err as any)?.code ?? (err as any)?.cause?.code @@ -34,7 +35,8 @@ function isRetryableError(err: unknown): boolean { // Undici/Fetch may surface as TypeError: fetch failed with nested cause if ((err as any) instanceof TypeError) { const msg = String((err as any).message || '').toLowerCase() - if (msg.includes('fetch failed') || msg.includes('network error')) return true + if (msg.includes('fetch failed') || msg.includes('network error')) + return true } return false @@ -75,7 +77,7 @@ export async function spawnConnectedTerminal(sandbox: e2b.Sandbox) { }, FLUSH_INPUT_INTERVAL_MS) const resizeListener = process.stdout.on('resize', () => - sandbox.pty.resize(terminalSession.pid, getStdoutSize()), + sandbox.pty.resize(terminalSession.pid, getStdoutSize()) ) const stdinListener = process.stdin.on('data', (data) => { inputQueue.push(data) @@ -114,9 +116,8 @@ class BatchedQueue { constructor( private flushHandler: (batch: T[]) => Promise, - private flushIntervalMs: number, - ) { - } + private flushIntervalMs: number + ) {} push(item: T) { this.queue.push(item)