Skip to content

pi-fff hangs Pi /reload forever — re-import of fff-bun module graph never resolves in the Bun binary #757

Description

@chenydev

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions