Conversation
…#196) Cursor's chat history showed as a single row labeled 'cursor' in the dashboard because the global state.vscdb has no workspace field on individual bubbles. The fix joins through Cursor's per-workspace storage: 1. Walk ~/Library/Application Support/Cursor/User/workspaceStorage/* 2. For each hash dir, read workspace.json -> folder URI 3. Open that dir's state.vscdb, read ItemTable['composer.composerData'] -> allComposers list 4. Build Map<composerId, folder URI> 5. emit one SessionSource per workspace plus a catch-all 'cursor' source for composers that did not register against any workspace (multi-root workspaces, no-folder-open windows, deleted workspaces with surviving global rows) The parser decodes source.path's #cursor-ws= tag, filters the parsed bubbles to the composerIds that belong to this workspace, and yields only those. The orphan-tag source negates the filter so it captures every composer not in any workspace. In passing, fix a real bug in the old code: parseBubbles set `sessionId: row.conversation_id ?? 'unknown'`, but the JSON `conversationId` field is empty in current Cursor builds, so every call shipped with `sessionId: 'unknown'`. We now derive the composer id from the row key (`bubbleId:<composerId>:<bubbleUuid>`) which is what the workspace map joins on. The old behavior masked the bug because every call went into a single 'cursor' project anyway; with per-workspace bucketing the bug becomes load-bearing. Cache version bumped 2 -> 3 to invalidate caches that still record 'unknown' as the session id. Live-tested against my real 1.9 GB Cursor DB: the single 'cursor' row with 1904 calls / $4.08 now breaks into 5 workspaces plus an orphan bucket, totals reconcile exactly. 8 fixture-based tests cover multi-workspace routing, orphan filtering, legacy bare DB path backwards compat, multi-root workspace skip, vscode-remote URI slugification, and total reconciliation across all sources. Full suite: 46 files, 653 tests passing.
3 tasks
iamtoruk
added a commit
that referenced
this pull request
May 10, 2026
…n) (#297) PR #296 (Cursor per-project breakdown) bumped DAILY_CACHE_VERSION from 4 to 5 but left MIN_SUPPORTED_VERSION at 2. The migration path (isMigratableCache + migrateDays) only fills in missing default fields; it does NOT recompute the providers / categories / models rollups from session data, because raw sessions are not retained in the cache. So a v4 cache migrated to v5 carried forward its old per-day provider totals (single 'cursor' bucket) for the full retention window. Effect on users post-#296: the macOS menubar's `current.providers.cursor` would show the orphan-bucket subtotal instead of the full Cursor cost for any historical day whose daily entry was computed before #296 landed. Live-test on my machine showed cursor=$3.78 against a migrated v4 cache vs cursor=$4.08 (correct) after the daily cache was discarded — the $0.30 gap was the workspace projects whose costs were no longer aggregated under the 'cursor' label by the new code. Fix: raise MIN_SUPPORTED_VERSION to 5 so any cache with version < DAILY_CACHE_VERSION is renamed to `.bak` and the cache is recomputed from scratch on next run. The recompute is the same operation that backfills the cache for a new user, so the cost is a one-time cold-path hit (~3s on the test machine). Test for the migration case updated to assert the new discard-and-bak behavior. Full suite: 46 files / 654 tests pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third and final PR addressing #196 / #159. Closes the per-project breakdown half of #196 (the activity-classifier half shipped in #289, the model-alias half shipped in #290).
The bug
PhilippMolitor's report in #196 showed every Cursor session under one row labeled
cursorwith 870 calls in "1 session". Cursor's global SQLite stores per-bubble token data but not per-bubble workspace, so the old provider had nothing to bucket on.The fix
Three layers, all in
src/providers/cursor.ts:~/Library/Application Support/Cursor/User/workspaceStorage/<hash>/. For each hash, readworkspace.json(folder URI) andstate.vscdb'sItemTable['composer.composerData'](the composer ids opened in that workspace). BuildMap<composerId, folderUri>once and memoize per CLI run.discoverSessionsemits oneSessionSourceper workspace with project namesanitizeWorkspaceUri(folderUri)(matches Claude's slug shape:file:///Users/me/proj->-Users-me-proj). Plus a catch-all source labeledcursorthat captures composers not registered in any workspace (multi-root, "no folder open", deleted workspaces).source.pathcarries the workspace tag via#cursor-ws=.... The parser decodes it, parses the global db once (cached across all workspace-scoped sources), then yields only the composers belonging to this source.Two side-effect bug fixes that fell out:
sessionIdwas always'unknown'for every Cursor call. The JSONconversationIdfield on bubbles is empty in current Cursor builds; the real composer id is in the row key (bubbleId:<composerId>:<bubbleUuid>). The old code masked the bug because every call collapsed under a singlecursorrow anyway.bubbleId:task-call_xxx\nfc_yyy:<bubbleUuid>with a literal newline in the composer segment. These are not standalone composers and would otherwise inflate the orphan project's session count.parseComposerIdFromKeynow rejects any composer segment containing CR/LF.Multi-agent + devil's advocate review caught and fixed before push
:and produced mangled session ids that landed in orphan. Now filtered explicitly. New test fixture for the real key shape.daily-cache.tsversion bump missed — bumped to 5. Without this, the 30-day dashboard would mix oldcursorrows with new per-workspace rows for the historical window.basenameimport — removed.allowedComposerson the orphan branch (it was actually the disallowed set) — split intocomposerFilter+ explicitfilterMode: 'include' | 'exclude'.#is illegal in POSIX paths" — softened.#is legal in POSIX file names; what matters is thatstate.vscdbpaths don't contain it because we construct them ourselves.Live verification
Against my real 1.9 GB Cursor DB (5,556 bubble rows, 12 workspace directories):
Projects: ContentFlow (642 / $1.50), llm (372 / $0.91), autogen-team (516 / $0.90), cursor / orphan (331 / $0.51), Public (42 / $0.26), TweeterGPTTraining (1 / $0.0001).
Tests
9 fixture-based tests in
tests/providers/cursor-workspace-breakdown.test.ts:cursorsource when no workspaceStorage existsfolderin workspace.json) skippedFull suite: 46 files, 654 tests passing.
Out of scope
cursor-agentprovider (separate provider) — uses different schema, not affected.Test plan
rm -f ~/.cache/codeburn/cursor-results.json && node dist/cli.js report --provider cursor -p allshould produce one row per Cursor workspace plus acursorrow for the catch-all.cursorrow).