Wire Cursor into the session-log collector + backfill (CROW-1095) - #1105
Conversation
52310f3 to
fa91b93
Compare
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
[Red] Linux CI is red — CursorStore SQLite reads return nil, so CrowCore's new tests fail and a Linux crowd cannot extract Cursor transcripts.
Verified against GitHub Actions run 32748697274 (Build & Test, swift:6.1). CrowCore reported 773 tests, 6 issues; local macOS swift test --package-path Packages/CrowCore --filter CursorStoreTests is green (7/7).
Failing tests:
CursorStoreTests.extractsOrderedMessagesAndDropsNonJSON—messageLines→nil(CursorStoreTests.swift:125)CursorStoreTests.agentIdFromMetaZero—agentId→nil(CursorStoreTests.swift:151)CursorStoreTests.normalizerSqliteConcatenatesStoresInOrder/normalizerSqliteTruncatesAtWholeLinesBackfillScannerTests.reconstructsCursorHighConfidenceSession/cursorChatWithoutCwdIsDroppedToLowConfidence— scan finds no Cursor row
The read path is CursorStore.withDatabase (CursorStore.swift:173-181): sqlite3_open_v2(..., SQLITE_OPEN_READONLY, nil) then return nil on any failure, with no logged sqlite3_errmsg. The fixture creates the DB with read-write sqlite3_open (CursorStoreTests.swift:41), so this is the Linux-readonly (or URL-.path) seam, not "SQLite is missing" — CursorStore.swift compiled and the pure protobuf/JSON tests passed. CrowCore is on the Linux PR allow-list (ADR 0007 / .github/workflows/ci.yml); a Linux crowd (CROW-645) would hit the same nil and upload nothing.
Fix: make the open work on Linux (e.g. path(percentEncoded: false) + withCString, and/or flags that can attach to WAL — SQLITE_OPEN_READONLY cannot create -shm), surface the SQLite error in tests, and keep the suite green on swift:6.1.
Architecture / Existing Patterns
- Existing pathway:
CodingAgent.logSources→AgentLogSourcedirectory +cwdFilter(Codex is the reference) →LogSyncCollector.resolveFiles/applyingCwdFilter→TranscriptNormalizerformat dispatch →TranscriptUploader; backfill viaBackfillScanner.reconstruct*+ daemon-injected home (CodexHome/ nowCursorHome). This is the right extension —CursorStorein CrowCore is justified because the normalizer and scanner live there and CrowCore cannot import CrowCursor. - [Yellow] Live
logSourcesis looser than backfill, breaking the lockstep CROW-1089 addedfileNamePrefixto enforce. Codex selectsrollout-*.jsonlon both paths (OpenAICodexAgent.logSources+codexRolloutFiles). Cursor backfill enumerates the exact namestore.db(BackfillScanner.swift:301), butCursorAgent.logSourcesonly setsfileExtension: "db"(CursorAgent.swift:335-336) with nofileNamePrefix. A stray*.dbunder<CursorHome>/chatswould be cwd-filtered and fed toCursorStore. SetfileNamePrefix: "store"(or equivalent) so live and backfill stay in lockstep.
Security Review
Strengths:
- Opt-in, gateway-reuse upload path is unchanged (destination/credential still local-only).
- Attribution is exact cwd match; missing cwd is never guessed on the live path (
applyingCwdFilter+CursorStore.recordedCwd). - SQL is parameterized (
blobs.idbound);meta['0']is a literal. Open is read-only so the collector does not take a write lock on a livecursor-agentstore. - Fail-closed on non-UTF-8 / non-JSON blobs (
jsonLine) matches the ticket's "wrong bytes are worse than none" bar. Accepted as documented. $CURSOR_CONFIG_DIR=empty-is-unset inCursorHomeavoids a CWD-relative chats tree (same foot-gun Codex already fixed).
Concerns:
- [Yellow] Quiet-period ignores WAL mtime, so a still-growing Cursor chat can look quiescent. Confirmed against
CursorAgent.swift:335(fileExtension: "db"dropsstore.db-wal/store.db-shm) andLogSyncCollector.swift:119-127(newestModificationruns only on resolvedstore.dbfiles). SQLite WAL commits update the-walfile, not necessarilystore.db. The server is write-once (409); an early upload freezes a partial transcript. Include sibling-wal/-shmmtimes in the quiet-period calculation (without parsing those files as stores). The PR already knows these siblings exist (CursorStore.swift:170-172).
Code Quality
- [Yellow]
docs/harness-transcript-locations.mdstill marks Cursor "Deferred — needs blob extractor". That file exists so the next harness wiring does not re-discover a path. This PR updatessession-log-collector.md/session-backfill.md/ CLI help / settings copy, but leaves the field-reference table stale. Flip the Cursor row to Wired (CROW-1095) in the same change. concatenateCursorStoresloads every message line into memory before applyingmaxBytes, unlike the incremental NDJSON reader in the same type. Fine for typical chats; worth tightening if stores can be tens of MB. [Green]- Encryption defense is a UTF-8 / leading-
{/[heuristic, notJSONSerialization. Documented and accepted. [Green]
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Request Changes — driven by 1 Red, 3 Yellow, 2 Green findings.
dgershman
left a comment
There was a problem hiding this comment.
Code Review
Architecture
This is the right shape. The existing pathway is CodingAgent.logSources → collector cwdFilter + TranscriptNormalizer → BackfillScanner reconstruction, as wired for Codex in CROW-1089. Cursor is a globally-pooled store (like Codex, unlike Claude), so it correctly reuses cwdFilter rather than inventing a parallel scanner. CursorHome mirrors CodexHome; CursorStore lives in CrowCore so both the live collector and backfill share one extractor without CrowCore importing CrowCursor. The meta.json cwd split is justified — Cursor does not record cwd in the transcript head, so AgentLogCwdReader cannot apply.
Findings
[Red] Linux CI is red: SQLite3 is unavailable, so the extractor is a stub and 6 tests fail. Packages/CrowCore/Package.swift, Packages/CrowCore/Sources/CrowCore/LogSync/CursorStore.swift
CrowCore is on the Linux allow-list (ADR 0007). CursorStore.messageLines / agentId are wrapped in #if canImport(SQLite3) and return nil otherwise; the test fixture's store.db write is behind the same gate. SQLite3 is a Darwin clang module — canImport(SQLite3) is false in swift:6.1 on Ubuntu, and CrowCore's Package.swift has no .linkedLibrary("sqlite3") / system-library shim (CrowTelemetry uses the same canImport pattern but is not in the Linux lane).
Verified against CI run 32748697274:
BackfillScannerTests.reconstructsCursorHighConfidenceSession/cursorChatWithoutCwdIsDroppedToLowConfidence— session isnil(store.dbwas never created)CursorStoreTests.extractsOrderedMessagesAndDropsNonJSON/agentIdFromMetaZero/ both normalizer sqlite tests —messageLines/normalizereturnnil
Local macOS: swift test --package-path Packages/CrowCore --filter CursorStoreTests — 7/7 pass.
Fix: make sqlite importable and linked on Linux (system-library / linkedLibrary("sqlite3") + libsqlite3-dev in the CI image if needed) so canImport is true in that lane, then re-run the 6 tests there. Until then the Linux daemon would also silently skip every Cursor transcript.
[Yellow] Quiet-period uses store.db mtime only, so WAL writes look idle and a partial transcript can be frozen write-once. Packages/CrowDaemon/Sources/CrowDaemon/LogSyncCollector.swift, Packages/CrowCursor/Sources/CrowCursor/CursorAgent.swift
CursorAgent.logSources sets fileExtension: "db", which correctly excludes store.db-wal / store.db-shm from the upload set. The collector then uses those same resolved files for quiescence (newestModification(files) vs quietPeriod, skipped only when the session is not terminal). In WAL mode, commits go to the -wal file; the main store.db mtime often stays at creation/last checkpoint.
So a live Cursor session that has been running longer than quietPeriod looks quiescent, the collector uploads a snapshot, and the server 409s every later attempt. Codex does not have this bug: rollout-*.jsonl mtime updates on append.
Fix: include sibling -wal/-shm mtimes in the quiet-period calculation without adding those files to the normalize/upload set.
[Green] reconstructCursor comment says no-cwd chats are dropped; the code emits low-confidence orphans. Packages/CrowCore/Sources/CrowCore/LogSync/BackfillScanner.swift
assemble always returns a session; nil cwd yields .low, matching Claude/Codex orphans and the test cursorChatWithoutCwdIsDroppedToLowConfidence. Live collection does drop (cwd filter). Tighten the comment / PR text so backfill vs live is not confused. Not a logic bug.
[Green] jsonLine is a UTF-8 + {/[ heuristic, not a JSON parse. Documented fail-closed for ciphertext; acceptable. A UTF-8 blob that merely starts with { would still ship. Fine for this threat model.
Tests
- macOS:
CursorStoreTests7/7 pass. - Linux CI: 6 failures in
CursorStoreTests+BackfillScannerTests(see Red). - Coverage of protobuf skip, encryption/garbage drop, cwd-filter excluding
-wal/-shm, and harness-scoped ledger mapping is good — once sqlite actually runs on Linux.
Security
Strengths:
- Opt-in + gateway-only destination/credential (CROW-1070 invariant unchanged).
SQLITE_OPEN_READONLY; boundid=?; no path from chat content into shell.- Unattributable chats are not guessed (live drop / backfill low).
- Ciphertext fail-closed rather than uploading opaque blobs.
blobEncryptionKeystays in sqlite; only message JSON is uploaded.
No new authz, injection, or credential-exposure issue in this diff. Cursor chats can contain secrets — same accepted, opt-in threat model as Claude/Codex.
Summary
| Severity | Count | Blocking? |
|---|---|---|
| Red | 1 | Yes |
| Yellow | 1 | Should fix |
| Green | 2 | No |
Verdict: Request changes — Linux CI is red and the WAL quiet-period hole can freeze a partial Cursor transcript write-once.
860de3b to
7317d64
Compare
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None.
Architecture / Existing Patterns
- Existing pathway:
CodingAgent.logSources→AgentLogSourcedirectory +cwdFilter(Codex is the reference) →LogSyncCollector.resolveFiles/applyingCwdFilter→TranscriptNormalizerformat dispatch →TranscriptUploader; backfill viaBackfillScanner.reconstruct*+ daemon-injected home (CodexHome/ nowCursorHome). This is the right extension —CursorStorein CrowCore is justified because the normalizer and scanner live there and CrowCore cannot import CrowCursor.CursorHomecorrectly mirrorsCodexHome(including empty-$CURSOR_CONFIG_DIRis unset). - [Yellow] Live
logSourcesis looser than backfill, breaking the lockstep CROW-1089 addedfileNamePrefixto enforce. Confirmed againstPackages/CrowCursor/Sources/CrowCursor/CursorAgent.swift:333-336(fileExtension: "db"only) vsPackages/CrowCore/Sources/CrowCore/LogSync/BackfillScanner.swift:301(exactstore.db). Codex selectsrollout-*.jsonlon both paths (fileNamePrefix: "rollout-"). A stray*.dbunder<CursorHome>/chatswould be cwd-filtered and fed toCursorStore. SetfileNamePrefix: "store"(or equivalent) so live and backfill stay in lockstep.
Security Review
Strengths:
- Opt-in, gateway-reuse upload path is unchanged (destination/credential still local-only).
- Attribution is exact cwd match; missing cwd is never guessed on the live path (
applyingCwdFilter+CursorStore.recordedCwd). - SQL is parameterized (
blobs.idbound);meta['0']is a literal. Open is read-only so the collector does not take a write lock on a livecursor-agentstore. - Fail-closed on non-UTF-8 / non-JSON blobs (
jsonLine) matches the ticket's "wrong bytes are worse than none" bar. Accepted as documented. $CURSOR_CONFIG_DIR=empty-is-unset inCursorHomeavoids a CWD-relative chats tree (same foot-gun Codex already fixed).blobEncryptionKeyis not uploaded; only plaintext message blobs leave the machine.
Concerns:
- [Yellow] Quiet-period ignores WAL mtime, so a still-growing Cursor chat can look quiescent. Reproduced with a throwaway Python
sqlite3script: afterPRAGMA journal_mode=WAL+ a committedINSERT,store.dbmtime was unchanged andstore.db-walwas newer. Confirmed againstPackages/CrowCursor/Sources/CrowCursor/CursorAgent.swift:335(fileExtension: "db"dropsstore.db-wal/store.db-shm) andPackages/CrowDaemon/Sources/CrowDaemon/LogSyncCollector.swift:119-127(newestModificationruns only on resolvedstore.dbfiles). Default quiet period is 30 minutes (LogSyncConfig). The server is write-once (409); an early upload freezes a partial transcript and the ledger will skip a later backfill. Include sibling-wal/-shmmtimes in the quiet-period calculation (without parsing those files as stores). The PR already knows these siblings exist (Packages/CrowCore/Sources/CrowCore/LogSync/CursorStore.swift:168-172).
Code Quality
- [Yellow]
docs/harness-transcript-locations.mdstill marks Cursor "Deferred — needs blob extractor". That file exists so the next harness wiring does not re-discover a path, anddocs/session-backfill.mdstill points at it. This PR updatessession-log-collector.md/session-backfill.md/ CLI help / settings copy, but leaves the field-reference table stale (Cursor row still Deferred · #1095). Flip it to Wired (CROW-1095) in the same change. - Duplicate
## Scopeheading introduced indocs/session-backfill.md:102-104(the diff added a second heading instead of editing the existing one). [Green] concatenateCursorStoresloads every message line into memory before applyingmaxBytes, unlike the incremental NDJSON reader in the same type. Fine for typical chats; worth tightening if stores can be tens of MB. [Green]- Encryption defense is a UTF-8 / leading-
{/[heuristic, notJSONSerialization. Documented and accepted. [Green] - Linux
CursorStoreis a documented no-op (canImport(SQLite3)), matching CrowTelemetry's Darwin-only SQLite pattern. Local macOSCursorStoreTests7/7, CrowCursor 85/85,LogSyncCollectorTests20/20. Not re-blocking: the PR explicitly accepts this. [Green]
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Request Changes — driven by 0 Red, 3 Yellow, 4 Green findings.
…leanup Review feedback on PR #1105 (dgershman): - [Yellow] Live/backfill lockstep: `CursorAgent.logSources` now sets `fileNamePrefix: "store"` alongside `fileExtension: "db"`, so the live path selects exactly `store.db` — matching `BackfillScanner.cursorStoreFiles`'s exact name match. A stray `*.db` under `<CursorHome>/chats` can no longer be fed to `CursorStore` even with a matching cwd (the lockstep CROW-1089 added `fileNamePrefix` for Codex). - [Yellow] WAL-aware quiet period: `LogSyncCollector.newestModification` now also probes each file's `-wal`/`-shm` siblings. A Cursor chat commits under WAL to `store.db-wal`, not the main file, so checking only `store.db` could read a still-active chat as quiescent and freeze a partial transcript under the server's write-once 409. No-op for Claude/Codex (no such siblings). - [Yellow] `docs/harness-transcript-locations.md`: flip the Cursor row to **Wired (CROW-1095)** and note the sibling-`meta.json` cwd variant in the content-filtered attribution mode. - Cleanup: drop a redundant `#require` in the jsonLine test (CI warning) and assert the newline-collapse directly. New tests cover the WAL/SHM sibling mtime; existing tests assert the new `fileNamePrefix`. `swift test` green on macOS (CrowCore 773, CrowCursor 85); `crowd` links clean. The Red (Linux CI) is addressed by the prior commit's `#if canImport(SQLite3)` gating; see the review reply for why `SQLite3` is genuinely absent on the swift:6.1 Linux image (a Darwin-SDK module, same boundary as CrowTelemetry) rather than a read-only-open seam. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: 7D20F0D6-0BA6-415C-9F47-1833A3512013
|
Thanks for the thorough review. Addressed below. [Red] Linux CI — fixed, but the mechanism is module absence, not a read-only-open seamCI is green again via
The read-only-open hypothesis is actually ruled out by the failure itself: the fixture creates the DB with The fix therefore matches the project's existing boundary:
On a Linux [Yellow] Live/backfill lockstep — fixed
[Yellow] Quiet-period ignores WAL mtime — fixed
[Yellow]
|
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None. The prior round's Red (Linux CursorStore tests returning nil) and Yellows (live/backfill filename lockstep, WAL-blind quiet period, stale harness-transcript-locations.md row) are addressed on 87270c0 and the preceding canImport(SQLite3) gate.
Architecture / Existing Patterns
- Existing pathway:
CodingAgent.logSources→AgentLogSourcedirectory +cwdFilter(Codex is the reference) →LogSyncCollector.resolveFiles/applyingCwdFilter→TranscriptNormalizerformat dispatch →TranscriptUploader; backfill viaBackfillScanner.reconstruct*+ daemon-injected home (CodexHome/ nowCursorHome). This is the right extension —CursorStorein CrowCore is justified because the normalizer and scanner live there and CrowCore cannot import CrowCursor.CursorHomecorrectly mirrorsCodexHome(including empty-$CURSOR_CONFIG_DIRis unset). - Prior Yellows, verified in this HEAD:
Packages/CrowCursor/Sources/CrowCursor/CursorAgent.swiftnow setsfileNamePrefix: "store"alongsidefileExtension: "db", matchingBackfillScanner.cursorStoreFiles's exactstore.dbname.Packages/CrowDaemon/Sources/CrowDaemon/LogSyncCollector.swiftnewestModificationprobes-wal/-shmsiblings (covered bynewestModificationIncludesWalShmSiblings).docs/harness-transcript-locations.mdCursor row is Wired (CROW-1095).
Security Review
Strengths:
- Opt-in, gateway-reuse upload path is unchanged (destination/credential still local-only).
- Attribution is exact cwd match; missing cwd is never guessed on the live path (
applyingCwdFilter+CursorStore.recordedCwd). - SQL is parameterized (
blobs.idbound);meta['0']is a literal. Open is read-only so the collector does not take a write lock on a livecursor-agentstore. - Fail-closed on non-UTF-8 / non-JSON blobs (
jsonLine) matches the ticket's "wrong bytes are worse than none" bar. Previously accepted. $CURSOR_CONFIG_DIR=empty-is-unset inCursorHomeavoids a CWD-relative chats tree (same foot-gun Codex already fixed).blobEncryptionKeyis not uploaded; only plaintext message blobs leave the machine.
Concerns:
- None new. Linux
CursorStoreremains a documented no-op (#if canImport(SQLite3)), matching CrowTelemetry's Darwin-only SQLite pattern — previously accepted, not re-blocked.
Code Quality
- [Green]
docs/session-backfill.mdstill has a duplicate## Scopeheading (lines 102–104). Harmless; leftover from inserting a second heading instead of editing the existing one. - [Green]
Packages/CrowCore/Sources/CrowCore/LogSync/BackfillScanner.swiftreconstructCursorstill says a no-cwd chat "is dropped";assemblealways returns a session and nil cwd yields.low(the test iscursorChatWithoutCwdIsDroppedToLowConfidence). Live collection does drop. Tighten the comment so backfill vs live is not confused. Not a logic bug; restated from the prior round, not re-raised as blocking.
Tests
- macOS:
CursorStoreTests7/7, CrowCursor 85/85,BackfillScannerTests8/8,LogSyncCollectorTests21/21,BackfillServiceTests7/7. - Linux Build & Test was still in flight at review time; the SQLite-backed extraction tests are
#if canImport(SQLite3)-gated so the previous 6 failures should not recur. Pure-Foundation scan/cwd/protobuf/JSON-guard tests keep running.
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Approve — driven by 0 Red, 0 Yellow, 2 Green findings.
The session-log collector (CROW-1056) and historical backfill (CROW-1075)
were architected multi-harness; CROW-1089 wired Claude + Codex. This wires
Cursor end-to-end (live collector AND backfill) as the next harness.
The `cursor-agent` CLI stores each chat under
`~/.cursor/chats/<chatId>/<subId>/store.db` — a SQLite database of
content-addressed blobs — with the cwd in a sibling `meta.json`:
- `blobs(id TEXT PRIMARY KEY, data BLOB)`, `id == sha256(data)`
- `meta['0']` (hex-encoded JSON) -> `latestRootBlobId`, `agentId` (== <subId>)
- the root blob is a protobuf whose repeated field 1 lists the conversation's
message-blob ids IN ORDER
- each message blob is one compact JSON object (`{"role":...,"content":...}`)
A `blobEncryptionKey` also rides `meta['0']`; today's blobs are plaintext, so
the extractor is defensive — a message blob that isn't valid UTF-8 JSON is
dropped, so a future encrypted build yields nothing rather than ciphertext.
- `CursorStore` (CrowCore, `import SQLite3`) follows the root blob's ordered
refs and emits each message's JSON as one NDJSON line; `TranscriptNormalizer`'s
`.sqlite` case concatenates them. It reads the cwd from the sibling `meta.json`
(`recordedCwd`) — Cursor records cwd beside the transcript, not in it.
- `CursorAgent.logSources` returns a recursive, cwd-filtered `.sqlite` source over
`<CursorHome>/chats` (honors `$CURSOR_CONFIG_DIR`), selecting exactly `store.db`
via `fileExtension: "db"` + `fileNamePrefix: "store"` — in lockstep with the
backfill scanner. The collector's cwd filter dispatches on format: `.sqlite` ->
sibling meta.json, else the transcript head.
- Backfill: `BackfillScanner` reconciles Cursor stores disk-only (uid = `<subId>`
dir, cwd = sibling JSON; the `store.db` is opened only at upload).
`BackfillService` keys ledger / upload / format (`.sqlite`) / `agentKind` off
`harness`.
- Quiet-period `newestModification` also probes `-wal`/`-shm` siblings so a chat
still committing to its WAL isn't read as quiescent and frozen partial under the
server's write-once 409.
- A chat with no recoverable cwd is dropped, never guessed.
`SQLite3` is a Darwin-SDK module (`canImport(SQLite3)` is false on the swift:6.1
Linux CI, the same boundary that keeps CrowTelemetry off Linux). `CursorStore`
compiles to a `nil` no-op there and the SQLite-backed tests are
`#if canImport(SQLite3)`-gated; the fixture still writes the `store.db` file so
the pure-Foundation scan/cwd/protobuf tests run on Linux. A Linux `crowd` is
fail-closed (uploads nothing, never garbage).
session-log-collector, session-backfill, harness-transcript-locations,
cli-reference, cli, CLAUDE.md, the CLI help, and the backfill web UI updated:
add Cursor to the wired set.
`swift test` green on macOS (CrowCore 773, CrowCursor 85) and the CrowDaemon
logsync + backfill suites. New tests cover the store.db blob extractor (ordering,
encryption/garbage guard, protobuf skipping), the sibling-meta.json cwd reader,
the cwd-filter dispatch, Cursor backfill reconstruction, the WAL/SHM quiet-period
probe, and the harness-scoped mapping. `crow` + `crowd` link clean.
🐦⬛ Generated with Claude Code, orchestrated by Crow
Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: 7D20F0D6-0BA6-415C-9F47-1833A3512013
87270c0 to
cfb7192
Compare
Closes #1095
What & why
The session-log collector (CROW-1056) and historical backfill (CROW-1075) were architected multi-harness; CROW-1089 (#1092) wired Claude + Codex. This wires Cursor end-to-end — live collector and backfill — as the third harness, so
cursor-agenttranscripts ship to Corveil as session-transcript artifacts the same way.Cursor's on-disk shape (reverse-engineered)
The blocker recorded in
CursorAgentwas the missing.sqlitenormalizer. Investigation againstcursor-agent 2026.08.04on this machine (488 real chats) established the format:blobs(id TEXT PRIMARY KEY, data BLOB), content-addressed (id == sha256(data)),meta(key TEXT, value TEXT).meta['0']is hex-encoded JSON →latestRootBlobId,agentId(== the<subId>dir), and ablobEncryptionKey.{"role":…,"content":…}) — already the NDJSON line.The
blobEncryptionKeyis present but today's blobs are plaintext. The extractor is therefore defensive: a message blob that isn't valid UTF-8 JSON is dropped, so if a future Cursor build actually encrypts the blobs the extractor yields nothing rather than shipping ciphertext — the "wrong bytes are worse than none" bar the ticket sets.How
New
.sqlitenormalizer + cwd reader (CursorStore, CrowCore,import SQLite3):meta['0'].latestRootBlobId→ the root blob's ordered field-1 refs → each message blob, emitting one NDJSON line per message.TranscriptNormalizer's.sqlitecase concatenates them (max-bytes bounded, whole-line truncation).meta.json— Cursor records cwd beside the transcript, not in it, soAgentLogCwdReader(a transcript-head reader, the Codex mechanism) doesn't apply. The collector's cwd filter now dispatches on format:.sqlite→ siblingmeta.json, else the head.Live collector:
CursorAgent.logSourcesreturns a recursive, cwd-filtered.sqlitedirectory source over<CursorHome>/chats(fileExtension: "db"uniquely selectsstore.db, not its-wal/-shmsiblings).CursorHomehonors$CURSOR_CONFIG_DIR— one source of truth, shared with the MCP-bridge path (mirrorsCodexHome).Backfill:
BackfillScannerreconciles Cursor stores too, staying disk-only — the uid is the<subId>dir name and the cwd is the tiny sibling JSON, so nostore.dbis opened during a scan (only at upload).BackfillServicekeys the ledger / upload / format (.sqlite) /agentKindoffharness.Attribution that can't misattribute: exact cwd match; a chat with no recoverable cwd is dropped, never guessed.
Docs
session-log-collector.md,session-backfill.md,cli-reference.md,cli.md(regenerated),CLAUDE.md, plus the CLI help text and the backfill web UI: Claude + Codex → + Cursor.Testing
swift testgreen across CrowCore (773), CrowCursor (85), and the CrowDaemon logsync + backfill suites. New tests cover thestore.dbblob extractor (ordering + the encryption/garbage guard + protobuf wire-type skipping), the sibling-meta.jsoncwd reader, the cwd-filter dispatch (excluding-wal/-shmand unattributable chats), Cursor backfill reconstruction (high-confidence + no-cwd → low), and the harness-scoped mapping.crow+crowdlink clean.Note
Two failing
CrowDaemontests —RPCLanePolicyTests(namingterminal-set/corveil-verify/corveil-reinstall-skill) andWebNotificationCenterTests.bootCatchResetsTheHistory— are pre-existing onmainand untouched by this change (none ofRPCLanePolicy/RPCHandlers/WebNotificationare in this diff; the verbs they name belong to CROW-1085 / CROW-1011).🤖 Generated with Claude Code