1
0
Fork 0
gibby/.opencode/plans/1775725199925-glowing-orchid.md
2026-04-20 23:41:37 +03:00

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_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.

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