# Event vocabulary

> The seam between the harness and any client (the Textual UI is one; a wire client is still possible later). Every event the harness emits — its type string and its fields — is the contract. A headless kin -p run colle…

The seam between the harness and any client (the Textual UI is one; a wire client is still possible later). Every event the harness emits — its `type` string and its fields — is the contract. A [headless `kin -p` run](/docs/kin/guide/headless/) collects the same events into its exit code + report instead of painting them; the [Outpost](/docs/kin/guide/outpost/) scheduler drives `kin -p` subprocesses and surfaces their terminal state (`done` reason, `needs_human`) through its own job API.

`src/kin/harness/events.py` is the single source of truth for the type strings and field names; renaming a field breaks `src/kin/tui/app.py`'s `on_kin_event` dispatch. Treat that file as load-bearing.

<!-- SOURCE: src/kin/harness/events.py, src/kin/harness/loop.py, src/kin/harness/session/core.py, src/kin/harness/session/roundtrip.py, src/kin/harness/session/subagents.py, src/kin/tui/app.py -->

## Shape

Each event is an `Event` dataclass with `seq`, `type`, and a `data` dict (plus `.get(key, default)`). Builder functions in `events.py` stamp a monotonic `seq` and return a ready-to-emit event.

```python
@dataclass
class Event:
    seq: int
    type: str
    data: dict[str, Any] = field(default_factory=dict)

    def get(self, key: str, default: Any = None) -> Any:
        return self.data.get(key, default)
```

## Handshake

