Skip to content

F: start and resume a task's plan through /api/runner (#91, part 2) - #105

Merged
mchwang merged 30 commits into
mainfrom
feat/f-runner-start-91
Oct 3, 2026
Merged

mchwang merged 30 commits into
mainfrom
feat/f-runner-start-91

Conversation

@mchwang

@mchwang mchwang commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

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 (start accepts in review or queued).

What this does

POST /api/runner gets two actions. Each sends expectedStateVersion and an actionId, with no attempt ID. Both run the plan item by item through ItemExecutor.

  • start runs a task that is in review or queued, has no execute attempt at its current plan revision, and has no pending requeue. An in review task moves to queued in the same action.
  • resume continues a running or queued task 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 claims requeue_pending in the admitting transaction. This is also how a task that stopped between items continues.
  • Both refuse:
  • A spent budget is refused by admission itself. That refusal also moves an idle running or queued task to needs human.
  • Owed work: when none of those refusals applies, a finding or scope pause owed from an earlier run is acted on first. Nothing is admitted (outcome: "settled").
  • GET /api/runner adds startable and resumable, from the same checks. Retrying an execute attempt is refused: a plan item runs again only through resume.

How it works: ItemExecutor is split into #begin (the synchronous prelude and the first item's admission), #admit and #continue, plus a public begin().

  • begin() runs inside the user action's transaction and throws any refusal, so the action's own writes (the move to queued) roll back with it. runTask keeps reporting a refusal as not started.
  • An owed finding is settled only after its escalation commits (afterCommit).

No UI yet; the buttons are a follow-up.

Validation

  • npm run typecheck: clean.
  • vitest without the real-Docker files: 56 files, 1,285 tests pass at fb10295.
  • This PR changes nothing under 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)

Round Found Outcome
1 A plan revised mid-run left the task stuck; an owed finding was settled before the action committed; view flags disagreed with the action; wrong test setup field; missing regressions Fixed (a3e0a3a)
2 The budget check bypassed admission's needs-human effect (a regression from round 1); #88 refused tasks with no commits; the shutdown test asserted nothing Fixed (ed14125)
3 The start rule and docs disagreed; an in-review task could get stuck; a strict begin could report "settled" on a revision change Fixed (9ea6e1b)
4 No code defects; test and doc gaps Fixed (fb10295)

Declined: 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)

  • A refusal check placed before an action that has a side effect (the budget) → captured as a one-off in the code comment ("Only the view stops here"); covered by AGENTS.md "Check an operation's source-state preconditions…".
  • An in-memory effect applied before the outer transaction commits → covered by the F1: apply a stop only after the caller's transaction commits #79 rule (afterCommit).
  • A test whose assertion never runs (the 503) → covered by AGENTS.md "A test's setup must leave the state the production path would…" and the Design system and plan format #1 E2E rule.
  • A rule change without a test that fails when reverted → covered by AGENTS.md "give it a test that fails if the claim is false".

After #102 merged

Open design questions from review round 5. Each needs a decision, so none is changed here:

  • start does not check plan-item approvals.
  • start and resume do not check the plan's review_version.
  • requeue_pending stays set when a resume ends in needs human. runner-lifecycle.md says exactly one path clears it.
  • The F2: continue a task after a scope pause (reconcile the prefix, validate the rest) #88 refusals run before owed work is acted on.
  • resume does not check that completed items' commits are still in the task head across runs.

Copilot 2 on #105 (fixed in ea7136b):

  • The early size check counted raw bytes, so text that only exceeds the budget once escaped still read comment pages. The title and body now go through the prompt's own serializer first. Lesson: covered by the existing AGENTS.md rule "Align subprocess output limits with every payload…".
  • JSON.stringify leaves C1 controls, U+2028/U+2029 and bidi characters literal. A new quoteForTerminal helper escapes them. Lesson: one-off (a terminal-quoting helper and its test); see AGENTS.md "quote agent-chosen text".

Head ea7136b: test and real-docker pass. 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

mchwang and others added 13 commits October 1, 2026 16:26
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Status polling repeatedly loads and decodes unbounded full attempt history.

Review effort: Balanced
Findings: 1 Medium severity

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.

Comment thread runner/execution.ts Outdated
mchwang and others added 9 commits October 2, 2026 18:14
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>
@mchwang
mchwang changed the base branch from feat/f-runner-wiring-91 to main October 3, 2026 19:34
mchwang and others added 3 commits October 3, 2026 12:39
…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>
mchwang and others added 4 commits October 3, 2026 12:52
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Prompt-size prevalidation remains incomplete, and recovery warnings permit Unicode line-control injection.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
Resolved since last review (1)

Comment thread github/issues.ts Outdated
Comment thread runner/production.ts Outdated
#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants