#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#
| Object | Lifetime | Meaning |
|---|---|---|
| Session | Stable until deleted, sandbox hard TTL, or sandbox deletion | Desired process specification and lifecycle policy |
| Attempt | One operating-system process execution | Changes after a restart, replacement, or runtime recovery |
| Event | Retained according to the session policy | Ordered state, output, control, and exit record |
| Attachment | One SSE or WebSocket connection | Ephemeral 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#
| Connection | Use it for | Reconnect behavior |
|---|---|---|
| HTTP | Create, inspect, update, stop, delete, input, signal, resize, and paged event reads | Retry normal requests; use Idempotency-Key, input_id, and expected_attempt_id where applicable |
| SSE | One-way retained and live event consumption | Reconnect with Last-Event-ID or after; deduplicate by seq |
| WebSocket | Duplex input/control plus retained and live events | Reconnect 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.
gopackage 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_idfor each logical write. - Reuse the same
input_idand content when retrying an ambiguous request. Once its receipt is durable, the server returnsduplicate: truewithout queueing it again. - Include
expected_attempt_idwhen input belongs to a particular process attempt. A newer attempt causes409 session_conflictinstead of receiving stale input. - Set
eof: trueto 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:
- Process each event and remember its
seqafter the side effect commits. - Reconnect SSE with
Last-Event-ID: <seq>or reconnect SSE/WebSocket with?after=<seq>. - 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_recoveryisrestart
The session ID and event sequence remain stable across these boundaries. runtime_generation distinguishes events and attempts produced by different sandbox runtime instances.
| Policy | Values | Default behavior |
|---|---|---|
restart.policy | never, on_failure, always | never |
restart.max_restarts | Non-negative integer | 5 restarts within the window |
restart.window_seconds | Non-negative integer | 60 seconds |
restart.initial_backoff_ms / max_backoff_ms | Non-negative integers | Exponential backoff from 250 ms to 5 s |
runtime_recovery | restart, stop | Start a new attempt after runtime replacement |
idle_timeout_seconds | 0 or positive integer | 0 disables session idle expiry |
max_lifetime_seconds | 0 or positive integer | 0 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:
| Type | Ready when |
|---|---|
process | The operating-system process starts |
delay | delay_ms elapses while the attempt remains alive |
output | The 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#
| Command | Purpose |
|---|---|
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.yaml | Create 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.yaml | Replace 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> --replace | Replace 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> TERM | Signal 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:
| Method | Path | Purpose |
|---|---|---|
GET, POST | /api/v1/sandboxes/{'{id}'}/sessions | List 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-state | Start or stop |
POST | /api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/attempts | Create or replace an attempt |
POST | /api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/inputs | Queue input or EOF |
POST | /api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/signals | Send a signal |
PUT | /api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/terminal | Resize a PTY |
GET | /api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/events | Read retained events |
GET | /api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/events/stream | Attach with SSE |
GET | /api/v1/sandboxes/{'{id}'}/sessions/{'{session_id}'}/ws | Attach 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.