Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions devlog/_plan/260826_quota_window_and_backlog/000_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# 000 — quota_window_and_backlog: Plan

## Objective

Codex removed the 5-hour rate-limit window some time ago and has now re-introduced it for
**Plus and Team**, while **Pro stays weekly-only**. OpenCodex has two quota parsers and only one
of them learned the lesson. Display and pool routing are both wrong for the affected plans.

Additionally: hide the Codex Spark window by default behind an operator switch, land three
quick wins, and close the backlog items that are already terminal.

## The observed failure (proven live, not inferred)

Running both parsers against the SAME upstream data on `dev` at `0a0a8821b`:

```
headers {primary 97% / 300 min, secondary 12% / 10080 min}
parseUpstreamQuotaHeaders -> {"weeklyPercent":97,"weeklyResetAt":...}
parseUsageQuota (WHAM) -> {"shortPercent":97,"shortWindowSeconds":18000,"weeklyPercent":12}
```

The header parser has only a monthly-vs-else branch
([quota.ts:344](../../../src/codex/quota.ts)), so **anything that is not explicitly monthly
becomes weekly** — including a 5-hour burst window. The WHAM parser classifies by duration
([quota.ts:205](../../../src/codex/quota.ts), `isExplicitShortWindow`) and gets it right.

Three consequences, in increasing order of damage:

1. The genuine weekly reading (12%) is **discarded** — `weeklyPercent` is overwritten by the
burst value before the secondary is ever consulted.
2. A 5h-exhausted account records `weeklyPercent: 100`. `isCodexQuotaExhausted` returns true,
which is the right answer for the wrong reason — and it **stays** true after the 5-hour
window resets, because nothing re-derives it until a WHAM refresh lands. Pool routing keeps
avoiding a healthy account.
3. The GUI shows a weekly bar at 100% and **no 5h bar at all**, so the operator cannot tell
which limit they actually hit.

The comment directly above the call site records the now-stale premise:
*"primary was the 5h window; it now carries weekly data for GPT plans"*
([core.ts:3777](../../../src/server/responses/core.ts)). That was true while the 5h window was
gone. It is not true now.

Corroborating evidence that this is a parser gap rather than a missing feature:
`tests/ws-endpoint.test.ts:287` already carries a `"x-codex-primary-window-minutes": "15"`
fixture — a 15-minute window — and nothing in the suite classifies it as short.

## Loop-spec

- **Loop archetype:** verifier-defined repair (wp1, wp3-wp5), judged design (wp2), evidence
closure (wp6-wp7).
- **Trigger:** owner report that Codex restored the 5h limit for Plus and Team.
- **Write scope:** `src/codex/quota.ts`, `src/providers/registry.ts`,
`src/providers/quota.ts`, `src/config.ts`, `src/types/`, `gui/`, `docs-site/`, `tests/`,
`devlog/`.
- **Out of scope:** npm publish, tag push, main/preview promotion, security pre-disclosure
notes in devlog, rewriting the WHAM parser (it is correct — the header parser converges on
it, not the other way round).
- **Verifier:** focused `bun test` per phase; `bun run typecheck` + `bun run test` before each
merge; `cd gui && bun test` for GUI phases.
- **Stop condition:** seven work-phases merged to dev, every named issue/PR terminal, dev HEAD
green.
- **Bounds:** commits are `--no-verify`; CI is fixed at the end; admin squash-merge per phase.

## Work-phase map (one phase = one full PABCD cycle)

