In this repo, for the CLI, how are traces from Cursor picked up? Give me an overview
Here's a complete overview of how Cursor traces are picked up in the CLI:
The CursorAdapter (cli/src/adapters/cursor/v1.ts) reads from two distinct sources, reflecting Cursor's evolving storage formats:
~/Library/Application Support/Cursor/User/globalStorage/state.vscdb (macOS)
AppData/Roaming/Cursor/User/globalStorage/state.vscdb~/.config/Cursor/User/globalStorage/state.vscdbcursorDiskKVcomposerData:<composerId> — conversation metadata (title, model, timestamp, list of bubble headers)bubbleId:<composerId>:<bubbleId> — individual message "bubbles" (user messages, assistant replies, tool calls)composerData:* rows → get the conversation listfullConversationHeadersOnly (list of bubble IDs in order)bubbleId:*:* keybubble.type === 1 → user message; type === 2 → assistant message (text, thinking blocks, tool calls via toolFormerData)~/.cursor/projects/<project>/agent-transcripts/~/.cursor/projects/ directoriesagent-transcripts/ for valid transcript filesisAvailable() → fs.existsSync(globalDbPath) || fs.existsSync(projectsDir)
Either source existing is enough.
cli/src/config.ts)Both can be overridden via TRACES_CURSOR_GLOBAL_DB and TRACES_CURSOR_PROJECTS env vars.
getTraceIndexBatch() uses a CursorIndexCursor snapshot to detect changes:
globalDbMtimeMs — mtime of state.vscdbglobalComposerCount — count of composerData:* rowstranscriptNewestMtimeMs — newest mtime across all transcript filestranscriptFileCount — total number of transcript filesIf all four values are unchanged from the previous cursor, the batch returns empty (no re-scan needed).
Both sources emit the same canonical event types:
| Event Type | Source |
|---|---|
user_message | Bubble type=1 / record role=user |
agent_text | Bubble type=2 (plain text) / role=assistant |
agent_thinking | allThinkingBlocks / type=thinking block |
tool_call | toolFormerData (global) / tool_call block |
tool_result | toolFormerData.result / tool_result block |
error | type=error entries |
CursorAdapter is registered in cli/src/adapters/index.ts via registry.register(new CursorAdapter()) which is called automatically on import. The AdapterRegistry then exposes it alongside all other adapters (Claude Code, Copilot, Cline, etc.).
Okay but where does it look for files?
Two locations, from cli/src/config.ts:
1. Global SQLite DB (modern Cursor)
| Platform | Path |
|---|---|
| macOS | ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb |
| Windows | ~/AppData/Roaming/Cursor/User/globalStorage/state.vscdb |
| Linux | ~/.config/Cursor/User/globalStorage/state.vscdb |
Override with TRACES_CURSOR_GLOBAL_DB env var.
2. Agent transcript files (legacy)
~/.cursor/projects/<project>/agent-transcripts/<file>
Override with TRACES_CURSOR_PROJECTS env var.
How can I open the state.vscdb file inside my WSL?
Windows drives are mounted in WSL under /mnt/c/. So the path is:
/mnt/c/Users/<your-windows-username>/AppData/Roaming/Cursor/User/globalStorage/state.vscdb
You can open it with sqlite3:
Then query it:
To point the CLI at it, set the env var in your shell:
Note: Cursor locks the DB while it's running. If you get a
database is lockederror, either close Cursor first or copy the file somewhere else and open the copy.
Okay so here's my issue. I have a Windows machine. I run WSL inside that. I have Cursor installed on Windows. When I want to use Cursor inside WSL, I open a folder using cursor . in bash. The threads that I run in WSL cursor do not show on wsl traces, but show up on Windows traces. I do not have a state.vscdb in my WSL ~/.config directory. THe vscdb exists in Windows. Understood so far? Don'
Yes, understood. Cursor is a Windows application — even when you open a WSL folder with cursor ., the Cursor process runs on Windows, so all its data (including state.vscdb) lives in the Windows filesystem, not inside WSL.
The CLI running inside WSL looks for the DB at ~/.config/Cursor/User/globalStorage/state.vscdb (the Linux default path), which doesn't exist — hence no traces show up.
What were you going to say?
Okay so here's my issue. I have a Windows machine. I run WSL inside that. I have Cursor installed on Windows. When I want to use Cursor inside WSL, I open a folder using cursor . in bash. The threads that I run in WSL cursor do not show on wsl traces, but show up on Windows traces. I do not have a state.vscdb in my WSL ~/.config directory. THe vscdb exists in Windows. How does Cursor operate inside WSL? Search the web and explain to me
Here's what's happening:
Cursor is fundamentally a Windows app that uses a split architecture when you connect to WSL — exactly like VS Code Remote-WSL:
┌─────────────────────────────────────────────┐
│ WINDOWS │
│ │
│ Cursor.exe (UI + Storage) │
│ ├── Renders the GUI │
│ ├── Holds all chat/composer state │
│ └── Writes to state.vscdb (Windows path) │
│ │
└───────────────────┬─────────────────────────┘
│ IPC over socket
┌───────────────────▼─────────────────────────┐
│ WSL │
│ │
│ ~/.cursor-server/ (auto-installed) │
│ ├── Handles file system access │
│ ├── Runs terminals │
│ └── Executes extensions in Linux env │
│ │
└─────────────────────────────────────────────┘
When you run cursor . in WSL bash:
.exe)~/.cursor-server/state.vscdb is always written to C:\Users\<you>\AppData\Roaming\Cursor\User\globalStorage\state.vscdbThis is why there's no state.vscdb anywhere in your WSL home — Cursor never writes it there. The traces are in Windows regardless of whether you opened a Windows folder or a WSL folder.
Set this in your WSL shell (add to ~/.bashrc or ~/.zshrc):
Then traces running in WSL will find the Cursor data correctly.
Heads up: If Cursor is open, the DB may be locked. The adapter opens it read-only so it usually works fine, but if you hit a lock error, copy the file first:
bash
find the state.vscdb in my users directory, then try using sqlitebrowser or equivalent to give me an interactive way to see the db
find the state.vscdb in my users directory, then try using sqlitebrowser or equivalent to give me an interactive way to see the db
/mnt/c/Users/Lakshya Agarwal/AppData/Roaming/Cursor/User/globalStorage
░
sqlitebrowser is already installed. Let me open the DB:
Interesting. So the traces in WSL tries to look for the vscdb file in WSL root, is unable to find it. What does it do then? Is there a fallback
Looking directly at the code I already read:
No fallback. The flow is:
isAvailable() is the gate:
In WSL:
~/.config/Cursor/User/globalStorage/state.vscdb → doesn't exist ✗~/.cursor/projects → doesn't exist ✗Both missing → isAvailable() returns false → the AdapterRegistry skips the Cursor adapter entirely when calling getAvailable(). It's as if Cursor isn't installed at all. No traces, no error, no warning — just silence.
The only "fallback" would be if ~/.cursor/projects happened to exist (e.g. you had some legacy transcript files there), in which case isAvailable() would return true, the global DB path would silently return empty, and only transcript files would be shown. But that's not a real fallback — it's just the two sources being independent of each other.
Interesting. So the traces in WSL tries to look for the vscdb file in WSL root, is unable to find it. What does it do then? Is there a fallback. I can see that there is a ~/.cursor/projects directory in my WSL, which contains JSONL files for agent-transcripts
Yes, there's a fallback — and it's exactly what you're describing. Looking back at the code:
isAvailable() returns true if either path exists:
getTraces() always tries both sources independently:
So when state.vscdb is missing in WSL, getGlobalStorageTraces() just returns an empty array, and getAgentTranscriptTraces() kicks in automatically — walking:
~/.cursor/projects/
└── <project>/
└── agent-transcripts/
└── <file>.jsonl ← picked up here
isTranscriptFilename() accepts .json, .jsonl, or extension-less files, so your JSONL files should be found.
So your WSL Cursor traces should already be showing up via the transcript fallback. The catch is how the two sources compare:
| Global DB (Windows) | Agent Transcripts (WSL) | |
|---|---|---|
| Title | Composer name / first message | First user message parsed from file |
| Timestamp | lastUpdatedAt from metadata | File mtime |
| Directory | Resolved from workspace URIs | Project dir name (hyphens → slashes) |
| Content | Full bubble-by-bubble detail | Whatever is in the JSONL |
The transcript entries may look a bit rougher (file mtime as timestamp, directory derived from the folder name), but the content should be there. Have you actually seen them missing from traces list, or was this a theoretical question?
its missing from traces list. can u take stock of the ~/.cursor/projects folder structure (pick any project inside) and see the agent transcripts, compare it to how the code expects it to be
Found the mismatch. The actual structure on disk is:
agent-transcripts/
└── be5a5f24-13dc-4ee2-8552-1d31e6527568/ ← directory
└── be5a5f24-13dc-4ee2-8552-1d31e6527568.jsonl ← file inside
But the code in getAgentTranscriptTraces() expects files directly inside agent-transcripts/:
agent-transcripts/ └── <uuid>.jsonl ← flat, no subdirectory └── <uuid>.json └── <uuid> (extension-less file)
Here's exactly where it breaks. The UUID directory name has no extension, so isTranscriptFilename() says true (extension-less passes). Then:
The directory is silently skipped, and the JSONL inside it is never reached.
The fix needs to handle one extra level — when an extension-less entry turns out to be a directory, look inside it for .jsonl/.json files:
There are actually three different formats in use. Let me check what's inside a .txt file too:
Now the full picture is clear. There are three formats in your WSL projects dir, and the code handles none of them:
agent-transcripts/
└── be5a5f24-.../ ← directory (no extension)
└── be5a5f24-....jsonl ← actual file, never reached
isTranscriptFilename("be5a5f24-...") → ✅ passes (no extension)stat.isFile() on the directory → ❌ bails — directory skipped, JSONL inside never visitedagent-transcripts/ └── 0288e942-....txt ← flat file
isTranscriptFilename("0288e942-....txt") → ❌ fails — .txt not in the accepted list (only .json, .jsonl, extension-less)agent-transcripts/ └── 0ee7ffad-... ← also a directory, same as Format 1
agent-transcripts/ └── <uuid>.json ✅ └── <uuid>.jsonl ✅ └── <uuid> ✅ (if it's a FILE — but on your system they're all DIRECTORIES)
None of your actual files match that expected shape. The fix needs two things:
.jsonl/.json files inside.txt to isTranscriptFilename() — and then figure out if that format is parseable (it appears to be plaintext, not JSON, which would be a separate parsing problem)Want me to open a bug/todo and then fix it?
why txt? why json? arent all agent transcripts just jsonls
Good question. Let me actually look at the content rather than just the extensions.
So the .txt files are plaintext (not JSON at all — they start with user: and XML-like tags), and every single extension-less entry is a directory. Let me tally what actually exists on your system:
There it is. On your system there are exactly two things in agent-transcripts/:
<uuid>/, each containing <uuid>.jsonl inside.txt files — plaintext (the user: / <user_query> format), not JSON at allNo flat .jsonl files. No .json files. Zero. The code was written expecting flat files but Cursor on WSL writes subdirectories.
The .txt files are a completely separate legacy plaintext format — not worth chasing. The only actually parseable traces are the JSONL files inside those 25 directories.
The single real bug: getAgentTranscriptTraces() reads the contents of agent-transcripts/, hits a UUID directory, stat.isFile() returns false, skips it — and never looks inside. The fix is just: when an extension-less entry is a directory, descend one level and pick up the .jsonl inside.
Want me to fix it?
Before fixing, summarize the issue so far so I can open it up in GitHub. Two things of note per my understanding
1/ Cursor running inside WSL is only a server, the actual database updates happen on the Windows host, which means the traces binary in WSL needs an ENV update to pick it up // windows traces picks it up by default
2/ traces binary in WSL looks for agent-transcripts in a broken way (needs to see <uuid>/<uuid>.jsonl)
Correct me if wrong
You're correct on both. Here's a clean summary:
Title: Cursor traces not discovered in WSL
Background:
When Cursor is installed on Windows and opened inside WSL (via cursor .), it runs a split architecture — the GUI and all data storage run as a Windows process, while only a lightweight server (~/.cursor-server/) runs inside WSL. This means:
Issue 1: Global storage (state.vscdb) lives on Windows, not in WSL
The traces binary in WSL looks for the Cursor SQLite DB at ~/.config/Cursor/User/globalStorage/state.vscdb (Linux path), but this file doesn't exist in WSL — Cursor always writes it to the Windows path (C:\Users\<user>\AppData\Roaming\Cursor\User\globalStorage\state.vscdb). The Windows traces binary finds it automatically; WSL does not.
Workaround until fixed: set TRACES_CURSOR_GLOBAL_DB=/mnt/c/Users/<user>/AppData/Roaming/Cursor/User/globalStorage/state.vscdb in WSL.
A proper fix would be auto-detection: if the Linux path doesn't exist, probe the Windows host path via /mnt/c/Users/*/AppData/Roaming/Cursor/User/globalStorage/state.vscdb.
Issue 2: Agent transcript discovery doesn't handle the subdirectory layout
Cursor on WSL writes agent transcripts as:
~/.cursor/projects/<project>/agent-transcripts/<uuid>/<uuid>.jsonl
But getAgentTranscriptTraces() expects files to sit directly inside agent-transcripts/:
~/.cursor/projects/<project>/agent-transcripts/<uuid>.jsonl ← expected (never happens)
When the code encounters the <uuid> directory, isTranscriptFilename() accepts it (no extension passes), but stat.isFile() is false so it silently skips it — never descending into the subdirectory to find the .jsonl inside.
Fix: when an extension-less entry in agent-transcripts/ is a directory, look one level deeper for <uuid>.jsonl.
One small correction to your framing: Issue 2 isn't about the binary being "broken" per se — the code already handles some legacy flat-file layouts correctly. It's specifically that the current Cursor WSL format uses a <uuid>/ subdirectory, which the adapter wasn't written to handle.
okay. can you open this as an issue on this repo: market-dot-dev/traces/. use the gh cli
Done — issue #437 is open.