The execution model
Execution in Chorus is first-class and native. A unit of work is an ExecutionTask; each
claim opens a RunAttempt; an attempt produces Delivery records (a PR, patch, or report)
and uses Checkout records (a sandbox). All four are distinct rows with their own lifecycle
vocabularies, defined in src/db/adapter.ts:227-250.
ExecutionTask — the unit of work (RFC-291) └─ RunAttempt — one per claim; attempt_number; claim_lock; primary_delivery? ├─ Delivery — a PR / patch / report; is_primary; required_for_completion └─ Checkout — a sandbox: worktree / clone / container / ...; cleanup_ownerNative tasks vs. the removed legacy task-signal
Section titled “Native tasks vs. the removed legacy task-signal”There are two distinct “task” concepts. The boundary is load-bearing.
Legacy signal(type=task) |
Native ExecutionTask |
|
|---|---|---|
| How created | emit --type task |
tasks.create (chorus_task_create) |
| Status | Removed — 422 LEGACY_TASK_EMIT_REMOVED |
Current |
| Shape | a Signal row |
a typed execution row (adapter.ts:293) |
The catalog command signals.emit.task exists only to reject. Its execute() throws
LEGACY_TASK_EMIT_REMOVED with zero side effects (src/commands/handlers/signals.ts:53-58),
and all four transports converge on that single rejection point.
Lifecycle vocabularies
Section titled “Lifecycle vocabularies”Each entity carries its own state enum (src/db/adapter.ts:227-247):
| Entity | State field | Values |
|---|---|---|
ExecutionTask |
TaskState |
triage, todo, ready, blocked, done, cancelled, archived |
ExecutionTask |
TaskExecutionState |
idle, in_flight, degraded |
RunAttempt |
RunState |
16 values: queued, claimed, running, awaiting_report, heartbeat_stale, stalled, completed, timed_out, crashed, interrupted, superseded, cancelled, reclaimed, gave_up, protocol_violation, spawn_failed |
Delivery |
DeliveryState |
13 values: dirty, committed, pushed, pr_opened, checks_pending, checks_passed, checks_failed, review_requested, changes_requested, approved, merged, dismissed, report_only |
Checkout |
CheckoutState |
provisioning, ready, in_use, released, orphaned, failed |
RUN_ACTIVE_STATES (adapter.ts:232) names the in-flight RunState values
(queued…stalled); the complement is terminal. TASK_TERMINAL_STATES is
done, cancelled, archived (adapter.ts:234). A Checkout also has a CheckoutKind
(git_worktree, git_clone, container, devcontainer, vm, ssh, scratch_dir,
local, external — adapter.ts:248).
The four entities
Section titled “The four entities”ExecutionTask(adapter.ts:293-323) —{ring, title, body?, state, execution_state?, priority, capabilities[], base_repo?, base_branch?, graph_revision, blocked_by[], blocks[], assignee?, spawn_hint?, claim_lock?, claim_expires?, ...}. A minimal create needs onlytitle+ring; the server inferscreated_byfrom the authenticated caller and defaultstask_spec_revisiontomanual/v1(adapter.ts:325-336).blocked_byandblocksare compatibility projections rebuilt from authoritative relation rows; generic task updates cannot write them.RunAttempt(adapter.ts:346-380) — one per claim. Carriesattempt_number,claimed_by_identity,executor_kind,claim_lock,claim_expires_at,heartbeat_at?,primary_delivery?,termination_reason?.Delivery(adapter.ts:397-425) — the output of an attempt.is_primary,required_for_completion, optionalpr(PullRequestRef),commits[],checks[],artifact_refs[].Checkout(adapter.ts:430-449) — the sandbox the attempt runs in.kind,cleanup_owner(run_attempt/task/ring_janitor/external),state.
Authoritative task dependencies
Section titled “Authoritative task dependencies”Each blocker is a durable task_dependency relation. A dependency points either to another
task in the same ring or to an external object identified by {system, id}. The relation,
not the ExecutionTask.blocked_by array, is the source of truth.
tasks.dependencies.addadds an edge and blocks the dependent in the same transaction when the edge is unsatisfied.tasks.dependencies.updaterecords a new state for an external dependency.tasks.dependencies.removeremoves an edge and re-evaluates the dependent.tasks.graph.getreturns bounded diagnostics: relation rows, direct dependents, the global graph revision, unsatisfied count, and authoritative readiness.
Task blockers are satisfied by done, cancelled, or archived. External observations use
terminal states such as completed, merged, resolved, or satisfied. Completing only one
of several blockers leaves the dependent blocked; the final satisfied or removed blocker moves
an otherwise blocked task to ready. Self, missing, duplicate, cross-ring, and cyclic task edges
fail without partial writes. Traversal and per-task edge counts are bounded.
Dependency authority cannot change underneath active work. Adding an unsatisfied edge, changing
an external edge from satisfied to unsatisfied, or reopening a task blocker when the dependent has
an active lease fails with 409 TASK_DEPENDENCY_ACTIVE_EXECUTION_CONFLICT. Run completion repeats
the authoritative zero-unsatisfied-edge check inside its write transaction, closing the race
between a stale dependency read and a terminal commit.
Migration 047 inventories legacy arrays, converts up to 100 safe entries per task to relation
rows, quarantines missing/cyclic/ambiguous/over-limit entries, and rebuilds both compatibility
projections atomically. A dependency whose bounded traversal cannot finish is quarantined as
graph_limit_exceeded; it is not reported as cyclic unless a cycle was actually found.
Claim and lease
Section titled “Claim and lease”Claiming a task opens a RunAttempt under a lease only when the task is ready and has zero
unsatisfied authoritative relations. The lease TTL is taskClaimTtlMs()
(adapter.ts:338-344): the TASK_CLAIM_TTL_MINUTES env var, or 15 minutes by default.
tasks.heartbeat renews claim_expires. Expired leases are reaped on the background reaper
tick and the run attempt is released (gap-3 §1.4).
chorus tasks create --title "first task" --ring dev# -> task:... State: ready
chorus tasks claim task:... --claimed-by <your-identity> \ --transition-id claim-first-task-0001# -> run_attempt:... State: claimedTask claim, heartbeat, release, and cancel plus run progress, heartbeat, and termination require
a caller-stable transition_id. Reuse it only for retries of the same logical mutation; a payload
change under the same ID returns 409 EXECUTION_IDEMPOTENCY_CONFLICT. The task/run mutation,
receipt, and webhook outbox row commit atomically. The in-process executionTransitionBus is then
only a low-latency wake-up for webhook delivery; it does not recreate an already committed event.
Run progress follows a closed graph (queued → claimed → running, with bounded recovery paths
through awaiting_report, heartbeat_stale, and stalled). Terminal states are accepted only by
the terminal command. completed requires exactly one coherent ready primary delivery, every
required delivery proof ready, and zero unsatisfied dependencies. Terminal disposition is explicit:
completion marks the task done; cancellation marks it cancelled; retryable failures return it to
ready until the retry limit, while gave_up/protocol_violation and exhausted failures block and
degrade it. Every terminal/release/reap path clears active uniqueness authority.
Non-lifecycle task and graph changes still publish a {kind, from_state, to_state} record onto
the executionTransitionBus; webhook delivery durably materializes those legacy/non-atomic events.
Blocker state changes also emit transitions for each direct dependent whose relation-derived
readiness changed.
The SSE stream is not fed by this bus: it subscribes to the separate signalBus and emits
Signal rows only (src/streaming/routes.ts:161).
See signals for the coordination plane and the core loop for the end-to-end create → claim → emit sequence.