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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
162 changes: 162 additions & 0 deletions devlog/_plan/260905_unified_quota_activation/010_unified_control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Unified quota-window activation

## Loop specification

C2, one spec-satisfaction PABCD work-phase (`wp1`). Trigger: owner requested removing
the quota activation row from every account card, one all-account/both-window toggle
inside Advanced settings, and a PR with admin merge. DONE is verified UI and merged
dev ancestry. No backend/API contract, credential, worker, real account settings,
release, or service changes. Use existing GitHub credentials only for this repository;
isolated synthetic browser fixture; no paid probes or purchased credits; no token
budget specified; three-hour wall-clock reassessment. Upward escalation: main reclaims
after two distinct failed audit dispatches; downward delegation is read-only audit
and verification only. Memory: this document and the session-bound goalplan.

## Design read and necessity

Existing quiet React/Vite developer dashboard with translated copy and native `.toggle`
buttons. Preserve fonts, colors, cards and collapse mechanism; variance 2, motion 1,
density D5. No generated concepts/assets: this is an existing utility settings screen.
Do nothing leaves repetition; deleting the feature loses requested control; configuring
alone cannot change the UI. Reuse `CodexAuthAdvancedSettings`, settings API, quota
availability, existing toggle/card/feedback CSS. New API or global policy is unnecessary.
The action covers currently listed main/added accounts, not future-account inheritance.

## Concrete changes

- MODIFY `gui/src/components/CodexAccountPool.tsx`: remove card toggle props and
display-only per-account quota settings merge. Replace per-account write handler
with one all-account operation using the existing `{id, window, enabled}` PUT
shape. Keep shared settings GET for Spark and quota state; block unknown state,
provide retry after read failure, synchronous duplicate-click guard, serialize
per-window writes and reconcile settings with GET after success or failure.
Retain failed batch target (ON or OFF) and expose explicit Retry that recomputes
remaining granular changes toward that same target; a partial OFF retry must never
send ON. Failed/uncertain writes never show success; failed reconciliation leaves disabled
unknown state with retry. Keep busy state until reconciliation finishes. Ignore
stale reads using the mutation revision; abort on unmount/apiBase change and stop
the remaining batch. Reuse `createBoundedFetch` for deadlines.
- NEW `gui/src/components/CodexQuotaAutoRefreshSetting.tsx`: small stateless setting
card inside Advanced. One button, `aria-pressed` false/true/mixed, description that
names all current accounts and both supported windows, actual warmup spending and
pool-mode scope. Disabled while accounts/settings unknown, saving, or no eligible
window and no enabled stale setting; show loading/error/empty/mixed feedback.
Derived window descriptors reuse `quotaAutoRefreshAvailability` (legacy fallback)
and authoritative account availability. Aggregate on iff all available windows
enabled (or only stale enabled settings remain, so OFF stays reachable); mixed
resolves toward ON. OFF clears enabled fiveHour/weekly settings even unavailable.
Preserve server completion markers by issuing only existing granular mutations.
- MODIFY `gui/src/components/codex-account-pool-cards.tsx` and
`gui/src/components/codex-account-pool-main-card.tsx`: remove rendered activation
rows, their now-unused props/import, and the unreferenced controls function after
checking all callers. Account data contract stays intact.
- MODIFY `gui/src/styles.css`: replace dead per-account row styles with minimal
unified setting layout reusing adjacent settings conventions, with mobile wrapping.
- MODIFY all `gui/src/i18n/{locale}.ts`: new unified description, mixed, empty,
loading-failure/partial-failure labels; retain keys still in use.
- MODIFY `gui/tests/codex-account-pool-toast-tone.test.tsx`: integration regressions
using injected controller and mocked API: hidden advanced/no card rows; main +
weekly-only + 5h/weekly + unsupported account coverage; on/off payloads; mixed;
settings load failure/retry; blocked busy double-click; partial PUT failure and
reconciliation failure/retry; stale initial response; legacy data compatibility.
Update `gui/tests/main-account-hard-lock-setting.test.tsx` props only where removed.
- MODIFY `docs-site/src/content/docs/getting-started/how-it-works.mdx`,
`docs-site/src/content/docs/reference/configuration/providers.md`, and
`structure/08_openai-provider-tiers.md`: replace per-card UI instructions with the
advanced bulk control, current supported windows, non-atomic batch/failure behavior.
No new serialized fields/enums: creation/serialization/deserialization unchanged;
existing PUT and GET consumers verified in `config-routes.ts:118,461,551`.

