jacked assumes every account has a 5-hour window. On 2026-10-30 Anthropic renamed Max 5x to Max and Max 20x to Ultra,
and removed the 5-hour limit for verified Ultra accounts (weekly limits unchanged, plus a separate weekly Fable limit). ChatGPT Pro
also has no 5-hour limit in Codex. This change moves "which windows does this account type have, what is it called, and how long is
each window" into a data file that ships with each jacked release (jacked/data/usage_plans.json), so the next provider
change is a data edit, and makes every consumer read windows through it. It also fixes the missing week-elapsed marker on the Fable
row, for the whole class of model-scoped bars.
default_claude_max_20x accounts return five_hour: null
and a limits[] with only weekly_all + weekly_scoped (Fable). Max 5x and Team return
kind: "session". Profiles carry organization.plan_display_name: "Ultra" on the 20x rows.windowDurationMins: 10080, secondary: null.ping_ok_no_window, backoff stuck at 600 s ("1 consecutive failures" every time).Every surface shows the windows an account really has, under the plan's current name, with a week-elapsed marker on every bar; jacked stops spending pings and quota on windows that cannot exist; a future provider change is a catalog edit in a release.
jacked/data/usage_plans.json ships in the wheel, has version, verified_at,
per-plan sources, window lengths, and plan entries for Claude (Ultra, Max, Team, Pro, Enterprise, Free, unknown-tier Max)
and Codex (Pro, Plus). A missing or malformed file degrades to built-in defaults with a WARNING, never a crash. A newer
version warns and reads best-effort.absent; an unverified Ultra (payload with a session limit) returns present + temporary; a Max or Team
row returns present whether or not a window is open; Codex Pro returns absent; a Codex partial read returns
unknown.plan_display_name, then catalog label, then the raw subscription type.
Ultra rows read "Ultra" (hover "Formerly Max 20x"), Max 5x rows read "Max", Team reads "Team". No regex on the tier multiplier remains.GET /api/auth/accounts and every WebSocket account payload carry plan and windows;
usage.five_hour is null (not 0) for an absent window; every per-model and binding entry carries
window_seconds.renderUsageBar call passes a literal null elapsed fraction.jacked usage shows "none" in the 5h columns and the plan label.no_session_window); it still pings for a turned-over weekly window, and a 7d-only header reading is stored for such
accounts; a ping_ok_no_window backoff escalates 600, 1200, 2400, 3600 s instead of resetting to 600 s.five_hour: null.jacked usage --json rows add plan, plan_id,
session_window, model_limits; the summary adds best_account_worst_window,
best_account_worst_window_resets_at, plan_catalog; the pinned fields keep their meaning. chain-of-command rule 7
reads the plan-agnostic fields.plan.drift and summary.plan_catalog.drift.pace, weekly_scoped_shares, seven_day_breakdown, or the dollar-denominated codename windows.auto_swap/burn.py.
flowchart LR
J[jacked/data/usage_plans.json] --> L[jacked/usage_plans.py loader + resolver]
R[(accounts row: subscription_type, rate_limit_tier, profile_json, cached_usage_raw)] --> L
L --> P[resolve_plan: label, policy, critical pct, drift]
L --> S[session_window: present / absent / unknown, temporary, seconds]
L --> M[model window_seconds]
P & S & M --> API[routes/auth.py AccountResponse.plan + windows]
P & S & M --> WS[usage_monitor WS payload plan + windows]
S --> K[window keeper skip + 7d-only cache]
P --> D[auto_swap thresholds + labels]
S --> U[compute_urgency_tier]
S --> C[jacked usage + pacing summary]
API & WS --> JS[usage.js renderModelBar + usageBarOpts per window key]
json catalog + account row -> usage_plans resolver -> {plan, session window, model window lengths}
-> API/WS payloads -> JS bars; -> keeper, auto-swap, urgency, CLI/pacing
limits[] entry with kind == "session", a five_hour object with a
non-null utilization or reset, or a Codex short window): present; temporary on an until_verified plan; drift
session_on_never_plan on a never plan (the payload still wins).absent, except a Claude always plan, which stays present (silence can
mean "no window open") with drift session_missing. A Codex always plan reads absent with drift
session_missing (Codex lists every window an account has).present
(temporary on until_verified). A partial read must never erase a live window.never → absent; until_verified or any Codex plan → unknown; Claude
always/reported → present (legacy idle reading)."Authoritative" for Claude: the raw payload has a limits list AND every entry's kind is one jacked knows
(session, weekly_all, weekly_scoped). An unknown kind makes the list non-authoritative and is drift
unknown_limit_kind, so a renamed session kind can never read as "no 5-hour limit". For Codex:
jacked.codex.usage.normalize_rate_limits(raw) gives five_hour.reported and windows_complete (authoritative).
unknown counts as having a window for every decision (keeper, headroom, urgency): jacked never skips a window it cannot rule out.
"plan": {
"id": "ultra", // catalog id, or null when no entry matched
"label": "Ultra", // provider plan_display_name > catalog label > raw subscription type (Title Case)
"formerly": "Max 20x", // or null
"label_source": "provider", // provider | catalog | raw
"drift": [], // codes: session_missing, session_on_never_plan, unknown_tier, unknown_limit_kind, renamed
"catalog_verified_at": "2026-11-01"
}
"windows": {
"five_hour": {"state": "absent", "temporary": false, "seconds": 18000,
"note": "Verified Ultra accounts have no 5-hour limit. Weekly limits still apply."},
"seven_day": {"state": "present", "temporary": false, "seconds": 604800, "note": null}
}
ModelUsage / binding entries: + "window_seconds": 604800
AccountUsage.five_hour: the cached value; when the cached value is null it is 0 only for a Claude account whose
session state is present, otherwise null. seven_day keeps today's rule (Codex null stays null, Claude
null reads 0). menubar_summary.summarize_account uses the same rule.
| Owner | Files | Change |
|---|---|---|
| Core (main loop, done first) | jacked/data/usage_plans.json, jacked/usage_plans.py, jacked/service/usage_pacing.py, tests/unit/test_usage_plans.py, tests/unit/test_usage_pacing_windows.py |
Catalog, loader, resolve_plan, session_window, account_view (plan/windows payloads, five_hour_reading, projected_row), project_row, per-plan and observed window lengths, drift log; pacing helpers account_windows, worst_window, account_recovery_at and the new summary fields. |
| B1 API + display backend | jacked/api/routes/auth.py, jacked/service/menubar_summary.py, jacked/web/oauth.py, jacked/web/database.py (raw-payload size guard only), jacked/statusline_common.py, jacked/statusline.py (import line only) + their tests |
AccountResponse plan/windows, five_hour rule, ModelUsage window_seconds, bulk-refresh crash, OAuth crash, summarize_account rule, per-model window_seconds, binding_model_compact window_seconds + is_active, raw-payload guard raised and logged, delete dead _tier_label. |
| B2a monitor + keeper | jacked/api/usage_monitor.py, jacked/web/auth.py (compute_urgency_tier and usage_cache_updates only), jacked/api/routes/menubar.py (urgency fallback only) + their tests |
Keeper skip / 7d-only cache / confirmed success / escalating backoff, burn-rate, WS payload plan + windows, projection at every decision entry point, recovery time, urgency, decision reason text, ingestion clears a 5h reading an authoritative payload no longer lists. |
| B2b auto-swap + CLI | jacked/web/auto_swap/diagnostics.py, selection.py, __init__.py, tiers.py, jacked/cli.py (usage command only), tests/unit/test_auto_swap.py, tests/unit/test_usage_cmd.py |
Thresholds and labels from the catalog, explicit headroom gate, weekly length per account, jacked usage rows (plan, session_window, model_limits, worst window, active), --provider filter. |
| B3 frontend | jacked/data/web/js/components/usage.js, accounts.js, panel.js, auto-swap.js, jacked/data/web/js/websocket.js + tests/unit/test_web_js_*.py, the JS call-site guard in tests/unit/test_codex_usage_display.py |
computeElapsedFraction(resetsAt, seconds) fed by windows[key].seconds, renderModelBar, usageBarOpts(acct, key, base), plan-aware windowReason, plan label (also refreshed by WebSocket updates), banner text, call-site guard. |
| Main loop (text) | jacked/data/skills/chain-of-command/SKILL.md, README.md, docs/architecture/auto-swap-system.md, lessons.md |
Rule 7 rewrite, README text that assumes a 5h window, architecture sections 3 and 11.1, one lesson. |
tests/unit/test_usage_plans.py (91 tests) + module doctests; tests/unit/test_usage_pacing_windows.py; the pinned tests/unit/test_usage_cmd.py contract still passes.jacked/data/usage_plans.json, jacked/usage_plans.py (stdlib only at import; the Codex parser is imported lazily).usage_pacing.account_windows / worst_window / account_recovery_at; summary adds best_account_worst_window, best_account_worst_window_resets_at, plan_catalog.limits[] kind is unknown_limit_kind drift, never a crash; normalize_rate_limits exposes each selected window's window_minutes and the resolver reuses it (a dropped or positional window never lends its length); model_window_seconds(..., plan=) honors per-plan lengths, and menubar_summary.parse_per_model(raw, plan=None) / binding_model_compact(raw, plan=None) emit window_seconds (+ is_active on the compact form) for every model entry (moved from B1 to core).Public API (jacked/usage_plans.py)
load_catalog(data_root=None) -> Catalog ; catalog() -> Catalog (cached)
window_seconds(provider, kind) -> int # provider length (session | weekly | weekly_model)
resolve_plan(row) -> PlanMatch # id, label, formerly, label_source, session policy, session_critical_pct, drift, window_seconds(kind)
session_window(row, raw=MISSING, plan=None) -> SessionWindow # state present|absent|unknown, temporary, seconds, note, drift
has_session(row, raw=MISSING) -> bool # False only for absent; unknown counts as a window
account_view(row, raw=MISSING) -> AccountView # .plan .session .weekly_state .weekly_seconds
.plan_payload() .windows_payload() .five_hour_reading() .projected_row()
project_row(row) -> row # 5h value and reset dropped when the window is absent
model_window_seconds(provider, window=None, slot="weekly_model") -> int
raw_usage(row) -> dict | None
jacked/service/usage_pacing.py
account_windows(acct, now) -> [(kind, pct, reset)] # "5h" only when the plan has one
worst_window(acct, now) -> (kind, pct, reset) | None # 7d wins ties
account_recovery_at(acct, now, constrained_threshold=90.0) -> datetime | None
routes/auth.py: PlanInfo, WindowInfo, AccountWindows; AccountResponse.plan, .windows; ModelUsage.window_seconds; _build_account_usage five_hour from five_hour_reading(); a resolver error never 500s the list.web/oauth.py: seed with usage_cache_updates(usage); regression test.menubar_summary.py: summarize_account via five_hour_reading() (all-None guard); the API passes the row's plan into parse_per_model (window lengths are already in core).web/database.py raw-payload guard: the silent 10 KB skip becomes a generous bound (256 KiB); a payload over it CLEARS cached_usage_raw (never keeps the previous payload as evidence for fresh readings) and logs a WARNING that names the account and size (data-integrity rule: no silent drop); tests for both._tier_label + re-export + its test.no_session_window), 7d-only header cache, success confirmed against the windows that required the ping (a needed 5h window must show a future reset; a needed 7d window is confirmed by a fresh read with no reset at all, because the API omits it for days at a time, or by a reset that is no longer lapsed; a non-null reset that is still lapsed is not confirmed), backoff popped only on success; repeated-sweep, read-back-timeout and restart tests.project_row, so no decision sees a stale 5h value on an absent window (cross-consumer regression test with a stale 5h=100 Ultra row: no 5h-critical departure, no 5h urgency).usage_cache_updates: an authoritative payload (a limits list whose kinds jacked knows) with no session entry and no five_hour object clears the 5h columns, even when the five_hour key is missing.plan and windows.next_recovery_at = min of account_recovery_at; log lines print "n/a" for a null 5h.compute_urgency_tier(..., *, has_session=True, usage_model=None) and its two callers.usage_monitor stops importing tier_label (B2b deletes it) and uses resolve_plan(active_acct).label directly.diagnostics.tier_critical_threshold from the catalog; tier_label replaced by plan_label; exports and tests (realistic fixture for the fake 10x tier; Team and Codex Pro cases).selection._has_5h_headroom: True when the window is absent; docstring of consulted fields.tiers.white_bar and the 7d constant: the account's weekly length (account_view(acct).weekly_seconds).jacked usage: rows add plan, plan_id, session_window, model_limits, worst_window_pct, worst_window, worst_window_resets_at, active (the provider's active account in jacked's database, db.get_active_account_id(provider); no credential-store read); --provider claude|codex filters rows and the summary; table: plan label, "none" 5h cells, a Model column.usage.js: computeElapsedFraction(resetsAt, windowSeconds); the 5h/7d helpers delegate to it; windowElapsed(acct, key) uses acct.windows[key].seconds; renderModelBar(model, opts) uses model.window_seconds; usageBarOpts(acct, windowKey, base); windowReason(acct, windowKey, nowMs): absent returns "not on <Plan>" / short form, absent + none, hint = note; unknown keeps "not reported"; renderUsageBar prints "none" for reason.none.accounts.js: plan text from acct.plan.label in a stable [data-plan-label] target (title "Formerly Max 20x", drift hint); binding + per-model bars through renderModelBar; card bars through the new helpers; the poll-tier fallback no longer guesses from 5h.panel.js: plan badge from org.plan.label; BOTH aggregate bars migrate to the window-key helpers; binding bar through renderModelBar.websocket.js: bars through the new helpers; binding through renderModelBar; refresh [data-plan-label] from the merged account (an event without plan keeps the current label).auto-swap.js: banner "Usage is available again in 3h 20m".renderUsageBar call).jacked usage --json --provider claude; read the row with active: true (worst_window_pct, worst_window_resets_at), and its Fable entry in model_limits for a Fable fan-out; fall back to summary.best_account_worst_window_pct when no row is active; keep the phrases the tests pin.docs/architecture/auto-swap-system.md sections 3 and 11.1, lessons.md.uv run --frozen ruff check ., uv run --frozen python -m pytest) to a log with the exit captured.account_view for every real row of ~/.claude/jacked.db (opened read-only) and prints plan + windows + projected 5h, compared against the live payload facts; (2) the dashboard and panel rendered by the branch in the scratch-HOME rig (temporary HOME, seeded fake accounts whose cached_usage_raw copies the real payload SHAPES with no tokens or emails, auto-swap and the keeper off, so no outbound call) and screenshotted; (3) the keeper exercised only through tests with every external call stubbed.max_pct ignore the model-scoped weekly cap (a Fable cap at 96% with 7d at 60% shows green). Product choice plus ranking change, own PR.always (silence reads as an idle window), so an idle Max is never mistaken for a plan without the limit; drift logs if Anthropic ever drops the window for them.unknown (treated as having a window), and a cached reading keeps a temporary window alive through a partial read: a wrong "absent" would also bypass the headroom gate and pacing, not just one keeper ping (pre-mortem finding).