Skip to content

fix(index): seed nested-worktree indexes from a sibling instead of rebuilding - #177

Open
ntotten wants to merge 2 commits into
ory:mainfrom
zuplo:nate/fix-worktree-index-seeding
Open

fix(index): seed nested-worktree indexes from a sibling instead of rebuilding#177
ntotten wants to merge 2 commits into
ory:mainfrom
zuplo:nate/fix-worktree-index-seeding

Conversation

@ntotten

@ntotten ntotten commented Jul 20, 2026

Copy link
Copy Markdown

Problem

Working in a fresh git worktree of an already-indexed repo re-embedded every file from scratch (minutes on a local embedder) instead of reusing a sibling worktree's embeddings. Two independent bugs defeated the existing donor-seeding path, and both bite Claude Code's default repo/.claude/worktrees/<name> layout, where worktrees are nested inside the repo.

Fixes

Bug 1 — donor discovery picked the wrong worktree (internal/config/seed.go)

FindDonorIndexBase selected the first git worktree list entry containing the project. git lists the main checkout first, so for a worktree nested inside the repo it identified the main checkout as "self", searched for donors at nonexistent <sibling>/.claude/worktrees/<name> paths, and skipped the one real donor — the parent repo's index. Fix: pick the deepest (most specific) containing worktree. That's the longest matching path, since every match is an ancestor of the project and they form a prefix chain.

Bug 2 — the CLI indexer never seeded (cmd/index.go, new cmd/seed.go)

Seeding only ran in the MCP search handler, but the SessionStart hook spawns lumen index, which created the DB first; SeedFromDonor then no-ops because the DB already exists, so the hook permanently won the race and forced a full rebuild. Fix: seed from a donor in runIndexer, under the index lock, before the DB is created.

Hardening — safe concurrent seeding (internal/index/seed.go)

Because both the CLI indexer and the MCP handler can now seed the same fresh worktree concurrently, SeedFromDonor copies to a unique temp file (os.CreateTemp) and publishes it via a create-if-absent hard link (os.Link fails with EEXIST). The loser of the race no-ops instead of renaming a fresh copy over a database the winner has already opened for writing.

Tests

Adds unit tests for the nested-worktree layout, concurrent seeding, and the runIndexer seed helper. go test and go vet pass for the config, index, and cmd packages.

Relationship to #170

#170 also makes the CLI indexer seed under the lock (Bug 2), by adding lock coordination to getOrCreate in the MCP handler. This PR addresses Bug 2 with a different approach — hardening SeedFromDonor for concurrent callers rather than refactoring the handler's locking — and additionally fixes donor discovery for nested worktrees (Bug 1), which #170 does not touch. Happy to rebase, split, or defer to #170 for the overlapping part if that's easier to land.

Summary by CodeRabbit

  • New Features

    • Indexes can now be seeded from an existing sibling worktree to reduce full rebuilds.
    • Seeding is best-effort: if prerequisites aren’t met or seeding fails, indexing continues with a full rebuild.
    • Nested worktrees now choose the most specific applicable donor index.
    • Concurrent seeding is handled safely so only one creator effectively wins without overwriting.
  • Bug Fixes

    • Fixed donor selection for nested worktree scenarios.
    • Improved reliability when multiple indexing operations run at the same time.
  • Tests

    • Added coverage for donor seeding logic, nested worktree selection, and concurrent seeding.

…-embedding

Working in a fresh git worktree of an already-indexed repo re-embedded every
file from scratch (~minutes on a local embedder) instead of reusing a sibling
worktree's embeddings. Two independent bugs defeated the existing donor-seeding
path, and both bite Claude Code's default repo/.claude/worktrees/<name> layout.

