Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
499eac6
feat(account): sync account names from accounts/check
Loongphy Mar 26, 2026
202a7e1
feat: bootstrap team account names and allow switch by name
Loongphy Mar 26, 2026
290009a
fix: align tests with team name matching behavior
Loongphy Mar 26, 2026
86e1b4a
fix: correct first-run e2e list assertion
Loongphy Mar 26, 2026
58022b0
docs: add execution isolation guidance
Loongphy Mar 26, 2026
a61ebf3
refactor: remove team bootstrap and split account api config
Loongphy Mar 26, 2026
1fcf519
merge(main): resolve origin/main conflicts
Loongphy Mar 26, 2026
310cc6b
fix(remove): preserve unique labels and safe account name updates
Loongphy Mar 26, 2026
8117f9f
fix: backfill api config and align remove output tests
Loongphy Mar 26, 2026
8584268
fix(display): preserve email labels for singleton accounts
Loongphy Mar 26, 2026
4385e2d
feat: refresh grouped account names in background
Loongphy Mar 26, 2026
b10c8a0
docs(account-name): finalize behavior and align test fixtures
Loongphy Mar 26, 2026
6831c27
fix: isolate post-switch account-name refresh
Loongphy Mar 26, 2026
8f9e99a
feat(auto): refresh team names during daemon cycles
Loongphy Mar 27, 2026
dc277df
fix: merge daemon account-name refresh onto latest registry
Loongphy Mar 27, 2026
205e90e
fix: refresh account names from stored snapshots
Loongphy Mar 27, 2026
3c2e43f
refactor: unify daemon account-name refresh flow
Loongphy Mar 27, 2026
3161146
fix: prefer freshest account-name snapshot
Loongphy Mar 27, 2026
d53218b
fix: stop backfilling account metadata from auth snapshots
Loongphy Mar 27, 2026
641f52e
fix: require ChatGPT account id for account refresh
Loongphy Mar 27, 2026
0edb835
fix(auto): bootstrap daemon account-name refresh
Loongphy Mar 27, 2026
7805b60
refactor: scope account name sync by chatgpt user id
Loongphy Mar 27, 2026
3df9878
fix: coordinate account name refresh selection and locking
Loongphy Mar 27, 2026
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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

After modifying any `.zig` file, always run `zig build run -- list` to verify the changes work correctly.

# Execution Isolation

- Run tests, review commands, and other side-effecting tooling from an isolated directory under `/tmp/<task-name>` with `HOME=/tmp/<task-name>`.

# Zig API Discovery

- Do not guess Zig APIs from memory or from examples targeting other Zig versions.
Expand Down
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Remove-Item "$env:LOCALAPPDATA\codex-auth\bin\codex-auth-auto.exe" -Force -Error
|---------|-------------|
| `codex-auth config auto enable\|disable` | Enable or disable background auto-switching |
| `codex-auth config auto [--5h <%>] [--weekly <%>]` | Set auto-switch thresholds |
| `codex-auth config api enable\|disable` | Use API-backed fallback or local-only usage refresh |
| `codex-auth config api enable\|disable` | Enable or disable both usage refresh and team name refresh API calls |

---

Expand Down Expand Up @@ -263,8 +263,6 @@ codex-auth config api disable

Changing `config api` updates `registry.json` immediately. `api enable` is shown as API mode and `api disable` is shown as local mode.

Implementation details are documented in [`docs/auto-switch.md`](docs/auto-switch.md).

## Q&A

### Why is my usage limit not refreshing?
Expand Down Expand Up @@ -337,8 +335,8 @@ This project is provided as-is and use is at your own risk.
**Usage Data Refresh Source:**
`codex-auth` supports two sources for refreshing account usage/usage limit information:

1. **API (default):** When `config api enable` is on, the tool makes direct HTTPS requests to OpenAI's endpoints using your account's access token. This is the current default mode.
2. **Local-only:** When `config api disable` is on, the tool scans local `~/.codex/sessions/*/rollout-*.jsonl` files without making API calls. This mode is safer, but it can be less accurate because recent Codex rollout files often contain `rate_limits: null`, so the latest local usage limit data may lag by several hours.
1. **API (default):** When `config api enable` is on, the tool makes direct HTTPS requests to OpenAI's endpoints using your account's access token. This enables both usage refresh and team name refresh.
2. **Local-only:** When `config api disable` is on, the tool scans local `~/.codex/sessions/*/rollout-*.jsonl` files for usage data and skips team name refresh API calls. This mode is safer, but it can be less accurate because recent Codex rollout files often contain `rate_limits: null`, so the latest local usage limit data may lag by several hours.

