# Sessions

Every execution belongs to one stable Workbench session. The first run shares its `wb_...` ID with the session. Each continuation creates a new internal run while the session ID stays fixed. A session stays locked to its original Workbench version, runner, model, runtime, workspace, and workspace bindings.

These commands let a person or a supervising agent drive a session without keeping a client attached.

| Command     | Does                                                                   |
| ----------- | ---------------------------------------------------------------------- |
| `wb ps`     | List active and resumable sessions.                                    |
| `wb attach` | Replay and follow a run. Read-only.                                    |
| `wb wait`   | Block until the next boundary and print one snapshot. Read-only.       |
| `wb send`   | Send input: to an idle session, as steering, or as a queued follow-up. |
| `wb answer` | Answer a pending permission or question.                               |
| `wb resume` | Continue a session with new input.                                     |
| `wb kill`   | Cancel the active run.                                                 |

## The loop

```sh
wb run project-core --task-file ./brief.md --detach --json
# {"session_id":"wb_...","run_id":"wb_...","input_id":"...","after_sequence":0}

wb wait wb_... --timeout 120 --json

wb send wb_... --task-file ./followup.md --json
# {"session_id":"wb_...","run_id":"wb_...","input_id":"...","after_sequence":42}

wb wait wb_... --run --after 42 --timeout 120 --json
```

Each command returns correlated session and run IDs. Sequence cursors belong to one run, so wait on the `run_id` and `after_sequence` returned by the command you just issued.

## ps

```sh
wb ps          # active sessions and sessions with resumable context
wb ps --all    # all session history, including terminal one-shot runs
wb ps --json   # one record per NDJSON line
```

`--json` includes `state`, `needs_input`, pending requests, and `project`, the project's display name or `null`. A run that seems slow with nothing happening is almost always waiting on a permission: check `needs_input`.

## attach

```sh
wb attach wb_...
wb attach                 # the latest session
wb attach wb_... --json   # replay the stored events as NDJSON
wb attach wb_... --final
```

Attaching replays the latest run's stored events and follows new ones. It never takes control, starts model work, or keeps a runtime alive. If an observer exits while a turn is active, the turn and all accepted follow-ups finish before the background worker closes.

## wait

```sh
wb wait <session-or-run> [--run] [--after <sequence>] [--timeout <seconds>] [--json]
```

`wait` is a read-only observer. It prints one snapshot when it reaches the first of:

- a completed turn after the cursor,
- an idle boundary,
- a pending permission, question, or authentication request,
- a terminal state,
- the timeout.

