@@ -106,7 +106,10 @@ const MATERIALIZER_REVISION_RADIX = 1000
106106/** Public provider alias used by builder/task tests and release tooling. */
107107export const E2B_SANDBOX_MATERIALIZER_REVISION = FUNCTION_SANDBOX_MATERIALIZER_REVISION
108108
109- /** Maximum continuous sandbox lifetime supported by E2B. */
109+ /**
110+ * E2B's continuous-runtime cap on the Pro tier: a running sandbox is killed this long after it
111+ * last started or resumed, however long its timeout says, and a pause plus resume resets the count.
112+ */
110113export const E2B_MAX_SANDBOX_LIFETIME_MS = 24 * 60 * 60 * 1000
111114
112115/** E2B sends sandbox lifetimes as whole seconds. */
@@ -164,14 +167,17 @@ function prepareE2BCommand(
164167 }
165168}
166169
170+ /** Only a command whose own timeout would outlive the cap can have been ended by it. */
167171function reachedE2BProviderLimit (
168172 error : unknown ,
169- providerLimitAtMs : number | undefined ,
173+ providerLimitAtMs : number ,
174+ commandDeadlineAtMs : number | undefined ,
170175 signal ?: AbortSignal
171176) : boolean {
172177 return (
173178 ! signal ?. aborted &&
174- providerLimitAtMs !== undefined &&
179+ commandDeadlineAtMs !== undefined &&
180+ commandDeadlineAtMs >= providerLimitAtMs &&
175181 Date . now ( ) >= providerLimitAtMs - E2B_PROVIDER_LIMIT_CLASSIFICATION_WINDOW_MS &&
176182 isE2BExecutionTimeout ( error )
177183 )
@@ -358,19 +364,30 @@ export async function stopE2BSessionProcess(
358364class E2BSandboxHandle implements SandboxHandle {
359365 private killed = false
360366 private killPromise : Promise < void > | null = null
367+ private sessionDeadlineAtMs ?: number
361368
362369 /**
370+ * @param providerLimitAtMs No later than E2B's continuous-runtime cap for this sandbox, which no
371+ * timeout request can extend past.
363372 * @param sessionDeadlineAtMs Earliest time the provider can reap this session sandbox, as set
364- * by this handle's own create, connect, or timeout request. Session deadlines only ever move
365- * later — every update path extends and none shortens — so it stays a valid lower bound.
373+ * by this handle's own create, connect, or timeout request and never past the cap. Session
374+ * deadlines only ever move later — every update path extends and none shortens — so it stays
375+ * a valid lower bound.
366376 */
367377 constructor (
368378 private readonly sandbox : E2BSandbox ,
369379 private readonly language : CodeLanguage ,
370- private readonly providerLimitAtMs ? : number ,
380+ private readonly providerLimitAtMs : number ,
371381 private readonly sessionKey ?: string ,
372- private sessionDeadlineAtMs ?: number
373- ) { }
382+ sessionDeadlineAtMs ?: number
383+ ) {
384+ if ( sessionDeadlineAtMs !== undefined ) this . recordSessionDeadline ( sessionDeadlineAtMs )
385+ }
386+
387+ /** E2B clamps every timeout to the cap, so no granted lease reaches past it. */
388+ private recordSessionDeadline ( atMs : number ) : void {
389+ this . sessionDeadlineAtMs = Math . min ( atMs , this . providerLimitAtMs )
390+ }
374391
375392 get sandboxId ( ) : string {
376393 return this . sandbox . sandboxId
@@ -393,7 +410,7 @@ class E2BSandboxHandle implements SandboxHandle {
393410 }
394411 const requestedAtMs = Date . now ( )
395412 await this . sandbox . setTimeout ( timeoutMs )
396- if ( this . sessionKey !== undefined ) this . sessionDeadlineAtMs = requestedAtMs + timeoutMs
413+ if ( this . sessionKey !== undefined ) this . recordSessionDeadline ( requestedAtMs + timeoutMs )
397414 }
398415
399416 async runCode (
@@ -484,6 +501,7 @@ class E2BSandboxHandle implements SandboxHandle {
484501 operation : 'code' | 'command'
485502 ) : Promise < SandboxCommandResult > {
486503 if ( this . sessionKey !== undefined ) options . signal ?. throwIfAborted ( )
504+ let commandDeadlineAtMs : number | undefined
487505 const outputBudget = new SandboxProcessOutputBudget (
488506 options . maxOutputBytes ?? MAX_SANDBOX_PROCESS_OUTPUT_BYTES
489507 )
@@ -553,6 +571,8 @@ class E2BSandboxHandle implements SandboxHandle {
553571 }
554572 let started : Awaited < ReturnType < E2BSandbox [ 'commands' ] [ 'run' ] > >
555573 try {
574+ /** E2B starts the process timeout at dispatch, after the ownership write above. */
575+ commandDeadlineAtMs = Date . now ( ) + processOptions . timeoutMs
556576 started = await this . sandbox . commands . run (
557577 processId
558578 ? sessionProcessCommand ( processId , prepared . command , options . rootUser )
@@ -662,7 +682,9 @@ class E2BSandboxHandle implements SandboxHandle {
662682 if ( outputBudget . error ) throw outputBudget . error
663683 if ( isSandboxOutputLimitError ( error ) ) throw error
664684 if ( isNonRetryableExecutionError ( error ) ) throw error
665- if ( reachedE2BProviderLimit ( error , this . providerLimitAtMs , options . signal ) ) {
685+ if (
686+ reachedE2BProviderLimit ( error , this . providerLimitAtMs , commandDeadlineAtMs , options . signal )
687+ ) {
666688 recordSandboxProviderLimit ( { provider : 'e2b' , operation } )
667689 return {
668690 stdout : '' ,
@@ -1085,9 +1107,7 @@ export const e2bProvider: SandboxProvider = {
10851107 return new E2BSandboxHandle (
10861108 sandbox ,
10871109 options ?. language ?? CodeLanguage . Python ,
1088- effectiveLifetimeMs === E2B_MAX_SANDBOX_LIFETIME_MS
1089- ? lifetimeStartedAtMs + E2B_MAX_SANDBOX_LIFETIME_MS
1090- : undefined ,
1110+ lifetimeStartedAtMs + E2B_MAX_SANDBOX_LIFETIME_MS ,
10911111 options ?. sessionKey ,
10921112 options ?. sessionKey && effectiveLifetimeMs !== undefined
10931113 ? lifetimeStartedAtMs + effectiveLifetimeMs
@@ -1116,19 +1136,38 @@ export const e2bProvider: SandboxProvider = {
11161136 'This workbench predates durable execution ownership and requires recovery before reuse'
11171137 )
11181138 }
1139+ const leaseMs = options . lifetimeMs === undefined ? 0 : e2bTimeoutMs ( options . lifetimeMs )
1140+ let resuming = candidate . state === 'paused'
1141+ // E2B silently clamps any timeout to its continuous-runtime cap. A pause plus resume restarts
1142+ // that count with memory, files, and processes intact, where a new workbench would lose them.
1143+ if (
1144+ ! resuming &&
1145+ Date . now ( ) + leaseMs > candidate . startedAt . getTime ( ) + E2B_MAX_SANDBOX_LIFETIME_MS
1146+ ) {
1147+ try {
1148+ await Sandbox . pause ( candidate . sandboxId , { apiKey } )
1149+ resuming = true
1150+ } catch ( error ) {
1151+ logger . warn (
1152+ 'Failed to pause workbench to reset its runtime cap; reusing it until the cap' ,
1153+ {
1154+ sandboxId : candidate . sandboxId ,
1155+ error : getErrorMessage ( error ) ,
1156+ }
1157+ )
1158+ }
1159+ }
11191160 // Connect also sets a timeout, including for running sandboxes. Preserve the active deadline,
11201161 // and grant the requested lease in the same request instead of a later getInfo + setTimeout.
11211162 const requestedAtMs = Date . now ( )
1122- const timeoutMs = Math . max (
1123- 5 * 60_000 ,
1124- candidate . endAt . getTime ( ) - requestedAtMs ,
1125- options . lifetimeMs === undefined ? 0 : e2bTimeoutMs ( options . lifetimeMs )
1126- )
1163+ const timeoutMs = Math . max ( 5 * 60_000 , candidate . endAt . getTime ( ) - requestedAtMs , leaseMs )
1164+ const providerLimitAtMs =
1165+ ( resuming ? requestedAtMs : candidate . startedAt . getTime ( ) ) + E2B_MAX_SANDBOX_LIFETIME_MS
11271166 const sandbox = await Sandbox . connect ( candidate . sandboxId , { apiKey, timeoutMs } )
11281167 return new E2BSandboxHandle (
11291168 sandbox ,
11301169 options . language ?? CodeLanguage . Python ,
1131- undefined ,
1170+ providerLimitAtMs ,
11321171 key ,
11331172 options . lifetimeMs === undefined ? undefined : requestedAtMs + timeoutMs
11341173 )
0 commit comments