You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
orchestrator: codex is quota-walled until 2026-08-20, so both reviewers are one claude-opus-5 — restore review independence by model when the latch is armed #197
Found by retrospective 34 (orchestrator/logs/retro-34.md, Finding 1).
This is not #126.#126 asks for the quota latch to be re-probeable and
expiring, on the evidence that a previous firing was transient. Today's firing
is not transient and a re-probe would correctly re-latch. This issue is about
what the loop should do while the latch is right.
Evidence
codex hit a real quota wall at 2026-08-15T09:17:00Z, on #13's impl.1. orchestrator/logs/13/codex.impl.1.log ends:
error: you've hit your usage limit. upgrade to pro (...), visit
https://chatgpt.com/codex/settings/usage to purchase more credits or
try again at aug 20th, 2026 11:42 am.
Five days out. review_diversity_lost has fired four times over the run
(2026-08-13T02:31:03Z, 2026-08-14T10:08:35Z, 2026-08-14T19:06:57Z, 2026-08-15T09:17:00Z).
REVIEWER_AGENTS = {1: "claude", 2: "codex"} (orchestrator/loop.py:990).
Once codex_ok() is false, resolve() (orchestrator/loop.py:232-237) sends
slot 2 down the identical final line as slot 1:
MODELS["reviewer"] is claude-opus-5 and ESCALATED_MODEL is claude-opus-5, so there is no code path that gives the two reviewers
different weights.review_stage() states the cost itself
(orchestrator/loop.py:1271-1273):
Cross-model on purpose: two instances of one model reviewing a diff behave
closer to one reviewer than to two. Once codex is out of quota both become
Opus, losing vendor diversity but keeping independent contexts.
Every issue merged since the latch armed was reviewed by two claude-opus-5
instances. Verified on disk, not inferred:
$ foriin 13 166 163 168 165;do ls orchestrator/logs/$i/ | grep -E 'review2\.';done
# claude.review2.*.log for all five; no codex.review2.* anywhere
$ ls orchestrator/logs/*/codex.review2.*.log
# 11 issues in the entire run ever had a codex reviewer:
# 5, 8, 9, 10, 11, 20, 39, 72, 79, 80, 106
$ ls orchestrator/logs/*/[a-z]*.review2.*.log | sed 's|.*/||; s|\..*||'| sort | uniq -c 69 claude 48 codex
Blast radius on the queue, not just the reviewers
$ gh issue list --state open --limit 300 --json body \ --jq '[.[] | .body // "" | capture("(?im)^\\s*Agent:\\s*(?<a>codex|claude|duel)").a] | group_by(.) | map({a:.[0], n:length}) | .[] | "\(.a)\t\(.n)"'claude 72codex 47duel 5
$ # plus 13 open issues with no Agent line, which default to codex
65 of 137 open issues (47%) name an agent that cannot run for the rest of the
run. And because resolve("codex", role) and resolve("claude", role) now
return the same ("claude", MODELS[role]) pair, Agent: codex and Agent: claude are indistinguishable — the planner's routing decision, the
retrospective's routing decision, and the whole duel alternation carry zero
information while the latch is armed. Nothing says so anywhere; the issue bodies
still read as if a vendor was chosen.
What to change
orchestrator/loop.py only. One behaviour: when the codex slot falls back, give
it a different model rather than a second copy of reviewer 1.
Add a constant next to MODELS (orchestrator/loop.py:99-107):
# The stand-in for codex once the quota latch is armed. A DIFFERENT# generation, not the same weights twice: review_stage()'s own comment is# that two instances of one model behave closer to one reviewer than to two,# and that is the check the merge path rests on.FALLBACK_MODEL=os.environ.get("FR_MODEL_FALLBACK", "claude-opus-4-8")
In resolve(), return it when standing in for codex and not escalating:
ifagent=="codex": # reached only when codex_ok() is Falsereturn"claude", FALLBACK_MODEL
Keep the escalate branch ahead of it — escalation is a deliberate reach for
the strongest model and must not be downgraded.
disable_codex() (orchestrator/loop.py:216-228) already records review_diversity_lost. Add the model actually substituted to that record — record("review_diversity_lost", reason=reason, fallback=FALLBACK_MODEL) —
so a later retrospective can tell "lost vendor diversity, kept model
diversity" from "lost both".
Acceptance criteria
A test in orchestrator/check_orchestrator.py that pins the invariant
directly: with _codex_disabled = True, resolve("claude", "reviewer") and resolve("codex", "reviewer") return different model strings. It must
fail if either the constant is set equal to MODELS["reviewer"] or the new
branch is deleted.
A test that resolve("codex", "reviewer", escalate=True) still returns ESCALATED_MODEL — escalation outranks the fallback.
bash scripts/gate.sh bootstrap passes, including orchestrator-runnable.
Changing REVIEWER_AGENTS, the number of reviewers, or the review verdict
parsing.
Rewriting the 65 open issue bodies that say Agent: codex. The fallback
already routes them correctly; only the reviewer slot is losing something
real. Do not touch other issues' bodies.
Any model choice for the implementer, critic, fixer, planner, resolver or
unblocker roles. Reviewer independence is the property under repair.
Gate: bootstrap
Agent: claude
Found by retrospective 34 (
orchestrator/logs/retro-34.md, Finding 1).This is not #126. #126 asks for the quota latch to be re-probeable and
expiring, on the evidence that a previous firing was transient. Today's firing
is not transient and a re-probe would correctly re-latch. This issue is about
what the loop should do while the latch is right.
Evidence
codex hit a real quota wall at
2026-08-15T09:17:00Z, on#13'simpl.1.orchestrator/logs/13/codex.impl.1.logends:Five days out.
review_diversity_losthas fired four times over the run(
2026-08-13T02:31:03Z,2026-08-14T10:08:35Z,2026-08-14T19:06:57Z,2026-08-15T09:17:00Z).REVIEWER_AGENTS = {1: "claude", 2: "codex"}(orchestrator/loop.py:990).Once
codex_ok()is false,resolve()(orchestrator/loop.py:232-237) sendsslot 2 down the identical final line as slot 1:
MODELS["reviewer"]isclaude-opus-5andESCALATED_MODELisclaude-opus-5, so there is no code path that gives the two reviewersdifferent weights.
review_stage()states the cost itself(
orchestrator/loop.py:1271-1273):Every issue merged since the latch armed was reviewed by two
claude-opus-5instances. Verified on disk, not inferred:
Blast radius on the queue, not just the reviewers
65 of 137 open issues (47%) name an agent that cannot run for the rest of the
run. And because
resolve("codex", role)andresolve("claude", role)nowreturn the same
("claude", MODELS[role])pair,Agent: codexandAgent: claudeare indistinguishable — the planner's routing decision, theretrospective's routing decision, and the whole
duelalternation carry zeroinformation while the latch is armed. Nothing says so anywhere; the issue bodies
still read as if a vendor was chosen.
What to change
orchestrator/loop.pyonly. One behaviour: when the codex slot falls back, giveit a different model rather than a second copy of reviewer 1.
Add a constant next to
MODELS(orchestrator/loop.py:99-107):In
resolve(), return it when standing in for codex and not escalating:Keep the
escalatebranch ahead of it — escalation is a deliberate reach forthe strongest model and must not be downgraded.
disable_codex()(orchestrator/loop.py:216-228) already recordsreview_diversity_lost. Add the model actually substituted to that record —record("review_diversity_lost", reason=reason, fallback=FALLBACK_MODEL)—so a later retrospective can tell "lost vendor diversity, kept model
diversity" from "lost both".
Acceptance criteria
orchestrator/check_orchestrator.pythat pins the invariantdirectly: with
_codex_disabled = True,resolve("claude", "reviewer")andresolve("codex", "reviewer")return different model strings. It mustfail if either the constant is set equal to
MODELS["reviewer"]or the newbranch is deleted.
resolve("codex", "reviewer", escalate=True)still returnsESCALATED_MODEL— escalation outranks the fallback.bash scripts/gate.sh bootstrappasses, includingorchestrator-runnable.Out of scope
duelsilently collapsed #126, and it is not thefix here: codex is genuinely walled until 2026-08-20.
REVIEWER_AGENTS, the number of reviewers, or the review verdictparsing.
Agent: codex. The fallbackalready routes them correctly; only the reviewer slot is losing something
real. Do not touch other issues' bodies.
unblocker roles. Reviewer independence is the property under repair.