Skip to content

feat(usage): add Muse Code local usage provider - #22379

Merged
nwparker merged 4 commits into
stablyai:mainfrom
nwparker:muse-usage-tracking
Sep 23, 2026
Merged

nwparker merged 4 commits into
stablyai:mainfrom
nwparker:muse-usage-tracking

Conversation

@nwparker

@nwparker nwparker commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

Orca's Stats & Usage page can now show how many tokens you spent in Muse Code, next to Claude, Codex and OpenCode. It reads the session logs Muse already writes on your computer, so there's nothing to set up beyond turning it on.

What Changed

  • Before: Muse sessions ran in Orca but didn't appear anywhere in Stats & Usage.
  • After:
    • Usage Analytics has a Muse tab with input, output, cached-input, reasoning and total tokens, sessions and model calls, a daily chart, and breakdowns by model and by project/worktree.
    • The Usage Overview lists Muse as a fourth provider. Its totals, heatmap and token mix include Muse, and there's an "Enable Muse" button.
    • Cost for Muse shows as n/a, because Muse's logs record tokens but not prices.
  • How it works: a new usage provider (src/main/muse-usage/) reads the model_completed events that Muse writes to ~/.local/share/muse/sessions/YYYY/MM/DD/<session>/session.jsonl ($XDG_DATA_HOME is respected).
    • Batched records: it also unpacks Muse's batched retained_frame records.
    • Subagent logs: Muse keeps subagent usage in separate subagent/<child>/session.jsonl files. It adds those to the parent session, because those files don't record a workspace.
    • Token counting: cached tokens are counted inside input and reasoning tokens inside output, the same way Codex is counted.
    • Duplicates: an event copied into two logs is counted once.
    • Scanning: it runs on the usage scan worker Claude, Codex and OpenCode already use, and skips logs whose modification time and size haven't changed.
  • Wiring: settings persistence, store slices, preload/IPC, and startup/quit, following the Claude/Codex/OpenCode providers. There are 13 new strings translated for es/fr/ja/ko/zh. The Stats & Usage description now names Muse.
  • Overview cost warning: "some model prices are unavailable" now only shows when some provider actually has prices. If nothing is priced, the total cost already reads n/a.

Why

Issue #19224 proposed this local-log approach as Option A. Muse's logs already contain exact per-call token counts, which is the same kind of data the existing local providers read, so this fits the existing design with no new moving parts.

Live rate limits (Option B) are left out on purpose. Muse's host protocol has usage/read for its 5-hour and weekly windows, but only inside a running muse serve process, and only after it has made a model request. Nothing is saved to disk, and Orca runs the normal Muse terminal UI, not muse serve. Reading them would mean running Orca's own Muse sessions through that protocol, or waiting for Muse to add a standalone way to read usage. Orca doesn't touch Muse's credentials.

Linked Issue

Fixes #19224

Visual Proof

Before (main) After (this PR)
Before: provider menu After: provider menu
Before: overview After: overview

After, Muse tab (all local Muse usage) with the updated description:

After: Muse tab

After: Muse breakdowns

With only Muse enabled, the Overview shows cost as n/a without the "some model prices are unavailable" note.

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • Manual check (macOS): I ran a dev build against real Muse 1.3.0 logs. A separate Python count over all 85 log files (including 51 subagent logs) matches the app exactly: 1,406,537 total tokens, 1,376,190 input, 30,347 output, 928,260 cached, 17 sessions, 85 events, and the same per-day totals.

  • Scan speed on those logs: 40 ms the first time and 4 ms after that, with nothing re-read on the second pass.

  • Automated:

    • New tests cover log parsing (including retained_frame, subagent roll-up and duplicate events), the scanner (on-disk log trees), and the summary/breakdown maths.
    • The overview, IPC, preload, startup, store and i18n suites pass.
    • pnpm tc, the localization checks, the max-lines ratchet and the changed-code quality gate pass.

