Skip to content

Commit bc3ff8c

Browse files
icecrasher321claude
andcommitted
fix(pi): raise the sandbox ceiling to the longest execution we allow
The ceiling was pinned just under E2B's one-hour *Hobby* session limit. Sim is on Professional, where the limit is 24 hours, so the cap was enforcing a restriction no plan imposes — and it sat below the 90-minute async execution ceiling, which made the sandbox the binding constraint. A long Babysit run could be handed a 90-minute budget and still lose its sandbox at 59. Derived from getMaxExecutionTimeout rather than given a number of its own, so the sandbox always outlasts the longest run the platform permits and an operator who raises the async timeout does not have to know this file exists. The provider session limit stays as a clamp, so the derivation can never ask E2B for a lifetime it will refuse. Effect: ceiling 59 -> 90 min, and the agent turn it funds 29 -> 60 min, since resolvePiTimeoutMs reserves the clone and both finalize budgets out of it. Runs with a shorter deadline are unaffected — resolvePiRunLifetimeMs already lowers the ceiling to whatever the run itself has left. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f716fa6 commit bc3ff8c

2 files changed

Lines changed: 43 additions & 14 deletions

File tree

apps/sim/lib/execution/remote-sandbox/conformance.test.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ vi.mock('@daytona/sdk', () => ({
7070
}))
7171
vi.mock('@/lib/core/config/env', () => ({ env: mockEnv }))
7272

73+
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
7374
import {
7475
executeInSandbox,
7576
executeShellInSandbox,
@@ -464,17 +465,23 @@ describe('Pi sandbox lifetime', () => {
464465
mockEnv.PI_SANDBOX_LIFETIME_MS = undefined
465466
})
466467

467-
it('asks E2B for a lifetime under the one-hour Hobby ceiling', async () => {
468+
it('asks E2B for a lifetime that outlasts the longest execution', async () => {
468469
useProvider('e2b')
469470

470471
await withPiSandbox({}, async () => undefined)
471472

472473
const [template, options] = mockE2BCreate.mock.calls[0]
473474
expect(template).toBe('sim-pi')
474-
// E2B's default is five minutes, which kills any Pi run that outlives it,
475-
// and it rejects a create above one hour on a Hobby account.
475+
// E2B's default is five minutes, which kills any Pi run that outlives it.
476476
expect(options.timeoutMs).toBe(PI_SANDBOX_MAX_LIFETIME_MS)
477-
expect(options.timeoutMs).toBeLessThan(3_600_000)
477+
// The ceiling has to reach the longest run the platform permits, or the
478+
// sandbox becomes the binding constraint and a long Babysit run stops on a
479+
// limit no plan imposes. It was previously pinned under E2B's *Hobby* hour
480+
// while the async ceiling was ninety minutes, which is exactly that bug.
481+
expect(options.timeoutMs).toBeGreaterThanOrEqual(getMaxExecutionTimeout())
482+
// And it must stay inside the session length E2B sells us, or every create
483+
// fails outright.
484+
expect(options.timeoutMs).toBeLessThanOrEqual(24 * 60 * 60 * 1000)
478485
})
479486

480487
it('clamps a configured lifetime that would exceed the ceiling', async () => {

apps/sim/lib/execution/remote-sandbox/pi-lifetime.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { createLogger } from '@sim/logger'
88
import { env } from '@/lib/core/config/env'
9-
import { getRemainingExecutionMs } from '@/lib/core/execution-limits'
9+
import { getMaxExecutionTimeout, getRemainingExecutionMs } from '@/lib/core/execution-limits'
1010

1111
const logger = createLogger('PiSandboxLifetime')
1212

@@ -20,12 +20,33 @@ function isLifetimeProvider(): boolean {
2020
}
2121

2222
/**
23-
* E2B documents a one-hour maximum sandbox lifetime for Hobby accounts (24 hours
24-
* for Pro) and rejects a create above it. The cap sits strictly below that hour:
25-
* one hour is the documented boundary, and betting on an exact-boundary create
26-
* buys nothing.
23+
* E2B rejects a create above the session length its plan allows: one hour on
24+
* Hobby, 24 hours on Professional, which is the plan Sim is on. Kept only as a
25+
* clamp so that raising an execution timeout can never produce a lifetime E2B
26+
* refuses — it is far above anything {@link PI_SANDBOX_MAX_LIFETIME_MS} would
27+
* otherwise reach, and is not itself a target.
2728
*/
28-
export const PI_SANDBOX_MAX_LIFETIME_MS = 59 * 60 * 1000
29+
const PI_SANDBOX_PROVIDER_LIMIT_MS = 24 * 60 * 60 * 1000
30+
31+
/**
32+
* The longest a Pi sandbox may live.
33+
*
34+
* Derived from the platform's longest execution rather than carrying a number of
35+
* its own, because the two had drifted: this was pinned just under E2B's *Hobby*
36+
* hour while the async execution ceiling is ninety minutes, so a long Babysit run
37+
* lost its sandbox with half an hour of budget left and stopped on a limit no
38+
* plan actually imposed.
39+
*
40+
* Tying it to {@link getMaxExecutionTimeout} means the sandbox is never the
41+
* binding constraint on a run the platform would let continue, and an operator
42+
* who raises the async timeout gets a sandbox that keeps up without editing this
43+
* file. `getMaxExecutionTimeout` already resolves its env at module scope, so
44+
* reading it here is exactly as static as the constant it replaces.
45+
*/
46+
export const PI_SANDBOX_MAX_LIFETIME_MS = Math.min(
47+
getMaxExecutionTimeout(),
48+
PI_SANDBOX_PROVIDER_LIMIT_MS
49+
)
2950

3051
/**
3152
* The shortest lifetime a Pi run can actually complete in, and the floor an
@@ -55,13 +76,14 @@ export const PI_SANDBOX_MIN_LIFETIME_MS = 31 * 60 * 1000
5576
* For E2B it defaults to {@link PI_SANDBOX_MAX_LIFETIME_MS}: a run that finishes
5677
* kills its sandbox explicitly, so on the normal path the lifetime is a ceiling
5778
* rather than a budget. It is not entirely free — if the web process dies
58-
* mid-run the orphaned sandbox now bills until this ceiling instead of the SDK's
79+
* mid-run the orphaned sandbox bills until this ceiling instead of the SDK's
5980
* five minutes — but five minutes is short enough to kill live runs, which is
60-
* the bug this replaces.
81+
* the bug this replaces. {@link resolvePiRunLifetimeMs} is what keeps that
82+
* exposure proportional, by lowering the ceiling to the run's own deadline.
6183
*
6284
* `PI_SANDBOX_LIFETIME_MS` may only lower it, and only as far as
63-
* {@link PI_SANDBOX_MIN_LIFETIME_MS}, so a misconfigured value can neither make
64-
* every create fail on a Hobby key nor leave a run without time to push.
85+
* {@link PI_SANDBOX_MIN_LIFETIME_MS}, so a misconfigured value can neither
86+
* outrun the provider's session limit nor leave a run without time to push.
6587
*/
6688
export function resolvePiSandboxLifetimeMs(): number | undefined {
6789
if (!isLifetimeProvider()) return undefined

0 commit comments

Comments
 (0)