Skip to content

Commit 79deeec

Browse files
mchwangclaude
andauthored
Check the planning agent at startup and before planning requests (#164) (#193)
* Check the planning agent at startup and before planning requests (#164) PlanningAgent.probe runs one planning invocation in planning's worker and profile, asking for the schema's own answer. PlanningCheck starts it when the server starts and again when the vendor in Settings changes; productionPlanning waits for it in `describe`, before the issue is read or anything is recorded. A failed check refuses the request with "Planning is unavailable: <reason>" for a minute, after which the next request checks again. Shutdown cancels a running check before it closes the worker. Closes #164 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the browser stand-in a passing check; test the check against the vendor (#164) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Start the check at once, bound the wait, recheck before the start (#164) Review round 1: the probe starts synchronously again (a deferred start broke three tests and let close wait out its cap); the check gets a real request's budget, since the first job may build the image, while a request waits for it at most 30 s; the request's validate refuses a vendor chosen during the issue read until its check passed; the refusal no longer advises a retry that cannot help; tests cover close waiting, the close cap, the wait and a wrong answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Start no check inside the Store's transaction; cancel replaced checks (#164) Review round 2: assertPassed, which the server calls inside its recorded action, refuses and starts a needed check on the next turn, so no worker, ledger or owner-token write runs inside the transaction; a check another vendor's replaces is cancelled; one deadline bounds a request's whole wait; the stub answers only the check's own prompt and schema; tests for a probe that throws and a replaced check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the stop after close and the single wait deadline; refusal wording (#164) Review round 3: tests show no check starts after close and one deadline bounds a wait across a vendor change; the deferred check reads the vendor again; the refusal ends its reason with a period and says a new agent is checked at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Put the check's reason last and whole; keep waiting past a replaced check (#164) Review round 4: a reason ending with a command is no longer followed by text; a request whose check was replaced while the vendor changed and changed back waits for the current check; the provider checks again as it invokes; the deadline test has margins a busy machine keeps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Promise only the recheck that happens; wording and test margins (#164) Review round 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Move a waiting request to a newer check at once; deferred check never throws (#164) Review round 6: a request waiting on a replaced check moves to the current one without waiting for the old one to stop; the deferred check reads the vendor inside its try; the refusal says 'Retry shortly' and names the configured recheck window; wider timing margins; the vendor test checks the leftovers record is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add a live Claude probe of the planning check; detach waits; wording (#164) Review round 7: the planning check against the real Claude service runs as a live probe in the Live probes workflow; a request no longer stays attached to the check after it settles; the still-checking refusal says the first check can take minutes; the shutdown waits are documented; wider timing margins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say when planning recovery runs; no listener warning for many waiters (#164) Review round 8: docs and comments say the planning check at startup is the process's first planning job; waiting requests no longer trigger Node's listener-leak warning; the stopping refusal carries the unavailable prefix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Reject from the wrapped invoke; say recovery needs a sign-in; wording (#164) Review round 9: the wrapped provider rejects instead of throwing; docs and comments say the startup check recovers only when it can sign in, holds the lock until the process ends, and builds the image beside the runner's; assertPassed after close says planning is stopping; wider timing margins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Exact wording on the planning lock, the image build and the verdict (#164) Review round 10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * First job is always a check; exact shutdown wait; live probe checks Docker (#164) Review round 11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bound each check; no recheck promise for a restart; exact claims (#164) Review round 12: a check that has not settled within a real request's budget and a minute fails and is cancelled, so the recheck applies; a reason that holds until a restart gets no recheck promise; the invoke-time check is described as defence in depth; startup failure exits only in the CLI; test names and docs say what they show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * A stopped wait is a refusal; no recheck promise without a sign-in; test the cancel (#164) Review round 13: an aborted request or Store error while waiting for the check is refused as such, never recorded as a failed GitHub read; a missing token, read only at startup, gets the restart wording; tests show a timed-out check is cancelled and stays failed if its probe answers late; durations under a minute read correctly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restart cases from codeboost's own text; close waits for timed-out probes (#164) Review round 14: a database with hard links gets the restart wording; the restart match ignores vendor output; close also waits, within its bound, for a timed-out check's probe; a Store failure while waiting reads as a failed check, not a stopped wait; shutdown cancels with the shutdown stop; the timeout rejects before it aborts; a test pins the probe's repository, head, prompt and schema; wider recheck margin; 'recheck window' in test names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restart wording for unsettled cleanup; stopping and Codex wording; limits stated (#164) Review round 15: cleanup that did not settle gets the restart wording; after close the refusal says planning is stopping; Codex is told to choose another agent; the Store-error test now fails the read after a wait has begun; the docs and comments state the shared-recovery, kept-pass and shutdown-deferral limits; test names say what they show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restart wording only for codeboost's restart-only reasons; stop on abort first (#164) Review round 16: a run whose cleanup did not settle keeps the recheck promise (its storage is retried, and the next check reports a real restart need), so the wording no longer depends on whether Claude printed anything; a request already aborted starts no check; docs state when a passed check is kept, the check's own unsettled cleanup, the lock after an abandoned worker and the replayed refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Codex wording names the next request; exact lock and wait wording (#164) Review round 17. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Exact check answer, repeated keys refused; test names and real cleanup text (#164) Review round 18: the check passes only on {"ok": true} written once; the cleanup-wording test uses cleanupFailed's real shape; test names say 'still checking' and 'recheck window'; the docs list the Store-error replay limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cite follow-up #198 for the review-view state and server-side limits (#164) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * JSON whitespace only in the check answer; exact wording and stub note (#164) Review round 19 nits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say when a changed agent is being checked; exact recovery and shutdown docs (#164) Review round 20: assertPassed's refusal says the chosen agent changed and is being checked; docs say a first job whose recovery fails leaves recovery to the next, that shutdown cancels the check after the server's grace, and how a check before the runner's setup fails on a task with runner commits (kept as a recheck, since the runner's setup sets the repository later). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restart wording says fix first; shutdown timing; drop a dead flag (#164) Review round 21: refusals for restart-only reasons say codeboost must be restarted after the cause is fixed; the still-checking refusal no longer claims every slow check is the first; the doc says shutdown cancels a check once planning requests settle or after the grace; the replaced flag, always implied by a changed current check, is gone; a test uses AgentWorker's real text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restart wording fits every restart-only reason; comment fixes (#164) Review round 22: a restart-only refusal says only a restart helps and to fix the cause first if the reason names one; the unreachable still-running branch is gone; the check timeout's doc says no request waits that long. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restart-only reasons tested from their sources; a cleared agent ends a wait (#164) Review round 23: the restart-wording tests build each reason with the code that gives it (abandonedWorkerText is exported for that); a request whose wait ran out goes on when Settings cleared the agent meanwhile; a Store error is labelled a stopped wait only when it is the request's own abort; the null-to-Claude path is the one tested for a vendor chosen during the issue read; the abort signal's listener limit, which Node 26 never warns about, is no longer raised; the suggestion-cancel row names the check that fails before the worker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Comment fixes; drop an abort check that cannot fire (#164) Review round 24: the request's signal is checked at the top of each loop with no await before the race, so the second check is gone; admit's doc covers no vendor chosen; the refusal prefix's doc names its refusals as examples; the recheck window's wording reads one way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Codex's stand-in check fails at once, as the real one does (#164) Review round 25: the tests that switch to Codex give it the probe answer PlanningAgent.probe gives (CODEX_PLANNING_REFUSED) and expect its refusal; a doc line is rewrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Reasons in the check's tests come from the code that gives them (#164) Review round 26: the vendor-output, unsettled-cleanup, Codex and multi-line reasons are built by planningOutput, PLANNING_FEATURE.cleanupFailed, CODEX_PLANNING_REFUSED and assertClear's wording; web/planning.ts says when the check starts with no vendor chosen; a comment names the recheck window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Real sign-in and Codex reasons in the check's tests; doc and layout (#164) Review round 27: the sign-in refusals use planningOutput's own text and the vendor-change test CODEX_PLANNING_REFUSED; checked() takes only the vendors Settings accept; the suggestion-cancel row gives the runner-repository case as one example of a check failing before recovery; comments rewrapped; abandonedWorkerText moved below the module's re-export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the wrapped invoke forwards after a pass; wording (#164) Review round 28: the invoke test first shows a passed check forwards the call as it is; the refusal for a changed vendor says codeboost checks it next, which holds even if the deferred check cannot start; 'records a planning request' in the docs; the browser spec's stand-in says no vendor is chosen there; one import from runner/planning.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pin the check's default wait, timeout and close bound (#164) Review round 29: one fake-clock test through productionPlanning, with nothing overridden, shows a request waits 30 s, a check fails after the planning budget and a minute, and close waits 20 s; it fails if any of them changes. The refusal comment notes the Store's 4,000-character record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Admit nothing after close, even under a passed check (#164) Review round 30: #check and assertPassed test for close before the covering check, so a request, its start and the wrapped invoke are refused as stopping once planning closed; a test shows it. The defaults test's close step names what close waits for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Admit nothing after close with no agent chosen either (#164) Review round 31: admit and assertPassed test for close before they read the vendor; the after-close test covers no agent chosen too. The PR body records the live probe run on the head with #163's budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * A request waiting at close is refused as stopping; close tests (#164) Review round 32: #verdict tests for close before a pass, so a check that passes after close admits nothing; tests cover a check deferred before close, start after close, and a request waiting at close whose check then passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Close wakes a waiting request at once; docs and a still-running test (#164) Review round 33: a request waiting at close is refused as stopping at once, even when its check's probe never stops; start's doc says it throws after close; web/planning.ts says the check gates a request when a vendor is chosen; a test covers validate meeting a check still running; a comment notes what a worker that never ends a job leaves for close. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Rewrap start's doc (#164) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Comment: the AbortSignal also holds one listener per waiting request (#164) Review round 34. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Rewrap a doc paragraph; drop a repeated issue tag from a test title (#164) Review round 35. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e46cb68 commit 79deeec

16 files changed

Lines changed: 1003 additions & 44 deletions

‎.github/workflows/live-probes.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
env:
4343
CODEBOOST_RUN_AUTH_PROBES: '1'
4444
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
45-
run: npx vitest run --project docker -t '\(live Claude\)$' --reporter=default --reporter=json --outputFile=live-probes.json test/agent-container.test.ts test/agent-question.test.ts test/agent-supervisor.test.ts
45+
run: npx vitest run --project docker -t '\(live Claude\)$' --reporter=default --reporter=json --outputFile=live-probes.json test/agent-container.test.ts test/agent-question.test.ts test/agent-supervisor.test.ts test/agent-planning.test.ts
4646
# Vitest exits 0 when it skips every probe, so check that each probe passed and nothing else ran.
4747
- name: Check that every probe passed
4848
run: node scripts/live-probes.ts check live-probes.json

‎docs/implementation/agent-isolation.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Each row is a T9 requirement for the Docker suite. The suite fails if any row fa
4242
| Task and scratch byte and inode limits hold | `agent-container`: task capacity; scratch capacity for Codex and Claude (`/tmp`, `HOME`, `CODEX_HOME`, output directory) |
4343
| Hard links and alias writes from `.git/config` and objects fail, and metadata stays unchanged | `agent-container`: metadata alias probe in planning, review and execute, with a digest of `.git` before and after |
4444
| Mountpoint replacement fails | `agent-container`: metadata and metadata alias probes (`mv` and `rm -rf` of `.git`) |
45-
| The Claude startup probes read the schema and return bounded valid output through their documented channel | `agent-supervisor` live probes (credentials required): Claude in questions through its stdout envelope; Claude in planning returns a schema-constrained answer as bare JSON; `agent-container`: the authenticated Claude startup path. Codex has no live probe while it is refused (#93). |
45+
| The Claude startup probes read the schema and return bounded valid output through their documented channel | `agent-supervisor` live probes (credentials required): Claude in questions through its stdout envelope; Claude in planning returns a schema-constrained answer as bare JSON; `agent-planning`: the planning check (#164) passes with a real sign-in; `agent-container`: the authenticated Claude startup path. Codex has no live probe while it is refused (#93). |
4646
| Nothing can be written beneath a gitlink, and execute and fix launch only on a clean tree (#81) | `agent-container`: the gitlink probe (writes, `mkdir`, `rmdir` and `mv` at every gitlink fail; the rest of `/work` stays writable); a pre-populated nested checkout, a restart with an untracked symlink parent and a restart with an occupied add destination are refused; a gitlink name Docker cannot mount, more than 256 gitlinks and an unreadable tree are refused; each check serves one profile over its own storage and head; the validator rejects a gitlink mount that is missing, writable, or of another size or mode; every directory above a nested gitlink is pinned, writable and `nosuid,nodev`, so renaming one fails (#99); the validator rejects a pin that is missing, read-only, at another subpath, from another volume, `volume-nocopy`, or not a volume |
4747
| Hostile input stays inside the boundary | `agent-container`: repositories with links that leave the checkout are refused, links inside the checkout still work, oversized repositories fail closed; `agent-policy`: option-like prompts; `agent-proxy`: hostile CONNECT traffic; `agent-supervisor`: hostile output |
4848

‎docs/implementation/ci.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ settings are not stored in the repository.
1010
| --- | --- | --- | --- |
1111
| `.github/workflows/ci.yml` | `test` | every pull request; pushes to `main` and `codex/**` | Type check; every top-level `test/*.test.ts` file except the Docker-backed files; the Playwright browser suite |
1212
| `.github/workflows/agent-isolation.yml` | `real-docker` | every pull request; pushes to `main` and `codex/**` | Type check; the complete Vitest suite against real Docker |
13-
| `.github/workflows/live-probes.yml` | `live-probes` | started by hand (`workflow_dispatch`); every night at 06:23 UTC | The four live Claude startup probes only |
13+
| `.github/workflows/live-probes.yml` | `live-probes` | started by hand (`workflow_dispatch`); every night at 06:23 UTC | The five live Claude startup probes only |
1414

1515
`test` and `real-docker` have no path filters. They run on every pull request,
1616
so both can be required checks. A required check that a path filter skips
@@ -112,8 +112,9 @@ The job:
112112
missing. A missing secret never gives a passing run.
113113
2. Runs the Docker Vitest project with `CODEBOOST_RUN_AUTH_PROBES=1`. It selects
114114
only the tests whose titles end in `(live Claude)`, in
115-
`test/agent-container.test.ts`, `test/agent-question.test.ts` and
116-
`test/agent-supervisor.test.ts`. The files run one at a time.
115+
`test/agent-container.test.ts`, `test/agent-question.test.ts`,
116+
`test/agent-supervisor.test.ts` and `test/agent-planning.test.ts` (the
117+
planning check, #164). The files run one at a time.
117118
3. Runs `node scripts/live-probes.ts check` on the Vitest JSON report. Vitest
118119
exits with success when it skips every test, so this step fails unless each
119120
probe listed in `scripts/live-probes.ts` passed and no other test ran.

‎docs/implementation/plan-library-alignment.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Issue #6 tracked the v1 plan library contract and the runner obligations that ca
2424
| Output limits at the exact byte | **This change** (tests; the supervisor already enforced the limits) | Supervisor enforcement: `test/agent-supervisor.test.ts` (real Docker). Decoder behaviour only, not a limit: the Claude decoder handles an envelope of exactly the stdout ceiling, and the 1 MiB parser limit applies to the decoded answer at the exact byte: `test/agent-adapter.test.ts`. At #6 the decoder re-serialized the answer, so the limit applied to the re-encoded text. *Later:* #166 returns the exact text of `structured_output` in the CLI's envelope, so the limit applies to those bytes |
2525
| Prompt byte budget before spawn | Done before this change | `MAX_PROMPT_BYTES` in `core/planning-author.ts`; `test/planning-author.test.ts` ("accepts exactly the prompt byte limit and rejects the next byte without truncation") |
2626
| Model budget before spawn | Moved to #163. *Later:* done in #163 | `MAX_PLANNING_INPUT_BYTES` and `PLANNING_MAX_TURNS` in `agents/policy.ts`; `test/agent-policy.test.ts` ("accepts a planning request at exactly its model budget and refuses one byte more"); [agent isolation](agent-isolation.md#planning-model-budget) |
27-
| Planning profile probed at startup | Moved to #164 | — |
27+
| Planning profile probed at startup | Moved to #164. *Later:* fixed in #164 (the review-view state is #198), checked at startup, on the first request after the vendor changes and on the first request a minute after a failure | `runner/planning.ts` (`PlanningAgent.probe`, `PlanningCheck`), `web/planning.ts`; `test/planning-production.test.ts` ("the planning check (#164)"), `test/planning-worker.test.ts`, `test/agent-planning.test.ts` (live Claude probe); `docs/implementation/planning-acceptance.md` |
2828
| Docker daemon log limits | Moved to #165. *Later:* fixed in #165 with `--log-driver=none`, checked before launch | `agents/container/profile.ts`, `agents/container/run.ts`; `test/agent-container.test.ts` ("creates the agent container with no Docker daemon log", "refuses an agent container with %s at validation") (real Docker), `test/agent-daemon-log.test.ts`. Agent containers only; the runner's other containers (task-storage scripts, the keeper, the egress proxy) keep the daemon's default log |
2929
| Claude structured output under the v1 parsing contract | Moved to #166 (found during this change). *Later:* fixed in #166 | `agents/adapters/claude.ts`; `test/agent-adapter.test.ts` ("lets draft validation refuse %s in a structured answer") |
3030

‎docs/implementation/planning-acceptance.md‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,59 @@ suggestion timer and the request share one ten-minute budget. The recording
8282
script still runs the provider in its own process and prints its runner label if
8383
it leaves anything.
8484

85+
The planning check (#164) proves that the vendor chosen in Settings can run a
86+
planning invocation before any request does. It is one planning job in the same
87+
worker and profile as a real request (read, list and search only, vendor-only
88+
egress, a mounted schema), on the review's code at the snapshot's head, with a
89+
real request's budget, since a process's first job may build the agent image. It
90+
asks for the schema's own answer, `{"ok": true}`, so it covers sign-in, the
91+
schema-constrained answer and the read-only profile (`PlanningAgent.probe`).
92+
`PlanningCheck` starts it when the server starts, before it listens, and again
93+
on the first request after the vendor changes, cancelling the check it replaces.
94+
`productionPlanning`'s `describe` waits for it, at most 30 s in all, before it
95+
reads the issue or records a planning request, and the request checks again just
96+
before it starts, so a vendor chosen during the issue read never runs unchecked.
97+
That last check runs inside the Store's transaction, so it starts no check
98+
itself: it refuses, and the check it needs starts on the next turn. The provider
99+
checks once more when it is invoked, as defence in depth (E3 invokes it in a
100+
microtask right after the recorded action); a refusal there settles the request
101+
as failed. A failed check refuses the request with a `GuardRefusal` that starts
102+
"Planning is unavailable:" and ends with the reason exactly as given (some end
103+
with a command to copy), and a check still running after the wait is refused
104+
with the same prefix; the server records the refusal and starts no agent.
105+
Requests within a minute of a failure are refused with it; the next request
106+
after that checks again. With no vendor chosen nothing is checked, and the
107+
request fails later, as before, with `NO_PLANNING_AGENT`. Once planning requests
108+
have settled, or after the server's 20 s grace for them, shutdown cancels a
109+
running check and waits up to 20 s more for it before closing the worker. A
110+
check that runs before the runner's setup has set the runner-owned repository
111+
fails, on a task with runner commits, with "which is not configured"; the
112+
recheck a minute later passes once the runner's setup has finished, and without
113+
a runner block it fails each time, as every planning request would. A check that
114+
has not settled within a real request's budget and a minute fails, so the
115+
recheck applies to it. A reason that holds until codeboost restarts (an
116+
abandoned worker, cleanup codeboost could not confirm, a database with other
117+
hard links, a missing token) gets no promise of a recheck, and Codex is told to
118+
choose another agent. A recheck joins the worker's one startup recovery, so a
119+
recovery that never ends fails each recheck until codeboost restarts. A passed
120+
check is kept until a check for another vendor replaces it: a worker abandoned
121+
later, cleanup that did not settle (the check's own run included, when its
122+
answer still came back), or a token revoked later, shows as a failed request. A
123+
refusal recorded when a request stopped waiting, or when the Store could not be
124+
read during the wait, is replayed if the browser resends the same action;
125+
keeping a Store error out of the record needs `web/server.ts`. Showing the
126+
check's state in the review view before a request, and the other limits that
127+
need `web/server.ts`, are #198. A check deferred from the Store's transaction
128+
can start during shutdown, before planning closes; close then waits for it
129+
within its bound. If startup fails after planning's setup, the CLI exits, and
130+
the next start's planning recovery removes what the check left. Tests:
131+
`test/planning-production.test.ts` ("the planning check (#164)", with a check
132+
that passes, one that fails and one that does not finish),
133+
`test/planning-worker.test.ts` (the check in planning's worker, including a
134+
wrong answer) and `test/agent-planning.test.ts` (against the vendor, real
135+
Docker: an invalid sign-in fails, and a live probe in the Live probes workflow
136+
passes with a real one).
137+
85138
The Store guarantees revision/snapshot binding and pending-only settlement across
86139
processes, and E3 requires the caller's snapshot identity before admission. G4 now
87140
composes the browser with production planning setup, lane D's provider boundary,

‎docs/implementation/runner-lifecycle.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,14 +361,14 @@ F1 owns the production planning endpoints that G4 needs. They wrap E3's coordina
361361
| `POST /api/plan/import` | `Store.importRevision` | expected revision; body of at most 6 MiB + 16 KiB, so every source the 1 MiB parser accepts fits (see `docs/plan-format.md`, #146) |
362362
| `POST /api/plan/suggestions` | E3 coordinator start (`beginSuggestions`) | expected revision and snapshot |
363363
| `GET /api/plan/suggestions/:id` | `Store.getSuggestions`, with the request's `mode` | reads only the request row; refuses a draft's ID ("Unknown suggestion request.") |
364-
| `POST /api/plan/suggestions/:id/cancel` | `pending`: `handle.cancel(reason)` on the `SuggestionHandle` that `SuggestionCoordinator.start()` returned. E3's coordinator has no cancel-by-ID method, so F keeps a map from request ID to handle, adds each handle when it starts the request, and removes it only after the handle's outcome settles. If the request is `pending` but F holds no handle, F calls `Store.cancelSuggestions`. After a restart this no longer arises while planning is on (a non-demo review with a `github` block): startup fails every request an earlier process left `pending` (#124). Without planning, such a request stays `pending` until it is cancelled here. It is safe to do so because a crashed process's planning container has no route back to the Store: its output went to that process's worker, which ended with it. A live planning provider runs only through D (#117), in planning's own worker, in containers labelled with this database's planning owner (not the runner's or Ask's). Like Ask's, those containers are removed by D's `recoverLeftovers` for that owner before the first planning request of the next process, not at startup; until then a stray container can keep running read-only (its deadline is enforced from the host, which is gone) until Claude exits or that recovery removes it, and its output is discarded. If a planning provider ever runs outside D, or could report into the Store from another process, this endpoint must refuse such a request instead, until it has been reconciled. `ready`: `Store.cancelSuggestions` (dismiss), because no invocation is running. | request is `pending` or `ready`, as `Store.cancelSuggestions` already allows |
364+
| `POST /api/plan/suggestions/:id/cancel` | `pending`: `handle.cancel(reason)` on the `SuggestionHandle` that `SuggestionCoordinator.start()` returned. E3's coordinator has no cancel-by-ID method, so F keeps a map from request ID to handle, adds each handle when it starts the request, and removes it only after the handle's outcome settles. If the request is `pending` but F holds no handle, F calls `Store.cancelSuggestions`. After a restart this no longer arises while planning is on (a non-demo review with a `github` block): startup fails every request an earlier process left `pending` (#124). Without planning, such a request stays `pending` until it is cancelled here. It is safe to do so because a crashed process's planning container has no route back to the Store: its output went to that process's worker, which ended with it. A live planning provider runs only through D (#117), in planning's own worker, in containers labelled with this database's planning owner (not the runner's or Ask's). Like Ask's, those containers are removed by D's `recoverLeftovers` for that owner in the next process's first planning job that can sign in: the planning check at startup when Claude is chosen and `CLAUDE_CODE_OAUTH_TOKEN` is set, unless that check fails before its recovery runs (for example, on a task with runner commits before the runner's setup has set the runner-owned repository, see `planning-acceptance.md`; or when planning's leftover record refuses it, or another process holds planning's lock) (#164, which also takes planning's lock from then until planning closes at shutdown, or until the process exits after an abandoned worker or a recovery stopped part way, and builds the agent image in planning's worker while the runner, if one is configured, builds its own), otherwise the first check a planning request starts (a check whose recovery fails leaves it to the next one); until then a stray container can keep running read-only (its deadline is enforced from the host, which is gone) until Claude exits or that recovery removes it, and its output is discarded. If a planning provider ever runs outside D, or could report into the Store from another process, this endpoint must refuse such a request instead, until it has been reconciled. `ready`: `Store.cancelSuggestions` (dismiss), because no invocation is running. | request is `pending` or `ready`, as `Store.cancelSuggestions` already allows |
365365
| `POST /api/plan/suggestions/:id/apply` | `Store.applySuggestion` | request `ready` and bound to the current revision and snapshot |
366366
| `POST /api/plan/drafts` | E3 coordinator start in draft mode (`beginSuggestions(…, 'draft')`, #124): a whole next revision of the current plan. The request lane D sees carries revision n+1, the revision the draft would become; the Store binds it to n | expected revision and snapshot; one planning invocation per plan at a time in this process, shared with suggestions (several drafts may be ready at once) |
367367
| `GET /api/plan/drafts/:id` | `Store.getDraft` | reads only a draft request's row |
368368
| `POST /api/plan/drafts/:id/cancel` | as suggestion cancel | request is a draft; a `pending` or `ready` one is cancelled, any other state is returned unchanged |
369369
| `POST /api/plan/drafts/:id/apply` | `Store.applyDraft`: imports the draft as revision + 1 with every import check | request is a draft, `ready` and bound to the current revision and snapshot |
370370

371-
Live planning runs for a non-demo review with a `github` block (#117): `web/planning.ts` reads the issue from GitHub before each start, and `runner/planning.ts` runs the request in planning's worker. E3's suggestion timer and the request share one ten-minute budget (`PLANNING_BUDGET_MS`). At shutdown E3 aborts every request; one that has not settled within 20 seconds (`PLANNING_SHUTDOWN_GRACE_MS`, as Ask) is ended by abandoning planning's worker. A start replays its recorded outcome before reading GitHub; a GitHub failure is a 502 and is recorded for replay like a refusal (`web/server.ts`; `test/planning-production.test.ts`). Without a `github` block, or in a demo, the server returns "Planning agent not available yet". Drafts follow the same rules (#124). A draft and a suggestion are both rows of `requests`, told apart by `mode` (schema v11); each kind's routes refuse the other's IDs. At startup, once the single-runner lock is verified to name the database (`web/planning.ts`), every request an earlier process left `pending` is failed ("The server stopped before this request finished. Ask again."): its provider ran in that process's planning worker, so nothing can complete it.
371+
Live planning runs for a non-demo review with a `github` block (#117): `web/planning.ts` reads the issue from GitHub before each start, and `runner/planning.ts` runs the request in planning's worker. E3's suggestion timer and the request share one ten-minute budget (`PLANNING_BUDGET_MS`). At shutdown E3 aborts every request; one that has not settled within 20 seconds (`PLANNING_SHUTDOWN_GRACE_MS`, as Ask) is ended by abandoning planning's worker, after the planning check (#164) has had up to 20 seconds more to stop. A start replays its recorded outcome before reading GitHub; a GitHub failure is a 502 and is recorded for replay like a refusal (`web/server.ts`; `test/planning-production.test.ts`). Without a `github` block, or in a demo, the server returns "Planning agent not available yet". Drafts follow the same rules (#124). A draft and a suggestion are both rows of `requests`, told apart by `mode` (schema v11); each kind's routes refuse the other's IDs. At startup, once the single-runner lock is verified to name the database (`web/planning.ts`), every request an earlier process left `pending` is failed ("The server stopped before this request finished. Ask again."): its provider ran in that process's planning worker, so nothing can complete it.
372372

373373
## Feedback-event contract (for lane J)
374374

0 commit comments

Comments
 (0)