Repository navigation
F1b: runner coordinator for attempts, slots and shutdown - #74
Merged
Merged
Conversation
In-memory jobs with two-step admission and a slot reservation; abortable preparation with task-budget and deadline timers; a synchronous launch check before D's start; first-reason stops that keep the slot until D settles; Store-chosen terminal states; unresolved markers on storage failure; cancel task; and shutdown that keeps existing reasons and waits for every job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane D (#71) made runnerOwner a required InvocationInput field and refuses task storage owned by another runner. The injected D now supplies the token in RunnerDeps, so preparation and the start call use the same one. F1d replaces the source with the per-database runner token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 28, 2026
…e or an admitted job - D settling with `unreleased` closes the runner to new work until restart, as Ask does. - A failed read after a refused markRunning no longer abandons the running handle. - Arming the timers moves inside the job, so a failed budget read ends the attempt instead of leaving it stuck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cause and the preparation timeout text - After D settles and the terminal write succeeds, remove the host-side preparation files before freeing the slot. A failed terminal write leaves them for startup recovery. - stop(..., 'stale', cause) keeps the cause; the agent's stderr is never recorded as the reason an attempt went stale. - A preparation timeout passes no D stop reason, so the row keeps "Timed out while preparing." as the spec requires. - Tests: the shutdown-keeps-reason case now covers an unsaved reason, which the durable row cannot repair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck the context first - The launch check refuses to start D once unreleased resources were reported, even for an already admitted attempt. - After the attempt deadline passes before launch, a later stop (shutdown, time limit) cannot replace the timeout. - The launch check tests the context before the time checks, so a changed context ends stale, as the spec orders it. - A stop after the terminal write, while only cleanup remains, is refused instead of setting a reason on a settled attempt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cover preparation failure and the held slot - A cancel task during a timed-out preparation is written onto the row by the Store; the job now shows it too. - Tests: preparation failure (spec regression round 7), and the start-not-saved marker keeping its slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed reason before cancel task - A context change at the launch check records stale, and a job that is ending before launch (cleanup, then the terminal write) refuses later stops, so shutdown or the budget cannot turn a stale or failed outcome into a requeue. - cancelTask first saves an earlier unsaved first reason, so the Store's own cancelled write does not replace it. The save bumps the state version, so it runs only when the caller's version is current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…writes; never free the slot before the marker - Admission can be part of a userAction transaction. The job now starts after that transaction ends, and if it rolled back, the job gives back its reservation without preparing anything. - A reason write that rolled back with its transaction now shows as unsaved, so the next cancel task saves it again. - An unexpected failure sets its marker before the job is removed, so the slot is never free in between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cancelTask mirrors the Store's cancelled write onto a job that no longer takes stops. If the caller's transaction rolls back, that reason is dropped again, so the attempt keeps its own outcome (for example the launch failure). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The adopted cancelled reason could reach the terminal write before the rollback check ran. It is now a status-only flag; the terminal write reads the row's own reason, so a committed cancel task still wins and a rolled-back one leaves the attempt's outcome alone at any microtask ordering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Transaction rollbacks can still cancel live work, and cleanup failures can release ownership prematurely.
Review effort: Balanced
Findings: 2
Open (3)
What changed in this PR
Adds the in-memory runner coordinator for attempt admission, execution, cancellation, settlement, slot ownership, and shutdown.
Changes:
- Coordinates writable and read-only attempt slots.
- Handles preparation, launch, stops, settlement, and unreleased resources.
- Adds comprehensive lifecycle and race regression tests.
| File | Description |
|---|---|
runner/coordinator.ts |
Implements runner lifecycle coordination. |
test/runner-coordinator.test.ts |
Tests admission, races, failures, cancellation, and shutdown. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…les remain - A settled result whose attempt ID or captured context is not this attempt's is never validated or saved; the attempt fails closed, as the question path does (Copilot review). - A failed removal of host-side preparation files, before launch or after the terminal write, keeps the slot held under a preparation-not-removed marker until startup recovery removes the attempt directory (Copilot review). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang
added a commit
that referenced
this pull request
Sep 29, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every invocation and checks against the task storage owner. executionDeps takes the database's token and the workspace allocates storage under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 29, 2026
mchwang
added a commit
that referenced
this pull request
Sep 29, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every invocation and checks against the task storage owner. executionDeps takes the database's token and the workspace allocates storage under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang
added a commit
that referenced
this pull request
Sep 29, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every invocation and checks against the task storage owner. executionDeps takes the database's token and the workspace allocates storage under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang
added a commit
that referenced
this pull request
Sep 29, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every invocation and checks against the task storage owner. executionDeps takes the database's token and the workspace allocates storage under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang
added a commit
that referenced
this pull request
Sep 30, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every invocation and checks against the task storage owner. executionDeps takes the database's token and the workspace allocates storage under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang
added a commit
that referenced
this pull request
Sep 30, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every invocation and checks against the task storage owner. executionDeps takes the database's token and the workspace allocates storage under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang
added a commit
that referenced
this pull request
Sep 30, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every invocation and checks against the task storage owner. executionDeps takes the database's token and the workspace allocates storage under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang
added a commit
that referenced
this pull request
Sep 30, 2026
RunnerDeps now carries runnerOwner (#74), which D requires on every invocation and checks against the task storage owner. executionDeps takes the database's token and the workspace allocates storage under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mchwang
added a commit
that referenced
this pull request
Oct 1, 2026
* F2b: per-item execution and the runner's commit step ItemExecutor runs a task's plan items in order as execute attempts. executionDeps materializes a fresh workspace and snapshots declared links, builds the prompt with prepareExecution, and in finish() inspects changes, audits them with auditRun, and makes the runner's own commit with Plan-Item/Plan-Revision trailers. The coordinator gains an async finish step and a release step after the terminal write; settleAttempt records the owned ledger entry with completed in the same transaction. A safety violation moves the task to needs human; out-of-scope files are committed and pause it in needs amendment with a checkpoint. The workspace is D's (#66), faked here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pass the runner owner token into execution deps RunnerDeps now carries runnerOwner (#74), which D requires on every invocation and checks against the task storage owner. executionDeps takes the database's token and the workspace allocates storage under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cover task storage release on the foreign-result and launch-failure paths After the rebase onto main, the coordinator releases task storage after the terminal write on main's foreign-result path too. Neither that path nor the launch-failure path had a test that failed without the release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix the independent review of F2b: runner-owned findings, storage on every path, stable plan, atomic pause - Safety violations are recorded by the runner's own audit (SafetyFindings), never read back from diagnostic text, so agent stderr cannot move a task to needs human, and a violation survives a stale or stop outcome. - auditRun treats any agent commit as a violation, per #66 decision 2 (no undo path). The TaskWorkspace.commit comment now says so. - An inspection that refuses sends the task to needs human (contract, Publishing step 2); an aborted one is the stop, not a finding. - Preparation that fails after allocating task storage hands it to the coordinator (PreparationFailure), which removes it after the terminal write. - The executor stops before the next item when the plan gets a new revision, and binds a checkpoint to the revision the item ran against. - The checkpoint and the move to needs amendment commit in one transaction (Store.pauseForAmendment), through the shutdown capability; a refused pause or status change returns a stopped outcome instead of throwing. - A rename stages both paths in the runner commit. - A storage-removal failure holds the slot as 'storage-not-removed', not 'result-not-saved'. - runTask returns stopped, with the items it completed, when admission is refused or the terminal write failed. - A test covers that storage is never removed when the terminal write fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 2 of the F2b review: keep scope findings, check before the commit, fail closed on bad reports - A scope finding always pauses the task: the checkpoint is bound to the revision the item ran against and the snapshot its own commit created (Store.snapshotWithHead), not to what is current, so a revision saved during release no longer drops the pause. The round-1 fix had turned that case into a stopped outcome, which a later run skipped past. - pauseForAmendment validates against the item's own revision; only a refused status change (a closed task) returns stopped, other errors throw. - Before the runner commit, after the last await, finish re-checks the stop signal and the captured context; a change makes no commit. - A change report missing any list fails closed as a safety violation, and an audit that throws is a violation too. - The foreign-result path removes host-side preparation files after the terminal write, like every other path. - snapshotDeclaredLinks gets every path the item declares, so D checks the actual entries (an earlier item may have renamed or added links). - Tests: the pause is atomic (a refused pause leaves no checkpoint), an aborted inspection is the stop, a stop or context change during the audit makes no commit, malformed reports, foreign-result cleanup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 3 of the F2b review: fail-closed manifests, a pause that cannot be skipped, whole-context checks - auditRun validates every field it reads before reading any: the metadata flag, each change's kind, entry types, underGit, rename old path, link target and traversal flag, and the total path bytes. A partial record fails closed as a violation instead of reading as clean (AGENTS.md). - A scope finding whose pause was never recorded (a failed write, the write gate, a crash) is paused at the start of the next run, before any item, so no run goes past it. A closed write gate is no longer taken for a refusal. - Between items the executor checks the whole context: the snapshot must be the one the previous item's commit created, and the assignment and referenced code unchanged, not only the plan revision. - An invalid commit ID from the workspace fails the attempt instead of breaking the terminal write. - Only the stop's own abort error counts as the stop; another inspection refusal stays a safety finding even when a stop is pending. - Pausing and escalating keep a status someone set during release: both require the task to still be running. - Tests for each, and for the audit-throws, rename-snapshot and checkpoint snapshot claims that had none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 4 of the F2b review: pauses that neither stick nor get skipped - A pause owed from an earlier run is paid when the task is queued again, not only while it runs, so a task can no longer get stuck on it. - A recorded pause holds until a person approves continuing on the current revision (continuationRevision); the continued run must name the next item, not one the checkpoint already completed. - A checkpoint is found by its item and its commit's head, not by the latest snapshot, so a later snapshot with the same head cannot make an approved pause owed again. - auditRun refuses a change path of "." or "..", and an old path on any kind but rename; the path-size limit counts only the saved paths. - finish refuses a commit equal to the base for a changed item. - A slot held under a marker is reported with that marker's real cause. - Tests for each, and for the referenced-code-only context check, the AbortError stop, the per-kind entry rules and foreign-result cleanup only after a saved terminal write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 5 of the F2b review: fail closed after a scope pause, canonical paths, JSON-sized limit - A task with a scope checkpoint runs no further items. Continuing after a scope pause needs its own design (reconcile the prefix, validate the rest from the checkpoint, consume the approval), tracked in #88; round 4's partial approval gate could block an approved continuation forever or skip items. Before round 4 the executor ran past the pause. - A checkpoint is found by its commit's head alone; two items cannot share the head of a scope commit. - This run's own pause and escalation keep any status someone set during release, queued included; only a pause owed from an earlier run is paid from queued. - auditRun refuses a path not in canonical form (./, a/../, //, a trailing /) and a .git part of any case at any depth. - The path-size limit measures each saved path as the JSON that stores it. - Tests for each, and for keeping task storage when a foreign result's terminal write fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 6 of the F2b review: escalate violations over any open status, stricter audit findings - A safety violation moves the task to needs human over any status someone set during release (queued or a human gate), since a run from there would launch the item again; only a closed task is left as it is. - A declared link retargeted into .git in any case or at any depth is refused, as paths already are. - A rename from an undeclared path records that source path as out of scope, not only the declared destination. - A change report that lists one path twice fails closed. - Agent-controlled paths in findings are quoted and lists cut short, and the finding text is bounded (AGENTS.md). - Tests for each, for task storage removed after the terminal write when a stop, a stale context or a cancel task lands during preparation, and for pauseForAmendment's plan-prefix check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 7 of the F2b review: owed safety findings, human gates kept, undeclared link removals - A safety finding is settled only once acted on. If moving the task to needs human fails, or the task sits at a human gate, the finding stays owed and the task's next run escalates it before launching anything. - Escalation moves only a running or queued task: leaving a human-gated or review status needs its own user action (runner-lifecycle.md). Round 6 had escalated over any open status. - Between items, a status someone changed during release stops the run. - Removing a pre-existing symlink, or turning it into a file, at an undeclared path is a safety violation. - The path-size limit counts a rename's old path, which round 6 made a saved scope finding when undeclared. Duplicate paths are found under the plan's path identity. - A refused runner commit fails with a quoted message. - Tests for each, and for the owed-pause status rule and the prefix check against the item's own revision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 8 of the F2b review: review statuses escalate, Git's .git spellings, undeclared link renames - A safety finding moves a review status (in review, approved but merge blocked) to needs human, so the task cannot be merged past it; only the human gates keep the finding owed. A task already in needs human settles the finding, so it is not escalated a second time. - Renaming a pre-existing symlink from an undeclared path is a violation, like removing or replacing it. - isDotGit refuses every spelling Git treats as .git (any case; NTFS trailing dots or spaces and git~1; HFS ignorable code points), in paths and link targets. - A case-only rename counts once under a case-folding identity. - runner-lifecycle.md lists every unresolved marker reason. - Tests for each, and for escalation refused by a merge in progress (the finding stays owed), escalation through the capability after the gate, an owed finding settled on a closed task, and completed plus the ledger entry being one transaction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 9 of the F2b review: scope pauses over review statuses, NTFS .git names, links to the root - A scope pause is recorded over a review status someone set during release (in review, approved but merge blocked), in the run that found it and when owed, so a merge cannot go past the finding; round 8 did this only for safety findings. A queued status set during release is still kept, and the next run pays the pause. - isDotGit ends a name where NTFS does (a stream separator or a backslash) before comparing it with .git. - A declared link retargeted to the repository root (., ./, a/..) is refused: the root contains .git. - A change report without a digest fails closed. - Tests for each, and for a declared link renamed to an undeclared path, a directory entry, an absolute path, every ignorable code point range, and escalation over approved but merge blocked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 10 of the F2b review: a split case-only rename is one path - A case-only rename that Git reports as a delete and an add (the file also changed a lot) counts as one path under a case-folding identity, instead of a duplicate that sent the task to needs human. Undeclared, the same pair is a scope finding; two adds of one folded path are still refused. - The runner commit's error handling drops a redundant abort special case: a stop records its first reason before it aborts, so the outcome is that stop. - Tests for an owed pause paid from a review status, a pause never overwriting needs human, possibly already fixed as a gate, task storage kept before launch when the terminal write fails, checkpointAtHead, and the unknown-snapshot check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 11 of the F2b review: a split rename is exactly one delete and one add - The duplicate rule keeps every entry per folded path and allows a second entry only for a case-only rename reported as exactly one delete and one add with different spellings. Round 10 compared each entry only with the last one, so add, delete, add (two adds of one path) got through. - The runner commit message writes the plan title on one line, so it cannot open a trailer block that forges Plan-Item or Plan-Revision. - Looking for .git reads a backslash as a directory separator (NTFS). - The completed-list comment says what a thrown error carries. - Tests for each, and for an assignment-only change between items, D's underGit flag on its own, task storage removed when the budget is spent at the launch check, and checkpointAtHead finding an older checkpoint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 12 of the F2b review: raw link targets, quoted preparation errors, remaining audit tests - A link target is checked for .git as written as well as resolved, so a .git part that a later .. cancels (.git/../src) is refused, as non-canonical changed paths already are. - A preparation error from the workspace is quoted in the diagnostic, like the inspection and commit refusals. - Tests for needs amendment as a human gate, snapshotWithHead picking the latest snapshot, .git behind a backslash in a link target, empty and NUL link targets, a NUL in a path, directory, other and gitlink old entries, the full HFS ignorable ranges, the 300-character quote cut, and an empty digest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 13 of the F2b review: quote every preparation error, test shutdown between items - The coordinator quotes every preparation error in the diagnostic, so a materialize error that names an agent-chosen path cannot forge a second line. Round 12 had quoted only the declared-link snapshot's errors. - Tests for runTask returning stopped with the completed items when shutdown refuses the next item's admission, and for a pause's executed prefix being built from the plan the item ran against when a revision inserts an item before it. The existing revision-during-release test now asserts that the revision really changed, since a failed import in release is absorbed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 14 of the F2b review: quote agent stderr, harden release-hook tests - The agent's stderr is quoted in the attempt diagnostic, so it cannot forge a runner line such as "Safety violation:" in the stopped reason. Round 13 quoted preparation errors for the same threat. - Tests that set up a change inside the release hook now also assert that no storage marker was left and the stop names the change, so a failing setup there cannot make them pass vacuously. - Tests for a rename with both sides undeclared and for an AbortError from the inspection with no stop pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 15 of the F2b review: refuse Windows path forms in link targets A declared link retargeted with a backslash or a drive prefix (..\..\outside, C:\Windows) passed the "leaves the repository" and "absolute" checks, which use POSIX paths, although the audit already reads a backslash as a separator when it looks for .git. Such link text is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 16 of the F2b review: a new run's start settles nothing of a run still finishing - runTask refuses to start while an earlier run of the task still has an active job (its storage release), so a second run cannot pay the first run's scope pause or safety finding and change that run's outcome. - Paying an owed finding or pause at the start of a run is that run's own decision, not settlement: it writes without the shutdown capability, and a closed write gate leaves it owed. - A finding whose terminal write failed reports the needs-restart cause and stays owed, instead of "An attempt is still active". - The coordinator's log lines quote D's error text. - Tests for each, and for a stop that lands before a rejected commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 17 of the F2b review: find every owed scope pause, cover the remaining claims - The owed-pause check looks at every completed execute attempt, not only the latest, so a later clean attempt cannot hide an earlier finding whose pause was never recorded. - Tests for an owed scope pause left owed (not paid through the shutdown capability) once the write gate closed, the ledger record's base, and the coordinator's log lines quoting D's error text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 18 of the F2b review: compare the whole context between items, stop after shutdown began - Between items the executor compares the whole context with what the previous item left: its commit's snapshot and a context generation raised by exactly that commit. A change that only bumps the generation (for example a same-valued reassignment) now stops the run too. - A run started after shutdown began pays nothing owed and starts nothing. - The needs-human settle test checks that no extra write happened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 19 of the F2b review: one run per task at a time, owed outcomes report not started - ItemExecutor holds a per-task in-flight flag from the start of runTask to its return, so a second run can never pay the first run's pause or finding, even in the microtasks between the coordinator dropping the job and the first run resuming (which the isActive check alone left open). - Paying or keeping an owed finding or pause reports the earlier item with state "not started", not that attempt's old state. - runner-lifecycle.md says when preparation-not-removed happens on each path. - Tests: a second run started at every microtask offset in the first run's release never pays its pause; owed outcomes report not started. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cover round 20 of the F2b review: every owed not-started path, the outside-job check, line separators Round 20 found no correctness bug. These tests close its gaps: an owed finding at a human gate, on a closed task and with a failed terminal write, and an owed pause refused at a gate, each reported as not started; runTask refusing while a job started outside the executor is still releasing its storage; and U+2028 in a plan title. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cover round 21 of the F2b review: a pause at a later item, an older owed finding Round 21 found no correctness bug. Tests now cover a scope pause at P2 (the checkpoint names the whole executed prefix) and an older owed finding escalated although a later attempt completed cleanly. The ItemExecutor docs state that its one-run-per-task guard needs one executor per Store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 22 of the F2b review: no control characters in the commit title, test a link to .. - The runner commit's title drops every control character, not only line breaks, so an agent-written plan title cannot put terminal escapes into git log. (A NUL is already refused earlier, by the prompt builder.) - A test covers a declared link retargeted to "..", the directory that holds the repository. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix round 23 of the F2b review: no bidi or format characters in the commit title, two more tests Round 23 found no correctness bug. - The runner commit's title also drops bidi and invisible format characters (U+200B-U+200F, U+202A-U+202E, U+2060-U+206F, U+FEFF), so an agent-written plan title cannot reorder how git log shows it. - Tests cover the C1 control range in that filter, and this run's own escalation rethrowing a closed write gate's error when it has no capability. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.


Lane F, step F1, slice F1b: runner coordinator. Related: #22, #51.
This replaces #56. GitHub closed #56 when #53 (F1a) merged and deleted #56's base branch, and a PR can't be reopened once its base is gone. This branch is #56 rebased onto
main. The review history is on #56.Changes since #56
main. The unsquashed F1a commit and the contract round 35 commit were dropped, because F1a: persist the runner lifecycle in the Store #53's squash already contains both. The F1b commit applied without conflicts.runnerOwnerthrough the coordinator. Lane D (D: label every resource with its runner, attempt and allocation (#51 item 3) #71) madeInvocationInput.runnerOwnerrequired, and D refuses task storage that belongs to another runner.RunnerDepsnow has arunnerOwner, and the coordinator passes it tocaptureInvocation. The injected D prepares storage and starts invocations under that same token. F1d replaces the source with the per-database runner token.26d9678).unreleased, the runner refuses all new work until restart, as Ask does. The resources are kept in memory and exposed asunreleased. They are not yet saved to the database: F1d adds that together with startup recovery.markRunning, the running D handle was dropped. Now the coordinator still cancels the handle and waits for it to settle.failed.7491e6b…db1d746).stop(..., 'stale', cause)keeps the cause. The agent's stderr is never recorded as the reason an attempt went stale.failed"Timed out while preparing.", as the spec says. A later shutdown, time limit or stop can't replace it.stale.unreleasedis not started.staleand isn't requeued. The same holds after the terminal write.cancelled. This runs only when the caller's state version is current.status()only.userAction. Retry and cancel are meant to run insideuserAction.4b69ea3).preparation-not-removedmarker until restart, and admission for the task says why.userActiontransaction that rolls back still cancels D. Fixing it needs a Store post-commit hook.What this does
Adds
runner/coordinator.ts, the per-process owner of in-memory jobs, slots and unresolved markers, as specified indocs/implementation/runner-lifecycle.md. D is injected (RunnerDeps.start), so this runs against a fake D until #51 lands; nothing is wired to the server yet.Store.admitAttemptruns. A refused transaction releases the reservation in the same turn. Writable kinds share one slot and read-only kinds share another; both limits are configurable.AbortSignal. The task budget timer recordstime-limit. The attempt deadline, before launch, fails the attempt with "Timed out" and records no first reason.pending, there is no first reason (saved or in memory), there is time left and the context is current. ThencaptureInvocation, then D's start. A context change during preparation endsstalewithout calling D. A stop during launch ends from its first reason. A start error endsfailedwith the launch error.markRunningis refused because a first reason exists, the handle is cancelled and settles normally. If the write fails with a storage error, the handle is cancelled withcapture-failure, settled, and the slot stays held under astart-not-savedmarker.status().stopRequested.savedshows when saving failed. The slot is never freed on cancel, only aftersettled.result-not-savedmarker that holds the slot until restart.cancelTaskgoes through the Store and stops the running work.close()rejects admission, recordsshutdownonly where no reason exists, and awaits every job with no timer. It leaves the Store open for the caller to close.Validation (head
4b69ea3)npm run typecheck: passes.CI's unit set (
npm testminus the Docker suites): 707 passed, 0 failed. The total rose from 488 becausemaingained tests. 45 of the tests are new, intest/runner-coordinator.test.ts, using a fake D whose promises the test controls, and real SQLite. The first test now also checks that D receives the injectedrunnerOwner. Each test added for a review fix fails without that fix (checked against the commit before it). The one exception is the case where saving the result and removing the files both fail, which guards existing behaviour.test/review.test.tsfailed once in about ten runs; it doesn't touch the runner code.Mutation check (run on F1b: runner coordinator for attempts, slots and shutdown #56's head
3767065): six guards were broken one at a time, and each was caught:close()not waiting;AGENTS.md race regressions covered here:
The Store-level cases (an old attempt settling after a retry, two processes admitting at once) are in F1a.
Browser tests weren't rerun: this slice doesn't touch
web/.Not in this slice
/api/runner, and the merge-coordinator catches.preparation_pgidis part of F1d, together with the abortable D helpers from D follow-ups required by the F1 runner lifecycle contract #51.🤖 Generated with Claude Code