ADR 0004: Adaptive probing instead of a single blind sleep¶
Status¶
Accepted. Implemented in M1 (policy); probe adapter planned for M2/M3.
Context¶
legacy/claude_autoresume.py (lines 505 and 667) does time.sleep(wait_seconds) — one sleep call
to either a parsed reset time or a fixed fallback, then a single retry.
Two problems, both explicitly requested to be fixed during planning:
- A
WindowExhaustedrejection with a far-futureresets_at(a seven-day window, say) sleeps the entire remaining duration even if an overage-driven rejection lifts hours or days earlier. - A
CreditsExhaustedrejection (see ADR 0003) has no reset time at all — only a human buying credits resolves it, and that top-up can happen at any moment, including moments before the fixed fallback wait elapses.
Decision¶
The waiting policy (domain/waiting.py) never computes "how long to sleep."
It computes the next instant to probe, and the run loop schedules a
cheap, throwaway turn at that instant rather than blocking. Behavior differs
by state:
CreditsExhausted— exponential backoff between a short interval (default 120s) and a ceiling (default 600s), with no upper bound derived from a timestamp, because none exists.WindowExhausted(resets_at=...)— probes atmin(resets_at + grace, now + window_probe_interval), so a long window still gets checked periodically rather than trusting the reset time blindly for its full duration.WindowExhausted(resets_at=None)— falls back to the interval alone.
Consequences¶
- A credit top-up or an early overage lift is noticed within one probe interval, not at the end of a fixed sleep — directly satisfying the requirement that the system "handle if the user adds more credits between the time that the system ran out of the session limit and when the session limit would otherwise reset naturally."
- The probe itself must be cheap and side-effect-free: a one-token prompt,
max_turns=1, no tools,setting_sources=None(noCLAUDE.mdloaded), andno-session-persistenceso it leaves no transcript and doesn't pollute the working session with throwaway "OK" turns. A rejected probe is not billed by the API, so the cadence is safe to run frequently. next_probe_instant()had a real bug caught by a Hypothesis property test during development: unbounded exponential backoff (interval * factor**probe_count) overflows Python'stimedeltamaximum magnitude at surprisingly modest probe counts. The fix computes the backoff in float seconds and clamps to the ceiling before constructing atimedelta. This is exactly the class of bug example-based tests tend to miss — see../../contributing/testing.mdfor why property tests are mandatory for anything with a numeric or time-based invariant.config.max_wait, when set, bounds the total time a run will spend waiting before giving up entirely — a run should never wait literally forever even under this design, just far more responsively than a fixed sleep would.