Skip to content
10 changes: 6 additions & 4 deletions docs/implementation/runner-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,12 +324,14 @@ This runs before the coordinator opens.

## HTTP and UI contract

**Status reads.** `GET /api/runner` returns only the task and attempt rows plus `available`, `stateVersion`, `retryable`, `startable`, `resumable`, `unresolved` and `stopRequested`. `stopRequested` comes from the in-memory job: null, or `{ attemptId, reason, saved }`, where `saved` is false while the first-reason write has failed. The UI shows "Stopping (not saved yet)" only from this field. It does not change `stateVersion`, so user actions still compare against the durable state version. `unresolved` is computed by the server from the in-memory marker: null, or `{ attemptId, reason: "result-not-saved" | "start-not-saved" | "preparation-not-removed" | "storage-not-removed" }`: the terminal write or the start could not be saved; the host-side preparation files could not be removed (before the terminal write when D never ran, after it once D settled); or the task storage could not be removed after a saved terminal write. The UI shows "Needs restart" with that cause only from this field, because the durable row alone may still look active. It does not rebuild Git history or the full review.
**Status reads.** `GET /api/runner` returns only the task and attempt rows plus `available`, `stateVersion`, `reviewVersion`, `retryable`, `startable`, `resumable`, `unresolved` and `stopRequested`. `stopRequested` comes from the in-memory job: null, or `{ attemptId, reason, saved }`, where `saved` is false while the first-reason write has failed. The UI shows "Stopping (not saved yet)" only from this field. It does not change `stateVersion`, so user actions still compare against the durable state version. `unresolved` is computed by the server from the in-memory marker: null, or `{ attemptId, reason: "result-not-saved" | "start-not-saved" | "preparation-not-removed" | "storage-not-removed" }`: the terminal write or the start could not be saved; the host-side preparation files could not be removed (before the terminal write when D never ran, after it once D settled); or the task storage could not be removed after a saved terminal write. The UI shows "Needs restart" with that cause only from this field, because the durable row alone may still look active. It does not rebuild Git history or the full review.

**Start and resume (#91 part 2).** `POST /api/runner` with `action: "start"` or `"resume"` sends `expectedStateVersion` and an `actionId`, but no attempt ID. Both run the plan through `ItemExecutor`, item by item. Everything up to the first item's admission happens inside the user action's transaction, so a refusal rolls back the action's own writes.
**Start and resume (#91 part 2, #107).** `POST /api/runner` with `action: "start"` or `"resume"` sends `expectedStateVersion`, `expectedReviewVersion` and an `actionId`, but no attempt ID. Both versions are part of the idempotent request and must still match before the action writes. Both run the plan through `ItemExecutor`, item by item. Everything up to the first item's admission happens inside the user action's transaction, so a refusal rolls back the action's own writes.
- Both require an approval for every item at the current plan revision. Before an item completes, those approvals must belong to the current snapshot. After a completed item, approvals survive only from the snapshots that the completed execution prefix actually used (or the current snapshot); the completed-prefix head guard below then distinguishes that expected runner movement from an unrelated moved-and-returned head. Each attribution choice and approval is stamped with the review version at which it was saved, and approvals must be later than the latest applicable choice. The review UI marks any approval this execution gate rejects as stale so the reviewer can replace it. Schema v12 stamps legacy unversioned choices at a finite boundary, clears legacy unversioned approvals so the UI offers reapproval, and advances the review version once so fresh approvals can follow.
- `start` runs a task that is `in review` or `queued`, has no execute attempt at its current plan revision, and has no pending requeue (that is `resume`'s). An `in review` task moves to `queued` in the same action.
- `resume` continues a task that is `running` or `queued` and has started running (it has an execute attempt at any plan revision, or recovery left it to requeue). It starts at the first item with no completed execute attempt at the current revision, whether the last item completed, failed or was stopped, and claims `requeue_pending` in the admitting transaction. This is also how a task stopped between items continues.
- Both refuse a closed task, a pending cancel, a runner that cannot run execute attempts, an active attempt, an earlier run still finishing, an active merge, a scope pause (#88), and a plan revised after an earlier revision's items made runner commits (#88: those commits must be reconciled with the new revision first; earlier attempts that committed nothing do not block). During shutdown both answer 503 before any of these, and record nothing. When none of these applies, a finding or scope pause owed from an earlier run is acted on first, and nothing is admitted (`outcome: "settled"`). Only then is the first item admitted: admission refuses a spent task budget, which also moves an idle `running` or `queued` task to `needs human` (a task still `in review` stays there: its move to `queued` rolls back with the refused action).
- `resume` continues a task that is `running` or `queued` and has started running (it has an execute attempt at any plan revision, or recovery left it to requeue). It starts at the first item with no completed execute attempt at the current revision, whether the last item completed, failed or was stopped, and claims `requeue_pending` in the admitting transaction. Start never consumes a recovery claim, including when safety or scope work is owed. A completed prefix must still end at the task's current head; a moved head refuses instead of silently skipping earlier items. When owed work settles without an admission, that same transaction claims and clears `requeue_pending`. This is also how a task stopped between items continues.
- Both refuse a closed task, a pending cancel, a runner that cannot run execute attempts, an active attempt, an earlier run still finishing, an active merge, a scope pause (#88), and a plan revised after an earlier revision's items made runner commits (#88: those commits must be reconciled with the new revision first; earlier attempts that committed nothing do not block). During shutdown both answer 503 before any of these, and record nothing. Before the pending-cancel, active-merge and #88 refusals, a safety finding owed from an earlier run is persisted and acted on, and nothing is admitted (`outcome: "settled"`); this prevents an in-memory finding from being lost at restart. An existing human gate is preserved: schema v13 persists the evidence and an explicit owed marker, restart restores the pending escalation, and it remains owed until the person resolves that gate. An owed scope pause is offered and acted on only when the task status, pending-cancel state and merge state allow `pauseForAmendment` to commit it; otherwise the ordinary blocker is shown. Only then is the first item admitted: admission refuses a spent task budget, which also moves an idle `running` or `queued` task to `needs human` (a task still `in review` stays there: its move to `queued` rolls back with the refused action).
- The action owns the review version for the whole multi-item run. Before each later item is admitted, `ItemExecutor` requires that version to be unchanged and rechecks the current-revision approvals. A choice, approval edit or note saved while an earlier item runs therefore stops the run before the next item; the person must review and resume from fresh state.
- `GET /api/runner` adds `startable` and `resumable`, computed by the same checks, so the view never offers an action that would be refused. The view also withholds them while the task budget has run out (the action may still act on owed work, or else lets admission refuse it with its move to `needs human`) and while the runner is shutting down or needs a restart. Retrying an execute attempt is refused: a plan item runs again only through `resume`.

**User actions.** Cancel, retry and "run again" requests send `attemptId`, `expectedStateVersion` and an `actionId` idempotency key (see "Feedback-event contract"). A replayed `actionId` returns the saved outcome. The server takes the plan identity from its trusted configuration, never from the request, and every `Store` call is scoped by that identity. A mismatch returns HTTP 409 with the current state. The UI then shows that state and keeps any draft.
Expand Down
Loading
Loading