Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. Dates are I
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- **Quota notifications now show the pool's best availability.** The 5-hour and weekly lines independently select the highest remaining percentage and earliest reset across enabled accounts, rather than showing the reset belonging to the account with the highest remaining percentage.

## [6.15.0] - 2026-08-31

### Added
Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,11 +300,10 @@ running, it checks all enabled accounts and alerts through Notification Center
when the best remaining 5-hour or weekly pool quota crosses 25%, 10%, or 0%.
The feature is disabled by default.

Each line reports the enabled account with the most headroom in that window,
together with that same account's reset time, so the pair always describes a
quota that one account actually has. Windows a plan has switched off are
skipped rather than counted as full. Account identities are omitted for
readability and lock-screen privacy:
Each line reports the most headroom available across enabled accounts in that
window together with the earliest reset across those accounts. Windows a plan
has switched off are skipped rather than counted as full. Account identities
are omitted for readability and lock-screen privacy:

```text
5h: 10% | resets 22:30
Expand Down
11 changes: 6 additions & 5 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,11 +270,12 @@ The sample above intentionally sets `"retryAllAccountsMaxRetries": 3` as a bound
Quota notifications query each distinct enabled account with bounded
concurrency. The 5-hour and weekly windows are tracked independently, and each
threshold alerts once until that window rises above it after a reset. Each line
reports the account with the most headroom in that window plus that same
account's reset time, so the percentage and the reset always come from one
account. A window the plan has switched off reports `used_percent: 0` and is
skipped rather than scored as a full quota. Account identities are omitted from
alerts and are never persisted in notification state.
reports the most headroom across enabled accounts in that window plus the
earliest reset across those accounts. The percentage and reset may come from
different accounts. A window the plan has switched off reports
`used_percent: 0` and is skipped rather than scored as a full quota. Account
identities are omitted from alerts and are never persisted in notification
state.
Set `notifyEveryCheck` to `true` to deliver the aggregate message after every
successful poll interval even when no threshold was crossed. Set
`thresholds` to `[]` to disable threshold alerts entirely; omitting the key
Expand Down
40 changes: 13 additions & 27 deletions lib/quota-notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,49 +79,35 @@ type MonitorDependencies = {
};

/**
* Reduce one quota window across accounts to the single account that has the
* most room left, reporting that account's own reset time.
*
* Two rules matter here:
*
* - A window the plan has switched off is skipped via {@link hasUsageWindow}.
* Such a window still reports `used_percent: 0`, so counting it would score
* a disabled window as 100% remaining and mask every other account.
* - The percentage and the reset time are taken from the *same* account. A
* max-percent/min-reset pair describes a quota no account actually has.
* Reduce one quota window across accounts to the most remaining quota and the
* earliest reset independently. A disabled window is skipped because it can
* report `used_percent: 0` and would otherwise mask active windows as 100% full.
*/
function aggregateWindow(
summaries: readonly AccountQuotaSummary[],
select: (summary: CodexUsageSummary) => CodexUsageSummary["primary"],
now: number,
): AggregatedQuotaWindow {
let best: { remainingPercent: number; resetAtMs?: number } | undefined;
let remainingPercent: number | undefined;
let resetAtMs: number | undefined;
for (const accountSummary of summaries) {
const window = select(accountSummary.usage);
if (!hasUsageWindow(window)) continue;
const remaining = getUsageLeftPercent(window.usedPercent);
if (remaining === undefined) continue;
const resetAtMs =
typeof window.resetAtMs === "number" &&
Number.isFinite(window.resetAtMs) &&
window.resetAtMs > now
? window.resetAtMs
: undefined;
if (best === undefined || remaining > best.remainingPercent) {
best = { remainingPercent: remaining, resetAtMs };
continue;
if (remainingPercent === undefined || remaining > remainingPercent) {
remainingPercent = remaining;
}
// Tie on headroom: prefer the account that recovers first, and prefer a
// known reset over an unknown one.
if (
remaining === best.remainingPercent &&
resetAtMs !== undefined &&
(best.resetAtMs === undefined || resetAtMs < best.resetAtMs)
typeof window.resetAtMs === "number" &&
Number.isFinite(window.resetAtMs) &&
window.resetAtMs > now &&
(resetAtMs === undefined || window.resetAtMs < resetAtMs)
) {
best = { remainingPercent: remaining, resetAtMs };
resetAtMs = window.resetAtMs;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
}
return best ? { remainingPercent: best.remainingPercent, resetAtMs: best.resetAtMs } : {};
return { remainingPercent, resetAtMs };
}

export function aggregateQuotaUsage(
Expand Down
27 changes: 18 additions & 9 deletions test/quota-notifications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function accountUsage(options: Parameters<typeof usage>[0]): AccountQuotaSummary
}

describe("quota notification aggregation", () => {
it("reports the account with the most headroom and that same account's reset", () => {
it("reports the most headroom and earliest reset independently", () => {
const result = aggregateQuotaUsage(
[
accountUsage({
Expand All @@ -67,17 +67,15 @@ describe("quota notification aggregation", () => {
1_000_000,
);
expect(result).toEqual({
// Second account: 60% left, resetting at its own 1_500_000.
// Second account has both the best quota and earliest reset.
fiveHour: {
remainingPercent: 60,
resetAtMs: 1_500_000,
},
// First account: 70% left, resetting at its own 3_000_000. Taking the
// other account's earlier 2_500_000 here would describe a 70% quota
// that recovers at a time no account recovers at.
// First account has the best quota; second account resets first.
weekly: {
remainingPercent: 70,
resetAtMs: 3_000_000,
resetAtMs: 2_500_000,
},
});
});
Expand Down Expand Up @@ -109,15 +107,26 @@ describe("quota notification aggregation", () => {
expect(result.weekly.remainingPercent).toBe(60);
});

it("prefers the earliest reset when two accounts tie on headroom", () => {
it("selects the earliest reset regardless of headroom", () => {
const result = aggregateQuotaUsage(
[
accountUsage({ fiveHourUsed: 50, fiveHourReset: 3_000 }),
accountUsage({ fiveHourUsed: 20, fiveHourReset: 3_000 }),
accountUsage({ fiveHourUsed: 50, fiveHourReset: 1_500 }),
],
1_000_000,
);
expect(result.fiveHour).toEqual({ remainingPercent: 50, resetAtMs: 1_500_000 });
expect(result.fiveHour).toEqual({ remainingPercent: 80, resetAtMs: 1_500_000 });
});

it("ignores reset timestamps from windows without valid usage", () => {
const result = aggregateQuotaUsage(
[
accountUsage({ fiveHourUsed: 50, fiveHourReset: 3_000 }),
accountUsage({ fiveHourReset: 1_500 }),
],
1_000_000,
);
expect(result.fiveHour).toEqual({ remainingPercent: 50, resetAtMs: 3_000_000 });
});

it("ignores expired reset timestamps", () => {
Expand Down