Skip to content

Commit e89902d

Browse files
committed
Fix semble creating multiple caches
1 parent 62845a1 commit e89902d

2 files changed

Lines changed: 114 additions & 28 deletions

File tree

src/services/code-index/semble/__tests__/provider.spec.ts

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -292,38 +292,108 @@ describe("SembleProvider", () => {
292292
expect(results[0].payload?.filePath).toBe("/workspace/src/good.ts")
293293
})
294294

295-
it("should use directoryPrefix when provided", async () => {
295+
it("should always search workspace root regardless of directoryPrefix", async () => {
296296
mockCli.search.mockResolvedValue([])
297297

298298
await provider.searchIndex("test", "/custom/path")
299299

300-
expect(mockCli.search).toHaveBeenCalledWith("test", "/custom/path", {
300+
// Should always pass workspace root to semble, not the directoryPrefix
301+
expect(mockCli.search).toHaveBeenCalledWith("test", "/workspace", {
301302
topK: SEMBLE_DEFAULTS.DEFAULT_TOP_K,
302303
content: SEMBLE_DEFAULTS.DEFAULT_CONTENT,
303304
})
304305
})
305306

306-
it("should resolve relative directoryPrefix against workspace path", async () => {
307+
it("should always search workspace root with relative directoryPrefix", async () => {
307308
mockCli.search.mockResolvedValue([])
308309

309310
await provider.searchIndex("test", "src/subdir")
310311

311-
const expectedPath = path.join("/workspace", "src/subdir")
312-
expect(mockCli.search).toHaveBeenCalledWith("test", expectedPath, {
312+
// Should always pass workspace root to semble
313+
expect(mockCli.search).toHaveBeenCalledWith("test", "/workspace", {
313314
topK: SEMBLE_DEFAULTS.DEFAULT_TOP_K,
314315
content: SEMBLE_DEFAULTS.DEFAULT_CONTENT,
315316
})
316317
})
317318

318-
it("should use absolute directoryPrefix as-is", async () => {
319-
mockCli.search.mockResolvedValue([])
319+
it("should filter results by directoryPrefix when provided", async () => {
320+
const mockResults = [
321+
{
322+
chunk: {
323+
content: "code in src/auth",
324+
file_path: "src/auth/login.ts",
325+
start_line: 1,
326+
end_line: 10,
327+
language: "typescript",
328+
location: "src/auth/login.ts:1-10",
329+
},
330+
score: 0.95,
331+
},
332+
{
333+
chunk: {
334+
content: "code in src/utils",
335+
file_path: "src/utils/helper.ts",
336+
start_line: 5,
337+
end_line: 15,
338+
language: "typescript",
339+
location: "src/utils/helper.ts:5-15",
340+
},
341+
score: 0.8,
342+
},
343+
{
344+
chunk: {
345+
content: "code in root",
346+
file_path: "README.md",
347+
start_line: 1,
348+
end_line: 5,
349+
language: "markdown",
350+
location: "README.md:1-5",
351+
},
352+
score: 0.6,
353+
},
354+
]
320355

321-
await provider.searchIndex("test", "/absolute/custom/path")
356+
mockCli.search.mockResolvedValue(mockResults)
322357

323-
expect(mockCli.search).toHaveBeenCalledWith("test", "/absolute/custom/path", {
324-
topK: SEMBLE_DEFAULTS.DEFAULT_TOP_K,
325-
content: SEMBLE_DEFAULTS.DEFAULT_CONTENT,
326-
})
358+
const results = await provider.searchIndex("test", "src/auth")
359+
360+
// Only the src/auth result should pass the filter
361+
expect(results).toHaveLength(1)
362+
expect(results[0].payload?.filePath).toBe("/workspace/src/auth/login.ts")
363+
})
364+
365+
it("should not filter results when no directoryPrefix is provided", async () => {
366+
const mockResults = [
367+
{
368+
chunk: {
369+
content: "code in src/auth",
370+
file_path: "src/auth/login.ts",
371+
start_line: 1,
372+
end_line: 10,
373+
language: "typescript",
374+
location: "src/auth/login.ts:1-10",
375+
},
376+
score: 0.95,
377+
},
378+
{
379+
chunk: {
380+
content: "code in src/utils",
381+
file_path: "src/utils/helper.ts",
382+
start_line: 5,
383+
end_line: 15,
384+
language: "typescript",
385+
location: "src/utils/helper.ts:5-15",
386+
},
387+
score: 0.8,
388+
},
389+
]
390+
391+
mockCli.search.mockResolvedValue(mockResults)
392+
393+
const results = await provider.searchIndex("test")
394+
395+
// All results should be returned
396+
expect(results).toHaveLength(2)
327397
})
328398

329399
it("should return empty array on search error and log telemetry", async () => {
@@ -458,12 +528,12 @@ describe("SembleProvider", () => {
458528
expect(results[0].payload?.filePath).toContain("/")
459529
})
460530

461-
it("should join file paths against the searchPath when directoryPrefix is provided", async () => {
531+
it("should always join file paths against workspace root, even with directoryPrefix", async () => {
462532
const mockResults = [
463533
{
464534
chunk: {
465535
content: "code",
466-
file_path: "file.ts",
536+
file_path: "src/file.ts",
467537
start_line: 1,
468538
end_line: 5,
469539
language: "typescript",
@@ -475,9 +545,10 @@ describe("SembleProvider", () => {
475545

476546
mockCli.search.mockResolvedValue(mockResults)
477547

478-
const results = await provider.searchIndex("test", "/custom/path")
548+
// Even with a directoryPrefix, file paths are joined against workspace root
549+
const results = await provider.searchIndex("test", "src")
479550

480-
expect(results[0].payload?.filePath).toBe("/custom/path/file.ts")
551+
expect(results[0].payload?.filePath).toBe("/workspace/src/file.ts")
481552
})
482553

483554
it("should assign sequential semble-N IDs to results", async () => {

src/services/code-index/semble/provider.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ export class SembleProvider implements ISembleProvider {
133133

134134
/**
135135
* Searches the codebase using `semble search`.
136+
*
137+
* Always searches the full workspace root to avoid creating separate
138+
* Semble cache directories for each subdirectory. When directoryPrefix
139+
* is provided, results are filtered post-search to only include files
140+
* within that directory.
136141
*/
137142
async searchIndex(query: string, directoryPrefix?: string): Promise<VectorStoreSearchResult[]> {
138143
if (!this._isInitialized) {
@@ -145,22 +150,32 @@ export class SembleProvider implements ISembleProvider {
145150
}
146151

147152
try {
148-
let searchPath = this.workspacePath
149-
if (directoryPrefix) {
150-
// Resolve relative paths against the workspace root
151-
searchPath = path.isAbsolute(directoryPrefix)
152-
? directoryPrefix
153-
: path.join(this.workspacePath, directoryPrefix)
154-
}
155-
console.log(`[SembleProvider] Searching for "${query}" in ${searchPath}`)
156-
const results = await this.cli.search(query, searchPath, {
153+
// Always search the full workspace to maintain a single Semble cache.
154+
// Semble creates a separate cache directory per path (SHA-256 of the
155+
// resolved absolute path), so passing subdirectories would create
156+
// redundant indexes and waste disk space.
157+
console.log(`[SembleProvider] Searching for "${query}" in ${this.workspacePath}`)
158+
const results = await this.cli.search(query, this.workspacePath, {
157159
topK: this.config.topK,
158160
content: this.config.content,
159161
})
160162

161-
// Semble returns file paths relative to the search path.
162-
// We join against searchPath (not workspacePath) to get correct absolute paths.
163-
const converted = this._convertResults(results, searchPath)
163+
// Semble returns file paths relative to the search path (workspace root).
164+
// We join against workspacePath to produce correct absolute paths.
165+
let converted = this._convertResults(results, this.workspacePath)
166+
167+
// Filter results to the requested directory prefix, if any.
168+
if (directoryPrefix) {
169+
const normalizedPrefix = path.resolve(this.workspacePath, directoryPrefix).replace(/\\/g, "/")
170+
converted = converted.filter((r) => {
171+
const filePath = (r.payload?.filePath ?? "").replace(/\\/g, "/")
172+
return filePath.startsWith(normalizedPrefix + "/") || filePath === normalizedPrefix
173+
})
174+
console.log(
175+
`[SembleProvider] Filtered to "${directoryPrefix}": ${converted.length} of ${results.length} results`,
176+
)
177+
}
178+
164179
console.log(
165180
`[SembleProvider] Search returned ${converted.length} results (raw: ${results.length}). Sample path: ${converted[0]?.payload?.filePath ?? "none"}`,
166181
)

0 commit comments

Comments
 (0)