**API Call Declaration:**
By enabling API-based usage refresh, this tool will send your ChatGPT access token to OpenAI's servers (specifically `https://chatgpt.com/backend-api/wham/usage`) to fetch current quota information. This behavior may be detected by OpenAI and could violate their terms of service, potentially leading to account suspension or other risks. The decision to use this feature and any resulting consequences are entirely yours.
By enabling API(`codex-auth config api enable`), this tool will send your ChatGPT access token to OpenAI's servers, including `https://chatgpt.com/backend-api/wham/usage` for usage limit and `https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27` for team name. This behavior may be detected by OpenAI and could violate their terms of service, potentially leading to account suspension or other risks. The decision to use this feature and any resulting consequences are entirely yours.
116 changes: 116 additions & 0 deletions docs/api-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# API Refresh

This document is the single source of truth for outbound ChatGPT API refresh behavior in `codex-auth`.

## Endpoints

### Usage Refresh

- method: `GET`
- URL: `https://chatgpt.com/backend-api/wham/usage`
- headers:
- `Authorization: Bearer <tokens.access_token>`
- `ChatGPT-Account-Id: <chatgpt_account_id>`
- `User-Agent: codex-auth`

### Account Metadata Refresh

- method: `GET`
- URL: `https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27`
- headers:
- `Authorization: Bearer <tokens.access_token>`
- `ChatGPT-Account-Id: <chatgpt_account_id>`
- `User-Agent: codex-auth`

The `accounts/check` response is parsed by `chatgpt_account_id`. `name: null` and `name: ""` are both normalized to `account_name = null`.

## Usage Refresh Rules

- `api.usage = true`: foreground refresh uses the usage API.
- `api.usage = false`: foreground refresh reads only the newest local `~/.codex/sessions/**/rollout-*.jsonl`.
- `list` refreshes only the current active account before rendering.
- `switch` refreshes only the current active account before showing the picker so the currently selected row is not stale.
- `switch` does not refresh usage for the newly selected account after the switch completes.
- the auto-switch daemon refreshes the current active account usage during each cycle when `auto_switch.enabled = true`
- the auto-switch daemon may also refresh a small number of non-active candidate accounts from stored snapshots so it can score switch candidates
- the daemon usage paths are cooldown-limited; see [docs/auto-switch.md](./auto-switch.md) for the broader runtime loop

## Account Name Refresh Rules

- `api.account = true` is required.
- A usable ChatGPT auth context with both `access_token` and `chatgpt_account_id` is required. If either value is missing, refresh is skipped before any request is sent.
- `login` refreshes immediately after the new active auth is ready.
- Single-file `import` refreshes immediately for the imported auth context.
- `list` schedules a detached background refresh after rendering.
- `switch` saves the selected account first, then schedules the same detached background refresh so the command can exit immediately without waiting for `accounts/check`.
- those `list` and `switch` background refreshes scan all registry-backed grouped scopes, not just the current `auth.json` scope.
- the auto-switch daemon uses the same grouped-scope scan during each cycle when `auto_switch.enabled = true`.
- `list`, `switch`, and daemon refreshes load the request auth context from stored account snapshots under `accounts/` and do not depend on the current `auth.json` belonging to the scope being refreshed.
- when multiple stored ChatGPT snapshots exist for one grouped scope, background and daemon refreshes pick the snapshot with the newest `last_refresh`.
- stored snapshots without a usable `access_token` or `chatgpt_account_id` are skipped.
- `list`, `switch`, and daemon refreshes do not backfill missing `plan` or `auth_mode` from stored snapshots before deciding whether a grouped Team scope qualifies.

At most one `accounts/check` request is attempted per grouped user scope in a given refresh pass.
Request failures and unparseable responses are non-fatal and leave stored `account_name` values unchanged.

## Refresh Scope

Grouped account-name refresh always operates on one `chatgpt_user_id` scope at a time.

- `login` and single-file `import` start from the just-parsed auth info
- `list`, `switch`, and daemon refreshes scan registry-backed grouped scopes and refresh each qualifying scope independently

That scope includes:

- all records with the same `chatgpt_user_id`

`chatgpt_user_id` is the user identity for this flow. A single user may have multiple workspace `chatgpt_account_id` values, and those workspaces can include personal and Team records under the same email.

