# 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](/docs/sandbox/contexts) for one-shot commands and REPL workflows. Use [Sandbox Services](/docs/sandbox/services) when the workload needs a public HTTP entrypoint; a supervised session does not create a public URL by itself.

<Callout variant="info">
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.
</Callout>

## Choose The Right Execution Primitive

| Primitive | Best For | Process Ownership |
|-----------|----------|-------------------|
| Context | One-shot commands and stateful REPL calls | The context API owns one command or REPL process |
| Supervised session | Long-running, reconnectable, restartable processes | The sandbox owns a stable session and its current attempt |
| Sandbox Service | Public HTTP routes, route policy, and on-demand command or function execution | The service runtime or your application owns the backing process |
| SSH | Human shell access and standard file transfer | The SSH connection owns the interactive shell |

## Mental Model

| Object | Lifetime | Meaning |
|--------|----------|---------|
| Session | Until explicitly deleted, sandbox deletion, or sandbox `hard_ttl` | Stable identity, process specification, desired state, and lifecycle policy |
| Attempt | One operating-system process execution | Replaced after a restart, explicit replacement, specification change, or runtime recovery |
| Attachment | One SDK event stream or WebSocket connection | Temporary view over retained and live events |
| Event | Retained according to the session policy | Ordered 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](/docs/get-started).

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `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)`
    },
    {
      label: "Python",
      language: "python",
      code: `from sandbox0 import SessionCreateOptions
from sandbox0.apispec.models.execution_session_spec import ExecutionSessionSpec

session = sandbox.create_session(
    ExecutionSessionSpec.from_dict({
        "name": "docs-worker",
        "command": [
            "/bin/sh",
            "-lc",
            "echo READY; i=0; while true; do i=$((i+1)); echo tick-$i; sleep 1; done",
        ],
        "lifecycle": {
            "restart": {"policy": "on_failure"},
            "runtime_recovery": "restart",
        },
        "readiness": {
            "type": "output",
            "output": "READY",
            "timeout_ms": 30000,
        },
    }),
    SessionCreateOptions(idempotency_key="docs-worker-v1"),
)
print(session.id)`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `const session = await sandbox.createSession({
    name: 'docs-worker',
    command: [
        '/bin/sh',
        '-lc',
        'echo READY; i=0; while true; do i=$((i+1)); echo tick-$i; sleep 1; done',
    ],
    lifecycle: {
        restart: { policy: 'on_failure' },
        runtimeRecovery: 'restart',
    },
    readiness: {
        type: 'output',
        output: 'READY',
        timeoutMs: 30000,
    },
}, {
    idempotencyKey: 'docs-worker-v1',
});
console.log(session.id);`
    },
    {
      label: "CLI",
      language: "bash",
      code: `SESSION_JSON="$(s0 -o json sandbox session create "$SANDBOX_ID" \\
    --name docs-worker \\
    --idempotency-key docs-worker-v1 \\
    --restart on_failure \\
    --runtime-recovery restart \\
    --readiness output \\
    --ready-output READY \\
    -- /bin/sh -lc \\
    'echo READY; i=0; while true; do i=$((i+1)); echo tick-$i; sleep 1; done')"

SESSION_ID="$(jq -r '.id' <<<"$SESSION_JSON")"
printf 'Session ID: %s\\n' "$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.

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `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)
}`
    },
    {
      label: "Python",
      language: "python",
      code: `import base64

from sandbox0 import SessionEventStreamOptions

last_seq = 0
output_count = 0
with sandbox.watch_session_events(session.id) as stream:
    for event in stream.iter_events():
        value = event.to_dict()
        last_seq = value["seq"]
        if "data_base64" in value:
            print(base64.b64decode(value["data_base64"]).decode(), end="")
            output_count += 1
        if output_count == 2:
            break

# Closing the stream did not stop the session. Resume after last_seq.
with sandbox.watch_session_events(
    session.id,
    SessionEventStreamOptions(after=last_seq),
) as resumed:
    event = next(resumed.iter_events())
    print(f"reattached at seq={event.seq}")`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `let lastSeq = 0;
let outputCount = 0;
const stream = await sandbox.watchSessionEvents(session.id);

for await (const event of stream) {
    lastSeq = event.seq;
    if (event.dataBase64) {
        process.stdout.write(Buffer.from(event.dataBase64, 'base64'));
        outputCount++;
    }
    if (outputCount === 2) break;
}
await stream.close();

// Closing the stream did not stop the session. Resume after lastSeq.
const resumed = await sandbox.watchSessionEvents(session.id, {
    after: lastSeq,
});
for await (const event of resumed) {
    console.log('reattached at seq=' + event.seq);
    break;
}
await resumed.close();`
    },
    {
      label: "CLI",
      language: "bash",
      code: `s0 sandbox session events "$SANDBOX_ID" "$SESSION_ID" --follow

# Press Ctrl-C to detach. The first column is the event sequence.
s0 sandbox session get "$SANDBOX_ID" "$SESSION_ID"

