Compare commits

...

1 Commits

Author SHA1 Message Date
gustav-fff 93ca282d78 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
2026-06-13 19:55:13 -07:00
2 changed files with 30 additions and 4 deletions
+26 -4
View File
@@ -285,6 +285,10 @@ export default function fffExtension(pi: ExtensionAPI) {
// deadlock at the native layer (issue #403).
let finderPromise: Promise<FileFinder> | 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();
});
+4
View File
@@ -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 };
}