## Verification and boundaries

Baseline attempts `bun test tests/gui/quota-bars-rows.test.ts` and GUI focused tests
found missing dependencies in the fresh worktree; install frozen lockfiles before
rerunning. Direct file arguments prove target coverage. Run existing focused files
before and after; add one failing regression before production changes. Runtime quota
tests cover existing settings behavior; no changed runtime code.

PR-ready commands: root `bun run typecheck`, `bun run test`, `bun run privacy:scan`;
GUI `bun test tests`, `bun run lint`, `bun run lint:i18n`, `bun run build`; docs
`bun install --frozen-lockfile && bun run build`. Script definitions inspected in
package manifests; new commands are pending execution, not claimed as passed.
Do not rerun passing checks against unchanged code. Native browser screenshot and
keyboard click-through use synthetic data on a separate localhost port (not 10100).
Independent read-only A and C audits. One cohesive PR (not a stack); screenshot is
privacy-safe and committed under `.github/pr-assets/`. Fill the repository PR template,
record the owner-authorized admin bypass, verify exact head CI and findings, then
merge with head-match guard and prove `merge-base --is-ancestor` on fetched dev.
No enforcement is introduced; all existing auth/worker gates remain authoritative.

## Acceptance activation matrix

| Trigger | Observable result |
| --- | --- |
| Default collapsed Advanced | No per-account activation row; no visible bulk toggle |
| Expand with all off | One accessible control; click enables supported windows for all current accounts |
| All enabled | Click clears fiveHour and weekly flags, including stale unavailable ones |
| Partial existing state | `aria-pressed=mixed`; click enables remaining supported windows |
| No windows/empty account set | Disabled control with explanation, no PUT |
| Delayed/failed GET | No guessed pressed state; retry reloads confirmed settings |
| Double click during delayed PUT | One batch only; busy holds through reconciliation |
| One PUT fails | No success claim; GET reveals actual partial state; explicit Retry retains original ON/OFF intent |
| Reconciliation fails | Unknown disabled state, explicit error and retry |
| Old apiBase GET resolves after switching proxy | Scope/revision guard prevents old state or old remaining writes |
| Legacy account payload | Availability fallback works; unrelated selection-order control still works |

## Evidence

Baseline after frozen installs: 13 root quota-row tests and 30 GUI tests passed.

A1 synthesis: accepted reviewer blocker: mixed defaults ON, so partial OFF must not
use the normal toggle as retry. Root cause was conflating aggregate display with
operation intent. No conflicting requirements; add retained failed target and an
explicit retry path (including a no-ON-writes partial-OFF regression). Initial GET
cannot race a mutation while the control is unknown/disabled, so replace that
unreachable row with old-apiBase GET / in-flight batch cancellation scenarios.

B verification: failing regression first proved per-card row remained, then passed
with the unified setting. Nine new integration cases plus eight original tests pass.
Lint required extracting shared pure data functions to `gui/src/codex-quota-activation.ts`
(component-only fast refresh) and keying settings snapshots by proxy/read revision
instead of resetting React state inside an effect. No runtime/API shape changes.
The existing large pool component remains the lifecycle owner; no unrelated split.
The single PR keeps the tightly coupled UI, regressions, all locales, and docs together.

C1 synthesis: accepted reviewer stale-proxy-incarnation blocker. An A/B/A return
could match an old A snapshot before the fresh read, and failed reads lacked the
mutation revision guard. Invalidate the snapshot at the API prop boundary (guarded
React state adjustment), advance its read revision, and guard errors like successes.
Add A/B/A pending+failed GET followed by OFF-batch coverage. Existing auto-switch
controller tests also need accurate settings GET fixtures and selectors scoped to
their own `.codex-auto-switch-card`; no assertions or behavior coverage removed.

