7.3 KiB
Plan: make local account state authoritative
Problem
Current accounts.json state is not acting like a real source of truth for /token:
active_account_idis persisted but not actually used to drive selectionlast_known_usageis mostly treated as a sort hint, then immediately replaced by a live refresh/tokenlive-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
accounts.jsonis the source of truth for/tokenselection.- If
active_account_idexists and local state says the active account is usable,/tokenmust try that account first. - Only if local state says the active account is blocked or unusable should
/tokenfall back to choosing another account. - Fallback selection can keep the current ranking approach: most-used eligible account first.
/usageshould continue to refresh all accounts live.- 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 resolvestate.active_account_idto anAccountRecord - 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_usageexists 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
/tokencall
/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_percentremaining_percentexhausted
- keep only canonical persisted snapshot data:
checked_atprimary_windowsecondary_windowlimit_reachedallowed
Implementation direction:
- keep
UsageSnapshotas the in-memory model for now, but derive:used_percentremaining_percentexhaustedfrom 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-memoryUsageSnapshotfrom 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_untilremains the strongest persisted block- if cooldown is clear but local
last_known_usagestill 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
- respect
/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_untilis 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_idfalls 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.jsonno longer writesused_percent,remaining_percent, orexhausted- loading canonical persisted snapshots still reconstructs full in-memory
UsageSnapshot /usage,refresh_limits.py, andoauth_helper.pystill 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.jsonafter/usageorrefresh_limits.pyand confirm snapshot entries contain only canonical fields - manually test
/tokenwith:- 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