From 0edfda008afb96175600dffd1e75f22dec6f757f Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 24 Jun 2026 00:56:38 -0700 Subject: [PATCH] perf(2a): pi SDK snapshot bundle (one snapshottable IIFE) esbuild bundles the lean pi-SDK graph (the 13 exports loadPiSdkRuntime needs) into a single IIFE, dist/pi-sdk-snapshot.js, that publishes globalThis.__PI_SDK_RUNTIME__. - src/snapshot-entry.ts: imports the SDK exports, assigns the runtime global. - scripts/build-snapshot-bundle.mjs: format:iife, platform:node (node builtins external), deep-import plugin to bypass the package exports map (avoids the ~100MB TUI graph the adapter skips), provider SDKs kept lazy/external, sha256 sidecar for dep-keying. C0 mitigations baked in: import.meta.url pinned to projected guest path; clipboard native-addon require made unreachable (DISPLAY=''). - package.json build chains build:snapshot; tsconfig excludes the esbuild-only entry. Validated (host, CJS harness): 13/13 exports populate, ~201ms eval, 880 modules inlined, 7.6MB. Traced top-level fs footprint = 2 deterministic package.json reads only (no net/ sockets/timers/native addons/per-session config). Artifact (dist) is gitignored. Step 2a of the agent-SDK snapshot goal. Co-Authored-By: Claude Opus 4.8 (1M context) --- registry/agent/pi/package.json | 3 +- .../pi/scripts/build-snapshot-bundle.mjs | 121 ++++++++++++++++++ registry/agent/pi/src/snapshot-entry.ts | 63 +++++++++ registry/agent/pi/tsconfig.json | 2 +- 4 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 registry/agent/pi/scripts/build-snapshot-bundle.mjs create mode 100644 registry/agent/pi/src/snapshot-entry.ts diff --git a/registry/agent/pi/package.json b/registry/agent/pi/package.json index a7819418c9..9ab4656f31 100644 --- a/registry/agent/pi/package.json +++ b/registry/agent/pi/package.json @@ -16,7 +16,8 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node scripts/build-snapshot-bundle.mjs", + "build:snapshot": "node scripts/build-snapshot-bundle.mjs", "check-types": "tsc --noEmit" }, "dependencies": { diff --git a/registry/agent/pi/scripts/build-snapshot-bundle.mjs b/registry/agent/pi/scripts/build-snapshot-bundle.mjs new file mode 100644 index 0000000000..6efb174cbc --- /dev/null +++ b/registry/agent/pi/scripts/build-snapshot-bundle.mjs @@ -0,0 +1,121 @@ +/** + * Builds the Pi SDK snapshot bundle (Step 2a). + * + * Bundles src/snapshot-entry.ts into a single IIFE at dist/pi-sdk-snapshot.js that + * evaluates the SDK graph and publishes it on globalThis.__PI_SDK_RUNTIME__. node: + * builtins stay external (provided by the V8 runtime's bridge polyfills, already in + * the snapshot heap); heavy provider SDKs reached only via dynamic import() stay + * external so they remain lazy and load post-restore from the VFS. + * + * The build env intentionally clears DISPLAY/WAYLAND_DISPLAY (C0 mitigation): the + * optional @mariozechner/clipboard NAPI addon is behind a DISPLAY guard; with it + * unset the SDK bakes `clipboard = null` so no native pointer enters the snapshot. + */ +import { createHash } from "node:crypto"; +import { createRequire } from "node:module"; +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { pathToFileURL } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const pkgRoot = join(here, ".."); + +// esbuild lives in the workspace pnpm store; resolve it from there. +const require = createRequire(import.meta.url); +const repoRoot = join(pkgRoot, "..", "..", ".."); +let esbuildPath; +try { + esbuildPath = require.resolve("esbuild", { paths: [pkgRoot, repoRoot] }); +} catch { + const { globSync } = await import("node:fs"); + const matches = globSync( + join(repoRoot, "node_modules/.pnpm/esbuild@*/node_modules/esbuild/lib/main.js"), + ); + if (matches.length === 0) throw new Error("esbuild not found in workspace"); + esbuildPath = matches.sort().reverse()[0]; +} +const { build } = await import(pathToFileURL(esbuildPath).href); +const outfile = join(pkgRoot, "dist", "pi-sdk-snapshot.js"); + +// Provider SDKs the pi-ai layer pulls only via dynamic import(); keep them lazy. +const lazyExternals = [ + "@anthropic-ai/sdk", + "openai", + "@google/genai", + "@mistralai/mistralai", + "@aws-sdk/client-bedrock-runtime", + "proxy-agent", + "@mariozechner/clipboard", +]; + +// The adapter deliberately imports deep `dist/core/*` paths to skip the SDK's TUI +// graph, but the package `exports` map only exposes `.`/`./hooks`. Mirror the +// adapter: resolve the deep specifiers to absolute file paths under the package's +// dist dir, bypassing the exports map (esbuild bundles absolute paths fine). +const { realpathSync, existsSync } = await import("node:fs"); +const piCodingRoot = [ + join(pkgRoot, "node_modules/@mariozechner/pi-coding-agent"), + join(repoRoot, "node_modules/@mariozechner/pi-coding-agent"), +].find((p) => existsSync(p)); +if (!piCodingRoot) throw new Error("pi-coding-agent not found in node_modules"); +const piCodingReal = realpathSync(piCodingRoot); +const piCodingDist = join(piCodingReal, "dist"); +// pnpm sibling layout: pi-coding-agent's own deps (incl. pi-agent-core) live in the +// same `.pnpm//node_modules/@mariozechner` dir. Resolve transitive +// @mariozechner/* deps from there so we bundle exactly what the SDK links against. +const piSiblingScope = dirname(piCodingReal); // .../node_modules/@mariozechner +const deepImportPlugin = { + name: "pi-deep-imports", + setup(b) { + b.onResolve({ filter: /^@mariozechner\/pi-coding-agent\/dist\// }, (a) => { + const rel = a.path.replace("@mariozechner/pi-coding-agent/dist/", ""); + return { path: join(piCodingDist, rel) }; + }); + b.onResolve({ filter: /^@mariozechner\/pi-agent-core/ }, (a) => { + const sub = a.path.replace("@mariozechner/pi-agent-core", "") || "/dist/index.js"; + return { path: join(piSiblingScope, "pi-agent-core", sub) }; + }); + }, +}; + +// esbuild stubs `import.meta` to `{}` in IIFE output, so import.meta.url is +// undefined and the SDK's top-level install-detection (fileURLToPath(import.meta.url)) +// crashes. Pin it to a single deterministic URL: the SDK's projected guest path, so +// any top-level dir/version computation resolves to where the SDK actually lives in +// the VM. Override with PI_SNAPSHOT_BASE_URL (e.g. the real host path for testing). +const baseUrl = + process.env.PI_SNAPSHOT_BASE_URL || + "file:///root/node_modules/@mariozechner/pi-coding-agent/dist/index.js"; + +const result = await build({ + entryPoints: [join(pkgRoot, "src", "snapshot-entry.ts")], + outfile, + bundle: true, + format: "iife", + platform: "node", // node: builtins become external require() calls + target: "esnext", + external: lazyExternals, + plugins: [deepImportPlugin], + define: { + "import.meta.url": JSON.stringify(baseUrl), + // C0 mitigation: force the headless path in the clipboard native-addon + // guard so `require("@mariozechner/clipboard")` (a NAPI addon) is never + // triggered at snapshot-eval time, regardless of the sidecar's env. + "process.env.DISPLAY": '""', + "process.env.WAYLAND_DISPLAY": '""', + "process.env.TERMUX_VERSION": '""', + }, + legalComments: "none", + logLevel: "info", + metafile: true, +}); + +const bytes = readFileSync(outfile); +const sha256 = createHash("sha256").update(bytes).digest("hex"); +writeFileSync(`${outfile}.sha256`, `${sha256}\n`); + +const inputs = Object.keys(result.metafile.inputs).length; +console.log( + `\npi-sdk-snapshot.js: ${(bytes.length / 1024).toFixed(0)} KiB · ${inputs} modules inlined · sha256 ${sha256.slice(0, 12)}…`, +); diff --git a/registry/agent/pi/src/snapshot-entry.ts b/registry/agent/pi/src/snapshot-entry.ts new file mode 100644 index 0000000000..bc90b5d140 --- /dev/null +++ b/registry/agent/pi/src/snapshot-entry.ts @@ -0,0 +1,63 @@ +/** + * Pi SDK snapshot entry (Step 2a/2c — eval/entry split). + * + * This module is bundled by esbuild into a single IIFE (`dist/pi-sdk-snapshot.js`) + * whose ONLY job is to evaluate the Pi SDK module graph and publish its exports on + * a well-known global, `globalThis.__PI_SDK_RUNTIME__`. The bundle is run once at V8 + * snapshot-creation time; the evaluated SDK heap is captured into the startup blob + * and reused to seed every fresh per-session isolate. After restore the adapter + * reads the global instead of dynamically importing the SDK from the guest VFS, + * collapsing the ~830ms per-session module-load/eval tax. + * + * INVARIANTS (enforced by the C0 snapshot-safety scan — see + * ~/.agents/research/pi-snapshot-safety-scan.md): + * - This entry performs NO per-session work: it must not read cwd, model, + * ANTHROPIC env vars, HOME, argv, open fds/sockets, or start timers. It only + * binds the SDK's static exports. All per-session configuration is injected + * post-restore by the adapter (newSession), exactly as before. + * - Heavy provider SDKs (@anthropic-ai/sdk, openai, …) are loaded by the SDK via + * dynamic import() and are intentionally NOT part of this static graph; they + * stay lazy and load post-restore from the VFS on first prompt. + * - The bundle is keyed by the sha256 of its own (fully inlined) bytes, so any SDK + * dependency change invalidates the snapshot. + */ + +import { Agent } from "@mariozechner/pi-agent-core"; +import { AuthStorage } from "@mariozechner/pi-coding-agent/dist/core/auth-storage.js"; +import { + getAgentDir, + getDocsPath, +} from "@mariozechner/pi-coding-agent/dist/config.js"; +import { DEFAULT_THINKING_LEVEL } from "@mariozechner/pi-coding-agent/dist/core/defaults.js"; +import { convertToLlm } from "@mariozechner/pi-coding-agent/dist/core/messages.js"; +import { ModelRegistry } from "@mariozechner/pi-coding-agent/dist/core/model-registry.js"; +import { DefaultResourceLoader } from "@mariozechner/pi-coding-agent/dist/core/resource-loader.js"; +import { + createAgentSession, + createCodingTools, +} from "@mariozechner/pi-coding-agent/dist/core/sdk.js"; +import { SessionManager } from "@mariozechner/pi-coding-agent/dist/core/session-manager.js"; +import { SettingsManager } from "@mariozechner/pi-coding-agent/dist/core/settings-manager.js"; +import { createAllTools } from "@mariozechner/pi-coding-agent/dist/core/tools/index.js"; + +// The shape mirrors `PiSdkRuntime` in adapter.ts so the adapter can read the +// global with no behavioral difference from the dynamic-import path. +const runtime = { + Agent, + AuthStorage, + DefaultResourceLoader, + DEFAULT_THINKING_LEVEL, + ModelRegistry, + SettingsManager, + SessionManager, + convertToLlm, + getAgentDir, + getDocsPath, + createAgentSession, + createCodingTools, + createAllTools, +}; + +// Publish on the global so it survives into every fresh context cloned from the +// snapshotted default context. +(globalThis as Record).__PI_SDK_RUNTIME__ = runtime; diff --git a/registry/agent/pi/tsconfig.json b/registry/agent/pi/tsconfig.json index bff7313256..e15131c84e 100644 --- a/registry/agent/pi/tsconfig.json +++ b/registry/agent/pi/tsconfig.json @@ -6,5 +6,5 @@ "rootDir": "./src" }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/snapshot-entry.ts"] }