This means a `free`, `plus`, or `pro` record can still trigger a grouped Team-name refresh when it belongs to the same `chatgpt_user_id` as Team records.

`accounts/check` is attempted only when:

- the scope contains more than one record
- the scope contains at least one Team record
- at least one Team record in that scope still has `account_name = null`

## Apply Rules

After a successful `accounts/check` response:

- returned entries are matched by `chatgpt_account_id`
- matched records overwrite the stored `account_name`, even when a Team record already had an older value
- in-scope Team records, or in-scope records that already had an `account_name`, are cleared back to `null` when they are not returned by the response
- records outside the scope are left unchanged

## Examples

Example 1:

- active record: `user@example.com / team #1 / account_name = null`
- same grouped scope: `user@example.com / team #2 / account_name = null`

Running `codex-auth list` should issue `accounts/check`. If the API returns:

- `team-1 -> "Workspace Alpha"`
- `team-2 -> "Workspace Beta"`

Then both grouped Team records are updated.

Example 2:

- active record: `user@example.com / pro / account_name = null`
- same grouped scope: `user@example.com / team #1 / account_name = null`
- same grouped scope: `user@example.com / team #2 / account_name = "Old Workspace"`

Running `codex-auth list` should still issue `accounts/check`, because the grouped scope still has missing Team names. If the API returns:

- `team-1 -> "Prod Workspace"`
- `team-2 -> "Sandbox Workspace"`

Then:

- `team #1` is filled with `Prod Workspace`
- `team #2` is overwritten from `Old Workspace` to `Sandbox Workspace`

The same grouped-scope rule also applies to detached `list` / `switch` refreshes and to the auto-switch daemon. Those paths re-load the latest `registry.json` before applying each grouped `account_name` update so concurrent registry changes are preserved.
11 changes: 5 additions & 6 deletions docs/auto-switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@ Each cycle:
1. keeps an in-memory candidate index for all non-active accounts, keyed by the same candidate score used for switching
2. reloads `registry.json` only when the on-disk file changed, then rebuilds that in-memory index
3. syncs the currently active `auth.json` into the in-memory registry when the active auth snapshot changed
4. refreshes missing account metadata from stored auth snapshots when needed
5. tries to refresh usage from the newest local rollout event first
6. if no new local rollout event is available, or the newest event has no usable rate-limit windows, and `api.usage = true`, falls back to the ChatGPT usage API at most once per minute for the current active account
7. keeps the candidate index warm with a bounded candidate upkeep pass instead of batch-refreshing every candidate
8. if the active account should switch, revalidates only the top few stale candidates before making the final switch decision
9. writes `registry.json` only when state changed
4. tries to refresh usage from the newest local rollout event first
5. if no new local rollout event is available, or the newest event has no usable rate-limit windows, and `api.usage = true`, falls back to the ChatGPT usage API at most once per minute for the current active account
6. keeps the candidate index warm with a bounded candidate upkeep pass instead of batch-refreshing every candidate
7. if the active account should switch, revalidates only the top few stale candidates before making the final switch decision
8. writes `registry.json` only when state changed

The watcher also emits English-only service logs for debugging:

Expand Down
22 changes: 7 additions & 15 deletions docs/implement.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Implementation Details

This document describes how `codex-auth` stores accounts, synchronizes auth files, and refreshes metadata. The tool reads and writes local files under `~/.codex`, and for ChatGPT-auth usage refresh it can call the ChatGPT usage endpoint for the current active account.
This document describes how `codex-auth` stores accounts, synchronizes auth files, and manages local state under `~/.codex`. Outbound API refresh rules, endpoint contracts, and grouped account-name sync examples now live in [docs/api-refresh.md](./api-refresh.md).

## Packaging and Release

Expand Down Expand Up @@ -167,6 +167,8 @@ When switching:

The switch command refreshes the current active account's usage once before rendering account choices, so the picker does not show stale data for the currently selected account. It does not refresh the newly selected account after the switch completes.

Grouped account-name metadata refresh, when needed, is scheduled after the switch has already been saved so the command can return immediately; see [docs/api-refresh.md](./api-refresh.md).

## Removing Accounts

`remove` now supports three foreground modes:
Expand Down Expand Up @@ -230,21 +232,14 @@ This document keeps only the cross-reference points that matter to the rest of t

## Usage and Rate Limits

