diff --git a/packages/pi-plugin/src/dreamer/pi-session-api.test.ts b/packages/pi-plugin/src/dreamer/pi-session-api.test.ts
index c5efe6d08..2b43c8112 100644
--- a/packages/pi-plugin/src/dreamer/pi-session-api.test.ts
+++ b/packages/pi-plugin/src/dreamer/pi-session-api.test.ts
@@ -1,12 +1,93 @@
///
import { beforeEach, describe, expect, it, mock } from "bun:test";
+import {
+ mkdirSync,
+ mkdtempSync,
+ realpathSync,
+ symlinkSync,
+ writeFileSync,
+} from "node:fs";
+import { tmpdir } from "node:os";
+import { dirname, join } from "node:path";
import {
clearCachedModule,
+ defaultLoaders,
loadDefaultPiSessionApi,
type ModuleLoader,
+ resolvePiCodingAgentModule,
} from "./pi-session-api";
+const PI_SPEC = "@earendil-works/pi-coding-agent";
+
+/** A fixture module whose listAll returns a unique marker, so tests can tell
+ * WHICH copy of pi-coding-agent was resolved through the public API. */
+function fixtureModule(marker: string): string {
+ return (
+ `export const __piShimFakeMarker = ${JSON.stringify(marker)};\n` +
+ `export const SessionManager = { listAll: async () => [${JSON.stringify(marker)}] };\n`
+ );
+}
+
+/** Write a fake pi-coding-agent package rooted at pkgRoot. */
+function writeFixturePackage(
+ pkgRoot: string,
+ opts: {
+ version?: string;
+ entry?: string;
+ /** Full manifest override — use for shapes the default can't express. */
+ manifest?: Record;
+ files: Record;
+ },
+): void {
+ mkdirSync(pkgRoot, { recursive: true });
+ writeFileSync(
+ join(pkgRoot, "package.json"),
+ JSON.stringify(
+ opts.manifest ?? {
+ name: PI_SPEC,
+ version: opts.version ?? "9.9.9",
+ exports: { ".": { import: opts.entry ?? "./index.js" } },
+ },
+ ),
+ );
+ for (const [rel, content] of Object.entries(opts.files)) {
+ const p = join(pkgRoot, rel);
+ mkdirSync(dirname(p), { recursive: true });
+ writeFileSync(p, content);
+ }
+}
+
+/** Loader that stands in for "the next strategy" when the first must fail. */
+function fallbackLoader(marker = "fallback-used"): ModuleLoader {
+ return {
+ name: "Fallback",
+ load: async () => ({
+ SessionManager: { listAll: async () => [marker] },
+ }),
+ };
+}
+
+/** Run fn with process.argv[1] temporarily replaced (undefined = no script arg). */
+async function withArgv1(
+ argv1: string | undefined,
+ fn: () => Promise,
+): Promise {
+ const origArgv = process.argv;
+ Object.defineProperty(process, "argv", {
+ value: argv1 === undefined ? [process.execPath] : [process.execPath, argv1],
+ configurable: true,
+ });
+ try {
+ return await fn();
+ } finally {
+ Object.defineProperty(process, "argv", {
+ value: origArgv,
+ configurable: true,
+ });
+ }
+}
+
/**
* These tests exercise the DEFAULT resolution path against the actually
* installed pi-coding-agent package. The Pi session-listing API drifted once
@@ -30,9 +111,6 @@ describe("loadDefaultPiSessionApi", () => {
it("parses JSONL session entries through the resolved loader", async () => {
const api = await loadDefaultPiSessionApi();
- const { mkdtempSync, writeFileSync } = await import("node:fs");
- const { tmpdir } = await import("node:os");
- const { join } = await import("node:path");
const dir = mkdtempSync(join(tmpdir(), "pi-session-api-test-"));
const file = join(dir, "session.jsonl");
@@ -52,6 +130,318 @@ describe("loadDefaultPiSessionApi", () => {
expect(entries.length).toBeGreaterThan(0);
}, 30000);
+ describe("default loader order", () => {
+ it('first default loader is "Resolve from running Pi binary entry" (prefer the running Pi version)', () => {
+ const names = defaultLoaders.map((l) => l.name);
+ expect(names[0]).toBe("Resolve from running Pi binary entry");
+ expect(names).toContain("Bare import");
+ });
+
+ it("resolves through a bin-shim symlink when argv[1] is the shim path", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "pi-symlink-test-"));
+ const pkgRoot = join(
+ dir,
+ "node_modules",
+ "@earendil-works",
+ "pi-coding-agent",
+ );
+ writeFixturePackage(pkgRoot, {
+ files: { "index.js": fixtureModule("fake-9.9.9") },
+ });
+ // bin shim: bin/pi -> /index.js (a symlink to the real entry)
+ const binDir = join(dir, "bin");
+ mkdirSync(binDir, { recursive: true });
+ const shim = join(binDir, "pi");
+ symlinkSync(join(pkgRoot, "index.js"), shim);
+
+ const origCwd = process.cwd();
+ try {
+ process.chdir(dir);
+ await withArgv1(shim, async () => {
+ clearCachedModule();
+ const api = await loadDefaultPiSessionApi();
+ expect(typeof api.listSessions).toBe("function");
+ expect(typeof api.loadEntriesFromFile).toBe("function");
+ // End-to-end through the PUBLIC resolve path: the fixture's
+ // listAll marker must come back. If the symlink walk broke and
+ // the bare-import fallback rescued resolution, the REAL
+ // installed package's sessions would come back instead and
+ // this assertion fails — the fallback cannot green this test.
+ expect(await api.listSessions()).toEqual(["fake-9.9.9"]);
+ // Belt and braces: the fake copy also exports a module marker
+ // the real installed pi-coding-agent does not.
+ const mod = (await resolvePiCodingAgentModule(defaultLoaders)) as {
+ __piShimFakeMarker?: string;
+ };
+ expect(mod.__piShimFakeMarker).toBe("fake-9.9.9");
+ });
+ expect(realpathSync(shim)).toBe(
+ realpathSync(join(pkgRoot, "index.js")),
+ );
+ } finally {
+ process.chdir(origCwd);
+ }
+ }, 30000);
+ });
+
+ describe("running-Pi resolver layouts", () => {
+ it("prefers the running Pi over a stale extension-tree copy", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "pi-running-vs-stale-"));
+ const pkgRoot = join(
+ dir,
+ "node_modules",
+ "@earendil-works",
+ "pi-coding-agent",
+ );
+ writeFixturePackage(pkgRoot, {
+ files: { "index.js": fixtureModule("running-pi-9.9.9") },
+ });
+
+ // The bare import resolves a STALE extension-tree copy. mock.module
+ // makes that copy observable: if the running-Pi resolver breaks and
+ // the bare import rescues resolution, the stale marker comes back
+ // and this test fails instead of greening on the fallback.
+ const originalModule = await import(PI_SPEC);
+ mock.module(PI_SPEC, () => ({
+ SessionManager: { listAll: async () => ["stale-extension-tree-copy"] },
+ }));
+ try {
+ await withArgv1(join(pkgRoot, "index.js"), async () => {
+ clearCachedModule();
+ const api = await loadDefaultPiSessionApi();
+ expect(await api.listSessions()).toEqual(["running-pi-9.9.9"]);
+ });
+ } finally {
+ mock.module(PI_SPEC, () => originalModule);
+ clearCachedModule();
+ }
+ }, 30000);
+
+ it("skips Pi's copied dist/package.json and resolves the parent package root", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "pi-dist-metadata-"));
+ const pkgRoot = join(dir, "pi-coding-agent");
+ writeFixturePackage(pkgRoot, {
+ entry: "./dist/index.js",
+ files: {
+ "dist/index.js": fixtureModule("parent-root-9.9.9"),
+ "dist/cli.js": "// binary entry\n",
+ // build:binary copies this metadata into dist/. Stopping the
+ // walk here would resolve ./dist/index.js against dist/ and
+ // construct dist/dist/index.js.
+ "dist/package.json": JSON.stringify({
+ name: PI_SPEC,
+ version: "9.9.9",
+ exports: { ".": { import: "./dist/index.js" } },
+ }),
+ },
+ });
+ await withArgv1(join(pkgRoot, "dist", "cli.js"), async () => {
+ clearCachedModule();
+ const api = await loadDefaultPiSessionApi();
+ expect(await api.listSessions()).toEqual(["parent-root-9.9.9"]);
+ });
+ }, 30000);
+
+ it("rejects a manifest entry that escapes the package root", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "pi-traversal-"));
+ const pkgRoot = join(dir, "pi-coding-agent");
+ writeFixturePackage(pkgRoot, {
+ entry: "../../outside.js",
+ files: { "cli.js": "// entry\n" },
+ });
+ await withArgv1(join(pkgRoot, "cli.js"), async () => {
+ clearCachedModule();
+ const fallback: ModuleLoader = {
+ name: "Fallback",
+ load: async () => ({
+ SessionManager: { listAll: async () => ["fallback-used"] },
+ }),
+ };
+ // The escaping entry must be rejected, falling through to the
+ // next loader rather than importing a path outside the package.
+ const api = await loadDefaultPiSessionApi([
+ defaultLoaders[0],
+ fallback,
+ ]);
+ expect(await api.listSessions()).toEqual(["fallback-used"]);
+ });
+ }, 30000);
+
+ it("running from a TypeScript source checkout loads the source entry, not stale dist output", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "pi-source-mode-"));
+ const pkgRoot = join(dir, "pi-coding-agent");
+ writeFixturePackage(pkgRoot, {
+ entry: "./dist/index.js",
+ files: {
+ "src/cli.ts": "// source entry (tsx/jiti)\n",
+ "src/index.ts": fixtureModule("source-checkout"),
+ // Stale build output left behind by an old build — must NOT win.
+ "dist/index.js": fixtureModule("stale-dist-output"),
+ },
+ });
+ await withArgv1(join(pkgRoot, "src", "cli.ts"), async () => {
+ clearCachedModule();
+ const api = await loadDefaultPiSessionApi();
+ expect(await api.listSessions()).toEqual(["source-checkout"]);
+ });
+ }, 30000);
+
+ it("source mode without a source counterpart falls through instead of loading stale dist", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "pi-source-missing-"));
+ const pkgRoot = join(dir, "pi-coding-agent");
+ writeFixturePackage(pkgRoot, {
+ entry: "./dist/index.js",
+ files: {
+ "src/cli.ts": "// source entry (tsx/jiti)\n",
+ "dist/index.js": fixtureModule("stale-dist-output"),
+ },
+ });
+ await withArgv1(join(pkgRoot, "src", "cli.ts"), async () => {
+ clearCachedModule();
+ const api = await loadDefaultPiSessionApi([
+ defaultLoaders[0],
+ fallbackLoader(),
+ ]);
+ expect(await api.listSessions()).toEqual(["fallback-used"]);
+ });
+ }, 30000);
+
+ describe("manifest entry validation", () => {
+ // The "./" requirement applies to every manually joined manifest
+ // target, not just exports: a non-prefixed "dist/index.js" in ANY
+ // field must be rejected, falling through to the next loader.
+ const nonPrefixedCases: Array<{
+ label: string;
+ manifest: Record;
+ }> = [
+ {
+ label: "exports string entry",
+ manifest: {
+ name: PI_SPEC,
+ exports: { ".": "dist/index.js" },
+ },
+ },
+ {
+ label: "exports import condition",
+ manifest: {
+ name: PI_SPEC,
+ exports: { ".": { import: "dist/index.js" } },
+ },
+ },
+ {
+ label: "module field",
+ manifest: { name: PI_SPEC, module: "dist/index.js" },
+ },
+ {
+ label: "main field",
+ manifest: { name: PI_SPEC, main: "dist/index.js" },
+ },
+ ];
+ for (const { label, manifest } of nonPrefixedCases) {
+ it(`rejects a non-prefixed entry in ${label}`, async () => {
+ const dir = mkdtempSync(join(tmpdir(), "pi-nonprefixed-"));
+ const pkgRoot = join(dir, "pi-coding-agent");
+ writeFixturePackage(pkgRoot, {
+ manifest,
+ files: {
+ "cli.js": "// entry\n",
+ "dist/index.js": fixtureModule("must-not-load"),
+ },
+ });
+ await withArgv1(join(pkgRoot, "cli.js"), async () => {
+ clearCachedModule();
+ const api = await loadDefaultPiSessionApi([
+ defaultLoaders[0],
+ fallbackLoader(),
+ ]);
+ expect(await api.listSessions()).toEqual(["fallback-used"]);
+ });
+ }, 30000);
+ }
+
+ it("resolves the default condition when no import condition exists", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "pi-default-cond-"));
+ const pkgRoot = join(dir, "pi-coding-agent");
+ writeFixturePackage(pkgRoot, {
+ manifest: {
+ name: PI_SPEC,
+ exports: {
+ ".": {
+ types: "./index.d.ts",
+ default: "./index.js",
+ },
+ },
+ },
+ files: { "index.js": fixtureModule("default-condition") },
+ });
+ await withArgv1(join(pkgRoot, "index.js"), async () => {
+ clearCachedModule();
+ const api = await loadDefaultPiSessionApi();
+ expect(await api.listSessions()).toEqual(["default-condition"]);
+ });
+ }, 30000);
+
+ it("resolves array exports by falling back to the first resolvable target", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "pi-array-exports-"));
+ const pkgRoot = join(dir, "pi-coding-agent");
+ writeFixturePackage(pkgRoot, {
+ manifest: {
+ name: PI_SPEC,
+ // First element is a require-only conditional (no import /
+ // node / default) — unresolvable for us; the array fallback
+ // must advance to the plain string target.
+ exports: {
+ ".": [{ require: "./index.cjs" }, "./index.js"],
+ },
+ },
+ files: { "index.js": fixtureModule("array-fallback") },
+ });
+ await withArgv1(join(pkgRoot, "index.js"), async () => {
+ clearCachedModule();
+ const api = await loadDefaultPiSessionApi();
+ expect(await api.listSessions()).toEqual(["array-fallback"]);
+ });
+ }, 30000);
+ });
+
+ describe("script entry whitelist", () => {
+ it("accepts an extensionless bin-style entry script", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "pi-extensionless-"));
+ const pkgRoot = join(dir, "pi-coding-agent");
+ writeFixturePackage(pkgRoot, {
+ files: {
+ // bin-style entry with no file extension (e.g. dist/cli)
+ cli: "// extensionless entry\n",
+ "index.js": fixtureModule("extensionless-entry"),
+ },
+ });
+ await withArgv1(join(pkgRoot, "cli"), async () => {
+ clearCachedModule();
+ const api = await loadDefaultPiSessionApi();
+ expect(await api.listSessions()).toEqual(["extensionless-entry"]);
+ });
+ }, 30000);
+
+ it("running from a .tsx source checkout loads the source entry, not stale dist", async () => {
+ const dir = mkdtempSync(join(tmpdir(), "pi-tsx-source-"));
+ const pkgRoot = join(dir, "pi-coding-agent");
+ writeFixturePackage(pkgRoot, {
+ entry: "./dist/index.jsx",
+ files: {
+ "src/cli.tsx": "// source entry (tsx/jiti)\n",
+ "src/index.tsx": fixtureModule("tsx-source-checkout"),
+ "dist/index.jsx": fixtureModule("stale-dist-output"),
+ },
+ });
+ await withArgv1(join(pkgRoot, "src", "cli.tsx"), async () => {
+ clearCachedModule();
+ const api = await loadDefaultPiSessionApi();
+ expect(await api.listSessions()).toEqual(["tsx-source-checkout"]);
+ });
+ }, 30000);
+ });
+ });
+
describe("resolution ladder mechanics", () => {
it("ladder order: first loader succeeds -> second never called", async () => {
const firstLoaderCalled = mock(() =>
@@ -124,6 +514,28 @@ describe("loadDefaultPiSessionApi", () => {
);
});
+ it("missing argv[1] names the walked entry (execPath) in the error, not undefined", async () => {
+ // Packaged-binary run: argv has no script path, so the loader falls
+ // back to process.execPath; the upward walk cannot find
+ // pi-coding-agent from there, and the error must name the path
+ // actually walked rather than interpolating `undefined`.
+ await withArgv1(undefined, async () => {
+ const loaders: ModuleLoader[] = [defaultLoaders[0]];
+ let error: Error | null = null;
+ try {
+ await loadDefaultPiSessionApi(loaders);
+ } catch (e: unknown) {
+ error = e as Error;
+ }
+ expect(error).not.toBeNull();
+ expect(error?.message).toContain(
+ "Could not locate @earendil-works/pi-coding-agent package.json from",
+ );
+ expect(error?.message).toContain(process.execPath);
+ expect(error?.message).not.toContain("from undefined");
+ });
+ });
+
it("memoization: two calls -> loaders invoked once", async () => {
const loaderCalled = mock(() =>
Promise.resolve({ SessionManager: { listAll: () => [] } }),
diff --git a/packages/pi-plugin/src/dreamer/pi-session-api.ts b/packages/pi-plugin/src/dreamer/pi-session-api.ts
index 0351e28c0..ba6c9fc3f 100644
--- a/packages/pi-plugin/src/dreamer/pi-session-api.ts
+++ b/packages/pi-plugin/src/dreamer/pi-session-api.ts
@@ -1,5 +1,13 @@
-import { readFileSync } from "node:fs";
-import { createRequire } from "node:module";
+import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
+import {
+ basename,
+ dirname,
+ extname,
+ join,
+ relative,
+ resolve,
+ sep,
+} from "node:path";
import { pathToFileURL } from "node:url";
/**
@@ -21,6 +29,191 @@ export interface PiSessionApi {
const PI_CODING_AGENT_MODULE = "@earendil-works/pi-coding-agent";
+// Script-like entries: explicit JS/TS extensions, or no extension at all
+// (bin-style entry scripts such as an extensionless `dist/cli`). Anything
+// with another extension (.json, .png, ...) is a CLI argument, not an entry.
+const SCRIPT_ENTRY_PATTERN = /\.(mjs|cjs|mts|cts|js|ts|tsx|jsx)$/i;
+const TS_ENTRY_PATTERN = /\.(mts|cts|ts|tsx)$/i;
+
+function isScriptEntry(filePath: string): boolean {
+ return SCRIPT_ENTRY_PATTERN.test(filePath) || extname(filePath) === "";
+}
+
+interface FoundPackage {
+ dir: string;
+ pkg: Record;
+}
+
+function readManifest(pkgJsonPath: string): Record | null {
+ try {
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
+ return pkg && typeof pkg === "object"
+ ? (pkg as Record)
+ : null;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * In a `bun build --compile` binary the pi-coding-agent code is embedded in the
+ * executable ($bunfs) and process.execPath IS that executable: there is no
+ * on-disk package to walk to, and process.argv[1] is a user-controlled CLI
+ * argument (e.g. `pi --print`), not an entry script. Detect this layout
+ * explicitly so the walker falls through to the next loader instead of walking
+ * up from an arbitrary user-supplied path.
+ */
+function isCompiledBunBinary(): boolean {
+ if (!process.versions.bun) {
+ return false;
+ }
+ const exe = basename(process.execPath).toLowerCase();
+ return exe !== "bun" && exe !== "bun.exe";
+}
+
+/**
+ * Determine the on-disk file the running Pi was started from, or null when
+ * there is none. The Pi binary is spawned with `process.argv[1]` pointing at
+ * the host `cli.js` (see subagent-runner resolvePiInvocation); when Pi is
+ * launched through a bin shim, argv[1] is a SYMLINK to the real cli.js
+ * (node/bun preserve the invocation path), so resolve the real path first.
+ * Only script-like files are accepted: anything else (absent argv[1], a CLI
+ * argument, a Jiti VIRTUAL module with no physical path) means there is no
+ * usable entry and the caller falls back to process.execPath.
+ */
+function resolveRunningEntry(): string | null {
+ const argv1 = process.argv[1];
+ if (argv1 && existsSync(argv1) && statSync(argv1).isFile()) {
+ const real = realpathSync(argv1);
+ if (isScriptEntry(real)) {
+ return real;
+ }
+ }
+ return null;
+}
+
+/**
+ * Walk up from `startDir` to the pi-coding-agent package root.
+ *
+ * Pi's `build:binary` copies Bun's metadata into `dist/package.json`, and that
+ * manifest ALSO carries the pi-coding-agent name. Stopping there would resolve
+ * an entry like `./dist/index.js` against `dist/`, producing
+ * `dist/dist/index.js`. Mirror Pi's own `findNodePackageDir` (dist/config.js):
+ * when a matching manifest sits in a `dist/` directory whose parent also owns
+ * a matching manifest, the parent is the package root.
+ */
+function findPackageRoot(startDir: string): FoundPackage | null {
+ let dir = startDir;
+ while (dir !== dirname(dir)) {
+ const pkg = readManifest(join(dir, "package.json"));
+ if (pkg?.name === PI_CODING_AGENT_MODULE) {
+ if (basename(dir) === "dist") {
+ const parentDir = dirname(dir);
+ const parentPkg = readManifest(join(parentDir, "package.json"));
+ if (parentPkg?.name === PI_CODING_AGENT_MODULE) {
+ return { dir: parentDir, pkg: parentPkg };
+ }
+ }
+ return { dir, pkg };
+ }
+ dir = dirname(dir);
+ }
+ return null;
+}
+
+/**
+ * Resolve a conditional/array exports target to a concrete path, mirroring
+ * Node's condition resolution for ESM imports: "import" first, then "node",
+ * then "default" ("types" is TypeScript-only and never a runtime target).
+ * Arrays are fallback lists — the first resolvable entry wins. Shapes we
+ * cannot interpret return undefined; the caller then falls through to
+ * module/main and ultimately to the bare-import loader (the designed
+ * safety valve).
+ */
+function resolveExportsTarget(target: unknown): string | undefined {
+ if (typeof target === "string") {
+ return target;
+ }
+ if (Array.isArray(target)) {
+ for (const item of target) {
+ const resolved = resolveExportsTarget(item);
+ if (resolved !== undefined) {
+ return resolved;
+ }
+ }
+ return undefined;
+ }
+ if (target && typeof target === "object") {
+ const conditions = target as Record;
+ for (const condition of ["import", "node", "default"]) {
+ if (condition in conditions) {
+ const resolved = resolveExportsTarget(conditions[condition]);
+ if (resolved !== undefined) {
+ return resolved;
+ }
+ }
+ }
+ }
+ return undefined;
+}
+
+/**
+ * Resolve the package's ESM entry from its manifest, mirroring Node's own
+ * export-target validation: the target must be relative ("./"-prefixed)
+ * and must stay inside the package root. The "./" requirement applies to
+ * EVERY manually joined target, not just exports: module/main entries are
+ * joined by hand here, so a non-prefixed "dist/index.js" must not skip the
+ * relative-path requirement. We import the file directly, bypassing the
+ * loader's built-in validation, so a broken or malicious manifest must not
+ * be able to point us at an arbitrary path.
+ */
+function resolveManifestEntry(found: FoundPackage): string {
+ const pkg = found.pkg as {
+ exports?: { "."?: unknown };
+ module?: unknown;
+ main?: unknown;
+ };
+ const entryRel =
+ resolveExportsTarget(pkg.exports?.["."]) ??
+ (typeof pkg.module === "string" ? pkg.module : undefined) ??
+ (typeof pkg.main === "string" ? pkg.main : undefined);
+ if (!entryRel) {
+ throw new Error(`No ESM entry found in ${found.dir}/package.json`);
+ }
+ if (!entryRel.startsWith("./")) {
+ throw new Error(
+ `Invalid entry "${entryRel}" in ${found.dir}/package.json: targets must start with "./"`,
+ );
+ }
+ const root = resolve(found.dir);
+ const resolved = resolve(root, entryRel);
+ if (resolved !== root && !resolved.startsWith(root + sep)) {
+ throw new Error(
+ `Invalid entry "${entryRel}" in ${found.dir}/package.json: target escapes the package root`,
+ );
+ }
+ return resolved;
+}
+
+/**
+ * Map a build-output entry (dist/index.js) back to its source counterpart
+ * (src/index.ts). Returns null when the entry does not follow the dist→src
+ * layout.
+ */
+function toSourceEntry(entryPath: string, pkgDir: string): string | null {
+ const rel = relative(pkgDir, entryPath);
+ const srcRel = rel
+ .replace(/^dist[/\\]/, "src/")
+ .replace(/\.mjs$/, ".mts")
+ .replace(/\.cjs$/, ".cts")
+ .replace(/\.jsx$/, ".tsx")
+ .replace(/\.js$/, ".ts");
+ if (srcRel === rel) {
+ return null;
+ }
+ return join(pkgDir, srcRel);
+}
+
export interface ModuleLoader {
name: string;
load: () => Promise;
@@ -28,20 +221,62 @@ export interface ModuleLoader {
export const defaultLoaders: ModuleLoader[] = [
{
- name: "Bare import",
- load: async () => await import(/* @vite-ignore */ PI_CODING_AGENT_MODULE),
- },
- {
+ // Resolve from the running Pi binary FIRST so the dreamer loads the SAME
+ // pi-coding-agent version that owns the live session format. A stale or
+ // mismatched copy in an extension tree (e.g. a host peer that was
+ // auto-installed once and never updated) can drift from the live session
+ // API and break retrospective / refresh-primers.
name: "Resolve from running Pi binary entry",
load: async () => {
- if (!process.argv[1]) {
- throw new Error("process.argv[1] is undefined");
+ if (isCompiledBunBinary()) {
+ throw new Error(
+ "Running inside a compiled Bun binary: pi-coding-agent is embedded in the executable and argv[1] is a user CLI argument, so there is no on-disk package entry to resolve",
+ );
+ }
+ // argv[1] may be absent, a non-script CLI argument, or a Jiti VIRTUAL
+ // module with no on-disk path — in those cases fall back to the
+ // interpreter binary (process.execPath) and walk from there.
+ const entry = resolveRunningEntry() ?? process.execPath;
+ if (!entry) {
+ throw new Error(
+ "Neither process.argv[1] nor process.execPath is available",
+ );
+ }
+ // pi-coding-agent ships ESM-only exports (no "require" condition), so
+ // createRequire(...).resolve("") fails with
+ // ERR_PACKAGE_PATH_NOT_EXPORTED. Walk up from the resolved entry to find
+ // the package by name and import its ESM entry directly.
+ const found = findPackageRoot(dirname(entry));
+ if (!found) {
+ // `entry` may be the execPath fallback (when argv[1] is a CLI arg,
+ // a virtual module, or absent), so the message names the path
+ // actually walked instead of interpolating `undefined`.
+ throw new Error(
+ `Could not locate ${PI_CODING_AGENT_MODULE} package.json from ${entry}`,
+ );
+ }
+ const entryPath = resolveManifestEntry(found);
+ // Source checkouts (tsx/jiti running src/cli.ts): the manifest entry
+ // points at build output under dist/, which can be STALE relative to
+ // the running sources. When the running entry is TypeScript, load the
+ // matching source file; if none exists, refuse to silently select
+ // stale build output and fall through to the next loader.
+ if (TS_ENTRY_PATTERN.test(entry)) {
+ const srcEntry = toSourceEntry(entryPath, found.dir);
+ if (srcEntry && existsSync(srcEntry)) {
+ return await import(pathToFileURL(srcEntry).href);
+ }
+ throw new Error(
+ `Pi is running from TypeScript source (${entry}) but no source counterpart of ${entryPath} exists; refusing to load possibly stale build output`,
+ );
}
- const require = createRequire(process.argv[1]);
- const resolved = require.resolve(PI_CODING_AGENT_MODULE);
- return await import(pathToFileURL(resolved).href);
+ return await import(pathToFileURL(entryPath).href);
},
},
+ {
+ name: "Bare import",
+ load: async () => await import(/* @vite-ignore */ PI_CODING_AGENT_MODULE),
+ },
];
let cachedModulePromise: Promise | null = null;