diff --git a/packages/coding-agent/src/changes.md b/packages/coding-agent/src/changes.md index 0658282025..199f57a218 100644 --- a/packages/coding-agent/src/changes.md +++ b/packages/coding-agent/src/changes.md @@ -1,5 +1,23 @@ # changes +## Anchor compiled-binary detection to exact virtual-filesystem url shapes (2026-08-30) + +### What changed + +- `packages/coding-agent/src/config.ts`: `isBunBinary` now derives from `isCompiledBunBinaryUrl()`, which matches only the exact embedded-module url shapes (`file:///$bunfs/...`, `file:///:/~BUN/...` raw or percent-encoded) instead of unrestricted substring checks. + +### Why + +- A stock `bun run` from a disk path containing a marker segment (e.g. `/tmp/$bunfs/...`) previously classified as a compiled binary, misrouting theme/package-dir resolution and, via the new compiled-host signal, mislabeling the eval js runtime badge as `native`. + +### Why an extension could not handle it + +- `isBunBinary` is core bootstrap classification consumed by config path resolution and the bundled-extension loader. + +### Expected merge conflict zones + +- LOW: `isBunBinary` definition in `packages/coding-agent/src/config.ts`. + ## Measure Cursor tool-result history at the wire representation (2026-08-29) ### What changed diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 9a05aca318..1d8160de0b 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -17,11 +17,18 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** - * Detect if we're running as a Bun compiled binary. - * Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path) + * Exact module-url shape of a module embedded in a Bun compiled binary: + * posix `file:///$bunfs/...` or the windows virtual drive + * `file:///:/~BUN/...` (raw or percent-encoded). Marker-named disk + * paths (e.g. `file:///tmp/$bunfs/...`) never match. Shapes are mirrored by + * senpi-codemode's runtime-info.ts. */ -export const isBunBinary = - import.meta.url.includes("$bunfs") || import.meta.url.includes("~BUN") || import.meta.url.includes("%7EBUN"); +export function isCompiledBunBinaryUrl(moduleUrl: string): boolean { + return moduleUrl.startsWith("file:///$bunfs/") || /^file:\/\/\/[A-Za-z]:\/(?:~BUN|%7EBUN)\//.test(moduleUrl); +} + +/** Detect if we're running as a Bun compiled binary. */ +export const isBunBinary = isCompiledBunBinaryUrl(import.meta.url); /** Detect if Bun is the runtime (compiled binary or bun run) */ export const isBunRuntime = !!process.versions.bun; diff --git a/packages/coding-agent/src/core/changes.md b/packages/coding-agent/src/core/changes.md index 34dde19825..65b597f671 100644 --- a/packages/coding-agent/src/core/changes.md +++ b/packages/coding-agent/src/core/changes.md @@ -1,5 +1,6 @@ # changes +<<<<<<< HEAD ## 2026-08-30 - Experimental workflow eval-only policy ### What changed @@ -18,6 +19,25 @@ ### Expected merge conflict zones - `agent-session.ts`: active-tool selection, tool registry refresh, reload, and system-prompt assembly. + +## 2026-08-30 - Publish compiled-binary host signal for sidecar extensions + +### What changed + +- `packages/coding-agent/src/core/resource-loader.ts`: before loading a bundled extension through `resolveBinaryFactory` in a compiled binary, the loader publishes `Symbol.for("@earendil-works/pi-coding-agent:compiled-binary-host")` on `globalThis`. + +### Why + +- Compiled binaries load bundled extensions (codemode) from the physical sidecar, whose module urls carry no bunfs marker; codemode's eval runtime badge reads this host classification to label the self-hosted runtime `native`. + +### Why an extension could not handle it + +- Only the binary loader knows it is executing the compiled-host loading path; extension code loaded from the sidecar cannot observe `isBunBinary` itself. + +### Expected merge conflict zones + +- `packages/coding-agent/src/core/resource-loader.ts` (bundled builtin extension loading) + ## 2026-08-29 - Bound Cursor serialized tool-result admissions ### What changed diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index 558960c3e0..494d927f7f 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -113,6 +113,14 @@ const VENDORED_BUILTIN_EXTENSION_PACKAGES: ReadonlyArray<{ builtinId: string; pa ]; const moduleRequire = createRequire(import.meta.url); +/** + * Published before loading bundled extensions from the physical sidecar of a + * compiled binary: sidecar module urls carry no bunfs marker, so extensions + * (codemode's eval runtime badge) read this host classification instead. + * Key convention matches the interactive theme registry. + */ +const COMPILED_BINARY_HOST_KEY = Symbol.for("@earendil-works/pi-coding-agent:compiled-binary-host"); + const bundledBuiltinExtensions: ReadonlyArray<{ id: string; resolvePackage: () => string; @@ -1389,6 +1397,7 @@ export class DefaultResourceLoader implements ResourceLoader { } try { if (isBunBinary && bundledExtension.resolveBinaryFactory) { + Reflect.set(globalThis, COMPILED_BINARY_HOST_KEY, true); const factory = await bundledExtension.resolveBinaryFactory(); const extensionPath = ``; const extension = await loadExtensionFromFactory( diff --git a/packages/coding-agent/test/config-compiled-binary-url.test.ts b/packages/coding-agent/test/config-compiled-binary-url.test.ts new file mode 100644 index 0000000000..7c538ae8bf --- /dev/null +++ b/packages/coding-agent/test/config-compiled-binary-url.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { isCompiledBunBinaryUrl } from "../src/config.ts"; + +describe("isCompiledBunBinaryUrl", () => { + it("matches the posix virtual filesystem root of a compiled binary", () => { + expect(isCompiledBunBinaryUrl("file:///$bunfs/root/pi")).toBe(true); + }); + + it("matches the windows virtual drive, raw and percent-encoded, any drive letter", () => { + expect(isCompiledBunBinaryUrl("file:///B:/~BUN/root/pi")).toBe(true); + expect(isCompiledBunBinaryUrl("file:///b:/%7EBUN/root/pi")).toBe(true); + }); + + it("never matches stock bun disk paths that merely contain a marker segment", () => { + expect(isCompiledBunBinaryUrl("file:///private/tmp/$bunfs-stock-integration/probe.ts")).toBe(false); + expect(isCompiledBunBinaryUrl("file:///tmp/$bunfs/project/config.ts")).toBe(false); + expect(isCompiledBunBinaryUrl("file:///Users/dev/~BUN/root/config.ts")).toBe(false); + expect(isCompiledBunBinaryUrl("file:///Users/dev/%7EBUN/root/config.ts")).toBe(false); + }); + + it("never matches ordinary disk urls", () => { + expect(isCompiledBunBinaryUrl("file:///Users/dev/senpi/src/config.ts")).toBe(false); + }); +}); diff --git a/packages/senpi-codemode/CHANGELOG.md b/packages/senpi-codemode/CHANGELOG.md index bc3376fdb5..4578c790c2 100644 --- a/packages/senpi-codemode/CHANGELOG.md +++ b/packages/senpi-codemode/CHANGELOG.md @@ -8,6 +8,10 @@ ### Changed +- Eval header runtime badges for the js kernel now read `native` when a compiled + standalone binary hosts the kernel itself (an omo/pi native build), e.g. + `eval js (native 1.4.0, ~/.omo/…/omo)`; stock bun and node badges and the eval + prompt host line are unchanged. - The eval prompt's dependency-graph section is now `` and states its contract directly: define the workflow spec in code, one node per logically distinct step, rather than hand-authoring the graph as a single opaque call. diff --git a/packages/senpi-codemode/README.md b/packages/senpi-codemode/README.md index 1c51395308..28cd51c98b 100644 --- a/packages/senpi-codemode/README.md +++ b/packages/senpi-codemode/README.md @@ -34,7 +34,10 @@ task-tool names are known. `eval js (node 26.7.0, /opt/…/bin/node)` — with the same `runtime` info on `EvalToolDetails` and its `cells` for RPC consumers; interpreter detection resolves absolute executable paths, and the eval prompt host line names the - JS runtime (`node`/`bun`). + JS runtime (`node`/`bun`). When a compiled standalone binary hosts the JS + kernel itself (an omo/pi native build), the badge reads `native` instead of + `bun` — `eval js (native 1.4.0, ~/.omo/…/omo)` — while the prompt host line + stays `bun` for engine capability. - JavaScript import rewriting for supported local modules and package imports in the persistent Node.js worker. - GPT models receive a terse `eval` prompt dialect that prioritizes composing diff --git a/packages/senpi-codemode/src/extension/runtime-info.ts b/packages/senpi-codemode/src/extension/runtime-info.ts index 2dd1c91985..b0001dabfa 100644 --- a/packages/senpi-codemode/src/extension/runtime-info.ts +++ b/packages/senpi-codemode/src/extension/runtime-info.ts @@ -6,19 +6,74 @@ export interface JsRuntimeVersions { readonly bun?: string | undefined; } -/** Identity of the in-process JS kernel host: bun when its marker exists, node otherwise. */ +/** + * Global-registry key the coding-agent binary loader publishes before loading + * this package from the physical sidecar of a compiled binary. Compiled hosts + * load codemode from disk, so this module's own url carries no bunfs marker; + * the host's classification is the authoritative signal. Follows the + * Symbol.for registry convention of the interactive theme. + */ +const COMPILED_HOST_KEY = Symbol.for("@earendil-works/pi-coding-agent:compiled-binary-host"); + +/** + * Exact module-url shapes of Bun's virtual filesystem in compiled binaries: + * posix `file:///$bunfs/...` and the windows virtual drive + * `file:///:/~BUN/...` (raw or percent-encoded). Anchored so stock bun + * runs from disk paths that merely contain a marker segment never match. + */ +const POSIX_VIRTUAL_URL_PREFIX = "file:///$bunfs/"; +const WINDOWS_VIRTUAL_URL_PATTERN = /^file:\/\/\/[A-Za-z]:\/(?:~BUN|%7EBUN)\//; + +export interface NativeRuntimeSignals { + readonly bunVersion?: string | undefined; + readonly moduleUrl?: string | undefined; + readonly hostCompiledBinary?: boolean | undefined; +} + +/** + * True when the bun runtime hosting this code is a compiled standalone binary + * (an omo/pi native build): the in-process JS kernel then runs inside the + * application binary itself rather than a stock bun install. + */ +export function isNativeSelfRuntime(signals: NativeRuntimeSignals = processNativeSignals()): boolean { + const bun = signals.bunVersion; + if (bun === undefined || bun.length === 0) return false; + if (signals.hostCompiledBinary === true) return true; + const moduleUrl = signals.moduleUrl ?? ""; + return moduleUrl.startsWith(POSIX_VIRTUAL_URL_PREFIX) || WINDOWS_VIRTUAL_URL_PATTERN.test(moduleUrl); +} + +function processNativeSignals(): NativeRuntimeSignals { + return { + bunVersion: process.versions.bun, + moduleUrl: import.meta.url, + hostCompiledBinary: Reflect.get(globalThis, COMPILED_HOST_KEY) === true, + }; +} + +/** + * Identity of the in-process JS kernel host: native when a compiled binary + * hosts the kernel itself, bun when its marker exists, node otherwise. + */ export function jsRuntimeInfo( versions: JsRuntimeVersions = process.versions, execPath: string = process.execPath, + nativeSelf: boolean = isNativeSelfRuntime(), ): EvalRuntimeInfo { const bun = versions.bun; - if (bun !== undefined && bun.length > 0) return { name: "bun", version: bun, path: execPath }; + if (bun !== undefined && bun.length > 0) { + return { name: nativeSelf ? "native" : "bun", version: bun, path: execPath }; + } return { name: "node", version: versions.node, path: execPath }; } -/** Short host-line segment, e.g. "node 26.7.0" or "bun 1.4.0". */ +/** + * Short host-line segment, e.g. "node 26.7.0" or "bun 1.4.0". Stays + * runtime-truthful for the eval prompt, so a native binary still reads "bun": + * the model needs the engine capability surface, not the install identity. + */ export function jsRuntimeLabel(versions: JsRuntimeVersions = process.versions): string { - const info = jsRuntimeInfo(versions, ""); + const info = jsRuntimeInfo(versions, "", false); return `${info.name} ${info.version}`; } diff --git a/packages/senpi-codemode/test/eval-render-runtime.test.ts b/packages/senpi-codemode/test/eval-render-runtime.test.ts index bf68f91796..8c64fe153e 100644 --- a/packages/senpi-codemode/test/eval-render-runtime.test.ts +++ b/packages/senpi-codemode/test/eval-render-runtime.test.ts @@ -58,6 +58,27 @@ describe("eval renderer runtime badge", () => { expect(rendered[0]).toBe("eval js (node 26.7.0) done"); }); + it("labels the js runtime as native when the kernel runs inside the compiled binary", () => { + const details: EvalToolDetails = { + language: "js", + durationMs: 0, + toolCalls: [], + truncated: false, + runtime: { name: "native", version: "1.4.0" }, + }; + + const rendered = renderLines( + renderEvalResult( + evalResult(details, "complete"), + { expanded: false, isPartial: false }, + undefined, + resultContext(undefined, false), + ), + ); + + expect(rendered[0]).toBe("eval js (native 1.4.0) done"); + }); + it("renders headers without any badge when runtime is unknown", () => { const rendered = renderLines( renderEvalResult( diff --git a/packages/senpi-codemode/test/runtime-info.test.ts b/packages/senpi-codemode/test/runtime-info.test.ts index 5fe074ffd8..92a86f891c 100644 --- a/packages/senpi-codemode/test/runtime-info.test.ts +++ b/packages/senpi-codemode/test/runtime-info.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vitest"; -import { jsRuntimeInfo, jsRuntimeLabel, runtimesFromAvailability } from "../src/extension/runtime-info.ts"; +import { + isNativeSelfRuntime, + jsRuntimeInfo, + jsRuntimeLabel, + runtimesFromAvailability, +} from "../src/extension/runtime-info.ts"; import type { InterpreterAvailability, LanguageAvailability } from "../src/interpreters/detect.ts"; const unavailable: LanguageAvailability = { enabled: false, detected: { ok: false } }; @@ -30,6 +35,85 @@ describe("jsRuntimeInfo", () => { path: "/usr/local/bin/node", }); }); + + it("reports native when the bun runtime is the compiled binary itself", () => { + expect(jsRuntimeInfo({ node: "26.7.0", bun: "1.4.0" }, "/Users/dev/.omo/binary-runtime/omo", true)).toEqual({ + name: "native", + version: "1.4.0", + path: "/Users/dev/.omo/binary-runtime/omo", + }); + }); + + it("keeps the bun name when the runtime is a stock bun install", () => { + expect(jsRuntimeInfo({ node: "26.7.0", bun: "1.4.0" }, "/opt/bun/bin/bun", false)).toEqual({ + name: "bun", + version: "1.4.0", + path: "/opt/bun/bin/bun", + }); + }); + + it("never reports native without the bun marker", () => { + expect(jsRuntimeInfo({ node: "26.7.0" }, "/usr/local/bin/node", true)).toEqual({ + name: "node", + version: "26.7.0", + path: "/usr/local/bin/node", + }); + }); +}); + +describe("isNativeSelfRuntime", () => { + it("trusts the compiled-host signal even when the module loads from a disk sidecar", () => { + expect( + isNativeSelfRuntime({ + bunVersion: "1.4.0", + hostCompiledBinary: true, + moduleUrl: "file:///Users/dev/node_modules/@code-yeongyu/senpi-codemode/src/extension/runtime-info.ts", + }), + ).toBe(true); + }); + + it("ignores the compiled-host signal without the bun marker", () => { + expect( + isNativeSelfRuntime({ bunVersion: undefined, hostCompiledBinary: true, moduleUrl: "file:///x/y.ts" }), + ).toBe(false); + }); + + it("detects the bun virtual filesystem in the module url of a compiled binary", () => { + expect(isNativeSelfRuntime({ bunVersion: "1.4.0", moduleUrl: "file:///$bunfs/root/runtime-info.ts" })).toBe(true); + }); + + it("detects windows virtual filesystem markers, raw and url-encoded", () => { + expect(isNativeSelfRuntime({ bunVersion: "1.4.0", moduleUrl: "file:///B:/~BUN/root/runtime-info.ts" })).toBe( + true, + ); + expect(isNativeSelfRuntime({ bunVersion: "1.4.0", moduleUrl: "file:///B:/%7EBUN/root/runtime-info.ts" })).toBe( + true, + ); + }); + + it("stays false for stock bun runs loading modules from disk", () => { + expect(isNativeSelfRuntime({ bunVersion: "1.4.0", moduleUrl: "file:///Users/dev/src/runtime-info.ts" })).toBe( + false, + ); + }); + + it("stays false when a stock bun disk path merely contains a marker segment", () => { + expect( + isNativeSelfRuntime({ bunVersion: "1.4.0", moduleUrl: "file:///tmp/$bunfs/project/runtime-info.ts" }), + ).toBe(false); + expect( + isNativeSelfRuntime({ bunVersion: "1.4.0", moduleUrl: "file:///Users/dev/~BUN/root/runtime-info.ts" }), + ).toBe(false); + expect( + isNativeSelfRuntime({ bunVersion: "1.4.0", moduleUrl: "file:///Users/dev/%7EBUN/root/runtime-info.ts" }), + ).toBe(false); + }); + + it("stays false under node even when the module url carries a marker", () => { + expect(isNativeSelfRuntime({ bunVersion: undefined, moduleUrl: "file:///$bunfs/root/runtime-info.ts" })).toBe( + false, + ); + }); }); describe("jsRuntimeLabel", () => { diff --git a/packages/senpi-codemode/test/runtime-label.test.ts b/packages/senpi-codemode/test/runtime-label.test.ts index abfe5719fc..17769f1504 100644 --- a/packages/senpi-codemode/test/runtime-label.test.ts +++ b/packages/senpi-codemode/test/runtime-label.test.ts @@ -18,6 +18,12 @@ describe("formatRuntimeBadge", () => { ).toBe("bun 1.4.0, /usr/local/bin/bun"); }); + it("labels the native self-hosted runtime like any js runtime", () => { + expect( + formatRuntimeBadge("js", { name: "native", version: "1.4.0", path: "/Users/dev/.omo/bin/omo" }, "/Users/dev"), + ).toBe("native 1.4.0, ~/.omo/bin/omo"); + }); + it("falls back to a version-only badge when no path is known", () => { expect(formatRuntimeBadge("py", { name: "python", version: "3.12.4" }, "/Users/dev")).toBe("3.12.4"); });