Skip to content

feat(usage-queue): per-tab + per-runtime auto-Enter (ADR-068) - #308

Merged
aterrylu merged 2 commits into
mainfrom
terry/usage-queue-per-runtime
Aug 7, 2026
Merged

feat(usage-queue): per-tab + per-runtime auto-Enter (ADR-068)#308
aterrylu merged 2 commits into
mainfrom
terry/usage-queue-per-runtime

Conversation

@aterrylu

@aterrylu aterrylu commented Aug 7, 2026

Copy link
Copy Markdown
Owner

What & why

Fixes a reported bug: the "auto-Enter when limit resets" button showed on a Codex agent's pane while it was Claude usage that was capped. The usage queue was written single-account-Claude — the cap was one account-wide value read by every pane, so a Codex (or Gemini) pane lit its button on the Claude limit and would have fired on the Claude clear. With split panes it was also nominal-per-tab (every tab saw the same global cap).

How — per-tab AND per-runtime (ADR-068)

flowchart LR
  A["armed pane\n(carries its provider)"] --> T{tick: group by provider}
  T -->|claude-code| C["claude-usage cap\n(hysteresis 90/80)"]
  T -->|codex| X["codex-usage cap"]
  T -->|gemini-cli| G["no probe → can't arm"]
  C -->|clear edge| FC["fire Claude panes"]
  X -->|clear edge| FX["fire Codex panes"]
Loading
  • Each armed pane carries its agent's provider (resolved server-side from the record). Block state is tracked per provider; each tick polls only the providers with armed panes and fires each on its own high→low clear edge — Claude via claude-usage, Codex via codex-usage. Gemini has no rolling-window source → can't arm, button never shows.
  • The queue core is plugin-agnostic (NormalizedUsage + injected probes); normalizeClaudeUsage / normalizeCodexUsage adapt each plugin's snapshot. All original invariants kept — re-entrancy guard, hysteresis, per-pane seenBlocked latch, notify-on-drop — just keyed per provider.
  • Route arms with the agent's provider and GET returns per-provider caps; SessionPaneUsageQueueButton(provider)useUsageQueue(sessionId, provider) reads only its pane's cap. Per-tab is then correct by construction.

Testing

  • make check green — server suite (20 usage-queue tests) + 362 dashboard tests, biome + tsc clean.
  • Mutation-verified: forcing the queue to ignore a pane's provider turns the isolation tests red ("Claude and Codex fire independently", "Codex fires on the Codex clear"). Plus "a Claude cap does not fire a Codex pane" (the reported bug), the adapter maps, evaluateCap, and the gemini no-op.

Checklist

  • make check passes locally
  • Conventional-commit title (feat:)
  • ADR recorded (ADR-068)
  • Not a visible default-scene change — the button only appears at a live cap, so no make hero

🤖 Generated with Claude Code

The usage queue (auto-press Enter when the limit next clears) was single-account
Claude: the cap was one account-wide value read by EVERY pane, so a Codex/Gemini
pane's button lit up on the CLAUDE limit and would have fired on the Claude
clear. Make it per-runtime: each armed pane carries its agent's provider, block
state is tracked per provider, and each tick fires each provider's panes on its
OWN clear edge (Claude via claude-usage, Codex via codex-usage; Gemini has no
window → can't arm). The route resolves the provider server-side and returns
per-provider caps; the dashboard button reads only its pane's cap, so per-tab is
correct by construction.

Preserves the original invariants (re-entrancy guard, hysteresis, per-pane latch,
notify-on-drop), keyed per provider. Plugin-agnostic core (NormalizedUsage +
injected probes) + normalizeClaude/normalizeCodex adapters. Tests: per-runtime
isolation (mutation-verified), adapters, evaluateCap, gemini no-op. See ADR-068.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CU3bFXWmkL2FPgkbCR5aop
@aterrylu
aterrylu enabled auto-merge (squash) August 7, 2026 07:44
Comment thread packages/server/src/routes/usageQueue.ts
Comment thread packages/server/src/usageQueue.ts
Comment thread packages/dashboard/src/hooks/useUsageQueue.ts
Comment thread packages/server/src/routes/usageQueue.ts Outdated

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — the per-runtime refactor is correct and well-tested on the server side, and the four findings are follow-ups rather than blockers.

What I verified

  • The core change is sound: blocked/resetsAt scalars → Map<Provider, …>, tick() groups armed panes by provider and polls only providers that have one, and each fires on its own high→low edge. Every original invariant survives — the re-entrancy guard still wraps the whole tick (so the double-Enter case stays closed across providers), hysteresis is unchanged, seenBlocked is now latched per provider, and the notify-on-drop path is intact.
  • The provider is resolved server-side from the agent record, so a client cannot arm a pane against the wrong runtime's limit. Good call over trusting a client-supplied value.
  • NormalizedUsage + injected probes is the right seam — the unit tests drive both runtimes with fakes and the reported bug ("a Claude cap does not fire a Codex pane") is pinned directly. The mutation evidence in the ADR checks out against what those tests assert.
  • Per-tab correctness genuinely does fall out for free once the button reads s.caps[provider].

Follow-ups (none blocking)

  1. 🟡 GET /api/usage-queue unconditionally probes Codex on every 15s poll. getCodexUsage()'s no-auth and expired-token branches skip the 60s cache and run an uncached recursive scan of ~/.codex/sessions/** plus up to 8 whole-file reads — 4×/min per dashboard tab, for Claude-only users too. Also awaited serially, so it lands on the Claude button's latency.
  2. 🟡 A stale Codex rollout fallback carries no errorKind, so it normalizes to non-empty windows with authError: false. Frozen at ≥90% (expired token, or /wham/usage down), an armed pane never sees the clear edge and never hits the auth-warning branch — a silent multi-hour wait, which is exactly what the Claude path notifies about.
  3. 🟡 useUsageQueue.dom.test.tsx was left on the old contract (one-arg call, {capped, resetsAt} body). It passes vacuously because tsconfig excludes test files from tsc and neither case asserts capped — so the client half of the fix (s.caps[provider]) is currently untested, with the button DOM tests mocking the hook wholesale.
  4. 🟢 POST /:sessionId returns { armed: true } even when arm() no-ops for a probe-less provider.

Details are in the inline comments. ADR-068 is thorough and honest about the alternatives.

- GET probes only providers that have an agent in the fleet — a Claude-only user
  no longer triggers the Codex probe's uncached recursive rollout scan 4×/min/tab.
- normalizeCodexUsage ignores a non-live (rollout) snapshot: a frozen disk
  number can't be a live cap, so an armed Codex pane can't latch at "capped"
  forever with no clear edge (holds like the Claude no-data case; still warns on
  a credential failure).
- POST /:sessionId returns the ACTUAL armed state (isArmed) — a gemini no-op no
  longer reports {armed:true}.
- Update useUsageQueue.dom.test.tsx to the (sessionId, provider) + caps contract
  (was on the old shape, passing vacuously) and add per-provider coverage: a
  Claude cap does not light a Codex pane. + a rollout-ignore adapter test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CU3bFXWmkL2FPgkbCR5aop
@aterrylu
aterrylu merged commit b64326f into main Aug 7, 2026
5 checks passed
@aterrylu
aterrylu deleted the terry/usage-queue-per-runtime branch August 7, 2026 08:01
aterrylu added a commit that referenced this pull request Aug 8, 2026
…-scoped

Per nox review on #309:
- add @autonomos/server (the bulk of #308 — usageQueue.ts, routes, the
  NormalizedUsage core + normalizeClaude/normalizeCodex adapters — is server;
  changelog-github only files into named packages' CHANGELOGs)
- rewrite the body to describe the fix (per-provider, per-tab, per-runtime)
  rather than the bug, and drop the backwards Claude-scoped framing (ADR-068
  was specifically about de-Claude-ing it)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SZXoaHsvgp4DrbrHdp9bm
aterrylu added a commit that referenced this pull request Aug 8, 2026
…ts for 5 undocumented PRs (#309)

* docs(changeset): retroactive changesets for #284/#288/#278/#307/#308

The v0.5.0 Version PR (#275) was missing changelog entries for five merged
PRs that shipped without a .changeset file — including #284 (ADR-055 PR A),
a breaking MCP transport change. nox flagged the gaps on #275; these restore
the entries so 0.5.0 ships a complete, accurate changelog.

- #284 ADR-055 PR A — /mcp + hook relay onto Unix socket (minor, breaking transport)
- #288 ADR-058 — deprecate agent capabilities (minor)
- #278 ADR-053 — compaction status recovery (patch)
- #307 — MCP_INSTRUCTIONS rewrite + drift-guard (patch)
- #308 ADR-068 — usage-queue per-tab/per-runtime auto-Enter (minor)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SZXoaHsvgp4DrbrHdp9bm

* fix(changeset): #308 is server+dashboard and per-provider, not Claude-scoped

Per nox review on #309:
- add @autonomos/server (the bulk of #308 — usageQueue.ts, routes, the
  NormalizedUsage core + normalizeClaude/normalizeCodex adapters — is server;
  changelog-github only files into named packages' CHANGELOGs)
- rewrite the body to describe the fix (per-provider, per-tab, per-runtime)
  rather than the bug, and drop the backwards Claude-scoped framing (ADR-068
  was specifically about de-Claude-ing it)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SZXoaHsvgp4DrbrHdp9bm

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
aterrylu added a commit that referenced this pull request Aug 8, 2026
Per nox review on #275: the retroactive changesets (#309) all attribute to
#309 and collapse to one line, so #284/#288/#278/#307/#308 were absent from
0.5.0 — including #284, a breaking MCP transport change. Hand-add them to the
right buckets (the generated Version PR's normal escape hatch), and correct
#302's ADR-065 -> ADR-067 (inherited from the merge-commit subject).
aterrylu added a commit that referenced this pull request Aug 8, 2026
* chore(release): version packages

* docs(changelog): surface the 5 collapsed PRs + fix #302 ADR label

Per nox review on #275: the retroactive changesets (#309) all attribute to
#309 and collapse to one line, so #284/#288/#278/#307/#308 were absent from
0.5.0 — including #284, a breaking MCP transport change. Hand-add them to the
right buckets (the generated Version PR's normal escape hatch), and correct
#302's ADR-065 -> ADR-067 (inherited from the merge-commit subject).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants