This document describes the behavior currently implemented by
codex-quota-linux. It is a maintainer reference, not a roadmap. User commands
stay in the README; proposed behavior should stay in issues or
pull requests until it is implemented.
flowchart LR
UI["Tray and CLI"] --> APP["app.py orchestration"]
APP --> CACHE["Project-local caches"]
APP --> DIRECT["Authenticated direct HTTPS clients"]
DIRECT -->|"success"| CACHE
DIRECT -->|"auth or schema failure"| SERVER["Codex app-server"]
SERVER --> HOME["Isolated temporary CODEX_HOME"]
SERVER -->|"parsed fallback"| CACHE
CACHE --> FORMAT["Quota and reset-credit formatters"]
FORMAT --> UI
The application keeps saved accounts and caches under .runtime/. Query code
never switches the main Codex login. Only the explicit switch action writes
the main Codex auth.json.
| Module | Responsibility |
|---|---|
cli.py |
Command parsing and terminal output |
indicator.py |
GTK/AppIndicator state, timers, menus, and background workers |
app.py |
Account discovery, query orchestration, caching, and fallback policy |
client.py |
Direct HTTPS clients and Codex app-server JSON-RPC transport |
quota.py |
Quota parsing, cache model, deadlines, labels, and icon status |
reset_credits.py |
Reset-credit parsing, cache model, and display rows |
auth_store.py |
Isolated login and account-slot creation |
switcher.py |
Explicit main-auth replacement |
activation.py |
Isolated rolling-window activation requests |
runtime.py, auth_sync.py |
Permissions, locks, temporary homes, atomic writes, and safe auth copy-back |
Both query features use the same policy: prefer the faster direct endpoint and use app-server only when direct authentication or response compatibility is the problem.
| Resource | Primary path | Direct auth/schema failure | Direct transient failure | App-server call failure |
|---|---|---|---|---|
| Quota usage | Direct usage endpoint | Parse app-server rate limits | Keep cached quota with an error | Keep cached quota with the original direct error |
| Reset credits | Direct reset-credit endpoint | Parse app-server rateLimitResetCredits |
Keep cached reset credits with an error | Keep cached reset credits with the original direct error |
A direct authentication failure first gets one bounded local recovery attempt.
The query compares the main Codex auth.json with the account slot under the
runtime lock. It copies the main credential into the slot only when the
credentials belong to the same account and the main credential is newer, then
retries the direct request once. If no safe copy is possible or the retry still
fails authentication, the existing app-server fallback runs. Schema and
transient failures do not trigger this main-auth recovery path.
A transient timeout, TLS failure, HTTP 408/429, or server-side 5xx response does not start app-server. It is usually faster and more stable to preserve the cache and try the direct endpoint again on a later refresh.
app.py uses one helper for quota and reset-credit fallbacks:
- Create an isolated temporary Codex home containing a copy of the account
slot's
auth.json. - Start
codex app-server --stdiowith bothCODEX_HOMEandCODEX_SQLITE_HOMEpointing to that home. - Complete the standard handshake:
initializerequest, initialize response,initializednotification, then the business request. - Parse the business response for the requested resource.
- Copy refreshed authentication back only if it is valid, belongs to the same account, and passes the freshness rules below.
- Stop app-server and remove the temporary home.
Each fallback starts a new app-server process; the project does not maintain a persistent app-server connection.
The app-server summary always needs a valid availableCount to be useful.
credits has two supported states:
- A list provides individual status, title or description, grant time, and optional expiration time.
nullor a missing field means only the count is known. The snapshot remains successful and the CLI reportsdetails unavailablewhen the count is positive.
App-server timestamps are Unix seconds and are normalized to UTC ISO strings before entering the shared reset-credit cache model. Opaque credit IDs are not stored.
fetch_state() discovers every valid account slot, puts the selected account
first, and queries quota snapshots in parallel with at most four workers.
Successful responses replace each account's cache.json. A failed query uses
the previous cache when one exists.
once fetches every saved account and prints the selected account first. The
tray uses the same snapshots for its current-account section and standby
submenu.
Reset credits are queried per account and cached separately from quota usage. Unless a refresh is forced, a cache is reusable until either:
reset_credits_refresh_interval_secondselapses; or- an available credit reaches its recorded expiration time.
The CLI command forces a refresh. The tray can either reuse the reset-credit cache or force the network path, depending on the trigger described below.
add runs codex login in an isolated temporary home, validates the resulting
token fields, and stores the credential under
.runtime/accounts/<Alias>/auth.json. It does not modify the main Codex login.
switch runs under the project runtime lock:
- Safely copy any refreshed main credential back to the currently selected slot.
- Atomically replace the main Codex
auth.jsonwith the target slot. - Save the new
selected_alias.
Tray switches are background operations, but auth replacements run in click order. Each click receives a generation number. A slow result from an older generation may update its account-specific cache, but it cannot overwrite the tray state after a newer account has been selected.
Activation runs one account at a time in an isolated temporary home. It invokes
an ephemeral, read-only codex exec using gpt-5.6-luna, low reasoning effort,
and the prompt Reply exactly: OK. Refreshed auth may be copied back to the
same saved slot, but the main Codex login is not replaced.
The command may consume a small number of tokens. Temporary homes are removed after each account, including timeout and failure paths.
Defaults come from AppConfig and are loaded when the tray starts. Restart the
tray after editing timer values.
| Trigger | Quota usage | Reset credits | Network behavior |
|---|---|---|---|
| Tray startup | All saved accounts | All saved accounts | Show cache first, then force both query paths in the background |
| Menu text timer | No query | No query | Every 60 seconds; rebuild text from in-memory state only |
| Active timer | Selected account | No query | Default every 120 seconds |
| Standby timer | All saved accounts, including selected | Refresh only when cache is stale | Default every 600 seconds |
Refresh all |
All saved accounts | All saved accounts | Force both query paths |
| Tray switch | All saved accounts after the switch | Cache only | Reorder cached UI immediately, then refresh quota |
Activate all |
All saved accounts after activation | Cache only | Activation requests, followed by quota refresh |
once |
All saved accounts | Cache only | Quota network requests only |
check-reset-credits |
No query | Selected, named, or all accounts | Always force reset-credit refresh |
The 60-second menu timer is strictly a display timer. It updates relative text
such as Updated 4m ago, reset in 42m, or expires in 3d; it must not call a
network client.
User-visible strings are produced by pure helpers in quota.py and
reset_credits.py; GTK code only places those strings into menus.
The label joins remaining percentages without percent signs, for example:
H68 · W43
The 5-hour window (H) controls the icon when present; otherwise the first
window is used.
| State | Icon |
|---|---|
| No usable quota windows, or failed refresh with cache at least one hour old | Gray |
| Less than 30% remaining | Red |
| 30% through 70% remaining | Yellow |
| More than 70% remaining | Green |
The selected account begins with ✓ <Alias> · <Plan>, followed by email,
quota-window lines, reset-credit status, and freshness. A standby entry begins
with the clickable Switch to <Alias> · <Plan> line and uses the same remaining
status lines.
Quota freshness text follows this order:
| Condition | Text |
|---|---|
| Authentication-related error | Auth needed |
| Incompatible response schema | Backend changed |
| No usable timestamp | Stale |
| Updated less than 60 seconds ago | Updated right now |
| Updated from 1 minute to less than 1 hour ago | Updated {n}m ago |
| Updated at least 1 hour ago | Stale |
A generic transport failure keeps the cached timestamp and therefore continues to show its age until the one-hour freshness boundary. Authentication and schema errors override the age text immediately.
Reset-credit menu text follows these templates:
| Condition | Text |
|---|---|
| No snapshot | Resets unknown |
| Authentication error | Resets Auth needed |
| Schema error | Resets Backend changed |
| Other failed refresh with a positive cached count | Resets {count} · stale |
| Other failed refresh without a positive cached count | Resets stale |
| Successful snapshot with a known nearest expiration | Resets {count} · expires ... |
| Successful snapshot without expiration details | Resets {count} |
For detailed snapshots, the tray gives an expired available credit a 60-second grace period. On the next menu redraw after that grace period, it subtracts the known expired credit from the displayed count and advances to the next known expiration. This is a display-only adjustment; cached backend data and count-only snapshots remain unchanged.
In terminal output, a positive count without credit rows is
details unavailable; zero without rows is none. Rows with expiration times
are sorted by nearest expiration across accounts.
Quota resets and reset-credit expirations share one formatter:
- at most 60 seconds:
{verb} now; - under 1 hour:
{verb} in {n}m; - under 6 hours:
{verb} in {h}h {m}m; - later today or tomorrow: local clock time;
- 2 through 6 calendar days: weekday and local time;
- 7 through 31 days:
{verb} in {n}d; - later dates: abbreviated month and day, plus year when needed.
| Path | Purpose |
|---|---|
.runtime/config.toml |
Selected alias and configurable intervals/timeouts |
.runtime/accounts/<Alias>/auth.json |
Saved account credential |
.runtime/accounts/<Alias>/cache.json |
Last quota snapshot |
.runtime/accounts/<Alias>/reset_credits_cache.json |
Last reset-credit snapshot |
.runtime/app.lock |
Cross-thread/process auth and configuration lock |
.runtime/tmp/codex-homes/ |
Isolated Codex homes for login, activation, and fallback |
The following rules are part of the design:
- Runtime directories use mode
0700; credentials, config, locks, and caches use mode0600. - Account slots and protected files must not be symbolic links.
- Sensitive files are written through a same-directory temporary file followed by atomic replacement.
- Temporary homes are deleted on normal exit and failure. Inactive leftovers
older than one hour are pruned later; the entire
.runtime/tree must still be treated as secret. - Refreshed auth is copied back only when JSON is valid, required token fields
exist, and
account_idmatches the destination slot. A timestamped destination is preserved when the sourcelast_refreshis missing, older, equal, or malformed. A newer timestamped source may replace a legacy destination; two legacy files without timestamps retain the previous compatibility behavior. RFC 3339 timestamps are compared in UTC with up to nanosecond precision; any malformed timestamp prevents copy-back. - Direct authentication recovery may read the main Codex auth and copy it into a matching saved slot under the runtime lock before one retry. It never writes the main Codex auth.
- Direct authenticated requests require HTTPS and follow redirects only when scheme, hostname, and effective port are unchanged.
- Only explicit
switchreplaces the main Codex auth file.
See the security policy for publication and vulnerability-reporting guidance.
| Behavior | Implementation | Main tests |
|---|---|---|
| Direct transport and app-server handshake | client.py |
tests/test_client.py |
| Query, cache, and fallback orchestration | app.py |
tests/test_app.py |
| Quota parsing and GUI strings | quota.py |
tests/test_quota.py |
| Reset-credit parsing and GUI strings | reset_credits.py |
tests/test_reset_credits.py |
| Tray timers, menu, and switch generations | indicator.py |
tests/test_indicator.py |
| Account login, switch, and auth safety | auth_store.py, switcher.py, auth_sync.py, runtime.py |
Matching tests/test_*.py files |
| Activation command and cleanup | activation.py |
tests/test_activation.py |
When changing an endpoint, fallback condition, timer, cache rule, auth write, or user-visible status string:
- Update the corresponding behavior test.
- Update this document if the intended contract changes.
- Update the README only when commands or user-facing capabilities change.
- Run
python3 -m unittest discover -s tests.