AI Disclosure

Anthropic Claude assisted with implementation, validation, and this pull request description.

Review

Follows the provider pattern from the existing Claude/Codex/OpenCode usage providers and the community DeepSeek provider (#12873).

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Local only: it reads local logs only, the same as the other local-log providers, so Muse usage on SSH/WSL hosts isn't counted yet. Nothing crosses the remote wire.
  • Paths: paths are built with Node path utilities, so it works on macOS, Linux and Windows. On Windows Muse writes to %USERPROFILE%\.local\share\muse, which I checked on a Windows host for feat(agents): add first-class Muse Code harness #22216.
  • Credentials: no credentials are read.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (typecheck, focused suites and quality gates pass locally; CI covers the full suite and build)

Scan Muse session logs (including subagent logs, which hold usage the parent
log does not) for model_completed token events and surface them as a fourth
local usage provider: shared scan worker, persisted per-file cache reused by
mtime/size, cross-log dedupe, Stats tab, and Usage Overview integration.
Muse logs carry no price, so the provider reports tokens only.
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 4237c622-4044-42a7-94bd-61ea5f135cae

📥 Commits

Reviewing files that changed from the base of the PR and between cbe03ad and 6d6a0c6.

📒 Files selected for processing (3)
  • src/main/muse-usage/muse-usage-provider.ts
  • src/main/muse-usage/scanner.test.ts
  • src/main/muse-usage/scanner.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a Muse usage provider that scans local session logs, extracts token events, attributes them to worktrees, and builds summaries, daily totals, breakdowns, and recent-session data. It connects the provider to the main-process store, worker, IPC, and preload API. The renderer adds Muse to usage overviews and provides a dedicated pane with filtering, status, token statistics, and session details. Tests and translations are also added or updated.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 6d6a0

The Muse usage change has no identified merge-blocking issue; it is ready for normal checks.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 66 functions across 51 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: adding Muse Code as a local usage provider.
Description check ✅ Passed The description covers the required sections, linked issue, user-facing changes, implementation details, visual proof, testing, AI disclosure, compatibility notes, and checklist. The full lint, typech…
Linked Issues check ✅ Passed Issue [#19224] requires a local Muse usage provider that aggregates model_completed token events and wires the provider through the usage-provider contract, store, worker, IPC, preload, and Usage Ov…
Out of Scope Changes check ✅ Passed The changes stay within issue [#19224]. Startup and quit handling, persistence, translations, UI components, IPC, preload, worker integration, and tests support the local Muse usage provider. The PR d…
  • Fix all pre-merge checks with AI

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.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 45902896-c4be-4820-91c0-88eb23c4dd98

📥 Commits

Reviewing files that changed from the base of the PR and between 9af6a3d and 54ec10b.

📒 Files selected for processing (57)
  • src/main/ipc/register-core-handlers/register-core-handlers.test.ts
  • src/main/ipc/register-core-handlers/register-core-handlers.ts
  • src/main/ipc/usage-provider-handlers.test.ts
  • src/main/ipc/usage-provider-handlers.ts
  • src/main/muse-usage/muse-session-log-discovery.ts
  • src/main/muse-usage/muse-usage-provider.ts
  • src/main/muse-usage/muse-usage-record-parser.ts
  • src/main/muse-usage/persisted-state-normalization.ts
  • src/main/muse-usage/scanner.test.ts
  • src/main/muse-usage/scanner.ts
  • src/main/muse-usage/snapshot-rollups.test.ts
  • src/main/muse-usage/snapshot-rollups.ts
  • src/main/muse-usage/store.ts
  • src/main/muse-usage/types.ts
  • src/main/startup/main-process-account-services.ts
  • src/main/startup/main-process-observers.ts
  • src/main/startup/main-process-preflight.ts
  • src/main/startup/main-process-quit.ts
  • src/main/startup/main-process-state.ts
  • src/main/startup/main-window-controller.ts
  • src/main/startup/main-window-core-services.ts
  • src/main/startup/main-window-service-readiness.ts
  • src/main/usage/usage-provider-contract.ts
  • src/main/usage/usage-scan-worker-client.ts
  • src/main/usage/usage-scan-worker-entry.ts
  • src/main/usage/usage-scan-worker-protocol.ts
  • src/main/usage/usage-scan-worker-spawn.ts
  • src/preload/api-types.ts
  • src/preload/api/agent-usage-api.ts
  • src/preload/api/muse-usage-bridge.ts
  • src/preload/index.ts
  • src/preload/usage-provider-api.test.ts
  • src/preload/usage-provider-api.ts
  • src/renderer/src/components/settings/settings-interface-secondary-section-renderers.tsx
  • src/renderer/src/components/stats/MuseUsageDetails.tsx
  • src/renderer/src/components/stats/MuseUsagePane.tsx
  • src/renderer/src/components/stats/StatsPane.tsx
  • src/renderer/src/components/stats/UsageOverviewPane.tsx
  • src/renderer/src/components/stats/stats-search.ts
  • src/renderer/src/components/stats/usage-overview-daily-series.ts
  • src/renderer/src/components/stats/usage-overview-model.test.ts
  • src/renderer/src/components/stats/usage-overview-model.ts
  • src/renderer/src/components/stats/usage-overview-sections.tsx
  • src/renderer/src/components/stats/usage-overview-types.ts
  • src/renderer/src/components/stats/usage-provider-normalization.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/store/index.ts
  • src/renderer/src/store/slices/store-test-helpers.ts
  • src/renderer/src/store/slices/usage-provider-slices.ts
  • src/renderer/src/store/slices/usage-web-client-fallback.test.ts
  • src/renderer/src/store/types.ts
  • src/shared/muse-usage-types.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No critical issues — two minor rough edges worth a look.

Reviewed changes

  • New Muse Code local usage provider (src/main/muse-usage/): reads session.jsonl logs from resolveMuseSessionsDir(), parses model_completed usage (cached nested in input, reasoning nested in output), unwraps retained_frame batches, rolls subagent logs into the parent session/cwd, dedupes records copied across logs, and caches per-file by mtimeMs/size.
  • Shared-worker integration: usage-scan-worker-{protocol,client,spawn,entry}.ts learn a 'muse' provider; the worker graph stays electron-free (enforced by plain-node-entry-guard.ts).
  • Main-process wiring: IPC registerUsageProviderHandlers, preload bridge/PreloadApi, preflight path init, observers, account-services guard, window readiness, quit flush, and settings persistence all follow the existing provider pattern.
  • Renderer: new Muse stats tab (MuseUsagePane/MuseUsageDetails), Muse folded into the Usage Overview providers/heatmap/token mix (museTokens on daily points), store slice, and search keyword.
  • Overview cost warning: hasPartialCost now also requires hasKnownCost, so "some model prices are unavailable" no longer shows when no provider is priced.
  • i18n: 13 new strings across en/es/fr/ja/ko/zh; two hashed keys replaced with named keys.

ℹ️ A deferring log is not reparsed when its owner is rewritten in place

Reclaim of a shared record only fires when the owning log's path disappears (lostOwnerPath). If an owner is later rewritten/truncated so it no longer contains a record that a sibling also holds — while the sibling's mtimeMs/size are unchanged — the sibling is reused without re-claiming it, so the record is under-counted until the sibling's stat changes. Under the stated append-only assumption this is unreachable; flagging only in case Muse compacts or rotates these logs.

Technical details
# Reclaim gate keys on a vanished path, not on changed owner content

## Affected sites
- `src/main/muse-usage/scanner.ts:125-129` — `lostOwnerPath` is true only when a previously-owned path is absent from `currentPaths`
- `src/main/muse-usage/scanner.ts:142-151` — `isStatUnchanged` gates reclaim on `mustReclaimDeferred`, which is false when no path vanished
- `src/main/muse-usage/scanner.ts:153-161,175-176` — pre-registration + reuse let an unchanged deferring file keep `ownedEventKeys` without re-claiming
- `src/main/muse-usage/scanner.ts:74-85` — `eventKey` is content-based, so the same record in two logs collides as intended

## Required outcome
- A shared record present in a deferring log should still be counted if its current owner no longer contains it, even when the owner's path still exists.

## Open questions for the human
- Are Muse `session.jsonl` files strictly append-only (grow-only)? If so, no change is needed and the reuse optimization is sound.

ℹ️ Nitpicks

  • Two parallel "Stats & Usage" descriptions still enumerate only Claude/Codex/OpenCode: settings-navigation-interface-sections.ts:81-84 (key b351014180) and the search entry stats-search.ts:7-10 (key 26bb901fcd). Inline note on the changed file.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@nwparker

Copy link
Copy Markdown
Contributor Author

Re: pullfrog's note about a log rewritten in place being under-counted. Muse's session.jsonl files are append-only: each line is a sequenced, durable event record, and a session's log is never rewritten. The scanner's offset-and-size reuse relies on that, the same as the Codex/Claude scanners. If a file ever shrinks, it gets a new size and modification time and is re-read in full, so the risk is limited to an in-place rewrite that keeps the same size, which Muse doesn't do. No change.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

Delta since the prior Pullfrog review (54ec10b → cbe03ad):

  • Unreadable sessions root now surfaces instead of reading as empty — listSubdirectories gained a { reportUnreadable } option; only the sessions-root read rethrows non-ENOENT errors (EACCES/ENOTDIR) so they reach scanState.lastScanError, while nested level reads stay tolerant. A scanner test proves a file in place of the root rejects with ENOTDIR.
  • Stats & Usage copy completed — stats-search.ts (key 26bb901fcd) and settings-navigation-interface-sections.ts (key b351014180) now name Muse alongside Claude/Codex/OpenCode, in the source default and all six locale catalogs, matching the equivalent string updated earlier in settings-interface-secondary-section-renderers.tsx.

This resolves the one prior inline note (thread resolved). The new error flows through the existing worker → UsageProviderStoreLifecycle → lastScanError path unchanged from the other local providers, so the fix is consistent with the established pattern.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Do not discard repeated keys within one session log. · muse-usage-record-parser.ts:62-110

src/main/muse-usage/muse-usage-record-parser.ts:62-110
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Do not discard repeated keys within one session log.

retained_frame unpacks each child as a separate record. Two model_completed children can share recorded_at and token counts while having different models or run data. The current local ownedEventKeys check drops the second event before claimEventKey runs. Since claimEventKey accepts a key already owned by the same path, removing this check counts both events and preserves cross-file copy deduplication.

Suggested fix
     for (const parsed of parseMuseUsageLine(line, context)) {
-      if (ownedEventKeys.has(parsed.eventKey)) {
-        continue
-      }
       if (!input.claimEventKey(parsed.eventKey)) {
         hasDeferredClaims = true
         continue

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 5cc602ae-aae1-49df-a2f5-493cf2fa7cd6

📥 Commits

Reviewing files that changed from the base of the PR and between 54ec10b and cbe03ad.

📒 Files selected for processing (10)
  • src/main/muse-usage/muse-session-log-discovery.ts
  • src/main/muse-usage/scanner.test.ts
  • src/renderer/src/components/stats/stats-search.ts
  • src/renderer/src/hooks/settings-navigation-interface-sections.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/main/muse-usage/scanner.test.ts
  • src/renderer/src/components/stats/stats-search.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/main/muse-usage/muse-session-log-discovery.ts
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/zh.json

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

@nwparker

Copy link
Copy Markdown
Contributor Author

Re @coderabbitai outside-diff "Do not discard repeated keys within one session log" (muse-usage-record-parser.ts:62-110): fixed in 6d6a0c6.

  • Record id can't be the key: a fork copy gets a new id, so keying on it would count shared turns twice. The content key stays for matching copies across logs.
  • Within one log, each record's key now carries its position among records with the same content (<content>#0, #1, ...). Two different same-content records in one log are both counted. A fork that copies the same records produces the same keys, so they're still counted once. I removed the per-file ownedEventKeys early skip.
  • Rescans stay correct because each changed file is fully reread and its claims are keyed by path. Unchanged files re-register their saved keys. I bumped the Muse cache schema to v2 so old keys without the ordinal get discarded.
  • Test: a retained_frame with two identical turns counts both, a fork copy counts once, and neither an unchanged rescan nor an append double-counts. On the local Muse logs (127 files), totals match an independent dedupe count exactly: 2,389,931 tokens, same after rescan.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

Delta since the prior Pullfrog review (cbe03ad → 6d6a0c6):

  • Same-content records in one log now count separately — parseMuseUsageFile derives eventKey as <content>:<ordinal> from a per-log occurrencesByContentKey counter, replacing the ownedEventKeys.has skip that silently dropped a distinct second model_completed sharing recorded_at and token counts (the CodeRabbit outside-diff finding). A fork that copies the same sequence numbers occurrences identically, so its copies still map onto the owner's keys and defer — the cross-log dedup invariant holds. listMuseSessionLogFiles sorts by path, so ownership stays deterministic.
  • Cache schema bumped to v2 — MUSE_USAGE_SCHEMA_VERSION moves to 2 so normalizeMuseUsageState discards v1's unsuffixed ownedEventKeys and rescans instead of mixing key formats, while preserving the user's opt-in (scanState.enabled).
  • New test — scanner.test.ts covers a retained_frame holding two identical turns in the parent plus a fork with copies: the first scan counts 220, an unchanged rescan stays 220, and appending a third turn yields 330. It fails with the old behavior, where the parent's second turn was skipped.

Verified locally: pnpm test src/main/muse-usage/scanner.test.ts (10 passing). The only prior Pullfrog thread (Stats & Usage copy listing three providers) was resolved in cbe03ad; nothing else is outstanding.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai

coderabbitai Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -p 'muse-usage-record-parser.ts|scanner.ts|scanner.test.ts|muse-usage-provider.ts' src/main/muse-usage || true
ast-grep outline src/main/muse-usage/muse-usage-record-parser.ts --items all
ast-grep outline src/main/muse-usage/scanner.ts --items all
rg -n -C 5 'occurrencesByContentKey|ownedEventKeys|hasDeferredClaims|claimEventKey|MUSE_USAGE_SCHEMA_VERSION|identical|fork|append|rescan' src/main/muse-usage/scanner.ts src/main/muse-usage/scanner.test.ts src/main/muse-usage/muse-usage-provider.ts
sed -n '50,120p' src/main/muse-usage/muse-usage-record-parser.ts

Length of output: 18636


@nwparker, thanks for the fix. I verified that scanner.ts adds a per-log occurrence suffix to each content key and no longer skips repeated keys within a file. The focused test covers identical records, a fork copy, an unchanged rescan, and an append. MUSE_USAGE_SCHEMA_VERSION is also 2. I did not run the tests or independently verify the reported local-log total. This addresses my finding.


✏️ Learnings added
Learnt from: nwparker
URL: https://github.com/stablyai/orca/pull/22379

Timestamp: 2026-09-23T05:21:16.159Z
Learning: In Orca's Muse usage scanner (`src/main/muse-usage/scanner.ts`), Muse record IDs are not suitable deduplication keys because fork copies can receive new IDs. Use content-based event keys with a per-log occurrence ordinal to count distinct identical records in one log while deduplicating copies across fork logs. Muse usage cache schema v2 stores these ordinal-suffixed keys.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

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.

[MuseCode follow-up] Usage/account tracking (rate limits + usage provider)

1 participant