Repository navigation
feat: pause and resume a run - #104
Conversation
|
Caution Review failedThe pull request is closed. Finishing Touches
Comment |
There was a problem hiding this comment.
Comment: One item must land before merge: documentation/hosts.md says a held step may start without a poll, and Execute hosts don't get that.
This adds Pause and Resume across the engine, local, postgres and sqlite, as PRD #94 describes. I traced the paused paths (pause, timeout re-arm, a decided outcome, cancel, abort), the claim filters in both backends, and the local hold. I found no wrong behavior. Settling a timer deletes its job in the same transaction as the delivery, so Resume's re-arm can't collide with an old row.
Should fix
S1. Held steps on Execute hosts wait for the queue's next retry (documentation/hosts.md:141)
Line 141 says a held step can start "without waiting for a poll" once the run leaves paused. Line 475 says an Execute host returns ErrNotClaimable for a held job until the queue retries it, and the scheduled RunOnce picks it up later. Resume wakes pull workers through JobsWritten, but Result.Jobs() returns only re-armed timers, so no task is sent again. After a Resume, an operator can wait as long as the queue's retry backoff, and the doc gives no bound. Fix: in hosts.md, say that on Execute hosts a held step starts at the queue's next retry, and that the queue's max backoff sets the delay. Or have Resume return the held step jobs for push hosts. Needed before merge, because hosts build against this doc.
Nits
- N1.
planning/specs/009-pause-resume.md:211namesd.setHeld, but the code's helper isd.holdinlocal/local.go. TheStatusRunninggrep rule at line 94 is also out of date. The code wins, so a one-line note is enough.
Prompt for your AI tool: fix S1
Fix this review finding in deepnoodle-ai/workflow, PR #104.
1. S1, documentation/hosts.md, the Pause section near line 141 and the
Execute section near line 475: the text says a held step may start
"without waiting for a poll", but on Execute hosts a held job's task gets
ErrNotClaimable until the queue retries it, and the scheduled RunOnce
picks it up later. Wanted: say that on Execute hosts a held step starts at
the queue's next retry, and that the queue's max backoff sets the delay.
Keep the pull-host wording, which is correct: Resume wakes the workers
through JobsWritten. Test: none; this is a doc change. Check that the two
sections agree.
Run the existing tests after the changes.
Follows S1 and N1 on #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host that pushes one request per job can now send the held steps again at Resume instead of waiting for its queue's next retry. Review S1 on #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2490605 to
48bafc0
Compare
The branch is rebased on the PRD branch, so the earlier commit hashes in the description changed. The description is updated. |
There was a problem hiding this comment.
Approve: Updated for 48bafc0. S1 is fixed, and the new commit has no Blocking or Should-fix items.
The branch was force-pushed, so the old head 249060… is gone. Its commit message now sits at f2e97f5, and I took that as the prior head. The new commit makes Resume return the held step jobs, so a host that pushes one request per job sends them again at Resume.
Fixed: S1
Nits
- N1. The spec still names
d.setHeldat line 223, but the code usesd.hold. The note at line 220 says so, so this is only a stale name. (planning/specs/009-pause-resume.md:223) - N2. The
llms.txtline reads "Resume's lists the timers…" and is missingResult.Jobs(). (llms.txt:129) - N3.
Outstandingreturns every running step unit, including steps a worker is still running, not just held ones. Pushing a running step is harmless because the second task finds it leased. The docs still call these "held step jobs." (documentation/hosts.md:479)
Addresses N1 and N3 on #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host that pushes one request per job can now send the held steps again at Resume instead of waiting for its queue's next retry. Review S1 on #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses N2 and N3 on #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
48bafc0 to
20ef13b
Compare
The branch was rebased on the PRD branch again, so the earlier commit hashes changed. The description lists the new ones; the S1 commit is now 49079d6. |
There was a problem hiding this comment.
Approve: Updated for 20ef13b. Resume now lists every running step, and a push of one that a worker still holds gets ErrNotClaimable, so it does not run twice.
Fixed: N1, N2, N3
Should fix
S2. The running-step case has no test (internal/hosttest/pause.go:177)
ResumeListsTheHeldSteps covers only a step that was held. Nothing tests a step a worker claimed before the Pause, though the docs now rely on it: Resume lists it, and a push of it is refused while its lease is live. The code does that (postgres/backend.go claimKeySQL checks the lease, and internal/hostcore/once.go:262 returns ErrNotClaimable), so this is a test gap, not a bug. Add a case that pauses while a handler runs, then checks that Resume lists the step and Execute returns ErrNotClaimable until the handler settles. This can land after merge.
Nits
- N4. The PR body still says Resume lists "the run's held step jobs". The code now lists every running step, so update the body. (PR description)
- N5.
documentation/hosts.md:482calls a push of a running step "harmless". It is harmless, but the push gets a 5xx, so the queue retries it. Say that. (documentation/hosts.md:482)
Prompt for your AI tool: fix S2
Fix this review finding in deepnoodle-ai/workflow, PR #104.
1. S2, internal/hosttest/pause.go, func testResumeListsTheHeldSteps:
The test covers only a step that was held during the pause. Wanted: add
a case where a worker claims a step before Pause and the handler is still
running. Pause, then Resume. Test: Resume's Result.Jobs() lists that
step; Execute on its JobRef returns ErrNotClaimable while the handler
runs; after the handler settles, the run completes.
There was a problem hiding this comment.
Approve: Updated for 5b2233e. The one new commit changes documentation/hosts.md only, and the new Cancel and decided-outcome text matches the code.
Fixed: N4 (the PR body now says Resume lists "held, or claimed before the Pause").
I checked the new text against abort.go, apply.go (failRun and cancelAll for a decided failure, EventCancel for a cancel), and internal/hostcore/store.go. CancelStep deletes the held job, so a held step never starts after a Cancel or a failure. Only on_failure and finally steps start while the run is paused, because the run status moves to failing or finishing.
Still open
- S2.
ResumeListsTheHeldStepscovers only held jobs, not a step claimed before the Pause. Fine as a follow-up. - N5.
hosts.mdcalls a push of a running step "harmless." The task getsErrNotClaimable, which is a 5xx for a task queue, and it retries. The wording should say so.
Prompt for your AI tool: fix S2
Fix this review finding in deepnoodle-ai/workflow, PR #104.
1. S2, internal/hosttest/pause.go, the ResumeListsTheHeldSteps suite test:
it covers only a step held by the Pause. Wanted: add a case where a step
is claimed before the Pause and its handler is still running. Resume's
Result.Jobs() must list that step's unit, and a push of its job must
return ErrNotClaimable while the job is leased. After the step settles,
the job must be gone. Test: extend the suite test so both postgres and
sqlite run it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A step timeout that fires while the run is paused no longer fails the attempt. Resume gives it a new, full timeout. Addresses B1 on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…does Addresses S1, S2, N1 and N2 on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses S3 on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows S1 and N1 on #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses N1 and N3 on #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses S4 on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a8c70f4 to
46d72fd
Compare
Add EventPause and EventResume next to EventCancel, and the paused run status. Apply advances a paused run as a running one; the host holds its dispatched steps. postgres and sqlite claim no step job of a paused run, and local waits before it calls the handler. A step timeout that fires during a pause is re-armed at Resume. Store.Pause and Store.Resume (with the same Tx variants as Cancel) and local Run.Pause and Run.Resume deliver the events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save could be slow, and a retry whose backoff ended meanwhile called its handler after Pause had returned. Review finding 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review finding 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review finding 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review finding 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 10ms backoff could end before RunOnce settled the first attempt, so it followed the retry and the run finished before the Pause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host that pushes one request per job can now send the held steps again at Resume instead of waiting for its queue's next retry. Review S1 on #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses N2 and N3 on #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Matches the spec's host rule; follows a CodeRabbit comment on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5b2233e to
a92f5f3
Compare
There was a problem hiding this comment.
Reviewed at a92f5f3. The branch was rewritten since the last review (5b2233e is no longer in the clone), so I read the full PR diff against base 46d72fd, not just the new commits.
Blocking: none.
What I checked
apply.go: a timeout that fires during a pause is held as zero and re-armed inrearmTimeoutsat Resume. A duplicate held timer event is dropped early.dropSettledTimeoutsclears entries for attempts that settled.outstanding.goandinternal/hostcore/store.go: on a Resume,Result.Jobs()lists the re-armed timers, then every running step.Result.Commandsis limited to theDispatchSteps, and they are added afterpersist, so nothing is written to the outbox twice.local/local.go: Resume emits noDispatchStepfor running steps, so held steps wait on theheldchannel and a step is not called twice.documentation/hosts.md: the held-step, cancel and outcome rules matchabort.goandapply.go. A claim made before the Pause runs on, and a Cancel or a decided outcome ends the pause withCancelStepfor held attempts.
Nonblocking, still open
- N5:
hosts.mdsays a push of a running step is "harmless". Its task getsErrNotClaimableand answers 5xx, so the queue retries it. Nothing runs twice, but "harmless" overstates it. Suggest "a 5xx and a retry, with no second run". - S2:
ResumeListsTheHeldStepscovers only a held step. No test covers a step claimed before the Pause that Resume lists. Worth a follow-up test.
No tests, builds or linters were run. This is a review of the code and docs as written.
* docs(prd): pause and resume a run by hand Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): hold paused runs in the host, and add the tech spec Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): approve pause and resume Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): link the PRD PR Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): a pause stops the timeout clock A step timeout that fires while the run is paused no longer fails the attempt. Resume gives it a new, full timeout. Addresses B1 on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): re-arm only fired timeouts, and name the rule for what it does Addresses S1, S2, N1 and N2 on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(spec): drop a held timeout when its attempt settles Addresses S3 on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): link the implementation PR Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): Resume lists the held step jobs; note the local hold as built Follows S1 and N1 on #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(spec): a held step's job stays in the outbox until Resume Addresses B2 on #94, and notes the Resume jobs rule as built in #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(spec): name d.hold, and say Resume lists every running step Addresses N1 and N3 on #104. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(spec): cite the test, not a commit, for the Resume jobs rule Commit hashes on #104 change when it is rebased. Follows S5 on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): Resume lists every running step unit, as the spec says Addresses S4 on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): record Curtis's approval of the Resume jobs rule Closes S4 on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(spec): held steps start only after Resume; note stale timer deliveries Follows two CodeRabbit comments on #94. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): rewrap the Resume jobs decision Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): the pause hold covers step jobs, not timers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(prd): rewrap the Resume jobs paragraph; name paused step jobs in R-6 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
Base moved from The diff against Q1. Are those planning files meant to land with this PR? If not, they should be removed from the branch before merge. I did not review their content. |
# Conflicts: # README.md # documentation/definitions.md # internal/hosttest/list.go # llms.txt # local/local.go # outstanding.go # planning/README.md # planning/prds/009-pause-resume.md
A paused run holds its decision jobs, as it holds every step job. But a decision's one-minute timeout still fired and used up an attempt, so a pause longer than about three minutes failed the decision's step. Now the pause holds that timeout too: Apply records it, Resume re-arms it with a full minute, and Outstanding leaves it out until then. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Approve: Updated for d513115. The new commit holds a decision's one-minute timeout during a pause, as a step's timeout is held, and the change is sound.
I reviewed only d513115 (apply.go, outstanding.go, pause_test.go, hosts.md). The merge 13d5e16 brings in main's #97, #100 and #103 changes, which are outside this PR's diff.
Still open
- S2. No test covers Resume listing a step claimed before Pause. (fine as a follow-up)
- N5.
documentation/hosts.md:669calls a push of a running step "harmless". Execute returnsErrNotClaimablefor a leased job, and the docs say errors get a 5xx and a retry. (fine as a follow-up)
New
- N6. The new sentence in
documentation/hosts.md:185makes that paragraph's line 185 about 132 characters. Rewrap the paragraph like its neighbours. - N7. The
Outstandingdoc comment atoutstanding.go:9-11has a line about 100 characters long. Rewrap it.
Prompt for your AI tool: fix S2
Fix this review finding in deepnoodle-ai/workflow, PR #104.
1. S2, pause_test.go and internal/hosttest/pause.go: no test covers
Resume listing a step that was claimed before Pause. Wanted: a test
where a step is claimed, then Pause, then Resume, and Result.Jobs()
includes that step's running job. Add it to the hosttest suite so
both backends run it.
Run the existing tests after the changes.
|
Checked the merge of Merge conflicts, as you asked:
Not covered: Still open, none blocking: S2 (a test for Resume listing a step claimed before Pause), N5 ("harmless" wording at The approval at d513115 stands. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Approve: Updated for 1d2413f. The only new commit rewraps the pause-timeout paragraph in documentation/hosts.md. The wording is unchanged, so the approval at d513115 still holds.
Fixed: N6 (the long line at hosts.md:185 is now wrapped).
Still open, not blocking:
- S2. No test covers Resume listing a step claimed before the Pause. (fine as a follow-up)
- N5. "harmless" wording at
documentation/hosts.md:670. (nit) - N7.
outstanding.go:32is 83 characters. (nit)
Merge after #94.
What changes for the user
An operator can pause a run, look, fix the cause, and resume it, on every host. The new methods are
Store.PauseandStore.Resumeon postgres and sqlite, andRun.PauseandRun.Resumeon local. A paused run has the statuspaused. Steps in flight finish, and their results are recorded. No new step or retry starts until Resume.Issue
No issue. This removes "Pausing and resuming a run by hand" from README's "Not yet" list. The PRD is #94.
See it
ExampleRun_Pauseinlocal/example_test.go. The firstchargeattempt fails while the run is paused, so its retry waits for Resume:Output:
Why
Today Cancel is the only way to stop a run that goes wrong, and Cancel ends the run for good. An operator who sees bad input, or a downstream API that is down, needs to stop new work on that one run now and continue it later. They must not lose the work the run already did.
Decisions to check
Claim,ClaimKeyandQueueStats. local waits before it calls the handler. Rejected: hold dispatches in the engine. That misses retries and queued steps that were dispatched before the Pause.ScheduleTimerwith a full timeout from the Resume time. Timer jobs are still claimed, so the hosts need no timer change. Rejected: count timeouts from handler start. That changes whattimeoutmeans for every run.on_failureandfinallystart with no Resume. Rejected: hold cleanup until Resume. That needs a paused form of each finishing status.Result.Jobs()lists the re-armed timeout timers, then every running step of the run (held, or claimed before the Pause). A host that pushes one request per job (Cloud Tasks) sends them again, so a held step starts at Resume, not at the queue's next retry. The list comes fromOutstanding, with no new query. A job pushed twice is claimed once. Rejected: rely on the queue's retry, whose backoff can be long.Run.Resume, next to the existingRunner.Resume. It is the same word on every host. The doc comment of each method names the other.How to verify
Start reading at
apply.go(theEventPauseandEventResumecases,rearmTimeouts). Then readnotPausedinpostgres/backend.goandsqlite/backend.go, andholdandwaitHeldinlocal/local.go. New tests:pause_test.go): pauses change nothing on random graphs; a timeout during a pause, also with two pauses; a sleep during a pause; each outcome while paused.internal/hosttest/pause.go, both backends):PauseHoldsStepsUntilResume,PauseStopsTheTimeoutClock,ResumeListsTheHeldStepsandCancelAPausedRun.local/pause_test.go): includes a slowSave.claim_test.go: the claim keeps its index plan with a paused run.Risk and rollout
runs.status.hosts.mdcontract says so.Status == StatusRunningto mean "not finished" now misses paused runs.Terminal()is the documented test. The postgres supersede example now selectsstatus IN ('running', 'paused').Not in this PR
Reviews
Pausereturned, whenSaveis slow. Fixed in da5a33b: the hold starts before the reply. A slow-Savetest fails without the fix.PauseandResumewrappers. Fixed in 57fe1ff: the suite now calls them on each backend.RunOnce.d.setHeld) was fixed in 27bee86 on docs(prd): pause and resume a run #94. N2 (thellms.txtline) and N3 (Resume lists every running step, not only held ones) were fixed in fa1f2e0.PRD: #94
🤖 Generated with Claude Code