skip to content
Jobs

Teaching Codex to Wait Out a Usage Limit

Codex usage-limit hits become a timed pause and auto-resume, not a dead run

Gantry sprints
Codex usage-limit hits become a timed pause and auto-resume, not a dead run

Gantry drives coding agents from more than one vendor, and each one eventually hits a usage limit — the point where the account has spent its allowance and the provider asks you to come back later. Gantry's Claude agent already handled this gracefully: it read the reset time out of the error, slept until the window reopened, and carried on. Codex did not. When Codex hit a usage limit it looked to Gantry like an outright failure, and the whole run stopped — even though the only thing wrong was that it was too early to try again.

There were two reasons Codex couldn't do what Claude did. First, Codex reports its final answer in a file, but the usage-limit message only ever appears in a separate stream of JSON events that the part of Gantry deciding success-or-failure never got to see. Second, even if it had seen the message, Codex had no code to read the reset time out of it. This job closed both gaps. Now a Codex usage-limit hit is treated the same way Claude's is: Gantry reads the "try again at" time, pauses until then, shows the wait plainly instead of looking hung, and resumes on its own — and when no time can be read, it falls back to spacing out retries the way it already did for a temporary overload.

Build

The work split a behaviorally inert plumbing change from the logic that depended on it, so the first piece could be proven a no-op before the second gave Codex any new behavior.

The decomposition drew its boundary between carrying a new signal and acting on it. The first sprint only widened the input the classification step receives, threading Codex's captured stdout JSON stream in alongside the answer text and stderr it already got — and its brief demanded the change be behaviorally inert: every harness had to classify exactly as before, with the three non-Codex plugins ignoring the new input and Codex accepting it in its signature without yet reading it. That constraint is what made the piece agent-sized. A fresh agent could land it and prove it with the existing classification tests unchanged, because "nothing's outcome moved" is a property you can check without any reset logic in existence.

Only then did the second sprint give Codex the logic: recognize the usage-limit event on the stream now reaching it, parse the reset time, compute the wait, and route it through the same retry accounting and the same pause-and-resume rendering Claude's path already used. That piece leaned entirely on the channel the first had proven wired, which is why sequencing them apart kept the harder change confined to one plugin.

The run stayed green from end to end — no gate sent a sprint back, no review forced a re-plan. That absence is worth reading rather than skipping past, because the reset-time parser was the part most likely to fight back: it turns an ambiguous bare wall-clock time, with no date and no timezone, into a duration to sleep. The brief dissolved that risk before an agent touched it — it pinned the work to a verbatim usage-limit sample captured from a real incident, specified the pure parser be built around an injected clock so its tests are deterministic and never actually sleep, and called out the pathological case up front: a reset time sitting just behind "now" must clamp to a short wait rather than parking a worker for most of a day. The difficulty was named in the brief, so it never surfaced as a repair.

Feature

Before, a Codex usage limit stopped a run outright; now Gantry reads the reset time, sleeps until the window reopens, and auto-resumes — the same handling Claude already had, on shared machinery.

Before this job, a Codex usage-limit hit was a terminal halt. The message announcing it lived only on the stdout JSON event stream, which the classification step never inspected, so an empty answer file plus that stream read as a systemic failure and ended the run. The fix is a shared input struct that carries the captured stdout stream into classification as a first-class channel: a stdout-answer harness like Claude sees the same bytes it already had, while a file-answer harness like Codex gains a genuinely new one. Codex's classifier now checks that stream, recognizes the usage-limit signal from either carrier the provider emits, and returns a retryable outcome with a computed wait when a "try again at" time is present and no hint — falling back to the fixed retry schedule — when it isn't. The wait draws on the usage-reset retry counter rather than the overload counter, matching Claude, and it renders through the existing pause note so a long park reads as a deliberate sleep-until-reset instead of a hang.

Standing in the tree today, all of it is still here and the design has grown rather than been replaced. The classification input struct, the Codex reset parser, and the shared rendering are all present at HEAD, and the harness documentation describes Codex as reset-aware in exactly these terms. The most telling drift is an extension: the parser now also handles Codex's dated weekly-limit form — a reset time carrying a full month and day, not just a bare clock time — computing a wait that can legitimately span days, with an upper-bound clamp so a worker is never parked past the longest real window. That later work built on the same parsing and reset-wait path this job established rather than standing up a second implementation, which is the mark of a boundary that held.

unavailable sessions
unavailable context
39,285 in / 88,448 out tokens
$14.56 cost
0 x 2 milestones x sprints
unavailable edits
unavailable commands
27m duration
unavailable roles
0 x 0 fixes x replans
- harness