Skip to content

fix(cli): speed up local recall searches - #12726

Merged
marius-kilocode merged 5 commits into
mainfrom
optimize-kilo-local-recall-performance
Aug 3, 2026
Merged

fix(cli): speed up local recall searches#12726
marius-kilocode merged 5 commits into
mainfrom
optimize-kilo-local-recall-performance

Conversation

@marius-kilocode

@marius-kilocode marius-kilocode commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Problem

Large local session histories made kilo_local_recall repeatedly scan and JSON-parse parts that can never match: reasoning parts, successful tool output, synthetic text, and ignored text. The old search also paged through the same SQLite session ranges and joined message metadata before knowing whether a part matched.

What Changed

Area Before After
SQLite access Scanned the general part_session_idx path and paged through all part ranges Uses recall_part_search_idx, a covering index over searchable fields only
Index installation Required a fatal startup migration with a 23-second large-database stall Creates the index lazily on the first recall search with CREATE INDEX IF NOT EXISTS; failure is logged, search falls back, and the next search retries
Search scope Traversed approximately 757,000 part rows Index contains approximately 43,000 searchable rows; exact candidate prefilter evaluated approximately 16,000 rows in measured queries
Memory bound Broad scan pages, then repeated filtering and joins Keyset pages contain at most 1,024 candidates; page data and message metadata are released before advancing
Cancellation Yielded between broad scan pages Yields and checks cancellation between every 1,024 candidate rows
Pagination Revisited completed session ranges on every page Removes session IDs already passed by the cursor before the next page
Content matching Pulled broad rows into JavaScript before filtering SQLite performs a conservative candidate prefilter, then JavaScript retains exact NFKC matching and ranking
Message metadata Joined message rows during the broad part scan Fetches message metadata only for actual content hits, page by page
Planner dependency Forced an index with INDEXED BY Lets SQLite select the covering index and includes a query-plan regression test
Coverage metric Counted all traversed part rows with a second index scan Reports exact transcript candidates evaluated during the search pass
Corrupt historical JSON Could be evaluated by the broad path json_valid excludes malformed rows from the derived index without modifying or deleting them

Benchmark Corpus

Measurement Baseline run Final run
Database 14 GB SQLite database Copy-on-write clone of the same production-scale database
Sessions in scope 3,085 3,094
Total part rows Approximately 757,000 More than 787,000
Searchable indexed rows Not separately indexed Approximately 43,000
Exact candidates evaluated per measured query Not prefiltered Approximately 15,800 to 15,900
Recall index size None Approximately 59 MB, 0.4% of the 14 GB database

The corpus-count difference reflects additional sessions and parts written between the baseline and final clone.

Search Performance

Timings are wall-clock seconds. Baseline timings were captured before the optimization. Final timings were captured three times after bounded pagination and removal of the duplicate count pass; the reported final value is the median.

Query Baseline Final run 1 Final run 2 Final run 3 Final median Improvement Speedup
kilo_local_recall 30.20s 0.991s 0.583s 0.601s 0.601s 29.599s faster 50.3x
recall-search 6.90s 0.625s 0.556s 0.589s 0.589s 6.311s faster 11.7x
zyx-no-match-987654 8.20s 0.732s 0.540s 0.627s 0.627s 7.573s faster 13.1x

Final repeated searches ranged from 0.540s to 0.991s while keeping candidate memory bounded to 1,024 rows.

Write Path

Legacy text parts are written at text start and text completion; token deltas use message.part.delta and do not upsert the full legacy part row per token. The benchmark below writes 100 separate completed 20 KB text parts, with an empty start row followed by one completed row.

Scenario Without recall index With recall index Difference
100 completed 20 KB text parts, median of 5 runs 15.62ms 18.82ms +3.20ms total, +20.5%
Average per completed part 0.156ms 0.188ms +0.032ms

The index intentionally trades this measured write overhead for the 11.7x to 50.3x search improvement.

Failure Behavior

