Skip to content

fix(semantic): worktrees search their own files in the shared dedup cache - #26

Merged
JRedeker merged 3 commits into
mainfrom
work/work-75aed740176ef0f172bdf768
Sep 25, 2026
Merged

JRedeker merged 3 commits into
mainfrom
work/work-75aed740176ef0f172bdf768

Conversation

@JRedeker

Copy link
Copy Markdown
Contributor

Implements LGREP-17.

Problem. With LGREP_WORKTREE_DEDUP=1, every checkout of a repository shared one cache that held one file set. Chunk rows carried no checkout, and the server aliased every later checkout to the first checkout's ProjectState, so a worktree searched (and indexed from) another checkout's files. Stale-file deletion was also off under dedup.

Change.

  • src/lgrep/storage/_chunk_store.py: rows carry a checkout column. Base rows (checkout = '') belong to the trunk. OverlayStore, a per-worktree view of the owner store, writes overlay rows only for files that differ from base or that base lacks. Worktree search prefilters (checkout = '' AND file_path NOT IN shadowed) OR checkout = '<worktree>'; trunk search filters checkout = ''. Base writes bump base_generation.json; an overlay whose recorded generation differs re-compares every file. sync_checkout leaves every row a checkout sees matching its file on disk. Scalar indexes on checkout (BITMAP) and file_path (BTREE) keep long shadow lists cheap in hybrid search. Existing caches gain the column via add_columns with no re-embedding. Zero-chunk markers now record the content hash.
  • src/lgrep/server/lifecycle.py: one owner ChunkStore per cache (_stores); each checkout path gets its own ProjectState and Indexer. A worktree brings its trunk's base current before comparing against it.
  • src/lgrep/server/tools_semantic.py, src/lgrep/cli.py: search reconciles a worktree overlay (hashing only) before searching it; indexing a worktree indexes its base first.
  • src/lgrep/indexing.py: stale-file cleanup runs again under dedup (through sync_checkout); a file whose content equals base drops its overlay.
  • src/lgrep/tools/prune_orphans.py, src/lgrep/tools/invalidate_worktree.py: gc_worktree_meta and invalidate_worktree_cache delete overlay rows of removed worktrees.

Evidence.

  • tests/test_worktree_cache.py::TestWorktreeOverlay: 13 tests (own files, trunk isolation, diff-only embedding, base-change recheck, migration without embedding, gc, first search before embedding via MCP and CLI, zero-chunk transitions, filter indexes). Each mechanism was mutated and its test fails without it.
  • Full suite: 993 passed, 1 xfailed.
  • Real 104,860-row cache copy, host load ~60: hybrid search with a 5,000-path shadow list took 17.9 s without scalar indexes and 3.3 s with them (baseline with no shadow list 4.5 s).

Known limits. A worktree's first search returns only files it shares with base until the background pass embeds its difference. Every base write bumps the generation, so the next worktree search after a trunk edit re-hashes that worktree's files (no embedding).

…ache

Under LGREP_WORKTREE_DEDUP every checkout of a repository shares one
LanceDB table. Rows now carry a checkout column: base rows (checkout '')
belong to the trunk, and each linked worktree writes overlay rows only
for files that differ from base or that base lacks.

- Worktree search prefilters (base minus shadowed paths) OR own overlay;
  trunk search filters checkout = ''. Scalar indexes on checkout and
  file_path keep long shadow lists cheap in hybrid search.
- Each checkout gets its own ProjectState and Indexer over one shared
  owner store (OverlayStore view), so discovery and hashing use the
  worktree's own files.
- Base writes bump base_generation.json; a worktree re-compares every
  file with base when the generation moved.
- Existing caches gain the column through add_columns with no
  re-embedding. Stale-file cleanup runs again under dedup.
- gc_worktree_meta and invalidate_worktree_cache delete overlay rows of
  removed worktrees.

LGREP-17
A worktree whose overlay had not compared its files with the current
base rows served base versions of files it changed or deleted until the
background index finished. Search now reconciles such an overlay first
(hashing only, no embedding): base rows of changed or deleted files are
hidden, and files still to embed appear once the background pass adds
them.

sync_checkout also drops overlay rows whose file changed since they were
embedded, so after any reconcile every row a worktree sees matches its
file on disk and checked_at is a valid mtime watermark.

LGREP-17
- CLI search reconciles a worktree overlay before searching it, as the
  MCP search does, so it never returns trunk versions of files the
  worktree changed or deleted.
- The staleness check reports never-indexed files even when the
  checkout sees no indexed rows, so a worktree whose every file differs
  from base schedules its index.
- Zero-chunk markers record the content hash that produced no chunks.
  A file that later gains code is indexed again, and a reconcile forgets
  markers of changed or deleted files. Markers written as a plain list
  load with an empty hash and are re-checked once.

LGREP-17
@JRedeker
JRedeker merged commit f662097 into main Sep 25, 2026
4 checks passed
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.

1 participant