---
name: codex-acct
description: >-
  Switch or rotate the Codex account (Codex.app desktop app and codex CLI, which share
  ~/.codex/auth.json) on cebers's MacBook and mac mini, pulling ChatGPT team accounts from
  the local OmniRoute codex pool and ranking by remaining 5-hour (session) AND weekly quota.
  Use when: Codex hit a rate/usage limit ("упёрся в лимит", "codex кончилась квота",
  "переключи codex аккаунт", "ротация codex", "switch codex account"); you need to pick the
  account with the most 5h+weekly quota; refresh the pool after tokens rotate; or import/preserve
  a codex account into OmniRoute. The switch writes ~/.codex/auth.json (chatgpt mode) and restarts
  Codex.app. NOT for non-cebers machines or providers other than codex/ChatGPT.
---

# codex-acct — Codex account switcher / rotator

Switches the account Codex (the **Codex.app** desktop app and the **codex CLI**) authenticates
with, by rewriting `~/.codex/auth.json` (ChatGPT-login format) from a local pool of team accounts
and restarting Codex.app. Confirmed: Codex.app re-reads `auth.json` on launch and shows the new account.

## Background (how the setup works)

- The user has ~300 ChatGPT **Team** member accounts living in only **2 workspaces**
  (`64b96fbd…` = 260 members, `bce038cb…` = 40 members). They are stored as `codex`
  `provider_connections` in **OmniRoute** (`group='codex-team-pool'`), tokens encrypted
  `enc:v1:` (aes-256-gcm, `scrypt(STORAGE_ENCRYPTION_KEY, "omniroute-field-encryption-v1", 32)`).
- Quota is **per member account**, tracked in OmniRoute `quota_snapshots`
  (`window_key='session'` = the 5-hour window, `window_key='weekly'`; `remaining_percentage`,
  `is_exhausted`). Only the 260-member workspace has quota snapshots; the 40 show `?`.
- OmniRoute locations:
  - **MacBook** (`MacBook-Air-CeBers`): own instance `~/.local/share/omniroute-app` — no sudo.
  - **mac mini** (`Cebers-mini`): the instance the user actually uses (dashboard :20129) runs as a
    **separate user `omniroute_svc`** → reading its DB/key needs `sudo -u omniroute_svc`
    (sudo password required for `refresh` and `prune` on the mini; `list/use/rotate/whoami` are sudo-free).
- Proxy/geo: mac mini routes Codex via local **xray** (must NOT use a RU exit — ChatGPT is
  geo-blocked in RU); MacBook uses **Happ** system-wide. This skill does NOT manage the proxy.

## The tool — `tools/codex-acct.mjs`

A self-contained Node script (uses built-in `node:sqlite`, no deps). Installed to
`~/.codex/codex-acct.mjs` with a PATH wrapper `/opt/homebrew/bin/codex-acct`.

```
codex-acct whoami         # current account + its (snapshot) 5h/weekly quota
codex-acct usage          # LIVE quota of the current account, straight from ChatGPT wham/usage
codex-acct scan           # classify whole pool LIVE: usable-now / quota-maxed / dead-token, by workspace
codex-acct list           # all accounts by snapshot min(5h, weekly)  (➜ current, [RL]) — snapshot is approximate
codex-acct use <email|N>  # switch to a specific account (warns if its token is dead/maxed)
codex-acct rotate         # switch to a LIVE-verified usable account; never strands on a dead token
codex-acct prune [--apply]# delete accounts ChatGPT rejects by auth (HTTP 401/402) from OmniRoute; dry-run without --apply
codex-acct capture [src]  # импорт в пул (preserved-personal, is_active=1). src = файл auth.json | ПАПКА (все *.json: импорт+сравнение/дедуп по account_id) | пусто=~/.codex/auth.json
codex-acct restart        # quit & relaunch Codex.app so it re-reads ~/.codex/auth.json
codex-acct refresh        # rebuild ~/.codex/accounts/pool.json from OmniRoute (decrypt tokens) — MANUAL ONLY
```

- **`prune` (чистка пула от мёртвых, 2026-06-09).** LIVE-классифицирует ВСЕ codex-строки в БД OmniRoute
  по реальному HTTP-коду `wham/usage` и удаляет только `401` (token_invalidated) + `402`
  (deactivated_workspace) — это «нужна переавторизация», НЕ лимит (`200`) и НЕ транзиентные сбои
  (`000`/прочее остаются нетронутыми). Dry-run по умолчанию; `--apply` делает: консистентный `.backup`
  БД (`storage.sqlite.bak-prune-<ts>`) → удаление по `id` → `refresh`. **mac:** стоп демона → delete →
  старт (своя инстанция). **mini:** удаляет ОНЛАЙН (`sudo -u omniroute_svc sqlite3`, WAL+busy_timeout,
  ACID) — prod-демон `com.cebers.omniroute` НЕ трогается, боты не падают. На mini нужен tty для sudo
  (запускать в терминале, не через `ssh` без `-t`).