Hosted React Doctor reported only `async-await-in-loop` on the intentional settings
write sequence. Classified false positive: unlike independent reads, writes must not
dispatch the rest of a billable opt-in batch before cancellation. The deferred-write
test proves that switching proxy prevents all unsent writes. Use the existing narrow
documented suppression convention from `IntegrationsOverview.tsx:398`, not a global
rule/config change or a parallel rewrite. No runtime behavior changes in this repair.

Verified implementation: GUI full suite 1453 pass / 0 fail; focused 49 pass; import-
connected root selection 110 pass / 0 fail; root typecheck, GUI lint/i18n/build,
privacy scan and 425-page docs build passed. Root full suite was interrupted with
exit 143 and is not claimed green; exact-head hosted runtime CI is the landing gate.
Independent reviewer closed partial-OFF and A/B/A findings with PASS. Browser drove
the actual pool component with synthetic data (no live credentials/upstream), including
ON, keyboard OFF, partial failure/retry, mixed and empty states. CSS widths 320,
390, 768, approximately 1024 and 1440 show no horizontal overflow or clipped setting
copy. Light/dark screenshots checked; port 10191 and temporary browser tab torn down.
Delivery PR: #3662; administrative approval bypass explicitly authorized by the owner
and recorded on the PR. No service restart or release belongs to this unit.

C2 synthesis (hosted Codex review): accepted P2 on transiently unavailable windows.
ON must skip unavailable windows entirely, preserving previously opted-in flags;
only explicit OFF clears those flags. The final readback verifies only the targeted
available windows for ON, but every window for OFF. Updated the mixed-state regression
to preserve the unavailable opt-in and then prove explicit OFF clears it. This corrects
the earlier stale-cleanup interpretation without changing the API or worker.

C3 copy-only review closure: French now names each account's supported windows and
uses the existing Mode Groupe label; traditional and simplified Chinese explicitly
say each account's own supported windows. This avoids an intersection-of-all-accounts
reading. No behavior or Korean layout changes; validate locale lint and GUI build.
9 changes: 7 additions & 2 deletions docs-site/src/content/docs/getting-started/how-it-works.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,13 @@ account before the request is forwarded upstream. The rule is intentionally spli
- **Quota and failure signals feed routing.** The dashboard can force a quota refresh with
`GET /api/codex-auth/accounts?refresh=1`; successful upstream responses capture quota headers,
429 puts an account in cooldown, and 401/403 marks it for reauthentication.
- **Idle rolling windows can be activated on time.** Each account card offers default-off 5-hour
and weekly switches only for windows that account actually reports. At reset, opencodex reuses
- **Idle rolling windows can be activated on time.** Under **Advanced settings**, one default-off
automatic activation control switches the supported 5-hour and weekly windows for all current
main and added accounts together. Mixed settings are shown explicitly; enabling applies only
to reported windows, and disabling also clears stale enabled windows. Changes use individual
settings writes: a partial failure is shown after reading back the saved state, and **Retry**
completes the original enable or disable action. Newly added accounts are not opted in automatically.
In Pool mode, at reset, opencodex reuses
its minimal non-stored account warmup request through the exact account whose window is due,
coalesces simultaneous windows into one request, and durably persists both reset timestamps to
prevent duplicate work after restarts. Paused accounts and accounts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ After GUI registration or OAuth login, the confirmation dialog lets you open the
| `contextCapValue?` | `number` | `350000` | Default value used by the dashboard context-cap controls. Changing it applies the value to every routed provider — including providers without an existing `providerContextCaps` entry — only when "apply to every routed provider" is toggled on; otherwise each provider keeps its own cap. |
| `codexAccounts?` | `CodexAccount[]` | `[]` | ChatGPT/Codex pool account metadata managed by Codex Auth. Secrets live separately in `codex-accounts.json`. |
| `pausedCodexAccountIds?` | `string[]` | `[]` | Accounts excluded from Pool selection until resumed, including the main `__main__` account when paused. |
| `codexQuotaAutoRefresh?` | `Record<string, object>` | `{}` | Per-Codex-login-account opt-in for automatic `fiveHour` and `weekly` window activation in Pool mode, which selects among the main and added accounts; Direct mode uses only the current account and does not run this pool worker. The setting is actionable only when the account's live WHAM payload reports the selected window; absent windows have no dashboard control, and API writes attempting to enable an unavailable window return HTTP 409 (disable writes are accepted so stale settings can be cleared). The Providers/Codex Auth account-pool UI and `/api/settings` manage this field without replacing unrelated settings. At a reported reset time, opencodex sends one minimal non-stored Codex message through that account and persists the activated reset timestamp. This does not apply to API-key providers. |
| `codexQuotaAutoRefresh?` | `Record<string, object>` | `{}` | Per-Codex-login-account opt-in for automatic `fiveHour` and `weekly` window activation in Pool mode; Direct mode does not run this worker. In Providers/Codex Auth **Advanced settings**, one control enables or disables both supported windows across all current main and added accounts. New accounts are not opted in automatically. Enable skips windows absent from live WHAM data; disable also clears stale enabled windows. The UI reuses granular `/api/settings` writes, reconciles partial failures, and retries the original ON/OFF intent without replacing unrelated settings or completed reset markers. The API still rejects enabling an unavailable window with HTTP 409. At a reported reset time, opencodex sends one minimal non-stored Codex message using that account's quota and persists the activated timestamp. This does not apply to API-key providers. |
| `codexAccountNamespaces?` | `Record<string, string>` | — | Optional map from an arbitrary public model selector to a stored Codex account target. When account-qualified picker rows are enabled, each selector whose target is present adds separate `<selector>/<native-openai-model>` rows to the Codex picker; each row uses only that account. With any selector active, bare native rows are hidden in the picker, but their ids remain routable and listed by raw `/v1/models` unless explicitly disabled. |
| `codexAccountPickerEnabled?` | `boolean` | off when the map is empty | Controls whether eligible `codexAccountNamespaces` mappings generate account-qualified Codex picker rows. `true` allows mapped rows to appear. If omitted with a non-empty map, it is treated as enabled for backward compatibility; if the map is empty, it is off. `false` hides generated rows and restores bare native picker rows without deleting mappings or disabling exact `<selector>/<native-openai-model>` routing. |
| `activeCodexAccountId?` | `string` | — | Manually selected Pool account for the next request. Selection clears thread affinity; in-flight requests keep captured credentials. |
Expand Down
27 changes: 27 additions & 0 deletions gui/src/codex-quota-activation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { quotaAutoRefreshAvailability } from "./codex-quota-utils";
import type { CodexAccountEntry } from "./hooks/useCodexAccountPool";

export type QuotaAutoRefreshSettings = Record<string, { fiveHour?: boolean; weekly?: boolean }>;

export function readQuotaActivationSettings(payload: unknown): QuotaAutoRefreshSettings {
const settings = payload && typeof payload === "object" && "codexQuotaAutoRefresh" in payload
? payload.codexQuotaAutoRefresh : null;
if (!settings || typeof settings !== "object" || Array.isArray(settings)
|| Object.values(settings).some(value => !value || typeof value !== "object" || Array.isArray(value)
|| [value.fiveHour, value.weekly].some(flag => flag !== undefined && typeof flag !== "boolean"))) {
throw new Error("Invalid quota activation settings");
}
return settings as QuotaAutoRefreshSettings;
}

export function quotaActivationWindows(accounts: CodexAccountEntry[], settings: QuotaAutoRefreshSettings) {
return accounts.flatMap(account => {
const id = account.isMain ? "__main__" : account.id;
const available = account.quotaAutoRefresh ?? quotaAutoRefreshAvailability(account.quota);
return (["fiveHour", "weekly"] as const).map(window => ({
id, window,
available: available[window === "fiveHour" ? "fiveHourAvailable" : "weeklyAvailable"],
enabled: settings[id]?.[window] === true,
}));
});
}
Loading
Loading