Skip to content

feat: add foreground quota-based automatic account switching - #175

Closed
cskwork wants to merge 2 commits into
Loongphy:mainfrom
cskwork:feat/auto-polling
Closed

cskwork wants to merge 2 commits into
Loongphy:mainfrom
cskwork:feat/auto-polling

Conversation

@cskwork

@cskwork cskwork commented Oct 24, 2026 •

Copy link
Copy Markdown

Adds an explicit foreground codex-auth auto command that polls usage and switches stored credentials when the active account approaches a rate limit. For example, codex-auth auto --threshold 90 --interval 60 selects the healthy account with the most remaining quota across its reported windows. The default activation cooldown is 300 seconds.

Supports --once, --dry-run, --json, and configurable threshold, interval, and cooldown. Switching requires fresh successful API responses; stale data, invalid or expired windows, failed accounts, and API-key accounts are excluded. An advisory lock prevents competing auto monitors. Dry-run leaves registry and credential files unchanged, including refusing registry migrations.

This is a foreground command, consistent with the v0.3 removal of background services. It installs no services and never restarts clients. Standard Codex clients may still need restarting after a credential switch. Existing manual-switch commands remain unchanged. Unlike #148's one-shot selector, this adds continuous threshold monitoring; #42 targets the earlier background-policy architecture.

Validation on macOS with Zig 0.16.0:

  • zig build test -j4 --summary all: 474 passed, 8 skipped, no failures.
  • Required zig build run -- list with isolated HOME and CODEX_HOME.
  • Compiled CLI smoke run using dummy credentials and a local curl fixture: dry-run file preservation, credential and registry read-back, two polling iterations, competing-monitor rejection, Ctrl-C lock release, API failure, migration refusal, and argument validation.
  • Regression reproduced and fixed redirected stdout overwriting prior poll events; a ReleaseSafe binary retains two separate JSON events in a regular file.
  • zig fmt --check for changed Zig files and git diff --check passed.

The documentation's no-inline-production-tests check already finds three inline tests in unchanged src/cli/live_tui.zig and src/tui/text_width.zig; this PR adds none. Real account quota changes and client restart behavior were not exercised. Windows and Linux runtime validation is left to upstream CI.

@Loongphy

Copy link
Copy Markdown
Owner

Thanks for the careful work here. The core instincts are right: only a successful response from the current poll may justify a mutation, expired windows and API-key accounts are excluded, no switch happens unless auth.json matches the registry, the registry is reloaded and re-checked before writing, --dry-run leaves files untouched, and the docs/help/README wiring is complete.

Closing this PR — not because automatic switching is unwanted, but because the shape is wrong.

1. Automatic switching belongs in --live, which already solves the hard half of this problem.

list --live / switch --live already do periodic quota refresh: a refresh timer, asynchronous refresh rounds, fresh-vs-cached rendering, and they already reach the same account-activation path (activateAccountByKey + registry save + previous-account tracking + auth backup) that this PR reimplements.

auto also blocks the foreground, must be kept alive, and stops on Ctrl-C — the process model is identical to --live. The only new capability is that nobody has to press Enter. That is a mode of the live view, not a new top-level command, and shipping both leaves two independent poll → select → switch pipelines plus a second "best remaining account" ranking next to the one already used when the active account is removed. Those will drift apart.

2. The threshold and selection rules need a design pass before they get code.

This branch hardcodes one trigger (max(all windows) >= threshold) and one selection rule (least-used wins). We already have requests for different rules — #143 wants the account whose weekly window resets soonest, #148 wants the earliest 5h reset — so trigger and selection have to become named, reusable policies rather than arithmetic inside a command.

Things that also need deciding, and which this shape hides:

  • A percentage-only trigger is blind in exactly the case the feature exists for: when an account really is rate limited, the usage endpoint commonly returns 429, and a rule that only trusts HTTP 200 reports blocked and does nothing.
  • Per-window thresholds vs one combined threshold, cooldown semantics, and what to do when every account is at or over the threshold.
  • Unattended account churn is not risk-free (Allowance percentages seem wrong and my accounts got suspended #121: accounts permanently suspended), so decisions must be observable and rate-limited, not just correct.
  • Switching auth.json does not reach a running client or the shared app-server daemon (switch on remote server does not take effect using codex app until remote connection is restarted #85). An unattended switch can therefore silently fail to relieve the limit; the UI has to say that out loud at switch time.
  • Machine-readable output in a loop conflicts with docs/json-api.md ("stdout contains exactly one JSON document"): either restrict --json to --once, or document the stream as an explicit exception.

Design questions and the direction going forward are collected in #176 — automatic mode inside --live, plus a separate one-shot selector for cron/GUI callers.

Two things worth carrying over as their own PRs:

  • src/core/io_util.zig: stdout must use a streaming writer. That is a genuine cross-command fix (redirecting or appending any command's output to a regular file), not an auto-specific one, and it deserves its own change plus an audit of the remaining .writer() sites in src/workflows/app.zig, src/workflows/clean.zig, and src/cli/tui.zig.
  • The shell-script fake curl fixture pattern: prefer the existing fake-curl helpers so the fixture does not depend on platform-specific exec semantics.

For reference when reworking the tests: the new integration test fails on Linux CI, and it is not caused by the branch being one commit behind main. tests/cli_integration_test.zig opens the fake curl script with .read_write and keeps that handle open via defer curl.close() until the test ends; on Linux execve on a file still open for writing fails with ETXTBSY, which surfaces through http_curl.zig as a generic RequestFailed, so the poll evaluates to blocked and the would_switch assertion never holds. macOS has no ETXTBSY semantics, which is why it is green locally. Wrapping the open/chmod in its own scope so the handle is closed before invoking the CLI makes the full suite pass: 477/482 tests, 5 skipped, no failures.

I would genuinely like the fresh-data-only guard and the pre-write re-check from this branch to land in the --live auto mode — that part is stricter and better than what we have today. If you want to take #176, say so and we can scope the first step together.

@Loongphy Loongphy closed this Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants