Skip to documentation
API + guides

01Documentation

Supervised Sessions

A supervised session runs a process as a sandbox-owned resource instead of tying the process to one client connection. The session keeps a stable identity and a replayable event journal while its current operating-system process is represented by an attempt.

Use supervised sessions for:

  • background workers and agent runtimes that must outlive an SDK call
  • processes whose output must be replayed after a client reconnects
  • interactive PTY workloads with explicit input, signals, and terminal resize
  • processes that should restart after failure or after the sandbox runtime is replaced

Use Contexts for one-shot commands and REPL workflows. Use Sandbox Services when the workload needs a public HTTP entrypoint; a supervised session does not create a public URL by itself.

Closing an SDK event stream, SSE connection, or WebSocket only detaches that client. It does not stop the session and does not close process stdin. Stop, delete, signal, and EOF are explicit operations.

Choose The Right Execution Primitive#

PrimitiveBest ForProcess Ownership
ContextOne-shot commands and stateful REPL callsThe context API owns one command or REPL process
Supervised sessionLong-running, reconnectable, restartable processesThe sandbox owns a stable session and its current attempt
Sandbox ServicePublic HTTP routes, route policy, and on-demand command or function executionThe service runtime or your application owns the backing process
SSHHuman shell access and standard file transferThe SSH connection owns the interactive shell

Mental Model#

ObjectLifetimeMeaning
SessionUntil explicitly deleted, sandbox deletion, or sandbox hard_ttlStable identity, process specification, desired state, and lifecycle policy
AttemptOne operating-system process executionReplaced after a restart, explicit replacement, specification change, or runtime recovery
AttachmentOne SDK event stream or WebSocket connectionTemporary view over retained and live events
EventRetained according to the session policyOrdered record of lifecycle, output, input acknowledgement, and control activity

The important distinction is between two kinds of continuity:

  • Connection continuity: disconnecting a client leaves the same attempt running.
  • Runtime continuity: replacing the sandbox runtime ends the old attempt. The session identity and journal remain, and runtime_recovery decides whether Sandbox0 starts a new attempt.

Every event has a monotonically increasing seq. An attempt has its own attempt_id, and events also include runtime_generation so clients can distinguish output produced before and after sandbox runtime replacement.

Create A Long-Running Session#

The following examples create a worker that prints READY, emits one line per second, restarts after a non-zero exit, and starts a new attempt when the sandbox runtime is replaced.

The SDK snippets assume you already created a client and selected a sandbox as shown in Get Started.

go
session, err := sandbox.CreateSession(ctx, apispec.ExecutionSessionSpec{ Name: apispec.NewOptString("docs-worker"), Command: []string{"/bin/sh", "-lc", "echo READY; i=0; while true; do i=$((i+1)); echo tick-$i; sleep 1; done"}, Lifecycle: apispec.NewOptExecutionSessionLifecycleSpec( apispec.ExecutionSessionLifecycleSpec{ Restart: apispec.NewOptExecutionSessionRestartSpec( apispec.ExecutionSessionRestartSpec{ Policy: apispec.NewOptExecutionSessionRestartPolicy( apispec.ExecutionSessionRestartPolicyOnFailure, ), }, ), RuntimeRecovery: apispec.NewOptExecutionSessionRuntimeRecoveryPolicy( apispec.ExecutionSessionRuntimeRecoveryPolicyRestart, ), }, ), Readiness: apispec.NewOptExecutionSessionReadinessSpec( apispec.ExecutionSessionReadinessSpec{ Type: apispec.NewOptExecutionSessionReadinessType( apispec.ExecutionSessionReadinessTypeOutput, ), Output: apispec.NewOptString("READY"), TimeoutMs: apispec.NewOptInt32(30_000), }, ), }, &sandbox0.CreateSessionOptions{ IdempotencyKey: "docs-worker-v1", }) if err != nil { log.Fatal(err) } fmt.Println(session.ID)

Creation defaults to desired_state: running, so the first attempt starts immediately. An Idempotency-Key lets a client safely retry the same creation request: the same key and normalized specification return the existing session, while reusing the key with a different specification returns a conflict.

