# Plan: rewrite token selection around a simple disk-first state model ## Goal Throw away the current layered selection/cooldown/state model and replace it with a small implementation that: - reads the main JSON file on every `/token` request - keeps only the minimum necessary account fields on disk - decides from file state first - refreshes usage only when missing or stale - validates the selected token before returning it - moves invalid accounts to `failed.json` - does not touch the helper scripts in this pass ## Required file model ### Main state file `accounts.json` ```json { "active_account": "user@example.com", "accounts": [ { "email": "user@example.com", "access_token": "...", "refresh_token": "...", "token_refresh_at": 1710000000, "usage": { "primary": { "used_percent": 72, "reset_at": 1710018000 }, "secondary": { "used_percent": 18, "reset_at": 1710600000 } }, "usage_checked_at": 1710000000, "disabled": false } ] } ``` Only these fields should exist for account state. ### Failed state file `failed.json` ```json { "accounts": [ { "email": "bad@example.com", "access_token": "...", "refresh_token": "...", "token_refresh_at": 1710000000, "usage": { "primary": { "used_percent": 100, "reset_at": 1710018000 }, "secondary": { "used_percent": 100, "reset_at": 1710600000 } }, "usage_checked_at": 1710000000, "disabled": false } ] } ``` Top-level must contain only `accounts`. ## Selection rules ### Active account first For each `/token` request: 1. Read `accounts.json` fresh from disk. 2. Resolve `active_account` by email. 3. Evaluate active first. ### When an account is usable An account is usable when: - `disabled == false` - `secondary.used_percent < 100` - `primary.used_percent < GIBBY_EXHAUSTED_USAGE_THRESHOLD` Default threshold remains `95`. ### Usage freshness Usage must be refreshed only when missing or stale. Add env: - `GIBBY_USAGE_STALE_SECONDS`, default `3600` Usage is stale when: - `usage` is missing - `usage_checked_at` is missing - `now - usage_checked_at > GIBBY_USAGE_STALE_SECONDS` If active account usage is stale or missing, refresh usage for that account before deciding if it is usable. ### Fallback selection If active account cannot be used, choose the next account by: - filtering to usable accounts - sorting by highest primary `used_percent` - using file order as the tie-breaker If a new account is chosen, write its email into `active_account` in `accounts.json`. ## Token flow For the chosen account: 1. Ensure token is fresh enough. 2. If `token_refresh_at` says refresh is needed, refresh token and persist new values. 3. After selection decisions are finished and the token is ready, validate it by calling: `https://chatgpt.com/backend-api/codex/models` 4. Only return the token if validation returns `200`. ## Invalid account handling If refresh, usage auth, or final validation shows the token/account is invalid: 1. Read current main state. 2. Remove that full account object from `accounts.json`. 3. Append the same full account object to `failed.json.accounts`. 4. If it was the active account, clear `active_account` before reselection. 5. Persist both files atomically. No `failed.txt` in the rewritten core flow. ## Files to rewrite - `/home/wzray/AI/gibby/src/gibby/settings.py` - keep only env needed for the new flow - `/home/wzray/AI/gibby/src/gibby/store.py` - rewrite as simple JSON read/write helpers for `accounts.json` and `failed.json` - `/home/wzray/AI/gibby/src/gibby/client.py` - keep only token refresh, usage fetch, and token validation calls - `/home/wzray/AI/gibby/src/gibby/manager.py` - rewrite into one small service for `/token` - `/home/wzray/AI/gibby/src/gibby/app.py` - keep thin FastAPI wiring for `/health` and `/token` ## Files to remove or stop using - `/home/wzray/AI/gibby/src/gibby/models.py` - `/home/wzray/AI/gibby/src/gibby/account_ops.py` Their logic should be folded into the new minimal data model and service flow instead of preserved. ## Out of scope for this pass - do not touch `scripts/oauth_helper.py` - do not touch `scripts/refresh_limits.py` - do not preserve old cooldown, failed.txt, dual-state, or derived snapshot machinery unless absolutely required to keep app booting during rewrite ## Verification - `uv run pytest -q` - API tests for: - `/health` returns `ok` - `/token` returns `503` when file has no usable accounts - `/token` prefers active account when usable - `/token` rereads the file between requests - stale usage triggers a refresh before decision - fresh usage skips refresh - invalid token moves full account object to `failed.json` - fallback chooses highest primary usage among usable non-disabled accounts - direct file tests for exact `accounts.json` and `failed.json` schema