Scenario Behavior
Index already exists Search proceeds normally
Index is missing First recall search creates it lazily; later searches use it
Index creation is blocked or fails Warning is logged; that search uses the ordinary SQLite path; the next search retries
Malformed historical part JSON Original row remains untouched and is excluded from the derived index
Process starts without invoking recall No recall index build or write lock is taken

Result

The reviewed, bounded recall path is 11.7x to 50.3x faster on the measured large-session corpus. It keeps peak candidate materialization at 1,024 rows, preserves exact Unicode matching and ranking, avoids fatal startup work, and uses a sub-60 MB derived index rather than a tokenized full-session search index.

@marius-kilocode
marius-kilocode enabled auto-merge (squash) July 31, 2026 11:02
Comment thread packages/opencode/src/kilocode/session/recall-search.ts Outdated
Comment thread packages/opencode/src/kilocode/session/recall-search.ts Outdated
Comment thread packages/opencode/src/kilocode/session/recall-search.ts Outdated
Comment thread packages/core/src/kilocode/session/recall-part-index.ts
Comment thread packages/core/src/database/migration/20260731102142_recall-part-index.ts Outdated
Comment thread packages/opencode/test/kilocode/recall-search.test.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
packages/core/src/database/migration/20260731102142_recall-part-index.ts 9 The 60 s busy_timeout only covers this connection. Other processes keep 5 s and still fail during the ~23 s build, and the migration stays fatal under Effect.orDie in Database.layer, so a lock timeout or failed build takes down startup for an index only recall uses.
packages/core/src/kilocode/session/recall-part-index.ts 11 Covering index duplicates each searchable part's full text and is rebuilt on every part upsert, adding write amplification to the streaming path. The PR now quantifies this (+20.5% / +0.032 ms per completed text part), so it is a measured, accepted tradeoff rather than an unknown — flagging only so the decision is explicit.

SUGGESTION

File Line Issue
packages/opencode/src/kilocode/session/recall-search.ts 64 The OR keyset cursor cannot act as an index seek next to session_id IN (...), so every page re-walks the index rows of already-consumed sessions. Cost is ~indexed_rows x pages / 2, which only bites when candidates approach row count (non-ASCII corpora where the GLOB fallback fires broadly). Dropping passed session ids from the bound list removes it.
packages/opencode/src/kilocode/session/recall-search.ts 59 With INDEXED BY gone, nothing asserts the planner still picks recall_part_search_idx; predicate drift would silently restore the full-scan path. An EXPLAIN QUERY PLAN assertion would lock in the win.
Resolved since the previous review
  • Unbounded result materialization and lost mid-scan cancellation — keyset pagination now caps each page at 1,024 rows and yields/checks the signal between pages.
  • Duplicate countSql index scan — removed; the metric is now the candidate count from the same pass.
  • Hard INDEXED BY dependency on a partial index — removed, so a planner mismatch degrades instead of dying.
  • Test fixture regression — noise parts are searchable user/text rows again, and a multi-page query plus cancellation on that query are asserted.
Files Reviewed (5 files)
  • packages/core/src/database/migration/20260731102142_recall-part-index.ts - 1 issue
  • packages/opencode/src/kilocode/session/recall-search.ts - 2 issues
  • packages/core/src/kilocode/session/recall-part-index.ts - 1 issue (unchanged file, verified still current)
  • packages/opencode/src/tool/recall.ts - no issues (searchedParts -> candidateParts has no other consumers in the repo)
  • packages/opencode/test/kilocode/recall-search.test.ts - no issues

Notes and assumptions

  • Verified PRAGMA busy_timeout = 60000 does take effect for the index build: migrations run inside begin deferred (packages/effect-drizzle-sqlite/src/effect-sqlite/session.ts:145), so the write lock is acquired at CREATE INDEX, after the pragma. The restore value 5000 matches the layer default at packages/core/src/database/database.ts:31.
  • Memory was re-checked per the leak focus: found, hits, and the per-page message map are released each iteration, and the accumulated per-session candidates stay bounded by term count and snippet length. No leak found.
  • Planner behavior and all timings come from the PR description; nothing was executed in this read-only review.

Fix these issues in Kilo Cloud

Previous Review Summary (commit 15ad465)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 15ad465)

