fix(engine): make pi.js imports static to fail fast on partial dist

Replace lazy `await import("./pi.js")` and `require("./pi.js")` calls in
runtime-resolution, agent-session-helpers, agent-heartbeat, and
cron-runner with top-level static imports. These dynamic imports were
documented as plugin-decoupling, but pi.js is already eagerly loaded
through index.ts re-exports and static imports in executor/merger/
reviewer/triage/mission-execution-loop, so the deferral never paid off
in practice.

The deferral did, however, introduce a TOCTOU race: a tsc rebuild that
momentarily emptied dist/pi.js would let the engine load fine and only
fail minutes later when the first session was created (e.g. FN-2860
errored two minutes into execution while pi.js was being rewritten).
With static imports, a missing/half-built dist now fails immediately at
process startup with a clear stack — verified by `mv dist/pi.js
dist/pi.js.bak` reproducing ERR_MODULE_NOT_FOUND on the first import of
runtime-resolution.js.

Also drops the DefaultPiRuntime.describeModelFn cache, which only
existed to paper over the require-on-first-call latency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-28 07:48:03 -07:00
parent 479f25d50f
commit 38933c770a
4 changed files with 11 additions and 42 deletions

View File

@@ -26,9 +26,7 @@ import { AgentLogger } from "./agent-logger.js";
import { resolveAgentInstructionsWithRatings, buildSystemPromptWithInstructions } from "./agent-instructions.js"; import { resolveAgentInstructionsWithRatings, buildSystemPromptWithInstructions } from "./agent-instructions.js";
import { heartbeatLog, formatError } from "./logger.js"; import { heartbeatLog, formatError } from "./logger.js";
import { createRunAuditor, type EngineRunContext } from "./run-audit.js"; import { createRunAuditor, type EngineRunContext } from "./run-audit.js";
import { promptWithFallback } from "./pi.js";
// Lazy import for pi — avoids pulling the pi SDK into the module graph
// when heartbeat execution isn't needed.
/** Resolved per-agent heartbeat config after validation and fallback */ /** Resolved per-agent heartbeat config after validation and fallback */
interface ResolvedHeartbeatConfig { interface ResolvedHeartbeatConfig {
@@ -1099,8 +1097,6 @@ export class HeartbeatMonitor {
}, },
}; };
// Lazy-load promptWithFallback
const { promptWithFallback } = await import("./pi.js");
const { createResolvedAgentSession, extractRuntimeHint } = await import("./agent-session-helpers.js"); const { createResolvedAgentSession, extractRuntimeHint } = await import("./agent-session-helpers.js");
const { buildSessionSkillContextSync } = await import("./session-skill-context.js"); const { buildSessionSkillContextSync } = await import("./session-skill-context.js");

View File

@@ -12,6 +12,7 @@ import type { PluginRunner } from "./plugin-runner.js";
import type { AgentSession } from "@mariozechner/pi-coding-agent"; import type { AgentSession } from "@mariozechner/pi-coding-agent";
import { resolveRuntime, buildRuntimeResolutionContext, type SessionPurpose } from "./runtime-resolution.js"; import { resolveRuntime, buildRuntimeResolutionContext, type SessionPurpose } from "./runtime-resolution.js";
import { createLogger } from "./logger.js"; import { createLogger } from "./logger.js";
import { promptWithFallback, describeModel } from "./pi.js";
/** Logger for agent session helpers */ /** Logger for agent session helpers */
const sessionLog = createLogger("agent-session"); const sessionLog = createLogger("agent-session");
@@ -130,10 +131,7 @@ export async function promptWithAutoRetry(
prompt: string, prompt: string,
options?: unknown, options?: unknown,
): Promise<void> { ): Promise<void> {
// Dynamic import to get the default runtime's promptWithFallback return promptWithFallback(session, prompt, options);
// This works because the default runtime delegates to the existing implementation
const { promptWithFallback: pwf } = await import("./pi.js");
return pwf(session, prompt, options);
} }
/** /**
@@ -143,6 +141,5 @@ export async function promptWithAutoRetry(
* @returns Model description string * @returns Model description string
*/ */
export async function describeAgentModel(session: AgentSession): Promise<string> { export async function describeAgentModel(session: AgentSession): Promise<string> {
const { describeModel: dm } = await import("./pi.js"); return describeModel(session);
return dm(session);
} }

View File

