You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Feature: true resumable coding_loop runs after hitting max_iterations (not just re-running on leftover files) #806
When a coding_loop_start run hits max_iterations (or otherwise stops without completing), there is currently no real 'resume' — only the option to start a brand new run against the same repo. Today that new run happens to see whatever uncommitted files the previous run left in the working tree on disk, so if the prior run got partway through editing a file before stopping, a new run can stumble onto that leftover diff and build on it. But that's incidental: it depends entirely on git not having discarded anything, and the new run has none of the previous run's actual reasoning, iteration history, or context for WHY it made those changes or what it still intended to do. If the working tree were clean, or another run had touched the repo since, there would be nothing to pick up at all.
Requested feature: genuine resumability for a run that hit its iteration ceiling (or failed for a recoverable reason).
When a run stops at max_iterations, keep its full session/reasoning context addressable (not just the code artifacts on disk).
Give the owner a way to review why it stopped — what it had completed, what it was mid-way through, what it still intended — before deciding what to do next.
Allow the owner to then choose: (a) reset and start clean, (b) resume the same run with its prior context intact, optionally adding new instructions/info/ticket updates without losing what it already figured out, or (c) resume as-is with more iterations granted.
This should work even hours later — the point is a human can check back on a stalled run on their own schedule, add clarifying info or a course correction, and let the agent continue rather than re-deriving everything from scratch each time.
This came up today (2026-09-12) while retrying issue #150 on proappstore-online/platform: a run died on a relay timeout at iteration 4 of 20 with a good partial fix already written but uncommitted. A second run happened to build on that leftover diff successfully, but only by chance — it had no actual memory of the first run's reasoning, and a true resume would have been far more reliable.
Slice 1 shipped in 1ee023c6: a run that stopped at max_iterations now tells its successor what already landed. Items 1–3 are NOT done; they need a design decision (below).
What existed before
#523 (7cae041a) already gives a new coding run a resume note: pendingCodingResumeNote (lib/coding-resume-note.ts), injected as goal.resumeNote at workflows/coding-session.ts:733. It lists the acts the previous run on the same session observed to land, built from agent_events. But the gate was
the coding CLI's usage window outlasted the run's wait (#541)
provider_credit
✅ new
balance ran dry; #773's own sentence says "start the run again" after topping up
failed
❌
the run gave its verdict; repeating its steps may be right
escalated
❌
the run asked a human, answered through its own handoff
cancelled
❌
a human's choice, possibly this issue's option (a) "start clean"
done / no_progress / budget
❌
a verdict, or not something a restart should inherit
The note's first line now states the true cause per stop reason. A step-limited run is not told it was "interrupted by the platform". The interrupted wording is byte-identical and pinned by a test. The note is still immediate-predecessor-only, with a 6-hour lookback.
Verified: 17 new tests in coding-resume-note.test.ts, 20 → 37. (The commit message says 22 → 37; 20 is correct.) Restoring the old gate fails 3 of them. Full suite 11,409/11,409, both API typechecks, biome and the static guards are clean.
Not verified:
No live run observed. It needs a run that hits max_iterations followed by a new run on the same session.
Session reuse not traced. I did not confirm that coding_loop_start re-uses the session a stopped run was on. The note is keyed on session_id; a fresh session gets no note.
The resume note (#523, and slice 1 here) never reached an agent-started run's successor. Fixed in f8ded1bb.
The slice-1 comment listed "Session reuse not traced" as unverified. I traced it, and the note was keyed on something the successor usually doesn't have.
The bug, verified in code
The lookup matched on session.lastUnfinishedRunForSession queried agent_loop_runs … WHERE … session_id = ?3, with the new run's own sessionId (workflows/coding-session.ts:733).
Agent-started runs open their own session. That covers delegate_goal, start_work, the Loop button and coding_loop_start. With no active session, ensureSession creates one and returns opened: true (lib/coding-session-open.ts:463-498), which becomes sessionOpenedByRun: opened (lib/loop-drivers.ts:334).
A run-opened session is closed when the run ends.shouldEndSessionAfterRun({ openedByRun }) leads to endSession(..., status), with status "error" for failed/max_steps (workflows/coding-session.ts:954-971).
So the next run opens a new session.getActiveSessionForRepo only matches status = 'active' (lib/coding-store.ts:396). The lookup then ran against a session with no history and returned null, so no note.
The note only ever reached runs on a session a human opened, since those outlive their runs. #523's own incident was a delegated run cut off after 15 pushes whose session ended status: error: exactly the case the note couldn't reach.
The fix
Lookup:lastUnfinishedRunForRepo joins agent_loop_runs to coding_sessions and matches the repo of the caller's session, looked up within the caller's own user and instance.
Unchanged: the rest of the logic. It still takes the newest finished run and then judges it (immediate predecessor only), with the 6-hour lookback and RESUMABLE_STOP_REASONS. The workflow call site is untouched.
Wording: the note now says "a previous run on this repository", not "on this session". The predecessor is usually on an earlier session, so the old wording pointed the model at history that isn't there. The test pinning the interrupted sentence moved by that one word, on purpose.
Verified
coding-resume-note-repo.test.ts, 6 new tests against the real migrated schema (d1-sqlite), run end to end through pendingCodingResumeNote:
Run A opens S1 on repo R, lands a push, stops at max_iterations, and S1 is closed as error. Run B on new session S2 of R gets the note, and it names the push.
A session on a different repo gets nothing.
A done run on any session of R in between ends the note's job.
The caller's own unfinished run is skipped, and A is still found.
Another user's session id resolves to no repo.
The 6-hour floor holds.
Mutation-checked:
Restoring the session-keyed query fails 2 tests, including the A → B case.
SELECT * in place of r.* fails 2: the join's own status/started_at would overwrite the run's.
Suite: full suite 11,477/11,477; API typecheck (source and tests), biome and the static CI checks are clean.
Not verified
No live run. That needs an agent-started run stopped for a resumable reason, followed by a new run on the same repo.
Whether the end step runs when the Workflow itself is killed, not caught. If it doesn't, the idle reaper still closes the session, and the fix covers that case the same way.
Noted, not changed
Acts are read instance-wide.actsInWindow filters by instance and time window (lib/instance-work.ts:400-404), not by repo. If another repo's run on the same instance overlapped run A's window, its acts would appear in the note. That predates this fix.
Two slices from the prior run are now on main. CI and Deploy API Worker both passed on f8ded1bb.
Commit
What it does
1ee023c6
The resume note now reaches runs that stopped at max_iterations, engine_limit and provider_credit, not just interrupted. It also states the real stop reason.
f8ded1bb
The previous run is now looked up by repo, not by session, so a run started by an agent (which opens a new session) receives the note.
At run start the successor already gets a count of uncommitted files, unattributed, via the REPOSITORY STATE instruction (workflows/coding-session.ts:700-704, readRepoWorkingState → dirtyClause). That instruction says "You did not create this state". A note that attributes the same files to the previous run needs to agree with it.
Item 3(a), "start clean": not split out. Today this is close to cancelling and starting a new run. cancelled is not in RESUMABLE_STOP_REASONS, so that new run gets no note.
actsInWindow reads actions across the whole instance (lib/instance-work.ts:400-404), not by repo. Noted in the comment above; not filed as its own issue.
No further code was written in this continuation run; its objective was verification and bookkeeping only.
One gap noted in the previous bookkeeping comment has since been resolved.
Closed: actsInWindow instance-wide scoping
The previous comment noted under "Not verified / not filed" that actsInWindow reads acts across the whole Coder instance rather than scoping to the run's session. That was filed as #809 and fixed in cda8ef91 (merged 2026-09-13). CI passed. The lookup is now scoped to the run's session.
Item 3(b) "resume the same run with its prior context intact" depends on (i).
Buildable without an owner decision:
(ii) "Grant more iterations and continue" — needs a new API plus console UI.
(iii) Carry the stopped run's uncommitted working-tree changes into the resume note. Backend only. The return null at lib/coding-resume-note.ts:113 must change, and the note must agree with the REPOSITORY STATE instruction at workflows/coding-session.ts:700-704.
Slice (iii) shipped in a3a9ded5: a run cut off with its work still uncommitted now briefs its successor.
The gap
The resume note was composed only from agent_events, and an edit that was never committed is not an event. codingResumeNote returned null when no act landed (lib/coding-resume-note.ts:113). So this issue's own incident — cut off at step 4 of 20, a good partial fix on disk, nothing pushed — told its successor nothing.
What changed
codingResumeNote(acts, endedBy, uncommittedFiles = 0). With an unfinished predecessor and a dirty tree, a note is produced even when no act landed.
The count is the repo-state-start read the workflow already makes (RepoWorkingState.changedFiles). Nothing new is captured at stop, and nothing new is stored.
The note stays inside what the platform saw:
With no acts it opens "…and nothing it did is on the record as landed", never the "ALREADY landed" sentence, and it drops "treat the listed work as done".
The files "may be that run's unfinished work". The platform saw the tree, not the author; a human's half-finished edit looks identical.
It agrees with the REPOSITORY STATE instruction beside it ("You did not create this state… do NOT discard"): read the diff, build on it, do NOT discard it.
The workflow call is inlined, so coding-session.ts stays inside its file-size pin.
Unchanged
A clean tree produces byte-identical notes (pinned by a test, including the interrupted opening).
A dirty tree with no unfinished predecessor briefs nobody; that stays REPOSITORY STATE's job.
Predecessor lookup, the 6-hour lookback and RESUMABLE_STOP_REASONS.
Verified
10 new tests: 8 unit in coding-resume-note.test.ts (one of them a wiring assertion on the workflow source), 2 in coding-resume-note-repo.test.ts against the real migrated schema.
Mutation-checked: restoring the old gate fails 6 of them.
Full suite 11,639/11,639; pnpm typecheck; CI's biome command; all 13 check-*.mjs; docs:drift.
Not verified
No live run observed. It needs a run stopped for a resumable reason with uncommitted edits, then a new run on the same repo; agent_trace on the successor shows whether the note fired and whether it built on the diff.
The count is taken at the successor's start, not the predecessor's stop. Files a human edits in between are counted too, which is why the wording is "may be".
Still open here
(i) owner decision: persist the stopped run's reasoning, or keep option (b). Items 1, 2 and 3(b) depend on it.
(ii) "grant more iterations and continue": API plus console UI.
Slice (ii) shipped in 1b8cdb44: a stopped run can be continued — same objective, a new ceiling, briefed on what already landed.
Item 3(c), the half that needed no owner decision. Items 1, 2 and 3(b) still wait on (i).
What existed before
Nothing. maxIterations is fixed once, at createLoopRun (lib/agent-loop-store.ts:183), and the only way to act on a stopped run was POST /:id/loop/:runId/cancel — which does not apply to a run that has already stopped. Starting again meant retyping the objective into POST /:id/loop and hoping the resume note found the right predecessor.
What changed
POST /v1/instances/:id/loop/:runId/continue (routes/loop-continue-routes.ts, registered from routes/tools.ts:1153).
It is a new run, not a resumed invocation. A finished agent_loop_runs row has no live Workflow to grant steps to, and #523's option (b) means there is no transcript to reanimate. The continuity is the resume note.
The route is deliberately thin — driver.start() already picks the repo, opens or reuses the session, runs admission and takes the single-flight claim. What it decides is only:
Decision
Behaviour
Which runs qualify
isResumableStopReason — the same four as the note. Everything else is a 409 naming the next step (done → new objective; failed → read the outcome first; cancelled → that was your clean start).
Still running
409 on finishedAt === null, not on status — a cooperative cancel is still running until the top of its next iteration, and admitting it there is the #208 collision.
Objective
The stopped run's, verbatim.
Repo
Resolved from the stopped run's sessionId → coding_sessions.repo_id, so a multi-repo Coder continues the right checkout rather than repos[0] (#374). A chat run has no session and passes none.
Ceiling
The stopped run's own by default; maxIterations in the body grants more. Clamped by the same per-account ceiling as POST /loop (#477) — a continue is not a way around it.
Budget
A freshopenBudget. #184's rule that every autonomous entry point admits separately; inheriting an unspent pool would let one admission fund an unbounded chain of continues.
The lookback — item 4, and the only reason this differs from a restart
RESUME_NOTE_LOOKBACK_MS is six hours (lib/agent-loop-store.ts:257). Item 4 asks that this work "even hours later — the point is a human can check back on a stalled run on their own schedule". Under the default floor, a Continue pressed the next morning would have started a run briefed on nothing at all — i.e. the button would have been a restart with extra steps.
So a continue passes CONTINUE_RESUME_LOOKBACK_MS (30 days) down the chain: route → LoopStartInput.resumeLookbackMs → CODING_SESSION params → pendingCodingResumeNote({lookbackMs}) → lastUnfinishedRunForRepo.
It moves the floor only. The predecessor is still the most recent finished run on the repo, judged afterwards, so a verdict in between still ends the note's job. That is also why the note is not pinned to the continued run's id: pinning would skip that check and brief the new run on a checkpoint another run has already consumed — this issue's complaint pointed the other way. Pinned by a test.
Every ordinary start is byte-identical: no lookbackMs, six-hour floor, same note.
Surfaces
Console — a Continue button beside the ending label in LoopRunsSection.tsx, offered only after an ending with no verdict. lib/loopContinue.ts is pure and tested, with a test that reads the worker's own RESUMABLE_STOP_REASONS so the two copies cannot drift silently.
42 new tests — 24 route (loop-continue-routes.test.ts), 13 console-lib, 3 real-schema lookback tests in coding-resume-note-repo.test.ts (driven end to end on an in-memory SQLite built from every migration), 1 driver-threading, 1 workflow-wiring source assertion.
Mutation-checked. Dropping the lookback at any of its three links (route, store bind, pendingCodingResumeNote), the cross-instance guard, the unfinished-run guard, or the ceiling default each turns tests red.
Full suite 11,793/11,793; pnpm -r typecheck; biome; docs:drift; all 13 check-*.mjs.
CI green on 1b8cdb44, along with Deploy API / MCP / Host Worker and Publish MCP Registry.
Not verified
No live run. This needs a real run stopped for a resumable reason, then Continue pressed on it — agent_trace on the successor shows whether the note fired and whether the new run built on it. Same gap the three earlier slices record.
The 30-day figure is a choice, not a measurement. It is wide enough that the immediate-predecessor rule, rather than the floor, is what ends the note's job — which is the intended division of labour, but nobody has observed a continue at the far end of it.
Item 3(a) "start clean" — still not split out; today it is cancel-then-start, and cancelled is deliberately outside RESUMABLE_STOP_REASONS, so that path correctly gets no note.
Current state of main: clean and level with origin/main. Full suite 11,824/11,824 and pnpm -r typecheck pass; docs:drift, size:check, mcp:parity and openapi:coverage are green. pnpm lint does NOT pass — one error, e2e/console.spec.ts:4360 lint/complexity/useArrowFunction, which is pre-existing and in a file neither commit touches. No open PRs. Nine remote branches exist, including the merged-but-undeleted loop-continue-806 — not main alone.
Item 3(a) "start clean": currently handled by cancel-then-start; cancelled is outside RESUMABLE_STOP_REASONS by design, so the path already works correctly. Could be filed as a separate issue if explicit UX for it is wanted.
Unverified in production: no live stopped run has been observed going through the continue route end-to-end with agent_trace confirming the note fired. All coverage is via tests.
Slice item-2 shipped in 8d245d57: you can now see what a Continue would carry forward, before spending a budget on it.
Item 2, the two-thirds option (b) can answer. Items 1, 3(b) and the "what it still intended" third of item 2 still wait on (i).
The gap
The platform already composes this. pendingCodingResumeNote reads the acts the stopped run landed and the state of the checkout and turns them into the briefing the next run is handed. The problem is when: that happens inside the successor's own start, into the successor's prompt and timeline — so the owner reads it a minute after committing a budget, never before. The console offered a Continue button and, beside it, the stopped run's ending label. Neither said what continuing would inherit.
The fact that changes the decision
lastUnfinishedRunForRepo takes the most recent finished run on the repo and only then asks what ended it. That rule is right — it stops one checkpoint being handed out twice — but it has a consequence invisible from outside: a Continue on run A carries A's work forward only while A is still the newest finished run on its repo. Let a run B reach a verdict in between, and a Continue on A is briefed on nothing. It is a restart that opens a fresh budget, and it looked identical to one that resumes.
So the preview does not pretty-print a note. It answers which of three situations the owner is in, by running the same computation the run will run:
briefing.kind
What it means
this-run
The new run is told what this run landed.
other-run
A more recent stopped run on the same repo is the predecessor, so the briefing is that one's. The objective carries; the checkpoint does not.
none
Nothing carries forward — a later run reached a verdict, or nothing landed and the tree is clean, or the predecessor is outside the lookback. Continue is a restart on the bare objective.
What changed
pendingCodingResumeCheckpoint (lib/coding-resume-note.ts) returns the checkpoint with the facts it was composed from, and pendingCodingResumeNote is now a projection of it (?.note ?? null). One computation, so the page cannot describe a briefing the run does not get — two estimates of "what carries forward" is how a surface comes to promise work the run then re-does, which is this ticket's complaint from the other side. The three-way act split both readers need is one exported splitActs. A checkpoint with note: null is a real and distinct state the projection cannot express: there is an unfinished predecessor and it left nothing worth saying.
GET /v1/instances/:id/loop/:runId/continue-preview, beside the POST. A GET and not a dryRun flag: the POST's preview question is "would this be refused and how big would it be"; this one is "what would the new run know", which needs two reads the POST does not make. 200 even when the run cannot be continued — the refusal is the answer, and an owner reading why the button is absent is who this is for. The refusal and the ceiling come from helpers the POST now also calls, so the two cannot disagree.
The working tree is tri-state.uncommittedFiles is null when the runner did not answer, never 0. That is the expected case here — item 4 is an owner checking back the next morning, when the machine is off — and reporting a clean tree we never saw would be the exact over-claim this surface exists to stop. When it is unreadable the previewed note omits a clause the real briefing may carry, and caveat says so.
MCP preview_instance_run_continue (read, no dry_run). Shipped with the route, because a console-reachable route with no tool opens a new KNOWN_GAPS entry against a list MCP parity: 71 console capabilities have no MCP path — the first run's inventory, by group #613 is driving to zero. Parity holds at 29 recorded gaps, unchanged. Server 0.1.44 → 0.1.45; 204 → 205 registrations, always-on 180 → 181. read rather than runtime though it sits beside continue_instance_run: it starts nothing and opens no budget, and classing a review surface with the action it describes would make reading before acting cost the scope of acting.
Console — a "What carries over?" disclosure beside Continue, fetched on demand: this section polls every 5s while a run is open, and per-row-per-tick would put a git status on someone's laptop on a timer to answer a question nobody asked. Every line comes from the server, headline included (previewLines), so the panel and the briefing cannot drift.
Verified
46 new tests — 17 pure (loop-continue-preview.test.ts), 17 route (loop-continue-preview-routes.test.ts), 6 real-schema driven end to end through the migrated SQLite (coding-resume-note-repo.test.ts, 12 → 18), 6 console-lib (loopContinue.test.ts, 13 → 21). The 24 existing POST tests are untouched and pass, which is the evidence the shared-helper refactor moved no behaviour.
Mutation-checked. Each of these turns tests red: dropping the continue lookback from the preview; passing 0 instead of the tree read; fixing kind to this-run; collapsing an unreadable tree to 0; having the console compose its own headline; making the note a second computation rather than a projection.
No live run. This needs a real stopped run previewed against a real checkout — agent_trace on a subsequent continue would show whether the note that fired is the one the preview showed. All coverage is tests. Same gap the four earlier slices each record.
other-run has not been observed in the wild. It is constructed in tests; nobody has seen the ordering that produces it on a real repo.
Still open here — and why this commit carries no closing keyword
(i) is an owner decision and a Pilot cannot make it. Items 1, 3(b) and the "what it still intended" third of item 2 all depend on whether the stopped run's reasoning is persisted, or whether #523's option (b) — checkpoint the objective, not the transcript — stands. A plan is not a fact the platform holds, and coding-resume-note.ts deliberately refuses to state a remainder it would have to invent; this slice does not work around that.
Every buildable engineering slice is now done:
Item
State
1 — keep the reasoning context addressable
blocked on (i)
2 — review before deciding
shipped here, minus "what it still intended" (blocked on (i))
3(a) — start clean
works today as cancel-then-start; cancelled is outside RESUMABLE_STOP_REASONS by design
3(b) — resume with prior context intact
blocked on (i)
3(c) — grant more iterations and continue
shipped 1b8cdb44 + ca866695
4 — works hours later
shipped 1b8cdb44 (30-day continue lookback), and honoured by this preview
If option (b) stands, items 1, 2-third and 3(b) should be closed as won't-do and this issue with them. If it is overturned, they are a separate persistence project and deserve their own issue rather than staying here.
Final slice shipped in b2e5d3c7: a continued run inherits what the stopped Pilot had worked out, and what its owner knows now. This commit closes the issue.
Why the "owner decision (i)" stopped blocking
Items 1, 3(b) and the "what it still intended" third of item 2 were parked on one question: persist the stopped run's reasoning, or keep #523's option (b) — checkpoint the objective, not the transcript. Every earlier comment here is right that a Pilot cannot make that call, and this slice does not make it.
What changed is a fact underneath it. #822 (caa79b5b, the day after the last comment's analysis) made the Pilot record what it learned as it goes, and each entry is already a brain row on the session's coding_timeline. They were added so the Pilot could remember across its own steps. They are also, exactly, the part of a run's reasoning that survives it: what it worked out, where it had got to, and very often what it meant to do next ("Fix written, tests green. Next: rebase, commit, push."). Handing them to the successor persists nothing new — the checkpoint is still composed at the next start from rows that already exist. Option (b) stands, unchanged.
What changed
Item
What it does now
1 — reasoning context addressable
The stopped run's learned notes are read by its own session and run window (same keying as its acts, #809) and carried in ResumeCheckpoint.learned.
2 — "what it was mid-way through, what it still intended"
GET …/continue-preview returns briefing.learned — all of them, oldest first — from the same checkpoint the run is briefed from, and counts them in the headline. The console's "What carries over?" panel quotes them, attributed ("its own words, not checked").
3(b) — resume with prior context, optionally adding instructions
The resume note quotes the newest 12 notes to the new Pilot. And POST …/continue takes note — what the owner knows now that the run did not.
The notes are QUOTED, never adopted.coding-resume-note.ts still refuses to state "what remains" in the platform's voice. The note says: "They are ITS words, not something the platform checked: each was true when it was written, and the repository may have moved since… verify any one you are about to rely on." They sit apart from the landed acts, which are asserted.
The owner's note rides in the OBJECTIVE, not as a hint. A hint is one round's message and the workflow clears it. The objective is in every Pilot decision, on the run's row, in the timeline — and therefore in what a further continue inherits. The stopped run's words stay verbatim and first; the addition is labelled Added by the owner when continuing this run: so a correction can be told from the original. Past the column's 2000 characters it is refused with the room available, never cut, and refused before a budget is opened.
No notes → every pre-existing resume note and preview headline is byte-identical (pinned by tests).
A failed notes read costs the notes, not the acts.
Console: an optional textarea inside the review panel; the one-click Continue beside the label stays one click and sends an empty body.
MCP: continue_instance_run gains note. Server 0.1.47 → 0.1.48, surface lock appended; registrations unchanged at 213. surface-lock.ts crossed 800 lines and takes its first size pin (an append-only ledger whose git history is the check is not split).
The engine's side needed nothing: a new session already adopts the repo's previous CLI conversation inside the four-day window, and is seeded from the timeline when it cannot (coding-session-continuity.ts, coding-seed-brief.ts).
Verified
30 new tests (12,076 → 12,106): 5 unit on the note's wording, 6 against the real migrated schema end to end through pendingCodingResumeCheckpoint, 5 preview-lib, 1 preview-route, 10 continue-route / continueObjective (it.each rows counted), 3 console-lib.
Mutation-checked. Each turns a test red: dropping type = 'brain' from the read; dropping the run-window bound (a human-opened session then hands over an earlier run's notes); rounding the window's start up instead of down (a note from the run's first second is lost — created_at is whole seconds, the run's bounds are ms).
One mutation did not bite and I replaced the test rather than keep a claim nothing held: I had a "last second" edge test that passed with the rounding removed, because a note written before the run finishes can never be stamped after it. The start edge is the real one.
No live run. Same gap every slice here records. It needs a real run stopped at max_iterations that had written learned notes, then a Continue (ideally with a note) — coding_loop_trace / agent_trace on the successor shows the briefing that fired.
No e2e spec drives the review panel, before or after this change; the new textarea is covered by typecheck, the control-labels source guard and the pure continueBody tests, not by a browser.
The per-step transcript is still not persisted. "Full session/reasoning context" is answered by what the Pilot chose to record, plus the acts, the tree and the engine's own conversation — not by a replay. A run from before #822, or one whose Pilot recorded nothing, carries acts and the tree exactly as before. If a verbatim transcript is ever wanted, that is overturning option (b) and deserves its own issue, as the previous comment said. Item 3(a) "start clean" remains cancel-then-start by design.
Item
State
1
shipped here (Pilot's recorded notes; not a transcript)
2
8d245d57 + here
3(a)
works as cancel-then-start
3(b)
shipped here
3(c)
1b8cdb44 + ca866695
4
1b8cdb44 (30-day continue lookback)
Reopen if the transcript question should be decided the other way.
Activity
feat(coding): a run that hit its step limit tells its successor what …
serge-ivo commentedon Sep 13, 2026
Slice 1 shipped in
1ee023c6: a run that stopped atmax_iterationsnow tells its successor what already landed. Items 1–3 are NOT done; they need a design decision (below).What existed before
#523 (
7cae041a) already gives a new coding run a resume note:pendingCodingResumeNote(lib/coding-resume-note.ts), injected asgoal.resumeNoteatworkflows/coding-session.ts:733. It lists the acts the previous run on the same session observed to land, built fromagent_events. But the gate wasand a Pilot that uses up its step budget ends
max_steps→max_iterations(lib/coding-pause.ts:270). So the case this issue describes got no note.What changed
The gate is now "did the run reach a verdict on its objective".
RESUMABLE_STOP_REASONSlists the endings that did not:interruptedmax_iterationsengine_limitprovider_creditfailedescalatedcancelleddone/no_progress/budgetThe note's first line now states the true cause per stop reason. A step-limited run is not told it was "interrupted by the platform". The
interruptedwording is byte-identical and pinned by a test. The note is still immediate-predecessor-only, with a 6-hour lookback.Verified: 17 new tests in
coding-resume-note.test.ts, 20 → 37. (The commit message says 22 → 37; 20 is correct.) Restoring the old gate fails 3 of them. Full suite 11,409/11,409, both API typechecks, biome and the static guards are clean.Not verified:
max_iterationsfollowed by a new run on the same session.coding_loop_startre-uses the session a stopped run was on. The note is keyed onsession_id; a fresh session gets no note.failed, this slice does not cover it.What this does NOT cover
The Agent work as first-class Tickets — granular, with reasoning + per-ticket conversation #150 example itself. The note lists acts recorded in
agent_events(pushes, PRs, merges). Uncommitted edits in the working tree are not acts, so a partial fix written but not committed is still carried only by the files on disk, exactly as this issue describes.Items 1–3 need an owner decision, not a follow-up commit:
coding-resume-note.tsheader explicitly rejects persisting the Pilot's reasoning (actionLog/transcriptare function-local incoding-loop.ts) and refuses to state "what remains", because the platform cannot know it.Suggested split for
pags-ba:git status/ diff summary captured at stop.Leaving this issue open.
fix(coding): the resume note finds its predecessor by repo — an agent…
serge-ivo commentedon Sep 13, 2026
The resume note (#523, and slice 1 here) never reached an agent-started run's successor. Fixed in
f8ded1bb.The slice-1 comment listed "Session reuse not traced" as unverified. I traced it, and the note was keyed on something the successor usually doesn't have.
The bug, verified in code
lastUnfinishedRunForSessionqueriedagent_loop_runs … WHERE … session_id = ?3, with the new run's ownsessionId(workflows/coding-session.ts:733).delegate_goal,start_work, the Loop button andcoding_loop_start. With no active session,ensureSessioncreates one and returnsopened: true(lib/coding-session-open.ts:463-498), which becomessessionOpenedByRun: opened(lib/loop-drivers.ts:334).shouldEndSessionAfterRun({ openedByRun })leads toendSession(..., status), with status"error"forfailed/max_steps(workflows/coding-session.ts:954-971).getActiveSessionForRepoonly matchesstatus = 'active'(lib/coding-store.ts:396). The lookup then ran against a session with no history and returnednull, so no note.The note only ever reached runs on a session a human opened, since those outlive their runs. #523's own incident was a delegated run cut off after 15 pushes whose session ended
status: error: exactly the case the note couldn't reach.The fix
lastUnfinishedRunForRepojoinsagent_loop_runstocoding_sessionsand matches the repo of the caller's session, looked up within the caller's own user and instance.RESUMABLE_STOP_REASONS. The workflow call site is untouched.interruptedsentence moved by that one word, on purpose.Verified
coding-resume-note-repo.test.ts, 6 new tests against the real migrated schema (d1-sqlite), run end to end throughpendingCodingResumeNote:max_iterations, and S1 is closed aserror. Run B on new session S2 of R gets the note, and it names the push.donerun on any session of R in between ends the note's job.SELECT *in place ofr.*fails 2: the join's ownstatus/started_atwould overwrite the run's.Not verified
endstep runs when the Workflow itself is killed, not caught. If it doesn't, the idle reaper still closes the session, and the fix covers that case the same way.Noted, not changed
actsInWindowfilters by instance and time window (lib/instance-work.ts:400-404), not by repo. If another repo's run on the same instance overlapped run A's window, its acts would appear in the note. That predates this fix.serge-ivo commentedon Sep 13, 2026
Run 6ae2f583 continuation — bookkeeping
Two slices from the prior run are now on
main. CI and Deploy API Worker both passed onf8ded1bb.1ee023c6max_iterations,engine_limitandprovider_credit, not justinterrupted. It also states the real stop reason.f8ded1bbWaiting on an owner decision
coding-resume-note.tsimplements today. Items 1 and 2 of this issue (keep the context available, review what the run intended) depend on this choice.Buildable without an owner decision
codingResumeNotereturnsnullwhen no action landed (lib/coding-resume-note.ts:113,if (!landed.length && !unobserved.length) return null;). A run that left only uncommitted edits, which is the Agent work as first-class Tickets — granular, with reasoning + per-ticket conversation #150 shape, gets no note today, so that check has to change too.REPOSITORY STATEinstruction (workflows/coding-session.ts:700-704,readRepoWorkingState→dirtyClause). That instruction says "You did not create this state". A note that attributes the same files to the previous run needs to agree with it.cancelledis not inRESUMABLE_STOP_REASONS, so that new run gets no note.Not verified / not filed
failed, it is outsideRESUMABLE_STOP_REASONSand (iii) would not reach it either.actsInWindowreads actions across the whole instance (lib/instance-work.ts:400-404), not by repo. Noted in the comment above; not filed as its own issue.No further code was written in this continuation run; its objective was verification and bookkeeping only.
fix(coding): scope actsInWindow to the run's session, not the whole i…
serge-ivo commentedon Sep 13, 2026
Run 199ec11f continuation — bookkeeping
One gap noted in the previous bookkeeping comment has since been resolved.
Closed:
actsInWindowinstance-wide scopingThe previous comment noted under "Not verified / not filed" that
actsInWindowreads acts across the whole Coder instance rather than scoping to the run's session. That was filed as #809 and fixed incda8ef91(merged 2026-09-13). CI passed. The lookup is now scoped to the run's session.Still open
Waiting on an owner decision:
Buildable without an owner decision:
return nullatlib/coding-resume-note.ts:113must change, and the note must agree with theREPOSITORY STATEinstruction atworkflows/coding-session.ts:700-704.docs(coding): CodingGoal.resumeNote names both of its writers, not th…
feat(coding): a run cut off with its work still uncommitted briefs it…
serge-ivo commentedon Sep 19, 2026
Slice (iii) shipped in
a3a9ded5: a run cut off with its work still uncommitted now briefs its successor.The gap
The resume note was composed only from
agent_events, and an edit that was never committed is not an event.codingResumeNotereturnednullwhen no act landed (lib/coding-resume-note.ts:113). So this issue's own incident — cut off at step 4 of 20, a good partial fix on disk, nothing pushed — told its successor nothing.What changed
codingResumeNote(acts, endedBy, uncommittedFiles = 0). With an unfinished predecessor and a dirty tree, a note is produced even when no act landed.repo-state-startread the workflow already makes (RepoWorkingState.changedFiles). Nothing new is captured at stop, and nothing new is stored.REPOSITORY STATEinstruction beside it ("You did not create this state… do NOT discard"): read the diff, build on it, do NOT discard it.coding-session.tsstays inside its file-size pin.Unchanged
interruptedopening).REPOSITORY STATE's job.RESUMABLE_STOP_REASONS.Verified
coding-resume-note.test.ts(one of them a wiring assertion on the workflow source), 2 incoding-resume-note-repo.test.tsagainst the real migrated schema.pnpm typecheck; CI's biome command; all 13check-*.mjs;docs:drift.Not verified
agent_traceon the successor shows whether the note fired and whether it built on the diff.failed, it is outsideRESUMABLE_STOP_REASONSand this slice does not reach it either.Still open here
Leaving this issue open.
serge-ivo commentedon Sep 19, 2026
Slice (ii) shipped in
1b8cdb44: a stopped run can be continued — same objective, a new ceiling, briefed on what already landed.Item 3(c), the half that needed no owner decision. Items 1, 2 and 3(b) still wait on (i).
What existed before
Nothing.
maxIterationsis fixed once, atcreateLoopRun(lib/agent-loop-store.ts:183), and the only way to act on a stopped run wasPOST /:id/loop/:runId/cancel— which does not apply to a run that has already stopped. Starting again meant retyping the objective intoPOST /:id/loopand hoping the resume note found the right predecessor.What changed
POST /v1/instances/:id/loop/:runId/continue(routes/loop-continue-routes.ts, registered fromroutes/tools.ts:1153).It is a new run, not a resumed invocation. A finished
agent_loop_runsrow has no live Workflow to grant steps to, and #523's option (b) means there is no transcript to reanimate. The continuity is the resume note.The route is deliberately thin —
driver.start()already picks the repo, opens or reuses the session, runs admission and takes the single-flight claim. What it decides is only:isResumableStopReason— the same four as the note. Everything else is a 409 naming the next step (done→ new objective;failed→ read the outcome first;cancelled→ that was your clean start).finishedAt === null, not onstatus— a cooperative cancel is still running until the top of its next iteration, and admitting it there is the #208 collision.sessionId→coding_sessions.repo_id, so a multi-repo Coder continues the right checkout rather thanrepos[0](#374). A chat run has no session and passes none.maxIterationsin the body grants more. Clamped by the same per-account ceiling asPOST /loop(#477) — a continue is not a way around it.openBudget. #184's rule that every autonomous entry point admits separately; inheriting an unspent pool would let one admission fund an unbounded chain of continues.The lookback — item 4, and the only reason this differs from a restart
RESUME_NOTE_LOOKBACK_MSis six hours (lib/agent-loop-store.ts:257). Item 4 asks that this work "even hours later — the point is a human can check back on a stalled run on their own schedule". Under the default floor, a Continue pressed the next morning would have started a run briefed on nothing at all — i.e. the button would have been a restart with extra steps.So a continue passes
CONTINUE_RESUME_LOOKBACK_MS(30 days) down the chain: route →LoopStartInput.resumeLookbackMs→CODING_SESSIONparams →pendingCodingResumeNote({lookbackMs})→lastUnfinishedRunForRepo.It moves the floor only. The predecessor is still the most recent finished run on the repo, judged afterwards, so a verdict in between still ends the note's job. That is also why the note is not pinned to the continued run's id: pinning would skip that check and brief the new run on a checkpoint another run has already consumed — this issue's complaint pointed the other way. Pinned by a test.
Every ordinary start is byte-identical: no
lookbackMs, six-hour floor, same note.Surfaces
LoopRunsSection.tsx, offered only after an ending with no verdict.lib/loopContinue.tsis pure and tested, with a test that reads the worker's ownRESUMABLE_STOP_REASONSso the two copies cannot drift silently.continue_instance_run. Not part of MCP parity: 71 console capabilities have no MCP path — the first run's inventory, by group #613's sweep: the route is new here, and shipping the console button without a tool would have added a gap to the ratchet MCP parity: 71 console capabilities have no MCP path — the first run's inventory, by group #613 is driving to zero. Parity holds at 29 recorded gaps, unchanged. Server0.1.38→0.1.39; 196 → 197 registrations.Verified
loop-continue-routes.test.ts), 13 console-lib, 3 real-schema lookback tests incoding-resume-note-repo.test.ts(driven end to end on an in-memory SQLite built from every migration), 1 driver-threading, 1 workflow-wiring source assertion.pendingCodingResumeNote), the cross-instance guard, the unfinished-run guard, or the ceiling default each turns tests red.pnpm -r typecheck; biome;docs:drift; all 13check-*.mjs.1b8cdb44, along with Deploy API / MCP / Host Worker and Publish MCP Registry.Not verified
agent_traceon the successor shows whether the note fired and whether the new run built on it. Same gap the three earlier slices record.failed, it is outsideRESUMABLE_STOP_REASONSand the Continue button would not be offered on it.Still open here
cancelledis deliberately outsideRESUMABLE_STOP_REASONS, so that path correctly gets no note.Leaving this issue open.
feat(loop): how long a run may be belongs to the agent, not to whoeve…
serge-ivo commentedon Sep 19, 2026
Run cda38e26 continuation – status update (2026-09-19)
What landed in this continuation run:
1b8cdb44) – already merged to main before this run started; the continue route (item 3c) is confirmed in main.ca866695(Per-instance minimum and maximum coding_loop iteration caps (owner-configurable floor) #820) – continue ceiling now clamps through the instance floor, so a run that died at max_iterations no longer just re-stalls at the same ceiling when continued. These two commits are load-bearing together.Current state of main: clean and level with
origin/main. Full suite 11,824/11,824 andpnpm -r typecheckpass;docs:drift,size:check,mcp:parityandopenapi:coverageare green.pnpm lintdoes NOT pass — one error,e2e/console.spec.ts:4360 lint/complexity/useArrowFunction, which is pre-existing and in a file neither commit touches. No open PRs. Nine remote branches exist, including the merged-but-undeletedloop-continue-806— not main alone.What remains on #806:
All buildable engineering slices are done. The only open threads are:
Owner decision (i): persist the stopped run's reasoning transcript, or keep option (b) from A 2-hour run that closed ten issues and pushed 15 times is reported as "outcome: failed" with a Wrangler docs link — the Cloudflare subrequest ceiling is relayed raw as the objective failing #523 (checkpoint the objective, not the transcript). Items 1, 2, and 3(b) are all blocked on this choice. If option (b) stands, those items should be closed as won't-do. If it's overturned, they become a new persistence project that should be tracked in a separate issue.
Item 3(a) "start clean": currently handled by cancel-then-start;
cancelledis outsideRESUMABLE_STOP_REASONSby design, so the path already works correctly. Could be filed as a separate issue if explicit UX for it is wanted.Unverified in production: no live stopped run has been observed going through the continue route end-to-end with
agent_traceconfirming the note fired. All coverage is via tests.Agent work as first-class Tickets — granular, with reasoning + per-ticket conversation #150's stop reason: if the original relay-timeout incident recorded as
failed, it sits outsideRESUMABLE_STOP_REASONSand none of the five commits reach it. Worth checking if that incident recurs.Leaving #806 open pending decision on (i).
feat(coding): the Pilot keeps what it learned, and is told it is the …
feat(loop): see what a continue would carry forward, before spending …
serge-ivo commentedon Sep 20, 2026
Slice item-2 shipped in
8d245d57: you can now see what a Continue would carry forward, before spending a budget on it.Item 2, the two-thirds option (b) can answer. Items 1, 3(b) and the "what it still intended" third of item 2 still wait on (i).
The gap
The platform already composes this.
pendingCodingResumeNotereads the acts the stopped run landed and the state of the checkout and turns them into the briefing the next run is handed. The problem is when: that happens inside the successor's own start, into the successor's prompt and timeline — so the owner reads it a minute after committing a budget, never before. The console offered a Continue button and, beside it, the stopped run's ending label. Neither said what continuing would inherit.The fact that changes the decision
lastUnfinishedRunForRepotakes the most recent finished run on the repo and only then asks what ended it. That rule is right — it stops one checkpoint being handed out twice — but it has a consequence invisible from outside: a Continue on run A carries A's work forward only while A is still the newest finished run on its repo. Let a run B reach a verdict in between, and a Continue on A is briefed on nothing. It is a restart that opens a fresh budget, and it looked identical to one that resumes.So the preview does not pretty-print a note. It answers which of three situations the owner is in, by running the same computation the run will run:
briefing.kindthis-runother-runnoneWhat changed
pendingCodingResumeCheckpoint(lib/coding-resume-note.ts) returns the checkpoint with the facts it was composed from, andpendingCodingResumeNoteis now a projection of it (?.note ?? null). One computation, so the page cannot describe a briefing the run does not get — two estimates of "what carries forward" is how a surface comes to promise work the run then re-does, which is this ticket's complaint from the other side. The three-way act split both readers need is one exportedsplitActs. A checkpoint withnote: nullis a real and distinct state the projection cannot express: there is an unfinished predecessor and it left nothing worth saying.GET /v1/instances/:id/loop/:runId/continue-preview, beside the POST. A GET and not adryRunflag: the POST's preview question is "would this be refused and how big would it be"; this one is "what would the new run know", which needs two reads the POST does not make. 200 even when the run cannot be continued — the refusal is the answer, and an owner reading why the button is absent is who this is for. The refusal and the ceiling come from helpers the POST now also calls, so the two cannot disagree.uncommittedFilesisnullwhen the runner did not answer, never0. That is the expected case here — item 4 is an owner checking back the next morning, when the machine is off — and reporting a clean tree we never saw would be the exact over-claim this surface exists to stop. When it is unreadable the previewed note omits a clause the real briefing may carry, andcaveatsays so.preview_instance_run_continue(read, no dry_run). Shipped with the route, because a console-reachable route with no tool opens a newKNOWN_GAPSentry against a list MCP parity: 71 console capabilities have no MCP path — the first run's inventory, by group #613 is driving to zero. Parity holds at 29 recorded gaps, unchanged. Server0.1.44→0.1.45; 204 → 205 registrations, always-on 180 → 181.readrather thanruntimethough it sits besidecontinue_instance_run: it starts nothing and opens no budget, and classing a review surface with the action it describes would make reading before acting cost the scope of acting.git statuson someone's laptop on a timer to answer a question nobody asked. Every line comes from the server, headline included (previewLines), so the panel and the briefing cannot drift.Verified
loop-continue-preview.test.ts), 17 route (loop-continue-preview-routes.test.ts), 6 real-schema driven end to end through the migrated SQLite (coding-resume-note-repo.test.ts, 12 → 18), 6 console-lib (loopContinue.test.ts, 13 → 21). The 24 existing POST tests are untouched and pass, which is the evidence the shared-helper refactor moved no behaviour.0instead of the tree read; fixingkindtothis-run; collapsing an unreadable tree to0; having the console compose its own headline; making the note a second computation rather than a projection.pnpm -r typecheckplus the three test-config typechecks (Three tsconfigs exclude test files and CI typechecks none of them, so a type-level guard in any worker test is inert #599); biome;docs:drift; all 13check-*.mjs.Not verified
agent_traceon a subsequent continue would show whether the note that fired is the one the preview showed. All coverage is tests. Same gap the four earlier slices each record.failed, it is outsideRESUMABLE_STOP_REASONS, the Continue button is not offered on it, and this preview would report it as refused.other-runhas not been observed in the wild. It is constructed in tests; nobody has seen the ordering that produces it on a real repo.Still open here — and why this commit carries no closing keyword
(i) is an owner decision and a Pilot cannot make it. Items 1, 3(b) and the "what it still intended" third of item 2 all depend on whether the stopped run's reasoning is persisted, or whether #523's option (b) — checkpoint the objective, not the transcript — stands. A plan is not a fact the platform holds, and
coding-resume-note.tsdeliberately refuses to state a remainder it would have to invent; this slice does not work around that.Every buildable engineering slice is now done:
cancelledis outsideRESUMABLE_STOP_REASONSby design1b8cdb44+ca8666951b8cdb44(30-day continue lookback), and honoured by this previewIf option (b) stands, items 1, 2-third and 3(b) should be closed as won't-do and this issue with them. If it is overturned, they are a separate persistence project and deserve their own issue rather than staying here.
Leaving this issue open.
serge-ivo commentedon Sep 20, 2026
Final slice shipped in
b2e5d3c7: a continued run inherits what the stopped Pilot had worked out, and what its owner knows now. This commit closes the issue.Why the "owner decision (i)" stopped blocking
Items 1, 3(b) and the "what it still intended" third of item 2 were parked on one question: persist the stopped run's reasoning, or keep #523's option (b) — checkpoint the objective, not the transcript. Every earlier comment here is right that a Pilot cannot make that call, and this slice does not make it.
What changed is a fact underneath it. #822 (
caa79b5b, the day after the last comment's analysis) made the Pilot record what itlearnedas it goes, and each entry is already abrainrow on the session'scoding_timeline. They were added so the Pilot could remember across its own steps. They are also, exactly, the part of a run's reasoning that survives it: what it worked out, where it had got to, and very often what it meant to do next ("Fix written, tests green. Next: rebase, commit, push."). Handing them to the successor persists nothing new — the checkpoint is still composed at the next start from rows that already exist. Option (b) stands, unchanged.What changed
learnednotes are read by its own session and run window (same keying as its acts, #809) and carried inResumeCheckpoint.learned.GET …/continue-previewreturnsbriefing.learned— all of them, oldest first — from the same checkpoint the run is briefed from, and counts them in the headline. The console's "What carries over?" panel quotes them, attributed ("its own words, not checked").POST …/continuetakesnote— what the owner knows now that the run did not.The notes are QUOTED, never adopted.
coding-resume-note.tsstill refuses to state "what remains" in the platform's voice. The note says: "They are ITS words, not something the platform checked: each was true when it was written, and the repository may have moved since… verify any one you are about to rely on." They sit apart from the landed acts, which are asserted.The owner's
noterides in the OBJECTIVE, not as a hint. A hint is one round's message and the workflow clears it. The objective is in every Pilot decision, on the run's row, in the timeline — and therefore in what a further continue inherits. The stopped run's words stay verbatim and first; the addition is labelledAdded by the owner when continuing this run:so a correction can be told from the original. Past the column's 2000 characters it is refused with the room available, never cut, and refused before a budget is opened.Also:
continue_instance_rungainsnote. Server0.1.47→0.1.48, surface lock appended; registrations unchanged at 213.surface-lock.tscrossed 800 lines and takes its first size pin (an append-only ledger whose git history is the check is not split).coding-session-continuity.ts,coding-seed-brief.ts).Verified
pendingCodingResumeCheckpoint, 5 preview-lib, 1 preview-route, 10 continue-route /continueObjective(it.each rows counted), 3 console-lib.type = 'brain'from the read; dropping the run-window bound (a human-opened session then hands over an earlier run's notes); rounding the window's start up instead of down (a note from the run's first second is lost —created_atis whole seconds, the run's bounds are ms).pnpm -r typecheck+ the three test-config typechecks (Three tsconfigs exclude test files and CI typechecks none of them, so a type-level guard in any worker test is inert #599); CI's biome command;docs:drift;openapi:coverage; all 13check-*.mjs.Not verified
max_iterationsthat had writtenlearnednotes, then a Continue (ideally with anote) —coding_loop_trace/agent_traceon the successor shows the briefing that fired.continueBodytests, not by a browser.failedit is outsideRESUMABLE_STOP_REASONSand no slice here reaches it.What this deliberately is not
The per-step transcript is still not persisted. "Full session/reasoning context" is answered by what the Pilot chose to record, plus the acts, the tree and the engine's own conversation — not by a replay. A run from before #822, or one whose Pilot recorded nothing, carries acts and the tree exactly as before. If a verbatim transcript is ever wanted, that is overturning option (b) and deserves its own issue, as the previous comment said. Item 3(a) "start clean" remains cancel-then-start by design.
8d245d57+ here1b8cdb44+ca8666951b8cdb44(30-day continue lookback)Reopen if the transcript question should be decided the other way.
test(#915): the Pilot's invariants are asserted by RUNNING the workfl…