Follow Output And Reconnect#

Use the SDK event stream for one-way output consumption. The stream first replays retained events after the requested cursor and then continues with live events.

go
stream, err := sandbox.WatchSessionEvents(ctx, session.ID, nil) if err != nil { log.Fatal(err) } var lastSeq int64 outputCount := 0 for outputCount < 2 { event, err := stream.Recv() if err != nil { 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)) outputCount++ } } if err := stream.Close(); err != nil { log.Fatal(err) } // Closing the stream did not stop the session. Reattach after the last // processed sequence to receive only newer events. stream, err = sandbox.WatchSessionEvents(ctx, session.ID, &sandbox0.SessionEventStreamOptions{ After: lastSeq, }) if err != nil { log.Fatal(err) } event, err := stream.Recv() if err != nil { log.Fatal(err) } fmt.Printf("reattached at seq=%d\n", event.Seq) if err := stream.Close(); err != nil { log.Fatal(err) }

Commit your application side effect before saving seq. On reconnect, replay is expected, so consumers should deduplicate events by sequence.

Paged Events, SSE, And WebSocket#

SDK OperationUse It ForReconnect Behavior
ListSessionEvents / list_session_events / listSessionEventsBounded, paged readsContinue after the final processed seq
WatchSessionEvents / watch_session_events / watchSessionEventsRetained plus live events over SSEReopen with after or Last-Event-ID
ConnectSession / connect_session / connectSessionDuplex input, signal, resize, and events over WebSocketReopen with after; disconnect does not imply EOF

SSE is the simplest default for workers and log consumers. Use WebSocket when one attachment needs both event delivery and interactive PTY control.

Inspect And Manage Sessions#

Session lifecycle is controlled independently from attachments.

TaskGoPythonTypeScriptCLI
ListListSessionslist_sessionslistSessionss0 sandbox session list
InspectGetSessionget_sessiongetSessions0 sandbox session get
StopSetSessionDesiredState(..., apispec.ExecutionSessionDesiredStateStopped)set_session_desired_state(..., ExecutionSessionDesiredState.STOPPED)setSessionDesiredState(..., 'stopped')s0 sandbox session stop
StartSetSessionDesiredState(..., apispec.ExecutionSessionDesiredStateRunning)set_session_desired_state(..., ExecutionSessionDesiredState.RUNNING)setSessionDesiredState(..., 'running')s0 sandbox session start
Replace attemptCreateSessionAttemptcreate_session_attemptcreateSessionAttempts0 sandbox session attempt --replace
DeleteDeleteSessiondelete_sessiondeleteSessions0 sandbox session delete

Stopping sets the desired state to stopped and ends the current attempt while retaining the session and journal. Starting creates a new attempt. Deleting stops the current attempt and removes the session identity and retained events.

Do not use a signal as a replacement for stop when you want the session to remain stopped. A signal can make the process exit, after which its restart policy may create another attempt. Set the desired state to stopped instead.

Send Input And EOF#

Pipe and PTY sessions can accept binary-safe input. The examples below assume session is a running process that reads stdin, such as /bin/cat.

go
attempt, ok := session.Attempt.Get() if !ok { log.Fatal("session has no current attempt") } _, err := sandbox.WriteSessionInput(ctx, session.ID, apispec.ExecutionSessionInputRequest{ InputID: "input-1", ExpectedAttemptID: apispec.NewOptString(attempt.ID), DataBase64: apispec.NewOptString(base64.StdEncoding.EncodeToString([]byte("hello\n"))), EOF: apispec.NewOptBool(true), }) if err != nil { log.Fatal(err) }

Input acceptance means the bytes entered the current attempt's input queue; it does not prove that the application consumed them.

  • Generate a unique input_id for each logical write.
  • Retry an ambiguous request with the same input_id and identical content.
  • Include expected_attempt_id when input belongs to one specific process attempt.
  • Set eof: true to close stdin after earlier queued bytes. Detaching a client is never EOF.

No process API can guarantee exactly-once application consumption. If the process receives bytes before the input receipt becomes durable, retrying after a transport failure can replay them. Use an application-level request ID when duplicate consumption would be unsafe.

Pipes And PTY#

ModeOutputBest For
pipesSeparate stdout and stderr eventsWorkers, servers, and non-interactive commands
ptyCombined pty outputShells and terminal applications

For PTY sessions, configure initial rows, cols, and term, then use ResizeSessionTerminal, resize_session_terminal, resizeSessionTerminal, or s0 sandbox session resize when the terminal size changes. Include the expected attempt ID so a stale terminal attachment cannot resize a replacement attempt.

Lifecycle And Recovery#

Desired State And Phase#

lifecycle.desired_state is the state requested by the user. phase is the supervisor's current observation.

PhaseMeaning
pending, startingThe session is waiting for or starting an attempt
runningThe current attempt passed readiness
backoffA restart is scheduled after a failed attempt
stopping, stoppedThe session is stopping or has no running attempt by request
exitedThe attempt exited and restart policy did not restart it
failedStartup, readiness, or restart limits failed
paused, suspendedThe attempt was paused or ended during sandbox runtime replacement

Restart Policy#

Restart policy applies when a process exits inside the current sandbox runtime.

PolicyBehavior
neverDo not restart after exit
on_failureRestart only after a non-zero exit
alwaysRestart after any exit unless the session is being stopped or deleted

The default restart window allows five restarts within 60 seconds with exponential backoff from 250 ms to 5 seconds.

Runtime Recovery#

Runtime recovery applies after pause/resume or another sandbox runtime replacement.

PolicyBehavior
restartStart a new attempt when the session was active before replacement
stopKeep the session and journal but change the desired state to stopped

Runtime recovery is process restart, not process checkpoint/restore. The old PID, memory, sockets, terminal connection, and in-flight requests do not survive runtime replacement.

The session is scoped to one sandbox identity. A fork receives filesystem state from its source, but copied supervised sessions are cleared before the fork starts so it cannot execute the source sandbox's processes. Sandbox deletion and hard_ttl delete the session and journal.

Readiness#

Readiness controls when an attempt moves from starting to running.

TypeReady When
processThe operating-system process starts
delaydelay_ms elapses while the attempt stays alive
outputThe configured byte sequence appears in 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 perform HTTP, TCP, or application-protocol health checks.

idle_timeout_seconds measures session activity such as process output, accepted input, signals, and terminal resize. Merely keeping an attachment open or reading session metadata does not keep an otherwise idle session active. max_lifetime_seconds is measured from session creation. Set either value to 0 to disable that limit.

Event Journal And Delivery#

The journal contains lifecycle events, process output, accepted input receipts, signals, and terminal resize events. Common event groups are:

GroupExamples
Session lifecyclesession.created, session.ready, session.backoff, session.failed, session.expired
Attempt lifecycleattempt.starting, attempt.started, attempt.stopping, attempt.exited
Process outputoutput with stream: stdout, stderr, or pty and base64-encoded bytes
Controlinput.accepted, signal.sent, terminal.resized

Delivery is cursor-based and at least once:

  1. Process an event and commit its application side effect.
  2. Save the event's seq.
  3. Reconnect after that sequence.
  4. Deduplicate replayed events by seq.

The default retention is 64 MiB and 24 hours per session. If a requested cursor is older than retained history, the SDK returns event_cursor_expired with the earliest available sequence. Read the current session state, reconcile it with your application, and continue from the retained window.

A slow live subscriber can be detached when its in-memory buffer fills. Retained journal data remains available, so reconnect from the last committed sequence.

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 a complete 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 complete specification
s0 sandbox session start|stop <sandbox-id> <session-id>Change desired state
s0 sandbox session attempt <sandbox-id> <session-id> --replaceReplace the current attempt
s0 sandbox session input <sandbox-id> <session-id> ...Send input or explicit 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> --followReplay and follow events
s0 sandbox session delete <sandbox-id> <session-id>Stop and delete the session and journal

Next Steps#