Part of #4. Comes after every other Codex issue: it is the payoff for the seam, not part of building it.
Why it is worth building
The user is on a phone, possibly over satellite, and a reply is the only thing they see. "Claude is rate limited" is a dead end when a second agent is installed and idle.
What is there today
finish() (app/Uplink/uplink.ts#L343) already branches on result.authFailure before replying, so
the plug point exists. Nothing above authFailure classifies a failure: a Claude weekly-limit message
and a Codex 429 are both just text in result.text.
Proposal
- Add a failure classification to
EngineResult, alongsideauthFailure: enough to tell "rate limited" from "the task failed". Codex's own vocabulary for this is visible in the binary (rate_limit_exceeded,usage_limited,budget_limited,workspace_owner_credits_depleted), and Claude's is its limit message. - Add
tried: Set<EngineId>toJob(app/Uplink/uplink.ts#L46) so a two-sided limit cannot ping-pong. - On a rate-limited result with an untried engine available, re-
begin()the same job against it and say so in the reply, because a silent engine switch would make the answer's style change for no visible reason.
Requirements
- Never retry a task that did work. A rate limit mid-run may have left files changed. Fall back only when the run produced no tool use, or say plainly in the text that the second agent is starting over.
- Do not carry the session id across. The other engine cannot resume it.
- One fallback per job. No loops.
- Off by default, or clearly visible in the reply. A user who asked for Codex should not silently get Claude.
Acceptance
- A test proves a rate-limited first engine leads to one run on the second and one reply that names the switch.
- A test proves a job that already ran tools is not silently retried.
- A test proves both engines rate limited produces one reply, not a loop.