Repository navigation
feat(cursor): read Cursor's dual-store layout and merge per session - #22
Conversation
Rewrite the Cursor source against Cursor's real on-disk storage. The old reader pulled only composerData/bubbleId from state.vscdb, dumped every session into a single double-prefixed `cursor:cursor:unknown` grouping, capped workspace resolution at the first 5 bubbles, and surfaced almost no content. It now reads both stores and merges by session id into one record each: - Global-state (composerData + bubbleId) is the content/metrics backbone: turns from bubbles in header order (text, thinking->reasoning, toolFormerData->tool calls with results+status), plus model, title, tokens, and timestamps. - agent-transcripts JSONL (flat + nested) supply the authoritative workspace (decoded project-dir name) and cover transcript-only sessions. - Sub-agent conversations are emitted as separate records tagged Provenance.SourceOrigin="cursor-subagent" with a parent_session_id link (discriminated by composerData.subagentInfo / the subagents/ transcript path). Sub-agents are never emitted twice or as plain top-level peers. - Composer sessions whose bubble rows were GC'd fall back to their transcript while keeping composer-derived title/model/start time. - buildIndex is memoized for the extraction pass; LookupSession resolves both composerData and transcript ids for the orchestrator. Double-prefix and the 5-bubble cap are gone; unresolvable window sentinels bucket as cursor:unknown. Verified against a live store: 227 records (95 top-level + 132 sub-agents) across 30 workspaces, 0 unknown/duplicate/empty records. Deviations from the original spec, each empirically justified: - Bubbles, not transcripts, are PRIMARY for overlapping sessions: prose is ~identical (1.656M vs 1.655M chars) but only bubbles carry reasoning, tool results, tokens, model, and timestamps. - The agentKv-by-requestId join is skipped: dead on current builds (0 composerData reference agentKv:blob, 0 store.db files); bubbles already carry the content. Gated on a completeness assertion. - The storage.json workspace map is dropped: project-dir + workspaceIdentifier + bubble workspaceUris attribute 100% of sessions with no reliable composer->workspace-hash join available.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe Cursor source extractor is refactored to build a memoized, unified session index from both global-state ChangesCursor Unified Session Index Extraction
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 docstrings
🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…g shells Addresses Greptile review on #22: - buildRecord only labels a record global-state sourced when the DB was actually read (hasComposer AND db != nil). A composer whose DB is unavailable at extract time falls back to its transcript, so SourcePath now points at the transcript and the "stores" extension no longer claims global-state was accessed. - suppressReasoning drops turns left content-less after clearing reasoning (a standalone thinking bubble would otherwise become an empty shell) and recomputes the usage counts. - Document the inherent ambiguity of Cursor's lossy dash-encoding in resolveEncodedParts (matches the vibe-replay reference; best-effort).
… edges Raises cursor source coverage from 82.7% to 90.9% (above the pre-rewrite baseline). New tests exercise SuppressReasoning including reasoning-only turn removal, the DB-unavailable fallback provenance, New() defaults, selectedModels model resolution, the bubble-workspaceUris fallback, tool-param unwrapping, transcript thinking blocks, an unknown bubble type, and the small decode/normalization edge branches.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/source/cursor/source_test.go`:
- Line 47: Replace the db.Exec() calls with db.ExecContext() to comply with the
noctx linting rule. In the buildCursorDB function, update both occurrences (the
create table cursorDiskKV statement and the other db.Exec call at line 87) to
use ExecContext instead, passing context.Background() as the first argument
since this is a test helper function that does not have a context parameter
available to plumb through.
In `@internal/source/cursor/source.go`:
- Around line 201-211: The db.Query call in the code lacks context propagation
for cancellation support. Replace db.Query with QueryContext in the query
execution, and thread a context.Context parameter through both the buildIndex()
and index() functions so that the context can be passed to QueryContext. Update
the query call from db.Query to db.QueryContext, passing the context as the
first argument before the SQL query string.
- Around line 785-787: The uriToPath function currently uses only
strings.TrimPrefix to remove the "file://" prefix, which does not handle
URL-encoded characters as specified in RFC 8089. Modify the uriToPath function
to decode the URI path after removing the "file://" prefix by using
url.PathUnescape on the result of TrimPrefix to ensure proper handling of
percent-encoded characters like spaces and special characters.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: 121f07cc-96d5-43bc-8f74-0176b4d50041
📒 Files selected for processing (2)
internal/source/cursor/source.gointernal/source/cursor/source_test.go
…ings Investigating CodeRabbit's URI-handling note on #22 showed that on real Cursor builds composerData.workspaceIdentifier.uri (and bubble workspaceUris) are serialized VS Code URI objects ({scheme, path, fsPath, ...}), not "file://" strings — so the composer-only workspace fallback never matched on real data. Read the path from the object (fsPath/path) as well as the string form. Cursor does not percent-encode these, so no URL unescaping is needed.
Rewrites the
cursorsource against Cursor's real on-disk storage. The old reader pulled onlycomposerData/bubbleIdfromstate.vscdb, collapsed every session into a single double-prefixedcursor:cursor:unknowngrouping, capped workspace resolution at the first 5 bubbles, and surfaced almost no content (54 of 165 real sessions, all tokens 0). It now reads Cursor's full multi-store layout and merges per session.The two stores
Cursor keeps the same conversation in two places, and they are not interchangeable:
state.vscdb,cursorDiskKV:composerData:<sid>+bubbleId:<sid>:<bid>) — the richer store. Carries assistant prose,thinkingreasoning,toolFormerDatatool calls with results and status,tokenCount, model (modelConfig.modelName), title (name), and timestamps.~/.cursor/projects/<project-dir>/agent-transcripts/..., flat and nested) — clean prose + tool-call inputs only, but the<project-dir>name decodes to the authoritative workspace path.For sessions present in both (the common case) the records are merged into one: bubbles are the content/metrics backbone, the transcript supplies the workspace. Sessions in only one store are read from that store. A session is never emitted twice.
What's in the source
thinking→Reasoning,toolFormerData→ToolCall(input/output/status). Tokens, model, title, andcreatedAtcome fromcomposerData.claudecodesource (flat + nested layouts; on-disk greedy decode of dash-encoded project dirs so hyphenated repo names likevibe-replayresolve correctly).task_v2; each gets its owncomposerData+ bubbles + asubagents/transcript) are emitted as separate records taggedProvenance.SourceOrigin = "cursor-subagent"with aparent_session_idlink. Discriminated authoritatively bycomposerData.subagentInfo(which also carriesparentComposerId).LookupSessionresolves bothcomposerDataand transcript ids (the orchestrator keys cursor lookups on acomposerDataid). The session index is memoized for the extraction pass.empty-window, numeric ids) bucket ascursor:unknowninstead of fabricated paths.Deviations from the original spec (each empirically justified)
composerDatareferenceagentKv:blob:and 0store.dbfiles exist; the content it was meant to recover is already in the bubbles. Gated on a post-build assertion that every real composer emits a non-empty record.storage.jsonworkspace map is dropped. Project-dir +workspaceIdentifier+ bubbleworkspaceUrisattribute 100% of sessions, and there is no reliablecomposer→workspace-hash join.Verification
Run against a live store: 227 records (95 top-level + 132 sub-agents, all parent-linked) across 30 workspaces, 0 unknown / duplicate / empty records, 165/165 with model and title — up from 54 records in one junk grouping.
Tests
go test ./...is green. Thecursorsuite grew from 5 to 20 tests covering the dual-store merge and bubble preference, sub-agent tagging + parent links + thesubagentInfoclassification override, the GC'd-bubble transcript fallback (including composer-metadata preservation), workspace/sentinel decoding, double-prefix and dedup invariants, andLookupSessionfor both id types.