@@ -5,6 +5,7 @@ import type { AutomationStore } from "@fusion/core";
import type { ScheduledTask, AutomationRunResult, AutomationStep, AutomationStepResult, Column, TaskCreateInput } from "@fusion/core"; import type { ScheduledTask, AutomationRunResult, AutomationStep, AutomationStepResult, Column, TaskCreateInput } from "@fusion/core";
import { createLogger } from "./logger.js"; import { createLogger } from "./logger.js";
import { defaultShell } from "./shell-utils.js"; import { defaultShell } from "./shell-utils.js";
import { createFnAgent, promptWithFallback } from "./pi.js";
const execAsync = promisify(exec); const execAsync = promisify(exec);
const log = createLogger("cron-runner"); const log = createLogger("cron-runner");
@@ -628,9 +629,6 @@ const AI_AUTOMATION_SYSTEM_PROMPT = [
* @returns An AiPromptExecutor function suitable for CronRunnerOptions. * @returns An AiPromptExecutor function suitable for CronRunnerOptions.
*/ */
export async function createAiPromptExecutor(cwd: string): Promise<AiPromptExecutor> { export async function createAiPromptExecutor(cwd: string): Promise<AiPromptExecutor> {
// We import lazily to keep the factory self-contained and to avoid
// pulling pi.ts into the module graph when AI execution isn't used.
const { createFnAgent, promptWithFallback } = await import("./pi.js");
const disposeLog = createLogger("cron-runner"); const disposeLog = createLogger("cron-runner");
return async (prompt: string, modelProvider?: string, modelId?: string): Promise<string> => { return async (prompt: string, modelProvider?: string, modelId?: string): Promise<string> => {

View File

@@ -11,6 +11,7 @@ import type { AgentRuntime, AgentRuntimeOptions, AgentSessionResult } from "./ag
import type { PluginRunner } from "./plugin-runner.js"; import type { PluginRunner } from "./plugin-runner.js";
import type { AgentSession } from "@mariozechner/pi-coding-agent"; import type { AgentSession } from "@mariozechner/pi-coding-agent";
import { createLogger } from "./logger.js"; import { createLogger } from "./logger.js";
import { createFnAgent, promptWithFallback, describeModel } from "./pi.js";
/** Logger for the runtime resolution subsystem */ /** Logger for the runtime resolution subsystem */
const runtimeLog = createLogger("runtime-resolver"); const runtimeLog = createLogger("runtime-resolver");
@@ -77,36 +78,16 @@ export class DefaultPiRuntime implements AgentRuntime {
readonly id = "pi"; readonly id = "pi";
readonly name = "Default PI Runtime"; readonly name = "Default PI Runtime";
// Synchronous cached describeModel function
private static describeModelFn: ((session: AgentSession) => string) | null = null;
/**
* Create an agent session using the default pi implementation.
*/
async createSession(options: AgentRuntimeOptions): Promise<AgentSessionResult> { async createSession(options: AgentRuntimeOptions): Promise<AgentSessionResult> {
const { createFnAgent } = await import("./pi.js");
return createFnAgent(options); return createFnAgent(options);
} }
/**
* Prompt with automatic retry and compaction.
* Delegates to the existing promptWithFallback implementation.
*/
async promptWithFallback(session: AgentSession, prompt: string, options?: unknown): Promise<void> { async promptWithFallback(session: AgentSession, prompt: string, options?: unknown): Promise<void> {
const { promptWithFallback: pwf } = await import("./pi.js"); return promptWithFallback(session, prompt, options);
return pwf(session, prompt, options);
} }
/**
* Get model description from session.
*/
describeModel(session: AgentSession): string { describeModel(session: AgentSession): string {
if (!DefaultPiRuntime.describeModelFn) { return describeModel(session);
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { describeModel } = require("./pi.js");
DefaultPiRuntime.describeModelFn = describeModel;
}
return DefaultPiRuntime.describeModelFn!(session);
} }
} }
@@ -216,18 +197,15 @@ function wrapPluginRuntime(
return adapter.promptWithFallback(session, prompt, options); return adapter.promptWithFallback(session, prompt, options);
} }
// Fallback to default pi promptWithFallback // Fallback to default pi promptWithFallback
const { promptWithFallback: pwf } = await import("./pi.js"); return promptWithFallback(session, prompt, options);
return pwf(session, prompt, options);
}, },
describeModel: (session: AgentSession) => { describeModel: (session: AgentSession) => {
const adapter = instance as Record<string, unknown>; const adapter = instance as Record<string, unknown>;
if (typeof adapter.describeModel === "function") { if (typeof adapter.describeModel === "function") {
return (adapter.describeModel as (s: AgentSession) => string)(session); return (adapter.describeModel as (s: AgentSession) => string)(session);
} }
// Fallback to default pi describeModel - use cached sync function // Fallback to default pi describeModel
// eslint-disable-next-line @typescript-eslint/no-require-imports return describeModel(session);
const { describeModel: dm } = require("./pi.js");
return dm(session);
}, },
}; };
} }