feat(eta): weekly-cap ETA projection for Pro/Max users [GET-21]#58
Conversation
Pure function computeWeeklyEta: linear fit over usage snapshots, R²-based confidence, flat/declining/boundary guards. 21 unit tests, all passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
appendUsageBudgetSnapshot / getUsageBudgetSnapshots / clearUsageBudgetSnapshots with 200-entry cap per org. 8 new tests covering append, ordering, pruning, isolation, and reset-clear path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…TS [GET-21] After each usage-limits store, appends a snapshot to the org's ring buffer. Detects weekly reset (drop >5pp) and clears stale snapshots automatically. Session-tier only; credit variant is skipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
applyWeeklyEta on OverlayState; overlay renders .lco-weekly-eta div with
"~{day} at this pace" copy, hidden for credit tier and when eta is null.
6 new overlay tests covering visibility, text, and tier-gating.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…etch [GET-21] computeEtaForOrg reads snapshots from chrome.storage.local and feeds computeWeeklyEta; result applied to overlay via applyWeeklyEta on both ORGANIZATION_DETECTED and post-STREAM_COMPLETE paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useDashboardData computes WeeklyEta from stored snapshots on every load and storage change; App threads weeklyEta to UsageBudgetCard; SessionBudget renders confidence-aware copy (high/medium/low). 7 new card tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tests [GET-21] Export usageBudgetSnapshotsKey from conversation-store so content script uses the same key builder instead of a hardcoded string. Switch chrome.storage.local to browser.storage.local (WXT polyfill). Add 5 new tests: reset-detection invariant (>5pp clear, exact-5 no-clear, rising growth), key-format contract, and storage-read parity for computeEtaForOrg. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@DevanshuNEU is attempting to deploy a commit to the Dev's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a weekly-cap ETA feature: persist timestamped usage snapshots, compute ETA via linear regression with confidence, surface ETA in overlay and sidepanel, and wire snapshot capture into background usage-limit handling and dashboard loading. Changes
Sequence Diagram(s)sequenceDiagram
participant BG as Background Service
participant Store as Browser Storage
participant CS as Content Script
participant Dashboard as Dashboard Hook
participant UI as Sidepanel UI
BG->>BG: STORE_USAGE_LIMITS received (session tier)
BG->>Store: appendUsageBudgetSnapshot(accountId, {timestamp, weeklyPct, sessionPct})
Store-->>BG: Snapshot appended (overflow pruned)
BG->>Store: detect sevenDayUtilization drop
alt Reset Detected
BG->>Store: clearUsageBudgetSnapshots(accountId)
end
CS->>Store: computeEtaForOrg() reads snapshots on usage fetch
Store-->>CS: snapshots[]
CS->>CS: computeWeeklyEta(snapshots, now) -> WeeklyEta|null
CS->>CS: applyWeeklyEta(state, eta)
Dashboard->>Store: loadWeeklyEta() on init/tab-change
Store-->>Dashboard: snapshots[]
Dashboard->>Dashboard: computeWeeklyEta() -> weeklyEta
UI->>Dashboard: reads weeklyEta from useDashboardData()
UI->>UI: if weeklyEta exists and budget is session -> render ETA line
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 52 minutes and 7 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
entrypoints/claude-ai.content.ts (1)
713-725:⚠️ Potential issue | 🔴 CriticalBring the token-count bridge under the same 5-layer validation.
This listener accepts any same-window
LCO_TOKEN_REQ, so a page script can triggerCOUNT_TOKENSwithout the origin, namespace, token, or schema checks used everywhere else in this file.Suggested guard
window.addEventListener('message', (event) => { - if (event.source !== window || !event.data || event.data.type !== 'LCO_TOKEN_REQ') return; + if (event.origin !== window.location.origin) return; + if (event.source !== window) return; + if (!event.data || event.data.namespace !== LCO_NAMESPACE) return; + if (event.data.token !== sessionToken) return; + if (event.data.type !== 'LCO_TOKEN_REQ') return; const { id, text } = event.data;As per coding guidelines: Validate all incoming postMessages with 5 layers: origin check, source check, namespace LCO_V1, session token (UUID v4 per load), schema validation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@entrypoints/claude-ai.content.ts` around lines 713 - 725, The message listener registered with window.addEventListener('message') for LCO_TOKEN_REQ currently forwards requests to browser.runtime.sendMessage(COUNT_TOKENS) without the five-layer validation; update the handler to: verify event.origin equals window.location.origin, keep the existing event.source === window check, require event.data.namespace === 'LCO_V1', require a session token field in event.data that matches the in-memory session UUID used across this file (e.g. the file's session token variable), and run the existing schema validator for token requests (or add a small validateTokenRequestSchema helper) to ensure id/text shape before calling browser.runtime.sendMessage({ type: 'COUNT_TOKENS', text }). If any check fails, do not call COUNT_TOKENS, log a warning/error, and respond with LCO_TOKEN_RES containing count: 0 (or no response) as per current failure behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@entrypoints/background.ts`:
- Around line 423-453: The get -> maybe clear -> append sequence inside the
async message handler (the block that reads message.kind === 'session' and calls
getUsageBudgetSnapshots, clearUsageBudgetSnapshots, and
appendUsageBudgetSnapshot) must be serialized per organization and funneled
through the existing 200ms batching mechanism rather than executed directly;
change the handler to enqueue the org-scoped work (or acquire a per-org mutex)
and then perform getUsageBudgetSnapshots -> conditional
clearUsageBudgetSnapshots -> appendUsageBudgetSnapshot from the batcher/queue so
concurrent STORE_USAGE_LIMITS messages cannot interleave and all storage writes
respect the 200ms postMessage/storage batching guideline.
In `@entrypoints/claude-ai.content.ts`:
- Around line 366-376: The code is vulnerable to using a stale currentOrgId when
async work completes; capture the org id into a local variable immediately
before calling fetchAndStoreUsageLimits/computeEtaForOrg (e.g., const orgId =
currentOrgId) and, after any await (especially around computeEtaForOrg), verify
orgId === currentOrgId and abort mutating state or calling
applyWeeklyEta/overlay.render if it no longer matches; apply the same pre- and
post-await stale-org guard inside the STREAM_COMPLETE callback as well so you
never apply an ETA or update state for a different org.
In `@entrypoints/sidepanel/hooks/useDashboardData.ts`:
- Around line 275-288: Clear weeklyEta whenever the active org changes: add a
small effect that watches the authoritative org-id state (not orgIdRef.current —
use the component/state value used elsewhere for the current org) and calls
setWeeklyEta(null) whenever that id becomes falsy or changes; also, if you call
loadWeeklyEta from loadActiveConversation, add loadBudget and loadWeeklyEta to
loadActiveConversation's dependency list so the loader is re-run on org switches
(references: loadWeeklyEta, setWeeklyEta, loadBudget, loadActiveConversation,
orgIdRef).
In `@lib/conversation-store.ts`:
- Around line 830-847: appendUsageBudgetSnapshot performs an unsafe
read-modify-write on the array returned by store().get which can be clobbered by
concurrent append calls; fix it by serializing writes per account (e.g.,
introduce a per-account mutex or queue keyed by
usageBudgetSnapshotsKey(accountId)) so only one operation reads, mutates and
calls store().set at a time, update appendUsageBudgetSnapshot to acquire/release
that lock around the existing get/modify/set logic and still enforce
MAX_USAGE_BUDGET_SNAPSHOTS trimming.
In `@tests/unit/weekly-cap-eta.test.ts`:
- Around line 226-237: The weekday assertion is brittle because it expects
English abbreviations while formatEtaLabel uses
Intl.DateTimeFormat(undefined,...), so update the test in
tests/unit/weekly-cap-eta.test.ts to derive the expected weekday from the
runtime formatter instead of hard-coding English; compute expectedWeekday using
new Intl.DateTimeFormat(undefined, { weekday: 'short' }).format(new Date(BASE +
6 * HOUR_MS)) and assert that formatEtaLabel(BASE + 6 * HOUR_MS) contains that
expectedWeekday (referencing formatEtaLabel, BASE and HOUR_MS).
---
Outside diff comments:
In `@entrypoints/claude-ai.content.ts`:
- Around line 713-725: The message listener registered with
window.addEventListener('message') for LCO_TOKEN_REQ currently forwards requests
to browser.runtime.sendMessage(COUNT_TOKENS) without the five-layer validation;
update the handler to: verify event.origin equals window.location.origin, keep
the existing event.source === window check, require event.data.namespace ===
'LCO_V1', require a session token field in event.data that matches the in-memory
session UUID used across this file (e.g. the file's session token variable), and
run the existing schema validator for token requests (or add a small
validateTokenRequestSchema helper) to ensure id/text shape before calling
browser.runtime.sendMessage({ type: 'COUNT_TOKENS', text }). If any check fails,
do not call COUNT_TOKENS, log a warning/error, and respond with LCO_TOKEN_RES
containing count: 0 (or no response) as per current failure behavior.
🪄 Autofix (Beta)
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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a3fc5c06-9aed-4f3d-920b-1186d982e932
📒 Files selected for processing (13)
entrypoints/background.tsentrypoints/claude-ai.content.tsentrypoints/sidepanel/App.tsxentrypoints/sidepanel/components/UsageBudgetCard.tsxentrypoints/sidepanel/hooks/useDashboardData.tslib/conversation-store.tslib/overlay-state.tslib/weekly-cap-eta.tstests/unit/conversation-store.test.tstests/unit/overlay-weekly-cap.test.tstests/unit/usage-budget-card.test.tsxtests/unit/weekly-cap-eta.test.tsui/overlay.ts
- claude-ai.content.ts: capture orgId before async computeEtaForOrg call in ORGANIZATION_DETECTED handler; add stale-org guard after await so a rapid org switch cannot apply ETA from the previous account - useDashboardData.ts: clear weeklyEta on logout, trigger loadBudget + loadWeeklyEta when org changes inside loadActiveConversation; move loadBudget/loadWeeklyEta declarations above loadActiveConversation so they are in scope for the dependency array - weekly-cap-eta.test.ts: replace hardcoded English weekday regex with runtime Intl.DateTimeFormat so the test passes on any locale Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
tests/unit/weekly-cap-eta.test.ts (1)
131-144: ⚡ Quick winMake the short-span flatness test assert a single expected outcome.
The current assertion accepts both
nulland non-null, so it will still pass if the wrong guard path is taken.Proposed test tightening
it('does not return null for a flat span shorter than 24h', () => { - // 10 snapshots over 10 hours, flat (< 24h so flatness guard does not apply) - const snaps = Array.from({ length: 10 }, (_, i) => - snap(BASE + i * HOUR_MS, 50 + (i % 2)), // only 1pp range, but <24h - ); + // 10 snapshots over 10 hours, small positive slope and <10pp range. + // This should bypass the 24h flatness guard and still produce an ETA. + const snaps = Array.from({ length: 10 }, (_, i) => + snap(BASE + i * HOUR_MS, 50 + i * 0.8), // range 7.2pp, span 9h + ); const now = nowAfter(10); - // Slope may be near zero, so null is acceptable — the key assertion is - // that the flatness guard (24h + <10pp) does NOT fire here. - // We just confirm the guard itself does not reject short spans. - // (A near-zero slope will still return null via the slope <= 0 guard.) const result = computeWeeklyEta(snaps, now); - // Either null (slope guard) or non-null (tiny positive slope): both are valid. - expect(result === null || result.hoursRemaining > 0).toBe(true); + expect(result).not.toBeNull(); + expect(result!.hoursRemaining).toBeGreaterThan(0); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/weekly-cap-eta.test.ts` around lines 131 - 144, The test currently allows both null and non-null outcomes which masks whether the flatness guard was correctly bypassed; change the synthetic snapshots created in the test (the snaps array built with snap and BASE/HOUR_MS) to produce a clear positive slope (e.g., steadily increasing values by a few percentage points across the 10-hour span) so the slope <= 0 guard cannot return null, then assert a single expected outcome: result is non-null and result.hoursRemaining > 0 by calling computeWeeklyEta(snaps, nowAfter(10)). Ensure you only modify the values used to construct snaps (not computeWeeklyEta) so the test deterministically verifies the flatness guard is not triggered for short spans.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@entrypoints/sidepanel/hooks/useDashboardData.ts`:
- Around line 211-224: The loadWeeklyEta async handler can race with
navigation/tab switches and repopulate weeklyEta after it was intentionally
cleared; fix by adding a request-scoped guard: introduce a weeklyEtaRequestIdRef
(incremented whenever you start or explicitly clear weeklyEta), capture its
value at the top of loadWeeklyEta, and before calling setWeeklyEta verify both
orgIdRef.current === orgId and weeklyEtaRequestIdRef.current === capturedId;
also increment weeklyEtaRequestIdRef when you clear weeklyEta elsewhere so
in-flight resolutions are ignored. Ensure you reference loadWeeklyEta,
setWeeklyEta, weeklyEtaRequestIdRef and orgIdRef in the change so stale
responses do not overwrite the cleared state.
In `@tests/unit/weekly-cap-eta.test.ts`:
- Around line 5-10: Replace all em dash characters ('—') in comments and
test/describe titles with colons or a rephrased sentence; specifically update
the header AC lines and any describe/test title strings that contain an em dash
so they use ":" or plain wording instead, ensuring the TypeScript text rule is
satisfied across the file (update every occurrence in comments and string
literals).
---
Nitpick comments:
In `@tests/unit/weekly-cap-eta.test.ts`:
- Around line 131-144: The test currently allows both null and non-null outcomes
which masks whether the flatness guard was correctly bypassed; change the
synthetic snapshots created in the test (the snaps array built with snap and
BASE/HOUR_MS) to produce a clear positive slope (e.g., steadily increasing
values by a few percentage points across the 10-hour span) so the slope <= 0
guard cannot return null, then assert a single expected outcome: result is
non-null and result.hoursRemaining > 0 by calling computeWeeklyEta(snaps,
nowAfter(10)). Ensure you only modify the values used to construct snaps (not
computeWeeklyEta) so the test deterministically verifies the flatness guard is
not triggered for short spans.
🪄 Autofix (Beta)
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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f4329da7-b61f-48bc-99af-9d6423244b53
📒 Files selected for processing (3)
entrypoints/claude-ai.content.tsentrypoints/sidepanel/hooks/useDashboardData.tstests/unit/weekly-cap-eta.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- entrypoints/claude-ai.content.ts
useDashboardData.ts: add weeklyEtaRequestIdRef (monotonic counter) to guard loadWeeklyEta against same-org double-fire races that the existing orgId stale-check cannot catch; increment on explicit clears (logout) so any in-flight resolution is invalidated before setWeeklyEta runs weekly-cap-eta.test.ts: replace all em-dash characters in describe strings and AC comment block with colons; rewrite the flat-span test from a tautological assertion to a deterministic positive-slope check that actually exercises the flatness guard bypass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary
Projects when a Pro/Max user will exhaust their 7-day rolling usage window, displayed as a one-liner under the weekly bar in both the overlay and the side panel. Uses a least-squares linear fit over a rolling snapshot buffer with R²-based confidence (high/medium/low) and flat/declining/post-reset guards. Closes GET-21.
Type of Change
feat— New featureWhat Was Changed
lib/weekly-cap-eta.ts(new) — Pure ETA agent:computeWeeklyEta(least-squares fit, R², confidence classification) +formatEtaLabel(Intl.DateTimeFormat). No DOM, no chrome refs.lib/conversation-store.ts— AddedappendUsageBudgetSnapshot/getUsageBudgetSnapshots/clearUsageBudgetSnapshotsring-buffer (cap 200). ExportedusageBudgetSnapshotsKeyso the content script reads the key from one source of truth.entrypoints/background.ts— After eachSTORE_USAGE_LIMITS(session tier), appends a snapshot. Detects weekly-reset (new weeklyPct < last - 5) and clears stale snapshots before appending.lib/overlay-state.ts— AddedweeklyEta: WeeklyEta | nulltoOverlayState+applyWeeklyEtastate transition.ui/overlay.ts— Renders.lco-weekly-etadiv below the weekly bar. Hidden for credit tier and null ETA.entrypoints/claude-ai.content.ts—computeEtaForOrgreads snapshots viabrowser.storage.local(WXT polyfill) using the exported key builder. Called onORGANIZATION_DETECTEDand post-STREAM_COMPLETE.entrypoints/sidepanel/hooks/useDashboardData.ts—loadWeeklyEtacallback; wired into init, storage-change listener, tab-switch, tab-update, tab-close handlers.entrypoints/sidepanel/App.tsx— PassesweeklyEtatoUsageBudgetCard.entrypoints/sidepanel/components/UsageBudgetCard.tsx— ETA line under weekly bar with three confidence variants (high: "At this pace…", medium: "Estimated cap…", low: "Estimating…"). Credit tier and null eta both hidden.How to Test
Checklist
bun run test) — 1759/1759bun run compile)bun run build)Related Issues
Closes GET-21
Notes for Reviewer
STORE_USAGE_LIMITS), so the ETA will not appear immediately on a fresh install — it needs ≥5 data points (typically 5 page loads or post-stream fetches). This is by design to avoid noisy early projections.chrome.storage.local(now viabrowser.*) rather than throughconversation-store'ssetStorageabstraction, because the content script does not callsetStorage. The key is now sourced from the exportedusageBudgetSnapshotsKeybuilder to prevent drift.Intl.DateTimeFormat.Summary by CodeRabbit
New Features
Tests