Skip to content

fix(harness): record a stop before interrupting, derive recovery in the drain - #150

Merged
sanchitrk merged 5 commits into
canaryfrom
fix/durable-stop-recovery
Nov 10, 2026
Merged

sanchitrk merged 5 commits into
canaryfrom
fix/durable-stop-recovery

Conversation

@sanchitrk

@sanchitrk sanchitrk commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

What changes

  • A stop survives a crash. A user stop on an open run publishes session.execution.stopping before anything is interrupted. The startup sweep ends a marked run as execution.interrupted { user } instead of crash, so a host killed while a stop winds down no longer resumes the stopped run. A shutdown that reaches a run already asked to stop ends it as the user's stop too.
  • Recovery is decided by the drain. A run the host cut off is resumed, its restart counted, or ended at the restart limit by whichever wake reaches the thread first. A prompt that arrives before startup recovery no longer leaves the restart uncounted. resumeUnfinished only wakes; recover / abandon and three in-memory sets are gone.
  • One place publishes a run's end. The loop returns how the run ended and the executor publishes the terminal, joined to the run's last step. A run whose execution.started cannot be published does not run.
  • parked on a thread summary. Idle, nothing owed a run, and a prompt pending, derived from the head and tail positions on every read. A change to what is pending carries the thread's revision forward so revision-based sync picks it up.
  • Apps. The session view shows a stop CodeWork accepted as "Stopping" from server state, the app server mirrors parked, and the queue reads "Parked · Runs with your next message".
  • Tests brought up to date. Two live runner tests, and two real-machine recovery expectations that predated the lane-order change.

Wire and schema

  • New durable event session.execution.stopping. The four run terminals keep their types.
  • New required boolean parked on the thread summary.
  • App database: new thread.parked column. No harness schema change.

Verification

  • vp check and vp run typecheck pass.
  • Non-live harness and CLI suites pass. New E2E recovery.stop-killed failed before the change.
  • Real-machine recovery suite on OrbStack: 5 of 5.
  • Real Electron run with a real model: stop, parked header, and each queue row action.

Known

  • apps/server sync.test.ts "a restart … resumes from the cursor" fails on canary before this change too.
  • One narrow stop race remains: a stop landing in the instant a drain opens its run, followed by a crash before it settles.
  • The "Stopping" label was not seen on screen: the stop settled faster than the UI's 200 ms show delay.

🤖 Generated with Claude Code


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

sanchitrk and others added 5 commits November 10, 2026 16:31
…he drain

A user stop on an open run now publishes session.execution.stopping before
anything is interrupted, so a host killed while the stop winds down ends the
run as stopped instead of resuming it as a crash. A shutdown that reaches a
run already asked to stop ends it as the user's stop too.

Recovery of a run the host cut off is decided by the drain from the thread
row, whichever wake reaches it first: a prompt that gets there before startup
recovery no longer leaves the restart uncounted. resumeUnfinished only wakes.

Every run terminal is published by the executor, joined to the run's last
step, and a run whose execution.started cannot be published does not run.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The thinking test ignores durable llm.progress events, and the retry test
expects the mid-stream attempt to keep what streamed. The retry proxy cuts at
the first delta that carries text, not at a role-only delta.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A thread whose open run carries the user's stop reads as stopping in every
window and after a reload. The real-machine recovery tests record the new
event and expect a parked follow-up and the next steer to go in together.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A thread summary carries `parked`: idle, nothing owed a run, and a prompt
pending. It is derived from the head and tail positions on every read, so
clients get the fact without the positions. A change to what is pending
carries the thread's revision forward, so a client that follows by revision
reads the thread again.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The app server mirrors a thread's `parked`, and the session queue reads
"Parked · Runs with your next message" while it holds.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@sanchitrk
sanchitrk merged commit a8199d0 into canary Nov 10, 2026
1 check passed
@sanchitrk
sanchitrk deleted the fix/durable-stop-recovery branch November 10, 2026 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant