@@ -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