diff --git a/packages/pi-fff/src/index.ts b/packages/pi-fff/src/index.ts index 608d09fda..637a484e3 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -259,6 +259,13 @@ function buildAtCompletionValue(path: string): string { return path.includes(" ") ? `@"${path}"` : `@${path}`; } +// heed refuses to reopen an LMDB env already open in the process, surfaced +// through FileFinder.create() as this error string. Matched to fall back to a +// transient (no-DB) finder for in-process subagents — see #760. +function isEnvAlreadyOpenError(error: string): boolean { + return /environment already open/i.test(error); +} + function createFffMentionProvider( getItems: (query: string, signal: AbortSignal) => Promise, ): AutocompleteProvider { @@ -396,7 +403,7 @@ export default function fffExtension(pi: ExtensionAPI) { } const { FileFinder } = await loadSdk(); - const result = FileFinder.create({ + let result = FileFinder.create({ basePath: cwd, frecencyDbPath, historyDbPath, @@ -405,6 +412,21 @@ export default function fffExtension(pi: ExtensionAPI) { enableFsRootScanning, }); + // In-process subagents (createAgentSession) load this extension a second + // time in the same process. LMDB opens an env at most once per path per + // process, so the shared frecency/history DBs are already owned by the + // parent session's finder — reopening them fails. Fall back to a + // transient finder without persistent scoring, exactly like aux finders + // do (#700). See #760. + if (!result.ok && isEnvAlreadyOpenError(result.error)) { + result = FileFinder.create({ + basePath: cwd, + aiMode: true, + enableHomeDirScanning, + enableFsRootScanning, + }); + } + if (!result.ok) throw new Error(`Failed to create FFF file finder: ${result.error}`); diff --git a/packages/pi-fff/test/extension.test.ts b/packages/pi-fff/test/extension.test.ts index eb977b9f9..7403bf921 100644 --- a/packages/pi-fff/test/extension.test.ts +++ b/packages/pi-fff/test/extension.test.ts @@ -49,10 +49,16 @@ function createMockFinder(): MockFinder { }; } +let createImpl: ((options: unknown) => unknown) | undefined; + const finderModule = { FileFinder: { create: mock((options: unknown) => { createCalls.push(options); + if (createImpl) { + const override = createImpl(options); + if (override !== undefined) return override; + } const finder = createMockFinder(); finders.push(finder); return { ok: true, value: finder }; @@ -163,6 +169,7 @@ beforeEach(() => { createCalls.length = 0; finders = []; mixedSearchImpl = undefined; + createImpl = undefined; scanProgressImpl = undefined; delete process.env.PI_FFF_MODE; delete process.env.FFF_ENABLE_HOME_SCAN; @@ -256,6 +263,43 @@ describe("pi-fff autocomplete registration", () => { expect(opts.enableHomeDirScanning).toBe(false); }); + // In-process subagents (createAgentSession) reopen the shared LMDB env, + // which fails. The main finder must fall back to a transient no-DB finder + // instead of throwing during session_start — see #760. + test("falls back to a transient finder when the LMDB env is already open", async () => { + process.env.FFF_FRECENCY_DB = "/tmp/fff-test/frecency"; + process.env.FFF_HISTORY_DB = "/tmp/fff-test/history"; + let firstCall = true; + createImpl = () => { + if (firstCall) { + firstCall = false; + return { + ok: false, + error: + "Failed to init frecency db: Failed to open frecency database env: environment already open in this program; close it to be able to open it again with different options", + }; + } + return undefined; + }; + + const { ctx } = await start(); + + expect(ctx.ui.notify).not.toHaveBeenCalledWith( + expect.stringContaining("FFF init failed"), + "error", + ); + expect(createCalls).toHaveLength(2); + const retry = createCalls[1] as { + frecencyDbPath?: string; + historyDbPath?: string; + }; + expect(retry.frecencyDbPath).toBeUndefined(); + expect(retry.historyDbPath).toBeUndefined(); + + delete process.env.FFF_FRECENCY_DB; + delete process.env.FFF_HISTORY_DB; + }); + test("session_start survives hosts without addAutocompleteProvider", async () => { const setup = createPi(); const ctx = {