Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/ancestor.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,8 @@ func findAncestorIndex(path, model string) string {
}
return ""
}

func hasLumenBoundaryFile(path string) bool {
info, err := os.Stat(filepath.Join(path, ".lumenignore"))
return err == nil && !info.IsDir()
}
53 changes: 34 additions & 19 deletions cmd/ancestor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestFindAncestorIndex(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("XDG_DATA_HOME", tmpDir)

got := findAncestorIndex("/some/deep/nonexistent/path", model)
got := findAncestorIndex(filepath.Join(t.TempDir(), "some", "deep", "path"), model)
if got != "" {
t.Fatalf("expected empty string, got %q", got)
}
Expand All @@ -39,37 +39,47 @@ func TestFindAncestorIndex(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("XDG_DATA_HOME", tmpDir)

// Create a fake DB for /project.
parentDBPath := config.DBPathForProject("/project", model)
project := filepath.Join(t.TempDir(), "project")
child := filepath.Join(project, "scripts", "util")
if err := os.MkdirAll(child, 0o755); err != nil {
t.Fatal(err)
}

parentDBPath := config.DBPathForProject(project, model)
if err := os.MkdirAll(filepath.Dir(parentDBPath), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(parentDBPath, []byte{}, 0o644); err != nil {
t.Fatal(err)
}

got := findAncestorIndex("/project/scripts/util", model)
if got != "/project" {
t.Fatalf("expected /project, got %q", got)
got := findAncestorIndex(child, model)
if got != project {
t.Fatalf("expected %q, got %q", project, got)
}
})

t.Run("skips ancestor when path crosses a SkipDir", func(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("XDG_DATA_HOME", tmpDir)

// Create a fake DB for /project.
parentDBPath := config.DBPathForProject("/project", model)
project := filepath.Join(t.TempDir(), "project")
child := filepath.Join(project, "testdata", "fixtures", "go")
if err := os.MkdirAll(child, 0o755); err != nil {
t.Fatal(err)
}

parentDBPath := config.DBPathForProject(project, model)
if err := os.MkdirAll(filepath.Dir(parentDBPath), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(parentDBPath, []byte{}, 0o644); err != nil {
t.Fatal(err)
}

// "testdata" is in merkle.SkipDirsthe parent index would never
// contain these files, so findAncestorIndex must return "".
got := findAncestorIndex("/project/testdata/fixtures/go", model)
// "testdata" is in merkle.SkipDirs; the parent index would never contain
// these files, so findAncestorIndex must return "".
got := findAncestorIndex(child, model)
if got != "" {
t.Fatalf("expected empty string (skip dir in route), got %q", got)
}
Expand All @@ -79,8 +89,8 @@ func TestFindAncestorIndex(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("XDG_DATA_HOME", tmpDir)

// No DBs exist anywhere should return "" without panic.
got := findAncestorIndex("/a/b/c/d/e", model)
// No DBs exist anywhere; should return "" without panic.
got := findAncestorIndex(filepath.Join(t.TempDir(), "a", "b", "c", "d", "e"), model)
if got != "" {
t.Fatalf("expected empty string, got %q", got)
}
Expand Down Expand Up @@ -119,8 +129,14 @@ func TestFindAncestorIndex(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("XDG_DATA_HOME", tmpDir)

// Create fake DBs for both /project and /project/src.
for _, dir := range []string{"/project", "/project/src"} {
project := filepath.Join(t.TempDir(), "project")
src := filepath.Join(project, "src")
pkg := filepath.Join(src, "pkg")
if err := os.MkdirAll(pkg, 0o755); err != nil {
t.Fatal(err)
}

for _, dir := range []string{project, src} {
dbPath := config.DBPathForProject(dir, model)
if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
t.Fatal(err)
Expand All @@ -130,10 +146,9 @@ func TestFindAncestorIndex(t *testing.T) {
}
}

// Searching from /project/src/pkg should find /project/src (nearest).
got := findAncestorIndex("/project/src/pkg", model)
if got != "/project/src" {
t.Fatalf("expected /project/src (nearest ancestor), got %q", got)
got := findAncestorIndex(pkg, model)
if got != src {
t.Fatalf("expected nearest ancestor %q, got %q", src, got)
}
})
}
46 changes: 37 additions & 9 deletions cmd/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (

"github.com/ory/lumen/internal/config"
"github.com/ory/lumen/internal/git"
"github.com/ory/lumen/internal/merkle"
"github.com/ory/lumen/internal/store"
)

Expand Down Expand Up @@ -132,18 +133,43 @@ func generateSessionContextInternalWithDirective(directive, cwd string, findDono
emb := newEmbedder(cfg)
modelName := emb.ModelName()
dims := cfg.ServerDims(0)
allowBackgroundIndex := false

// Normalize cwd to the git repository root so the DB path matches what
// `lumen index` and the MCP handler use. For non-git directories, walk
// up to reuse an existing ancestor index.
// `lumen index` and the MCP handler use. For non-git directories, reuse an
// existing ancestor index, but do not invent a new background crawl at a
// plain parent/root just because a host opened a session there.
if root, err := git.RepoRoot(cwd); err == nil {
cwd = root
} else if ancestor := findAncestorIndex(cwd, modelName); ancestor != "" {
cwd = ancestor
if unindexable, _ := merkle.IsRootUnindexable(root); !unindexable {
cwd = root
allowBackgroundIndex = true
} else if !hasLumenBoundaryFile(cwd) {
if ancestor := findAncestorIndex(cwd, modelName); ancestor != "" {
cwd = ancestor
allowBackgroundIndex = true
}
}
} else if !hasLumenBoundaryFile(cwd) {
if ancestor := findAncestorIndex(cwd, modelName); ancestor != "" {
cwd = ancestor
allowBackgroundIndex = true
}
} else {
allowBackgroundIndex = true
}
if hasLumenBoundaryFile(cwd) {
allowBackgroundIndex = true
}
Comment on lines 142 to +162

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

.lumenignore boundaries are still bypassed on the git-root path in cmd/hook.go and cmd/resolve.go. generateSessionContextInternalWithDirective in cmd/hook.go and resolveIndexRoot in cmd/resolve.go only consult hasLumenBoundaryFile(...) on fallback branches. When the current directory is a bounded subdirectory inside a normal git repo, both functions still collapse to the repo root, so SessionStart can prewarm the wrong index and search can open the wrong DB. Please short-circuit on the boundary before any git-root adoption in both files, and add matching regression cases in cmd/hook_test.go and cmd/resolve_test.go for “git repo exists + boundary in subdir”.

🤖 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 `@cmd/hook.go` around lines 142 - 162, The current logic in
generateSessionContextInternalWithDirective (cmd/hook.go) and resolveIndexRoot
(cmd/resolve.go) checks git.RepoRoot and may adopt the repo root before honoring
.lumenignore boundaries, causing background indexing and DB opens at the wrong
root; update both functions to call hasLumenBoundaryFile(cwd) first and
short-circuit (keep cwd and not adopt git.RepoRoot) when a boundary exists, only
falling back to git.RepoRoot/findAncestorIndex if no boundary is present,
preserving allowBackgroundIndex semantics; after changing the control flow
around hasLumenBoundaryFile, add regression tests in cmd/hook_test.go and
cmd/resolve_test.go for the case "git repo exists + boundary in subdir" to
assert cwd remains the subdir and SessionStart/prewarm and resolveIndexRoot use
the bounded path.

if unindexable, reason := merkle.IsRootUnindexable(cwd); unindexable {
return directive + " Index root blocked: " + reason + "."
}

dbPath := config.DBPathForProject(cwd, modelName)
if _, err := os.Stat(dbPath); err != nil {
if !allowBackgroundIndex {
return directive + " No index yet — auto-created on first semantic_search call."
}

// No index yet — kick off background pre-warming so the first search
// in this session doesn't pay the full seed + embed cost synchronously.
bgIndexer(cwd)
Expand All @@ -162,10 +188,12 @@ func generateSessionContextInternalWithDirective(directive, cwd string, findDono
// Spawn background indexer if the index is stale or has never been
// successfully completed. This avoids spawning on every session start
// when the index was recently updated.
if val, metaErr := s.GetMeta("last_indexed_at"); metaErr != nil || val == "" {
bgIndexer(cwd)
} else if t, parseErr := time.Parse(time.RFC3339, val); parseErr != nil || time.Since(t) > backgroundIndexStaleness {
bgIndexer(cwd)
if allowBackgroundIndex {
if val, metaErr := s.GetMeta("last_indexed_at"); metaErr != nil || val == "" {
bgIndexer(cwd)
} else if t, parseErr := time.Parse(time.RFC3339, val); parseErr != nil || time.Since(t) > backgroundIndexStaleness {
bgIndexer(cwd)
}
}

stats, err := s.Stats()
Expand Down
Loading
Loading