Skip to content

Turns and rounds

A turn is one intent (review, address, nudge, act, the probe, or one your profile defines) on one unit, usually a pull request, from the moment wloop sends it until a record closes it. A turn on a pull request is also called a round: the first review round, a follow-up round, an address round.

The daemon keeps every turn in its store. What GitHub does not keep (your answers, the guidance of each round, what each session recorded, whether you stood behind a verdict) lives there and comes back in every later turn’s instructions, so a new session knows what the last one did.

StateMeans
sentwloop stored the turn and sent it; no agent has started it yet
startedan agent ran wloop instructions <intent> --turn <id>, or you started a turn by hand
askedthe agent stopped on a question for you
donethe work is finished; also set when a review of yours lands on GitHub while a review turn asks
settleda review round with nothing new to publish
failedthe turn broke; nothing went out
unrecordedthe session stopped without a record, or never started the turn
dismissedyou closed the question or the stopped turn without answering
replaceda newer turn of the same intent took over while this one was open

A turn’s instructions are stored text, rendered when the turn is created and read by hash, so editing your profile never changes a round already sent. They hold:

  • the procedure: the mechanics of the turn (reading the pull request, the ambiguity gate, publishing, the record that ends it), from the intent’s instructions and the shared partials;
  • the judgment: the blocks of your review profile that match this pull request (see Profiles and intents);
  • the brief, when briefs are on (see The brief);
  • your guidance for this round, verbatim;
  • Earlier turns: up to 10 earlier turns of the pull request, newest first, with their state, verdict, the head they covered, what the session wrote and what you said.

The first line names the turn id. The agent reads them with wloop instructions <intent> --turn <id>.

A turn of an intent with a [record] table ends with a record: its state, a one-line summary, the outcome (verdict, URL, counts) and, for a question, its text, its options (marking those that publish), a recommendation, the draft to read and the notes a new session needs to finish.

The agent sends it with the turn_end tool of wloop‘s MCP server, which the daemon serves at http://127.0.0.1:<mcp.port>/mcp. The daemon stores a record only for the turn it sent to, or that was started by, the calling session. A malformed record comes back with every problem listed, and the agent fixes it and calls again. A session without the tool writes the record to a file and runs wloop turn end <url> --turn <id> --file <path>.

A run can owe more than its record: record.requires in an intent’s manifest names a record kind of records.toml its run owes, and record.missing the gate of gates.toml a run opens when it fails or ends without its record, as when its session stops or goes away. That gate opens on its task, once, in place of a question; you answer it to resume the run with your text, continue it as it was, or dismiss it. It closes on its own when the run goes on another way, or a newer run of the intent takes its place. The bundled start requires a report, and every bundled intent that asks you a question names missing-record. An intent with no [record] table records no outcome and owes nothing, as attach does; one that sets no record.requires owes nothing more than its record, and its [record] table needs states only when it sets one of when, artifacts, outcome, require or attach_url.

wloop install adds three hooks to each Claude Code home:

  • SessionStart registers the session: its manager and process handle (an Orca terminal, or a Claude Desktop session), worktree, agent session id and home.
  • Stop asks the daemon whether the session’s turn owes a record; a turn of an intent without [record], such as attach, owes none. When it does, the hook blocks the stop once, telling the agent how to record. A second stop without one marks the turn unrecorded with the agent’s last message.
  • UserPromptSubmit records when you type in a session, so cleanup never closes a session under your hands. It prints nothing to the agent.

All three exit at once outside a session of an enabled workspace manager, give the daemon 2 seconds, and allow the stop on any error, so a broken wloop never traps an agent.

An asked turn shows its question in the cockpit’s detail, in a notification (macOS) and in the terminal client (W). You can pick an option, answer Yes or No to a confirmation, or write your own answer; text given with an option reaches the agent as a note after the choice.

