perf: skip copying and indexing sidechain conversations to reduce disk usage#111
Open
minyek wants to merge 1 commit into
Open
perf: skip copying and indexing sidechain conversations to reduce disk usage#111minyek wants to merge 1 commit into
minyek wants to merge 1 commit into
Conversation
…k usage The goal is to cut episodic-memory's on-disk footprint. Sidechain transcripts — subagent/Task dispatches, Warmup stubs, and prompt-suggestion/ai-title sidechains — are already excluded from search by the `AND is_sidechain = 0` filter in search.ts, yet were still embedded, written to the DB, and copied to the archive on every sync. In one install that was ~40% of the SQLite DB and ~25% of the archive — disk spent on data no query can reach. This stops that waste; search results are unchanged, and sync latency is roughly the same (it skips embedding sidechains but adds a cheap per-file check). Two layers: * Index time — a single exported indexExchanges() in indexer.ts owns the embed-and-insert loop and skips exchanges flagged isSidechain. All five indexing paths route through it (full reindex, single session, unprocessed backlog, background sync, verify/repair), so the rule can't be applied to some paths and missed on others. * Copy time — isSidechainFile() in sync.ts classifies a file by reading whole JSONL records (chunked, bounded to 1 MB) until one carries an isSidechain flag, so a large opening record isn't truncated. It is conservative on every error path, so ambiguous and mixed-content files still get copied and fall through to the per-exchange filter. The check sits behind a cheap mtime gate so unchanged files aren't re-read each sync. Sidechain files are never copied, indexed, or summarized. The is_sidechain schema column stays in place so a future opt-in can lift the search filter and re-index from the archive. Tests cover both the manual reindex path and the background sync path: top-level and nested sidechain files are not copied, regular files are, mixed-content files index only their non-sidechain exchanges, and a sidechain file whose first record exceeds the read chunk is still skipped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
perf: skip copying and indexing sidechain conversations to reduce disk usage
Description
The primary goal of this PR is to reduce episodic-memory's on-disk footprint. The plugin copies and indexes sidechain conversations (subagent dispatches, Warmup/
ai-titlestubs, prompt-suggestion sidechains) that search already excludes and never returns. In my install that was ~40% of the SQLite DB and ~25% of the conversation archive — disk spent on data no query can reach. This PR stops copying and indexing them so that storage isn't consumed going forward. It also avoids the embedding work for those exchanges, but the motivation is storage, not speed — sync latency is roughly unchanged. If a sidechain type later proves worth searching, it can be added with summarisation and query support.sync.tscopies every discovered.jsonlfrom~/.claude/projects/into~/.config/superpowers/conversation-archive/, including nested subagent dispatches at<project>/<session>/subagents/agent-*.jsonland top-level harness stubs at<project>/agent-*.jsonl(Warmup, prompt-suggestion sidechains,ai-title).indexer.tsthen embeds every parsed exchange into a 384-float vector and inserts rows intoexchangesandvec_exchanges, regardless ofisSidechain.search.tsunconditionally excludes them viaAND e.is_sidechain = 0in both vector and text query paths.This is a storage change, not a behavior change: search results are identical before and after, because the
AND e.is_sidechain = 0filter (originally requested in #42) already hides these exchanges from every query and has no off switch. That left an asymmetry — the data is filtered out of every result, yet still embedded, written to the DB, and copied to disk. This PR removes that wasted storage by stopping at copy time and index time, matching the filter that search already applies.Why These Files Exist
All
isSidechain: true, all legitimate Claude Code artifacts:<project>/<parent-session>/subagents/. The agent's report-back already lives in the parent transcript and is searchable; the sidechain holds only the agent's internal trace (files read, intermediate reasoning).<project>/agent-aXXX.jsonl. Prompt-cache warmup stubs ({"content":"Warmup"}), prompt-suggestion sidechains,ai-titlestubs. Most parse to 0 exchanges./btwasides — Claude Code's/btw("by the way") command asks a quick question off the main thread; its records areisSidechain: trueand never join the main conversation. We considered surfacing these but intentionally don't: like every sidechain here they're already excluded from search by theis_sidechain = 0filter, so this PR doesn't change their searchability, and they're a small volume of deliberately ephemeral content —/btwexists precisely to keep asides out of history — so summarising and indexing them for search isn't worth the cost.None have semantic value for cross-session search as currently surfaced.
Change
Two layers:
1. Skip sidechain exchanges at index time. A single exported
indexExchanges()insrc/indexer.tsowns the embed + insert loop and short-circuits onisSidechain, skipping both the embedding generation and the DB insert — symmetric with the existingAND e.is_sidechain = 0filter insrc/search.ts. All five indexing paths route through it (full reindex, single session, unprocessed backlog, background sync, and verify/repair), so the filter can't be applied to some paths and missed on others. The two that matter most aresyncConversations— the background path that runs on every session start — andrepairIndex; both would otherwise index inline sidechain exchanges from mixed-content files. The helper returns the inserted (non-sidechain) count, which the user-facing "Exchanges: N" logs report.2. Skip sidechain files at copy time. New
isSidechainFile()insrc/sync.tsreads the file in chunks and inspects whole (newline-terminated) records until one carries anisSidechainflag, returning that value. It reads complete records rather than a fixed byte window, so a subagent file whose opening record is large (a big initial prompt with attachments) is classified correctly instead of truncated; the scan is bounded (1 MB) so a pathologically large file is never read in full. Conservative on every error path (unreadable file, an unparseable record, or no record carrying the field within the cap) so ambiguous and mixed-content files still get copied and handled by the per-exchange filter above. The check is gated behind a cheap mtime comparison, so unchanged files aren't re-read on every sync. Sidechain files are counted asskipped, never copied, and don't enterfilesToIndexorfilesToSummarize.Schema groundwork for the
is_sidechaincolumn stays in place, so a future opt-in feature could lift the search filter and re-index from archive.Tests
test/indexer-sidechain.test.ts(new, 2 tests) — the manual reindex path (indexUnprocessed):COUNT(*) FROM exchangesis 1,COUNT(*) WHERE is_sidechain = 1is 0.COUNT(*) FROM exchangesis 0.test/sync.test.ts(5 new tests):agent-a01.jsonlWarmup stub → not copied.<session>/subagents/agent-a01.jsonlsubagent dispatch → not copied.syncConversationspath, which the indexer test above doesn't exercise; it indexed both rows before this fix.The sync test fixture now sets
EPISODIC_MEMORY_CONFIG_DIRto isolate from any pre-existingexclude.txtthat would have pre-filteredsubagents/dirs at discovery and masked the test signal. Existingtest/incremental-indexing.test.tsandtest/exclude-nested.test.tsstill pass.Validation
Beyond the unit suite, the change was exercised against real transcripts in an isolated scratch environment (real files read-only; scratch DB and archive, summaries off):
Environment
For existing installs
This PR only prevents new sidechain data. Existing archived files and DB rows from prior versions stay until cleaned up. To reclaim space, run from the plugin directory:
Step 2 covers
<session>/subagents/dirs; top-level<project>/agent-aXXX.jsonlWarmup stubs from prior versions are smaller and can be left in place or removed manually. After this PR lands, no new sidechain files are written, so the cleanup is one-shot.Safe to skip — search results are unaffected either way (sidechain rows weren't returned before, aren't now). Worth running if disk pressure matters; ~40% DB and ~25% archive reduction in my install.