Skip to content

feat: onResume option on connect/resume - #1800

Open
bchalios wants to merge 9 commits into
mainfrom
feat/on-resume-option
Open

feat: onResume option on connect/resume#1800
bchalios wants to merge 9 commits into
mainfrom
feat/on-resume-option

Conversation

@bchalios

@bchalios bchalios commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Resuming a paused sandbox always restores its memory snapshot, so a snapshot whose memory image wedges the guest has no self-serve way out. onResume: 'reboot' cold-boots from the sandbox's disk state instead. The reboot neither modifies nor deletes the memory snapshot, so a reboot that fails to start leaves the sandbox paused and the resume retryable; after a successful reboot the way to keep a snapshot is to pause again. 'restore' remains the default and keeps travelling as an omitted memory field — only the 'reboot' literal sends memory: false, so an unrecognized value from an untyped caller falls back to a memory restore rather than silently cold-booting.

// Recover a paused sandbox whose restored memory hangs on wake.
const sandbox = await Sandbox.connect(sandboxId, { onResume: 'reboot' })

// Default: restore the memory snapshot, processes and connections intact.
const sameSandbox = await Sandbox.connect(sandboxId)
sandbox = Sandbox.connect(sandbox_id, on_resume="reboot")
sandbox = await AsyncSandbox.connect(sandbox_id, on_resume="reboot")

Disk state carries crash-recovery semantics, so writes not flushed before the pause may be lost. Where filesystem-only resume is not enabled, a 'reboot' that would actually drop memory is rejected with an error rather than quietly restoring it. The no-op cases (a snapshot holding no memory, a sandbox still running) never reach that gate and succeed either way — the API short-circuits them before it consults the flag.

The spec pin is real: spec/infra-ref points at the infra commit that drops x-not-implemented from these fields, and the tracked specs come from the Copybara fetch at that commit rather than from anything placed by hand. redocly.yaml's filter-out prunes the marked property, so until that marker was gone codegen produced clients with no memory field at all — which is why this PR waited on it. Generated files is green.

The rest of the spec diff is churn the pin had fallen behind by: removed access-token models, httpsPorts and reworded transform rules on the network config, Rig* schemas that survive the tag filter as orphan components, and the resume request body becoming optional.

Verified: 5 JS + 10 Python request-shape tests over sync, async, instance and static-by-id forms, each assertion mutation-checked (flip the literal to !== 'restore', force false, drop the guard, drop each forwarding hop — every mutant reddens exactly the intended row). pnpm run lint, format and typecheck clean. The credential-backed suites can't run without an E2B_API_KEY: locally they fail identically to main (231 JS, 426 Python, byte-identical failure sets), so CI's run is the real check there.

Picks up `memory` on ConnectSandbox and ResumedSandbox, which the API
serves and the spec no longer marks unimplemented. The rest is churn the
pin had fallen behind: the removed access-token models, httpsPorts and
the reworded transform rules on the network config, and Rig schemas that
survive the tag filter as orphan components.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Resuming a memory-inclusive snapshot whose memory image wedges the guest
had no self-serve way out. `onResume: 'reboot'` (`on_resume` in Python)
cold-boots from disk state instead, and the memory snapshot is never
touched, so a later restore still works.

'restore' is the API's own default and travels as an absent `memory`
field; only the 'reboot' literal sends `memory: false`, so an unrecognized
value from an untyped caller falls back to a memory restore rather than
silently cold-booting.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
@cla-bot cla-bot Bot added the cla-signed label Sep 3, 2026
@cursor

cursor Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

PR Summary

Medium Risk
onResume: 'reboot' changes how paused sandboxes resume and can drop unflushed in-memory state; wrong wire mapping could cold-boot when restore was intended.

Overview
Adds onResume / on_resume on Sandbox.connect() in the JS and Python SDKs ('restore' default, 'reboot' for disk-only resume). Only 'reboot' sends memory: false on the connect/resume API body; restore leaves memory omitted.

OpenAPI and generated clients pick up optional memory on ConnectSandbox and ResumedSandbox, plus spec pin churn (access-token types removed, rig admin schemas, httpsPorts, network rule docs, 501, optional deprecated resume body). Changeset marks minor releases for e2b and @e2b/python-sdk.

Reviewed by Cursor Bugbot for commit 9ab04ef. Bugbot is set up for automated code reviews on this repo. Configure here.

@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9ab04ef

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
e2b Minor
@e2b/python-sdk Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 50135b4. Download artifacts from this workflow run.

JS SDK (e2b@2.46.2-feat-on-resume-option.0):

npm install ./e2b-2.46.2-feat-on-resume-option.0.tgz

CLI (@e2b/cli@2.18.1-feat-on-resume-option.0):

npm install ./e2b-cli-2.18.1-feat-on-resume-option.0.tgz

Code Interpreter JS SDK (@e2b/code-interpreter@2.7.3-feat-on-resume-option.0):

