-
Notifications
You must be signed in to change notification settings - Fork 28
fix(index): safely seed nested worktree indexes #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
7131d88
fix(index): seed nested-worktree indexes from a sibling instead of re…
ntotten 5d882b3
test,refactor: address CodeRabbit review feedback
ntotten 74bbe0b
fix(index): harden worktree index seeding
aeneasr 6e78fc0
Merge remote-tracking branch 'origin/main' into aeneasr/review-pr-177
aeneasr 1ec5bc8
test(index): adapt seed metadata assertion
aeneasr 70aa3dc
fix(index): snapshot donor safely during seeding
aeneasr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| // Copyright 2026 Aeneas Rekkas | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log/slog" | ||
| "os" | ||
|
|
||
| "github.com/ory/lumen/internal/config" | ||
| "github.com/ory/lumen/internal/index" | ||
| ) | ||
|
|
||
| type seedOptions struct { | ||
| findDonor func(string, string) string | ||
| seed func(context.Context, string, string, string) (bool, error) | ||
| status func(string) | ||
| } | ||
|
|
||
| // seedFromDonorIfNew seeds dbPath from a sibling worktree's index when dbPath | ||
| // does not yet exist, so a fresh git worktree reuses an already-indexed | ||
| // worktree's embeddings instead of re-embedding every file from scratch. | ||
| // | ||
| // Callers may additionally hold the index lock for dbPath, but SeedFromDonor | ||
| // has its own advisory lock so CLI and MCP callers cannot duplicate the copy. | ||
| // Seeding is best-effort — any failure is logged and indexing continues with a | ||
| // from-scratch build. The returned warning is suitable for surfacing to MCP | ||
| // clients. When dbPath already exists it is a single stat on the hot path. | ||
| func seedFromDonorIfNew(ctx context.Context, dbPath, projectPath, model string, logger *slog.Logger, opts seedOptions) string { | ||
| if _, err := os.Stat(dbPath); !os.IsNotExist(err) { | ||
| // Exists already, or stat failed for some other reason — nothing to do. | ||
| return "" | ||
| } | ||
|
|
||
| findDonor := opts.findDonor | ||
| if findDonor == nil { | ||
| findDonor = config.FindDonorIndex | ||
| } | ||
| donorPath := findDonor(projectPath, model) | ||
| if donorPath == "" { | ||
| return "" | ||
| } | ||
|
|
||
| logger.Info("seeding index from donor worktree", | ||
| "project_path", projectPath, | ||
| "donor_path", donorPath, | ||
| ) | ||
| if opts.status != nil { | ||
| opts.status("Seeding index from sibling worktree...") | ||
| } | ||
|
|
||
| seed := opts.seed | ||
| if seed == nil { | ||
| seed = index.SeedFromDonorContext | ||
| } | ||
| seeded, err := seed(ctx, donorPath, dbPath, projectPath) | ||
| if err != nil { | ||
| logger.Warn("seed from donor worktree failed", | ||
| "project_path", projectPath, | ||
| "donor_path", donorPath, | ||
| "error", err, | ||
| ) | ||
| warning := fmt.Sprintf("index seeded from scratch (sibling copy failed: %v)", err) | ||
| if opts.status != nil { | ||
| opts.status(fmt.Sprintf("Sibling index copy failed: %v; indexing from scratch.", err)) | ||
| } | ||
| return warning | ||
| } | ||
| if seeded && opts.status != nil { | ||
| opts.status("Seeded index from sibling worktree.") | ||
| } else if opts.status != nil { | ||
| if _, statErr := os.Stat(dbPath); statErr == nil { | ||
| opts.status("Index was seeded by another process.") | ||
| } else { | ||
| opts.status("Sibling index could not be reused; indexing from scratch.") | ||
| } | ||
| } | ||
| return "" | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| // Copyright 2026 Aeneas Rekkas | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "io" | ||
| "log/slog" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| ) | ||
|
|
||
| func seedTestLogger() *slog.Logger { | ||
| return slog.New(slog.NewTextHandler(io.Discard, nil)) | ||
| } | ||
|
|
||
| func TestSeedFromDonorIfNew(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| setupDB bool // pre-create the destination DB | ||
| donor string | ||
| seedErr error // error returned by the seed stub | ||
| wantFind bool // donor discovery should run | ||
| wantSeed bool // seed should run | ||
| }{ | ||
| { | ||
| name: "skips when DB already exists", | ||
| setupDB: true, | ||
| donor: "/donor.db", | ||
| wantFind: false, | ||
| wantSeed: false, | ||
| }, | ||
| { | ||
| name: "seeds when DB missing and donor found", | ||
| donor: "/donor.db", | ||
| wantFind: true, | ||
| wantSeed: true, | ||
| }, | ||
| { | ||
| name: "no seed when no donor found", | ||
| donor: "", | ||
| wantFind: true, | ||
| wantSeed: false, | ||
| }, | ||
| { | ||
| name: "seed error is swallowed", | ||
| donor: "/donor.db", | ||
| seedErr: errors.New("copy failed"), | ||
| wantFind: true, | ||
| wantSeed: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| dbPath := filepath.Join(t.TempDir(), "index.db") | ||
| if tt.setupDB { | ||
| if err := os.WriteFile(dbPath, []byte("existing"), 0o644); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| } | ||
|
|
||
| var findCalled, seedCalled bool | ||
| var gotDonor, gotDst string | ||
| var statuses []string | ||
| warning := seedFromDonorIfNew( | ||
| context.Background(), | ||
| dbPath, | ||
| "/project", | ||
| "model", | ||
| seedTestLogger(), | ||
| seedOptions{ | ||
| findDonor: func(_, _ string) string { | ||
| findCalled = true | ||
| return tt.donor | ||
| }, | ||
| seed: func(_ context.Context, donor, dst, projectPath string) (bool, error) { | ||
| seedCalled = true | ||
| gotDonor, gotDst = donor, dst | ||
| if projectPath != "/project" { | ||
| t.Errorf("seed project path = %q, want /project", projectPath) | ||
| } | ||
| return tt.seedErr == nil, tt.seedErr | ||
| }, | ||
| status: func(message string) { | ||
| statuses = append(statuses, message) | ||
| }, | ||
| }, | ||
| ) | ||
|
|
||
| if findCalled != tt.wantFind { | ||
| t.Errorf("donor discovery called = %v, want %v", findCalled, tt.wantFind) | ||
| } | ||
| if seedCalled != tt.wantSeed { | ||
| t.Errorf("seed called = %v, want %v", seedCalled, tt.wantSeed) | ||
| } | ||
| if tt.wantSeed && (gotDonor != tt.donor || gotDst != dbPath) { | ||
| t.Errorf("seed called with (%q, %q), want (%q, %q)", gotDonor, gotDst, tt.donor, dbPath) | ||
| } | ||
| if tt.seedErr != nil { | ||
| if warning == "" { | ||
| t.Error("expected warning when seed fails") | ||
| } | ||
| if len(statuses) != 2 { | ||
| t.Fatalf("expected start and failure status, got %v", statuses) | ||
| } | ||
| } else if warning != "" { | ||
| t.Errorf("unexpected warning: %q", warning) | ||
| } | ||
| }) | ||
| } | ||
| } |
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.