The question leaves the row as soon as its answer reaches the session, typed, noticed through the Stop hook or carried by a resume turn: the row shows the loop working on it, and offers the question’s answers, Continue and Dismiss no more. An answer waiting for a busy session shows as queued in place of the question, with no options; an answer sent then replaces it, and Continue and Dismiss stay. When the agent records a new question in the same turn, that question shows as any other.

The question is a message, as is every note an agent posts mid-turn with task_post, and each waits on its gate (see Messages); every answer names the message and the gate it answers. The agent never receives the answer as text typed into its session. wloop tells the session that gate G was answered, and the agent reads the answer with the task_read tool, or wloop turn answer <unit> --gate G without the tool:

agent ──turn_end asked / task_post──▶ message N, gate G ◀── you resolve G
▲ │
│ notice: "gate G answered" │ answer stored with message N and gate G
└──── task_read ──▶ the answers of G, marked read

The notice goes:

  • into the session that asked, typed, when that session is active, loaded wloop‘s MCP server, is idle and was quiet for less than an hour;
  • through the Stop hook while that session is working or waiting on a permission: the answer waits, and the agent’s next stop is blocked with the notice as its reason. A newer answer to the question replaces one still waiting;
  • otherwise, for a question, as a resume turn: a new turn of the same intent, in a new session, for the pull request as it is now, with the question, the draft, the notes, the head it worked at and your answer. When the head moved since, it reviews the new commits before publishing an approval. An answer to a post whose session is gone stays stored. When that resume cannot start (its workspace is gone, or preparing it fails, as a git fetch without the SSH key loaded), or the notice cannot be typed, the answer is refused with the reason, the daemon logs it, and the question asks again through a new gate. A refused answer the agent reads anyway, as when the session took the notice later than its manager watched for it, counts as typed: its question leaves the card.

task_read with a gate returns the message of that gate, its options and every answer that resolved it; without one, every answer the agent has not read, oldest first. It also returns the task the session works on, its brief, its open gates and what you wrote on the task’s chat since its agents last read it (see Chat). Reading marks an answer and the chat read: the message’s card in the cockpit says whether the agent read each answer, a resume turn’s answer counts as read, and the answer-unread workflow notifies you when a session leaves a gate’s answer unread for 15 minutes (session.unread_gates). The texts of the notices live in the profile’s notices.toml.

A question the pull request moved past. While a question waits, the pull request’s task waits on you, and the rounds of its intent wait too: the sweep logs skip the <round> of <pr> at <head>: <why> once a day per head and reason, the reason being that the turn asks you something or that an answer is on its way to it. A turn.start step with open_question = "replace_if_moved", as the bundled review-follow-up has, lets a round planned on a head other than the one the question was asked on run anyway: it replaces the asked turn, its gate closes as superseded (no one answered it), and the new turn’s history carries the question and its options, so the agent asks it again when it still needs you or says why it no longer applies. An answer on its way to the question, stored but not yet in the turn, holds every automatic round of the intent until the agent records again. An answer that cannot reach the agent (typing its notice or resuming the turn fails) is refused with the reason, so it never holds a round; a gate’s answer stored and never routed, as when the daemon stops in between, is routed when the gate’s consequence runs again.

Continue sends a resume turn with no answer to an asked, failed or unrecorded turn. Dismiss closes it as dismissed. Answer, Continue and Dismiss are the intents of the question gate open on the unit, the question’s or the missing gate of a stopped turn; with none open, Answer answers again a question whose answer is still queued, which replaces it. An answer typed directly in the agent’s session still works, and closes the question’s gate as superseded.

A session is an agent’s conversation in one worktree, under an id wloop mints and starts the agent with (claude --session-id <id>). It is active while it has a process, dormant when its manager no longer lists a process for it, and closed when its worktree or transcript is gone, you closed it in its manager, or session.close closed it. A session missing from its manager’s census turns dormant, never closed, and a manager that quits or restarts never touches another manager’s sessions. A process the census lists orphaned, one no window of the app shows any longer with no agent in it, turns its session dormant through hygiene-session-orphaned without being ended, and a census that shows it in a window again wakes the session; session-orphan-close, off unless you turn it on, ends such a process once the census proves the orphan durable (see Cleanup guards). See the glossary.

