From 82008e4f38e0c6aca8a1375d6f06641ddc42bb68 Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 28 May 2026 13:00:25 +0000 Subject: [PATCH] fix(cli): restored legacy pi package root remaps Added bundled root overrides for legacy pi package imports in compiled binaries and corrected fallback resolution to use canonical @oh-my-pi specifiers. Fixes #1474 --- packages/coding-agent/CHANGELOG.md | 2 + packages/coding-agent/scripts/build-binary.ts | 19 +++--- .../extensibility/plugins/legacy-pi-compat.ts | 60 ++++++++++++------- .../legacy-pi-ai-type-remap.test.ts | 26 +++++++- 4 files changed, 77 insertions(+), 30 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 08ece2def..42d32772b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -51,6 +51,8 @@ ### Fixed +- Fixed compiled-binary legacy plugin loading for `@earendil-works/*` imports of bundled package roots such as `@earendil-works/pi-coding-agent`; compat now rewrites all bundled pi package roots to bunfs entrypoints and resolves fallback peer dependencies through the canonical `@oh-my-pi/*` specifier. + - Fixed agent yielding silently on `response.incomplete` (OpenAI Responses / Codex `stopReason: "length"`). The agent now treats output-side incompletion as a recovery case: drops the truncated/reasoning-only assistant turn, attempts context promotion to a larger model, and falls back to compaction or handoff. `AutoCompactionStartEvent.reason` and the custom-tool `auto_compaction_start.trigger` discriminator gain an `"incomplete"` value. The handoff strategy is honored for `"incomplete"` (unlike `"overflow"`, where the input is broken and handoff would hit the same wall). - Fixed `eval` tool to resize large displayed images and append dimension notes to text output - Fixed `write` tool to strip malformed or loose hashline section headers before writing file content diff --git a/packages/coding-agent/scripts/build-binary.ts b/packages/coding-agent/scripts/build-binary.ts index 64fcbed97..c92934b85 100644 --- a/packages/coding-agent/scripts/build-binary.ts +++ b/packages/coding-agent/scripts/build-binary.ts @@ -56,15 +56,16 @@ async function main(): Promise { "../stats/src/sync-worker.ts", "./src/tools/browser/tab-worker-entry.ts", "./src/eval/js/worker-entry.ts", - // Legacy pi-* extension compat shims served by `legacy-pi-compat.ts`. - // Both are reached only via the computed `TYPEBOX_SHIM_PATH` / - // `LEGACY_PI_AI_SHIM_PATH` constants (which `--compile`'s static - // analyzer cannot trace), so each shim must be listed here to land - // in bunfs alongside the workers above. The bunfs entry path is - // `--root`-relative with a `.js` extension, e.g. - // `/$bunfs/root/packages/coding-agent/src/extensibility/typebox.js`, - // which is what the `isCompiledBinary()` branch in - // `legacy-pi-compat.ts` resolves to at runtime. + // Legacy pi-* extension compat entrypoints served by + // `legacy-pi-compat.ts`. These are reached via computed bunfs paths + // (which `--compile`'s static analyzer cannot trace), so each must be + // listed here to land in bunfs at + // `/$bunfs/root/packages//.js`. + "../agent/src/index.ts", + "../natives/native/index.js", + "../tui/src/index.ts", + "../utils/src/index.ts", + "./src/index.ts", "./src/extensibility/typebox.ts", "./src/extensibility/legacy-pi-ai-shim.ts", "--outfile", diff --git a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts index 226dbf81a..4ec89508b 100644 --- a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts @@ -4,6 +4,8 @@ import * as path from "node:path"; import * as url from "node:url"; import { isCompiledBinary } from "@oh-my-pi/pi-utils"; +const IS_COMPILED_BINARY = isCompiledBinary(); + // Canonical scope for in-process pi packages. Plugins published against any of // the aliased scopes below (mariozechner's original publish, earendil-works' // fork, or the canonical @oh-my-pi scope itself) are remapped to this scope and @@ -14,10 +16,9 @@ import { isCompiledBinary } from "@oh-my-pi/pi-utils"; const CANONICAL_PI_SCOPE = "@oh-my-pi"; // Scopes that have historically been used to publish (or alias) the same set -// of internal pi-* packages. `@oh-my-pi` is intentionally included so that -// direct imports of the canonical name still flow through `Bun.resolveSync` -// against the host binary, avoiding a duplicate copy being pulled in from a -// plugin's own node_modules tree at install time. +// of internal pi-* packages. `@oh-my-pi` is intentionally included so direct +// canonical imports still pass through the same host-bundled package resolution +// path instead of pulling a duplicate copy from plugin node_modules. const PI_SCOPE_ALIASES = ["oh-my-pi", "mariozechner", "earendil-works"] as const; // Internal pi-* package basenames bundled inside the omp binary. @@ -58,19 +59,33 @@ const resolvedSpecifierFallbacks = new Map(); const TYPEBOX_SPECIFIER = "@sinclair/typebox"; const TYPEBOX_SPECIFIER_FILTER = /^@sinclair\/typebox$/; -// In-process compat shim paths. In dev `import.meta.dir` is the source folder of -// this file, so the dev branches resolve to the real `.ts` source. In compiled +// In-process compat paths. In dev `import.meta.dir` is the source folder of +// this file, so the dev branches resolve to the real source files. In compiled // binaries `import.meta.dir` collapses to `/$bunfs/root`, so the runtime cannot -// recover the source layout that way; instead, each shim file is registered as -// a `--compile` entrypoint in `scripts/build-binary.ts`, which Bun emits into -// bunfs at a deterministic `--root`-relative path with a `.js` extension. The -// literals below must stay in sync with that listing — if either path drifts, -// every legacy plugin loading the shim fails with a missing-module error in -// release builds (without affecting `bun test`/dev). -const TYPEBOX_SHIM_PATH = isCompiledBinary() - ? "/$bunfs/root/packages/coding-agent/src/extensibility/typebox.js" - : path.resolve(import.meta.dir, "../typebox.ts"); +// recover the source layout that way; instead, each computed entrypoint path +// below must be registered as a `--compile` entrypoint in +// `scripts/build-binary.ts`, which Bun emits into bunfs at a deterministic +// `--root`-relative path with a `.js` extension. If either side drifts, legacy +// plugins fail with missing-module errors in release builds. +const BUNFS_PACKAGE_ROOT = "/$bunfs/root/packages"; +type SourcePiPackageDir = "agent" | "coding-agent" | "tui" | "utils"; + +function getBundledPackageIndexPath(packageDir: SourcePiPackageDir): string { + return IS_COMPILED_BINARY + ? `${BUNFS_PACKAGE_ROOT}/${packageDir}/src/index.js` + : path.resolve(import.meta.dir, "../../../..", packageDir, "src/index.ts"); +} + +function getBundledNativesIndexPath(): string { + return IS_COMPILED_BINARY + ? `${BUNFS_PACKAGE_ROOT}/natives/native/index.js` + : path.resolve(import.meta.dir, "../../../../natives/native/index.js"); +} + +const TYPEBOX_SHIM_PATH = IS_COMPILED_BINARY + ? `${BUNFS_PACKAGE_ROOT}/coding-agent/src/extensibility/typebox.js` + : path.resolve(import.meta.dir, "../typebox.ts"); // Legacy extensions historically imported `Type` (and `Static`/`TSchema`) from // the package root of `@(scope)/pi-ai`. pi-ai 15.1.0 removed the runtime `Type` // export (see `packages/ai/CHANGELOG.md`), so the bare canonical specifier no @@ -79,11 +94,16 @@ const TYPEBOX_SHIM_PATH = isCompiledBinary() // plus the borrowed `Type` runtime from the Zod-backed TypeBox shim. Subpath // imports such as `@oh-my-pi/pi-ai/utils/oauth` continue to resolve directly // against the bundled pi-ai package. -const LEGACY_PI_AI_SHIM_PATH = isCompiledBinary() - ? "/$bunfs/root/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.js" +const LEGACY_PI_AI_SHIM_PATH = IS_COMPILED_BINARY + ? `${BUNFS_PACKAGE_ROOT}/coding-agent/src/extensibility/legacy-pi-ai-shim.js` : path.resolve(import.meta.dir, "../legacy-pi-ai-shim.ts"); const LEGACY_PI_PACKAGE_ROOT_OVERRIDES: Record = { + [`${CANONICAL_PI_SCOPE}/pi-agent-core`]: getBundledPackageIndexPath("agent"), [`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH, + [`${CANONICAL_PI_SCOPE}/pi-coding-agent`]: getBundledPackageIndexPath("coding-agent"), + [`${CANONICAL_PI_SCOPE}/pi-natives`]: getBundledNativesIndexPath(), + [`${CANONICAL_PI_SCOPE}/pi-tui`]: getBundledPackageIndexPath("tui"), + [`${CANONICAL_PI_SCOPE}/pi-utils`]: getBundledPackageIndexPath("utils"), }; let isLegacyPiSpecifierShimInstalled = false; @@ -298,11 +318,11 @@ function resolveLegacyPiSpecifier(args: { path: string; importer: string }): { p } catch { // Fallback for compiled binary mode: the bundled packages live inside // /$bunfs/root and aren't reachable by filesystem resolution. Try the - // original (pre-remap) specifier against the importing file's directory, - // which resolves to the plugin's installed peer dep. + // canonical specifier against the importing file's directory, which + // resolves to the plugin's installed @oh-my-pi peer dependency. const importerDir = path.dirname(args.importer); try { - return { path: Bun.resolveSync(args.path, importerDir) }; + return { path: Bun.resolveSync(remappedSpecifier, importerDir) }; } catch { return undefined; } diff --git a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts index e4982ae53..47c2049b3 100644 --- a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts +++ b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts @@ -1,4 +1,4 @@ -import { afterAll, describe, expect, it } from "bun:test"; +import { afterAll, afterEach, describe, expect, it, vi } from "bun:test"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; @@ -17,6 +17,10 @@ installLegacyPiSpecifierShim(); const tempRoots: string[] = []; +afterEach(() => { + vi.restoreAllMocks(); +}); + afterAll(async () => { for (const dir of tempRoots) { await fs.rm(dir, { recursive: true, force: true }); @@ -95,3 +99,23 @@ describe("legacy-pi @(scope)/pi-ai root `Type` remap (issue #1437)", () => { expect(typeof loaded.fn).toBe("function"); }); }); + +describe("legacy pi package root remaps (issue #1474)", () => { + it("loads @earendil-works/pi-coding-agent root imports when host package resolution is unavailable", async () => { + const realResolveSync = Bun.resolveSync.bind(Bun); + vi.spyOn(Bun, "resolveSync").mockImplementation((specifier: string, from: string) => { + if (specifier === "@oh-my-pi/pi-coding-agent" && from.endsWith(path.join("src", "extensibility", "plugins"))) { + throw new Error("compiled binary host package resolution unavailable"); + } + return realResolveSync(specifier, from); + }); + const entry = await writeFixtureExtension( + ['import { VERSION } from "@earendil-works/pi-coding-agent";', "export const loadedVersion = VERSION;"].join( + "\n", + ), + ); + + const loaded = (await loadLegacyPiModule(entry)) as { loadedVersion: string }; + expect(loaded.loadedVersion).toMatch(/^\d+\.\d+\.\d+/); + }); +});