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
Companion PR: Tuesday PR 2 (search error codes + UX polish #2) — merge this PR first; shared cache dir under ~/.claude-code-chat-browser/.
Problem
GET /api/search is a brute-force substring scan: for every project, every .jsonl session file, it calls get_cached_session() (full JSONL parse) and walks every message in memory. Complexity is O(projects × sessions × messages) per query. The in-memory LRU session cache (PR #90) avoids re-parses within a server run but does not reduce work on the first search after restart. On a power-user install (20+ projects, 300+ sessions), a single search can take tens of seconds. tests/benchmarks/test_search_bench.py exercises live-scan only; benchmarks/baselines.json excludes test_search_full_corpus from the gate, so the search path is unguarded.
cppa-cursor-browser solved this with a derived search_index.sqlite (SQLite FTS5), mtime-keyed fingerprint invalidation, background rebuild, and live-scan fallback (cppa-cursor-browser PR #113). Claude has flat .jsonl files under ~/.claude/projects/ — the index must use line-by-line JSONL extraction, not a verbatim port of cppa's composer-bubble model.
Week 1 PR #111 (session summary disk cache) is merged; FTS index files live beside session_summary_cache.sqlite in the same cache root.
Goal
One merged PR that adds a local FTS5 search index with live-scan fallback, default 30-day search window (all_history opt-in), and re-enables the search benchmark regression gate. Structured error codes and search-page UX copy land in Tuesday PR 2.
Scope
Part A — FTS search index (utils/search_index.py, new)
Build (line-by-line, not get_cached_session): walk projects via list_sessions(); line-by-line JSON decode (no full parse_session()); extract searchable text via utils/jsonl_helpers.py / session_peek.py. Deliberate deviation from operator issue-description wording that mentions get_cached_session during build — lower memory, faster rebuild, cppa PR claude-code-chat-browser: FTS search index, search UX, and distinct /api/search error codes #113 parity. get_cached_session remains source-of-truth for live-scan fallback and session detail/export only.
Indexed text scope: user/assistant text and content on each message line plus tool-result searchable blobs (parity with fields api/search.py scans today around line 67). Only denormalized strings go into messages_fts, not full SessionDict.
Lifecycle:ensure_search_index, start_search_index_background (daemon on app.py startup), index_is_usable; bypass via CLAUDE_CODE_CHAT_BROWSER_NO_SEARCH_INDEX=1.
tests/benchmarks/: re-enable test_search_full_corpus in benchmarks/baselines.json gate (currently excluded).
Optional stretch (not release-blocking)
Wire PR #111summary cache into live-scan fallback: consult session_summary_cache for (path, mtime) title/timestamp pre-filter before get_cached_session during live-scan. Defer to week 3 if time runs short.
Out of scope (Tuesday item #2)
Structured error codes (SEARCH_EMPTY_QUERY, SEARCH_INDEX_UNAVAILABLE, etc.) — empty q may still return 200 [] until Tuesday merges.
Calendar Day
Monday, July 6, 2026 (PR 1 of 2 — search bundle)
Planned Effort
8 story points (Task) — sprint item #1
Companion PR: Tuesday PR 2 (search error codes + UX polish #2) — merge this PR first; shared cache dir under
~/.claude-code-chat-browser/.Problem
GET /api/searchis a brute-force substring scan: for every project, every.jsonlsession file, it callsget_cached_session()(full JSONL parse) and walks every message in memory. Complexity is O(projects × sessions × messages) per query. The in-memory LRU session cache (PR #90) avoids re-parses within a server run but does not reduce work on the first search after restart. On a power-user install (20+ projects, 300+ sessions), a single search can take tens of seconds.tests/benchmarks/test_search_bench.pyexercises live-scan only;benchmarks/baselines.jsonexcludestest_search_full_corpusfrom the gate, so the search path is unguarded.cppa-cursor-browser solved this with a derived
search_index.sqlite(SQLite FTS5), mtime-keyed fingerprint invalidation, background rebuild, and live-scan fallback (cppa-cursor-browser PR #113). Claude has flat.jsonlfiles under~/.claude/projects/— the index must use line-by-line JSONL extraction, not a verbatim port of cppa's composer-bubble model.Week 1 PR #111 (session summary disk cache) is merged; FTS index files live beside
session_summary_cache.sqlitein the same cache root.Goal
One merged PR that adds a local FTS5 search index with live-scan fallback, default 30-day search window (
all_historyopt-in), and re-enables the search benchmark regression gate. Structured error codes and search-page UX copy land in Tuesday PR 2.Scope
Part A — FTS search index (
utils/search_index.py, new)~/.claude-code-chat-browser/(same root as PR Session list performance: disk summary cache, display-name cache, and count alignment #111session_summary_cache.sqlite).search_index.<uuid>.sqlite+search_index.activepointer (atomic swap — cppa pattern; safe on Windows).projects_dir+ sorted manifest of every.jsonlas(relative_path, mtime_ns, size_bytes)+ hash of serialised exclusion rules.index_meta;sessions(session_id, project_name, title, first_ms, last_ms, file_path);messages_ftsFTS5(session_id, project_name, role, timestamp_ms, text, tokenize='unicode61').get_cached_session): walk projects vialist_sessions(); line-by-line JSON decode (no fullparse_session()); extract searchable text viautils/jsonl_helpers.py/session_peek.py. Deliberate deviation from operator issue-description wording that mentionsget_cached_sessionduring build — lower memory, faster rebuild, cppa PR claude-code-chat-browser: FTS search index, search UX, and distinct /api/search error codes #113 parity.get_cached_sessionremains source-of-truth for live-scan fallback and session detail/export only.textandcontenton each message line plus tool-result searchable blobs (parity with fieldsapi/search.pyscans today around line 67). Only denormalized strings go intomessages_fts, not fullSessionDict.SearchHitDictwith ±80-char snippets; applysince_mswindow; include undated sessions in windowed search (cppa_INCLUDE_UNKNOWN_TIMESTAMPS_IN_WINDOWparity); apply exclusion rules on hit candidates (prefer PR Session list performance: disk summary cache, display-name cache, and count alignment #111 summary cache when warm).ensure_search_index,start_search_index_background(daemon onapp.pystartup),index_is_usable; bypass viaCLAUDE_CODE_CHAT_BROWSER_NO_SEARCH_INDEX=1.threading.Lockfor build; readers use?mode=ro.Part B — Search API window params (
api/search.py)DEFAULT_SEARCH_WINDOW_DAYS = 30,resolve_search_since_ms(all_history, since_days).all_history=1/true;since_days=N(validation tightening for invalid values → Tuesday #2 /SEARCH_INVALID_SINCE_DAYS).list[SearchHitDict]; pagination_DEFAULT_LIMIT=50,_MAX_LIMIT=500preserved.Part C — Tests and benchmarks
tests/test_search_index.py(new): schema, fingerprint, FTS hit, window filter,all_history, tool-result text indexed,NO_SEARCH_INDEXfallback, pointer swap.tests/benchmarks/: re-enabletest_search_full_corpusinbenchmarks/baselines.jsongate (currently excluded).Optional stretch (not release-blocking)
Wire PR #111 summary cache into live-scan fallback: consult
session_summary_cachefor(path, mtime)title/timestamp pre-filter beforeget_cached_sessionduring live-scan. Defer to week 3 if time runs short.Out of scope (Tuesday item #2)
SEARCH_EMPTY_QUERY,SEARCH_INDEX_UNAVAILABLE, etc.) — emptyqmay still return200 []until Tuesday merges.docs/api-reference.mderror catalog updates.Follow-up (post-merge)
Acceptance Criteria
FTS index
utils/search_index.pybuilds FTS5 index under~/.claude-code-chat-browser/..jsonl(path, mtime, size)or rules fingerprint changes.get_cached_session/ fullparse_session).text/contentand tool-result searchable blobs.GET /api/searchuses index when usable; live-scan fallback when disabled/missing.all_history=1searches full corpus; undated sessions in window.Flask==3.1.3;sqlite3is stdlib.tests/test_search_index.pypasses;test_search_full_corpusre-enabled in benchmark gate.SearchHitDictresponse envelope and pagination preserved.General
mypy --strict, fullpytest, andruffpass.Verification
Manual:
GET /api/search?q=test(noall_history) — old session outside 30 days absent.GET /api/search?q=test&all_history=1— old session present.CLAUDE_CODE_CHAT_BROWSER_NO_SEARCH_INDEX=1— live-scan still works.