This commit is contained in:
commit
7cef56de15
23 changed files with 3136 additions and 0 deletions
163
.opencode/plans/1775725199925-glowing-orchid.md
Normal file
163
.opencode/plans/1775725199925-glowing-orchid.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# Plan: make local account state authoritative
|
||||
|
||||
## Problem
|
||||
|
||||
Current `accounts.json` state is not acting like a real source of truth for `/token`:
|
||||
|
||||
- `active_account_id` is persisted but not actually used to drive selection
|
||||
- `last_known_usage` is mostly treated as a sort hint, then immediately replaced by a live refresh
|
||||
- `/token` live-refreshes every candidate it tries, so local limits are not truly trusted
|
||||
- the persisted snapshot contains duplicate derived fields (`used_percent`, `remaining_percent`, `exhausted`) in addition to canonical window and flag data
|
||||
- as a result, the file is hard to reason about: some fields look authoritative but are only decorative or transient
|
||||
|
||||
## Desired behavior
|
||||
|
||||
1. `accounts.json` is the source of truth for `/token` selection.
|
||||
2. If `active_account_id` exists and local state says the active account is usable, `/token` must try that account first.
|
||||
3. Only if local state says the active account is blocked or unusable should `/token` fall back to choosing another account.
|
||||
4. Fallback selection can keep the current ranking approach: most-used eligible account first.
|
||||
5. `/usage` should continue to refresh all accounts live.
|
||||
6. Persisted usage snapshots should store canonical data only, without duplicated derived fields.
|
||||
|
||||
## Recommended approach
|
||||
|
||||
### 1. Split `/token` selection into local selection and live validation
|
||||
|
||||
In `src/gibby/manager.py`:
|
||||
|
||||
- Add `_find_active_account(state)` to resolve `state.active_account_id` to an `AccountRecord`
|
||||
- Add `_is_locally_blocked(account, current)` to decide from local file state only whether an account can be tried
|
||||
- Add `_build_selection_order(state, current)` that:
|
||||
- returns the active account first if it exists and is not locally blocked
|
||||
- otherwise falls back to the remaining eligible accounts sorted by current saved-usage ranking
|
||||
- never duplicates the active account in the fallback list
|
||||
|
||||
`_is_locally_blocked()` should use only persisted local state:
|
||||
|
||||
- blocked if `cooldown_until > now`
|
||||
- blocked if `last_known_usage` exists and local usage indicates exhaustion
|
||||
- otherwise not blocked
|
||||
|
||||
This gives the exact behavior the user requested:
|
||||
|
||||
- active account is mandatory first choice when nothing local blocks it
|
||||
- local file decides whether active is allowed before any network call
|
||||
- live refresh remains only a validation step for the chosen candidate
|
||||
|
||||
### 2. Keep live refresh only for accounts actually attempted
|
||||
|
||||
In `src/gibby/manager.py`:
|
||||
|
||||
- keep the current live refresh path (`refresh_account_usage`) once an account has been selected for an attempt
|
||||
- if active account passes local checks but fails live validation, persist the updated state and continue to the next candidate
|
||||
- if active account is locally blocked, skip live refresh for it during that `/token` call
|
||||
|
||||
`/usage` stays as-is and continues refreshing all accounts live.
|
||||
|
||||
### 3. Clean up the persisted usage snapshot schema
|
||||
|
||||
In `src/gibby/models.py` and `src/gibby/store.py`:
|
||||
|
||||
- stop persisting derived snapshot fields:
|
||||
- `used_percent`
|
||||
- `remaining_percent`
|
||||
- `exhausted`
|
||||
- keep only canonical persisted snapshot data:
|
||||
- `checked_at`
|
||||
- `primary_window`
|
||||
- `secondary_window`
|
||||
- `limit_reached`
|
||||
- `allowed`
|
||||
|
||||
Implementation direction:
|
||||
|
||||
- keep `UsageSnapshot` as the in-memory model for now, but derive:
|
||||
- `used_percent`
|
||||
- `remaining_percent`
|
||||
- `exhausted`
|
||||
from canonical fields when loading/parsing
|
||||
- update `store._snapshot_to_dict()` to write only canonical fields
|
||||
- update `store._snapshot_from_dict()` to reconstruct the full in-memory `UsageSnapshot` from canonical persisted fields
|
||||
|
||||
This keeps code churn smaller than a full model rewrite while making the file itself cleaner and more honest.
|
||||
|
||||
### 4. Keep cooldown as the persisted local block, but make local exhaustion matter too
|
||||
|
||||
Local selection should not depend on a fresh API round-trip.
|
||||
|
||||
For `/token`:
|
||||
|
||||
- `cooldown_until` remains the strongest persisted block
|
||||
- if cooldown is clear but local `last_known_usage` still says exhausted, treat the account as locally blocked too
|
||||
- only accounts that pass local checks are eligible to be attempted live
|
||||
|
||||
This changes current behavior in an important way:
|
||||
|
||||
- today, an account with expired or missing cooldown can still be live-refreshed even if local snapshot says exhausted
|
||||
- after the change, local state truly gates the initial decision
|
||||
|
||||
### 5. Preserve existing fallback ranking for non-active accounts
|
||||
|
||||
After active account is rejected locally, keep the current fallback sort in `manager.py`:
|
||||
|
||||
- primary window used percent descending
|
||||
- secondary window used percent descending
|
||||
|
||||
That avoids a larger policy change in this pass and isolates the refactor to "trust local state first".
|
||||
|
||||
## Files to modify
|
||||
|
||||
- `/home/wzray/AI/gibby/src/gibby/manager.py`
|
||||
- respect `active_account_id`
|
||||
- add local-only eligibility predicate
|
||||
- change selection order to active-first-when-locally-usable
|
||||
- `/home/wzray/AI/gibby/src/gibby/models.py`
|
||||
- keep canonical usage derivation helpers centralized
|
||||
- support reconstructing derived values from canonical fields
|
||||
- `/home/wzray/AI/gibby/src/gibby/store.py`
|
||||
- write canonical snapshot shape only
|
||||
- read canonical snapshot shape into full in-memory model
|
||||
- `/home/wzray/AI/gibby/src/gibby/account_ops.py`
|
||||
- keep refresh path aligned with canonical snapshot handling
|
||||
- reuse a local exhaustion predicate if helpful instead of duplicating logic
|
||||
- `/home/wzray/AI/gibby/tests/test_core.py`
|
||||
- add and update selection behavior tests
|
||||
- `/home/wzray/AI/gibby/tests/test_account_ops.py`
|
||||
- update snapshot persistence assumptions if needed
|
||||
- `/home/wzray/AI/gibby/tests/test_app.py`
|
||||
- adjust fixture shapes only if response expectations change
|
||||
- `/home/wzray/AI/gibby/tests/test_refresh_limits.py`
|
||||
- ensure live refresh still rewrites canonical local state correctly
|
||||
- `/home/wzray/AI/gibby/tests/test_oauth_helper.py`
|
||||
- ensure oauth helper stores canonical snapshot shape correctly
|
||||
|
||||
## Test plan
|
||||
|
||||
### Selection behavior
|
||||
|
||||
Add or update tests in `tests/test_core.py` for:
|
||||
|
||||
- active account is used first when locally allowed, even if another account has higher saved usage
|
||||
- active account is skipped without live refresh when `cooldown_until` is still active
|
||||
- active account is skipped without live refresh when local snapshot says exhausted
|
||||
- active account passes local checks but fails live refresh, then fallback account is tried
|
||||
- missing or stale `active_account_id` falls back cleanly to non-active selection logic
|
||||
- fallback ordering still prefers higher saved primary usage and uses secondary as tie-breaker
|
||||
|
||||
### Snapshot/file behavior
|
||||
|
||||
Add or update tests to verify:
|
||||
|
||||
- `accounts.json` no longer writes `used_percent`, `remaining_percent`, or `exhausted`
|
||||
- loading canonical persisted snapshots still reconstructs full in-memory `UsageSnapshot`
|
||||
- `/usage`, `refresh_limits.py`, and `oauth_helper.py` still persist refreshed canonical state correctly
|
||||
|
||||
## Verification
|
||||
|
||||
- `uv run pytest -q tests/test_core.py tests/test_account_ops.py tests/test_app.py tests/test_refresh_limits.py tests/test_oauth_helper.py`
|
||||
- inspect a real `accounts.json` after `/usage` or `refresh_limits.py` and confirm snapshot entries contain only canonical fields
|
||||
- manually test `/token` with:
|
||||
- a locally usable active account
|
||||
- a locally blocked active account
|
||||
- a dangling `active_account_id`
|
||||
- verify that active account is not live-refreshed when local file state already blocks it
|
||||
Loading…
Add table
Add a link
Reference in a new issue