1
0
Fork 0
This commit is contained in:
Arthur K. 2026-04-20 23:41:37 +03:00
commit 7cef56de15
23 changed files with 3136 additions and 0 deletions

View 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