* fix(cursor-agent): leave Cursor IDE chat transcripts to the cursor provider Cursor IDE agent chats write agent transcripts too, so on days without a Cursor import they were counted by both cursor (state.vscdb) and cursor-agent. Discovery now skips a transcript or store whose id is an IDE composerData key; cursor keeps them with per-bubble times and models. Daily cache v73 lets cursor-agent days shrink once. * fix(cache): drop old cursor-agent slices the IDE parse now explains On an existing cache, settled days kept their old cursor-agent slice after v73 because the fresh parse no longer has one there and the merge carries missing slices forward. While cursor-agent is pending its v73 re-derive, a day the fresh parse explains with a cursor slice and no cursor-agent slice drops the old cursor-agent slice; days with no fresh cursor slice keep it.
4.6 KiB
Cursor Agent
Agent transcripts from the Cursor CLI (cursor-agent) and Cursor IDE agent chats. IDE chats are counted by cursor, so their transcripts are skipped here.
- Source:
src/providers/cursor-agent.ts - Loading: lazy (
src/providers/index.ts:62-87) - Test:
tests/providers/cursor-agent.test.ts(243 lines)
Where it reads from
~/.cursor/projects/<projectId>/agent-transcripts/. Inside each project, two layouts coexist:
- Legacy:
*.txtflat files. - Composer 2: UUID-named subdirectories, each containing JSONL.
Sessions with no exported transcript are read from ~/.cursor/chats/<hash>/<agentId>/store.db (#986). A store whose agentId has a transcript is skipped at discovery, so a session is counted from one source only. meta['0'] is hex JSON (agentId, createdAt, and a blobEncryptionKey that is never read); blobs holds JSON messages and protobuf conversation roots (field 1: message blob ids in order, 9: workspace URI, 26: ms stamp). Every root is merged in rowid order, because the latest root drops what Cursor summarized away. Turn time comes from the prompt's <timestamp> tag. Store turns are priced at the Cursor (auto) rate, like transcripts, so a session does not change price when Cursor writes its transcript at the end.
A transcript or store whose id is a composerData:<id> key in Cursor's IDE state.vscdb belongs to an IDE chat. The cursor provider counts it from the IDE database, so it is skipped at discovery and the chat counts once.
Subagents (delegated runs) live in subagents/ subdirectories under the parent (cursor-agent.ts:479-490). They are picked up too.
Storage format
- Legacy: free-form text transcripts. The parser does line-based heuristic parsing (
cursor-agent.ts:219-314). - Composer 2: JSONL (
cursor-agent.ts:167-217).
Caching
None at the provider level. Conversation metadata is read from ~/.cursor/ai-tracking/ai-code-tracking.db, table conversation_summaries. A transcript turn is dated by the latest prompt <timestamp> tag before it, like store turns. A transcript with no tag (about a fifth of them) falls back to the summary's updatedAt, then the file mtime, which is the session's end.
Deduplication
Per <provider>:<conversationId>:<turnIndex> (cursor-agent.ts:379).
Quirks
- A file with a UUID-shaped name is treated as the conversation ID directly (
cursor-agent.ts:142-143); other names are derived from the parent directory. - Token counts are estimated from char count (
CHARS_PER_TOKEN = 4,cursor-agent.ts:35,:81-84). The legacy text format never reports real tokens. - Every assistant message counts as a turn: agentic loops emit dozens of assistant messages per user message, and each carries the last user message forward. Tool_use inputs are serialized into the output text; input tokens use the full user text, billed once on the first assistant message after it, while the displayed message stays truncated at 500 chars.
- The text parser is regex-driven and brittle. It is easier to fix a Composer 2 (JSONL) bug than a legacy (text) bug.
Exact or estimated
- Exact (billed sync). Cursor's usage export (see cursor.md) includes Agent CLI requests: the CLI signs in to the same account and bills it. The export has no field that tells CLI from IDE, so inside synced or imported coverage local
cursor-agentcalls are dropped and the billed rows show undercursor(projectCursor (imported)). Expect far fewer calls there: Cursor bills one row per request, while the transcript holds one turn per assistant message (3 billed rows against 101 local turns on a real run). - Estimated (local). Every local call is
costIsEstimated: transcripts andstore.dbrecord no token counts, so tokens are characters / 4 priced at the Cursor (auto) rate. A prompt's turns all carry its minute, so once the prompt falls inside coverage they are all replaced, including steps that ran after the newest synced event, until the next sync brings those rows in. Untagged transcripts still date at the session's end and can sit past the coverage end until the next sync. - How to get exact. Keep the Cursor app signed in with the sync on, or run
codeburn import cursor --sync.
When fixing a bug here
- Check which format the failing transcript uses before opening a fix.
- For text-format bugs, copy the redacted transcript verbatim into
tests/fixtures/cursor-agent/so the regex change can be regression-tested. - If the bug is "wrong project", look at
cursor-agent.ts:46-50and whether aconversation_summariesrow exists for the conversation.