Repository navigation
F1: apply a stop only after the caller's transaction commits (#79) - #96
Merged
Merged
Conversation
A stop made inside Store.userAction (cancel attempt, cancel task) used to set the job's first reason, abort preparation and cancel D in the same turn as its reason write. If the action then rolled back, D stopped anyway and the attempt ended cancelled while the task stayed open. - Store.afterCommit runs a callback after the outermost transaction commits, drops it on rollback (including a failed COMMIT), and runs it at once outside a transaction. - The coordinator applies a stop's in-memory effect from that callback. The first stop in a transaction wins while its commit is pending. A reason write that throws still applies the stop at once, unsaved. - The cancel-task re-save and cancelShown are set on commit, replacing the microtask checks. - ROLLBACK runs only while SQLite still has a transaction open, so a failed COMMIT reports its own error. - Writes inside a transaction refuse to run once SQLite rolled it back on its own, so they cannot autocommit one by one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The transaction and coordinator changes consistently address #79 with focused regression coverage.
Review effort: Balanced
Findings: None
What changed in this PR
Defers runner stop effects until the enclosing Store transaction commits, preventing rolled-back cancellations from stopping active work.
Changes:
- Adds commit/rollback callbacks and guards against SQLite auto-rollback.
- Defers coordinator cancellation effects while preserving first-reason semantics.
- Adds lifecycle documentation and regression coverage.
| File | Description |
|---|---|
runner/store.ts |
Adds transaction callbacks and rollback safeguards. |
runner/coordinator.ts |
Applies stop effects only after commit. |
test/runner-lifecycle-store.test.ts |
Tests transaction callback behavior. |
test/runner-coordinator.test.ts |
Tests cancellation commit and rollback races. |
docs/implementation/runner-lifecycle.md |
Documents the revised lifecycle guarantees. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
# Conflicts: # test/runner-lifecycle-store.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #79.
A stop made inside
Store.userAction(cancel attempt, cancel task) used to apply its in-memory effect at once: the job's first reason, the aborted preparation andhandle.cancel. If the action then threw, or its COMMIT failed, the write rolled back but D stopped anyway. The attempt then endedcancelledfrom the job's in-memory reason, and the task stayed open.What changes
Store.afterCommit(commit, rollback?)commitat once (a throw reaches the caller). Inside one, it queuescommituntil the outermost transaction commits (a throw is logged, so a committed write never looks failed). On rollback, including a failed COMMIT, it dropscommitand runsrollback.RunnerCoordinator.#requestStoppendingReasonmakes this first stop win over later ones; a rollback clears it. A reason write that throws still applies the stop at once, unsaved ("Rules for the running state", rule 1). If the durable reason cannot be read back, the stop shows as unsaved, as before.RunnerCoordinator.cancelTaskcancelShown, are now set on commit. This replaces the two microtask checks (#confirmSavedand thecancelShownrecheck).Store.#transactionisTransaction). Before, a COMMIT that SQLite had already rolled back (for example on a full disk) reported "cannot rollback" instead of its own error.Store.#runrunner-lifecycle.mdValidation (local, head
7b4de20)npm run typecheckpasses.Runner and Store files (
runner-coordinator,runner-lifecycle-store,runner-shutdown,runner-execution,runner-recovery,store): 263 of 263 pass.Full suite without the two real-Docker files: 1247 pass, and 2 failed under load:
question-leftovers(a question-leftovers test) andreview(one test took 36 s). Both files pass alone, 61 of 61. This change does not touch them.16 new tests. 14 were checked to fail before their fix: the 7 coordinator behaviour tests, the 4 Store
afterCommittests, the two COMMIT-failure tests (withcommittedset before COMMIT), the auto-rollback ROLLBACK test, the autocommit guard test and the read-failure test. The two remaining tests guard behaviour the issue says to keep: a stop whose write fails still stops D at once.Merge of
main(F2b: durable safety findings, failed-run audit and partial output (#87, part 2) #94, Ask: recover only this review's Docker leftovers by owner (#65) #95) at7f890fc: one test-file conflict, where both sides added test blocks, resolved by keeping both. The typecheck passes, and the runner and Store files pass 277 of 277. F2b: durable safety findings, failed-run audit and partial output (#87, part 2) #94's new stop paths go through#requestStop. Its new error handling (the failed-run audit and partial export) runs outside Store transactions.Review rounds
• A failed read of the durable reason shows the stop as unsaved, not saved.
• Comments and docs no longer say "storage error" where any throw counts, or "nothing in memory changes".
• Docs say a failed COMMIT drops the stop.
/code-review high1• Removed a dead
!job.attemptIdguard.• Tests use
vi.spyOn.• No callback list is allocated for a transaction without callbacks.
handle.cancelin a commit callback: D'scancelrecords its reason in its first statement and does not throw (agents/adapters/supervisor.ts).• Mocks are restored in
finallyand inafterEach.• The two
afterCommitdescribe blocks are merged.#runrefuses writes after SQLite's own rollback.7b4de20, once on7f890fc)Review lessons (merge gate)
runner-lifecycle.mdstates it for the coordinator.#transactionand#run, so every caller gets the fix, and a rule would only repeat the code.🤖 Generated with Claude Code