Backoff That Waits for the Reset Time
When a usage window runs out, sleep until it reopens instead of halting the run.
A long autonomous build can run into a subscription usage limit partway through: the coding CLI prints something like "you've hit your session limit, resets in the morning" and stops. Gantry used to read that as a fatal error and end the whole run — the single most common way a multi-hour build stalled overnight, with nobody watching. And even when Gantry did retry a temporary upstream failure, it backed off on a fixed schedule, sleeping the same preset amount whether the cause was a brief overload or a usage window that would not reopen for hours.
This job taught Gantry to recognise a usage or session limit as something to wait out rather than fail on, and — when the CLI states the wall-clock time the window reopens — to sleep until that moment and try again, instead of guessing at how long to pause.
Build
Four sprints in order: broaden which failures are retryable, add an optional wall-clock wait to the retryable outcome and make the loop honour it, parse the real reset time in the Claude harness, then render and document the wait. That order put the timezone-coupled parser behind an interface already proven without it.
The decision that shaped this cut is where the reset-time math lives relative to where it is used. The middle sprint built only the data model — the retryable outcome gains an optional wall-clock wait, and the retry loop learns to sleep that wait instead of walking its fixed schedule. Its brief makes the point explicit: the whole thing is testable by injecting hinted failures into the existing stubbed loop, with no dependency on time, timezones, or any real message format. Nothing in it reads a clock.
That is what let the following sprint carry the genuinely awkward part on its own. Parsing the reset time is the only piece that reads a wall-clock moment out of a message and resolves a named timezone — and the project has no timezone-aware library, so it does the resolution by shelling out to GNU `date`. Placed after the wait already existed and had been proven, that parser could be given an injected "now" and tested for exact seconds without touching the real clock. Reverse the order and there is nowhere to put a parsed reset time; the environment-coupled work would have had no proven interface to plug into.
One design choice in the loop sprint carries its own justification: reset-waits draw on a separate bounded counter rather than the overload schedule, so a plan that crosses two usage windows in a night is not killed because earlier, unrelated overload blips had used up its retry budget hours before.
Every sprint passed its gate and none forced a re-plan or a retry. The one failing gate in the run sits before any sprint began, in the run's baseline setup — so none of the four decomposition boundaries was ever load-tested by a red gate. Read against the plan, that is the difficulty being dissolved in advance: the hard part, timezone-aware arithmetic with no library, was concentrated into a single sprint and isolated behind an interface the prior sprint had already grounded.
Feature
Gantry now treats a usage or session limit as retryable, and on the Claude harness computes the sleep from the reset time the CLI states, waiting on its own bounded counter and rendering the pause as a deliberate wait with a known wake time.
Before this job, a usage-window exhaustion was terminal and stopped the run, and the only backoff Gantry had was a static schedule indifferent to why the failure happened. The answer runs across the layers. The shared, harness-neutral matcher that decides whether a failure is retryable gains the session- and usage-limit wording, while keeping ordinary ceiling messages terminal. The retryable outcome type carries an optional wait hint. The retry loop sleeps that hint regardless of attempt index, draws it from a bounded counter separate from the overload schedule, and collapses to a terminal "gave up after N usage-limit waits" when that counter is spent. The Claude harness — and only it, since the wording is Claude-Code-specific — parses the stated `resets <time> (<zone>)`, resolves the named zone through GNU `date`, rolls a past time forward to the next day, then buffers and clamps the result so a misparse can never wedge a worker. Both front-ends and the per-harness docs render and record the behaviour: claude is reset-aware, the other harnesses get the broadened classification but fall back to the fixed schedule.
Standing in the tree today, the design holds. The matcher's added wording, the parser with its named buffer and clamp constants, the environment tunable for the reset-wait budget, the separate counter with its give-up reason, and the distinct waiting line are all present at HEAD. One piece of drift is worth naming plainly: the retry loop the job modified has since moved out of the build module — which is gone from the tree — and into the agent module, carrying the reset-wait logic with it, folded into a later reorganisation rather than removed. The README lines the job first added are no longer present, the README having been rewritten since, but the per-harness documentation of reset-awareness persists.