A session's orchestrator (the main agent) can start work that keeps running
on its own: sub-agents (Agent/Task), workflows (Workflow), background
shells (Bash with run_in_background), monitors (Monitor), teammates
and remote agents. Nerve keeps a durable roster of that work per session:
one record per agent, with an accurate status, its brief, live progress, a
transcript, tokens and an estimated cost.
Two facts are kept apart and never derived from each other:
| Fact | Meaning | Drives |
|---|---|---|
| The turn is running | The orchestrator is producing a turn: a message you sent, or an autonomous turn the runtime started after background work settled | is_running, the Stop button, Esc, session_running |
| Agents are live | At least one agent is starting, running or stopping |
has_background_tasks, background_count, the agent dock, keeping the runtime alive between turns |
Background agents alone never make a session look "running": the composer stays usable while they work, and messages you send while a turn streams wait in a visible queue.
Sub-agent traffic never enters the orchestrator's turn. Everything a
sub-agent produces (text, thinking, tool calls and results, model usage)
goes to its own record and transcript; the orchestrator's messages contain
only its own work. The spawning tool_call block in the orchestrator's
message carries the record's agent_id.
Every record has a public id (ag_ + 12 hex, for example
ag_3f9a1c2b7d4e) from the moment its spawning tool call appears; the
runtime's task id binds later. The runtime reports a background task before
the spawning call's result, often without the call's id; that result names
the task, so every frame of the task lands on the one record the call
created (a task no call claims within 10 seconds gets a record of its own).
A foreground Bash call is part of the orchestrator's (or an agent's) turn,
not background work: it gets no record, unless it runs past its timeout and
the runtime moves it to the background.
Kinds: agent, workflow, shell, monitor, teammate,
remote_agent, mcp_task, other. Housekeeping tasks of the CLI
(dream, auto_mode_scan) are ambient: kept out of every list and count.
Statuses:
| Status | Meaning |
|---|---|
starting |
The spawning call ran; the runtime has not reported the task yet |
running |
Working |
stopping |
Nerve asked the runtime to stop it; waiting for the runtime to confirm |
completed |
Finished its work |
failed |
Ended with an error (or its spawn failed) |
stopped |
Stopped: by you (user), by Stop all (stop_all), with the turn (turn_stop), by the orchestrator's own TaskStop (orchestrator), or by something unknown (unknown_stop). Stopped is never shown as done |
interrupted |
Its runtime went away: gateway_restart, process_ended, force_stop, model_switch, archived |
lost |
It left the runtime's live set and no final status ever came (vanished), or it never started within 5 minutes |
The first final status wins; only a SendMessage resume of the same task
makes a finished agent running again (runs counts the runs).
Attention (server side): needs_input (a foreground agent asked a
question), failed, interrupted, lost, stop_unconfirmed (a stop with
no confirmation after stop_confirm_seconds). The web UI adds quiet for a
running agent with no activity for quiet_after_minutes.
Tokens and cost. Tokens come from the runtime's own meter when it
reports one, otherwise from the agent's model calls (each call counted once,
by its last frame). Cost is a live estimate from those calls, shown as ~$;
session totals still come from the runtime and are not changed by it.
- Inline lines. Each
Agentcall in the chat renders as one line at its spawn point: status, name, activity, elapsed time, tokens,[Open]and a two-step[■]stop. Old history without a record keeps the old card. - Agent dock. One line above the composer while the session has agents: counts (running, needing attention, done) and the latest activity. Click it to open the Agents panel.
- Agents panel. A tab in the side panel, grouped Needs attention →
Running (nested children under their parent) → Done → Ended, with
[Stop all…]in the header. - Agent tabs. One per opened agent: the live transcript, or a shell's output, or a workflow's tree. They never close on their own.
- Split Stop. The red button stops the turn. Its caret offers "Stop turn and send" and "Stop all agents…".
- Queued messages. While a turn streams, Enter queues the message: it is shown above the composer until it runs, in run order (the top one runs next). Any queued message can move up or down. You can edit or cancel your own (either way its text comes back into the composer).
| Action | What ends | Where |
|---|---|---|
Stop (red button, Esc, WebSocket stop, channel /stop) |
The orchestrator's current turn only. Background agents, workflows, shells and monitors keep running. With no turn running, nothing is sent | POST /api/sessions/{id}/stop {"scope": "turn"} |
| Stop one agent | That agent, and the agents it started (deepest first). The turn and every other agent keep going | POST /api/sessions/{id}/agents/{agent_id}/stop |
| Stop all agents… | Every live agent (shells and monitors optionally), then the turn. Task notifications the runtime already queued are dropped, so they cannot wake the orchestrator into relaunching | {"scope": "all"} |
| Force stop… | Everything: the session's runtime is shut down (the next message starts a new one); every live agent ends as interrupted (force_stop) |
{"scope": "force"} |
Messages queued in Nerve are never touched by a stop: a queued message runs after the stopped turn. Cancel it from its own chip (only its sender can).
When the turn does not stop. If the turn has not ended 5 s after the
interrupt and agents are live, the turn is not cancelled (a cancel shuts down
the runtime and would end them): the stop answers not_stopped with the
agents that would end, and the UI offers Force stop. With no live agents the
turn is cancelled as before.
When the runtime cannot spare agents. Nerve tells the Claude CLI at its
first initialize that an interrupt must end only the turn (the CLI's
perTaskStopAffordance flag, which the Python SDK cannot send; Nerve adds it
to that one frame). If that flag could not be sent (it is turned off, or an
SDK upgrade moved the code it relies on), an interrupt would end every
background agent and workflow. Stop then answers needs_confirmation with
the agents it would end and sends nothing; the UI asks first. Channel
/stop (Telegram, Slack) never confirms: on such a runtime it ends nothing
and replies that it couldn't stop the session without stopping its
background agents (stop them from the web UI). A turn that did not end
within 5 s while agents run is left running too, and /stop says so.
A stopped agent cannot be resumed with SendMessage (the CLI refuses);
relaunch it instead. Stops are recorded as session events with who asked:
stop_requested (details scope, outcome, live_agents),
agent_stop_requested, agents_stop_all_requested, force_stop_requested.
A message that arrives while the session's turn runs waits for it. While it
waits it is listed (GET /api/sessions/{id}/queue, the queue_update
WebSocket event, the agent snapshot) with a preview, where it came from
(web, api, a channel name) and who sent it. Internal triggers (scheduled
wakeups and runs) are listed with a fixed label, never their prompt.
Its sender can cancel it (DELETE /api/sessions/{id}/queue/{queue_id}): it
then runs nothing and stores nothing, and its text comes back. A message
sent with a client_msg_id that the session already saw in the last 10
minutes (a retry after a dropped connection) runs once.
The queue runs in its listed order: when a turn ends, the session's run lock
goes directly to the first queued message, and new messages go to the end.
The order can change (PUT /api/sessions/{id}/queue/order) for any queued
message, whoever sent it: also messages of other people, channel relays and
internal triggers. A queued message stays in Nerve until its turn starts:
the model never sees it during an earlier turn.
- Records and transcripts are stored (
session_agents,session_agent_events), written behind in batches about once a second per session; status changes are written at once. The roster survives a reload, a second tab and a gateway restart. - Every way a session's runtime goes away (a crash, a model switch, a force
stop, an idle reap, archive, delete, a gateway shutdown) marks its live
agents
interruptedwith the reason. A gateway that crashed without a shutdown settles leftover live records asinterrupted (gateway_restart)when it starts. - The next turn after such an ending carries one reminder for the orchestrator listing the agents that ended outside its control; it is not stored in the history.
- A runtime with live agents is never reaped for idleness, and a model switch
waits until they finish (
PATCH /api/sessions/{id}answers409 live_background_agents). - Archiving a session with live agents is refused (
skippedwith the reasonbackground agents running) unless forced (?force=true); the web UI asks first. Automatic cleanup skips such sessions. Deleting a session deletes its records.
| Keys | Action |
|---|---|
⌘/Ctrl ⇧ E |
Toggle the Agents panel |
↓ in an empty message box |
Open the Agents panel |
↑ ↓ Home End (in the panel) |
Move between agents |
Enter |
Open the agent's tab |
x x |
Stop the focused agent (press twice) |
Esc (in the panel) |
Back to the message box (never stops the turn) |
agent.subagents in config.yaml (see config.md):
| Key | Default | Meaning |
|---|---|---|
per_task_stop_affordance |
true |
Stop spares background agents (the CLI initialize flag). Off: Stop asks before it would end them |
progress_summaries |
true |
A one-line, model-written status per running agent (agentProgressSummaries; a few small-model calls per agent) |
forward_subagent_text |
true |
Stream foreground and nested agents' text and thinking too (background agents always stream everything) |
quiet_after_minutes |
10 |
The UI flags a running agent with no activity for this long |
lost_grace_seconds |
30 |
A member of the runtime's live background set that disappears with no final status becomes lost after this long |
stop_confirm_seconds |
10 |
A stop with no confirmation after this long is flagged stop_unconfirmed |
transcript_item_max_bytes |
16384 |
Cap on each stored or streamed tool input or result in a transcript |
Every key takes effect without a restart. The first three are read when a session's runtime starts, so they apply to runtimes started after the change.
- Codex sessions. Collab agents appear as read-only records (no
per-agent stop, no transcript); their result settles them, and one the
turn ended without a result becomes
lost. - Workflows are one record each; the agents inside a workflow stay in its tree and cannot be stopped one by one (stop the workflow).
- Transcripts follow telemetry retention when it is enabled
(
retention.*, pruned afterretention_days, starred sessions included); they are not part of the message compaction afterretention_full_days. Records (with their prompt and final report) live as long as the session. - Messaging an agent directly is not possible from the host: "Follow up…"
asks the orchestrator to use
SendMessage. - A tab subscribes to at most 8 agent transcripts at once.
- The
perTaskStopAffordancepatch relies on private SDK code (SubprocessCLITransport,_configure_can_use_tool, the shape of theinitializeframe), pinned bytests/test_claude_transport.py. After an SDK upgrade, check that a background agent survives Stop. When the patch does not apply, Nerve logsclaude transport seam unavailableor never logsclaude transport: initialize patched, and Stop asks before ending agents. nerve -vlogs one line per runtime task frame:agents[<session>] task_started task=<id> tool_use=<call id> type=<task_type> bg=… status=… depth=….- The tables are
session_agents(one row per record) andsession_agent_events(agent_id,seq,kind,payload). See api.md for the routes and events.