Skip to content

Commit 5f72af9

Browse files
bidahclaude
andcommitted
🐛 fix(sandbox): fix E2B deadline_exceeded timeout errors during agent execution
Set explicit gRPC deadline matching sandbox lifetime instead of timeoutMs: 0, which was silently converted to undefined by Connect transport, causing E2B's server to apply its own shorter default. Also improve error recovery for deadline timeouts with user-friendly messages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1f86204 commit 5f72af9

1 file changed

Lines changed: 36 additions & 5 deletions

File tree

‎apps/web/lib/claude-code-service.ts‎

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,19 @@ export class ClaudeCodeService {
254254

255255
// Use background: true to avoid E2B's internal timeout on foreground commands
256256
// Background mode returns a command handle that can stream output and wait for completion
257+
// NOTE: We use a large explicit timeoutMs (30 min) instead of 0 because:
258+
// - timeoutMs: 0 is converted to undefined by the Connect transport, sending no grpc-timeout header
259+
// - Without a client-specified deadline, E2B's envd server applies its own shorter default
260+
// - A large explicit value sets a server-side gRPC deadline long enough for complex agent tasks
261+
const sandboxTimeoutMs = parseInt(process.env.E2B_SANDBOX_TIMEOUT_MS || '1800000', 10)
257262
const commandHandle = await sandbox.commands.run(
258263
command,
259264
{
260265
background: true as const,
261266
envs: {
262267
ANTHROPIC_API_KEY: globalThis.process.env.ANTHROPIC_API_KEY || '',
263268
},
264-
timeoutMs: 0, // No timeout - let it run as long as needed
269+
timeoutMs: sandboxTimeoutMs, // Match sandbox lifetime to avoid premature gRPC deadline
265270
onStdout: (data: string) => {
266271
stdoutChunkCount++
267272
receivedAnyOutput = true
@@ -453,19 +458,45 @@ export class ClaudeCodeService {
453458
// Handle execution failure
454459
// Note: With spawn() we no longer hit the 120-second timeout issue that run() had
455460
if (executionError) {
456-
console.error('[Claude Code Service] Execution error detected:', executionError.message)
461+
const isDeadlineExceeded = executionError.message.includes('deadline_exceeded') ||
462+
executionError.message.includes('the operation timed out') ||
463+
executionError.message.includes('timeoutMs')
464+
465+
console.error('[Claude Code Service] Execution error detected:', {
466+
message: executionError.message,
467+
isDeadlineExceeded,
468+
receivedAnyOutput,
469+
completionDetected,
470+
stdoutChunkCount,
471+
})
457472

458473
// If we received output (content was already streamed to frontend), don't treat
459-
// transient E2B errors like "[unknown] terminated" as fatal — the agent likely
460-
// finished or nearly finished. Treat as success with whatever we got.
461-
if (receivedAnyOutput && (completionDetected || stdoutChunkCount > 5)) {
474+
// transient E2B errors like "[unknown] terminated" or deadline timeouts as fatal —
475+
// the agent likely finished or nearly finished. Treat as success with whatever we got.
476+
// For deadline_exceeded specifically, even a single chunk of output means the agent
477+
// was working — the E2B server just terminated the gRPC stream.
478+
const canRecoverFromError = receivedAnyOutput && (
479+
completionDetected ||
480+
stdoutChunkCount > 5 ||
481+
(isDeadlineExceeded && stdoutChunkCount > 0)
482+
)
483+
484+
if (canRecoverFromError) {
462485
console.log('[Claude Code Service] Execution error after receiving output — treating as completion', {
463486
receivedAnyOutput,
464487
completionDetected,
465488
stdoutChunkCount,
489+
isDeadlineExceeded,
466490
error: executionError.message,
467491
})
468492
// Fall through to success path below
493+
} else if (isDeadlineExceeded) {
494+
// Provide a user-friendly message instead of the raw E2B SDK error
495+
await callbacks.onError(
496+
'The AI agent took too long to respond. This can happen with complex requests. ' +
497+
'Please try again — the agent will resume from where it left off if you send the same message.'
498+
)
499+
return
469500
} else {
470501
await callbacks.onError(executionError.message)
471502
return

0 commit comments

Comments
 (0)