Bug 1 — donor discovery picked the wrong worktree (internal/config/seed.go).
FindDonorIndexBase selected the FIRST `git worktree list` entry containing the
project. git lists the main checkout first, so for a worktree nested inside the
repo it identified the main checkout as "self", searched for donors at
nonexistent <sibling>/.claude/worktrees/<name> paths, and skipped the one real
donor (the parent repo's index). Fix: pick the deepest (most specific)
containing worktree — the longest matching path, since every match is an
ancestor of the project and they form a prefix chain.

Bug 2 — the CLI indexer never seeded (cmd/index.go, cmd/seed.go). Seeding only
ran in the MCP search handler, but the SessionStart hook spawns `lumen index`,
which created the DB first; SeedFromDonor then no-ops because the DB exists, so
the hook permanently won the race and forced a full rebuild. Fix: seed from a
donor in runIndexer, under the index lock, before the DB is created.

Because both the CLI indexer and the MCP handler can now seed the same fresh
worktree concurrently, harden SeedFromDonor (internal/index/seed.go) to copy to
a unique temp file (os.CreateTemp) and publish via a create-if-absent hard link
(os.Link fails on EEXIST). The loser of the race no-ops instead of renaming a
fresh copy over a database the winner already opened for writing.

Adds unit tests for the nested-worktree layout, concurrent seeding, and the
runIndexer seed helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Jul 20, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bf7025ab-fe5e-45ab-95e5-01e55dcde317

📥 Commits

Reviewing files that changed from the base of the PR and between 269f2cb and 6105ffb.

📒 Files selected for processing (2)
  • cmd/seed_test.go
  • internal/index/seed.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/index/seed.go

📝 Walkthrough

Walkthrough

Indexer startup now seeds new project databases from sibling worktree indexes. Donor selection handles nested worktrees, and database publication uses temporary copies with create-if-absent hard links for concurrent seeders. Tests cover discovery, fallback, nested paths, and concurrent publication.

Changes

Index seeding

Layer / File(s) Summary
Nested worktree donor selection
internal/config/seed.go, internal/config/seed_test.go
Donor discovery selects the deepest containing worktree, with coverage for nested worktree paths.
Concurrent-safe donor publication
internal/index/seed.go, internal/index/seed_test.go
Seeding copies the donor database to a unique temporary file and publishes it with a create-if-absent hard link; concurrent callers and temporary-file cleanup are tested.
Indexer startup integration and helper validation
cmd/index.go, cmd/seed.go, cmd/seed_test.go
Indexer startup invokes best-effort seeding under the project lock, with tests for existing databases, missing donors, successful seeding, and swallowed errors.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant runIndexer
  participant seedFromDonorIfNew
  participant FindDonorIndexBase
  participant SeedFromDonor
  participant destinationDB
  runIndexer->>seedFromDonorIfNew: seed new database
  seedFromDonorIfNew->>FindDonorIndexBase: find donor worktree index
  FindDonorIndexBase-->>seedFromDonorIfNew: donor path
  seedFromDonorIfNew->>SeedFromDonor: copy donor to destination
  SeedFromDonor->>destinationDB: publish with create-if-absent hard link
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: nested worktree indexes are seeded from a sibling instead of being rebuilt.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/config/seed_test.go (1)

68-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider a table-driven test for multiple layout cases.

As per coding guidelines, use table-driven tests for multiple test cases in Go. Consider refactoring this new nested worktree test, along with the adjacent TestFindDonorIndex_WithSibling and TestFindDonorIndex_WrongModel, into a single table-driven test to cleanly group related worktree discovery scenarios.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config/seed_test.go` around lines 68 - 105, Refactor
TestFindDonorIndex_NestedWorktree, TestFindDonorIndex_WithSibling, and
TestFindDonorIndex_WrongModel into one table-driven test covering each worktree
discovery scenario. Define per-case setup and expected donor results, reuse
shared Git and database setup where possible, and preserve each test’s existing
assertions and behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/seed_test.go`:
- Around line 39-93: Consolidate the four seedFromDonorIfNew test functions into
one table-driven test with cases for an existing database, successful seeding,
no donor, and seed error. Keep each case’s stub behavior and assertions intact,
using per-case fields or callbacks to express expected calls and outcomes; run
cases with t.Run and preserve the existing temporary database setup and
discardLogger usage.

In `@internal/index/seed.go`:
- Around line 76-86: Update the temporary-file handling in the seed flow around
tmpFile and copyFile: defer tmpFile.Close(), change the copy operation to use
the already-open file descriptor instead of reopening tmp by path, and handle
any Close error before linking the completed seed. Preserve cleanup of the
temporary path and existing error wrapping.

---

Nitpick comments:
In `@internal/config/seed_test.go`:
- Around line 68-105: Refactor TestFindDonorIndex_NestedWorktree,
TestFindDonorIndex_WithSibling, and TestFindDonorIndex_WrongModel into one
table-driven test covering each worktree discovery scenario. Define per-case
setup and expected donor results, reuse shared Git and database setup where
possible, and preserve each test’s existing assertions and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 59f20e4e-cc9b-451f-a686-cf8cdbac5644

📥 Commits

Reviewing files that changed from the base of the PR and between d0dee0e and 269f2cb.

📒 Files selected for processing (7)
  • cmd/index.go
  • cmd/seed.go
  • cmd/seed_test.go
  • internal/config/seed.go
  • internal/config/seed_test.go
  • internal/index/seed.go
  • internal/index/seed_test.go

Comment thread cmd/seed_test.go Outdated
Comment thread internal/index/seed.go
@ntotten ntotten changed the title fix(index): seed nested-worktree indexes from a sibling instead of reindex fix(index): seed nested-worktree indexes from a sibling instead of rebuilding Jul 20, 2026
- SeedFromDonor: copy into the open temp descriptor instead of closing and
  re-opening it by name (avoids a Windows sharing violation), defer the
  descriptor's close, and check the Close error before publishing via os.Link
  so a short write can't be linked into place. Removes the now-unused copyFile.
- cmd/seed_test.go: consolidate the four seedFromDonorIfNew cases into a single
  table-driven test, per the repo's Go testing guideline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ntotten

ntotten commented Jul 20, 2026

Copy link
Copy Markdown
Author

CI status

Only the E2E job is red; Build (macOS + Ubuntu), Lint, Test, Script tests (all three OSes), CLA, and CodeRabbit are green.

The E2E failures are all in the TestLang_* suite (JSON/Dart/Ruby/Go/PHP/Rust), each with a single subtest running ~400–500s before a cascade of fast failures — the signature of the embedding-backend timing out under CI rather than an assertion regression. This PR only touches git-worktree donor seeding (internal/config/seed.go, internal/index/seed.go, cmd/index.go, cmd/seed.go); it doesn't touch the chunker or search ranking that TestLang_* exercises.

The same suite fails on the most recent main run (run 27392108271) — with TestLang_Ruby, TestLang_Go, and TestLang_Rust failing there too — so this looks pre-existing/environmental, not introduced here. go test/go vet for the config, index, and cmd packages pass locally.

Happy to dig in if you think it's related, but I don't believe this change can affect the language suite.

@aeneasr

aeneasr commented Jul 27, 2026

Copy link
Copy Markdown
Member

E2E failing indicates an issue in the PR of this code, since it's passing on master

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.

3 participants