# Cursor Schema — Storage Observations

## Two storage locations

Cursor uses SQLite databases (`state.vscdb`) in two locations:

| Location | Path (relative to `~/Library/Application Support/Cursor/User/`) |
|----------|----------------------------------------------------------------|
| **Global** | `globalStorage/state.vscdb` |
| **Workspace** | `workspaceStorage/<hash>/state.vscdb` (one per workspace) |

## Which db to ingest

**Global db (`globalStorage/state.vscdb`)** is the correct and only source for `cursor_vscdb` ingestion. It contains all rich data in the `cursorDiskKV` table:

- `composerData:<composerId>` — session metadata, conversation headers, inline messages
- `bubbleId:<composerId>:<bubbleId>` — per-bubble data (text, tokenCount, createdAt, allThinkingBlocks)
- `agentKv:blob:<hash>` — content-addressed binary blobs for agent conversation state

**Workspace dbs (`workspaceStorage/*/state.vscdb`)** are lightweight copies only:
- `ItemTable` has session index + user prompts (`composer.composerData`, `aiService.prompts`)
- No assistant content, no token counts in `cursorDiskKV` (it's empty)
- Not useful for ingestion

## conversationState format and blob loading

The `composerData` entry contains a `conversationState` field that can be:

1. **String** (common case): Base64-encoded protobuf with embedded SHA-256 hashes. Optional leading `~` prefix. The protobuf contains length-delimited fields of exactly 32 bytes, each being a SHA-256 hash pointing to an `agentKv:blob:<hex>` row in the same table. These blobs contain the actual conversation messages (user/assistant/tool) as JSON.

2. **Dict** (legacy): Direct hash map where values are 64-char hex SHA-256 hashes pointing to the same `agentKv:blob:<hex>` rows.

`load_session()` handles both formats, extracting hashes and loading the corresponding blobs into `session.agent_kv_entries`.

## Transform modes

`transform_cursor_vscdb` detects and dispatches to three modes:

| Mode | Detection | Data source |
|------|-----------|-------------|
| **inline** | `conversation[]` with text content | Bubble text directly in composerData |
| **hashchain** | `conversationState` is a non-empty string | agentKv blobs via protobuf hash extraction |
| **headers_only** | `fullConversationHeadersOnly[]` | bubbleId entries for text/tokens |

If hashchain produces 0 messages (e.g., blobs missing/deleted), it falls back to headers_only when `fullConversationHeadersOnly` entries exist.

## Workspace path extraction (for git metadata)

`composerData` does NOT contain `cwd`, `workspacePath`, or `createdOnBranch`. However, Cursor injects a `<user_info>` block into the first user message of every hashchain session:

```
<user_info>
OS Version: darwin 25.2.0
Shell: zsh
Workspace Path: /Users/sagar/work/all-things-quickcall/trace
Is directory a git repo: Yes, at /Users/sagar/work/all-things-quickcall/trace
Today's date: Friday Feb 20, 2026
</user_info>
```

The collector extracts `Workspace Path:` from the first user message content and passes it as `cwd` to `_build_session_context()`, which calls `resolve_repo()` to populate repo_name, git_branch, git_commit, and repo_url.

Verified: 9/10 recent hashchain sessions have this block.

## Current coverage

| Format | Source type | Status |
|--------|-----------|--------|
| Global db (`cursorDiskKV`) | `cursor_vscdb` | Supported — all three modes |
| Workspace dbs (`ItemTable`) | — | Not ingested (lightweight copies only) |
| Text transcripts (`~/.cursor/projects/*/agent-transcripts/*.txt`) | `cursor` | Supported — captures full conversation content |
