Claude Code transcripts grow to tens of MB. Three blockers prevent using them as memory: (1) they exceed any context window; (2) summarization is lossy — decisions and the "why" die in compaction; (3) there is no predefined taxonomy to search by — topics are implicit.
| Fact | Consequence |
|---|---|
Records carry uuid/parentUuid/timestamp written by the harness |
thread reconstruction is deterministic, not heuristic |
promptId exists ONLY on user records (12,553 of 12,553 user records; 0 assistant) |
thread membership must be resolved transitively up the parent graph |
Sidechain records carry sourceToolAssistantUUID (10,385 records) |
subagent transcripts attach to their spawning thread |
| Signal is small: ~4.2MB assistant text + 2.7MB tool inputs + 1MB tool results out of 84MB | the searchable corpus fits easily in SQLite; bulk is images/attachments |
| The live transcript is REWRITTEN by the pruner (52 generations, 3GB of backups exist) | byte offsets into the live file are perishable; backups are immutable |
| Live-agent wikilink tagging produced 4 tags in 31k records under an enforced protocol | the write path of memory cannot depend on model compliance |
| 1 line in the corpus contains concatenated JSON objects | the reader must recover multiple objects per line |
| Invalid UTF-8 can appear in tool output | offsets must be computed on bytes (surrogateescape), not replaced text |
Vault = the original files: immutable backup generations
(v0-raw.jsonl, vN-pruned.jsonl) + the mutable live transcript tail.
Never modified by fable (the pruner writes new generations, atomically).
Map = SQLite:
copies(uuid, file_id, lineno, offset, length)— ground truth: every copy of every record in every file.records(uuid, …, file_id, offset, length, fidelity, fts_rowid, ts_epoch, prompt_id, parent_uuid, source_uuid, session_id)— the denormalized BEST pointer (max length; ties → earliest generation), recomputed fromcopieswhenever a file is rescanned, rewritten, or vanishes. If no copy survives, the record is forgotten and its FTS row and citations are reaped (reconcile).threads(prompt_id, session_id, first/last_ts, turn_count, est_tokens, first/leaf_uuid)— rebuilt by aggregation after every index run.fts(FTS5) — extracted signal text only; maintained by rowid (an UNINDEXED-column WHERE is a full virtual-table scan; rowid keying took the real-corpus index from 4m55s to 6.6s).terms(term, kind=operative|target|concept|wikilink, prompt_id, count, score)— deterministic discovery vocabulary.cards(prompt_id, title, type=decision|workflow|insight|concept, topics, decisions, files, outcome, summary, source, model)— LLM semantic layer.citations(from_uuid, ref)— which turns consumed which recalled arcs.sessions(session_id, project, title, live_path)— multi-project dimension.
iter_records (byte-exact spans, concat recovery, surrogateescape)
→ copies upsert + best-pointer upsert + FTS extract (inception guard)
→ recompute_best for invalidated pointers
→ resolve_thread_membership (iterative parent-graph walk, memoized)
→ rebuild threads → reconcile orphans → single commit
→ index_terms (operatives/targets/TF-IDF concepts per thread)
search = FTS5 BM25 per record, aggregated per thread, intersected with
term facets, joined to cards/sessions. thread = canonical chain from the
leaf via parentUuid (orphan edit-branches and sidechains labeled), each
turn read by seek+read, text verbatim, bulky blocks elided to the budget
with [truncated — fable block <uuid>] markers, hard output cap at
1.5× budget. block = one byte-identical record. All reads check file
size/mtime against the Map first (StaleIndexError instead of silently
serving garbage).
- Recall output is sentinel-wrapped (
<historical_context … arcs="…">). - The extractor strips sentinel spans before FTS (records citations); the renderer stubs out inner sentinels so nesting cannot occur.
- The pruner replaces sentinel spans with
<consulted_arcs refs="…"/>. - Prune
--replaceprotocol: snapshot size → mandatory backup → index that backup → evict gate (every uuid must have an IMMUTABLE full-fidelity copy ≥ its current length) → write temp → append any post-snapshot bytes verbatim → fsync →os.replace. Backup names aremax(version)+1with exclusive create — generations can never be clobbered, even by concurrent prunes.
Two independent reviews (correctness; ops/robustness) ran against the first complete implementation; ~19 findings each. Criticals fixed at the root: fidelity-downgrade-on-prune (→ copies table), truncating in-place rewrite (→ atomic protocol), TOCTOU append loss (→ tail preservation), UTF-8 offset drift (→ surrogateescape spans), O(N²) FTS maintenance (→ rowid keying), orphan FTS ghosts (→ reconcile), gate proving the wrong property (→ immutable-copy gate). Each has a regression test (tests/test_hardening.py, tests/test_prune.py::TestLifecycleFidelity).
Known accepted limits: exFAT 2s mtime granularity could miss a same-size
rewrite (re-stat catches size changes; not applicable on APFS);
render --raw is verbatim by definition (inner sentinels not stubbed);
card content is LLM output from transcript data — treat card text as data,
not instructions, when surfacing into sessions.
Synthesis of three sources: the arc-memory README (kept: Vault/Map split, typed arcs, citation graph, inception guard; rejected: live wikilink tagging, XML vault, JSON-file indexes, alias-map/NER machinery, importance feedback loop), prune_transcript_v1.py (kept: noise/bloat rules, compaction handling, chain rebuild, versioned backups; fixed: image bloat, atomicity, gate), and the empirical structure of real Claude Code transcripts.