From 93ca282d78d3fd26e2b93e358cbec1d7d3f6c1bc Mon Sep 17 00:00:00 2001 From: gustav-fff <286169375+gustav-fff@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:55:13 -0700 Subject: [PATCH] fix(pi-fff): non-blocking session_start warmup Schedule FileFinder.create + waitForScan via setTimeout(0) so Pi /new and /resume return immediately. A monotonic lifecycleId tagged on each session_start / session_shutdown lets a stale warmup detect that the session has been replaced and skip both ensureFinder and ctx.ui.notify. ensureFinder now returns the local handle so a shutdown mid-warmup can't hand back a destroyed/replaced finder. Closes #477 --- packages/pi-fff/src/index.ts | 30 ++++++++++++++++++++++---- packages/pi-fff/test/extension.test.ts | 4 ++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/pi-fff/src/index.ts b/packages/pi-fff/src/index.ts index 7c8091f4..761d9001 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -285,6 +285,10 @@ export default function fffExtension(pi: ExtensionAPI) { // deadlock at the native layer (issue #403). let finderPromise: Promise | null = null; let activeCwd = process.cwd(); + // Bumped on every session_start / session_shutdown so an async warmup + // started for a previous session can detect that it's stale and bail out + // before touching `finder` / notifying a destroyed UI. + let lifecycleId = 0; // Mode resolution: flag > env > default let currentMode: FffMode = @@ -355,10 +359,13 @@ export default function fffExtension(pi: ExtensionAPI) { if (!result.ok) throw new Error(`Failed to create FFF file finder: ${result.error}`); - finder = result.value; + const created = result.value; + finder = created; finderCwd = cwd; - await finder.waitForScan(15000); - return finder; + await created.waitForScan(15000); + // Return the local handle: a shutdown during warmup may null/replace + // `finder`, but the caller still needs the instance they were promised. + return created; })().finally(() => { finderPromise = null; }); @@ -467,6 +474,7 @@ export default function fffExtension(pi: ExtensionAPI) { pi.on("session_start", async (_event, ctx) => { try { activeCwd = ctx.cwd; + const sessionLifecycleId = ++lifecycleId; // Restore persisted mode from session entries. This handles session // resume after process restart where env vars are lost, and ensures @@ -492,7 +500,20 @@ export default function fffExtension(pi: ExtensionAPI) { } registerAutocompleteProvider(ctx); - await ensureFinder(activeCwd); + + // Warm the finder in the background — Pi /new and /resume must not + // wait on the initial scan. Subsequent tool calls / mention lookups + // share the same in-flight promise via ensureFinder(). + setTimeout(() => { + if (sessionLifecycleId !== lifecycleId) return; + ensureFinder(activeCwd).catch((e: unknown) => { + if (sessionLifecycleId !== lifecycleId) return; + ctx.ui.notify( + `FFF init failed: ${e instanceof Error ? e.message : String(e)}`, + "error", + ); + }); + }, 0); } catch (e: unknown) { ctx.ui.notify( `FFF init failed: ${e instanceof Error ? e.message : String(e)}`, @@ -502,6 +523,7 @@ export default function fffExtension(pi: ExtensionAPI) { }); pi.on("session_shutdown", async () => { + lifecycleId++; destroyFinder(); }); diff --git a/packages/pi-fff/test/extension.test.ts b/packages/pi-fff/test/extension.test.ts index 1fa80ef4..a6820115 100644 --- a/packages/pi-fff/test/extension.test.ts +++ b/packages/pi-fff/test/extension.test.ts @@ -118,6 +118,10 @@ async function start(mode?: string) { const sessionStart = setup.events.get("session_start"); expect(sessionStart).toBeDefined(); await sessionStart?.({ reason: "startup" }, ctx); + // session_start now schedules the finder warmup via setTimeout(0); flush + // the queue so tests can observe FileFinder.create / waitForScan calls. + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); return { ...setup, ctx }; }