Skip to content

feat(sdk): filesystem-only snapshots via keepMemory on createSnapshot - #1925

Draft
bchalios wants to merge 1 commit into
mainfrom
feature/fs-only-snapshot
Draft

bchalios wants to merge 1 commit into
mainfrom
feature/fs-only-snapshot

Conversation

@bchalios

@bchalios bchalios commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds keepMemory (JS) and keep_memory (Python) to createSnapshot / create_snapshot. When false, the SDK sends memory: false on the snapshot request and the API takes a filesystem-only snapshot: only the filesystem is persisted, so the snapshot is smaller and faster to take, and sandboxes created from it cold-boot instead of restoring memory. The source sandbox keeps running either way. When omitted, nothing is sent and the API default, a full memory snapshot, applies.

The option mirrors the keepMemory / keep_memory already accepted on pause, and the wire field mirrors the pause request's memory. Spec copy updated from the API, both generated clients regenerated, docstrings and a changeset (minor for both SDKs) included.

Server side

The API support merged in e2b-dev/belt#4073 on top of the orchestrator support in e2b-dev/belt#4049. The API refuses memory: false with 400 snapshot_filesystem_only_disabled while the feature is not enabled for the team, and with 409 snapshot_filesystem_only_unsupported_node when the sandbox's node runs an orchestrator that predates the field. A request is never downgraded to a memory snapshot by a server that understands the field.

Why this is a draft

An API deployment from before e2b-dev/belt#4073 accepts the request body, ignores memory, and takes a memory snapshot while answering 201. Releasing this before production runs the new API would let callers ask for a filesystem-only snapshot and silently get a full one. Keep this in draft until production has the API build, then release.

The SDK tests for the new option call the live API with keepMemory: false. Where the feature is off for the test team the API answers 400 and the tests skip with that reason; where it is on, they prove the snapshot kind: the source keeps its kernel boot id across the snapshot and the sandbox created from it reports a different one, which a memory snapshot cannot produce. Against an API that predates the field those assertions fail, by design.

Verification

  • Rebased on main; Python modules compile; the JS SDK typechecks.
  • spec/runtime-ref is bumped to the e2b-dev/runtime commit that carries the API change and both clients are regenerated with make codegen, so the Generated files check passes. The bump also pulls in the upstream spec changes since the previous pin (team id description, cached node admission fields, the secret_limit_reached error code, envd oom_kills replacing the memory counters, is_symlink on the filesystem proto); none of them is read by SDK code outside the generated files.
  • keep_memory is keyword-only in Python. Both SDKs document the 400 and 409 refusals on createSnapshot / create_snapshot.
  • The three filesystem-only snapshot tests (JS, Python sync, Python async) assert the cold boot through the kernel boot id, read with a command rather than files.read because envd serves procfs files as an empty 200, and skip on the 400 snapshot_filesystem_only_disabled refusal.
  • End-to-end behaviour of the filesystem-only snapshot (no memfile written, source keeps the same Firecracker process, cold-boot from the template, 400 and 409 refusals) was verified against a dev cluster from the API side.

🤖 Generated with Claude Code

@cla-bot cla-bot Bot added the cla-signed label Oct 1, 2026
@changeset-bot

changeset-bot Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5920e38

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

@cursor

cursor Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

PR Summary

Medium Risk
Snapshot semantics change and depends on a matching API/orchestrator; older APIs can ignore memory and still return success, so callers may not get filesystem-only behavior until production is updated.

Overview
Adds filesystem-only snapshots to both SDKs via keepMemory (JS) / keep_memory (Python, keyword-only) on createSnapshot / create_snapshot. When set to false, the client sends memory: false on the snapshot API (same wire field as pause); sandboxes spawned from the result cold-boot from disk while the source sandbox keeps running. Docs call out 400 (snapshot_filesystem_only_disabled) and 409 (snapshot_filesystem_only_unsupported_node).

Integration tests (JS + Python sync/async) assert cold boot by comparing kernel boot_id before/after restore, skip when the feature is disabled for the team, and include a minor changeset for both packages.

spec/runtime-ref is bumped and OpenAPI/envd/filesystem clients are regenerated, pulling in unrelated spec deltas (SandboxSnapshotRequest.memory, secret_limit_reached, team prj_ IDs, envd oom_kills, EntryInfo.is_symlink, node metric copy)—mostly generated surface, not new hand-written SDK logic beyond snapshot options.

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

@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 compliance review (sdk-harness TASTE.md). Checked the changed surface against parity (T-1, T-1d, T-2), API shape (T-3, T-3a, T-5, T-6, T-10, T-14, T-23), generated types (T-18, T-20), timeouts/signal (T-46), and docs (T-47, T-69–T-71).

