# 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 connection continuity and the two runtime recovery modes:

- **Connection continuity:** disconnecting a client leaves the same attempt running.
- **Filesystem-only recovery:** replacing compute without a memory checkpoint ends the old attempt. The session identity and committed journal remain, and `runtime_recovery` decides whether Sandbox0 starts a new attempt.
- **Memory restoration:** a compatible memory resume or system migration preserves the captured attempt and process state, then rebinds the supervisor to the new runtime generation. Reconnect attachments after the transition.

Within each session, events have monotonically increasing `seq` values. 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.

### Create In An Exact Runtime Generation

When a process depends on generation-bound preparation, use the conditional
creation endpoint `POST /api/v1/sandboxes/{id}/sessions/conditional` with a
required `Idempotency-Key` and a body containing `sandbox_id`,
`runtime_generation`, and `spec`. The sandbox identity must match the URL.

The SDK creation options expose this as `RuntimeGeneration` (Go, `*int64`),
`runtimeGeneration` (TypeScript), or `runtime_generation` (Python), together with
the idempotency key. The gateway requires that exact running generation and
never auto-resumes. The supervisor checks its owner and generation through
initial process startup. Changed or unavailable identity returns HTTP 409.
An old gateway or supervisor lacks this distinct route; clients must not fall
back to unconditional creation.

Retries preserve the original sandbox/generation/specification. Reusing the key
with a different condition conflicts, including after memory migration. Attach
to the retained session explicitly instead. The condition governs creation;
the session's configured restart and runtime recovery policies still govern
later attempts. Use `runtime_recovery: stop` when replacement must await a new
application admission.

Conditional creation is an execution precondition, not a credential-clean
attestation or proof of storage/lifecycle completion. Its private creation
identity uses session state format 3, which older procd versions reject.
Ordinary sessions continue to write format 2. Rollback must preserve compatible
session/checkpoint state rather than discard the recorded condition.

## 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, set `io.mode: pty` and configure initial `rows`, `cols`, and `term` in `io.terminal`, 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 filesystem-only pause/resume or another cold runtime replacement. The default is `restart`. Explicit compatible memory restoration preserves the captured attempt instead of applying this restart policy.

| 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">
Filesystem-only runtime recovery starts a new process. Its old PID, memory, sockets, terminal connection, and in-flight requests do not survive. Explicit memory restore has separate compatibility requirements; existing external connections are still not guaranteed to survive.
</Callout>

The session is scoped to one sandbox identity. A filesystem-only fork receives filesystem state from its source, but copied supervised sessions are cleared before the child starts. An explicit memory fork instead retains captured session processes and rebinds their ownership to the child sandbox, as described in [Snapshot And Restore](/docs/sandbox/snapshot-restore#explicit-memory-fork). 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>


### Read a conditional creation receipt without resuming

Use `GET /api/v1/sandboxes/{id}/sessions/conditional/lookup` with the original
`Idempotency-Key` header and positive `runtime_generation` query parameter.
The gateway checks the running runtime identity and the supervisor checks the
same identity while reading its creation index. Neither side auto-resumes,
creates a session, restarts an attempt, updates activity, or repairs a journal.
The distinct two-component suffix cannot match older gateways' ordinary
`GET /sessions/{session_id}` route.

The matching receipt can include a failed or stopped process. It is historical
process information, not storage authority, credential qualification or permission
to attach to another generation. A `session_not_found` error is only an observation:
a preceding create can still be in flight. Missing/unsupported routes, unavailable
procd, and generation conflicts must not cause unconditional creation or lookup.
After cold generation replacement, use the external lifecycle/restart operation
receipt; this endpoint does not recover old process metadata from a damaged
supervisor. After a qualified memory restoration, use the explicit restored
lookup described below to inspect the still-running original attempt.

SDKs expose `FindConditionalSession(ctx, key, generation)` (Go),
`findConditionalSession(key, generation)` (TypeScript), and
`find_conditional_session(key, generation)` (Python). The CLI equivalent is
`s0 sandbox session find-conditional <sandbox-id> <creation-key> --runtime-generation 7`.


### Conditional terminal control foundation

The supervisor has an internal `ConditionalAttachment` capability bound to a
conditional creation key, Sandbox identity, runtime generation, Session and
attempt. Each input, signal and resize checks that binding while holding the
same generation admission lock as runtime handover. The captured attempt ID is
always supplied to process control, even when an input omitted it. A replacement
attempt cannot receive commands through the old handle.

Handover invalidates handles before changing the in-memory generation, including
when a later journal persistence step fails. Retrying the handover never restores
old capabilities. Detach releases subscriptions and cancels the handle without
closing stdin or stopping the process; memory handover preserves the live attempt.
Protocol handlers must close their socket when the handle's `Done` closes and
must not fall back to ordinary Session operations after rejection.

The public attachment is
`GET /api/v1/sandboxes/{id}/sessions/{session_id}/conditional/ws` with the original
`Idempotency-Key` header and required `runtime_generation` and `attempt_id` query
parameters; optional `after` selects event replay. It never auto-resumes or falls
back to an ordinary attachment. Generation revocation closes the socket even
while replay is blocked on network I/O. Every outgoing frame checks the binding;
replay omits events for other attempts. Attempt replacement invalidates controls
and closes the socket on the next event or request. Frames already written before
revocation can remain buffered at the receiver.

SDKs expose `ConnectConditionalSession(ctx, sessionID, key, generation, attemptID, options)`
(Go), `connectConditionalSession(sessionId, key, generation, attemptId, options)`
(TypeScript), and `connect_conditional_session(session_id, key, generation, attempt_id, options)`
(Python), using the existing Session WebSocket options and connection types.
Missing or unsupported endpoints and identity conflicts are returned without retry.

Sandpi native terminal integration and qualification remain pending. Existing
ordinary WebSocket routes retain their legacy semantics and must not be used for
native qualified attachment. This capability is process control, not lifecycle or
storage authority or proof of credential qualification.

### Reattach To A Memory-Restored Conditional Session

For an original conditional session preserved by memory restore or migration,
lookup and conditional WebSocket attachment accept an additional
`creation_runtime_generation` query parameter. Supply the original positive
creation generation, the current running `runtime_generation`, the original
creation key, and, for attachment, the original `attempt_id`.

The creation generation must be earlier than the current generation. The
supervisor requires the original attempt to remain running and unfinished;
a cold restart or replacement attempt cannot satisfy these conditions.
SDKs expose `FindRestoredConditionalSession` and
`ConnectRestoredConditionalSession` in Go, `findRestoredConditionalSession` and
`connectRestoredConditionalSession` in TypeScript, and
`find_restored_conditional_session` and `connect_restored_conditional_session`
in Python. These are explicit calls with both generation values; they do not
refresh conditions or fall back to ordinary attachment after rejection.
