feat(usage-queue): per-tab + per-runtime auto-Enter (ADR-068) - #308
Merged
Conversation
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
enabled auto-merge (squash)
August 7, 2026 07:44
nox-0x
reviewed
Aug 7, 2026
nox-0x
reviewed
Aug 7, 2026
nox-0x
reviewed
Aug 7, 2026
nox-0x
reviewed
Aug 7, 2026
nox-0x
approved these changes
Aug 7, 2026
nox-0x
left a comment
Collaborator
There was a problem hiding this comment.
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/resetsAtscalars →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,seenBlockedis 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+ injectedprobesis 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)
- 🟡
GET /api/usage-queueunconditionally 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. - 🟡 A stale Codex rollout fallback carries no
errorKind, so it normalizes to non-empty windows withauthError: false. Frozen at ≥90% (expired token, or/wham/usagedown), 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. - 🟡
useUsageQueue.dom.test.tsxwas left on the old contract (one-arg call,{capped, resetsAt}body). It passes vacuously because tsconfig excludes test files fromtscand neither case assertscapped— so the client half of the fix (s.caps[provider]) is currently untested, with the button DOM tests mocking the hook wholesale. - 🟢
POST /:sessionIdreturns{ armed: true }even whenarm()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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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"]claude-usage, Codex viacodex-usage. Gemini has no rolling-window source → can't arm, button never shows.NormalizedUsage+ injectedprobes);normalizeClaudeUsage/normalizeCodexUsageadapt each plugin's snapshot. All original invariants kept — re-entrancy guard, hysteresis, per-paneseenBlockedlatch, notify-on-drop — just keyed per provider.GETreturns per-providercaps;SessionPane→UsageQueueButton(provider)→useUsageQueue(sessionId, provider)reads only its pane's cap. Per-tab is then correct by construction.Testing
make checkgreen — server suite (20 usage-queue tests) + 362 dashboard tests, biome + tsc clean.evaluateCap, and the gemini no-op.Checklist
make checkpasses locallyfeat:)make hero🤖 Generated with Claude Code