Skip to content

perf(storage): keep no raw copy of imported events that projected - #66

Merged
0xCardinal merged 2 commits into
mainfrom
perf/storage
Oct 22, 2026
Merged

0xCardinal merged 2 commits into
mainfrom
perf/storage

Conversation

@0xCardinal

Copy link
Copy Markdown
Member

Phase 7 of the ingest performance plan. Stacked on #65 (which is stacked on #64).

Where the bytes go

Fresh import of a copy of real Claude history (611 MB), by dbstat: events 222 MB (payload 104 MB, detail 70 MB), raw_ingest_events 138 MB (raw_payload 103 MB), FTS 32 MB, indexes ~33 MB.

raw_payload is the same body as events.payload, except capped at 64 KB, and nothing reads it back except the raw-ingest listing.

What changed

  • An imported event that projects keeps its ledger row (metadata, hash, status) but no raw_payload. The event row (uncapped) and the transcript on disk both still hold the body.
  • Kept as before: hook bodies (cleanup migrations at db.py 579/719/1856 delete hook events while their ledger rows stay, so the raw copy can be the only one), failed, ignored, malformed and duplicate rows.
  • Existing DBs: prune_import_raw_payloads() runs once at the end of the background startup work, 2,000 rows per write so ingest gets in between, and records completion in settings. Freed pages are reused by later writes; shrinking the file on disk needs a VACUUM, which this PR doesn't run.

Numbers

  • Fresh import: 437 MB → 320 MB (−27%), same speed (15.1 s).
  • On a real 3.4 GB cot.db (read-only count): projected import rows hold 198 MB of raw payload, about 6%. Projected and duplicate hook rows hold another ~366 MB; left alone here for the reason above.

Not done

A contentless FTS index: FTS is 32 MB of 320 MB and search reads from it, so not worth the risk here.

Tests

New tests in test_raw_ingest_drift.py: which rows keep a raw copy, and the one-time batched prune. 628 passed.

🤖 Generated with Claude Code

…d write

Watcher (app/watch.py): with passive mode on, a watchdog observer (FSEvents
on macOS, inotify on Linux) watches the Claude, Cursor and Codex transcript
folders. A burst of .jsonl writes is debounced (1.5 s) and the changed,
enabled agents are imported in-process with live sessions included, so a
running session shows up in about two seconds instead of after the schedule
and the 10-minute hold-back. In-process passes now close only imported
sessions quiet for 10 minutes (complete-imported takes quiet_seconds; an
older bridge's {} still closes all). The schedule keeps running for analysis,
OpenCode/Hermes, and anything a dropped event missed. Without watchdog, or
with COT_PASSIVE_WATCH=0, nothing is watched.

Large files: new bytes are read 1 MB at a time with parser state carried
across chunks, so the bridge peaks at ~140 MB instead of ~720 MB, and
transcripts over 50 MB are no longer skipped. A line longer than a chunk is
read whole up to 64 MB; a longer one is skipped with a warning. Lines split
on "\n" only, offsets are cut on bytes (invalid UTF-8 no longer shifts them).

Fixes Cursor appends being dropped: import line numbers restarted at 0 on
every pass, so an appended line got the dedup key of the file's first line.
Line numbers are now file-wide (kept in the identity sidecar, counted once
for older entries); keys of already-imported lines are unchanged.

Overlap: the import poster sends batches from a background thread while the
next batch parses (SQLite and HTTP release the GIL).
The raw ingest ledger stored every body twice over: raw_payload (cut at
64 KB) next to events.payload (uncapped). For an imported event that
projected, the event row and the transcript on disk both hold the body, so
the ledger row now keeps its metadata, hash and status but no payload.
Hook bodies keep theirs (cleanup migrations can delete hook events while
their ledger rows stay), as do failures, ignored rows and duplicates.

Existing DBs are pruned once, in batches of 2,000 per write, at the end of
the background startup work. Freed pages are reused by later writes;
shrinking the file needs a VACUUM, left to the user.

A fresh import of 611 MB of Claude history: 437 MB -> 320 MB, same speed.
Base automatically changed from perf/watch-stream to main October 22, 2026 00:59
@0xCardinal
0xCardinal merged commit 52a332f into main Oct 22, 2026
7 checks passed
@0xCardinal
0xCardinal deleted the perf/storage branch October 22, 2026 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant