You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Status (2026-08-20): the mtime-keyed cache this issue asked for shipped in PR #132 (v1.0.83). Cold start went from 8–10s to ~4s and stopped repeating. What is left is the last item the original fix direction explicitly deferred — the first launch still pays one full scan — plus two optional follow-ups. Retitled and rewritten so the open part is what the title says; the original text is preserved below under "Original report" for the reasoning and measurements.
What remains
1. One cold scan per app launch (the actual open item).
Nothing survives process exit: enrichedFileState is a module-level Map, so every launch re-greps all ~100 transcripts (~894MB corpus on the reference machine) before custom titles appear. Measured ~4s. Within a run it is already incremental — a stat pass of a few milliseconds, re-reading only transcripts that actually grew.
Two ways to close it, and they are alternatives, not both:
Persist the cache — write {sessionId → {mtimeMs, size, title, branch, prLink}} next to the other user-level state in ~/.config/codev/, load it at startup, and keep the same stat check as the freshness test. Small, self-contained, no new dependency.
Let the Batch-2 full-text index subsume it — the SQLite FTS5 index (docs/session-finding-plan.md §5.2) already has to track per-file byte offsets and persist them, so once it exists this cache is duplicated state. If FTS is close, do nothing here.
2. Optional follow-ups, only if the cold scan still hurts after the above.
Single-pass grep. Each transcript is still read four times per scan: three grep spawns (custom-title, ai-title, pr-link) plus a tail read for gitBranch (claude-session-utility.ts, loadSessionEnrichment). One pass matching all patterns would cut process spawns ~3×.
What shipped (verified against the code on 2026-08-20 — do not redo)
Original problem
Status
Where
TTL flaw — titlesCacheTimestamp stamped at function entry, so a scan longer than the 5s TTL wrote an already-stale cache and the next caller started another full scan
Fixed. The wall-clock TTL is gone from this path entirely; the stat check is the freshness test
enrichedFileState: Map<string, {mtimeMs, size}>
No in-flight dedup — top-100 pass, VS Code pass and deep-search enrichment each started independent full scans
Fixed. Scans serialize through a promise queue, which stays correct when callers pass different session sets
enrichmentQueue
Per-call maps rebuilt from scratch, so a caller with a different session set could observe holes
Fixed. Module-level accumulators persist across calls
cachedCustomTitles ??= new Map() and siblings
Random title/branch dropout — ~400 concurrent exec greps starved the biggest transcripts past a silent timeout
Fixed. Bounded batches of 25, 5s timeout
runInBatches
Branch read from tail -n 5 — an active session's tail is usually tool output with no gitBranch
Fixed. 256KB tail read, and an explicit detached HEAD clears a stale branch while a tail with no gitBranch line deliberately keeps the old value
readTailUtf8
Shell exposure and lost work on failure
Fixed as well (not in the original report). Reads are shell-free execFile, and a transcript is marked scanned only when every read succeeded, so a timeout retries next pass instead of being cached as empty
CACHE_TTL_MS still exists in that file, but it belongs to readClaudeSessions (the session-list cache) — a different cache from the one this issue is about.
"Would rewriting in Rust help?"
No — the bottleneck is disk IO plus process spawning plus re-scanning unchanged bytes, not per-byte scan speed. A Rust scanner would speed up the scanning fraction; the durable win is reading less, which is what the mtime cache did and what persisting it would finish. Same conclusion as the ecosystem survey in docs/session-finding-plan.md §2.
docs/session-finding-plan.md §5.2 — the FTS index that may subsume this
Original report (2026-07-12) — kept for the measurements and reasoning
Symptoms (measured on the 1.0.83 build, 2026-07-13)
Fresh app launch → Sessions tab: ~2s until the last-AI-message lines (◀) appear, 8–10s until custom titles appear. Values persist afterwards (renderer merges results), but the cost repeats more than it should (see TTL flaw below).
Two related defects existed before this investigation and were already fixed on the PR #132 branch (d07c385):
Random title/branch dropout (pre-existing, seen in old builds too): ~400 concurrent exec greps (100 sessions × 4) starved the biggest transcripts past the silent 3s timeout (errors resolve to '') → the busiest session's title/branch vanished at random. Fixed by batching (10/batch) + 5s timeout.
Branch read from tail -n 5: an active session's tail is often tool output with no gitBranch field (measured on a live 26MB session: tail -5 hit 0, tail -20 hit 12). Fixed by widening to 50 lines.
Remaining structural problems
Cold cost: every scan runs 3 full-file greps per session (custom-title, ai-title, pr-link) over up to ~100 transcripts (~894MB corpus on the reference machine). That's the 8–10s.
TTL flaw (cubic finding on PR feat(sessions): Batch 1 — full-prompt search, match snippets, junk fold, highlight fix #132, discussion r3566660448): titlesCacheTimestamp is set to the value captured at function entry. When the scan itself takes longer than the 5s CACHE_TTL_MS, the just-written cache is already stale → the next caller starts another full scan — perpetual rescans while the popup is in use. There is also no in-flight dedup, so concurrent callers (top-100 pass, VS Code pass, deep-search lazy enrichment) each start independent full scans.
Per-call result maps are rebuilt from scratch, so a caller passing a different session set (e.g. deep-search appended matches) can briefly observe holes within a TTL window.
Fix direction (being implemented on the PR #132 branch)
On each call: stat every requested transcript (~1–2ms total for 100 files); re-grep only new or changed files; serialize scans through a promise queue (in-flight dedup that stays correct for different session sets); drop the wall-clock TTL entirely — the stat check is the freshness test.
First-ever launch still pays one cold scan; afterwards only actively-growing transcripts are re-read.
Possible follow-ups if still needed: single-pass grep (all 4 patterns in one scan), per-batch streaming of results to the renderer (top rows light up first), and ultimately the Batch-2 FTS index (docs/session-finding-plan.md §5.2) subsumes this entirely (it tracks per-file byte offsets anyway).
🤖 On behalf of @grimmerk — generated with Claude Code
What remains
1. One cold scan per app launch (the actual open item).
Nothing survives process exit:
enrichedFileStateis a module-levelMap, so every launch re-greps all ~100 transcripts (~894MB corpus on the reference machine) before custom titles appear. Measured ~4s. Within a run it is already incremental — astatpass of a few milliseconds, re-reading only transcripts that actually grew.Two ways to close it, and they are alternatives, not both:
{sessionId → {mtimeMs, size, title, branch, prLink}}next to the other user-level state in~/.config/codev/, load it at startup, and keep the samestatcheck as the freshness test. Small, self-contained, no new dependency.docs/session-finding-plan.md§5.2) already has to track per-file byte offsets and persist them, so once it exists this cache is duplicated state. If FTS is close, do nothing here.2. Optional follow-ups, only if the cold scan still hurts after the above.
grepspawns (custom-title,ai-title,pr-link) plus a tail read forgitBranch(claude-session-utility.ts,loadSessionEnrichment). One pass matching all patterns would cut process spawns ~3×.loadSessionEnrichmentawaits the whole scan before returning, so nothing paints until everything is done. Emitting each batch of 25 would light the top rows up first — the same "show what you have" philosophy as perf: terminal/IDE badge appears 0.5-1s late — show last-known badge first (SWR) #133.What shipped (verified against the code on 2026-08-20 — do not redo)
titlesCacheTimestampstamped at function entry, so a scan longer than the 5s TTL wrote an already-stale cache and the next caller started another full scanstatcheck is the freshness testenrichedFileState: Map<string, {mtimeMs, size}>enrichmentQueuecachedCustomTitles ??= new Map()and siblingsexecgreps starved the biggest transcripts past a silent timeoutrunInBatchestail -n 5— an active session's tail is usually tool output with nogitBranchHEADclears a stale branch while a tail with nogitBranchline deliberately keeps the old valuereadTailUtf8execFile, and a transcript is marked scanned only when every read succeeded, so a timeout retries next pass instead of being cached as emptyCACHE_TTL_MSstill exists in that file, but it belongs toreadClaudeSessions(the session-list cache) — a different cache from the one this issue is about."Would rewriting in Rust help?"
No — the bottleneck is disk IO plus process spawning plus re-scanning unchanged bytes, not per-byte scan speed. A Rust scanner would speed up the scanning fraction; the durable win is reading less, which is what the mtime cache did and what persisting it would finish. Same conclusion as the ecosystem survey in
docs/session-finding-plan.md§2.Cross-refs
docs/session-finding-plan.md§5.2 — the FTS index that may subsume thisOriginal report (2026-07-12) — kept for the measurements and reasoning
Symptoms (measured on the 1.0.83 build, 2026-07-13)
Fresh app launch → Sessions tab: ~2s until the last-AI-message lines (◀) appear, 8–10s until custom titles appear. Values persist afterwards (renderer merges results), but the cost repeats more than it should (see TTL flaw below).
Two related defects existed before this investigation and were already fixed on the PR #132 branch (
d07c385):execgreps (100 sessions × 4) starved the biggest transcripts past the silent 3s timeout (errors resolve to'') → the busiest session's title/branch vanished at random. Fixed by batching (10/batch) + 5s timeout.tail -n 5: an active session's tail is often tool output with nogitBranchfield (measured on a live 26MB session: tail -5 hit 0, tail -20 hit 12). Fixed by widening to 50 lines.Remaining structural problems
custom-title,ai-title,pr-link) over up to ~100 transcripts (~894MB corpus on the reference machine). That's the 8–10s.titlesCacheTimestampis set to the value captured at function entry. When the scan itself takes longer than the 5sCACHE_TTL_MS, the just-written cache is already stale → the next caller starts another full scan — perpetual rescans while the popup is in use. There is also no in-flight dedup, so concurrent callers (top-100 pass, VS Code pass, deep-search lazy enrichment) each start independent full scans.Fix direction (being implemented on the PR #132 branch)
mtime-keyed per-file incremental cache:
{mtimeMs, size}scan state.statevery requested transcript (~1–2ms total for 100 files); re-grep only new or changed files; serialize scans through a promise queue (in-flight dedup that stays correct for different session sets); drop the wall-clock TTL entirely — the stat check is the freshness test.Possible follow-ups if still needed: single-pass grep (all 4 patterns in one scan), per-batch streaming of results to the renderer (top rows light up first), and ultimately the Batch-2 FTS index (
docs/session-finding-plan.md§5.2) subsumes this entirely (it tracks per-file byte offsets anyway).🤖 On behalf of @grimmerk — generated with Claude Code