Repository navigation
Description
Reported
"I can't stop the loop for Heartfull."
The server accepted the stop. Nothing in the UI ever says so.
Live state of the run right now (f8ddc272, run 252061dc):
status: running
cancelRequested: true ← the stop WAS recorded
iteration: 32 / 50
startedAt: 2026-08-07T07:58:02Z
lastProgressAt: 2026-08-07T09:06:31Z
So requestCancel worked: agent_loop_runs.cancel_requested = 1. The API returns it — it is right
there in check_instance_loop's payload as cancelRequested.
No UI reads it. git grep cancelRequested across store/console/src and
agents/coder/web/src returns nothing.
The consequence, in LoopRunsSection.tsx:
- pressing Stop sets a transient local message —
"Stopping — the current step will finish first."
(:87), which is honest but is component state and vanishes on the next poll, re-render or
navigation; - the row itself renders from
r.status === "running"alone (:108,:125), so it keeps showing
· running and keeps offering a live Stop button.
So: press Stop → a message flashes → the row still says running → press Stop again → same. The user
correctly concludes it does not work. The flag is set, re-set, and re-set; the UI is simply not
looking at it.
Why the wait is long enough to matter
Cancellation is cooperative by design and the reasoning is sound (agent-loop-store.ts:160-166):
the flag is read at the top of each iteration (workflows/agent-loop.ts:77) so the in-flight
step settles its spend, because killing mid-step strands a budget reservation against the shared
tree pool.
But the in-flight step is act-${iteration} — handing an instruction to a Coder that drives Claude
Code through a real task. That is minutes, not seconds. This run has been 6m45s past its last
recorded progress and is presumably still inside iteration 32.
A cooperative cancel with a multi-minute settling window is fine. A cooperative cancel with a
multi-minute window and no persistent acknowledgement is indistinguishable from a broken button.
Fix
- Render the pending state. When
cancelRequested && status === "running", show Stopping…
instead of running, and disable the Stop button. The data is already on the wire; this is a
read, not a new endpoint. - Say what it is waiting for — "finishing the current step" — so a multi-minute wait reads as
the documented behaviour rather than a hang.work-report.ts:87already composes this sentence
for the work report ("a cancel has been requested"); the console just never got it. - Consider a second press meaning something. Once a cancel is pending, a further Stop could
offer a hard abort. Not obviously right — it reintroduces the stranded-reservation problem the
cooperative design exists to avoid — so this needs a decision, not just an implementation.
Also worth noting from the same data
Three of this instance's six runs died with WorkflowInternalError: Attempt failed due to internal workflows error, and two more with the $50.00 daily limit (#343). The run list is mostly failures.
Not this issue, but the same instance is the evidence for several open ones.
Activity
fix(console): a stopping loop run now says it is stopping (#376)
fix(coder): the Coding tab's Loop stops being the loop, and starts on…
fix(console): 31 colour utilities named tokens that do not exist (#367)
fix(voice): a phrase you bound is yours, the agent cannot command its…
fix(console): the Terminal tab reads whether the runner is there, ins…
serge-ivo commentedon Aug 8, 2026
Audit (picked this up for implementation and found it already delivered): shipped in
e9e6bd4, which is onmainand deployed. No further code was needed.Both implementable parts are present, and the third was correctly left as a decision:
store/console/src/lib/loopStopState.ts—loopStopControl({status:"running", cancelRequested:true})returnsphase:"stopping",statusLabel:"Stopping…",canStop:false.LoopRunsSection.tsx:124routes the row through it (:132disables the button,:147renders the label).STOPPING_HINT= "Stopping — the current step has to finish first. This can take a few minutes.", rendered persistently at:150and as the buttontitleat:134— component state that vanished on the next poll is exactly what the ticket was about, and this is derived from the server field on every render instead.Two edge cases in
loopStopState.test.tsworth noting because they are the ones that would have bitten:cancel_requestedis never cleared, so every cancelled run carries it forever — reading the flag without the status would have pinned "Stopping…" on ended rows permanently.undefined/nullread as no cancel pending, so an older server omitting the field cannot freeze the Stop button on every run.There is also a source-scanning guard asserting that
LoopRunsSection.tsxandInstanceDetail.tsxactually route through the resolver — the right shape here, since the defect was never a wrong resolver but that no component asked one, and a unit test of a pure function cannot catch a function nobody calls.Closing as delivered. Not verified live in WebKit at 320/390px — this is a text-and-disabled-state change inside an existing row rather than new layout, so the risk is low, but I did not check it and am saying so.
The tail observation in the ticket (three runs lost to
WorkflowInternalError, two to the$50.00daily limit) is untouched by this and belongs to #343.fix(#931): a "repo busy" refusal links to the run holding the repo, a…