Sessions and turns
A session is a durable unit of work backed by SQLite. A turn is one pass through the agent loop within that session. Both are runtime concepts with persisted state, not presentation details, which is why a Zuno session can be resumed, replayed, and inspected after the process that created it is gone.
The model
| Concept | What it is | Where it lives |
|---|---|---|
| Session | One durable conversation and its work state | A session row plus its events |
| Turn | One user input driven to a terminal outcome | Turn-scoped runtime state plus durable events |
| Event | Anything model-visible: prompts, tool results, reports, retries | The session event log |
| Inbox | The FIFO queue of admitted-but-not-yet-promoted inputs | Durable inbox rows |
| Prompt receipt | The exact assembled prompt for one provider request | Durable receipt with section ids and a digest |
Child sessions are real sessions. Delegation creates one per child, with its own events, usage, and lineage. zuno session list hides them by default; --no-roots includes them.
Lazy materialization
An interactive new session is prepared without inserting a row. Its process-local identity is stable across model, agent, MCP, and theme changes, but opening, browsing, or leaving the welcome screen creates nothing durable. The first model-bound submission inserts the session and its user message in one transaction.
/new in the terminal application selects another prepared session in the same activation. It opens an empty conversation shell directly and does not bypass this boundary.
Durable inputs
Every model-visible external input is admitted to the event log and durable inbox in one SQLite transaction before execution is attempted. The inbox, not an in-process channel, is the source of truth across active turns, idle sessions, restarts, and competing drivers.
Drivers promote inputs in FIFO order. Promotion is transactional and can target one input identifier for a live soft interrupt. A malformed input records a session error and does not strand later queue entries.
User prompts and subagent reports share the same protocol:
- An active parent receives a soft interrupt and promotes the report at the next tool-safe point.
- If the report misses the final safe point, the wake coordinator waits for the active lease to end, then starts another turn while the input is still pending.
- An idle parent is claimed and driven immediately.
- A restarted process recovers pending reports from the durable inbox.
This is why a background delegation with reportDelivery: nextStep cannot lose its report to a polling race: the settle, the admission, and the wake are one transactional sequence.
Root-session peer messages use the same durable admission boundary. The root-only session_message tool writes a typed sessionMessage row into the target inbox. It may address another root in the same project or a descendant of the current root; children cannot send, and one root cannot address another root's child. The rendered text names the source session, Agent, and title and states that peer context is not user authorization.
TUI, HTTP server, and ACP drivers all recognize this shape as model-visible text. An active target receives a soft input at a safe point, an idle resident target starts a turn, and an offline target keeps the row queued until it is loaded again.
Product-agent, workflow, council, and durable background-command completions use the same continuation rule. The previous assistant reply may already have ended with a message such as “the background task is still running; I will wait.” That prose is not execution state. When the task settles, Zuno admits a typed durable input and starts another turn if the process still owns a runtime. TUI, ACP, and the HTTP server observe that turn through their normal event path. After a process restart, the same input is redriven when the session is activated; the command or subagent itself is not replayed.
An explicit close is different from an ended turn. Ending a turn leaves the session eligible for background continuation. Closing a session unregisters its resident notification watcher, interrupts and joins any detached continuation, then applies the surface's lifecycle policy to owned work. It does not promise that a non-resident process will continue generating.
For an SSH-hosted TUI, zuno tui --background supplies that resident owner. A detached loopback supervisor retains the PTY, TUI process, scrollback, and active turn while each SSH client is only an attachment. Ctrl+] detaches; --background-shutdown is the explicit lifecycle boundary that stops every retained child.
Prompt provenance
Prompt assembly uses stable section identifiers, an exact source, ordered content, and a content digest. The post-hook prompt is persisted before the provider request, so what the model saw is a durable fact rather than a reconstruction.
zuno debug prompt --session ses_1a2b3c --step 2
zuno debug prompt --session ses_1a2b3c --step 2 --show-sensitive2
--show-sensitive prints instruction, AGENTS, skill, and memory content verbatim. Treat it as sensitive output.
Compaction
When history approaches the model window, Zuno compacts older conversation rather than failing the request.
{
"compaction": {
"auto": true,
"threshold_percent": 80,
"tail_turns": 2,
"reserved": 12000
}
}2
3
4
5
6
7
8
threshold_percent accepts 1..=100 and defaults to 80, applied to the usable window after the model's output allowance and the configured reserve are removed. auto: false disables proactive compaction while leaving /compact available. A provider-confirmed context-limit failure still uses the bounded recovery path, which is recovery from an already failed request rather than the proactive threshold.
Compaction changes the provider transcript boundary. It does not delete the durable Goal, Plan, Todo, Job, inbox, event log, or prompt receipts. Those are regenerated from SQLite for the next relevant request, including a bounded runtime.work_state section capped at 64 entries per collection and 16 KiB overall.
Historical image bytes are excluded from the compaction request. A stable label such as [Attached diagram.png (image/png)] stands in, while the original durable file part is untouched for authoritative replay.
Optional continuity
Configuration examples and switching rules are documented in History and Notes continuity.
When continuity.history is enabled, the model can inspect normalized evidence from this session through successful-compaction windows. The tool never crosses a session boundary and excludes reasoning, encrypted content, synthetic internal prompt text, and binary attachment bytes.
When continuity.notes is enabled, logical working documents are stored by session_id + Agent. They are part of session lifecycle rather than filesystem state: session deletion cascades through them, prune counts and removes them, and session export/import preserves both notes and their idempotency ledger. Sanitized exports redact note identity and content and remove that ledger.
Interruption
Stopping targets the current input or turn, not future conversation. TUI cancellation captures that identity; supporting ACP clients can send expectedTurnId. A delayed cancellation for T1 cannot cancel T2. Legacy session-only cancellation targets what is current when handled; without a turn identifier it cannot recover the sender's intent.
After an ordinary stop, send a new message normally; /resume is not required. The new request has its own durable work scope. Old Plans remain visible as context, but a status question does not adopt them or automatically continue unfinished work. Only native Work authorization or a typed Plan/Todo mutation binds that work to a request. Old-cycle callbacks can be stored without waking a stopped cycle.
An interrupted Goal remains paused. Independent conversation still works; resuming the Goal requires the explicit Resume goal choice or /goal resume. Skipping the choice is not consent. Approval, Plan, authentication, budget and uncertain-side-effect barriers remain separate. /resume never certifies that an uncertain operation was inspected.
An old ordinary pause can be recognized automatically when new user input arrives only if there is no failed bridge and the original cycle, native events, and timing provide sufficient interruption evidence. Successful follow-up replies alone do not establish that authority. Existing v0.10.32 failed bridges did not retain complete preceding pause provenance, so they and unknown pauses remain gated and require explicit /resume for ordinary Work recovery. All existing recovery checks still apply. Finding a cancellation elsewhere in history is not enough; old failed receipts are not reset, reopened, or replayed.
After a stop is confirmed, the interface keeps the stopping state visible and suppresses late provider or tool output until a terminal event establishes the boundary. Durable persistence still runs. A side effect that completed before cancellation remains an observed result and is never mechanically replayed.
An execution gate is different from rejecting a message. If a new input was saved to history but could not be applied, its receipt stays recorded and carries executionGate, without appliedAt, completedAt, or turnId. ACP reports -32005 with admission: "accepted" and reason: "executionGated". Do not resend the message: inspect the receipt's recovery hint and the recorded gate. The hint does not authorize recovery or bypass any of the barriers above.
When ordinary /resume passes its native checks, it carries only the matching gated, unapplied anchor into the new cycle, without recording the text twice. A duplicate message-ID request may still observe the same gate until a real turn binds the input; normal application and completion follow. This does not reset existing failed, cancelled, applied, or completed receipts, resume a paused Goal, or authorize old callbacks. See ACP execution gates for the receipt fields and client behavior.
Retry and recovery
Recoverable provider, network, stream, SQLite-contention, Agent step-limit, and eligible tool failures persist an exponential-backoff retry before waiting, so a process restart reconstructs the deadline from SQLite. A turn that spends its own token, tool-call, or wall-clock allowance is not retried: it pauses the Goal with turn_budget and waits for a person, because the next turn would spend the same allowance the same way. A Goal whose own token_budget is what ran out ends as budget_limited instead.
{
"goal": {
"retry": {
"initial_delay_ms": 2000,
"max_delay_ms": 300000,
"jitter_percent": 20,
"poll_interval_ms": 250
}
}
}2
3
4
5
6
7
8
9
10
Delays are positive, capped, jittered, and interruptible by user input. A valid peer Retry-After is clamped to the configured ceiling and is never replaced by an earlier local delay. Retry decisions come from typed errors, never rendered messages: authentication failures and user interruption pause, while invalid protocol, corrupt durable state, and permanent configuration failures block.
Durable storage follows the same typed split. SQLite contention met while a Goal's budget is read or charged, or while the host writes Goal-owned state such as a Plan reconciliation or a human request, persists a database_busy retry and the Goal stays active, because a lock another writer holds is a condition that clears by itself. Any other database failure while the budget is read or charged stops the turn with usage_unknown and pauses the Goal so a person can inspect the database; durable state this build cannot read at all blocks the Goal. The persisted goal_retry row is what a restarted process reads back, so a restart neither loses nor shortens the wait.
The engine also detects a no-progress work-state read loop. plan_get and todo_get publish a semantic fingerprint of the authoritative state they observed; model-supplied intent narration is deliberately excluded. Three consecutive single-tool reads of the same fingerprint stop with typed stagnant_tool_loop. For an active Goal this persists paused(no_progress) and requires explicit resume. A changed Plan or Todo fingerprint resets the count.
Successful plan_update and todo_update calls invalidate the volatile prompt projection. Before the next provider request, the host rebuilds Goal/Plan/Todo/Job context from SQLite. A successful Plan mutation also replaces the one-time create-or-replace instruction with the maintenance instruction, so runtime.work_state cannot keep describing the Plan that existed before this turn changed it.
Clamping also decides what happens when a peer asks for more time than the same-request recovery has left. Under the default policy, replacement work gets a 180-second provider recovery window; a Retry-After longer than what remains of that window is neither slept through nor replaced by a shorter local backoff. The turn ends with the peer's error, and the Goal retry waits the peer's value clamped to max_delay_ms.
Continuing a session
zuno run --continue "now cap the page size at 100"
zuno run --session ses_1a2b3c "what changed?"
zuno run --session ses_1a2b3c --agent plan "what would a safe migration look like?"2
3
A resumed session keeps the Agent, model, and reasoning level it last ran with. Every surface that reopens a session — --continue, --session, the TUI /session picker, and ACP session/load or session/resume — resolves them in the same order, so a flag still wins and configuration is only the fallback:
| Setting | Precedence |
|---|---|
| Agent | --agent or picker > saved on the session > default_agent > orchestrator |
| Model | --model or picker > a preset chosen in this process > saved on the session > routed through configuration |
| Reasoning level | --variant, --thinking, or picker > saved with the session's model > configured defaults |
ACP stdio lifetime belongs to the editor that launched it. If Zed closes or loses that process, relaunch zuno acp and load the durable session id; the old stdio transport does not survive. Stable ACP v1 uses load as the authoritative reconnection path. A negotiated resume operation may be used by supporting clients, but transport reconnection never replaces SQLite inbox, event, Goal, Plan, and prompt-receipt recovery. The background-TUI supervisor intentionally wraps TUI PTYs, not ACP stdio.
In short: flag > session > config default. The session row records the pair it last ran with: a model, level, preset, or Agent pick in the TUI, an ACP configuration change, and session creation all write the model reference (with its variant) back, so the next resume starts from it; a zuno run flag applies to that run alone and does not rewrite the row. Naming a different Agent than the saved one re-routes the model through configuration, exactly as switching Agent inside a live session does; pass --model to pin it. When the saved Agent has left the roster or the saved model is no longer in the catalog, the resume falls back to the next rule and says so in a status note rather than failing; a saved level the model no longer declares is dropped with the same kind of note.
Forking a session is not part of this binary, so exploring an alternative without polluting the session you intend to keep means starting a fresh one: name neither --continue nor --session, and give it a --title.
Retention
The store grows. Listing, previewing, and cleanup are all zuno session:
zuno session list
zuno session list --no-roots --archived --format json
zuno session prune --older-than 90
zuno session prune --older-than 90 --archive
zuno session delete ses_1a2b3c2
3
4
5
With neither --archive nor --delete, prune is an inert preview and its counts match the delete that would follow. --archive sets a reversible marker; --delete is irreversible and has no undo in this binary. Read Session retention before running either at scale.