Releases: coreweave/cwsandbox-client
Release list
v0.26.0
v0.26.0 (2026-06-11)
This release is published under the Apache-2.0 License.
Bug Fixes
-
fss: Harden snapshot lifecycle, retries, and error mapping (
d80e6cc) -
snapshot() waits for RUNNING before archiving, matching
exec/read_file/write_file -
create-snapshot sets max_timeout_seconds when wait_for_ready
(mirrors snapshot-on-stop) -
bare gRPC NOT_FOUND on a snapshot op maps to SnapshotNotFoundError
when a snapshot ID is in context -
delete_snapshot treats NOT_FOUND on a retry as success (a committed
delete whose response was lost) -
list_snapshots retry restarts pagination from page 1 instead of
resuming at the last page_token -
doc/comment fixes (FileSystemSnapshot return type, keyword-only
bucket-config args) and regression tests for each -
fss: Raise on incompatible snapshot-on-stop coalescing (
ae4be3e)
stop() coalesces concurrent callers onto a single shared _stop_task (first-caller-wins), discarding later callers' parameters. A stop(snapshot_on_stop=True) caller that joined an in-flight plain stop — or a sandbox already stopping/stopped — silently inherited "no snapshot": the sandbox was torn down with no archive, file_system snapshot_id stayed None, and .result() returned success. Silent data loss.
Detect the incompatible join under _stop_lock and raise the new SnapshotOnStopConflictError instead of coalescing. Coalescing is kept for every compatible case (plain joining plain, plain joining snapshot, snapshot joining snapshot, user-stop joining a server-initiated drain), preserving the idempotent-teardown contract the context manager and cleanup handlers rely on. Mirrors the backend's FailedPrecondition for a snapshot-on-stop that arrives after the sandbox has begun terminating.
Cases that now raise when snapshot_on_stop=True: a plain stop already in flight; a TERMINATING drain with no owned stop task (no snapshot RPC will be sent); already terminal with no snapshot captured. Already-terminal with a snapshot already captured stays a convergent no-op, and a never-started sandbox stays on the normal no-op path (no mount to archive).
Addresses PR #137 review finding #1.
- fss: Size snapshot-on-stop client deadline to cover the grace phase (
430a1b3)
The backend runs a snapshot-on-stop in two sequential phases: it archives the mount (bounded by max_timeout_seconds) and THEN deletes the pod (bounded by graceful_shutdown_seconds). The client deadline was hard-set to the archive budget alone (DEFAULT_FSS_STOP_TIMEOUT_SECONDS + 5s ≈ 605s), ignoring the additive pod-delete grace — so a healthy stop whose archive runs long plus a real grace could exceed the client deadline and surface a spurious DEADLINE_EXCEEDED. The old 5s buffer was also smaller than the backend's ~30s gateway request slack, so a near-budget archive alone could trip it.
Decouple the proto field from the client deadline: - proto max_timeout_seconds stays DEFAULT_FSS_STOP_TIMEOUT_SECONDS (600, the archive budget; the backend defaults to this and does not cap it). - client deadline = archive budget + effective grace + slack, where effective grace substitutes the backend's 30s default when 0 is sent (sending 0 does not mean "no grace"), and slack (~35s) covers the backend's ~30s gateway request slack plus network round-trip. This keeps the client deadline ~5s past the backend's worst-case wall-clock at every grace value.
Adds DEFAULT_FSS_STOP_GRACE_FALLBACK_SECONDS and DEFAULT_FSS_STOP_CLIENT_SLACK_SECONDS (named + commented) and documents the grace semantics on stop(). No client-side graceful>300 validation; the backend rejects it.
Backend behavior confirmed against coreweave/aviato: 600s is a default, not a cap; the only hard cap is graceful_shutdown_seconds <= 300.
Addresses PR #137 review finding #7.
- fss: Stop retrying the client deadline on a wait-for-ready snapshot (
c7b64d0)
snapshot(wait_for_ready=True) uses a ~605s client deadline (just past the 600s server-side wait bound) and wraps the create in _retry_transient_rpc (30s inter-attempt budget). A client DEADLINE_EXCEEDED maps to SandboxRequestTimeoutError, which is retryable, and the loop bounds only the gap between attempts — not attempt duration — so a wedged backend that blows past its own wait-timeout triggers a second full ~605s attempt: ~1210s wall-clock vs the ~605s the ceiling implies.
A client deadline on a wait-for-ready create is the ceiling being hit, not a transient blip (that's UNAVAILABLE / RESOURCE_EXHAUSTED / throttle, which still retry). Treat it as terminal: add an optional non_retryable override to _retry_transient_rpc and pass SandboxRequestTimeoutError on the create call. The wait now ends at ~605s, and the surfaced error is unchanged (SandboxRequestTimeoutError — the snapshot may still finish server-side; poll get_snapshot()). Scoped to snapshot(); the stop path has no retry loop.
Addresses PR #137 review finding #8.
Features
- fss: Add file-system snapshot support (
83f60ca)
Wire the backend file-system snapshot (FSS) feature into the SDK.
- FileSystemSnapshotOptions mount config on run()/Session.sandbox()/ @session.function() and SandboxDefaults (mount_path, size, restore via file_system_snapshot_id) - snapshot() captures a mid-life snapshot and returns the new ID; stop(snapshot_on_stop=True) snapshots on shutdown and exposes the ID via the file_system_snapshot_id property - management classmethods: get_snapshot, list_snapshots, delete_snapshot, get_snapshot_bucket_config, set_snapshot_bucket_config - FileSystemSnapshot record plus status/trigger/bucket enums and the FileSystemSnapshotBucketConfig type - SandboxSnapshotError hierarchy mapped from CWSANDBOX_FSS_* reasons - bounded client-side transient retry for the FSS RPCs, reusing the poll loop's retry classification and AIP-193 backoff; create auto-generates an idempotency key so a retried create dedups - vendored gateway proto stubs regenerated for the FSS messages and RPCs - unit and integration tests, an example, and docs
Detailed Changes: v0.25.0...v0.26.0
v0.25.0
v0.25.0 (2026-06-09)
This release is published under the Apache-2.0 License.
Bug Fixes
- files: Address review feedback on large-file handling (
9396d21)
Resolves the review on PR #133. Eight findings from @djenriquez, by severity:
Blockers:
-
Truncation false-positive (read_file auto-fallback). _verify_no_truncation statted the file after the read, so a file appended to between cat's EOF and the stat made
expectedexceed what was legitimately delivered and raised a spurious truncation error — on the default read_file() 32–256 MiB fallback, where in prod this check is the only truncation defense and the false positive sits on the critical path (e.g. read_file('/var/log/app.log') on a live log). Now the file's size is captured before the read: the read_file fallback feeds the server-reported pre-read size straight in (no extra stat round-trip), and read_file_streaming stats once up front. A concurrent append only grows the delivered count, so it can no longer look like a short read. The inverted docstring justification is corrected. -
stat timeout overshoot. The stat used the original caller budget, so a 600s read that spent 599s streaming could spend up to 10s more on the stat and blow past the deadline (the comment/docstring claimed otherwise). The streaming read now tracks an absolute monotonic deadline at op start; the pre-read stat draws from the remaining budget capped at STAT_INTEGRITY_TIMEOUT_SECONDS, and the read gets what's left.
-
Sync iterable on the event loop. _write_file_streaming_async consumed a sync iterable by calling next() directly on the shared background loop, so a blocking source (file handle, NFS/FUSE read, network generator) stalled every other operation on that loop. Each next() (and chunk coercion) now runs in the default executor via a small async generator; the iterator advances one step per consumed item, so no prefetch buffer or hand-off race is needed and the downstream exec stdin queue still provides backpressure. The TypeError contract for bad chunks is preserved (raised in the worker, re-raised unchanged).
-
Observed cap not clamped. _record_observed_cap cached the server cap with no upper bound and the old frame-safe limit had been deleted, so a cluster reporting a cap near/above the 100 MiB gRPC frame ceiling would route a sub-cap payload to the unary path and hit a frame-size reject. Restored MAX_FILE_UNARY_BYTES (channel limit − 1 MiB framing headroom) and clamp the effective cap to it in _file_op_cap; anything above streams instead.
Reason split:
- CWSANDBOX_FILE_TOO_LARGE covered two opposite conditions — a size-policy refusal (no data lost, switch to streaming) and a post-hoc short read (data lost, already streaming, "use read_file_streaming" is a no-op). Split the truncation case into its own CWSANDBOX_FILE_TRUNCATED reason while it is still unreleased (free now, breaking later).
Reviewer questions, resolved:
-
STREAM_BACKPRESSURE no longer rides on .reason (the AIP-193 namespace). It now has its own .stream_code attribute on SandboxStreamBackpressureError; .reason is None there. The class remains the discriminator and existing
except SandboxExecutionErrorhandlers are unaffected. -
The streaming-read integrity check is gated by size band (TRUNCATION_CHECK_MIN_BYTES): silent truncation is a large-payload phenomenon, so below the band the pre-read stat is skipped entirely. The read_file fallback pays no stat at all (it reuses the server-reported size).
Docs:
- read_file's docstring no longer over-promises that all >256 MiB reads are refused: when the backend signals via resource exhaustion rather than a sized CWSANDBOX_FILE_TOO_LARGE the client can't know the remote size, so a very large file can still be buffered — callers are pointed at read_file_streaming. (The uncapped-buffer OOM on that branch is pre-existing and tracked separately.)
ruff + mypy clean; full unit suite green (1171 tests, +9 new covering the pre-read baseline, growing-file no-false-positive, frame-safe clamp, sync-iter executor offload, remaining-budget stat timeout, band gating, and stream_code).
Chores
- Add djenriquez and brandonrjacobs as codeowners (
ae523d5)
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Documentation
- Add large-file streaming example (
3b01836)
Adds examples/large_file_streaming.py demonstrating how to stream large files without tripping backpressure, and registers it in examples/README.md.
The example shows:
-
the fast-drain read loop (read_file_streaming into a local file with only the
write inline) that keeps up with the producer; -
the slow-inline-work anti-pattern that overruns the buffer and raises
SandboxStreamBackpressureError, clearly labeled as what NOT to do; -
catching that error and recovering with the fast-drain pattern (it is not
retryable as-is — the read pace must be fixed, or the work chunked, first). -
Keep streaming guidance free of internal implementation detail (
23036ef)
Scrub the user-facing read_file_streaming docstring and the large-file streaming example of implementation wording that doesn't belong in the public SDK surface: drop the internal queue-size constant name and "server frame size" mechanics, and reword "the stream's buffer fills and the server ends it" to describe the observable behavior (output produced faster than it's read; the stream is ended early). The actionable guidance — keep the read loop tight, move slow work off it, memory grows with how far behind you fall — is retained.
No behavior change; docstring/comment wording only.
Features
- Surface truncated exec output as a typed error (
8c910cf)
When a command runs to completion but some of its output is lost in transit, the server now ends the stream with a STREAM_TRUNCATED error instead of returning partial output alongside a success exit code. Map it to a new typed SandboxStreamTruncatedError (a SandboxExecutionError subclass) carrying the code on .stream_code, with guidance: for large output write to a file and retrieve it rather than streaming over stdout, and re-run only if the command is idempotent. No automatic retry — re-running streams over the same path and may truncate again, and may have side effects.
Mirrors the existing SandboxStreamBackpressureError handling; unknown codes still surface as a generic SandboxExecutionError.
- Typed file-too-large handling and streaming file APIs (
6abd8ff)
Large file operations previously failed with a generic SandboxError when the payload exceeded the backend's per-file size cap, with no typed signal and no working path for files in the 32-256 MiB range. This adds typed errors, a transparent streaming fallback for mid-size files, and dedicated streaming APIs for very large files — all without changing the behavior of small-file ops.
Behavior by payload size (read_file / write_file):
<= ~32 MiB unary call (unchanged) ~32-256 MiB transparently fall back to a streaming exec transfer; the first fallback per Sandbox logs once at INFO naming the explicit streaming method as the deterministic path > ~256 MiB refused with a typed SandboxFileError(CWSANDBOX_FILE_TOO_LARGE) pointing at the streaming APIs
New public surface:
- CWSANDBOX_FILE_TOO_LARGE error reason, surfaced as SandboxFileError with .filepath, .reason, and structured .metadata (size_bytes, max_size_bytes, operation). The metadata["operation"] value is the public method name (read_file / write_file / read_file_streaming) — never an internal RPC name — so callers can branch on it as a stable contract. - read_file_streaming(filepath) -> StreamReader[bytes] for chunked consumption of large files without buffering the whole payload. - write_file_streaming(filepath, source) accepting bytes, a sync Iterable[bytes], or an AsyncIterable[bytes], piped through the streaming exec channel without materializing the payload. Non-bytes-like chunks raise TypeError rather than being silently NUL-padded via bytes(int). - SandboxStreamBackpressureError (a SandboxExecutionError subclass): raised when output is produced faster than the consumer reads it and the stream is ended early. read_file, write_file, and the explicit *_streaming methods all surface this
same typed error for this condition; it is never remasked into a generic SandboxFileError. carries reason == "STREAM_BACKPRESSURE".
Integrity and safety:
- After a streaming read, a shared post-read check (_verify_no_truncation) stats the file size and raises CWSANDBOX_FILE_TOO_LARGE if fewer bytes were delivered than the file holds, so a backend that silently truncates the channel cannot cause a partial read to be consumed as a complete file. The check skips when the file stats as size 0 (procfs/sysfs pseudo-files legitimately report 0) and only flags a short read (delivered < expected), so a concurrent append is not mistaken for truncation. The stat uses a short dedicated timeout so it cannot double the request's wall-clock budget on a slow channel. - read_file_streaming delivers a terminal error to the consumer with guaranteed delivery even when its bounded output queue is full, so a slow reader that triggers backpressure surfaces the error instead of hanging on an empty queue.
read_file / write_file docstrings document the size bands and the exceptions they can now raise. Backend coordination: the > cap refusal and the STREAM_BACKPRESSURE code depend on ser...
v0.24.0
v0.24.0 (2026-05-26)
This release is published under the Apache-2.0 License.
Bug Fixes
- logs: Address inline review feedback on log-stream resume (
20aa890)
Addresses the 5 inline review comments on PR #130 commit f05cf05.
-
tail_lines / since_time were re-emitted on every fresh-fallback re-init. A caller running
stream_logs(follow=True, tail_lines=100)would see the last-100 window replayed on every SESSION_NOT_FOUND / REPLAY_GAP / RUNNER_* fallback, contradicting the in-code docstring that says fresh fallback "restarts from the current head with no replay of older bytes." Introduces anis_first_attemptflag threaded into the request_generator. tail_lines / since_time go on the first attempt only; timestamps (formatting flag, not replay window) persists across attempts. -
The first transport error was never retried because the resume gate required
session_idto be populated, which only happens after the first frame arrives. A flaky gateway connection at the opening edge of the tail therefore surfaced fatally. Drops thesession_idguard; the outerattempt < MAX_ATTEMPTSis the only budget. Whensession_idis empty, the next attempt's request_generator falls into the empty-resume_id branch — a bounded fresh-init retry, which is the right behavior at the opening edge. -
Exceptions raised inside the retry block were silently swallowed: the outer
finallyunconditionally enqueued the EOF sentinel before the outerexceptenqueued the exception. StreamReader stops iteration on the first None, so the exception was never surfaced to the consumer. Tracksinner_exit_cleanand emits the sentinel only on the success path; on the failure path the outerexceptis the only thing that touches the queue. -
stubwas acquired once outside the retry loop and reused on every retry attempt. gRPC aio handles subchannel reconnect transparently in the common case, but if anything tears the channel down between attempts (the integration test forcibly closes it;stop()invalidates it during shutdown), the retry path silently callsStreamLogson a dead stub. Moves thechannel/stubacquisition inside the attempt loop;_get_or_create_streaming_channelreturns the cached channel when it is still healthy, so this is free in the common case. -
_is_resumable_transport_errordispatches on rawgrpc.StatusCodeonly and skips the AIP-193 reason metadata path that_translate_rpc_errorconsults. Not a live bug today (the backend does not attachCWSANDBOX_*reasons to streaming errors), but a forward-compat hazard worth documenting. Adds an inline note explaining the intentional divergence and what would need to change if the backend evolves to attach AIP-193 reasons to streaming errors.
Tests:
- TestStreamLogsReplayFiltersFirstAttemptOnly: asserts that
tail_linesandsince_timeare only on the first attempt's init, thattimestampspersists across attempts. - TestStreamLogsRetryFirstFrameTransportError: asserts that a transport error before the first frame triggers a fresh-init retry with empty resume fields. - TestStreamLogsExceptionOrderingAtEnd: asserts that an exception in per-attempt setup arrives at the queue without a preceding EOF sentinel. - TestStreamLogsStubReacquiredPerAttempt: asserts that_get_or_create_streaming_channelis called once per attempt via a counting channel factory.
Test plan: - mise run check (lint, format, mypy, 1134 unit tests, +6 new) — clean - mise run test:e2e against staging — 113 passed, 2 skipped (env-gated)
- logs: Align stream_logs resume with documented wire contract (
f05cf05)
Address review feedback on PR #130. Brings the in-band error dispatcher in line with the wire contract documented in streaming_pb2.pyi (LogStreamError.code), preserves the gRPC cause on resume exhaustion, fixes a partial-line corruption bug on fresh-init fallback, and adds deterministic unit coverage for the resume state machine.
Wire-contract handling (src/cwsandbox/_sandbox.py):
- REPLAY_GAP: previously
continued the inner loop, which silently terminated the stream because the collector had already enqueued its end-of-iteration sentinel. Now triggers a fresh re-init from the current head, matching the .pyi prescription. - RUNNER_UNAVAILABLE / RUNNER_DRAINING: previously fell through to fatal SandboxError. Now route through the fresh re-init path, since the wire contract documents both as transient ("logs moved"). - INVALID_RESUME_OFFSET: previously fell back to fresh init. Now surfaces as a terminal SandboxError without retry — retrying with the same state would loop on a corrupt echoed offset. - SESSION_NOT_FOUND: unchanged (already routed to fresh init). - Consolidates fresh-reinit codes into a single _STREAMING_FRESH_REINIT_CODES set, so future wire-contract additions land in one place. - Drops theattempt+1 < MAX_ATTEMPTSgate from the resume / fresh branches: the outerwhile attempt < MAX_ATTEMPTSis now the only budget, so the synthesized
SandboxUnavailableError on exhaustion is the single place that translates "budget spent" to a user-visible error.
Other fixes:
- Partial-line buffer was preserved across the fresh-init fallback, which could splice unrelated bytes into a stale partial line (e.g. "hello wor" + later "baz qux\n" emitted as "hello worbaz qux\n"). The buffer is now cleared on every fresh fallback path. - Resume-exhaustion previously synthesized SandboxUnavailableError with no cause. Now chains the underlying AioRpcError so the gRPC status code is reachable post-mortem, matching the
raise X from rpc_errorconvention used elsewhere in the module. Also chains cause on the translated-error path so behavior is uniform across resume and non-resume failure routes. - Removes grpc.StatusCode.CANCELLED from the resumable transport status set. CANCELLED is overwhelmingly a teardown signal (sandbox.stop(), call.cancel() in shutdown) and retrying it just burns the resume budget on a session that is being torn down on purpose. - Replaces a stale comment on thecompletebranch that described a drain step the code did not implement.
Tests (tests/unit/cwsandbox/test_sandbox.py, +410 lines):
- New TestStreamLogsResumeStateMachine drives _stream_logs_async end- to-end against a programmable stub that yields real protobuf messages and patches asyncio.sleep so backoff is instant. Covers: resume after transport error, fresh after SESSION_NOT_FOUND, REPLAY_GAP and RUNNER_UNAVAILABLE → fresh, INVALID_RESUME_OFFSET → terminal, exhaustion → SandboxUnavailableError with cause chain, and the partial-line buffer clear on fresh fallback. - New TestStreamLogsResumeTransportClassification covers the CANCELLED exclusion and UNKNOWN inclusion. - New TestStreamLogsInitWireShape asserts the SDK actually populates resume_session_id / resume_offset on the wire when resuming, and omits them on a fresh init. Supersedes the prior protoc round-trip tests, which were tautological — protobuf already guarantees field round-tripping; what matters is that the SDK populates the right fields at the right time.
Integration test (tests/integration/cwsandbox/test_sandbox.py):
- Hardens test_stream_logs_reconnects_after_transient_disconnect so the post-disconnect assertion cannot be satisfied purely from buffered items in the reader's queue. Requires counter values strictly greater than first_batch[-1] + 5 — within the 30s post-disconnect deadline the live tail will always advance past this threshold, but a reader paused with several seconds of backlog cannot. Defers deterministic state-machine assertions to the unit tests above.
Test plan: - mise run check (lint, format, mypy, 1128 unit tests) — clean - mise run test:e2e against staging — 113 passed, 2 skipped (env-gated)
Chores
-
Update CODEOWNERS to Applied Aviato (
ef17a1a) -
proto: Regenerate streaming stubs with resume fields (
3ff826b)
Pulls in the new wire fields on the streaming protocol:
- LogStreamData.session_id, LogStreamData.offset (cumulative byte count)
- LogStreamInit.resume_session_id, LogStreamInit.resume_offset
- ExecStreamInit.resume_session_id
- StreamingExecReady.session_id
Generated at protobuf 5.26.1 to keep the no-runtime-version-check policy documented in scripts/update-protos.sh.
Documentation
- Flush stdout in async TerminalSession example (
fa7f9ae)
Features
- logs: Auto-resume stream_logs across transient disconnects (
d307da3)
stream_logs(follow=True) now captures session_id and the cumulative byte offset from each LogStreamData frame and, on a transient transport error (UNAVAILABLE / CANCELLED / INTERNAL / UNKNOWN), reconnects with resume_session_id + resume_offset so the live tail continues without the caller seeing the disconnect.
Server replies of SESSION_NOT_FOUND or INVALID_RESUME_OFFSET drop the resume state and fall back to a fresh init; REPLAY_GAP is treated as a non-fatal warning. Servers that don't speak the resume wire protocol emit session_id="" and the resume branch is never entered, so behavior is unchanged against older backends.
Three new e2e tests cover the wire shape (session_id + cumulative offset), the in-band SESSION_NOT_FOUND reject contract for unknown resume tokens, and end-to-end recovery across a forced disconnect. Eight unit tests cover the transport-error classifier and the wire field accessors.
Also fixes three latent _sandbox_id closu...
v0.23.3
v0.23.3 (2026-05-21)
This release is published under the Apache-2.0 License.
Bug Fixes
- Add file operation exec fallback (
108f860)
Detailed Changes: v0.23.2...v0.23.3
v0.23.2
v0.23.2 (2026-05-20)
This release is published under the Apache-2.0 License.
Bug Fixes
Detailed Changes: v0.23.1...v0.23.2
v0.23.1
v0.23.1 (2026-05-20)
This release is published under the Apache-2.0 License.
Bug Fixes
- Raise gRPC message limit (
152bc7d)
Detailed Changes: v0.23.0...v0.23.1
v0.23.0
v0.23.0 (2026-05-08)
This release is published under the Apache-2.0 License.
Documentation
- Clarify mounted_files are read-only at runtime (
d0b6895)
Update docstrings across Sandbox.run(), Session.sandbox(), and @session.function() to document that mounted files are read-only and suggest using write_file() for writable files.
https://claude.ai/code/session_01BrtogWK3Y6KWdcLNsKxxJV
Features
- Remove pickle serialization from remote functions (
12056f3)
BREAKING CHANGE: Serialization, FunctionSerializationError, and the serialization= keyword argument on Session.function() and RemoteFunction are removed. JSON is the only serialization mode for remote functions; arguments, closures, captured globals, and return values must be JSON-serializable.
Breaking Changes
serialization,FunctionSerializationError, and theserialization=keyword argument onSession.function()andRemoteFunctionare removed. JSON is the only serialization mode for remote functions; arguments, closures, captured globals, and return values must be JSON-serializable.
Detailed Changes: v0.22.0...v0.23.0
v0.22.0
v0.22.0 (2026-04-29)
This release is published under the Apache-2.0 License.
Features
- Add poll retry budget and RPC timeout configuration (
db6c818)
Plumbing for the upcoming retry wrapper. Two SandboxDefaults fields:
- poll_retry_budget_seconds (30s): wall-clock retry budget. Separate
from request_timeout_seconds because one is cumulative across
attempts, the other is per-call. - poll_rpc_timeout_seconds (15s): per-call poll Get deadline so a
wedged poll fails fast instead of riding the broader request timeout.
_validate_poll_config rejects NaN/inf/negative at every construction site; NaN would silently defeat the wall-clock deadline check once the retry loop lands.
- Add retryable/fatal exception subclasses for poll translation (
720db5f)
SandboxNotRunningError and SandboxTimeoutError each span retryable and fatal cases (UNAVAILABLE vs CANCELLED; DEADLINE_EXCEEDED vs command timeout). Class-based dispatch can't distinguish them today, blocking the upcoming poll retry classifier.
Add four subclasses and wire the translator to emit them: SandboxUnavailableError, SandboxRequestTimeoutError, SandboxCommandTimeoutError, SandboxResourceExhaustedError. Umbrella parents are preserved so existing except clauses still catch.
- Retry post-stop NOT_FOUND before surfacing backend ambiguity (
1adca9b)
The backend persists terminal state for stopped sandboxes, so post-stop Get should return COMPLETED or FAILED. NOT_FOUND here is a narrow race with the backend's terminal-state write, or rollout skew.
On post-stop NOT_FOUND (_stop_owned or _missing_ok_observe path), retry Get within NOT_FOUND_AFTER_STOP_RETRY_BUDGET_SECONDS (2s). If the race resolves, waiters see the real terminal. If NOT_FOUND persists, raise SandboxTerminalStateUnavailableError so callers see the ambiguity explicitly rather than a fabricated terminal.
The retry lives in _do_poll_complete, not _classify_poll_error, because NOT_FOUND retryability is contextual (stop-state-machine-aware), not class-based. External-delete NOT_FOUND still surfaces immediately.
- Retry transient errors in the sandbox-status poll loop (
50dcbb9)
A single UNAVAILABLE/DEADLINE_EXCEEDED blip or RESOURCE_EXHAUSTED spike previously propagated as a fatal SandboxError even when the sandbox itself was healthy. Wrap the shared _running_task / _complete_task with _poll_with_retry; _classify_poll_error dispatches on subclass, with SandboxNotFoundError short-circuiting to fatal.
Retry state lives in the coroutine, not on self, because concurrent waiters share the poll task.
Honors AIP-193 retry_delay when present (capped so a misconfigured server can't stall). Decorrelated jitter on the no-hint path prevents fleet-wide synchronization during regional outages.
Detailed Changes: v0.21.0...v0.22.0
v0.21.0
v0.21.0 (2026-04-23)
This release is published under the Apache-2.0 License.
Features
Regenerate gateway protos with the ListSandboxes pagination fields and route Sandbox.list() through a shared paginate_async helper. Reuse the helper for discovery list RPCs so all paginated list calls share deadline, loop-detection, and max-page handling.
This prevents high-volume listings from silently stopping at the server's first-page cap.
Detailed Changes: v0.20.0...v0.21.0
v0.20.0
v0.20.0 (2026-04-22)
This release is published under the Apache-2.0 License.
Chores
- proto: Regenerate stubs with profile_names fields (
cd5e6bc)
Bump buf.build plugin pins so the vendored stubs pick up two newly added proto fields: StartSandboxRequest.profile_names (field 33) and ListSandboxesRequest.profile_names (field 7). The previous pins predated these fields and produced stubs that silently dropped them.
Refs: #110
Features
- Add profile_names field support across SDK surfaces (
5acc816)
Adds a name-based profile selector alongside the existing ID-based profile_ids. profile_names is the preferred form; profile_ids remains supported and both may be combined.
The two selectors resolve independently through None/empty/defaults precedence: setting one explicitly does not suppress the other's default. Threaded through SandboxDefaults, Sandbox.run/list, Session.sandbox/list, @session.function, and cwsandbox ls with a new --profile-name flag.
Requires a backend version that accepts the new wire fields; older backends silently drop profile_names.
Closes #110
Detailed Changes: v0.19.3...v0.20.0