# Replace 8 with the last sequence your consumer committed.
s0 sandbox session events "$SANDBOX_ID" "$SESSION_ID" --after 8 --follow`
    }
  ]}
/>

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 Operation | Use It For | Reconnect Behavior |
|---------------|------------|--------------------|
| `ListSessionEvents` / `list_session_events` / `listSessionEvents` | Bounded, paged reads | Continue after the final processed `seq` |
| `WatchSessionEvents` / `watch_session_events` / `watchSessionEvents` | Retained plus live events over SSE | Reopen with `after` or `Last-Event-ID` |
| `ConnectSession` / `connect_session` / `connectSession` | Duplex input, signal, resize, and events over WebSocket | Reopen 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.

| Task | Go | Python | TypeScript | CLI |
|------|----|--------|------------|-----|
| List | `ListSessions` | `list_sessions` | `listSessions` | `s0 sandbox session list` |
| Inspect | `GetSession` | `get_session` | `getSession` | `s0 sandbox session get` |
| Stop | `SetSessionDesiredState(..., apispec.ExecutionSessionDesiredStateStopped)` | `set_session_desired_state(..., ExecutionSessionDesiredState.STOPPED)` | `setSessionDesiredState(..., 'stopped')` | `s0 sandbox session stop` |
| Start | `SetSessionDesiredState(..., apispec.ExecutionSessionDesiredStateRunning)` | `set_session_desired_state(..., ExecutionSessionDesiredState.RUNNING)` | `setSessionDesiredState(..., 'running')` | `s0 sandbox session start` |
| Replace attempt | `CreateSessionAttempt` | `create_session_attempt` | `createSessionAttempt` | `s0 sandbox session attempt --replace` |
| Delete | `DeleteSession` | `delete_session` | `deleteSession` | `s0 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.

<Callout variant="warning">
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.
</Callout>

## 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`.

<Tabs
  tabs={[
    {
      label: "Go",
      language: "go",
      code: `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)
}`
    },
    {
      label: "Python",
      language: "python",
      code: `import base64

from sandbox0.apispec.models.execution_session_input_request import ExecutionSessionInputRequest

attempt_id = session.to_dict()["attempt"]["id"]
sandbox.write_session_input(
    session.id,
    ExecutionSessionInputRequest(
        input_id="input-1",
        expected_attempt_id=attempt_id,
        data_base64=base64.b64encode(b"hello\\n").decode(),
        eof=True,
    ),
)`
    },
    {
      label: "TypeScript",
      language: "typescript",
      code: `if (!session.attempt) throw new Error('session has no current attempt');

await sandbox.writeSessionInput(session.id, {
    inputId: 'input-1',
    expectedAttemptId: session.attempt.id,
    dataBase64: Buffer.from('hello\\n').toString('base64'),
    eof: true,
});`
    },
    {
      label: "CLI",
      language: "bash",
      code: `ATTEMPT_ID="$(s0 -o json sandbox session get "$SANDBOX_ID" "$SESSION_ID" \\
    | jq -r '.attempt.id')"

s0 sandbox session input "$SANDBOX_ID" "$SESSION_ID" \\
    --attempt-id "$ATTEMPT_ID" \\
    --input-id input-1 \\
    --data $'hello\\n' \\
    --eof`
    }
  ]}
/>

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

| Mode | Output | Best For |
|------|--------|----------|
| `pipes` | Separate `stdout` and `stderr` events | Workers, servers, and non-interactive commands |
| `pty` | Combined `pty` output | Shells 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.

| Phase | Meaning |
|-------|---------|
| `pending`, `starting` | The session is waiting for or starting an attempt |
| `running` | The current attempt passed readiness |
| `backoff` | A restart is scheduled after a failed attempt |
| `stopping`, `stopped` | The session is stopping or has no running attempt by request |
| `exited` | The attempt exited and restart policy did not restart it |
| `failed` | Startup, readiness, or restart limits failed |
| `paused`, `suspended` | The attempt was paused or ended during sandbox runtime replacement |

### Restart Policy

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

| Policy | Behavior |
|--------|----------|
| `never` | Do not restart after exit |
| `on_failure` | Restart only after a non-zero exit |
| `always` | Restart 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.

| Policy | Behavior |
|--------|----------|
| `restart` | Start a new attempt when the session was active before replacement |
| `stop` | Keep the session and journal but change the desired state to stopped |

<Callout variant="warning">
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.
</Callout>

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.

An explicit copied-session reset also clears legacy session state without an owner marker. Without that reset instruction, ownerless legacy state is adopted in place. An existing marker matching the target sandbox preserves its sessions on an activation retry.

### 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 stays alive |
| `output` | The 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:

| Group | Examples |
|-------|----------|
| Session lifecycle | `session.created`, `session.ready`, `session.backoff`, `session.failed`, `session.expired` |
| Attempt lifecycle | `attempt.starting`, `attempt.started`, `attempt.stopping`, `attempt.exited` |
| Process output | `output` with `stream: stdout`, `stderr`, or `pty` and base64-encoded bytes |
| Control | `input.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

| 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 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.yaml` | Replace the complete specification |
| `s0 sandbox session start\|stop <sandbox-id> <session-id>` | Change desired state |
| `s0 sandbox session attempt <sandbox-id> <session-id> --replace` | Replace the current attempt |
| `s0 sandbox session input <sandbox-id> <session-id> ...` | Send input or explicit 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> --follow` | Replay and follow events |
| `s0 sandbox session delete <sandbox-id> <session-id>` | Stop and delete the session and journal |

## Next Steps

<CardGroup>
  <Card title="Contexts" href="/docs/sandbox/contexts" cta="Continue">
    Run one-shot commands and stateful REPL workflows.
  </Card>

  <Card title="Pause And Resume" href="/docs/sandbox/pause-resume" cta="Continue">
    Understand what survives when Sandbox0 releases and replaces a runtime.
  </Card>

  <Card title="Sandbox Services" href="/docs/sandbox/services" cta="Continue">
    Expose workloads through public HTTP routes with auth, CORS, and rate limits.
  </Card>
</CardGroup>
