Repository navigation
fix(verify): a patient route timer waits for stale offers too - #8834
Conversation
Quest-Claim: 2de13792fb33a170
A patient timer is one long enough for the news of a closed session to cross the mesh, so it also outlasts every offer through that session sent before the news reached its sender. The model let it fire once no DEAD was in flight, while an older offer naming the dead hop could still be queued, so a relay forgot the hop and took that offer back. HoldCycle broke NoStaleReturn that way under random seeds, and Twice and Loop within seconds. Lift and Expire now wait for every message naming the hop, so all three check NoStaleReturn and HoldCycle samples a new seed each run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 1 minute. View limit details
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. |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run reviewed the strengthened Patient guard in the route model, plus the config and skill updates that follow from it.
Patientguard covers stale offers:LiftandExpirenow wait until no queued message names a hop they would let a relay forget. That covers an offer's path, a DEAD's hop, and a DEAD's moved paths, using the newMsgHops/Severed/InFlighthelpers. I confirmed the old DEAD-only check is a subset of the new one, because a DEAD's hop always carries a closed generation.SeveredexcludesOHop.Disconnectempties the link's queues andDeliverstays enabled while anything is queued, so the stronger guard cannot deadlock a run.- Configs:
HoldCycle.cfgno longer pins-seed 1, andTwice.cfgandLoop.cfgnow checkNoStaleReturn. - Skill text: the
verify-routedescription of the timer's cost now matches the new guard. The claim that two failures let a stale route return whatever the timer is removed.
ℹ️ search.rs still uses the old guard
rs/moq-net/src/route/tests/search.rs describes itself as RouteHandoff.tla's check run on compiled code. Its patient() still waits only on DEAD and DRAIN messages. It checks no stale-return rule, so this causes no false failures today. If a stale-return rule is ever added there, it needs this PR's stronger guard.
claude-opus-5-5 | 𝕏
Main's patient timer (#8834) waits for stale offers as well as DEAD news, which closes the hole that let a relay take an offer naming a hop it had forgotten died. TwoHomes.cfg with Stable passes 40,000 traces on seed 1, where the pre-merge model failed within a minute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Finishes quest/m2/cluster-routes/holdcycle-stale-return.md:
HoldCycle.cfgnow holdsNoStaleReturnunder any simulation seed.Summary
HoldCycle.cfgbrokeNoStaleReturnunder random seeds. The cause was the model'sPatientguard, not the router.The failing behaviour (48 steps, found with TLC's 2026-11-15 nightly build) runs like this:
The router follows this path. A scratch
Simreplay underRule::PATHheld the 5→2 queue while every timer fired, then delivered one message. Relay 2 routeda/through 5 on path0,1,3,5, which crosses the closed 1–3 session. The replay is not committed.This is an early timer, not a router bug. The replay needs one message delayed longer than the 1 s doubt timer. That is the cost
Early.cfgalready documents: a timer that fires early lets a stale route return. A timer long enough for the news to cross the mesh also outlasts every offer sent before the news reached its sender. The model'sPatientonly waited for the DEAD.Changes:
RouteHandoff.tla: withPatient,Lift(n, x)andExpire(n, hop)now wait until no message in flight names the hop they let a relay forget. That covers a DEAD's own hop, a DEAD's moved paths, and an offer's path. ForLift, a hop counts when it names a closed session ending atx. The DRAIN check is unchanged. New helpers:MsgHops,Severed,InFlight.HoldCycle.cfg: drops-seed 1, so Nightly samples new orderings each night. An unseeded simulation always reruns underTLA_CACHE.Twice.cfgandLoop.cfgnow checkNoStaleReturn. The same defect is why they left it out. Onmainthe pinned TLC breaks it within seconds in both, and with this guard both pass.verify-routeskill:Patientdescription is updated.HoldCyclerow says a new seed each run.No Rust changes.
Decisions
All of these were taken by the agent while the maintainer was away. Each can be revisited in review.
StaleIsChasedbounds.Lift's guard counts only hops of closed sessions (Severed). It does not count every hop ending atx. An offer through an open session toxis not stale, and blocking on it would delay many lifts and shrink coverage for no reason.NoStaleReturntoTwice.cfgandLoop.cfg, which goes beyond the quest's HoldCycle scope. The quest said to keepTwice.cfgwithout it unless a behaviour showed a second session change was the cause. The behaviour shows the opposite: Twice's own counterexample (17 steps) is the same stale offer outliving a patient timer.Loop.cfgkeeps its-seed 1. That is outside this quest.Note for moq-dev/moq#8734
Its
TwoHomes.cfgdropsStablebecause "a relay can take an offer older than the timer that names a hop it has already forgotten died". That is this mechanism. Under this guard,TwoHomes.cfgmay holdStableagain, and whichever PR lands second should rerun it. Both PRs also add invariants toTwice.cfgandLoop.cfg, so expect a trivial conflict in theirINVARIANTSlists.Validation
The random-seed failure reproduces only with TLC's nightly build (2026-11-15). On the pinned TLC 1.8 and the 2026-10-02 build, 1.2M-state runs of
HoldCycle.cfgpassed on every model version tried, with or without the fix. So I used the nightly build for the before/after comparison:12537d1c2HoldCycle,num=3000or10000,-depth 100, random seed98ed0f827HoldCycle,num=3000 -depth 100, random seedmain(250a13a17)HoldCycle, the failing behaviour replayed as a fixed action sequenceNoStaleReturnat step 48Lift, no errormainHoldCycle,num=10000 -depth 100, random seedmain+NoStaleReturnTwice,num=10000 -depth 100main+NoStaleReturnLoop,num=3000 -depth 150With the pinned TLC 1.8, as Nightly runs it:
HoldCycle.cfg(newSimulate:line, unseeded): 1.6M states, no error.Twice.cfgandLoop.cfgwithNoStaleReturn: 4.0M and 601K states, no error. Onmain's model the same two runs fail within 158K and 16K states.RouteHandoff.cfg: no error, 434,956 distinct states, down from 476,515. The states removed are timers firing while a stale offer is queued, whichEarly.cfg(Patient = FALSE) still covers.Prefer.cfg: 11,582 distinct states, no error.Upstream.cfg: 63,335 distinct states, no error.The stronger guard only removes behaviours, so no config that passed before can fail. A queued message keeps
Deliverenabled, so the guard can't deadlock a run.Router: the scratch
Simreplay above (Rule::PATH, 1 s doubt) reproduced the stale route on the compiled router.just checkpasses.Caused by
Patientas "no DEAD about its node is in flight", which left out stale offers sent before the news (found by check: random-seed simulation).Public API / wire impact
None. The change is limited to the model, its configs, and the skill.
Follow-ups
None.
Models
Size: M
Implementer: Claude Code, claude-opus-5-5, unknown effort (arm B)
🤖 Generated with Claude Code
(Written by Claude Opus 5.5)
Review
Rewritten by
quest reviewafter each round.Head: 06a461a
Base: origin/main at ca88fc1
Risk: medium,
the base has no RISK.md to name what is always high or customer-facing, so every change is mediumClassified: medium by default
Reviewer: Codex, gpt-6.1-sol, high effort
Session:
01a1dfbb-b7f6-7390-8431-b3a43f8e63ce, round 1Score: 9/10,
No actionable defects were found; the patient-timer guards consistently cover stale offers and replacement paths, though tests and model checks were not run as instructed.Rounds: 1; first round 0; findings 0: fixed 0, refuted 0, open 0
Findings: none