SANDBOX/Session Supervisor

#Session Supervisor

The Session Supervisor runs and observes long-lived processes inside a sandbox. A session has a stable identity, one current process attempt, and a retained event journal. Client connections are attachments to that state; they do not own it.

Use a supervised session when a process must continue across API reconnects, expose replayable output, accept explicit stdin and EOF, or recover after the sandbox runtime is replaced. Use Contexts for short-lived command execution and REPL-oriented workflows that do not need a durable event journal.

Closing an HTTP response, SSE stream, or WebSocket never stops the session and never closes process stdin. Stop, delete, signal, and EOF are explicit operations.

Execution Model#

ObjectLifetimeMeaning
SessionStable until deleted, sandbox hard TTL, or sandbox deletionDesired process specification and lifecycle policy
AttemptOne operating-system process executionChanges after a restart, replacement, or runtime recovery
EventRetained according to the session policyOrdered state, output, control, and exit record
AttachmentOne SSE or WebSocket connectionEphemeral view over retained and live events

Every event has a monotonically increasing seq. Process-bound operations can include expected_attempt_id so stale clients cannot write to, signal, or resize a newer attempt accidentally.

Connection Choice#

ConnectionUse it forReconnect behavior
HTTPCreate, inspect, update, stop, delete, input, signal, resize, and paged event readsRetry normal requests; use Idempotency-Key, input_id, and expected_attempt_id where applicable
SSEOne-way retained and live event consumptionReconnect with Last-Event-ID or after; deduplicate by seq
WebSocketDuplex input/control plus retained and live eventsReconnect with after; the session and stdin remain unchanged while disconnected

SSE is the simplest default for output consumers. WebSocket is useful for interactive PTY attachments, but it does not change the lifecycle or delivery model.

SDK Quickstart#

These examples attach to an existing sandbox from SANDBOX0_SANDBOX_ID, start /bin/cat, send one input followed by explicit EOF, consume the journal, then prove the session still exists after the stream is closed.

go
package main import ( "context" "encoding/base64" "fmt" "io" "log" "os" "time" sandbox0 "github.com/sandbox0-ai/sdk-go" "github.com/sandbox0-ai/sdk-go/pkg/apispec" ) func main() { ctx := context.Background() options := []sandbox0.Option{ sandbox0.WithToken(os.Getenv("SANDBOX0_TOKEN")), } if baseURL := os.Getenv("SANDBOX0_BASE_URL"); baseURL != "" { options = append(options, sandbox0.WithBaseURL(baseURL)) } client, err := sandbox0.NewClient(options...) if err != nil { log.Fatal(err) } sandbox := client.Sandbox(os.Getenv("SANDBOX0_SANDBOX_ID")) created, err := sandbox.CreateSession(ctx, apispec.ExecutionSessionSpec{ Name: apispec.NewOptString("docs-pipe"), Command: []string{"/bin/cat"}, Io: apispec.NewOptExecutionSessionIOSpec(apispec.ExecutionSessionIOSpec{ Mode: apispec.NewOptExecutionSessionIOMode(apispec.ExecutionSessionIOModePipes), }), }, &sandbox0.CreateSessionOptions{ IdempotencyKey: fmt.Sprintf("docs-%d", time.Now().UnixNano()), }) if err != nil { log.Fatal(err) } defer sandbox.DeleteSession(ctx, created.ID) attempt, ok := created.Attempt.Get() if !ok { log.Fatal("session has no current attempt") } stream, err := sandbox.WatchSessionEvents(ctx, created.ID, nil) if err != nil { log.Fatal(err) } _, err = sandbox.WriteSessionInput(ctx, created.ID, apispec.ExecutionSessionInputRequest{ InputID: fmt.Sprintf("input-%d", time.Now().UnixNano()), ExpectedAttemptID: apispec.NewOptString(attempt.ID), DataBase64: apispec.NewOptString(base64.StdEncoding.EncodeToString([]byte("hello from Go\n"))), EOF: apispec.NewOptBool(true), }) if err != nil { log.Fatal(err) } var lastSeq int64 for { event, err := stream.Recv() if err != nil { if err == io.EOF { break } log.Fatal(err) } lastSeq = event.Seq if encoded, ok := event.DataBase64.Get(); ok { data, err := base64.StdEncoding.DecodeString(encoded) if err != nil { log.Fatal(err) } fmt.Print(string(data)) } if event.Type == "attempt.exited" { break } } if err := stream.Close(); err != nil { log.Fatal(err) } current, err := sandbox.GetSession(ctx, created.ID) if err != nil { log.Fatal(err) } fmt.Printf("session=%s phase=%s cursor=%d\n", current.ID, current.Phase, lastSeq) }

Input, EOF, and Attempt Fencing#

POST /inputs accepts base64-encoded bytes, explicit EOF, or both. The response means the operation was accepted into the current attempt's input queue; it does not mean the application consumed the bytes.

  • Generate a unique input_id for each logical write.
  • Reuse the same input_id and content when retrying an ambiguous request. Once its receipt is durable, the server returns duplicate: true without queueing it again.
  • Include expected_attempt_id when input belongs to a particular process attempt. A newer attempt causes 409 session_conflict instead of receiving stale input.
  • Set eof: true to close stdin after earlier queued bytes. Disconnecting an attachment is never EOF.

No process API can provide exactly-once application consumption. If the runtime fails after bytes reach the process but before the input receipt is durable, a retry can replay them. Design the application protocol to tolerate this ambiguity when it matters.

Retained Events and Reconnect#

Paged reads and streams share the same journal cursor:

  1. Process each event and remember its seq after the side effect commits.
  2. Reconnect SSE with Last-Event-ID: <seq> or reconnect SSE/WebSocket with ?after=<seq>.
  3. Deduplicate by seq; replay is expected.

For a limited HTTP page, continue with the final event's seq. The page's cursor.earliest and cursor.latest describe the full retained window, not the next-page position.

The default retention is 64 MiB and 24 hours per session. A cursor older than retained history returns 410 event_cursor_expired and reports the earliest sequence in the retained window. Reconcile current session state with GET /sessions/{'{session_id}'}, then resume with after=earliest-1 (or after=latest when the retained window is empty), or apply application-specific recovery.

Slow live subscribers can be detached when their in-memory delivery buffer fills. This does not lose retained journal data; reconnect from the last committed sequence.

Attempts, Restart, and Runtime Recovery#

An attempt ID identifies one process execution. It changes when:

  • the restart policy starts the process again after exit
  • a client creates or replaces an attempt
  • an execution-relevant session specification is replaced
  • a paused or replaced sandbox runtime is restored and runtime_recovery is restart

The session ID and event sequence remain stable across these boundaries. runtime_generation distinguishes events and attempts produced by different sandbox runtime instances.

PolicyValuesDefault behavior
restart.policynever, on_failure, alwaysnever
restart.max_restartsNon-negative integer5 restarts within the window
restart.window_secondsNon-negative integer60 seconds
restart.initial_backoff_ms / max_backoff_msNon-negative integersExponential backoff from 250 ms to 5 s
runtime_recoveryrestart, stopStart a new attempt after runtime replacement
idle_timeout_seconds0 or positive integer0 disables session idle expiry
max_lifetime_seconds0 or positive integer0 disables session lifetime expiry

Sandbox pause stops the current runtime after flushing session state into the checkpointed root filesystem. Resume creates a new runtime generation. It does not restore the old PID, memory, sockets, or terminal connection; it either creates a new attempt or leaves the session stopped according to runtime_recovery.

Readiness#

Readiness controls when an attempt moves from starting to running:

TypeReady when
processThe operating-system process starts
delaydelay_ms elapses while the attempt remains alive
outputThe configured byte sequence appears across stdout, stderr, or PTY output

For delay and output, timeout_ms terminates an attempt that never becomes ready. Readiness observes process bytes only; it does not interpret an application protocol.

PTY Sessions#

Set io.mode to pty with initial rows, cols, and term. Use PUT /terminal or a WebSocket resize message to update the terminal size. Use expected_attempt_id on resize and signal messages to prevent a stale terminal attachment from controlling a replacement attempt.

PTY output is emitted as stream: "pty". Pipe sessions keep stdout and stderr separate.

CLI Reference#

CommandPurpose
s0 sandbox session list <sandbox-id>List sessions
s0 sandbox session create <sandbox-id> -- <command...>Create and start a session
s0 sandbox session create <sandbox-id> --spec-file session.yamlCreate from the complete API specification
s0 sandbox session get <sandbox-id> <session-id>Inspect phase, attempt, generation, and cursor
s0 sandbox session update <sandbox-id> <session-id> --spec-file session.yamlReplace the specification with PUT semantics
s0 sandbox session start|stop <sandbox-id> <session-id>Set desired state
s0 sandbox session attempt <sandbox-id> <session-id> --replaceReplace the current attempt explicitly
s0 sandbox session input <sandbox-id> <session-id> ...Queue UTF-8, base64, or file input and optional EOF
s0 sandbox session signal <sandbox-id> <session-id> TERMSignal the current attempt
s0 sandbox session resize <sandbox-id> <session-id> <rows> <cols>Resize a PTY
s0 sandbox session events <sandbox-id> <session-id> --after <seq>Read retained events
s0 sandbox session events <sandbox-id> <session-id> --follow --last-event-id <seq>Follow retained and live events over SSE
s0 sandbox session delete <sandbox-id> <session-id>Stop and delete the identity and journal

HTTP API#

All public paths are scoped by sandbox:

MethodPathPurpose
GET, POST/api/v1/sandboxes/{'{id}'}/sessionsList or create
GET, PUT, DELETE/api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}Inspect, replace, or delete
PUT/api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/desired-stateStart or stop
POST/api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/attemptsCreate or replace an attempt
POST/api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/inputsQueue input or EOF
POST/api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/signalsSend a signal
PUT/api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/terminalResize a PTY
GET/api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/eventsRead retained events
GET/api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/events/streamAttach with SSE
GET/api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/wsAttach with WebSocket

Deleting the sandbox or reaching its hard TTL deletes its sessions. Deleting an individual session stops its current attempt and removes its retained state.