fix(engine): close resume-replay edges from #62 (final-cycle replay, stale result_json, record bloat)#63
Conversation
Three follow-ups to the session-end durability / resume-continuation work in #62 (which closed #57). Each only bites the new crash-in-the-post- session-window replay path, so none regress steady-state behavior. (a) Final-review-cycle replay dropped a recorded clean review to defer. `_review_and_commit`'s loop was guarded by `review_cycle < max`, but a host death in the post-session window of the *last* allowed cycle resumes with `review_cycle == max`, so the replayed clean pass never entered the loop and the story fell through to defer. Relax the guard to also enter while a `resume_result` is pending; the replay branch already skips the re-increment, so no extra budget is burned. (b) `SessionRecord.result_json` aliased the dict `_reconcile_generic_terminal_status` mutates in place. A crash between reconcile's spec write and the dev-decision save persisted a pre-reconcile dict; on replay reconcile early-returns and never re-folded `followup_review_recommended`, so the follow-up review could be silently skipped. Fix: persist a defensive copy at record time, and on reconcile's idempotent early-return sync the stale dict from the already-read frontmatter (status + followup). Fold followup only when the frontmatter carries the key — on the generic path that is the same source `devcontract.synthesize_result` reads it from, so a present key can never disagree, but an absent one must not clobber the session's own result.json value to a phantom False. (c) Triage/sweep and labeled plugin-workflow sessions persisted large, never-consumed `result_json` into state.json (`_resumable_session` only ever matches dev/review task ids). The same defensive-copy site gates these to None via `resumable = label is None and role in ("dev", "review")`. Tests: final-cycle clean replay -> DONE (zero re-run, no defer); final- cycle dirty replay -> defer with no extra budget; reconcile early-return heals a stale resumed dict; record isolation under post-record mutation; result_json persisted only for resumable roles. Refs #62, #57 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThe review loop's resume condition now permits loop entry when a resume_result is present even after the review-cycle budget is exhausted. Terminal-status reconciliation heals in-memory result_json from frontmatter on resume. Session persistence now stores result_json only for dev/review roles, as a defensive copy. ChangesResume Durability and result_json Persistence
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🤖 Augment PR SummarySummary: Fixes three resume/replay edge cases introduced in #62 so recorded session results are correctly replayed after a crash. Changes:
Tests:
🤖 Was this summary useful? React with 👍 or 👎 |
Summary
Maintainer follow-up to #62 (which closed #57), closing the three non-blocking edges called out in that PR's review. Each only surfaces on the new crash-in-the-post-session-window replay path #62 introduced, so none regress steady-state behavior.
(a) Final-review-cycle replay dropped a recorded clean review to defer.
_review_and_commit's loop was guarded bywhile task.review_cycle < max_review_cycles. A host death in the post-session window of the last allowed review cycle resumes withreview_cyclealready== max_review_cycles, so the replayed continuation evaluated the guard as false, never entered the loop body, and the durably-recorded clean review was dropped — the story fell through to defer. The guard now also enters while aresume_resultis pending; the replay branch already skips thereview_cycle += 1re-increment, so no extra budget is burned, andresume_resultis nulled once consumed so every later iteration falls back to the normal budget guard.(b)
SessionRecord.result_jsonaliased the dict_reconcile_generic_terminal_statusmutates in place._run_sessionstoredresult_json=result.result_json(same object) and saved it before_dev_phaseran reconcile, which mutates that dict in place. A crash between reconcile's spec write and the dev-decision_save()persisted a pre-reconcile dict; on replay reconcile early-returns (fm_status == success_status) and never re-foldedfollowup_review_recommended, sotask.followup_review_recommendedread stale and the follow-up review could be silently skipped. Two-part fix:Persist a defensive shallow copy at record time so the durable record is a stable snapshot of what the adapter returned, decoupled from downstream in-place mutation (
HookContextalready copies; reconcile only touches top-level keys).On reconcile's idempotent early-return, heal the stale dict from the already-read frontmatter (sync
status, foldfollowup_review_recommended).Scoping note: the heal folds
followup_review_recommendedonly when the frontmatter carries the key, rather than defaulting an absent key toFalse. On the generic path the spec frontmatter is exactly the sourcedevcontract.synthesize_resultalready derives the flag from, so a present key can never disagree with the session's own value — but when it is absent (a signal the session may have carried only in itsresult.json), defaulting toFalsewould clobber a real recommendation on the ordinary, non-resumed path. Gating on key presence is behaviorally identical for real generic sessions and avoids that clobber.(c) Triage/sweep + labeled plugin-workflow sessions persisted never-consumed
result_json(state.json bloat)._run_sessionrecordedresult_jsonfor every session, but_resumable_sessiononly ever matchesdev/reviewtask ids underDEV_RUNNING/REVIEW_RUNNING.role="triage"sweep sessions and labeled plugin-workflow sessions can never be consumed on resume, so their payloads were pure bloat. The same defensive-copy site gates them toNoneviaresumable = label is None and role in ("dev", "review").Tests
Added to
tests/test_engine.py, reusing the existingmake_engine/resume_engine/dev_effect/review_effecthelpers and #62's crashing-_emitpattern:max_review_cycles=1, review completes clean, host dies at itspost_session; resume drives the story to DONE,review_cycle == 1, zero re-run sessions, noresume-restart, no defer.review_cycle == 1), and the story defers.done+followup_review_recommended: true, stale dict →statussynced and followup folded; idempotent path journals nothing.result.result_jsonafter_run_sessionleaves both the in-memory and on-disk record'sresult_jsonunchanged.result_json; triage and labeled records persistNone.Full suite:
1410 passed, 1 skipped. Fulltrunk checkclean on both modified files.Refs #62, #57
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests