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
_rollback_or_pause can raise RunPaused through preserve_attempt_commits → pause_for_manual_recovery when commits exist above baseline and the recovery ref cannot be
created. pause_for_manual_recovery calls self._save() before raising, so Phase.DEFERRED is
persisted — and statemachine.py declares Phase.DEFERRED: frozenset(), i.e. terminal.
Net: the story is durably DEFERRED, but the tail after the rollback never runs and can never run.
The story-deferred journal entry and the defer notification are skipped permanently, not deferred
until resume.
Impact — forensics only, as far as I can tell
Worth stating plainly so this is triaged at the right size:
No data loss. The pause leaves the working tree untouched, so the deferred_work snapshot
captured just above has nothing to restore — the session's ledger edits are still on disk.
The operator is not left uninformed. They get the bold ACTION REQUIRED manual-recovery
notice instead, which is louder than the defer notification would have been.
Today: in-place defer (isolation="none") with commits above baseline whose attempt-preserve/*
ref fails to create. The isolated arm returns before the rollback, so worktree mode is unaffected.
After #341: a second path, a failed uncommitted-work snapshot over a tree that would still lose
work. Same shape, same consequence.
Possible shapes
Journal + notify before _rollback_or_pause, so the defer record exists regardless of what the
recovery does. Changes the ordering guarantee — the notice would name a preserve_ref the
rollback has not set yet, so _defer_recovery_note would need to move or be re-emitted.
Catch RunPaused around the rollback, emit the defer record, re-raise. Keeps the ordering but
adds a catch of the control-flow exception, which the codebase otherwise treats as
unwind-to-the-top.
Advance to DEFERRED only after the rollback returns, so a pause leaves the task in its prior
phase and resume re-enters _defer. Most faithful, but changes what a resumed run sees and
interacts with _finish_inflight's handling of the pre-defer phase.
No strong preference — the first is smallest, the third is most correct.
Acceptance
A decision plus, if behavior changes, a test at the Engine seam driving a defer whose rollback
pauses, asserting the defer record survives. Ablate it: the test must fail with the fix removed.
Split out of #340 / PR #341 review, which widens the reachability but does not cause it.
Current behavior
Engine._defer(engine.py) advances the task before it recovers the tree:_rollback_or_pausecan raiseRunPausedthroughpreserve_attempt_commits→pause_for_manual_recoverywhen commits exist above baseline and the recovery ref cannot becreated.
pause_for_manual_recoverycallsself._save()before raising, soPhase.DEFERREDispersisted — and
statemachine.pydeclaresPhase.DEFERRED: frozenset(), i.e. terminal.Net: the story is durably DEFERRED, but the tail after the rollback never runs and can never run.
The
story-deferredjournal entry and the defer notification are skipped permanently, not deferreduntil resume.
Impact — forensics only, as far as I can tell
Worth stating plainly so this is triaged at the right size:
deferred_worksnapshotcaptured just above has nothing to restore — the session's ledger edits are still on disk.
ACTION REQUIREDmanual-recoverynotice instead, which is louder than the defer notification would have been.
story-deferredjournal entry (so a post-mortem readingjournal.jsonlsees the story stop with no defer record) and the_defer_recovery_notepointernaming where the attempt survives (Name the attempt-preserve ref in the defer notification, status, and --json #333).
Reachability
Today: in-place defer (
isolation="none") with commits above baseline whoseattempt-preserve/*ref fails to create. The isolated arm returns before the rollback, so worktree mode is unaffected.
After #341: a second path, a failed uncommitted-work snapshot over a tree that would still lose
work. Same shape, same consequence.
Possible shapes
_rollback_or_pause, so the defer record exists regardless of what therecovery does. Changes the ordering guarantee — the notice would name a
preserve_reftherollback has not set yet, so
_defer_recovery_notewould need to move or be re-emitted.RunPausedaround the rollback, emit the defer record, re-raise. Keeps the ordering butadds a catch of the control-flow exception, which the codebase otherwise treats as
unwind-to-the-top.
DEFERREDonly after the rollback returns, so a pause leaves the task in its priorphase and resume re-enters
_defer. Most faithful, but changes what a resumed run sees andinteracts with
_finish_inflight's handling of the pre-defer phase.No strong preference — the first is smallest, the third is most correct.
Acceptance
A decision plus, if behavior changes, a test at the
Engineseam driving a defer whose rollbackpauses, asserting the defer record survives. Ablate it: the test must fail with the fix removed.