| WP | Doc | Slice | Depends on |
|----|-----|-------|------------|
| wp1 | 010 | Header parser learns the duration rule; Plus/Team get a 5h bar, Pro unchanged | — |
| wp2 | 020 | Spark hidden by default + Codex Auth switch | — |
| wp3 | 030 | #2406 CommandCode image capabilities | — |
| wp4 | 040 | #1215 OpenCodex-scoped noProxy | — |
| wp5 | 050 | #1060 subscription billing-period date | — |
| wp6 | 060 | Evidence-backed closures (#2442 #2423 #2060, PR #1769 #2215) | — |
| wp7 | 070 | Backlog triage devlog | wp6 (records what wp6 closed) |

wp1 is sequenced before wp2 for review clarity, not as a data dependency (audit finding 7):
both touch the quota display contract, and hiding one row is easier to review once the
neighbouring 5h/weekly rows are correct. Neither consumes the other's output. wp3-wp5 are independent and could run in any
order; they are sequenced by ascending blast radius. wp7 is last because it records wp6's
outcome.

## Accept criteria

Mirrored into the goalplan `criteria[]` — see `.codexclaw/goalplans/opencodex-quota-window-backlog-cleanup-loop-2608/goalplan.json`.

The load-bearing one is wp1's: **given identical upstream data, the two parsers must agree**.
That is a property, not an example, and it is the assertion that would have caught this defect
when the 5h window first disappeared.

103 changes: 103 additions & 0 deletions devlog/_plan/260826_quota_window_and_backlog/001_audit_response.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 001 — audit response: the roadmap was locally right and globally incomplete

An independent read-only auditor returned **FAIL** with five blocking findings. I verified each
against the tree. **All five hold.** One of them is the kind that turns a fix into a regression,
so it is worth stating plainly rather than burying in a table.

## B1 — the parser fix alone would have BROKEN routing. ACCEPTED, and it is the important one.

`src/routing/quota.ts:37` computes routing-profile headroom from `weeklyPercent` and
`monthlyPercent` — and **not** `shortPercent`. That omission is invisible today precisely
because the header parser is broken: the 5h value is being written into `weeklyPercent`, so
routing accidentally sees it.

Measured on the live module:

```
headroom BEFORE the wp1 fix : 0.03 (reads 97% used — accidentally correct)
headroom AFTER the wp1 fix : 0.88 (reads 12% used — WRONG, burst is at 97%)
```

Fixing the parser without fixing `routing/quota.ts` would take a 5-hour-exhausted account from
"3% headroom" to "88% headroom" and route traffic straight into a 429. The bug is currently
cancelling itself out, and the roadmap's "only the parser changes" claim would have removed one
half of the cancellation.

Note the asymmetry that made this easy to miss: `computeCodexUsageScore` in
`src/codex/routing.ts:339` **does** fold in `shortPercent`, and that is the file I read.
`src/routing/quota.ts` is a different module with a similar name and a different rule.

**Fold:** wp1 gains `src/routing/quota.ts` — add `shortPercent` to the percent set and
`shortResetAt` to the reset set — plus a regression asserting headroom stays low when only the
burst window is exhausted.

## B2 — wp2's server-filter rationale named the wrong surface. ACCEPTED with a correction.

I justified server-side filtering by claiming `maxQuotaUtilisation` would reorder Codex account
cards. The auditor checked: `maxQuotaUtilisation` sorts the **Providers overview**
(`ProviderOverviewDashboard.tsx:66`), not the Codex Auth cards. My stated reason was wrong.

The conclusion survives on a better reason. Spark reaches the GUI through **two** independent
projections — `/api/codex-auth/accounts` and `/api/provider-quotas` (the latter via
`listCodexAuthAccountsSnapshot`, `providers/quota.ts:1129`). Filtering one leaves the other
showing the row the operator switched off.

**Fold:** wp2 filters at a shared projection covering both surfaces, and tests both. The
label-exact requirement stands and is now better supported: `customWindows` carries Cursor's
`First-party models`/`API usage`, Anthropic's `Fable`/`Opus`/`Sonnet`, Antigravity's
`Gem`/`Cla`, Kimi's `Total subscription credits` and a dozen dynamic provider labels. A
"drop custom windows" filter would blank all of them.

## B3 — wp4's own dedupe criterion would have failed. ACCEPTED.

`applyProxyEnv` builds `seen` from **lowercased** entries (`config.ts:3122`) but my proposed
loop pushed configured entries without normalizing, so a configured `LOCALHOST` would be
followed by `localhost`. The plan's own accept-criteria row would have failed the plan's own
code.

Also accepted: #1215 asks for `string[]`; I specified a comma-separated `string` without
recording the deviation. **Decision, now recorded:** accept `string | string[]` and normalize.
The string form matches `NO_PROXY` syntax the operator already knows and matches the sibling
`proxy` field; the array form is what the issue asked for and is unambiguous about separators.
Supporting both costs one `Array.isArray` branch.

## B4 — wp5 was written against a UI that does not exist. ACCEPTED.

I claimed the GUI "drops `expiresAt`". It drops the **entire `creditsUsd` object**
(`report.ts:42`), and `AccountQuota` has no credits contract at all. There is no credits
figure to render a date beneath, and `gui/tests/provider-report.test.ts` — which I cited as the
test location — does not exist.

**Fold:** wp5 projects the whole typed `creditsUsd` shape and creates the presentation, which
makes it the largest of the three quick wins rather than the smallest. Label corrected to
**"Billing period ends"**: the source is `subscription.currentPeriodEnd`, and "Renews" asserts
a continuation the field does not promise.

## B5 — acceptance evidence gaps. ACCEPTED.

- The wp1 "property" test was three fixed examples. Add the **24-hour boundary**: 1439 minutes
is short, 1440 is not — strict `<` in both predicates, verified at `quota.ts:211`.
- wp3's negative table used short names; upstream ids are `deepseek/deepseek-v4-flash`,
`deepseek/deepseek-v4-pro`, `zai-org/GLM-5.2`, `zai-org/GLM-5.3`, `xai/grok-4.6`. A
shortened name asserts absence of something that was never present — vacuously green.
- Goalplan criteria carry no `expectedEvidence` and no work-phase mapping. Fill both.

## B6 (finding 7) — phase ordering. PARTIALLY REBUTTED.

The auditor is right that wp1→wp2 is not a data dependency and that wp3-wp5 are independent.
I accept the correction and have removed the dependency claim from wp1→wp2.

Where I do not fully agree: PHASE-SPLIT-01 forbids ordering by **effort or payoff speed**, not
ordering independent slices at all. wp3-wp5 have no edges between them, so *some* order must be
chosen; ascending blast radius (static data → config plumbing → GUI surface) is a risk ordering,
not a quick-win-first ordering. B4 makes this concrete: wp5 turned out to be the largest of the
three, and it stays last — an effort ordering would now move it first.

## Net effect

Five folds, one partial rebuttal. The scope grows in two places that matter: wp1 gains a second
file without which it would regress routing, and wp5 roughly doubles. The roadmap docs are
amended in place; this document records why.

VERDICT accepted: **near-pass with five folded blockers**. Proceeding to B.

Loading
Loading