Status: 6 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/session/recall-search.ts 226 Search result set is fully materialized with no LIMIT; peak memory now scales with the whole searchable corpus, the scan is one uninterruptible synchronous block, and signal no longer cancels mid-scan. The non-ASCII GLOB fallback can defeat the prefilter entirely.
packages/core/src/kilocode/session/recall-part-index.ts 11 Covering index duplicates each part's full text and is rebuilt on every PartUpdated upsert, adding write amplification to the streaming path. Read path is benchmarked, write path is not.
packages/core/src/database/migration/20260731102142_recall-part-index.ts 9 ~23 s CREATE INDEX transaction runs during Database.layer init under Effect.orDie with busy_timeout = 5000; concurrent processes can make it fail startup, and it blocks their writes while it runs.

SUGGESTION

File Line Issue
packages/opencode/src/kilocode/session/recall-search.ts 225 countSql re-scans the same covering index purely to populate the parts metric; also silently changes that metric's meaning.
packages/opencode/src/kilocode/session/recall-search.ts 55 INDEXED BY against a partial index hard-fails if predicate matching ever breaks; predicate is duplicated in three places and failure becomes a defect rather than a slower search.
packages/opencode/test/kilocode/recall-search.test.ts 305 Fixture swap from searchable user/text noise to excluded reasoning parts removes coverage of the single path that lost its LIMIT.
Files Reviewed (10 files)
  • packages/opencode/src/kilocode/session/recall-search.ts - 3 issues
  • packages/core/src/kilocode/session/recall-part-index.ts - 1 issue
  • packages/core/src/database/migration/20260731102142_recall-part-index.ts - 1 issue
  • packages/opencode/test/kilocode/recall-search.test.ts - 1 issue
  • packages/core/src/session/sql.ts - no issues (index definition correctly extracted to a kilo-only module; keeping it in the shared Drizzle schema is required so fresh databases get the index, since DatabaseMigration.apply marks migrations complete without running them)
  • packages/core/src/database/schema.gen.ts - no issues (generated)
  • packages/core/src/database/migration.gen.ts - no issues (generated)
  • packages/core/schema.json - no issues (generated; the array reformatting and dropped trailing newline are drizzle-kit output artifacts)
  • packages/core/test/kilocode/database-migration-compat.test.ts - no issues
  • .changeset/fast-local-recall.md - no issues

Notes and assumptions

  • The SQL prefilter was checked for correctness against the JS fold() (NFKC + lowercase): SQLite's ASCII-only lower() plus instr is safe for ASCII text, and the non-ASCII GLOB fallback covers folding differences, so no false negatives were found. The cost of that fallback is the concern, not its correctness.
  • BATCH = 8_192 bound parameters is within the SQLite 3.32+ SQLITE_MAX_VARIABLE_NUMBER default of 32,766 for both the bun:sqlite and node:sqlite backends.
  • Reported timings and index sizes were taken from the PR description; they were not independently reproduced (read-only review, no execution).

Fix these issues in Kilo Cloud


Reviewed by claude-opus-5 · Input: 64 · Output: 25.1K · Cached: 2.7M

Review guidance: REVIEW.md from base branch main

Comment thread packages/core/src/database/migration/20260731102142_recall-part-index.ts Outdated
Comment thread packages/opencode/src/kilocode/session/recall-search.ts
Comment thread packages/opencode/src/kilocode/session/recall-search.ts
@marius-kilocode
marius-kilocode disabled auto-merge July 31, 2026 12:12
@marius-kilocode
marius-kilocode enabled auto-merge (squash) July 31, 2026 12:30
@marius-kilocode
marius-kilocode merged commit 2fbd380 into main Aug 3, 2026
30 checks passed
@marius-kilocode
marius-kilocode deleted the optimize-kilo-local-recall-performance branch August 3, 2026 10:22
t7tran pushed a commit to t7tran/kilocode that referenced this pull request Aug 14, 2026
* fix(cli): speed up local recall searches

* fix(cli): bound local recall scans

* fix(cli): make recall index initialization lazy
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.

2 participants