A round you start by hand continues in the session of its workspace. An automatic round keeps that session only if it printed within the last hour, while its prompt cache is warm; otherwise it starts a fresh one, which costs less than rereading a long context. A warm dormant session is woken in its own manager: Orca opens a new terminal with claude --resume, Claude Desktop reopens the session. wloop types a turn only into a session that loaded its MCP server since it last started.

In Claude Desktop, wloop creates the session headlessly, imports it into the app and types each prompt through the app’s per-session socket. Prompts typed that way are not slash-expanded, so a slash command reaches the agent as an explicit instruction to run that skill. wloop creates Desktop worktrees under its own state directory (worktrees/<repo>/<name>), never under .claude/worktrees, which the app collects on its own. A worktree the app made there for one of its sessions is still Claude Desktop’s while that session is not archived, even when Orca lists it too: a round on the pull request developed in it runs its session there, in Claude Desktop, and moves to the next manager only when Claude Desktop cannot launch the session. Once the app’s session is archived, the app may collect the worktree, and Claude Desktop no longer lists it. While Claude Desktop is not running, its worktrees stay listed but their processes cannot be read, so their sessions keep their last state. Importing a session may bring Claude Desktop to the front.

A turn of an intent whose manifest’s runners include headless runs as one read-only call of the agent, with no session and no workspace. It may read only the unit’s checkout and its dossier, and proposes up to 3 drafts, which wait in the Outbox. The bundled act intent runs this way. headless.tier, headless.per_call_usd (0.5) and headless.per_day_usd (5) cap these calls.

The same caps count two kinds of calls that are not turns, so no turn row holds them: the dossiers’ compaction and closing calls, and the omnibox’s agent call. Each reserves headless.per_call_usd while it runs and records its cost, or that cap when the agent reports none, as an event (dossier_call, omnibox_call); the day’s spend is the headless turns’ plus those events, and a call the day’s cap leaves no room for is refused.

A request the omnibox cannot resolve on its own (⌘↵, or a sentence no verb or address matches) runs one read-only headless call: no session, no MCP server, no shell and no checkout. The daemon writes a snapshot of the read model into a fresh directory of its own, <state>/omnibox/snapshots/ask-*/: the open tasks with their units and properties (tasks.json), the views (views.json) and the repositories the workspace managers know (repos.json). The agent runs in the empty <state>/omnibox/run/, and its only tools are Read, Grep and Glob, granted on that one snapshot directory, not on the other calls’ snapshots beside it. The snapshot is removed when the call returns; one a stopped daemon left is removed when the daemon starts again. The prompt is the profile’s prompts/omnibox.md.tmpl: its before and after around the user’s request, and its follow for each question already answered, written after the request; wloop places the per-call markers around both, quoted as untrusted data. The call is bounded by two minutes.

The call answers either a list (a text and the ids of the tasks it lists) or a proposal (operations, each with its reason, and one question when something is ambiguous). Tasks and units the snapshot lacks are dropped, and so is every operation the omnibox’s vocabulary does not run, each with its reason. Nothing is stored but the call’s cost: the proposal is not a gate and resolves none, the client carries a question’s answer to the next call, and only the operations the user confirms run, through the same API calls a click makes. Over the day’s cap the call answers 429; an agent that fails or answers out of shape, 502; one that passes the two minutes, 504; an installed agent with no restricted mode, 503.

A dossier is the working memory of a task, shared by the turns on every pull request or thread it holds (a unit no task holds keeps its own), a local git repository under <state>/dossiers/ that the daemon commits after every turn. A turn’s instructions name its path; a session may read it and write under log/, sources/ and drafts/. A resolved dossier is archived after dossiers.archive_after (30 days) and purged dossiers.purge_after (400 days) later. See Files on disk.

Terminal window
wloop turn show https://github.com/acme/api/pull/7 # every turn, record and answer
wloop turn show https://github.com/acme/api/pull/7 --json # with each turn's instructions