> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blobhub.io/llms.txt
> Use this file to discover all available pages before exploring further.

# blobhub-worker instance

> Inspect and stop the workers running on this machine

`blobhub-worker instance ls | show | stop` inventory every worker on this machine and let you stop one.
None of the three resolves a credential or calls the API — all three read the filesystem directly, which
is what makes them usable when the API is the thing you suspect.

## Synopsis

```bash theme={null}
blobhub-worker instance ls [--running] [--json]
blobhub-worker instance show <selector> [--json]
blobhub-worker instance stop <selector> [--force] [--timeout <seconds>] [--json]
```

| Command              | Flag        | Default | Description                                                                                |
| -------------------- | ----------- | ------- | ------------------------------------------------------------------------------------------ |
| `ls`, `show`, `stop` | `--json`    | off     | Print the returned payload as JSON and nothing else — progress narration is suppressed.    |
| `ls`                 | `--running` | off     | Only show instances in the `running` state.                                                |
| `stop`               | `--force`   | off     | Escalate to `SIGKILL` if the worker has not exited within `--timeout`.                     |
| `stop`               | `--timeout` | `30`    | Seconds to wait for a graceful exit before `--force` (or `INSTANCE_STOP_TIMEOUT`) applies. |

`show` and `stop` both take a required `<selector>` — see [Selector grammar](#selector-grammar) below.

## What counts as an instance

Discovery walks `~/.blobhub-worker/profiles/*` and `~/.blobhub-worker/keys/*` — every [state
tree](/worker/filesystem) on the machine, not a scan for lock files. A clean shutdown unlinks
`instance.yaml`, so a lock-only scan would be blind to every worker that stopped properly, and to the
config, cursors and thread state it left behind — exactly what "what was it doing?" is asking about. A
tree with no lock at all is reported, not omitted.

Each tree is in exactly one of five states:

| State              | Shown as             | Meaning                                                                                                                                      |
| ------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `running`          | `running`            | Lock present, pid alive, and that pid is not positively something else.                                                                      |
| `stale`            | `stale`              | Lock present, pid dead. The next `start` reclaims the tree.                                                                                  |
| `stale_pid_reused` | `stale (pid reused)` | Lock present, pid alive, but that process is **not** a worker.                                                                               |
| `unreadable`       | `unreadable`         | `instance.yaml` is present but can't be trusted — invalid YAML, fails schema validation, or can't be read at all (e.g. a permissions error). |
| `stopped`          | `stopped`            | No lock. Clean shutdown, or the tree was never started.                                                                                      |

`stale (pid reused)` exists because pids get recycled: a lock naming a pid that is alive but belongs to
some other program is not a worker to trust, and it is not the same thing as `stale`, where the pid is
simply gone. See [Why `stop` can refuse](#why-stop-can-refuse) for how that distinction is made.

**`running` is the default when the check can't be sure, not a guarantee.** The demotion to `stale (pid
reused)` only fires on a positive "this is not a worker" — where `ps` can't answer at all (missing,
non-zero, timed out), the tree still reports `running`, because treating an unverifiable pid as not
running risks a second worker landing on the same tree and corrupting its cursors. The only place that
distinction surfaces is `stop --json`'s `pid_verified` field (`true`/`false`/`null`) — see
[`instance stop`](#instance-stop) below.

## Selector grammar

One argument, matched against every discovered tree and lock:

| Form                      | Matches                                                                                                                                                                     |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| All digits (e.g. `41283`) | `instance.pid`.                                                                                                                                                             |
| `key:<digest>`            | A key tree whose digest starts with `<digest>`.                                                                                                                             |
| Anything else             | A profile tree named exactly that; **or** an instance id where `instance_id` starts with the selector, or with `ins_` + the selector; **or** a key digest starting with it. |

An instance id may be given as a unique prefix, with or without the `ins_` prefix. Matches are unioned:
none of them → `INSTANCE_NOT_FOUND`; more than one → `INSTANCE_AMBIGUOUS`, naming every candidate. A
profile name that happens to prefix a key digest collides into `INSTANCE_AMBIGUOUS` too — disambiguate
with `key:`.

The selector is required on both `show` and `stop`; neither defaults to "the only one running", because
that would make the target of a `stop` depend on whatever else happens to be running at the time.

## `instance ls`

```
$ blobhub-worker instance ls
INSTANCE                        TREE                  PID    UPTIME  VERSION  STATE
ins_01K2X8QNVCWJ8TE9QF48ZQF48Q  prod                  72716  11h27m  0.5.0    running
-                               key:9f31c2ab44de017c  -      -       -        stopped
-                               staging               -      -       -        stopped
```

Ordering: running first, then by tree label. `--running` filters to `running` only. With no state trees
at all, `ls` prints `no worker state trees on this machine` instead of an empty table.

The TREE column carries two optional annotations, both derived from the credential store and dropped
silently if it is missing or unreadable:

* `key:<digest> (=name)` — a key tree whose digest matches a stored profile's key. This is the everyday
  shape of running with `BLOBHUB_API_KEY` exported alongside a stored profile of the same key: the worker
  runs from the key tree instead of the profile's, and this annotation is the way to notice.
* `<profile> (orphan)` — a profile tree with no stored profile of that name. The credential was deleted;
  the state tree was not.

The key digest is printed in full (16 characters) rather than truncated, so it can always be pasted back
into `show` or `stop` as `key:<digest>`.

`--json` prints `{"instances": [...]}`, one object per row with `instance_id`, `tree`, `kind`, `label`,
`path`, `pid`, `version`, `uptime_seconds`, `state`, and `state_label`.

## `instance show`

```
$ blobhub-worker instance show prod
Instance: ins_01K2X8QNVCWJ8TE9QF48ZQF48Q running (up 11h27m)
Tree:     ~/.blobhub-worker/profiles/prod   [prod]
User:     Egor Pushkin (usr_a1)
Process:  pid 72716 on mbp.local, v0.5.0, started 2026-08-08T11:46:02Z
Config:   1 section, max_agents 4

Sections
  work       ses_7fa2                     attached 2026-08-08T11:46:03Z      last event 2026-08-08T13:59:41Z

Threads (1)
  missions/m1/cells/build            active      /proj/api                Implemented the retry path; tests pass.
```

| Block             | Source                                                                                                   |
| ----------------- | -------------------------------------------------------------------------------------------------------- |
| Instance, Process | `instance.yaml`                                                                                          |
| Tree              | the tree's path, plus the same annotations `ls` uses                                                     |
| User              | `identity.yaml`                                                                                          |
| Config            | `config.yaml` — section count, `concurrency.max_agents`                                                  |
| Sections          | each `jobs/*/section.yaml` — name, resolved session id, `session_prefix`, attach status/time, last event |
| Threads           | each `jobs/*/threads/**/thread.yaml` — alias, `agent.state`, `workspace.work_folder`, last event         |

Every source is read defensively: an unreadable file degrades that block to a note (or an `unreadable`
row for one section or thread) rather than failing the whole command. A thread's alias, when not recorded
on the object itself, is rebuilt from its path under the job's `threads/` directory.

**`show` works on a stopped tree, and that is the point of it — not a tolerated edge case.** The header
reads `Instance: -` followed by the bare state (`stopped`, `stale`, …) instead of `running (up …)`, and
the `Process:` line is omitted since there is no pid to report. Everything else — Tree, User, Config,
Sections, Threads — is the state the worker left behind, which is otherwise the only way to answer "what
was it doing when it died" for a headless worker.

A refused section (`attachment.status: refused`) prints its error code in place of the attach time — a
section that never attached is the most common reason a running worker looks idle.

`--json` prints the same dict this text rendering is built from: the top-level instance/process fields,
plus `tree`, `user`, and `config` objects and `sections`/`threads` arrays.

## `instance stop`

```
$ blobhub-worker instance stop prod
signalling ins_01K2X8QNVCWJ8TE9QF48ZQF48Q (pid 72716) with SIGTERM
stopped after 0.2s
```

`SIGTERM` reaches the same handler `start` installs for Ctrl-C — sections drain, in-flight turns finish,
the lock is released. See [`start` → Shutdown](/worker/cli/start#shutdown). This holds identically for a
`--tui` worker: the drain runs unbounded, the same as headless, so `--timeout` means the same thing in
both modes — how long `stop` itself waits before giving up (or escalating with `--force`), not a bound on
the worker's own drain. (Pressing `q` inside the dashboard is a separate, deliberately bounded path — see
[`start` → Shutdown](/worker/cli/start#shutdown) — but that only matters at the keyboard, not to `stop`.)

Outcomes by state, all five handled deliberately:

| State                | Outcome                                                                                 |
| -------------------- | --------------------------------------------------------------------------------------- |
| `running`            | `SIGTERM`, then poll every 200ms up to `--timeout` (default 30s).                       |
| `stopped`            | `INSTANCE_NOT_RUNNING`.                                                                 |
| `stale`              | Unlinks the lock, reports the removal, exits 0 — the target is already gone.            |
| `stale (pid reused)` | `INSTANCE_PID_NOT_WORKER` — refuses. See [Why `stop` can refuse](#why-stop-can-refuse). |
| `unreadable`         | `INSTANCE_NOT_RUNNING`, naming the path. The lock is **not** deleted.                   |

On timeout: without `--force`, `INSTANCE_STOP_TIMEOUT` and the worker is left running. With `--force`,
`SIGKILL`, then up to 5 more seconds, then `INSTANCE_STOP_TIMEOUT` if it is somehow still alive.

**A `--force` stop leaves the lock behind.** `SIGKILL` never runs the process's cleanup, so
`instance.yaml` outlives it. That is safe — the pid is dead, so the next `start` reclaims the tree — but
`instance ls` will show a `stale` row until then, and the command says so at the time rather than leaving
it to be discovered.

If the process has already exited between the state check and the signal, that counts as success. Any
other signalling failure — a `PermissionError` for a worker owned by another user is the realistic one —
is reported as `INSTANCE_SIGNAL_FAILED`.

`--json` prints the same payload `stop` returns, for example after a `SIGTERM`:

```json theme={null}
{"instance_id": "...", "pid": 72716, "tree": "prod", "action": "sigterm", "stopped": true,
 "waited_seconds": 0.2, "pid_verified": true}
```

`pid_verified` is tri-state — `true`, `false`, or `null` when `ps` could not answer — and rides along on
the `sigterm` and `sigkill` outcomes only; the stale-lock-removal outcome has no pid to verify and omits
the field.

## Why `stop` can refuse

`instance.yaml` records a pid, and the operating system recycles pids. Before signalling, `stop` asks
`ps` what that process actually is, judging **`argv[0]`** — the console script, or a Python interpreter
running `-m blobhub_worker` — never the whole command line. A pid that is alive but positively not a
worker is `stale (pid reused)`, and `stop` refuses it:

```
$ blobhub-worker instance stop staging
error: INSTANCE_PID_NOT_WORKER: the lock names pid 77624, but that process is '/bin/sleep 300' --
refusing to signal it; the lock is stale, remove ~/.blobhub-worker/profiles/staging/instance.yaml
```

The lock is deliberately left in place — a lock this command cannot account for is not one it deletes for
you.

If `ps` cannot answer at all (absent, non-zero, timed out), `stop` proceeds anyway and prints a warning
first. That is what plain `kill` does unconditionally, and a stop command that stops working because a
system tool is missing would be worse than one that occasionally signals without having verified.

## Errors

| Code                      | Raised by      | Meaning                                                                 |
| ------------------------- | -------------- | ----------------------------------------------------------------------- |
| `INSTANCE_NOT_FOUND`      | `show`, `stop` | Selector matched no tree and no lock.                                   |
| `INSTANCE_AMBIGUOUS`      | `show`, `stop` | Selector matched more than one; the message lists them.                 |
| `INSTANCE_NOT_RUNNING`    | `stop`         | Target has no live worker to signal (`stopped`, or an unreadable lock). |
| `INSTANCE_PID_NOT_WORKER` | `stop`         | The lock's pid is alive but is positively not a worker.                 |
| `INSTANCE_STOP_TIMEOUT`   | `stop`         | Still alive after the timeout (and after `SIGKILL`, under `--force`).   |
| `INSTANCE_SIGNAL_FAILED`  | `stop`         | The signal itself failed — permissions, typically.                      |

These are raised by `instance show`/`instance stop`, not by `start`'s preflight — full descriptions
alongside the process-level codes: [Reference → Instance command
codes](/worker/reference#instance-command-codes).

## See also

* [`blobhub-worker start`](/worker/cli/start)
* [Filesystem Layout](/worker/filesystem)
* [Configuration](/worker/configuration)
* [Reference](/worker/reference)
