ISS-10812: seed a worktree's code index from the main checkout's (PLN-2070 PR 1) - #32
Merged
Merged
Conversation
PLN-2070 PR 1. A worktree engine's index starts as a copy-on-write clone of main's (under main's index flock and SQLite's write locks), keeps main's repo_id through an alias row, refreshes once right away, is never fully rebuilt or vacuumed, takes Zoekt candidates from main's server, and retires when its worktree is removed or goes idle. Co-Authored-By: Claude <noreply@anthropic.com>
…ests PLN-2070 PR 1. Co-Authored-By: Claude <noreply@anthropic.com>
PLN-2070 PR 1, D5. symphony-alpha's fts.sqlite WAL held 195k frames (808 MB) that auto-checkpoint never finished; checkpointing them on the seed's path took 5.35 s. lc code index now checkpoints after its reindex so a seed finds a small WAL. Co-Authored-By: Claude <noreply@anthropic.com>
…ts directory PLN-2070 PR 1. Measured on the symphony-alpha clone: git worktree remove raced the worktree's first refresh, which wrote its store back and recreated the directory, so a directory check never saw the worktree go. Co-Authored-By: Claude <noreply@anthropic.com>
… default PLN-2070 PR 1, AC-1.6: five seeded worktree engines on a symphony-alpha clone added 190 MiB RSS (140 MiB with the first, ~13 MiB each after); the 1800 s default stands. Co-Authored-By: Claude <noreply@anthropic.com>
…an import cycle worktree_seed reached back into engine.py for a private constant through a deferred import, while engine.py imports worktree_seed at module level. Co-Authored-By: Claude <noreply@anthropic.com>
Two threads opening one unseeded worktree both seeded it: the loser's non-blocking flock failed and surfaced as IndexRebuilding, or it seeded again and swapped files under the engine the first had just handed out. Co-Authored-By: Claude <noreply@anthropic.com>
When the main checkout's index can seed it, --reindex re-seeds a linked worktree's index instead of rebuilding it. Co-Authored-By: Claude <noreply@anthropic.com>
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.
Campaign iss-10812-20260921, chunk C1 of 2: seed a worktree's index from the main checkout's.
This PR builds PLN-2070 PR 1, for ISS-10812: code search in a git worktree answers from the main checkout's index.
What this lands
A linked worktree's code index is no longer built from scratch, and no longer left as the partial index that edit-triggered reindexes create today. The first time an engine opens for a worktree, its index is seeded as a copy-on-write clone of the main checkout's index. One incremental refresh then brings it in line with the worktree:
infra/code_intel/worktree_seed.py). The clone is taken under main's index lock and SQLite's own write lock, after aPASSIVEcheckpoint, and swapped into place by atomic rename. The copy is marked with aseeded_fromstamp and arepo_id_alias:<worktree id>row, so it keeps main'srepo_idand shares its disk pages.CodeContextEngine.__init__. Nothing else in the engine computesrepo_id.lc code index --reindexin a worktree re-seeds. A seeded index is never vacuumed.index_state: rebuildingwith a seeding note, and the next call seeds..gitfile is gone, or aftercode_context.worktree_engine_idle_s(default 1800 s) without a request. Retiring stops its autosync and frees its cached scoped capabilities. Main-checkout engines are never retired..gitis a file.lc code indexcheckpoints the index WALs after each reindex, so a seed usually finds a small WAL.This PR changes no routing. Sessions still default to the main checkout until C2. Calls that already pass
repo_root=<worktree>(175 in the transcripts ISS-10812 cites) now get a complete index.Acceptance ledger
8 criteria: 7 met, 1 met differently, 0 not met.
ensure_seeded→seed_worktree_index; test plus counterfactual_seed_reasoncovers missing, stale and partial; the engine refuses the rebuild and skips VACUUMgit worktree removeretired the engine on the next sweeprepo_id,engine_stateand Zoekt root unchanged; main is never tracked for retirementMeasurements (a local copy of symphony-alpha; the real repo was never touched)
Time: a seed takes 0.05–0.14 s to clone and 0.002–0.008 s to checkpoint. Today's live WALs would take 5.55 s to checkpoint (fts: 808 MB, 195,566 frames), which is why
lc code indexnow checkpoints after each reindex.Disk, as the
df -kdelta on a dedicated APFS volume, cumulative KiB:Most of it comes from the 20 edits, through the engine's normal index writes.
Memory (RSS):
Deviations from the plan
code_context.sqliteandintel.sqliteare written at query time without the index flock. The plan's fallback, SQLite's backup API, would write a full private 5.8 GB copy, so the seed instead holds SQLite's write lock (BEGIN IMMEDIATE) on each main database while cloning. That gives the same guarantee that no commit is in flight, and it keeps the clone copy-on-write.-walis cloned only when main's checkpoint could not fold it in, and it is folded into the staged copy before the swap. Each database is replaced by one atomic rename. The clone's retrieval cache is emptied, and itsindex_versionis bumped past the index it replaces..gitfile, so a daemon launched inside a worktree seeds too..gitfile, not its directory. Measured:git worktree removeraced the first-refresh subprocess, which recreated the directory._retire_code_enginealso drops cached scoped capabilities that hold the retired engine. Otherwise retiring frees no memory.lc code index, because it measured 5.55 s against the plan's 5 s threshold.engine.pyis unchanged for this.dfmoved 548 MB during a seed that added 1.3 MB.Review
The deep review (5 reviewers, run cr-19457, at
d78c751c) was NEEDS_ATTENTION, with 5 verified findings: 1 HIGH and 4 MEDIUM. It proved it read this worktree at that commit.Fixed:
worktree_seed.pyandengine.py: the indexer semantics version moved intoinfra/code_intel/store.py(52a97bac).lc-debtwith a per-worktree upgrade path, and has a regression test (526da0f2).--reindexhelp and docstring now say what it does in a linked worktree (768c583e).Declined, with the reasons verbatim:
_flockduplicates the engine's index-write lock: "Both acquirers take flock(LOCK_EX) on the same .indexlock file, and that is the whole protocol: flock excludes one holder from the other whatever their poll loops look like, so the two cannot diverge on who holds the lock. What differs is each caller's wait policy, and that is deliberate. A seed runs on a code tool's request path and must give up after about 2 s so the caller gets IndexRebuilding and retries; an indexer waits LEMONCROW_INDEX_LOCK_TIMEOUT_S (10 s by default) and logs while it waits. The citedstealhandling is a documented no-op ("retained for API compatibility but never bypasses a live holder"), so there is no engine-side behaviour the seed is missing. A shared helper would mean rewriting CodeContextEngine._index_write_lock, the lock every indexer in every process takes, to save about 20 lines. Not worth that risk now.".git-file parsers: "The duplication is real, but the validation differences are deliberate because the copies answer different questions. main_root_of has to reject submodules and bare repositories because it needs a main checkout to seed from. zoekt/server.py's _git_dirs has to accept them because it only reads the checkout's own HEAD: a submodule's gitdir holds its HEAD and has no commondir file, which is exactly the fallback _git_dirs takes. Unifying the parsers on main_root_of's validation, as recommended, would stop Zoekt reading HEAD in a submodule."Re-review: the gate fired because 4 production files changed. The re-review (cr-31110,
--since-last-review, at768c583e) was APPROVED with 0 findings and proved it read that commit.Noted, not changed:
linked_worktree_ofwas moved unchanged frommcp_server.py. It resolves a relativegitdir:(fromgit worktree add --relative-paths) against the process cwd, so it returns None and fails safe to the workspace root. That behavior predates this PR.Verification
tests/infra/code_intel: 399 passed. This includes the uneditedtest_reads_during_reindex.pyand 13 new seed tests.tests/core/test_code_context.py: 98 passed.tests/gateway/cli/test_code_client.py: 5 passed.What C2 still owes
PLN-2070 PR 2 routes each session's calls to its own worktree:
cwd;repo_root→pathsinside a worktree → the recorded sessioncwd→ per-sessionbashcwd → workspace root;Until C2 lands, sessions still search the main checkout by default.