Summary
/reload in pi hangs forever (stuck on "Reloading keybindings, extensions, skills, prompts, themes, and context files..." with CPU ≈ 0) whenever @ff-labs/pi-fff is installed. Bisected against all other packages: pi-fff alone reproduces it; disabling pi-fff makes reload complete in ~2s.
Root cause
Pi reloads extension modules with jiti (moduleCache: false), so loadSdk() in packages/pi-fff/src/sdk.ts runs a fresh dynamic import("@ff-labs/fff-bun") on every reload. Inside the fff-bun module graph, packages/fff-bun/src/embedded.ts has a top-level:
export const embeddedLibPath: string | null = await resolveEmbeddedLibPath();
// → import(`@ff-labs/fff-bin-linux-x64-gnu/libfff_c.so`, { with: { type: "file" } })
In the Bun-compiled pi binary this second import never resolves (the first one at startup resolves in ~20ms). Because pi awaits all session_start handlers without a timeout (runner.js emit()), the reload screen blocks permanently. The same import twice in plain bun works fine — the hang is specific to the pi Bun binary + jiti re-import combination.
Evidence
- PTY reproduction in an isolated env (copied agent dir,
PI_CODING_AGENT_DIR): reload stuck >60s, no "Reloaded keybindings" line, process alive at ~0% CPU (dead-wait on futex/epoll).
- Instrumented timeline shows:
loadSdk importing @ff-labs/fff-bun printed, loadSdk resolved never (startup: 107ms vs reload: ∞).
FileFinder.create() / waitForScan(15000) themselves are fine (52ms in isolation, including a create→destroy→create cycle) — the hang is the module import, not the scan.
Suggested fix (verified)
Cache the first import on globalThis so reloads reuse it instead of re-importing:
export function loadSdk(): Promise<{ FileFinder: FileFinderStatic }> {
if (sdkPromise) return sdkPromise;
const g = globalThis as Record<string, unknown>;
if (g.__fffSdkPromiseGlobal) {
sdkPromise = g.__fffSdkPromiseGlobal as Promise<{ FileFinder: FileFinderStatic }>;
return sdkPromise;
}
const p = import(pkg) as Promise<{ FileFinder: FileFinderStatic }>;
sdkPromise = p;
(globalThis as Record<string, unknown>).__fffSdkPromiseGlobal = p;
return p;
}
Verified with the full 28-package user settings: 3 consecutive /reload calls all succeed in ~2s.
Related
Summary
/reloadin pi hangs forever (stuck on "Reloading keybindings, extensions, skills, prompts, themes, and context files..." with CPU ≈ 0) whenever@ff-labs/pi-fffis installed. Bisected against all other packages: pi-fff alone reproduces it; disabling pi-fff makes reload complete in ~2s.Root cause
Pi reloads extension modules with
jiti(moduleCache: false), soloadSdk()inpackages/pi-fff/src/sdk.tsruns a fresh dynamicimport("@ff-labs/fff-bun")on every reload. Inside the fff-bun module graph,packages/fff-bun/src/embedded.tshas a top-level:In the Bun-compiled pi binary this second import never resolves (the first one at startup resolves in ~20ms). Because pi awaits all
session_starthandlers without a timeout (runner.jsemit()), the reload screen blocks permanently. The same import twice in plainbunworks fine — the hang is specific to the pi Bun binary + jiti re-import combination.Evidence
PI_CODING_AGENT_DIR): reload stuck >60s, no "Reloaded keybindings" line, process alive at ~0% CPU (dead-wait on futex/epoll).loadSdk importing @ff-labs/fff-bunprinted,loadSdk resolvednever (startup: 107ms vs reload: ∞).FileFinder.create()/waitForScan(15000)themselves are fine (52ms in isolation, including a create→destroy→create cycle) — the hang is the module import, not the scan.Suggested fix (verified)
Cache the first import on
globalThisso reloads reuse it instead of re-importing:Verified with the full 28-package user settings: 3 consecutive
/reloadcalls all succeed in ~2s.Related
/new//resumeon scan wait) — different trigger (15s scan wait vs. permanent import hang), same package. PR fix(pi-fff): non-blocking session_start warmup #599's non-blocking warmup would mask the UI hang but the background import would still hang and break subsequent fff tool calls (theyawaitthe same in-flightsdkPromise), so this import-level fix is still needed.