| Option                | Effect                                                                                                                                                         |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--run`               | Treat the ID as an exact run. Without it, a session ID selects its latest run. The session and its first run share an ID, so use `--run` to pin the first run. |
| `--after <sequence>`  | Wait past this event sequence. Without it, observation starts at the beginning of the run.                                                                     |
| `--timeout <seconds>` | Stop observing after this long. The run is unchanged.                                                                                                          |
| `--json`              | Print one JSON result. Never an event stream.                                                                                                                  |

It returns the first completed turn after the cursor even if queued work starts immediately. Repeat with `--after` set to the returned `sequence` to see the next boundary. A timeout or interrupted wait never cancels the run, and waiting never attaches a controlling client, starts authentication, or keeps a runtime alive.

For an authoring run started by `wb create`, `wait` waits for package verification too.

### States

`state` in the result is one of:

| State            | Meaning                                                                     | Exit code |
| ---------------- | --------------------------------------------------------------------------- | --------- |
| `starting`       | The run is being prepared.                                                  | 0         |
| `running`        | A turn is active.                                                           | 0         |
| `idle`           | The session is open and waiting for input.                                  | 0         |
| `turn_completed` | A turn replied. The run may continue; cleanup has not necessarily finished. | 0         |
| `completed`      | The run finished.                                                           | 0         |
| `needs_input`    | A permission, question, or authentication request is pending.               | 2         |
| `failed`         | The run failed, including a run with no authenticated route.                | 1         |
| `cancelled`      | The run was cancelled.                                                      | 130       |
| `timeout`        | `--timeout` passed first. The run is unchanged.                             | 124       |

`wait` itself exits 130 when it is interrupted. The full result shape is on [JSON output](/docs/cli/json#wb-wait-json).

### Usage in a wait

`usage` counts usage events strictly after `--after` through the returned `sequence` (`kind: delta`). `usage_total` counts the whole run through that sequence (`kind: total`). Advance the cursor before summing another wait's `usage`; waiting on the same cursor again repeats the same interval. Costs are provider-reported model costs, not billing or infrastructure charges.

## send

```sh
wb send wb_... "Check the rollback" --json
wb send wb_... "Focus on cleanup" --steer --json
wb send wb_... --task-file followup.txt --queue --json
wb send wb_... --stdin --json
```

| Option                                    | Effect                                                      |
| ----------------------------------------- | ----------------------------------------------------------- |
| `[text]`, `--task-file <file>`, `--stdin` | The input. Explicit alternatives, never implicit fallbacks. |
| `--steer`                                 | Change the active turn. Requires an active execution.       |
| `--queue`                                 | Queue a FIFO follow-up for the next turn boundary.          |
| `--json`                                  | Print one receipt.                                          |

An ordinary send rejects an active turn instead of silently queuing it. Sending to a closed resumable session starts a fresh run from its saved native context. `send` uses the inherited environment for credentials.

Steering support depends on the runner: OpenCode steers mid-turn, Claude Code at the next tool result, and Pi at its next model boundary. A receipt and the input lifecycle events show when the runner accepts and delivers the input; a receipt is not proof the runner consumed it.

## answer

```sh
wb answer wb_... <request-id> allow --json
wb answer wb_... <request-id> deny
wb answer wb_... <request-id> '[["First option"],["Second option"]]'
wb answer wb_... <request-id> --response-file answer.json
wb answer wb_... <request-id> --reject
```

`answer` resolves only a request ID that `wait` or `ps` currently reports. For a permission, `allow` grants it once and `deny` rejects it. For a question, pass the answer text, or a JSON array of string arrays for several questions or multi-select. `--response-file` and `--stdin` read the response; `--reject` rejects a pending question.

Authentication requests expose the runner's URL and instructions, never a channel for submitting a credential. Permissions and questions exist only when the runner has a native protocol for them: OpenCode and Claude Code do, Pi does not.

If the same paths prompt for permission again and again, the Workbench's runner permissions are missing entries. Report it to the author rather than approving everything.

## resume

```sh
wb resume wb_... "Review the latest change"
wb resume wb_... --task "Run the checks" --detach
wb resume wb_... --task "Continue" --allow-host-docker
```

`resume` continues a session with new input. It joins an active run's follow-up queue, or starts a linked run from saved native context when the previous one closed. It requires input: without it, `Session wb_... needs new input`.

It accepts the task options of `run` (`--task`, `--task-file`, `--stdin`), the output options (`--detach`, `--json`, `--final`, `--color`), `--env-file`, `--env`, `--connection`, and `--allow-host-docker`. A Workbench that declares host Docker access needs `--allow-host-docker` again on every resume.

`resume` uses the exact package and workspace recorded by the session. It is rejected if the package no longer matches the recorded name, version, runner, model, or workspace, or if the runner never reached a resumable state. Use `wb send` for agent-driven continuation.

## kill

```sh
wb kill wb_...
```

Cooperatively cancels the active run. The worker terminates the runner, emits `run.cancelled`, and marks the run cancelled. The session and its resumable context remain.

## clean

```sh
wb clean                              # preview terminal history older than 30 days
wb clean --older-than 7d --apply
wb clean --older-than 0s --include-sessions --apply
wb clean --json
```

`wb clean` never removes anything without `--apply`. The default selects terminal, non-resumable sessions and obsolete run history older than 30 days. Durations look like `12h`, `7d`, or `4w`. Active runs are never eligible. Resumable context and its latest run are protected unless `--include-sessions` is also passed. Repository sessions keep every earlier attempt's outcomes until the session is removed. `--json` returns the preview or result with byte counts and protected resources.

It also finds managed Docker containers and E2B sandboxes whose run is terminal or missing. It never removes images, build caches, E2B templates, runner credential volumes, or unrelated resources.

## Where sessions live

Sessions, runs, events, and projects are rows in one SQLite database, `$WORKBENCH_HOME/wb.db` (normally `~/.workbench/wb.db`). `WORKBENCH_HOME` must be on a local disk; the database does not work on network filesystems such as NFS. A run's native runner state, managed checkout, and scratch live under `$WORKBENCH_HOME/files/`.