3 violations across 8 inline comments:

  • T-3a — new Python keep_memory parameter is positional-or-keyword instead of keyword-only (6 signatures: sync + async, instance overload, static overload, implementation).
  • T-14 — boolean keepMemory selects a snapshot kind; an enum/literal union is preferred (flagged as a design decision, given pause precedent).
  • T-70 / T-47 — JS default documented in prose instead of @default; new 400/409 failure modes undocumented (T-69 / T-62).

Compliant: JS/Python names mirror (T-1a/T-10), option lives on the existing named CreateSnapshotOpts (T-23), static form still forwards full connection opts and signal (T-6, T-46), omitted values stay unset on the wire, generated SandboxSnapshotRequest doesn't leak into the public surface (T-18).

Not line-specific: the internal _cls_create_snapshot helpers could take keep_memory keyword-only too for consistency, though they're not public.

def create_snapshot(
self,
name: Optional[str] = None,
keep_memory: Optional[bool] = None,

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.

T-3a — new Python optionals must be keyword-only, enforced by a bare * in the signature. keep_memory is a brand-new parameter, so it should get the * from day one; as written, create_snapshot("name", False) binds positionally and any future reordering becomes a breaking change. Placing the * after the already-shipped name keeps existing callers working.

Suggested change
keep_memory: Optional[bool] = None,
*,
keep_memory: Optional[bool] = None,

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.

Done in d0f0024. keep_memory is keyword-only now in all six create_snapshot signatures (sync and async, instance overload, static overload and implementation), with the bare * placed after name so existing positional callers of name keep working. I also made it keyword-only on the internal _cls_create_snapshot helpers, which were already called with keywords.

def create_snapshot(
sandbox_id: str,
name: Optional[str] = None,
keep_memory: Optional[bool] = None,

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.

T-3a — same as above: keep_memory is new, so make it keyword-only with a bare *.

Suggested change
keep_memory: Optional[bool] = None,
*,
keep_memory: Optional[bool] = None,

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.

Done in d0f0024, same change as in the first thread.

def create_snapshot(
self,
name: Optional[str] = None,
keep_memory: Optional[bool] = None,

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.

T-3a — same as above: keep_memory is new, so make it keyword-only with a bare *.

Suggested change
keep_memory: Optional[bool] = None,
*,
keep_memory: Optional[bool] = None,

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.

Done in d0f0024, same change as in the first thread.

async def create_snapshot(
self,
name: Optional[str] = None,
keep_memory: Optional[bool] = None,

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.

T-3a — same as above: keep_memory is new, so make it keyword-only with a bare *.

Suggested change
keep_memory: Optional[bool] = None,
*,
keep_memory: Optional[bool] = None,

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.

Done in d0f0024, same change as in the first thread.

async def create_snapshot(
sandbox_id: str,
name: Optional[str] = None,
keep_memory: Optional[bool] = None,

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.

T-3a — same as above: keep_memory is new, so make it keyword-only with a bare *.

Suggested change
keep_memory: Optional[bool] = None,
*,
keep_memory: Optional[bool] = None,

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.

Done in d0f0024, same change as in the first thread.

async def create_snapshot(
self,
name: Optional[str] = None,
keep_memory: Optional[bool] = None,

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.

T-3a — same as above: keep_memory is new, so make it keyword-only with a bare *.

Suggested change
keep_memory: Optional[bool] = None,
*,
keep_memory: Optional[bool] = None,

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.

Done in d0f0024, same change as in the first thread.

* of the source sandbox. The source sandbox keeps running either way.
* Defaults to `true` (full memory snapshot).
*/
keepMemory?: boolean

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.

T-14 — prefer an enum (string-literal union) over a boolean for an option that selects a behavior. TASTE.md uses exactly this case as its counter-example: pause(sandboxId, { mode: 'memory' }), not { keepMemory: true }. A snapshot kind is the textbook case for a future third variant (e.g. memory-only / incremental), and a boolean has no room for it without a breaking change.

I recognize pause already ships keepMemory, so this mirrors existing surface (T-10); but createSnapshot is a fresh option and copying the pattern spreads it to a second method. Worth deciding deliberately before release (the PR is draft anyway) — e.g. kind?: 'memory' | 'filesystem' / kind="filesystem" on createSnapshot, with pause migrating at the next major via T-66/T-67. If the team decides parity with pause wins, this can be resolved as-is.

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.

I agree this is a shape decision rather than a bug, and it is one for the SDK maintainers to settle before this leaves draft. The current form mirrors the keepMemory option that pause already ships, so a caller sees one vocabulary for both operations. If the team prefers a kind literal on createSnapshot and a later migration of pause, I will switch this PR to it. Leaving the thread open for that call.

* cold-boot (start fresh from disk) instead of restoring memory, so they
* begin without the running processes, in-memory state and open connections
* of the source sandbox. The source sandbox keeps running either way.
* Defaults to `true` (full memory snapshot).

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.

T-70 / T-47 — defaults are documented with the JSDoc @default tag (as the other options in this file do, e.g. @default true / @default 100), not in prose.

Also T-69 / T-62: the option introduces new failure modes (400 snapshot_filesystem_only_disabled when the feature isn't enabled for the team, 409 snapshot_filesystem_only_unsupported_node) that the docs should say when they happen and what to do. Consider adding that to the createSnapshot JSDoc / Python docstrings (@throws).

Suggested change
* Defaults to `true` (full memory snapshot).
*
* @default true

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.

Done in d0f0024. The JS option now documents its default with @default true instead of prose. Both createSnapshot JSDoc blocks (static and instance) and all six Python docstrings now say what happens when the API refuses the request: a SandboxError (SandboxException in Python) with status 400 when the feature is not enabled for the team, error code snapshot_filesystem_only_disabled, and 409 when the sandbox's node runs an orchestrator that predates the option, error code snapshot_filesystem_only_unsupported_node. In both cases a full memory snapshot still works, and for 409 a pause and resume moves the sandbox to a node that supports it. I kept it to the generic error class because that is what the SDK raises for these statuses today; no new error type is introduced here.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 840108b. Download artifacts from this workflow run.

JS SDK (e2b@2.52.1-feature-fs-only-snapshot.0):

npm install ./e2b-2.52.1-feature-fs-only-snapshot.0.tgz

CLI (@e2b/cli@2.21.1-feature-fs-only-snapshot.0):

npm install ./e2b-cli-2.21.1-feature-fs-only-snapshot.0.tgz

Code Interpreter JS SDK (@e2b/code-interpreter@2.8.1-feature-fs-only-snapshot.0):

npm install ./e2b-code-interpreter-2.8.1-feature-fs-only-snapshot.0.tgz

Desktop JS SDK (@e2b/desktop@2.4.1-feature-fs-only-snapshot.0):

npm install ./e2b-desktop-2.4.1-feature-fs-only-snapshot.0.tgz

Python SDK (e2b==2.52.0+feature.fs.only.snapshot):

pip install ./e2b-2.52.0+feature.fs.only.snapshot-py3-none-any.whl

Code Interpreter Python SDK (e2b-code-interpreter==2.10.1+feature.fs.only.snapshot):

pip install ./e2b_code_interpreter-2.10.1+feature.fs.only.snapshot-py3-none-any.whl

Desktop Python SDK (e2b-desktop==2.6.0+feature.fs.only.snapshot):

pip install ./e2b_desktop-2.6.0+feature.fs.only.snapshot-py3-none-any.whl

@bchalios
bchalios force-pushed the feature/fs-only-snapshot branch 2 times, most recently from d0f0024 to db975dd Compare October 1, 2026 13:29
`createSnapshot({ keepMemory: false })` (JS) and
`create_snapshot(keep_memory=False)` (Python) send `memory: false` on the
snapshot request, mirroring the pause option: only the filesystem is
persisted, so the snapshot is smaller and faster to take, and sandboxes
created from it cold-boot instead of restoring memory. The source sandbox
keeps running either way. Omitted, nothing is sent and the API default (a
full memory snapshot) applies.

The runtime spec pin (spec/runtime-ref) moves to the e2b-dev/runtime
commit that carries the API change, and both clients are regenerated
from it with the repo's codegen. The pin bump also brings the spec
changes landed upstream since the previous pin (18 September): the
team_id description, cached node admission fields, the
secret_limit_reached error code and 409 body, the envd metrics change
from mem_total_mib/mem_used_mib to oom_kills, and is_symlink on the
filesystem proto. None of them is read by SDK code outside the
generated files.

keep_memory is keyword-only in Python. Both SDKs document the two
refusals: status 400 (snapshot_filesystem_only_disabled) while the
feature is off for the team, and 409
(snapshot_filesystem_only_unsupported_node) when the sandbox's node
runs an orchestrator that predates the option.

The SDK tests prove the snapshot kind rather than only the file copy:
the source keeps its kernel boot id across the snapshot and the sandbox
created from it reports a different one, which a memory snapshot
cannot produce. The boot id is read with a command, not files.read,
because envd serves procfs files as an empty 200. Where the feature is
off for the team the API answers 400 and the tests skip with that
reason.

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@bchalios
bchalios force-pushed the feature/fs-only-snapshot branch from db975dd to 5920e38 Compare October 1, 2026 14:11

This branch has not been deployed

No deployments
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.

1 participant