- **Restart semantics:** `rotate` exits `0` only when it actually **switched** an account (and already
  relaunched Codex); it exits **`3`** when it **stayed** (current account is fine, or whole workspace
  maxed) — in that case it does NOT relaunch. The Desktop launcher restarts Codex itself on exit `≠0`,
  so a double-click always leaves Codex freshly reading the current `auth.json`. `restartApp` waits for
  the app to fully quit (polls `pgrep -x Codex`) before reopening, then confirms it came back up.

- **Selection is LIVE, not snapshot.** OmniRoute `quota_snapshots` are stale (Codex talks to ChatGPT
  directly, bypassing OmniRoute). The tool classifies each account by its **access_token** against
  `https://chatgpt.com/backend-api/wham/usage` → `ok` (usable now) / `maxed` (token valid, quota spent) /
  `dead` (token revoked/expired). The `list`/snapshot ranking is only an approximate hint.
- **`rotate` ранжирует кандидатов по ЖИВОМУ качеству** (не берёт «первого попавшегося»):
  `3` свободны обе квоты → `2` заблокирован только 5h, но **недельная ещё есть** (заработает после сброса 5h) →
  `1` **недельная выжата** (сброс через дни — плохо) → `0` мёртвый токен. Среди равных: для `ok` — больше всего
  остатка обеих квот; для «только-5h» — у кого скорее сброс. Переключается, только если кандидат **строго лучше**
  текущего по качеству (или при равном `ok` — заметно больше остатка), иначе остаётся. Так он не посадит тебя на
  аккаунт с выжатой недельной, когда есть аккаунты с недельным запасом. Кандидаты — team-pool + personal.
- **5h limit is PER-WORKSPACE** — all 260 members of `64b96fbd` share one 5h + weekly budget. Rotating
  within one workspace does NOT add 5h quota; only different workspaces / the personal account do.
- **`ingest` (drop-folder авто-пополнение, только macbook).** Кидаешь выгруженные `.json` (любой формат:
  top-level `access_token` / `tokens.{}` / `rt` / cpa-synthetic) в `~/.codex/inbox/`; `ingest` парсит,
  **live-проверяет**, живые импортит в пул OmniRoute (стоп демона → INSERT клон-шаблона → старт), дедуп по email,
  мёртвые → `inbox/dead/`, импортированные → `inbox/imported/`. Лаунчер вызывает `ingest` ПЕРЕД ротацией.
  Демон трогается только если есть что импортировать (пустой inbox = мгновенный no-op).
  Скачивание остаётся ручным: источник (`cpa.569521.xyz`) за **Cloudflare managed challenge** + завязан на
  cookie-сессию юзера → авто-curl/headless не проходит; плюс раздаточные токены мрут за минуты-часы
  (≈90% уже мёртвые к моменту импорта — отсеиваются автоматически). На mini `ingest` пока не работает (рестарт демона = sudo).
- Each switch backs up the previous `~/.codex/auth.json` to `auth.json.bak-prev` and restarts Codex.app.
- The script auto-detects machine via hostname (`/mini/i`); on the mini `liveUsage` goes via xray
  (`-x http://127.0.0.1:2081`) and `refresh` uses `sudo -u omniroute_svc`.

## Desktop one-click launcher — `tools/codex-switch.command`

A double-clickable macOS `.command` placed on the Desktop: shows current account LIVE quota (`usage`) →
`rotate` (live-verified) → shows resulting account LIVE quota → auto-closes. This is the user's
"just double-click to switch" entry point. **No `refresh` step** — it would clobber a live-recaptured
token with a stale OmniRoute snapshot (OmniRoute holds the same one-time tokens, nothing fresher).

## Install / deploy

Run `tools/install.sh` on the target machine (or via ssh for the mini). It:
1. copies `codex-acct.mjs` → `~/.codex/codex-acct.mjs`,
2. writes the `/opt/homebrew/bin/codex-acct` wrapper (`NODE_NO_WARNINGS=1`),
3. drops the Desktop launcher,
4. runs `codex-acct refresh` (mini: prompts for sudo) to build the pool.

## Gotchas

- **Модель для ChatGPT-аккаунта = `gpt-5.5`** (в `config.toml`). `gpt-5.4`, `gpt-5.5-codex`, `gpt-5-codex`,
  `gpt-5.1*`, `codex-mini-*` → ошибка `"model not supported when using Codex with a ChatGPT account"`
  (это API-only модели). При невалидной модели Codex не генерит НИ НА КАКОМ аккаунте — диагностится как
  «аккаунт не работает», хотя дело в модели. **Free-аккаунты Codex УМЕЮТ** (cebersdev free + gpt-5.5 → OK),
  просто у них одно окно квоты (`secondary_window=null`, ~30 дней), а не 5h+weekly. Codex.app иногда
  переписывает `model` в config (GUI-пикер) — если перестал работать, проверь что там `gpt-5.5`.
- **`capture` (импорт аккаунта в пул, 2026-06-10).** Источник — `~/.codex/auth.json` (после `codex login`)
  или файл-аргумент. Live-проверяет (HTTP 200 обязателен, иначе НЕ импортирует), пишет в OmniRoute как
  `preserved-personal`, **`is_active=1`** (чтобы `refresh` положил в pool.json и `rotate` видел). ⚠️ У codex
  oauth id_token **может НЕ быть email** — берём из ответа `wham/usage` (там `email`), иначе из `chatgpt_user_id`.
  Поэтому `currentEmail()` при отсутствии email в токене сопоставляет активный `auth.json` с пулом **по
  `account_id`** — иначе `whoami/usage/rotate` не опознают аккаунт (`current: (none)`). Экспортные токены
  быстро мрут (`token_invalidated`), если исходную сессию используют/рефрешат после экспорта — надёжнее
  `codex login` прямо на машине + сразу `capture`. **Папка-источник** (`capture <dir>`) импортит все живые
  (access HTTP 200), дедуп по `account_id`, мёртвые пропускает; вшито в десктоп-скрипт
  `~/Desktop/Codex — переключить аккаунт.command` (импорт из `~/Мои аккаунты codex` → usage → rotate → restart).
- **Симптом GUI «access token could not be refreshed, please log out and sign in again»** = access_token жив
  (usage 200), но **refresh_token потрачен** (one-time rotating, исходную сессию прокрутили после экспорта).
  `capture` это НЕ ловит (проверяет только access; refresh-токен JWE, снаружи не виден, проверка = его трата).
  Лечится только свежим `codex login` на этой машине. Экспортный токен годен для OmniRoute /v1 (access напрямую)
  до `exp`, но не для нативного Codex GUI (он рефрешит при старте).
- **`rotate` берёт ЛЮБОЙ аккаунт пула** (раньше фильтровал по `group` team-pool/preserved-personal →
  отсекал аккаунты с `group=null`, напр. captured free). Теперь кандидат = любой codex-аккаунт пула
  (is_active=1) кроме текущего, не-RL, с токеном.

- **⚠️ NEVER OAuth-refresh these tokens.** Codex uses **one-time rotating refresh tokens**; the stored
  snapshot is already spent → presenting it returns `refresh_token_reused`, which can **revoke the whole
  token family including the still-valid access_token**. The tool deliberately does NOT refresh; it only
  reads usage via the existing access_token. A diagnostic refresh-scan once killed ~26 live access tokens.
- **Tokens are NOT renewable.** access_token lives ~9 days (e.g. exp ~2026-06-18) and works until then;
  after expiry the account is dead until someone does a **real re-login** (email+password / `codex login`).
  Re-importing the same archives does NOT help — they hold the same spent token links.
- **5h is per-workspace** — 260 accounts in `64b96fbd` share ONE 5h budget. When it's maxed, `rotate`
  finds 0 usable team accounts (that's expected, not a bug); it reports the reset time and falls back to
  personal if possible. The pool gives weekly headroom + dead-token failover, NOT 5h multiplication.
- **Use a DIFFERENT account on each machine** — never run the same single account (esp. personal) on
  both Codex.apps at once: they each rotate its token chain and kill each other.
- `~/.codex/accounts/pool.json` holds decrypted tokens → keep it `chmod 600`.
- **Pool state (2026-06-09, после `prune`):** team-пул схлопнулся — почти все токены умерли. LIVE-замер
  по `wham/usage`: из ~230 на машину только **2 живых** (`nusurafaxolo34@gmail.com` weekly-выжат,
  `cebersdev@gmail.com` 200/usable), остальное — **401 token_invalidated** (~75, восстановимо ре-логином)
  + **402 deactivated_workspace** (~150, воркспейс забанен, ре-логин НЕ поможет). Удалены `prune --apply`
  на обеих машинах (MacBook 233→2, mini 228→2). На mini `cebersdev` активирован (`is_active=1`) — codex
  снова отдаётся. **Прежняя оценка «~234 valid-but-maxed + ~26 dead» больше неверна** — массовый
  dronix.site-пул сгорел (one-time refresh-токены + бан воркспейсов). DB-бэкапы:
  `storage.sqlite.bak-prune-20260609-*` на обеих машинах.