Foreground usage refresh is active-account-only and depends on `api.usage`:
Detailed API-backed refresh behavior now lives in [docs/api-refresh.md](./api-refresh.md). This section keeps only the local-state and rollout rules that interact with the rest of the implementation.

Foreground usage refresh still depends on `api.usage`:

1. If `api.usage = true`, foreground refresh tries only the ChatGPT usage API with the current active `~/.codex/auth.json`.
1. If `api.usage = true`, the API contract and timing rules are defined in [docs/api-refresh.md](./api-refresh.md).
2. If `api.usage = false`, read only the newest `~/.codex/sessions/**/rollout-*.jsonl` file by `mtime`.

- ChatGPT API refresh sends `Authorization: Bearer <tokens.access_token>` and `ChatGPT-Account-Id: <chatgpt_account_id>` to `https://chatgpt.com/backend-api/wham/usage`.
- Foreground API refresh updates only the current active account. The background watcher keeps a daemon-local in-memory candidate index for non-active accounts and can refresh candidate ChatGPT accounts from their stored `accounts/<account file key>.auth.json` snapshots without reloading the whole candidate set every cycle.
- In watcher mode, candidate freshness bookkeeping is runtime-only: the daemon keeps per-candidate last-checked timestamps in memory, does bounded top-candidate upkeep while the active account is healthy, and revalidates only the current heap top / next top candidates before a switch instead of re-fetching every candidate on every 1-second loop.
- In watcher mode, active-account API fallback cooldown is scoped to the current active account; switching to a different active account resets that cooldown for the new account.
- Watcher logs use compact `[local]`, `[api]`, and `[switch]` tags.
- Local rollout watcher logs print the actual window lengths from the snapshot first, then the local event timestamp, then the full rollout basename (including the UUID suffix); when the newest event has no usable usage windows the same `[local]` log line also adds `fallback-to-api`.
- `config auto enable` prints a short usage-mode note after installing the watcher so the user can see whether auto-switch is currently running with API-backed usage or local-only fallback semantics.
- API refresh writes a new snapshot only when the fetched snapshot differs from the stored one; unchanged API responses do not rewrite `registry.json`.
- Watcher API logs are reduced to `refresh usage | status=...`, where `status` is either the HTTP status code, `NoUsageLimitsWindow`, `MissingAuth`, or the direct request error name.
- In API-only mode, API failures do not overwrite the stored usage snapshot and do not fall back to local rollout files.
- The rollout scanner looks for `type:"event_msg"` and `payload.type:"token_count"`.
- The rollout scanner reads only the newest rollout file. Within that file, it uses the last `token_count` event whose `rate_limits` payload is a parseable object.
- If the newest rollout file has no usable `rate_limits` payload (for example `rate_limits: null` on every `token_count` event), refresh does not overwrite the account's existing stored usage snapshot.
Expand All @@ -253,14 +248,11 @@ Foreground usage refresh is active-account-only and depends on `api.usage`:
- Rate limits are mapped by `window_minutes`: `300` → 5h, `10080` → weekly (fallback to primary/secondary).
- If `resets_at` is in the past, the UI shows `100%`.
- `last_usage_at` stores the last time a newly observed snapshot was written; identical API refreshes leave it unchanged.
- `list` and `switch` use the foreground active-account refresh path.
- The background auto-switch watcher has its own near-real-time refresh strategy; see `docs/auto-switch.md`.
- In watcher mode, rollout scanning caches the newest rollout file between bounded full rescans so large `~/.codex/sessions` trees are not fully re-walked on every 1-second loop.
- The free-plan `35%` real-time guard applies only when the 5h trigger comes from an actual 300-minute window or an unlabeled primary window; weekly-only free accounts still switch based on the configured weekly threshold.
- For auto-switch candidate scoring, free accounts that expose only a single weekly (`10080`-minute) window still remain eligible and use that weekly remaining percentage as their candidate score.
- On Linux/WSL, watcher installation/removal now explicitly deletes the old `codex-auth-autoswitch.timer` unit file so legacy minute-timer installs do not continue to fire after migration to the watcher service.
- `switch` refreshes only the current active account before the selection/switch step; it does not refresh the newly selected account after the switch completes.
- API refresh does not mutate any local rollout attribution state.
- The rollout files still do not expose a stable account identity, so local-session ownership remains activation-window based rather than identity based.

Current registry/account field roles:
Expand Down
Loading
Loading