Skip to content

feat(composer): show provider usage limits and stop the send button jumping - #64

Open
gfsaaser24 wants to merge 3 commits into
mainfrom
t3code/duplicate-pull-request
Open

feat(composer): show provider usage limits and stop the send button jumping#64
gfsaaser24 wants to merge 3 commits into
mainfrom
t3code/duplicate-pull-request

Conversation

@gfsaaser24

@gfsaaser24 gfsaaser24 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Note

Update: also cherry-picked fix(usage): address review findings on the usage meters from the upstream submission (pingdotgg#5945), which fixes four review findings: the Android usage menu never firing its own onPress (now uses ControlPillMenu's render-function child), the web popover titling one provider's quota with another's name, claimRefreshSlot wedging when the wall clock moves backwards, and fetchClaudeUsage's timeout not covering a trickled response body. One conflict hand-resolved in ControlPill.tsx (main's iOS branch lacks the upstream long-press pass-through, so only the render-function support was taken). Typecheck and affected suites re-verified clean.

Note

Duplicate of #54, ported to main. That PR merged into turbo only; main didn't have the feature. Both commits were cherry-picked onto main with three files hand-resolved where the branches had diverged (ComposerPrimaryActions.tsx + its test, and the mobile ThreadComposer.tsx — main lacks the turbo-only settings sheet and orb context, so the mobile delta was re-applied against main's toolbar). Port verification: typecheck clean across contracts, client-runtime, server, web, and mobile (0 errors); the PR's test suites pass (18 client-runtime, 20 web, 61 server) with ProviderRegistry.test.ts at its documented Windows-flaky baseline (19–21 failures, unrelated to this change).

Original PR description follows.


Implements docs/superpowers/specs/2026-08-09-composer-usage-limits-design.md.

The problem

The composer never showed how much of your Claude or Codex subscription was left — even though both adapters already emit an account.rate-limits.updated runtime event that nothing consumed. The pipe was wired to a wall.

Separately, the primary action changed size the moment a turn started: send is 36px below the sm breakpoint, stop was 32px. The button and the meters beside it shifted at every turn boundary.

What changed

Usage limits. One provider-agnostic window type ({ id, label, usedPercent, resetsAt }) on ServerProvider. Two feeds write into a usage store: the free turn events, and a 60s-debounced pull for Claude, whose numbers only ever arrive on request. Claude renders session / weekly / Fable, Codex its two windows, and Cursor / Grok / OpenCode render nothing at all — no placeholder, so switching providers costs no layout shift. Compact mode collapses to one circle showing the bucket closest to running out.

Stop-button containment. The primary action now sits in a fixed slot, identical in every state, relative and non-clipping so a running-state overlay can be drawn over it contributing zero layout.

Notes for review

  • Two concerns in one PR, per the spec: they touch the same container, and the meters make the button jump worse. Say the word and I'll split them.
  • No before/after images. I don't spin up browsers without being asked. The visible deltas are: three new circles left of the send button, and the stop button growing 36px→ matching send below sm.
  • The orb doesn't exist on this branch. apps/web/src/turbo/orbs/ is on turbo-beta, not here, so the flex-col stacking bug the spec describes isn't present. The slot is built to the spec's shape so the orb drops in with no further layout work.
  • Spec open question feat: self-host T3 Connect relay + resilient release builds #1 answered: the SDK's rate_limit_event does not break out Fable — it carries one bucket per event and has no Fable bucket type. Fable populates from the pull only, as the spec anticipated.
  • Deviations: Codex reads its cold-start usage on the app-server connection the status probe already opens, rather than spawning a process for a separate pull. Mobile uses a native menu with the percentage and reset in each row's subtitle rather than tap-to-expand — the reset fits, so the second tap wasn't needed.

Review pass

Four parallel Opus 5 reviewers went over this (CodeRabbit CLI isn't installed on this machine). Nine real defects found and fixed, including three that would have shipped broken:

  • Claude's turn-event feed could never produce a reading — normalizeClaudeUsage expected a bucketed payload, but the SDK sends a flat single-bucket rate_limit_info. The test that "covered" it asserted against an invented shape. Now normalized properly, with a test at the actual adapter→normalizer seam.
  • Sparse readings blanked meters. Codex documents its rolling update as sparse and Claude's events are single-bucket, so the store now merges by window id; only full readings retire a window.
  • The Codex usage read could hang and take the whole status probe with it, dropping Codex to no models and no skills. It now has its own 2s deadline.
  • Credential selection read the wrong account in two multi-instance cases (global macOS keychain used for a scoped instance; ambient CLAUDE_CONFIG_DIR ignored). Now reuses the canonical resolver.
  • Every rate-limit event re-broadcast the entire provider array to every client, once per turn, because the server-generated updatedAt defeated the change check.
  • Mobile never triggered a refresh at all, so Claude meters never appeared in a phone-only session.

Part B was verified clean at every breakpoint. One pre-existing gap left alone: the pending-question branch still uses a 32px control, so it retains a 4px jump — out of scope here.

Verification

Typecheck clean across contracts, client-runtime, server, and web. Mobile unchanged at its pre-existing 64 navigation-typing errors. Lint and format clean. 62 tests pass across six files. ProviderRegistry and server suites match baseline — that suite is flaky on Windows independent of this change (baseline 19–21 failures, this branch 19–20).

Not run: the app itself. No browser verification without your say-so.

Model: Claude Opus 5 (1M context), harness: Claude Code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added provider usage meters showing quota percentages, reset times, and stale-data indicators.
    • Added usage details to web and mobile composers, including compact and expanded views.
    • Added support for retrieving usage data from Claude and Codex providers.
    • Added best-effort usage refresh when opening meters, focusing the app, or returning to mobile.
    • Added an activity indicator behind the stop-generation button without shifting layout.
    • Added Android rendering support for the gauge control icon.
  • Documentation
    • Added documentation covering usage meters, supported providers, refresh behavior, and interpreting readings.

Gabe Fletcher and others added 2 commits August 10, 2026 19:38
…umping

The composer had no way to tell you how much of your Claude or Codex
subscription you had left, even though both adapters already emit an
`account.rate-limits.updated` runtime event that nothing consumed. Separately,
the primary action changed size the moment a turn started — send is 36px below
the `sm` breakpoint, stop was 32px — so the button and the meters beside it
shifted at every turn boundary.

Reconnects the existing event feed into a new provider usage store, adds a
debounced pull for Claude (whose numbers only ever arrive on request), and
renders the result as small circles beside the context-window meter. Claude
shows session/weekly/Fable, Codex shows its two windows, and providers that
report nothing render nothing. The primary action now sits in a fixed slot
that is the same size in every state, with room for a running-state overlay to
be drawn over it without contributing layout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…labels

CodeRabbit review pass on the usage-meter work. The real ones: a Claude
credentials file that stores `expiresAt` in epoch seconds was read as
milliseconds, so every such token looked decades expired and the meters
stayed permanently blank; `Number("")` is 0, so a provider reporting an
empty percentage drew a confident "0% used"; the popover's reset labels
were computed against epoch zero before it opened; a defect handling one
usage event could take the whole ingestion feed down for the life of the
process; the composer slot test passed when neither side matched; and the
glossary's new links redefined reference labels [20] and [21], silently
repointing the checkpointing entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds provider quota usage contracts, normalization, volatile server storage, runtime ingestion, refresh RPCs, and usage meters to web and mobile composers. It supports Claude and Codex usage sources and adds related tests and documentation.

Changes

Provider usage reporting

Layer / File(s) Summary
Usage contracts and client utilities
packages/contracts/src/server.ts, packages/contracts/src/rpc.ts, packages/client-runtime/src/state/*
Defines normalized usage windows, selection and formatting utilities, and a single-flight usage refresh RPC command.
Provider normalization and storage
apps/server/src/provider/usageLimits.ts, apps/server/src/provider/claudeUsageFetch.ts, apps/server/src/provider/Layers/ProviderUsageLimits.ts, apps/server/src/provider/Layers/CodexProvider.ts
Normalizes Claude and Codex quota data, reads Claude credentials and OAuth usage, merges full and partial readings, and stores usage per provider instance.
Runtime ingestion and refresh flow
apps/server/src/provider/Layers/*, apps/server/src/provider/ProviderRegistry.ts, apps/server/src/server.ts, apps/server/src/ws.ts
Ingests runtime rate-limit events, performs background refreshes, decorates provider snapshots, publishes volatile usage updates, and exposes the authorized refresh RPC.
Web composer meters and action layout
apps/web/src/components/chat/*
Adds responsive usage meters, refresh behavior, stale and reset labels, and fixed send/stop action slots with optional activity-orb rendering.
Mobile presentation and documentation
apps/mobile/src/features/threads/ThreadComposer.tsx, apps/mobile/src/components/*, docs/*
Adds mobile usage menus and foreground refreshes, maps the gauge symbol on Android, and documents usage windows and meter behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: juliusmarminge

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes both primary changes: provider usage limits and stable send-button layout.
Description check ✅ Passed The description clearly explains the changes, rationale, UI impact, deviations, and verification, but omits the template headings, checklist, and requested screenshots or video.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/duplicate-pull-request

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/provider/claudeUsageFetch.ts`:
- Around line 165-176: Update the request effect around client.execute so
Effect.timeoutOption(USAGE_REQUEST_TIMEOUT_MS) encompasses response status
validation and httpResponse.json decoding, ensuring stalled bodies also time
out. Preserve the existing null result for timeouts, non-2xx responses, and JSON
failures, and keep the effect requirements limited to HttpClient.HttpClient
without introducing Scope.

In `@apps/server/src/provider/Layers/ProviderRegistry.ts`:
- Around line 369-385: The applyProviderUsageLimits flow currently retains stale
usage limits when a same-ID provider rebuild omits usageLimits. Clear the
usageStore entry before applying limits for rebuilt instances, clear it when an
instance is removed, and add a regression test covering a same-instanceId
rebuild with no new usageLimits so the published provider does not reuse prior
values.

In `@apps/server/src/provider/usageLimits.ts`:
- Around line 90-95: Update the alias selection in readClaudeBucket so each
percent field is passed through readPercent in order, falling through to
utilization, usedPercentage, and percent when an earlier value is present but
unreadable. Use the first non-null parsed result, and return null only when none
of the aliases parses successfully.

In `@apps/web/src/components/chat/ChatComposer.tsx`:
- Around line 3256-3258: Update the usage-meter props near usageWindows and
usageUpdatedAt to derive the displayed provider name from selectedProviderEntry
or selectedInstanceId, matching the usage selection logic around the existing
usage lookup. Do not use activeThreadProviderDisplayName, which can remain stale
after an unsaved picker change, and add coverage for switching providers before
sending.

In `@apps/web/src/components/chat/ComposerPrimaryActions.tsx`:
- Around line 20-25: Update the production ChatComposer call to
ComposerPrimaryActions to pass the intended activity node through activityOrb,
preserving the shared visibility condition with the stop button; alternatively
remove activityOrb and its isolated test if no production activity indicator is
intended.

In `@docs/user/usage-meters.md`:
- Around line 28-31: Update the usage-meter documentation text around the hover
description to say “displayed percentage” instead of “exact percentage,”
matching the value produced by formatUsagePercent and the mobile usage rows.

In `@packages/contracts/src/server.ts`:
- Around line 180-188: Update ProviderUsageLimitsStore.set so accepted refreshes
publish updatedAt changes even when windows are unchanged, while retaining
deduplication for unchanged full provider snapshots. Ensure client usageLimits
receives the new timestamp so formatUsageUpdatedAtLabel reflects the latest
successful refresh.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f3d87ff3-6504-4ec8-b57f-80ab7e3d6c28

📥 Commits

Reviewing files that changed from the base of the PR and between 24d2564 and 1700076.

📒 Files selected for processing (33)
  • apps/mobile/src/components/AppSymbol.tsx
  • apps/mobile/src/features/threads/ThreadComposer.tsx
  • apps/server/src/auth/RpcAuthorization.ts
  • apps/server/src/provider/Drivers/ClaudeSkills.ts
  • apps/server/src/provider/Layers/CodexProvider.ts
  • apps/server/src/provider/Layers/ProviderRegistry.test.ts
  • apps/server/src/provider/Layers/ProviderRegistry.ts
  • apps/server/src/provider/Layers/ProviderUsageIngestion.test.ts
  • apps/server/src/provider/Layers/ProviderUsageIngestion.ts
  • apps/server/src/provider/Layers/ProviderUsageLimits.test.ts
  • apps/server/src/provider/Layers/ProviderUsageLimits.ts
  • apps/server/src/provider/Layers/ProviderUsageRefresher.ts
  • apps/server/src/provider/Services/ProviderUsageLimits.ts
  • apps/server/src/provider/claudeUsageFetch.test.ts
  • apps/server/src/provider/claudeUsageFetch.ts
  • apps/server/src/provider/usageLimits.test.ts
  • apps/server/src/provider/usageLimits.ts
  • apps/server/src/server.test.ts
  • apps/server/src/server.ts
  • apps/server/src/ws.ts
  • apps/web/src/components/chat/ChatComposer.tsx
  • apps/web/src/components/chat/ComposerPrimaryActions.test.ts
  • apps/web/src/components/chat/ComposerPrimaryActions.tsx
  • apps/web/src/components/chat/UsageLimitsMeters.test.tsx
  • apps/web/src/components/chat/UsageLimitsMeters.tsx
  • docs/internals/glossary.md
  • docs/user/usage-meters.md
  • packages/client-runtime/package.json
  • packages/client-runtime/src/state/providerUsage.test.ts
  • packages/client-runtime/src/state/providerUsage.ts
  • packages/client-runtime/src/state/server.ts
  • packages/contracts/src/rpc.ts
  • packages/contracts/src/server.ts

Comment thread apps/server/src/provider/claudeUsageFetch.ts Outdated
Comment on lines +369 to +385
const applyProviderUsageLimits = Effect.fn("applyProviderUsageLimits")(function* (
provider: ServerProvider,
) {
if (provider.usageLimits !== undefined) {
yield* usageStore.set(provider.instanceId, provider.usageLimits, "full");
}
const usageLimits = yield* usageStore.get(provider.instanceId);
if (!usageLimits) {
const { usageLimits: _usageLimits, ...providerWithoutUsage } = provider;
return providerWithoutUsage;
}
return { ...provider, usageLimits };
});

const decorateProvider = Effect.fn("decorateProvider")(function* (provider: ServerProvider) {
return yield* applyProviderUsageLimits(yield* applyProviderUpdateState(provider));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear retained usage when an instance is rebuilt or removed.

When a rebuilt instance keeps its instanceId and its snapshot omits usageLimits, Line 375 restores the prior store entry. The registry can then publish limits from the previous account or driver configuration.

Clear the usage-store entry before accepting a rebuilt instance. Clear it when an instance is removed. Add a regression test for a same-ID instance rebuild with no new usage reading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Layers/ProviderRegistry.ts` around lines 369 - 385,
The applyProviderUsageLimits flow currently retains stale usage limits when a
same-ID provider rebuild omits usageLimits. Clear the usageStore entry before
applying limits for rebuilt instances, clear it when an instance is removed, and
add a regression test covering a same-instanceId rebuild with no new usageLimits
so the published provider does not reuse prior values.

Comment on lines +90 to +95
const usedPercent = readPercent(
value.used_percentage ?? value.utilization ?? value.usedPercentage ?? value.percent,
);
if (usedPercent === null) {
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fall through to the next alias when a percent field is present but unreadable.

?? stops at the first non-null value. If used_percentage is "" or a non-numeric string, the chain never reaches utilization, and readClaudeBucket drops the whole bucket. Select the first alias that parses instead.

🐛 Proposed fix
-  const usedPercent = readPercent(
-    value.used_percentage ?? value.utilization ?? value.usedPercentage ?? value.percent,
-  );
+  const usedPercent = [
+    value.used_percentage,
+    value.utilization,
+    value.usedPercentage,
+    value.percent,
+  ].reduce<number | null>((found, candidate) => found ?? readPercent(candidate), null);
   if (usedPercent === null) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const usedPercent = readPercent(
value.used_percentage ?? value.utilization ?? value.usedPercentage ?? value.percent,
);
if (usedPercent === null) {
return null;
}
const usedPercent = [
value.used_percentage,
value.utilization,
value.usedPercentage,
value.percent,
].reduce<number | null>((found, candidate) => found ?? readPercent(candidate), null);
if (usedPercent === null) {
return null;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/usageLimits.ts` around lines 90 - 95, Update the
alias selection in readClaudeBucket so each percent field is passed through
readPercent in order, falling through to utilization, usedPercentage, and
percent when an earlier value is present but unreadable. Use the first non-null
parsed result, and return null only when none of the aliases parses
successfully.

Comment thread apps/web/src/components/chat/ChatComposer.tsx
Comment on lines +20 to +25
/**
* Activity indicator shown beside the stop button. Passed in rather than
* derived here: this component stays a dumb renderer, and the orb and the
* stop button share one condition, so they appear and disappear together.
*/
activityOrb?: ReactNode;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show every declaration and production call site that references activityOrb.
rg -n -C 4 '\bactivityOrb\b' apps/web

Repository: gfsaaser24/t3code

Length of output: 4507


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ComposerPrimaryActions references ---'
rg -n -C 3 '\bComposerPrimaryActions\b' .

printf '%s\n' '--- ChatComposer relevant sections ---'
rg -n -C 8 'ComposerPrimaryActions|activity|orb' apps/web/src/components/chat/ChatComposer.tsx

Repository: gfsaaser24/t3code

Length of output: 12848


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Direct caller at ChatComposer.tsx:443 ---'
sed -n '420,475p' apps/web/src/components/chat/ChatComposer.tsx

printf '%s\n' '--- Mobile callers ---'
sed -n '2828,2875p' apps/web/src/components/chat/ChatComposer.tsx
sed -n '3115,3165p' apps/web/src/components/chat/ChatComposer.tsx

printf '%s\n' '--- Footer primary-actions composition ---'
sed -n '3238,3295p' apps/web/src/components/chat/ChatComposer.tsx
rg -n -C 6 'function ComposerFooterPrimaryActions|const ComposerFooterPrimaryActions|export .*ComposerFooterPrimaryActions' apps/web/src/components/chat

Repository: gfsaaser24/t3code

Length of output: 10620


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '390,425p' apps/web/src/components/chat/ChatComposer.tsx
rg -n -i -C 4 'activity.*orb|orb.*activity|ActivityOrb|activityOrb' apps/web/src

Repository: gfsaaser24/t3code

Length of output: 9637


Connect activityOrb from a production caller.

All production ComposerPrimaryActions call sites omit activityOrb. The orb therefore renders only in the unit test and is absent from the web composer. Pass the intended activity node from ChatComposer, or remove the unused prop and isolated test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/chat/ComposerPrimaryActions.tsx` around lines 20 -
25, Update the production ChatComposer call to ComposerPrimaryActions to pass
the intended activity node through activityOrb, preserving the shared visibility
condition with the stop button; alternatively remove activityOrb and its
isolated test if no production activity indicator is intended.

Comment thread docs/user/usage-meters.md
Comment on lines +28 to +31
Hover a circle for the exact percentage and when that limit resets. Resets
less than a day away count down ("resets in 2h 15m"); further out they show
the day and time ("resets Mon 9:00 AM"), and beyond a couple of days the date
comes along too.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Describe the value as the displayed percentage.

formatUsagePercent rounds values of 10% or more to whole percentages and smaller values to one decimal place. The mobile usage rows use this formatter, so the UI does not expose the raw percentage. Replace “exact percentage” with “displayed percentage” unless the detail view shows the raw value.

Proposed wording
-Hover a circle for the exact percentage and when that limit resets.
+Hover a circle for the displayed percentage and when that limit resets.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Hover a circle for the exact percentage and when that limit resets. Resets
less than a day away count down ("resets in 2h 15m"); further out they show
the day and time ("resets Mon 9:00 AM"), and beyond a couple of days the date
comes along too.
Hover a circle for the displayed percentage and when that limit resets. Resets
less than a day away count down ("resets in 2h 15m"); further out they show
the day and time ("resets Mon 9:00 AM"), and beyond a couple of days the date
comes along too.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/user/usage-meters.md` around lines 28 - 31, Update the usage-meter
documentation text around the hover description to say “displayed percentage”
instead of “exact percentage,” matching the value produced by formatUsagePercent
and the mobile usage rows.

Comment on lines +180 to +188
/**
* Account-level quota usage for one configured provider instance.
* `updatedAt` answers the only staleness question the UI asks ("as of when?").
*/
export const ProviderUsageLimits = Schema.Struct({
windows: Schema.Array(ProviderUsageWindow),
updatedAt: IsoDateTime,
});
export type ProviderUsageLimits = typeof ProviderUsageLimits.Type;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep updatedAt synchronized with accepted refreshes.

ProviderUsageLimitsStore.set stores the new timestamp but suppresses publication when windows are unchanged. The client then retains the old usageLimits.updatedAt. After a successful refresh with unchanged usage, formatUsageUpdatedAtLabel can report stale data.

Publish timestamp-only changes, or add a lightweight usage freshness update path. Preserve full provider-snapshot deduplication separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/contracts/src/server.ts` around lines 180 - 188, Update
ProviderUsageLimitsStore.set so accepted refreshes publish updatedAt changes
even when windows are unchanged, while retaining deduplication for unchanged
full provider snapshots. Ensure client usageLimits receives the new timestamp so
formatUsageUpdatedAtLabel reflects the latest successful refresh.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 11.3 KiB 11.3 KiB −2 B (−0.0%) 15.1 KiB
Codex Thread snapshot wire 5.5 KiB 5.5 KiB +1 B (+0.0%) 7.3 KiB
Codex Live turn WebSocket wire 5.9 KiB 5.9 KiB −3 B (−0.0%) 7.8 KiB
Codex Live turn WebSocket decoded 49.7 KiB 49.7 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 16 16 0 (0.0%) 21
Claude Total thread wire 11.3 KiB 11.3 KiB +13 B (+0.1%) 15.1 KiB
Claude Thread snapshot wire 5.5 KiB 5.5 KiB +6 B (+0.1%) 7.3 KiB
Claude Live turn WebSocket wire 5.9 KiB 5.9 KiB +7 B (+0.1%) 7.8 KiB
Claude Live turn WebSocket decoded 50.6 KiB 50.6 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 16 16 0 (0.0%) 21

Baseline: 24d2564 · PR result: 861c88c · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 94.6 KiB
  • Claude decoded thread snapshot: 95.3 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Four real ones from the bot pass:

- The Android usage menu never ran its own onPress. `AndroidAnchoredMenu`
  wraps a plain child in a `pointerEvents="none"` view so the anchor owns
  the tap, so opening the menu never restamped the relative labels or
  asked for a fresh reading. `ControlPillMenu` now also accepts the
  render-function child form, which keeps the trigger interactive there.
- The web popover titled one provider's quota with another's name. The
  meters read the selected instance (the choice for the next turn) while
  the title read the thread's persisted provider, so picking a new model
  swapped the numbers but not the label.
- `claimRefreshSlot` wedged when the wall clock moved backwards: a
  negative elapsed read as "still inside the debounce window" and blocked
  refreshes until the clock caught up.
- `fetchClaudeUsage` only bounded `client.execute`, so a 2xx response
  with a trickled body parked the refresh fiber past the timeout.

Written by Claude Opus 5 in Claude Code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant