acpmux: preserve retries and adopted prompt outcomes across restart (takes over #26981)
Part of HQ#5075 (PR order 2: durable retry and adopted prompt ledger) and HQ#4879 (finding 2: a turn a daemon restart interrupts is not always resumed; an auto-resume wait does not survive a restart).
Takes over #26981 (its owner session is gone), carried onto current feat-cmux-next with its commits and authorship, plus a fix for the review finding that blocked it.
Automatic retries survive daemon restart at their original deadline, with a stable prompt ID suppressing duplicate execution. Non-hosted turns interrupted by restart continue immediately; adopted running prompt IDs remain running on resend. See #26981 for the full design, its write-boundary test tables and its earlier red/green runs.
Leo: "I think the various situations we have with cloud and whatnot, like just having that done real nicely will be quite important."
Leo: "ACCEPT IT BREAKING AT SOME POINT BUT IT RESUMES WHATEVER IT INTERRUPTED. END OF STORY FOR THE DAEMON UPDATE."
What changed relative to #26981
- Merge onto base (765 commits later). Conflicts resolved:
hub/auto_resume.rs:take_turnkeeps base's carried-turn start (#27536,nextstarts in the same step) on the per-session retry mark (Session::auto_resume) this branch moves to;stopped_for_testreads that mark;set_retrykeeps this branch's signature with base'sJsonSet::set.hub/hosts.rs: a recovered turn's finish keeps the carried next turn (#27536) and also publishes the adopted prompt's outcome (outcome.send_replace).hub/restart_resume.rs: both the carried-steer scan (#27536) and the durableturn_stop_requestedmarker.hub/auto_resume/restore.rs: tests calltake_turn(.., None); the one newv["k"] = xwrite usesJsonSet::set(ebce6312).- #27040 (autoResumeDelaysMs sets the attempt count) and b0f77cc2 (Stop before a retry's turn starts) cover neighbouring ground but not this: neither restores a scheduled retry after a restart.
- Review fixes. The earlier review of #26981 and four review rounds here found these; each has a failing test committed before its fix:
daemon/cloud_handoff.rs: a late retry decision could replace a finished turn's end. After a manual retry completed,end()setNow::Quiet; with Cloud away its done report waited inlater; the superseded automatic retry'sauto_resume_skippedbecameNoRetryFor, andfinish()replaced the waiting done withfailed. The reporter now keeps the newest turn whose end it reported (Track::ended, alongside the turn waiting inlater), drops aNoRetryFororRetryRestoredfor that turn or an older one while quiet, and drops an older turn's end infinishso it never removes a newer waiting end. Only a begin the store accepted or never answered, of the newest ended turn, reopens it. ARetryRestoredrenews a held turn's wait only when it names that turn. Tests:a_late_retry_decision_keeps_a_finished_turns_end,a_restored_retry_of_another_turn_keeps_the_held_turns_deadline,an_older_turns_end_never_drops_a_newer_waiting_end.hub/restart_resume.rs: #26981 changedlogged_promptto answerNonefor a prompt that was queued but never started, which broke the restart continuation's once-only guard. A crash between the continuation'squeuedrecord and the cut turn'sturn_resultqueued it twice. The guard now also counts aqueuedrecord with that ID (queued_logged). Test:a_crash_before_the_cut_turn_settles_queues_its_continuation_once.hub/inbox.rs: on a restart, base's restored inbox review (#27686) could wake an idle chat with a turn of its own before the durable retry fired, so the retry was skipped as superseded.inbox_reviewnow waits whilecontinuation_pending, asdigest.rsandsubagent_retry.rsalready do. Test:an_idle_chat_waits_for_its_cut_turns_continuation_before_its_inbox.- Panic ratchet: the
Valueindex reads this PR added in production code use.get()(acpmuxindexing_slicingstays at base's count). - Not changed:
make_roomevicting a quiet track'sTrack::ended. Eviction leaveslateralone, whichended_herealso checks, so an end still waiting stays protected. An end the store already has can't be replaced, because the store refuses any report on a turn that already ended (module doc,Outcome::Refused).
Observed / Not run
Latest, at aaeff484938c1f32107c8bd44e78c7b8079fb2e5, run 41073089483: red at 426076ea (the inbox test), green at head: lib 2,273 passed, durable_agents 131 passed, clippy clean. Earlier runs:
Run 41028785396 (Blacksmith 32 vCPU, cmux-ci test) at 13acc523f8aedc5b9dcd80463a9b65fe032c140f:
- Red at
d798debb(tests before the fix):a_restored_retry_of_another_turn_keeps_the_held_turns_deadlineandan_older_turns_end_never_drops_a_newer_waiting_endfail. - Red at
3f483395:a_crash_before_the_cut_turn_settles_queues_its_continuation_oncefails,left: 2, right: 1(twoqueuedrecords). - Earlier runs 41002430556:
a_late_retry_decision_keeps_a_finished_turns_endfails at46a071c9,3977a341andf215a828, each before its fix. - Green at head:
cargo test -p acpmux --lib2,027 passed, 0 failed;cargo test -p acpmux --test durable_agents130 passed, 0 failed;cargo clippy -p acpmux --all-targets -D warningsclean.
Not run: no Swift is changed, so there is no Mac build or app capture. No two-daemon restart run on a live host (that is HQ#5075's later acceptance test).
Changelog
Fixed: automatic retries and running prompt outcomes recover across daemon restarts.
Agent session: agent session
š¤ Generated with Claude Code