feat(cursor-agent): discover and reconstruct sessions from store.db (fixes #986) - #1233
feat(cursor-agent): discover and reconstruct sessions from store.db (fixes #986)#1233maheshsingh20 wants to merge 1 commit into
Conversation
…chats/**/store.db Fixes getagentseal#986. ## What changed - **Discovery**: discoverSessions() now scans ~/.cursor/chats/<hash>/<uuid>/store.db in addition to the existing ~/.cursor/projects/**/agent-transcripts/ transcript walk. probeRoots() gains a chats entry so codeburn doctor can report it. - **Schema validation**: each store.db is checked for the required meta and �lobs tables before any reads are attempted. A missing or incompatible schema emits a warning and falls through to the transcript fallback. - **Metadata decoding**: meta['0'] is hex-decoded from UTF-8 JSON to extract �gentId, latestRootBlobId, ame, createdAt, and lastUsedModel. �lobEncryptionKey is explicitly excluded from the decoded object and must never appear in logs, caches, exports, snapshots, or committed fixtures. - **Blob-graph reconstruction**: starting at latestRootBlobId, the parser walks extBlobId chains and childBlobIds arrays in BFS order, classifying blobs by ole (user / assistant / model / request). Unknown fields are silently ignored for forward compatibility. - **Token provenance**: explicit per-request inputTokens/outputTokens fields are used when present and plausible (≤ 2 000 000 — values above that are treated as context-window gauges and discarded). Missing components are estimated from character counts. costIsEstimated is set on calls where any count was estimated. - **Timestamps**: internal request/blob timestamps take precedence; the session createdAt is used as a session-level fallback. File mtime is never reported as an exact request timestamp. - **WAL-aware cache**: the in-memory per-session cache is keyed on a fingerprint combining the store.db mtime and the -wal sidecar size/mtime, so active sessions (written via WAL) always trigger a re-parse. - **Source precedence + dedup**: store.db sources are emitted first. A successfully decoded store records a sentinel in seenKeys; transcript parsers for the same session UUID check the sentinel and skip themselves. A store that fails schema/metadata validation does not set the sentinel, so the transcript fallback runs normally. ## Tests (44 new, 0 regressions) Covers all items from the issue test matrix: empty chats dir, valid minimal store, multi-turn blob graph, malformed hex metadata, missing/invalid root blob, unknown fields, seconds vs milliseconds timestamps, exact/partial/gauge/absent token data, store+transcript dedup, transcript fallback on corrupt store, WAL-aware cache invalidation, blobEncryptionKey redaction.
ozymandiashh
left a comment
There was a problem hiding this comment.
Thanks for the work on #986. Requesting changes; as it stands the feature does not run in the CLI, and CI would fail at the first step. Details, all verified on the branch and on a merge with current main (which is clean):
-
Store sources are discovered but never parsed.
appendStoreSourcesemitspath: "cursor-agent-store:<dbPath>:<uuid>".src/parser.ts:3333fingerprints every source first, andfingerprintFile(src/session-cache.ts:1456) only strips virtual suffixes, so it stats the literalcursor-agent-store:/...string, getsnull, andparser.ts:3348skips the source silently.createSessionParseris never reached for anystore.db. The 44 new tests pass only because they callcreateSessionParser().parse()directly. Fix is the existing convention: real path first, synthetic data as a suffix (<dbPath>#cursor-store=<uuid>, likecursor.tsdoes with#cursor-ws=). That also gets-walfolding for free and makes the hand-rolledfingerprintStore/storeCacheunnecessary. -
npx tsc --noEmithas 6 new errors insrc/providers/cursor-agent.ts:835-863:let hashDirs: Awaited<ReturnType<typeof readdir>>resolves the Buffer overload. AnnotateDirent[]or let inference from thewithFileTypes: truecall site work.tests.ymlruns tsc before vitest. -
tests/provider-probe-roots.test.tsfails on the branch (passes on main) because the newchatsprobe root was not added to the existing assertion. -
Precedence only holds within one pass. The
cursor-agent-store-decoded:<uuid>sentinel is set when the store parser runs, butparser.tsonly parses changed sources. In the #986 scenario (transcript export lands after the store), the store is unchanged, not re-parsed, no sentinel, and the transcript's calls are added on top of the cached store turns. Key namespaces are disjoint so cache dedup cannot catch it. Precedence has to be decided at discovery time. -
Signs this was not run against a real store:
decodeStoreMetadecodeslastUsedModel,name,modeand none is used; the model fallback issource.project, i.e. the session UUID, which then flows intocalculateCost; the timestamp fallback isnew Date(), which CONTRIBUTING forbids for parsers;fetchBlobassumes JSON where the issue describes protobuf; token fields are tried across four aliases each. The PR template's real-data section (npm run dev -- today,models --provider cursor-agent, terminal output against a real~/.cursor/chats/**/store.db) is missing entirely. CONTRIBUTING is explicit that guessing storage schemas is not acceptable; please install Cursor, generate a session, and paste what the CLI shows.
Credit where due: reusing src/sqlite.ts means read-only open, busy_timeout and SQLITE_BUSY handling are correct, the blobEncryptionKey redaction is real, and there is no path traversal or injection. The skeleton is fine; it needs to be wired in and proven on real data.
Summary
Fixes #986.
The
cursor-agentprovider previously missed CLI sessions stored in~/.cursor/chats/**/store.db. This PR adds full discovery and reconstruction for those sessions, with proper source precedence, deduplication, WAL-aware caching, andblobEncryptionKeyredaction.Changes
src/providers/cursor-agent.tsDiscovery
discoverSessions()now scans~/.cursor/chats/<hash>/<uuid>/store.dbbefore the existing transcript walk, without following symlinks beyond two directory levels.probeRoots()gains achatsentry socodeburn doctorreports it.Schema validation
meta+blobstables before any data is read. A missing or incompatible schema emits a warning and falls through to the transcript fallback.Metadata decoding
meta['0']is hex-decoded from UTF-8 JSON to extractagentId,latestRootBlobId,name,createdAt, andlastUsedModel.blobEncryptionKeyis explicitly excluded from the decoded object and never appears in logs, caches, exports, snapshots, emitted calls, or stderr output.Blob-graph reconstruction
latestRootBlobId, followingnextBlobIdchains andchildBlobIdsarrays.role(user / assistant / model / request).textstrings and content-block arrays ([{type:'text', text:'...'}]).toolCalls: [{name}]and content-block{type:'tool_use', name}formats.Token provenance
inputTokens/outputTokensused when present and plausible (<=2,000,000). Values above that threshold are treated as context-window gauges and replaced by char-based estimates.cacheCreationInputTokensandcacheReadInputTokensrecorded when present.costIsEstimated: trueset on any call where at least one count was estimated.Timestamps
createdAtused as session-level fallback. File mtime is never reported as an exact request timestamp.WAL-aware in-memory cache
-walsidecar size/mtime. Active sessions always trigger a re-parse when the WAL changes.Source precedence and deduplication
discoverSessions().cursor-agent-store-decoded:<uuid>sentinel inseenKeys; transcript parsers for the same session UUID check it and skip themselves.tests/providers/cursor-agent-store.test.ts(new, 44 tests)Test results