npm install ./e2b-code-interpreter-2.7.3-feat-on-resume-option.0.tgz

Desktop JS SDK (@e2b/desktop@2.3.5-feat-on-resume-option.0):

npm install ./e2b-desktop-2.3.5-feat-on-resume-option.0.tgz

Python SDK (e2b==2.46.4+feat.on.resume.option):

pip install ./e2b-2.46.4+feat.on.resume.option-py3-none-any.whl

Code Interpreter Python SDK (e2b-code-interpreter==2.9.3+feat.on.resume.option):

pip install ./e2b_code_interpreter-2.9.3+feat.on.resume.option-py3-none-any.whl

Desktop Python SDK (e2b-desktop==2.4.6+feat.on.resume.option):

pip install ./e2b_desktop-2.4.6+feat.on.resume.option-py3-none-any.whl

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TASTE.md review

Checked the SDK-facing changes (packages/js-sdk/src/sandbox/sandboxApi.ts, index.ts, packages/python-sdk/e2b/sandbox/sandbox_api.py, sandbox_sync/, sandbox_async/, __init__.py, and the new tests) against: parity (T-1, T-2, T-10), API shape (T-3, T-6, T-14, T-15, T-19, T-22, T-23), boundary mapping (T-18), entry-point exports (T-54), client-side validation (T-52), and docs (T-69–T-74). Generated files (schema.gen.ts, api/client/models/*, spec/) were not judged.

Violations: 1 (low severity, same issue mirrored in sync/async Python — flagged inline on both).

  • T-3 / T-1don_resume lands as another positional-with-default in the Python connect() signatures, extending the existing timeout into a chain of optional positionals (Sandbox.connect(id, 300, "reboot") now type-checks). Since JS carries it in SandboxConnectOpts, the Python mirror should be keyword-only. timeout/logger are pre-existing and out of scope, so a * separator can't be dropped in without moving them; noted as a design-debt item rather than something to block on.

Everything else lines up well: 'restore' | 'reboot' instead of a memory: boolean mirror of the wire field (T-14); the wire boolean is mapped at the boundary and the generated ConnectSandbox type never reaches users (T-18); SandboxOnResume is a named, exported type (export type in JS, __all__ in Python — T-23, T-54); no client-side check of whether the deployment supports filesystem-only resume (T-52); all four Python connect docstrings plus the JS option JSDoc document the parameter, default, and rejection behavior (T-69, T-74). Observation, not a violation: onResume/on_resume reuses the on<Event> shape T-27 reserves for callbacks, but onTimeout already set that precedent for lifecycle action config, so consistency wins here.

Comment thread packages/python-sdk/e2b/sandbox_sync/main.py Outdated
Comment thread packages/python-sdk/e2b/sandbox_async/main.py Outdated
Landing it after `timeout` made `Sandbox.connect(id, 300, "reboot")` a
valid call, while JS only accepts the option through SandboxConnectOpts.
Placing it after `logger` behind a `*` separator keeps both pre-existing
parameters positional, so nothing existing moves.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
The gate short-circuits a memoryless snapshot before it consults the
feature flag, and a running sandbox never reaches the gate, so "not
enabled rejects reboot" was only true for a paused snapshot that holds
memory.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Replaces the placeholder pin with the commit that actually carries it, so
the tracked specs come from the Copybara fetch. Also picks up the freeze
endpoint's mode parameter and vanished counts on the envd spec; both are
x-internal, so no SDK client changes.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
The docs said a reboot leaves the memory snapshot restorable later. It does
not: while the sandbox runs, connect is a keep-alive; a later pause writes a
new snapshot over the row; kill ends the resume path. The guarantee that does
hold is for a reboot that fails to start, so the docs now say that and point
at pausing again to keep a snapshot.

The keyword-only guard asserted the wrong signature. class_method_variant
wraps the instance method, so inspect.signature resolved to `(self, timeout,
*, on_resume)` and the static-by-id chain was never inspected — dropping the
`*` from _cls_connect_sandbox left the whole suite green while
`connect(id, 300, logger, "reboot")` became valid again. Each form is now
asserted directly.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Saying a reboot that fails to start leaves the sandbox paused held for the
three failures after dispatch, but not for the two refusals before it. A
sandbox whose start is already in flight is refused outright and is not
paused, so a caller following that advice retries into a keep-alive that
returns 200 without rebooting — the server's own message on that path says
to retry once it is running or paused.

The docs now scope the retryable claim to a reboot that started, and say a
running or starting sandbox has to be paused before it can be rebooted,
which nothing said before.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Two claims about reachability were wrong. A running sandbox is not returned
unchanged — connect keeps it alive and extends its timeout, as the option
above it already documents — and telling the caller to pause a sandbox whose
start is in flight sends them into a second refusal, because a reservation
holds no store entry and pause answers that it is already paused.

The option is ignored for a running sandbox, so pausing first is the way to
reboot one; a request that would drop memory is refused mid-start, so the
start has to finish. Both bounds are scoped to requests that would actually
drop memory, since a snapshot without memory joins a start and cold-boots
regardless.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>

@mishushakov mishushakov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all looks good, however i'd shorten the comments in the doc strings and probably exclude Rig from the type-gen (this is probably a separate issue?)

Review feedback: the option docs carried the whole rescue contract (in-flight
refusal, failed-boot state, pause-again guidance). Keep what a caller needs at
the call site — the two values, the crash-consistency caveat, flag-off
rejection, and the no-op cases — and leave the rest to the resume docs.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
*
* @default 'restore'
*/
onResume?: SandboxOnResume

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shortened the docstrings in 9ab04ef — JSDoc and all six Python :param on_resume entries now carry just the two values, the crash-consistency caveat, the flag-off rejection and the no-op cases; the rest of the contract moves to the resume docs.

Agreed on Rig being separate: the SDK bundles in redocly.yaml filter operations by tag but never prune component schemas (remove-unused-components is on for envd/volume only), so every schema leaks into both generated clients. Opening a separate PR that turns it on for js-sdk and python-sdk — drops 20/23 unused schemas (Rig*, Admin*, Node*, TeamAPIKey*, VolumeToken…), none referenced from SDK source.

@bchalios

bchalios commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Rig / unused-schema pruning is #1803 (remove-unused-components on the two SDK bundles). Once it merges, regenerating this branch drops the Rig* models from the diff.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ab04ef8a3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +2 to +3
'e2b': minor
'@e2b/python-sdk': minor

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Publish the access-token removals as breaking changes

For consumers that import CreatedAccessToken or NewAccessToken from the Python generated models, or reference those schemas through the root-exported JavaScript components type, installing this minor release causes runtime import failures or downstream TypeScript compilation failures because this same commit deletes those public symbols. Either preserve compatibility while adding onResume, or mark both affected packages for a major release rather than allowing ordinary minor upgrades to receive the removals.

AGENTS.md reference: AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good — a small, well-scoped feature addition with matching implementations across JS and both Python sync/async SDKs.

What was reviewed: the onResume/on_resume wiring from connect() through to the ConnectSandbox request body (memory: false only for 'reboot', otherwise omitted) in JS and Python sync/async; parity across sandboxApi.ts, sandbox_sync/sandbox_api.py, and sandbox_async/sandbox_api.py; the spec regeneration (spec/infra-ref bump plus consistent memory field additions in openapi.yml, envd.yaml-derived Python models, and schema.gen.ts); and the new unit tests, which cover default/restore/reboot/unrecognized-value cases plus keyword-only-parameter enforcement on both static and instance connect forms.

Extended reasoning...

Overview

The PR adds an onResume/on_resume option ('restore' | 'reboot') to Sandbox.connect() across JS and Python (sync + async). 'reboot' sends memory: false in the ConnectSandbox request body to cold-boot from disk state instead of restoring the memory snapshot; 'restore' (the default) omits the field entirely, matching the API's own default. The change also bumps spec/infra-ref and regenerates spec/openapi.yml, spec/envd/envd.yaml, the JS schema.gen.ts, and several unrelated Python generated models that had fallen behind the pin (rig/network/access-token models). A changeset was added for both e2b and @ e2b/python-sdk.

Security risks

None identified. This is a client-side option that only toggles an optional boolean field in an existing authenticated request; it does not touch auth, crypto, or permission logic. The untyped-caller fallback behavior (only the literal 'reboot' string triggers memory: false) is a deliberate safety choice that avoids silently dropping memory data on unexpected input, and this exact behavior is tested.

Level of scrutiny

This warranted a moderate level of scrutiny: it changes public SDK surface across three language runtimes (JS, Python sync, Python async) and touches auto-generated spec files that must never be hand-edited. I verified: (1) the spec/infra-ref pin was bumped and the memory field additions in openapi.yml/schema.gen.ts/Python models are consistent with a genuine codegen regeneration rather than a hand edit; (2) the JS and both Python implementations wire onResume/on_resume through to the request body identically (memory: false iff 'reboot', else UNSET/undefined); (3) an inline review comment from devin-ai-integration about making on_resume keyword-only was addressed by a real follow-up commit (5155388) plus dedicated regression tests (test_on_resume_is_keyword_only), not merely self-resolved without action.

Other factors

Test coverage is strong: 5 JS tests and roughly 10 Python tests cover default/restore/reboot/unrecognized-value cases across sync, async, static-by-id, and instance forms, plus explicit keyword-only-parameter enforcement tests to prevent positional-argument foot-guns. The PR author's description of mutation-testing each assertion adds confidence, though I did not independently re-run that mutation testing. The docstrings went through several iterative refinements across commits, all narrowing scope/wording rather than changing behavior. No CODEOWNERS file was found restricting these paths. Given the small, mechanical, well-tested nature of the change and that the one substantive reviewer objection was demonstrably fixed with a commit and tests, this is approvable without requiring further human review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants