Skip to content

fix(menubar): apply --project/--exclude to the provider list on carried days - #889

Merged
iamtoruk merged 1 commit into
getagentseal:mainfrom
ozymandiashh:fix/865-provider-list-project-filter
Aug 4, 2026
Merged

fix(menubar): apply --project/--exclude to the provider list on carried days#889
iamtoruk merged 1 commit into
getagentseal:mainfrom
ozymandiashh:fix/865-provider-list-project-filter

Conversation

@ozymandiashh

Copy link
Copy Markdown
Collaborator

Refs #865, which was closed when #864 (2de4d10) fixed the Overview headline via sliceDayToProject. This PR closes the adjacent gap that issue documented and #864 deliberately left alone: the menubar provider list still ignores --project/--exclude on carried days. Verified present on current main (44a94f5, unfilteredProviderDays at usage-aggregator.ts:637).

The bug

buildMenubarPayloadForRange built the provider list from its own day set rather than the one the headline came from:

const unfilteredProviderDays = [
  ...(rangeStartStr <= historicalRangeEndStr ? getDaysInRange(cache, rangeStartStr, historicalRangeEndStr) : []),
  ...(await getTodayAllDays()).filter(d => d.date >= rangeStartStr && d.date <= rangeEndStr),
]

Historical days came straight out of the cache unfiltered, while getTodayAllDays() was already name-filtered. Under --project / --exclude, per-provider costs therefore counted every carried day whole and could not be reconciled with either the headline or the By Project panel.

On my own data, status --format menubar-json --period 30days --project codeburn:

before after
headline cost 141.65 141.65
sum of provider costs 12384.12 141.65
gap 12242.47 0.00

The fix

Reuse durable.days, captured a few lines above as cacheDaysForPeriod. It is the same cache-plus-today union the headline is built from, already narrowed by range, day selection and the project filter. The comment above the buildDurablePeriod call already stated that this section reuses that slice, so this makes the code match what it claims:

// ... The provider list and
// daily-history sections below reuse its cache + today slice.

cacheDaysForPeriod is non-null in this branch: reaching it implies !isClaudeConfigScoped, which forces the !effectivelyScoped path that assigns it.

On the concern that raised this as a policy question

#865 held this back because "narrowing it also changes which providers appear in the list at all". In practice it does not. The installed-but-zero backfill immediately below still adds any provider with discoverable sources at cost: 0:

for (const p of allProviders) {
  if (providers.some(pc => pc.name === p.name)) continue
  const sources = await safeDiscoverSessions(p)
  if (sources.length > 0) providers.push({ name: p.name, displayName: p.displayName, cost: 0 })
}

Confirmed on real data: under --project codeburn, the providers whose entire spend is excluded still appear, at 0.00.

codex=120.09  claude=21.56  copilot=0.00  gemini=0.00  hermes agent=0.00
kimi code=0.00  open design=0.00  antigravity=0.00  cursor=0.00  cursor agent=0.00

That is a weaker disappearance than the By Project panel already allows, since buildTopProjects drops zero-cost projects outright.

Pre-v15 days with no per-project split keep #864's accepted policy for free, because the slicing happens inside sliceDayToProject rather than here.

Tests

Two cases added to tests/project-filter-durable-totals.test.ts, reusing that file's fixture style. A new carriedDayTwoProviders helper builds a carried day whose providers own disjoint projects (claude on keep-me, codex on drop-me), so filtering drop-me out must take codex's whole contribution with it.

  1. slices the provider list by the project filter so it reconciles with the headline asserts the provider costs sum to the headline and that codex drops to zero. Without the fix it fails with expected 100 to be close to 30, received difference is 70.
  2. leaves the provider list untouched when no project filter is given locks the unfiltered path.

Verified unfiltered on real data as well: same provider set, identical costs to the cent, no rows dropped.

tsc --noEmit clean. Full suite passes apart from failures that reproduce identically on unmodified main in my environment (tests/cache-refresh-lock.test.ts, which is flaky under parallel load and passes in isolation, and the two tests/parser.test.ts durable-orphan cases that fail on their first parse with expected +0 to be 200).

…ed days

The provider list rebuilt its own day set straight from the daily cache,
unioning unfiltered historical days with an already-filtered today. Per-provider
costs therefore counted every carried day whole while today honoured the name
filters, so the list could not be reconciled with the headline or the By Project
panel. getagentseal#864 fixed the headline and left this deliberately untouched.

Reuse durable.days, which is the same union the headline is built from, already
narrowed by range, day selection and project filter. That is what the comment
above the buildDurablePeriod call already promised this section would do.

Providers whose entire spend is excluded do not vanish from the list: the
installed-but-zero backfill below still adds them at cost 0.

@iamtoruk iamtoruk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed on the merged tree, closes the #865 gap that #864 left. Real money-accuracy fix: on carried days the menubar provider list was built from an unfiltered cache slice, so under --project/--exclude per-provider costs counted every carried day whole and could not reconcile with the headline or the By Project panel.

  • The fix reuses cacheDaysForPeriod (durable.days), the same cache-plus-today union the headline is built from, already narrowed by range, day selection and the project filter. Verified non-null in this branch: it is the !isClaudeConfigScoped path, which forces the !effectivelyScoped assignment.
  • The installed-but-zero backfill below still adds any provider with discoverable sources at cost 0, so narrowing the totals does not drop providers from the list (the concern #865 raised).
  • Mutation-checked: reverting to the unfiltered union fails the new "slices the provider list by the project filter so it reconciles with the headline" test.
  • tsc clean; the new project-filter test passes; the pre-existing cli-durable-totals single-provider parity failure is on a different code path and reproduces on main (unrelated to this change).

Good to merge.

@iamtoruk
iamtoruk merged commit 2c3319b into getagentseal:main Aug 4, 2026
4 checks passed
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