Repository navigation
F: start and resume a task's plan through /api/runner (#91, part 2) - #105
Merged
Merged
Conversation
An opt-in `runner` block in review.json turns on the production runner. After the Store opens and before the server listens, setUpRunner verifies the lock, reads the database's runner token, builds the agent image and runs startup recovery with D's own recoverLeftovers, exportTaskDiff and removal. It then assembles the execution deps over the real workspace and a Claude launcher. - Schema v9 saves each allocation's metadataBaseline and seeded commit, so recovery can export a recovered storage. Handles must match the row's attempt and allocation IDs. Diffs go through saveDiagnostic. - RunnerDeps.kinds: admission refuses attempt kinds the deps cannot run. - ItemExecutor.close(): shutdown awaits plan runs in progress before the Store closes. - GhIssueGateway.issueText: the issue's title, body and collaborators' comments, bounded, for the execute prompt. - CLI: --release-preparation; "Still stopping agents…" on a second Ctrl+C; refused runner startups print their message and exit 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The execute schema is chmod 0444 after writing: a restrictive umask made it unreadable to the container user, failing every launch. - Recovery's retention keeps every diff the same recovery saved; before, the second save could delete the first before finalization referenced it. - The agent image is built after D's recovery stopped leftover agents. - Unowned objects are listed one shell line each, the reason as a comment. - Issue text drops other people's comments before checking their body, and exactly 1000 comments no longer fails. - /api/runner refuses to retry an execute attempt outside ItemExecutor. - A failing shutdown step no longer skips stopping the runner or closing the Store; --release-preparation prints its error as a message. - Tests: materialize saves the baseline; D adapter arguments; server close order with the executor; retry refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Build the agent image as soon as D's recovery returns storage, before recoverStartup arms the first export deadline: the build blocks, so a cold build used up that deadline and lost the partial output. - Shutdown runs every step even when one fails, awaiting plan runs before the Store closes, and reports the first failure, not a later one. - /api/runner's view no longer offers retry for an execute attempt that the action refuses. - CLI refusals before the lock (bad port, config, runner block) print their message instead of a stack. - Unowned object names are shell-quoted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d take it (#91) - retryable also requires the deps to run the attempt's kind and no unconfirmed cleanup (runner.unreleased), as admission does. - Later shutdown failures are logged, not dropped behind the first. - dRecoveryDeps keeps its doc comment; startup order wording fixed in setUpRunner's comment and the README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ReviewService.planContext reads a base commit's tree once: each runner item no longer runs blocking git on the server's thread. - The runner reuses the issue text across a run's items for up to five minutes instead of rereading every collaborator and comment page. - A partial diff D cut at its limit ends with a codeboost: notice line. - A Ctrl+C during startup takes effect after startup recovery finishes. - removalCommand is shared by the runner's and Ask's leftover messages; RecoveryBlocked lists its items one per line. - CLI tests run web/cli.ts: runner refusal, bad port, release-preparation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The issue reuse window runs on the monotonic clock. - A second Ctrl+C during startup stops at once (recovery reruns at the next start); the first says so, and that a running image build stops. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Demos never run the runner, so telling a demo to add a runner block was advice that changed nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- start runs a task that is in review or queued and has not begun at its plan revision; an in-review task moves to queued in the same action. - resume continues a begun or requeued task from its first unfinished item (completed, failed or stopped last item alike) and claims the requeue in the admitting transaction, which also covers a task stopped between items. - ItemExecutor.begin admits the first item synchronously inside the user action, throwing the refusal so the action's own writes roll back; runTask keeps reporting it as not started. - GET /api/runner adds startable and resumable from the same checks. - Retrying an execute attempt points to resume. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A plan revised after it started running is refused by both actions, with #88 named, instead of each pointing at the other. - An owed safety finding settles only once the escalation commits: a user action rolling back after begin keeps it owed. - runChoice also refuses a closed task first, a runner that cannot run execute attempts, an active merge and a spent task budget, so the view's startable and resumable match the actions. - Tests: the settled-finding outcome, budget and revision refusals, replay and stale version, shutdown, begin under a rolled-back action; the between-items setup saves result as production does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A spent budget is refused by admission again, not by runChoice, so the refusal's effect still moves the idle task to needs human; the view alone checks the budget. - The #88 refusal applies only when an earlier revision's items made runner commits; a revised plan whose earlier attempts committed nothing resumes from its first item. - The view and runChoice see a run still finishing in ItemExecutor, and the view no longer hides a storage error as "not offered". - Tests: the shutdown case now reaches the action (rejectAdmission with the server open); budget asserts the needs-human move; the settled finding is no longer owed after the commit; both revision cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- start needs no attempt at the current plan revision; resume needs one at any revision (or a requeue). An in-review task whose only attempts were at an earlier, uncommitted revision can start again. - The doc and runChoice comment state that rule. - A strict begin throws on a revision change instead of reporting a settled run. - Tests: an earlier item completed unchanged is no commit and reruns at the new revision; a 503 start is not recorded and its action ID is refused afresh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Tests: an in-review task whose only attempts were at an earlier, uncommitted revision can start; a queued one is offered both actions, which run the same item; the unchanged-item test checks the revision. - Doc: start also refuses a pending requeue; a spent budget moves only a running or queued task to needs human. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Status polling repeatedly loads and decodes unbounded full attempt history.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Adds transactional start and resume actions to /api/runner, building on the production runner wiring from #102.
Changes:
- Adds start/resume admission checks and status flags.
- Refactors item execution for synchronous first-item admission.
- Adds lifecycle documentation and regression coverage.
| File | Description |
|---|---|
web/server.ts |
Exposes start/resume actions and availability flags. |
runner/execution.ts |
Splits execution into admission and continuation phases. |
runner/coordinator.ts |
Supports atomic recovery requeue claims. |
test/runner-start.test.ts |
Tests start/resume behavior and races. |
test/runner-execution.test.ts |
Tests synchronous admission and rollback behavior. |
test/runner-production.test.ts |
Updates execute-retry refusal expectations. |
README.md |
Documents the new API actions. |
docs/implementation/runner-lifecycle.md |
Defines start/resume lifecycle semantics. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Resolves the conflicts with #100 (gitlink mounts and the pre-launch tree check) and passes the tree check through claudeLauncher: D requires it for an execute start, so without it every production launch would fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot review on #105: progress() loaded and decoded every attempt (results up to 1 MiB) twice per /api/runner poll. Store.executeProgress now aggregates only the execute attempts' item, state, plan revision and unchanged flag in SQL, and the view computes it once for both flags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ma with D's reader (#91) A comma or line break in runner.root or diagnosticsDir passed the config and then failed every launch in D's mount check; it is refused at config time now. The launcher test also reads schema.json through D's own readCapturedFile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- issueText checks the issue against the execute prompt's own budget and serializer (dataJSON, 32 KiB), so an issue it accepts is one a prompt can carry; a larger one is refused at the fetch with the reason. - Storage limits must be own keys: inherited names such as toString are refused like any unknown limit. - README: the diagnostics cap is a retention target, not a hard limit. - CLI tests drive Ctrl+C during startup in a child process held by a fake docker: the first waits and exits 0 after startup, the second exits 130. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Diagnostics live per database, in <diagnosticsDir or root>/<runnerOwner>/ diagnostics: retention reads references from one Store only, so runners of databases sharing a root never delete each other's diffs. - --release-preparation without --config reports that it needs one, instead of printing help and exiting 0. - A demo configuration ignores its runner block however it is opened (config.demo, not the --demo flag). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it; clearer start refusal; tests that fail when broken (#91) - earlierCommits counts fix and rebase-fix commits too, as hasRunnerCommit does, and a result that is not valid JSON counts as a commit (fail closed). - start names the task's status, not resume, for a task resume cannot run either. - The rollback test now reaches the move to queued (admission refuses a spent budget) and replays the recorded refusal. - New tests: begin's in-flight guard between items, an owed scope pause on resume, an owed finding on start from in review, a fix attempt's earlier commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's tree equals this branch's part 1 commit 15db8db, which the branch already contains, so every conflict resolves to this branch's side; the merged tree is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tests for the queue move and paying nothing at shutdown (#91) - start points to resume for a running task that ran only an earlier revision, where resume runs. - Tests: start from in review queues before it escalates an owed finding; begin pays nothing owed once admission closed. - Docs: GET /api/runner's field list; when the view withholds start and resume. Comment on how earlierCommits differs from hasRunnerCommit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hutdown; refusals point only to an action that runs (#91) - During shutdown, start and resume answer 503 before any refusal, so nothing is recorded under the action ID (test: a body that finishes arriving after shutdown began). - A run still finishing between items is refused as such, not as an active attempt. - Start no longer points a fully run task to resume; resume names the status of a task that is neither running, queued nor in review. - Docs: owed work is acted on before admission refuses a spent budget; the full refusal list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sages name an action that runs (#91) - Test: while a run is still finishing between items, the view offers neither action and resume is refused. - "Still finishing" says try again, not start; resume points to start only where start's checks pass; start names the task's status for a run revision. - Docs: resume is no longer a later part. Test title matches what the 503 test checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- start states a pending requeue as the reason when nothing ran at this revision. - Docs list start and resume among user actions; the in-flight comment covers begin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- issueText refuses a title and body already over the prompt budget before reading any collaborator or comment page. - Recovery warnings (recoveryWarnings) quote Docker labels and file names, so a newline or control character cannot forge a line of output. - A lock.verify failure after startup prints its message and exits 1, not a stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#91) - issueText runs the prompt's own serializer on the title and body before reading any collaborator or comment page, so text that only exceeds the budget once escaped is refused just as early. - quoteForTerminal (lifecycle.ts) escapes what JSON quoting leaves literal and a terminal still acts on: C1 controls, U+2028/U+2029, and invisible format and bidi characters. Recovery warnings use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Part 2 of #91. Part 1 (#102) is merged (80ac5f2), and this PR now targets
main.Decisions: #91 (comment) and the 2026-10-02 comment on #91 (
startacceptsin revieworqueued).What this does
POST /api/runnergets two actions. Each sendsexpectedStateVersionand anactionId, with no attempt ID. Both run the plan item by item throughItemExecutor.startruns a task that isin revieworqueued, has no execute attempt at its current plan revision, and has no pending requeue. Anin reviewtask moves toqueuedin the same action.resumecontinues arunningorqueuedtask that has started running: it has an execute attempt at any revision, or recovery left it to requeue. It starts at the first item the current revision has not completed, whether the last item completed, failed or was stopped. It claimsrequeue_pendingin the admitting transaction. This is also how a task that stopped between items continues.runningorqueuedtask toneeds human.outcome: "settled").GET /api/runneraddsstartableandresumable, from the same checks. Retrying an execute attempt is refused: a plan item runs again only throughresume.How it works:
ItemExecutoris split into#begin(the synchronous prelude and the first item's admission),#admitand#continue, plus a publicbegin().begin()runs inside the user action's transaction and throws any refusal, so the action's own writes (the move toqueued) roll back with it.runTaskkeeps reporting a refusal as not started.afterCommit).No UI yet; the buttons are a follow-up.
Validation
npm run typecheck: clean.vitestwithout the real-Docker files: 56 files, 1,285 tests pass at fb10295.agents/and adds no Docker path. Part 1's real-Docker files (runner-workspace,agent-question) passed locally. Watch CI's real-docker job to the end before merging, because it is not required.Review rounds (independent agents, fresh context)
begincould report "settled" on a revision changeDeclined: acting on an owed in-memory safety finding before the other refusals. It exists only because its durable save failed, a restart loses it regardless of this order, and nothing unsafe runs meanwhile.
Review lessons (merge gate)
afterCommit).After #102 merged
main(F: wire the runner and startup recovery into the server (#91, part 1) #102, squashed) was merged into this branch (997b63e).main's tree equals part 1's final commit 15db8db, which this branch already had, so the merge changed no file.lock.verify()failure after startup prints its message instead of a stack.Open design questions from review round 5. Each needs a decision, so none is changed here:
startdoes not check plan-item approvals.startandresumedo not check the plan'sreview_version.requeue_pendingstays set when a resume ends in needs human. runner-lifecycle.md says exactly one path clears it.resumedoes not check that completed items' commits are still in the task head across runs.Copilot 2 on #105 (fixed in ea7136b):
JSON.stringifyleaves C1 controls, U+2028/U+2029 and bidi characters literal. A newquoteForTerminalhelper escapes them. Lesson: one-off (a terminal-quoting helper and its test); see AGENTS.md "quote agent-chosen text".Head ea7136b:
testandreal-dockerpass. A full local run was not possible (machine load average ~155); the changed files' tests pass locally.The five design questions above are not fixed here. They are follow-ups for a person to decide.
🤖 Generated with Claude Code