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

# Handoff

> The thread lifecycle state machine: pending, active, completed, failed

A thread is handed to the worker and returned through its lifecycle state machine. The user drives the
thread by setting `instance.state = pending`; the worker takes over, runs the agent, and reports progress
back through the worker activity log. This page covers the **dynamics** — the states, the transitions, and
what the worker does in each. The static envelope shape and field-ownership rules live on the
[Job Session Object](/worker/session-agent-harness/thread-object) page.

The control channel is `instance.state` on the thread envelope (`value.thread.metadata`), with the values
`pending`, `active`, `completed`, and `failed`. The user owns the workspace and agent settings and sets
`instance.state` to `pending` (hand off) or `completed` (clean stop), and resets `failed → pending` to
retry. The worker writes only `instance.state`, and only the values `active` and `failed`.

## State machine

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending: user creates the envelope, sets instance.state = pending
    pending --> active: work_folder + agent valid, worker spawns the agent
    pending --> failed: activation fails
    active --> completed: user sets instance.state = completed (clean stop)
    active --> failed: agent crash / terminal API error / oversized item
    failed --> pending: user resets
```

The user-driven states are `pending` (hand off), `completed` (clean stop), and the `failed → pending`
reset. The worker-driven states are `active` (activation succeeded) and `failed` (a thread-level error).

## `pending → active` checklist (worker side)

When the worker sees a thread envelope with `instance.state == "pending"`, it runs this sequence with
respect to the envelope:

1. Validate `workspace.work_folder`:
   * must be absolute → else `WORK_FOLDER_NOT_ABSOLUTE`
   * must exist → else `WORK_FOLDER_NOT_FOUND`
   * must be a directory → else `WORK_FOLDER_NOT_A_DIR`
   * must be readable → else `WORK_FOLDER_NOT_READABLE`
2. Validate `agent.type` is in the supported set (`claude_code`, `codex`) → else `AGENT_TYPE_UNSUPPORTED`.
3. Validate the resolved `permissions` is `autonomous` or `approval` → else `PERMISSIONS_UNSUPPORTED`.
4. Reserve a slot from the `concurrency.max_agents` semaphore. If saturated, the thread stays in `pending`
   until a slot frees.
5. Spawn the configured agent with its working directory set to `work_folder`. Record the resumable
   `agent_session_id` returned by the SDK adapter (local-only).
6. Catch up: fetch `list_session_thread_items(created_since = items.last_consumed.created_at,
   ascending=true)`, drop self items, and feed the remaining user items in `created_at` order to the
   agent's first turn.
7. Write `instance.state = "active"` on the envelope; persist `thread.yaml` (recording the local
   `agent_session_id` and clearing the local error).
8. Post a `thread_active` activity-log item on the `worker` thread.

Validation failures (steps 1–3) transition straight to `failed` with the corresponding error code; the
semaphore slot is not reserved.

## Active behavior

While the thread is `active`:

* **Inbound** — new thread items posted by **other** users (not the worker's own user) are queued in
  memory. Between turns the worker drains the queue, coalesces all queued items in `created_at` order into
  one prompt, and runs the next agent turn. The agent is never interrupted mid-turn.
* **Outbound** — every agent emission becomes a thread item with a brief `content[0].text` and structured
  `metadata`. See [Thread items](/worker/session-agent-harness/thread-items).
* **Interactive prompts** — when the agent asks a question, the turn blocks and the worker posts a
  `pending_prompt` item. See [Interactive prompts](/worker/session-agent-harness/interactive-prompts).
* **Cursors** — `items.last_consumed` advances only at a completed turn boundary, so a crash mid-turn
  replays the items the resumed agent hasn't yet folded in. `items.last_posted` advances per successful
  post.

## `active → completed` (clean stop)

The user stops a thread cleanly by setting `instance.state = "completed"` on the envelope. The worker
observes the change, tears down the live turn loop, releases the concurrency slot, records the state
locally so a later restart does not try to recover the thread, and posts a `thread_completed` activity-log
item on the `worker` thread. `completed` is user-set — the worker never writes it.

## `active → failed` triggers

| Trigger                                | Code                         |
| -------------------------------------- | ---------------------------- |
| Agent process exited unexpectedly      | `AGENT_CRASHED`              |
| Agent executable missing on PATH       | `AGENT_EXECUTABLE_NOT_FOUND` |
| Persistent 4xx posting items           | `THREAD_POST_FAILED`         |
| Item exceeds cap even after truncation | `THREAD_ITEM_TOO_LARGE`      |

A failed session resume surfaces as `AGENT_CRASHED` — there is no separate resume error code.

On transition to `failed`, the worker writes `instance.state = "failed"` on the envelope, records the
error in the local `thread.yaml`, releases the concurrency slot, cancels any active prompt, and posts a
`thread_failed` activity item that carries the error code and message. The error itself is **not** placed
on the envelope — it lives in `thread.yaml` locally and surfaces server-side only through the
`thread_failed` activity item.

## `failed → pending` reset

To retry a failed thread, the user updates the envelope to set `instance.state = "pending"`. The worker
observes the change, clears its local agent block, and re-runs the `pending → active` activation.

The worker never auto-retries a failed thread.

## See also

* [Job Session Object](/worker/session-agent-harness/thread-object)
* [Thread items](/worker/session-agent-harness/thread-items)
* [Interactive prompts](/worker/session-agent-harness/interactive-prompts)
* [Recovery](/worker/session-agent-harness/recovery)
* [Reference](/worker/session-agent-harness/reference)
* [`upload_session_object`](/blob-types/workflow/operations/upload-session-object)