`session` and `ready` open every session (fresh or [resumed](/docs/kin/guide/sessions/#resume-continue)); the state-echo events below them (`mode_changed`, `model_changed`, `nudges_changed`, etc.) also fire once at session start to seed the UI with the session's current state. See [Sessions, resume & compaction](/docs/kin/guide/sessions/).

### session

The session opener. Emitted once, before `ready`.

| Field | Type | Meaning |
|---|---|---|
| `session_id` | `str` | The session id (also shown on the StatusBar) |
| `model` | `str` | The model id the harness is calling |
| `workspace` | `str` | The session workdir |
| `posture` | `str` | The concise `auto`-mode containment line for the welcome banner (e.g. `auto · shell sandboxed (Seatbelt)`); empty in `strict` mode |

### ready

Emitted after `session`. The UI treats this as "cold-start done."

### mode_changed

Emitted when the active [permission mode](/docs/kin/guide/modes-and-permissions/) changes (and once at session start).

| Field | Type | Meaning |
|---|---|---|
| `mode` | `str` | The new mode (`auto` or `strict`) |
| `error` | `str` | Non-empty on a refused change (e.g. an unknown mode name) |

### model_changed

Emitted on `/model <id>` and once at session start. See [Models & providers](/docs/kin/guide/models-and-providers/).

| Field | Type | Meaning |
|---|---|---|
| `model` | `str` | The new model id (the **configured alias** — what the harness sends on the wire; may be `"default"` for a vLLM-style `served-model-name` alias) |
| `error` | `str` | Non-empty on a refused change |

### model_discovered

The **active** model id — what the server actually used — surfaced by a
`/v1/models` probe (session start) or by `response.model` piggybacked off
each completed turn. Distinct from `model_changed`: the latter echoes the
configured alias on `/model` swaps, the former surfaces the resolved name.
Drives the top bar's live model slot and the welcome banner's `model` row
when it's still mounted. Both events can co-fire for one `/model` swap
(alias toast + resolved toast).

| Field | Type | Meaning |
|---|---|---|
| `active` | `str` | The resolved model id (canonical first `served-model-name` entry on a vLLM serve, or the actual model on real providers). **Sanitized at capture** — control chars, Unicode bidi overrides, and zero-width glyphs are stripped (see `sanitize_model_id`); an empty result is suppressed so the top bar never blanks out. |

> **Display-sink hardening**
>
> The `active` value is server-supplied (untrusted input). The harness
> sanitizes it at every capture site (`_fetch_active_model`,
> `_OpenAITurn._run`, `_AnthropicTurn._run`, `force_tool_call`,
> `count_tokens`) AND passes `markup=False` on the toast so a
> compromised / malicious server can't phish the visible label via
> markup-shaped strings. The top bar / banner paths render via
> `rich.text.Text.append(str, style=…)` which treats the string as
> literal text (no markup interpretation). See
> `REFERENCE.md § Display sinks` for the pattern.

### nudges_changed

Emitted after every `/nudges` Apply and at every session start, including when
the set is empty. It carries the complete ordered set so a client can replace
its display atomically and clear stale chips after a session switch. See
[Session nudges](/docs/kin/guide/workflows/#session-nudges).

| Field | Type | Meaning |
|---|---|---|
| `active` | `list[str]` | Complete active registered-key list in stable registry order |

### planning_changed

Emitted when the session's read-only [**planning freeze**](/docs/kin/guide/modes-and-permissions/#planning) toggles. Drives the
`⏸ planning` status-bar indicator (shown next to the mode badge). Mirrors
`nudges_changed` as a human-visible state echo, but with a deliberate
asymmetry: the model can ENTER planning (dispatching the read-only `planner`
subagent, whose `write_plan` call auto-enters the freeze on its first write —
a capability *reduction*, so it's non-escalating and safe to self-trigger),
while only a human EXITs it (the plan-approval modal or a bare `/plan`).
Never persisted — a transient overlay, not a mode.

| Field | Type | Meaning |
|---|---|---|
| `on` | `bool` | Whether the read-only planning freeze is now on |

### effort_changed

Emitted on `/effort [low|medium|high|...]` and once at session start when a
resumed session restored the level on. Drives the reasoning-effort chip on
the TopBar. Mirrors `nudges_changed` / `planning_changed` shape (a
state echo of a wire-time sampling knob).

| Field | Type | Meaning |
|---|---|---|
| `level` | `str` | The reasoning-effort level now active (`low` / `medium` / `high` / model-specific) |
| `error` | `str` | Empty on success; non-empty if the level couldn't be applied (the UI surfaces the error in the chip) |

### settings_mode_changed

Emitted on `/settings on|off`. Drives the `/settings on` echo (~800-token
in-conversation guide for the model — `propose_settings` is otherwise denied
mid-plan, so this is the persistent human-only opt-in). See
[Model-writable settings](/docs/kin/guide/modes-and-permissions/#model-writable-settings)
and [settings.toml keys § Editing settings from inside kin](/docs/kin/reference/settings-toml/#editing-settings-from-inside-kin).
Mirrors `nudges_changed` / `planning_changed` as a third human-only session-wide
state echo. Survives resume + compaction + clear + rewind.

| Field | Type | Meaning |
|---|---|---|
| `on` | `bool` | Whether the session-wide `/settings` guide is now active |

## Assistant output

### stream_chunk

A streaming text delta from the model. The UI groups these under one `AssistantMessage` per `message_id`.

| Field | Type | Meaning |
|---|---|---|
| `message_id` | `str` | The assistant message this chunk belongs to |
| `content` | `str` | The delta (not the full text so far) |
| `source` | `str` | Optional carrier label |

### stream_done

Marks the end of streaming for one `message_id`. The UI finalizes the assistant block.

### stream_retry

A headless subagent's transient mid-stream failure is about to be retried. Its
`ForwardingEmit` removes the partial tail before the replacement stream begins,
preventing duplicated prose. Root TUI sessions do not enable this retry posture.

| Field | Type | Meaning |
|---|---|---|
| `discard_chars` | `int` | Number of already-accumulated trailing characters to remove |

### text

The fully assembled text for a turn segment (assistant / system / user / tool). The legacy "complete text" event the UI prefers for non-streaming paths.

### reasoning

A reasoning / thinking chunk. The UI renders a collapsible `Reasoning` block per `message_id`.

| Field | Type | Meaning |
|---|---|---|
| `message_id` | `str` | The reasoning block id |
| `content` | `str` | The reasoning chunk (may be empty on deltas) |
| `done` | `bool` | True on the final delta for this block |

### thinking

Soft-toggle for the model's "thinking" mode (for example Qwen or a profiled
Poolside Laguna serve using `enable_thinking`).

| Field | Type | Meaning |
|---|---|---|
| `active` | `bool` | Whether thinking is currently active |
| `status` | `str` | Optional status text |

## Tools

See [Tools](/docs/kin/guide/tools/) for what each tool does; this section only covers the wire shape.

### tool_call

A model-issued tool invocation.

| Field | Type | Meaning |
|---|---|---|
| `id` | `str` | The tool-call id (used to match the result) |
| `name` | `str` | The tool name |
| `arguments` | `Any` | The arguments (parsed dict for typed callers, JSON string on the legacy OpenAI wire) |
| `source` | `str` | Optional carrier label |
| `batch_id` | `str` | Present only when this call is part of a **parallel batch** (`len(tool_calls) > 1`, non-serial); a shared id the UI groups by. Absent on single / serial calls. |
| `batch_size` | `int` | Number of calls in the batch (present with `batch_id`) |
| `batch_index` | `int` | This call's position in the batch (present with `batch_id`) |

### tool_stream

A live-tail chunk for a tool that's still running (shell stdout, build output).

| Field | Type | Meaning |
|---|---|---|
| `id` | `str` | The tool-call id this tail belongs to |
| `name` | `str` | The tool name |
| `message` | `str` | The new tail text |
| `parent_tool_call_id` | `str` | For subagent tool calls — the parent's `task` tool-call id so the UI can nest the stream |
| `spawns_agent` | `bool` | True when this progress line is the signal that the tool call just became a real subagent spawn (`task`'s foreground branch — including when the model dispatches `critic` itself from `present_plan`'s adversarial-review option) — the UI flips the already-mounted tool-call row into a live-updating agent-mode timer instead of a one-shot tail line. Defaults False. |

### tool_call_draft

A throttled progress event while the model is still streaming a tool call's
arguments. Carries the tool name (once known) and a cumulative raw-JSON char
count so the status bar can flip to `drafting <name>… N chars` while a long
`write_file` body or shell command is in flight. Never carries argument content
itself (a draft may include `api_key` values, secrets, or shell command bodies);
the UI is responsible for a name + length only. Dropped by `ForwardingEmit`
for subagent children (keeps the parent row's label stable). Headless runs
**deliver** `tool_call_draft` to `on_event` taps and the journal the same as
any other event — they simply don't paint (no contribution to
`HeadlessResult.text`).

| Field | Type | Meaning |
|---|---|---|
| `name` | `str` | The tool name once the model has announced a `tool_use` block; empty until then |
| `chars` | `int` | Cumulative raw-JSON character count streamed so far |

### tool_result

A tool's outcome.

| Field | Type | Meaning |
|---|---|---|
| `id` | `str` | The tool-call id |
| `name` | `str` | The tool name |
| `result` | `Any` | The legacy string form of the result |
| `error` | `str` | Non-empty on a refused / failed call |
| `parent_tool_call_id` | `str` | Subagent nesting (see `tool_stream`) |
| `content_blocks` | `list \| None` | Structured payload (text / image / notebook cell); omitted for plain-string results |
| `batch_id` / `batch_size` / `batch_index` | `str` / `int` / `int` | Parallel-batch grouping (mirrors `tool_call`); present only on calls from a `len > 1` non-serial batch |

## Blocking events

Blocking events park the turn on an in-process `Future` keyed by a correlation id; the UI's `resolve_*` modal workers unblock the turn. The answer encoding lives in `REFERENCE.md § "Event contract & IPC"`. A [headless run](/docs/kin/guide/headless/#the-permission-envelope-fail-closed-never-hanging) has no modal to resolve one of these — it resolves the blocking event as a structured `needs-human` tool error instead of parking forever.

### approval_request

A tool call that needs your decision. See [Modes & permissions](/docs/kin/guide/modes-and-permissions/) for what ALLOW / ASK / DENY / SANDBOX mean per mode.

| Field | Type | Meaning |
|---|---|---|
| `id` | `str` | The correlation id (used by `resolve_approval`) |
| `tool` | `str` | The tool name |
| `arguments` | `Any` | The arguments (rendered in the modal) |
| `scope` | `str` | Human-readable scope label for an "always allow this session" choice |
| `kind` | `str` | Permission kind used to choose the modal body |
| `args` | `dict \| None` | Parsed structured arguments when available |
| `diff` | `str` | Precomputed unified diff for edit approvals; empty when unavailable |

### question

A single- or multi-select (or free-text) question.

| Field | Type | Meaning |
|---|---|---|
| `id` | `str` | The correlation id |
| `question` | `str` | The prompt text |
| `header` | `str` | Short header |
| `options` | `list[dict]` | The option list |
| `allow_other` | `bool` | Allow a free-text "other" |
| `multi_select` | `bool` | Multi-select |

### plan_ready

A finished plan presented for approval (`present_plan`), parking the [planning freeze](/docs/kin/guide/modes-and-permissions/#planning) until you choose.

| Field | Type | Meaning |
|---|---|---|
| `id` | `str` | The correlation id |
| `plan_path` | `str` | Path to the saved plan file (under `<workdir>/.kin/plans/`) — always populated, since `write_plan` persists unconditionally |
| `content` | `str` | The plan body, display-sanitized at capture (terminal controls, bidi overrides, and zero-width characters removed; Markdown newlines and tabs preserved) |
| `options` | `list[dict]` | The three action choices — **Approve & execute** (keep context), **Approve, clear & re-inject** (reseed just the plan + an execution preamble), **Review plan first** (adversarial critic). A dismissal (esc) keeps planning |

### proposal_ready

A model-proposed settings change parked on the artifact-edit `ProposalModal` (the `propose_settings` tool — see [settings.toml keys § Editing settings from inside kin](/docs/kin/reference/settings-toml/#editing-settings-from-inside-kin)), parking the turn until you approve or reject the diff. Mirrors `plan_ready`, but the modal answer is the **string** `"apply"` / `"reject"` (decoded by `resolve_proposal`), not the int index `plan_ready` carries.

| Field | Type | Meaning |
|---|---|---|
| `id` | `str` | The correlation id |
| `label` | `str` | The artifact label (e.g. `"settings"`) |
| `path` | `str` | The file the change targets (`~/.kin/settings.toml`) |
| `content` | `str` | The unified diff **plus** the harness-generated refusal banner naming any human-only keys the model tried to set (deterministic tool text the model's narrative can't suppress) |

### sandbox_access_request

A typed request for a bounded sandbox capability and lifetime, parked on the
dedicated access modal. See [Auto mode & the OS sandbox](/docs/kin/guide/auto-mode-and-sandbox/).

| Field | Type | Meaning |
|---|---|---|
| `id` | `str` | Correlation id used by the modal resolver |
| `request_id` | `str` | Identity of the retained denied operation |
| `executor` | `str` | Executor that needs access (`shell` or `run_code`) |
| `operation` | `str` | Sanitized operation summary |
| `cwd` | `str` | Requested working directory |
| `reason` | `str` | Why the capability is needed |
| `permissions` | `dict` | Typed capability profile |
| `evidence` | `str` | Reactive denial evidence, when present |
| `reactive` | `bool` | Whether a real sandbox denial triggered the request |
| `workspace_allowed` | `bool` | Whether workspace writes are included |
| `state_loss` | `bool` | Whether denying may discard live executor state |
| `partial_execution` | `bool` | Whether the retained operation may have partly run |
| `risks` | `list` | Human-facing bounded risk notes |

## Durable external-work markers

These events let a live client mount the same quiet Beam card that journal
replay reconstructs later. The journal marker remains the durable authority;
the event is presentation metadata only.

### beam_submitted

Emitted after Outpost accepts a Beam handoff.

| Field | Type | Meaning |
|---|---|---|
| `job_id` | `str` | Outpost job identity used for live status |
| `run_id` | `str` | Initial run identity when the submit response supplies one |
| `workspace` | `str` | Exact Outpost workspace slug |
| `branch` | `str` | Feature branch handed off |
| `folder` | `str` | Short local checkout label |
| `workdir` | `str` | Originating local checkout |
| `focus` | `str` | Human-authored handoff focus |
| `source` | `str` | Submission path (for example `native`) |

### beam_returned

Emitted by the deterministic return controller after the locally recorded
feature branch has fast-forwarded in its originating checkout and the pulled
commit count is known. The prompt-driven `/beam back` path reaches the
controller through the always-ask `outpost-send return` operation.

| Field | Type | Meaning |
|---|---|---|
| `job_id` | `str` | The returned Outpost job |
| `run_id` | `str` | The terminal (or explicitly partial) run returned |
| `workspace` | `str` | Exact Outpost workspace slug from the local locator |
| `branch` | `str` | Locally recorded feature branch that fast-forwarded |
| `folder` | `str` | Short local checkout label |
| `workdir` | `str` | Originating local checkout |
| `commits` | `int` | Deterministic count of commits pulled, when known |
| `partial` | `bool` | Whether the human explicitly returned a needs-human checkpoint |
| `source` | `str` | Return path (currently `controller`) |

## Status / lifecycle

### context_usage

The context-window meter.

| Field | Type | Meaning |
|---|---|---|
| `usage` | `float` | Fraction of the effective prompt budget used (`0.0`–`1.0`) |
| `used` | `int` | Total prompt tokens |
| `max` | `int` | The window size |
| `cached` | `int` | Cached tokens (if the carrier reports them) |
| `level` | `str` | Optional override (`warning` / `critical`) |
| `source` | `str` | The budget side the count is from |
| `output_reserved` | `int` | Output tokens reserved from the shared context window |
| `compact_at` | `int` | Effective prompt-meter denominator and compact trigger/hard limit |

### todos

The pinned task-panel contents. Full rewrite on every `todos` tool call. See
[Tasks vs todos](/docs/kin/concepts/tasks/#tasks-vs-todos) for how this differs
from the `tasks` DAG below.

| Field | Type | Meaning |
|---|---|---|
| `todos` | `list[dict]` | The todo items (`content`, `status` ∈ `pending` / `in_progress` / `completed`) |

## Background subagent lifecycle

Three events layer on top of the existing `tool_call` / `tool_result` plumbing (which still re-stamps through `ForwardingEmit`). They drive the ambient bar and agent panel; the root's separate exactly-once event inbox drives automatic MCA attention. All three carry an `agent_id` for correlation. See [Subagents](/docs/kin/guide/subagents/).

None of these events carry the subagent's prose — the model-facing reminder is metadata-only (a bg agent could have fetched web content); the human-facing panel reads it from the registry buffer.

### subagent_dispatched

Emitted when a bg subagent is spawned (`task` or `fork_agent` with
`run_in_background=true`). The panel paints the new row immediately.

| Field | Type | Meaning |
|---|---|---|
| `agent_id` | `str` | The bg agent id (e.g. `agent1`) |
| `profile` | `str` | The profile name (general / researcher / coder / custom) |
| `prompt_preview` | `str` | First line of the prompt, truncated to 60 chars (for the row) |
| `parent_tool_call_id` | `str` | The parent's `task` tool-call id — the UI registers it so the agent's forwarded child tool calls fold into its ambient-strip row (live activity), not the transcript |
| `context_scope` | `str` | `isolated` for `task`, `full_conversation` for `fork_agent` |
| `inherited_turns` | `int` | Number of represented human turns inherited by a full-context fork; 0 for isolated tasks |

### subagent_progress

Emitted for cooperative and health transitions: `pause_requested`, `paused`,
`resumed`, `interrupted`, `stale`, `healthy`, and `closed`.

| Field | Type | Meaning |
|---|---|---|
| `agent_id` | `str` | The bg agent id |
| `phase` | `str` | The phase label |
| `message` | `str` | Optional message text |

### subagent_completed

Emitted once for a registered subagent run's terminal state. The panel flips
the row while the root event inbox independently regains MCA attention.

| Field | Type | Meaning |
|---|---|---|
| `agent_id` | `str` | The bg agent id |
| `status` | `str` | `ok` / `error` / `killed` |
| `duration_s` | `float` | Wall-clock seconds from spawn to completion |
| `tool_calls` | `int` | Number of tool calls made by the subagent |
| `prompt_tokens` | `int` | Prompt tokens the subagent used |
| `completion_tokens` | `int` | Completion tokens (0 on backends that don't report) |
| `lifecycle` | `str` | Terminal lifecycle (`idle` or `closed`) |
| `outcome` | `str` | Normal/error/interruption outcome |
| `done_reason` | `str` | Loop terminal reason |
| `run_seq` | `int` | Physical run sequence on this row |
| `error_available` | `bool` | Exact diagnostics exist behind `agent_inspect` |
| `context_scope` | `str` | `isolated` or `full_conversation` |
| `inherited_turns` | `int` | Inherited human-turn count for the launch snapshot |

## Tasks DAG

See [Tasks](/docs/kin/concepts/tasks/) for the `tasks` tool this event mirrors.

### tasks_changed

Emitted after every `tasks` tool call. Carries the full post-mutation DAG snapshot — the same authoritative shape the tool returns, so the tool_result and the event agree (and the panel reads the same shape the model sees).

| Field | Type | Meaning |
|---|---|---|
| `snapshot` | `dict` | `{tasks: [{id, content, status, blockedBy, blocks, ...}, ...]}` |

## Dynamic workflows

Telemetry for a `workflow` tool run (a model-authored orchestration script
fanning out subagents). All four carry the `workflow_id` (the tool-call id) so a
client can correlate a run's `started → phase/log/agent → finished` arc. The
authoritative state lives in `Session.workflows` (the `/workflows` polling
modal reads it there, mirroring the agent panel); these events are the
human-facing transcript breadcrumbs. **None carry subagent prose** — the only
thing re-entering model context is the tool's returned string.

### workflow_started

Emitted when a `workflow` run begins (after approval).

| Field | Type | Meaning |
|---|---|---|
| `workflow_id` | `str` | The workflow's tool-call id |
| `goal` | `str` | The one-line goal shown at approval |

### workflow_phase

Emitted on each `phase(title)` call in the script.

| Field | Type | Meaning |
|---|---|---|
| `workflow_id` | `str` | The workflow's tool-call id |
| `title` | `str` | The new phase title |

### workflow_log

Emitted on each `log(message)` call in the script.

| Field | Type | Meaning |
|---|---|---|
| `workflow_id` | `str` | The workflow's tool-call id |
| `message` | `str` | The log line |

### workflow_agent_started

Emitted when a subagent appears in a workflow fan-out (the `agent()` primitive is
called). The renderer learns the agent **exists** and its row label here; later
state changes come via `workflow_agent_status`.

| Field | Type | Meaning |
|---|---|---|
| `workflow_id` | `str` | The workflow's tool-call id |
| `agent_id` | `str` | The agent's id, minted as `<workflow_id>:<n>` |
| `label` | `str` | The row name — the `label` opt, else a prompt truncation |
| `status` | `str` | Starts at `queued` |

### workflow_agent_status

Emitted on each per-agent transition: `queued → running → done` / `error`, plus
two extra states — `structuring` (the brief forced-tool / prompt-JSON call after a
`schema` agent's own loop ends — see [structured output](/docs/kin/guide/workflows/#structured-output-agentschema))
and `cached` (a resume hit replayed the result from the run-journal instead of
dispatching — see [resume](/docs/kin/guide/workflows/#resume-within-run-by-cache)). The
renderer keys on `agent_id` to update the row.

| Field | Type | Meaning |
|---|---|---|
| `workflow_id` | `str` | The workflow's tool-call id |
| `agent_id` | `str` | The agent whose state changed |
| `status` | `str` | `queued` / `running` / `structuring` / `done` / `error` / `cached` |

### workflow_finished

Emitted on the run's terminal state.

| Field | Type | Meaning |
|---|---|---|
| `workflow_id` | `str` | The workflow's tool-call id |
| `status` | `str` | `done` / `error` / `cancelled` |
| `agents_run` | `int` | Total subagents the run spawned |

A `workflow` tool call mounts a **`WorkflowCard`** (anchored on the
`tool_call(name="workflow")` event, which carries the `goal` and the `script` —
the card pre-parses the script's `phase("…")` literals into its upcoming-steps
trail), and the renderer registers that id as a suppression key: every forwarded
child `tool_call` / `tool_result` / `tool_stream` stamped with that
`parent_tool_call_id` is **dropped**, so the card replaces the subagents'
tool-call firehose. The card is driven by `workflow_phase` (the trail + the
status-bar spinner label) + `workflow_agent_started` / `workflow_agent_status`
(the rows) + `workflow_log` (the card's one-line narrator; the `/workflows`
panel keeps the full history) and finalized by the workflow's own `tool_result`
(which carries the distilled string). `workflow_started` / `workflow_finished`
are no-ops in the transcript (the card is the surface).

## Terminal

### done

Emitted at the end of every turn. In a [headless run](/docs/kin/guide/headless/#exit-codes-the-contract) most non-clean `reason` values map directly onto the exit-code contract (`turn_cap`/`token_budget` → exit `1`, an unresolved blocking event → exit `2`).

| Field | Type | Meaning |
|---|---|---|
| `reason` | `str` | `stop` (clean) / `error` / `loop_detected` / `interrupted` / `turn_cap` / `token_budget` / `truncated` / `retired` |

Non-clean reasons mount a `SystemNote` + transient notify; clean / empty reasons stay silent. `turn_cap` and `token_budget` are the [fleet-safety](/docs/kin/guide/headless/#fleet-safety-the-per-run-token-budget) caps; `loop_detected` is the doom-loop guard.

### interrupted

Emitted when you press ++escape++ mid-turn (after the worker re-raises `CancelledError` and any in-flight tools clean up). See [Rewind & retry](/docs/kin/guide/sessions/#rewind-retry) for what happens to an interrupted turn's history.

### error

A harness-level error (back-end outage that exhausted retries, a config that fails resolution). A headless run's error paths are documented in [Headless runs § Exit codes](/docs/kin/guide/headless/#exit-codes-the-contract).

| Field | Type | Meaning |
|---|---|---|
| `message` | `str` | The error message |
| `recoverable` | `bool` | Guidance hint, **not** a retry gate. `true` for a transient/endpoint-reachability failure (a dead port, a 5xx/429 after retries exhausted); `false` (the default) for a request rejection (a 4xx — bad api key / model id / base url) the user must fix first. The UI renders different guidance from it ("the endpoint may be unreachable …" vs "the request was rejected …") and offers a focusable retry control either way. Set only at the permanent backend-error emit site (`loop.py`, re-running `_is_transient` on the cause); every other `error` emit site leaves the `false` default. |
| `redrive` | `bool` | **Retry gate** — distinct from `recoverable`. `true` ONLY at the single turn-path backend-hard-error emit site (`loop.py`'s permanent-error branch). Drives the UI's `RetryNote` affordance + the safe re-drive path: `Session.pop_for_redrive()` pops the failed turn's user message + trailing partial-assistant and rewrites the journal so a re-send lands ONE fresh user turn (no dup). Every other `error` site leaves the `false` default — a `/compact` or workflow-failure emit, or a generic catch in `_run_turn`, would otherwise re-drive into the prior COMPLETED turn and silently erase user input. Companion to `recoverable`: the guidance bit tells the UI what to render; the redrive bit tells it whether to enable the retry button. |
