toryo docs
Documentation navigation

Dispatch

The job queue. Everything that runs an agent session goes through dispatch. Other parts of toryo push work in; a daemon drains the queue, spawning one worker through Claude Code, Codex, GitHub Copilot or Cursor per job, up to a concurrency cap.

Dispatch is deliberately not a scheduler (it has no concept of time) and not a sequence engine (it never chains one job to the next). It holds work, picks it in order, runs a bounded number at once, records the outcome, and reports.

Prompt and orientation#

A one-off dispatch job has two separate instructions. The prompt is the concrete, self-contained task for this run. The orientation selects durable role guidance and a default permission posture. Orientation shapes how the worker approaches its task, but it does not replace the prompt.

OrientationIntended useDefault access
plannerInvestigate and produce an actionable planRead-only
designerProduce design artifacts and minimal scaffoldingWrite-capable
reviewerAssess the target and report evidence and a verdictRead-only
implementerComplete and verify the requested changeWrite-capable
explorerResearch and synthesize findingsRead-only

One-off dispatch defaults to implementer when orientation is omitted. For example, an MCP caller can make both choices explicit:

dispatch_enqueue({
  prompt: 'Add a health endpoint to the billing API and test its response.',
  orientation: 'implementer',
});

For advanced callers, role instructions resolve in this order: an explicit role instruction override, a caller-supplied persona path, then the bundled persona for the selected orientation. The target project's own instructions still apply. See the CLI & contract reference for the complete command and payload surface.

A Claude session runs at most three subagents at once by default. The agents a Claude Workflow starts count toward that limit, though a workflow started beside running subagents can exceed it. The Worker setting maxSubagents changes that for every job, and a payload's own maxSubagents changes it for one.

How a job gets picked#

Jobs are claimed in priority order, then oldest-first. Priority runs 1 (highest) to 5, defaulting to 4.

Concurrency is capped, two at a time by default, and the cap is re-read every tick, so raising it in board's Settings applies immediately without a restart.

Write claims#

Within one working directory, a job that would write to files another job has claimed waits until that claim clears, and a job with no declared write scope locks the whole directory. Across worktrees of the same project, two jobs conflict only when their scopes name the same entry, since the risk there is a merge conflict rather than two agents in one tree. A sequence run holds its claim for the life of the run, not just its implementer step, so a sibling run waits with run-conflict instead of colliding at merge. When two runs overlap, the older one goes first.

A run's claim only holds up other worktrees while the run is on a step that writes code. While it waits on review, CI or a human gate, the claim goes dormant and siblings can proceed; it keeps its place in line for when it writes again. A job whose predicted files are already committed and pushed goes dormant the same way.

For a write conflict, toryo dispatch status <jobId> and the overlap row in Board's job detail name the entries in this job's own write scope that collide, and who holds them. Those are the entries you can narrow. If either side declared no scope, the overlap is the whole working directory. toryo dispatch conflicts --dir <path> --project <id> shows what a directory is queued behind.

Job states#

queued → running → success | error | crashed | cancelled

cancel on a queued job stops it immediately; on a running job it sends a termination signal on the next sweep.

If the daemon dies, a running job's session keeps going. On the next boot dispatch checks each one: a session still alive keeps its job running and its write claim, and a session that already ended is recorded from its own output. A clean finish becomes success, and anything else becomes crashed. A cancelled job never becomes a success this way.

What you get back#

Each job records its harness and model, exit status, token usage, duration, optional cost, and any artifacts the session produced.

For a Claude job the record also carries the session id, which a later payload can pass as resumeSessionId to continue that conversation instead of starting cold. If the old transcript has expired the job still runs, cold, and says so with resumedCold: true. Resuming a session recorded in a different working directory is refused. Claude's interactive mode and the other harnesses do not support resume, and a sequence retry does not use it.

Two logs are kept per job:

  • The event log, the session's full structured output: every tool call and result.
  • The stderr log, holding API timeouts, auth failures, and harness panics that never make it into the structured stream. Check this first when a job stalls, as it is usually the only place the real reason appears.

Board's Dispatch screen surfaces both, along with a lifecycle timeline per job and a crash view that groups recent failures so a systemic problem is visible as a pattern rather than a string of one-offs.

Admission before spawn#

Dispatch applies several checks before an agent process exists. A job naming a disabled harness is refused at enqueue, so it never enters the queue. After enqueue, required capabilities are negotiated against the selected harness before its provider is spawned. An unsupported requirement is also a refusal, not a partially started session.

Accepted work can remain queued when it is temporarily unsafe or too expensive to start. Dispatch evaluates the concurrency cap, provider circuit state, overlapping write claims, and provider-scoped usage headroom on every tick. Board's Dispatch view shows the deferral reason. Capacity in use produces slots-full; overlapping files produce file-conflict; a degraded provider produces circuit-open; a conflicting sequence run produces run-conflict; a paused queue produces admission-paused; and usage pressure identifies the held provider and budget.

The usage policy is configured in Board's Settings view with usageGatePolicy (--usage-gate-policy), while Board's Usage view shows the meters and headroom behind the decision:

  • included admits work only within the included allowance.
  • paid also admits work when the provider reports paid continuation as available, and is the default.
  • disabled turns off only the usage gate. Other admission checks still apply.

The controlling provider meter is compared with meterThreshold (--meter-threshold) per provider. The boundary is inclusive: utilization equal to the threshold is held. Missing, unavailable, or unreadable usage data fails open, so an unknown reading does not masquerade as zero capacity or stop the queue. Headroom is selected from what that provider reports; one provider's exhausted window does not hold another provider's jobs.

An enqueue with force is an explicit operator override. It bypasses the concurrency cap and usage hold, and its worker does not consume a normal slot. It does not bypass write-conflict protection, an open provider circuit, harness enablement, required-capability negotiation, or an admission pause.

You can also force a job that is already waiting, with toryo dispatch force <jobId> or Force start in Board's job detail. A job held by a conflict with another worktree of the same project can be let through with toryo dispatch override-conflict <jobId> or Start despite conflict, which names the other holder and the files both will edit. The override never passes a conflict inside the same directory, and neither command skips write conflicts in general, an open circuit, or a pause. Both act only on a queued job and exit 68 otherwise.

The observable distinction is deliberate: disabled harnesses and unsupported required capabilities are refused with an error, while capacity, usage, write conflicts, circuit state, and an admission pause leave an accepted job queued with a reason that can clear on a later tick.

Pausing the queue#

Run toryo dispatch pause to stop dispatch claiming new work, for example before an update. Jobs already running finish normally; queued jobs wait with the reason admission-paused, and a forced job waits too. The concurrency cap is left alone, so toryo dispatch resume restores claiming and nothing else. The pause survives a daemon restart and never expires, so Board shows a Queue paused banner on the Dispatch and Sequence screens for as long as it is set. The same switch is the admissionPaused setting (--admission-paused).

A dispatch that is stopping finishes its in-flight jobs first and reports draining in toryo dispatch health. toryo foreman restart --force will not kill it mid-drain.

Autonomous recovery#

A failed job is not simply dead. Dispatch climbs an escalation ladder (retry, re-probe authentication, refresh git state, and finally park the job for a human) with backoff that respects provider rate limits and a circuit breaker that stops hammering a provider that is failing every request.

This is why a transient API error overnight does not usually cost you a run.

totalCostUsd is separate from those gauges. For new Codex and Copilot jobs on a known priced model it is an API-equivalent token estimate, not a subscription charge or AI-credit balance. Cursor jobs are priced against Cursor's own published rates, which it does bill usage-based, so that figure is nearer a real charge while still being an estimate. Unknown models omit the field, and pricing updates do not backfill old jobs.

Live output#

The daemon publishes a live event stream, which is how board shows a running session's output as it happens rather than after the fact. A subscriber that falls too far behind is disconnected rather than being allowed to slow the dispatcher down.

Held sessions#

Conversational screens such as Sequence Design keep one Claude process open for the whole conversation instead of starting one per message. These held sessions have their own cap, maxHeldSessions (--max-held-sessions, default 3), and do not take a job slot. A held session claims its declared files like a job, so an overlapping job waits with file-conflict. An idle session closes after 15 minutes, and none survive a daemon restart.

Where things go wrong#

  • A job sits queued forever. Inspect its deferral in Board's Dispatch screen. Capacity, usage, a write claim, or an open provider circuit can hold it without refusing it.
  • A job ends crashed with harness_lingered. Claude finished its session but its process did not exit, so dispatch stopped it after 30 seconds instead of waiting out the job's timeout. The job's diagnostic names why the session ended.
  • A job goes straight to error. Read the stderr log, since auth and API problems land there and nowhere else.
  • Nothing runs at all. Run toryo foreman check; the dispatch daemon is probably down, or admission is paused: look for the Queue paused banner and run toryo dispatch resume.

Full command surface: CLI & contract reference.