diff --git a/AGENTS.md b/AGENTS.md index 748cc8b21..14af3d4f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,16 +29,17 @@ This repo contains multiple packages, but **`packages/coding-agent/`** is the pr - **Class privacy**: use ES `#private` fields; leave externally accessible members bare. **No `private`/`protected`/`public` keyword on fields or methods**, except on **constructor parameter properties** where TypeScript requires it (e.g. `constructor(private readonly session: ToolSession)`). - **Promises**: use `Promise.withResolvers()` instead of `new Promise((resolve, reject) => ...)`. - **Prompts**: never build prompts in code (no inline strings, template literals, or concatenation). Prompts live in static `.md` files; use Handlebars for dynamic content. Import them via `import content from "./prompt.md" with { type: "text" }` — not `readFile`. -- **Worker scripts**: spawn workers with the dev/compile-safe hybrid pattern. `with { type: "file" }` only copies the entry as a raw asset and does **not** bundle its imports — workers crashed silently in compiled binaries on every prior incarnation of that pattern (issues #1011, #1027). Use this shape instead: +- **Worker scripts**: workers re-enter the CLI entrypoint; never spawn separate worker entry modules. `cli.ts` declares itself as the worker host at startup (`declareWorkerHostEntry()` from `@oh-my-pi/pi-utils/env`) and dispatches hidden argv selectors (`__omp_stats_sync_worker`, `__omp_tab_worker`, `__omp_js_eval_worker`, `--tiny-worker`) before loading the command registry. Spawn sites use: ```ts - import { isCompiledBinary } from "@oh-my-pi/pi-utils"; - const worker = isCompiledBinary() - ? new Worker("./packages//src/.ts", { type: "module" }) + import { workerHostEntry } from "@oh-my-pi/pi-utils"; + const hostEntry = workerHostEntry(); + const worker = hostEntry + ? new Worker(hostEntry, { type: "module", argv: ["__omp__worker"] }) : new Worker(new URL("./.ts", import.meta.url).href, { type: "module" }); ``` - The literal in the compiled branch is what Bun's `--compile` static analyzer needs to discover the worker — its path is **`--root`-relative** (repo root, since `build-binary.ts` passes `--root ../..`), so it must start with `./packages/...`. The `new URL` form in the dev branch keeps spawns portable across cwds. - In addition, every worker entry **MUST** be listed as an extra `--compile` entrypoint in `packages/coding-agent/scripts/build-binary.ts`. Without that the analyzer sees the literal but the worker never gets emitted into bunfs. The three current entries (`sync-worker.ts`, `tab-worker-entry.ts`, `worker-entry.ts`) live there as the working reference. - Validate any new worker with the dedicated smoke probe: `omp --smoke-test` spawns the stats sync worker, pings it, and exits — it's wired into `ci:test:smoke` and `scripts/install-tests/run-ci.sh` so binary, source-link, and tarball installs all exercise it. Add a sibling smoke if the new worker is on a different module graph. + When the process was started from the omp CLI — source `cli.ts`, npm-bundle `dist/cli.js`, or compiled binary — `workerHostEntry()` is `Bun.main` and the worker re-enters the single entry module, so no per-worker `--compile` entrypoints or bundle entries exist. Outside a CLI host (`bun test`, SDK embedding, standalone `omp-stats`) it returns `null` and the direct-module fallback loads the worker source. New worker kinds MUST add their selector to the dispatch table in `cli.ts` and keep the fallback branch. + History: `with { type: "file" }` only copied the entry as a raw asset (workers crashed silently in compiled binaries — issues #1011, #1027), and the later literal-path + extra-entrypoint pattern required keeping spawn literals and two build scripts in sync (issue #1150). The repro tests for those issues now pin the worker-host contract instead. + Validate any new worker with the dedicated smoke probe: `omp --smoke-test` spawns the stats sync worker and the tiny-model subprocess, pings them, and exits — it's wired into `ci:test:smoke` and `scripts/install-tests/run-ci.sh` so binary, source-link, and tarball installs all exercise it. Add a sibling smoke if the new worker is on a different module graph. ## Bun Over Node diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e2fdcdb4c..544136aed 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,12 +6,15 @@ - New `omp usage` command: a detailed per-account breakdown of provider usage limits (bars, windows, reset times, plan metadata) covering every stored credential — accounts with no usage endpoint are listed as "no usage data" rows. Each provider section ends with per-window capacity stats ("need: 5h → 3 of 5 accounts"). Flags: `--provider` to filter, `--json` for the broker-shaped report payload, and `--redact` to mask account emails/ids down to a two-char anchor plus a minimal middle-out differentiator (`ca*9*`) for screenshot-safe sharing. - Startup hangs are now self-diagnosing (speculative fix for the "zero output, hangs even on `omp -h`" report class): a watchdog prints a stderr line every 10s naming the deepest in-flight startup phase (via `logger.openSpanPath()`) until a mode runner takes over, pausing around legitimate interactive waits (fork/move prompts, the `--resume` session picker); `PI_DEBUG_STARTUP` is restored as streaming synchronous `[startup]` phase markers covering command-module imports and the native addon load, which the post-startup `PI_TIMING` tree structurally cannot show for a hang; and waiting on piped-stdin EOF announces itself after 1s instead of blocking silently. +- npm installs now execute a prebundled single-file entry: `bin.omp` points at `dist/cli.js` (built by `scripts/bundle-dist.ts` during `prepack`, ~18MB minified, natives/transformers/mupdf external), cutting npm-install cold start by roughly 3x versus transpiling the raw TypeScript graph per launch; `src/**` stays published for SDK consumers and worker fallbacks ### Changed - Cached custom model alias maps and built them lazily on first custom model reference lookup, avoiding unnecessary startup model-registry initialization - Cached resolved auth broker configuration and snapshot reads for the process lifetime so repeated startup paths reuse the same `OMP_AUTH_BROKER_*` resolution instead of re-running config/token discovery - Reused task-agent discovery results for repeated `TaskTool.create` calls in the same working directory to avoid repeated plugin scans during subagent startup +- Worker threads (stats sync, browser tab, JS eval) and the tiny-model subprocess now re-enter the CLI entrypoint with hidden argv selectors (`__omp_*`, `--tiny-worker`) via the declared worker-host entry (`workerHostEntry()`), collapsing the per-distribution spawn branches; outside a CLI host (bun test, SDK embedding) spawn sites fall back to loading the worker module directly, and both binary build scripts dropped their per-worker `--compile` entrypoint lists +- The CLI entry no longer top-level-awaits `runCli` — the floating call reports rejections to stderr and exits 1, keeping the entry module CJS-lowerable and the bundle parse-friendly - Tightened the system prompt and tool prompts: deduped restated warnings (bash "catch yourself" list, search/find shell-fallback recaps, read instruction/critical overlap, the AST metavariable primer duplicated across both ast tool descriptions), factored the repeated repo-default clause in the `gh` search ops, dropped a dead `rsed` reference and an internal `tool-timeouts.ts` pointer, and pruned internal mechanism the agent can't act on (screenshot temp-file/downscaling pipeline, browser spawn lifecycle, `gh` "replaces former op" history and run-watch grace period, output-minimizer heuristics, BM25 ranking name, `task.maxConcurrency` pointer) - Extended the prompt-efficiency pass to the full prompt surface (subagent/plan-mode/notice/title/commit system prompts, agent definitions, goals, memories, review and autoresearch prompts): RFC-keyed prescriptive prose, fixed garbled grammar and a stale `` placeholder in the plan-approval reminder, deduped intra-file restatements, and corrected the `todo` op table's claim that `rm` requires a `task`/`phase` (bare `rm` clears the whole list) - Replace tool prompt no longer recommends `sed -i`/`cat`-heredoc commands that the bash interceptor blocks; its bash-alternatives table now only lists non-intercepted commands diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index bbb30e6ee..ab8715664 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -28,7 +28,7 @@ "main": "./src/index.ts", "types": "./src/index.ts", "bin": { - "omp": "src/cli.ts" + "omp": "dist/cli.js" }, "scripts": { "build": "bun scripts/build-binary.ts", @@ -40,7 +40,7 @@ "fmt": "biome format --write . && bun run format-prompts", "format-prompts": "bun scripts/format-prompts.ts", "generate-docs-index": "bun scripts/generate-docs-index.ts", - "prepack": "bun scripts/generate-docs-index.ts", + "prepack": "bun scripts/generate-docs-index.ts && bun scripts/bundle-dist.ts", "generate-template": "bun scripts/generate-template.ts" }, "dependencies": { @@ -87,6 +87,8 @@ }, "files": [ "src", + "dist/cli.js", + "dist/*.node", "scripts", "examples", "README.md", diff --git a/packages/coding-agent/scripts/build-binary.ts b/packages/coding-agent/scripts/build-binary.ts index 97376a758..d5ffb9d8d 100644 --- a/packages/coding-agent/scripts/build-binary.ts +++ b/packages/coding-agent/scripts/build-binary.ts @@ -4,6 +4,7 @@ import { createRequire } from "node:module"; import * as path from "node:path"; const packageDir = path.join(import.meta.dir, ".."); +const repoRoot = path.join(packageDir, "..", ".."); const outputPath = path.join(packageDir, "dist", "omp"); // Transformers.js is an optional, native-heavy dependency that is never bundled @@ -19,9 +20,13 @@ function shouldAdhocSignDarwinBinary(): boolean { return process.platform === "darwin"; } -async function runCommand(command: string[], env: NodeJS.ProcessEnv = Bun.env): Promise { +async function runCommand( + command: string[], + env: NodeJS.ProcessEnv = Bun.env, + cwd: string = packageDir, +): Promise { const proc = Bun.spawn(command, { - cwd: packageDir, + cwd, env, stdout: "inherit", stderr: "inherit", @@ -55,19 +60,8 @@ async function main(): Promise { "--external", "mupdf", "--root", - "../..", - "./src/cli.ts", - // Worker entrypoints. Bun's `--compile` discovers the literal in - // `new Worker("…", …)` at each spawn site, but only actually - // emits the worker into the bunfs root when it is listed here as - // an explicit additional entry. Paths are relative to this - // script's cwd (packages/coding-agent) and the `--root` above - // (../..) makes them appear inside the binary at - // `/$bunfs/root/packages//src/.js`, which is - // exactly what the literals at the spawn sites resolve to. - "../stats/src/sync-worker.ts", - "./src/tools/browser/tab-worker-entry.ts", - "./src/eval/js/worker-entry.ts", + ".", + "./packages/coding-agent/src/cli.ts", // 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 @@ -77,17 +71,18 @@ async function main(): Promise { // breaks the CLI entry when the same package's barrel appears as an // extra entrypoint (issue #1474), so legacy `pi-coding-agent` imports // resolve through `legacy-pi-coding-agent-shim.ts` instead. - "../agent/src/index.ts", - "../natives/native/index.js", - "../tui/src/index.ts", - "../utils/src/index.ts", - "./src/extensibility/typebox.ts", - "./src/extensibility/legacy-pi-ai-shim.ts", - "./src/extensibility/legacy-pi-coding-agent-shim.ts", + "./packages/agent/src/index.ts", + "./packages/natives/native/index.js", + "./packages/tui/src/index.ts", + "./packages/utils/src/index.ts", + "./packages/coding-agent/src/extensibility/typebox.ts", + "./packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts", + "./packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts", "--outfile", - "dist/omp", + "packages/coding-agent/dist/omp", ], buildEnv, + repoRoot, ); // Bun 1.3.12 emits a truncated Mach-O signature on darwin builds. diff --git a/packages/coding-agent/scripts/bundle-dist.ts b/packages/coding-agent/scripts/bundle-dist.ts new file mode 100755 index 000000000..ad835aeb9 --- /dev/null +++ b/packages/coding-agent/scripts/bundle-dist.ts @@ -0,0 +1,81 @@ +#!/usr/bin/env bun + +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { isEnoent } from "@oh-my-pi/pi-utils"; + +const packageDir = path.join(import.meta.dir, ".."); +const outDir = path.join(packageDir, "dist"); +const cliPath = path.join(outDir, "cli.js"); +const shebang = "#!/usr/bin/env bun\n"; + +async function runCommand(command: string[]): Promise { + const proc = Bun.spawn(command, { + cwd: packageDir, + stdout: "inherit", + stderr: "inherit", + }); + const exitCode = await proc.exited; + if (exitCode !== 0) throw new Error(`Command failed with exit code ${exitCode}: ${command.join(" ")}`); +} + +async function ensureShebang(): Promise { + const text = await Bun.file(cliPath).text(); + if (text.startsWith(shebang)) return; + const withoutExisting = text.startsWith("#!") ? text.slice(text.indexOf("\n") + 1) : text; + await Bun.write(cliPath, shebang + withoutExisting); +} + +function formatBytes(bytes: number): string { + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; + return `${(bytes / (1024 * 1024)).toFixed(2)}MB`; +} + +async function cleanBundleOutputs(): Promise { + // dist/ is shared with the dev binary (dist/omp); only remove this + // script's own outputs (entry bundle + copied native assets). + let entries: string[]; + try { + entries = await fs.readdir(outDir); + } catch (err) { + if (isEnoent(err)) return; + throw err; + } + await Promise.all( + entries + .filter(entry => entry === "cli.js" || entry.endsWith(".node") || entry.endsWith(".js.map")) + .map(entry => fs.rm(path.join(outDir, entry), { force: true })), + ); +} + +async function main(): Promise { + const start = Bun.nanoseconds(); + await cleanBundleOutputs(); + await runCommand([ + "bun", + "build", + "--target=bun", + "--outdir", + "dist", + "--minify-whitespace", + "--minify-syntax", + "--keep-names", + "--external", + "mupdf", + "--external", + "@oh-my-pi/pi-natives", + "--external", + "@huggingface/transformers", + "--define", + 'process.env.PI_BUNDLED="true"', + "./src/cli.ts", + ]); + await ensureShebang(); + const stat = await fs.stat(cliPath); + const elapsedMs = (Bun.nanoseconds() - start) / 1_000_000; + process.stdout.write( + `Bundled coding-agent CLI to dist/cli.js (${formatBytes(stat.size)}) in ${elapsedMs.toFixed(0)}ms\n`, + ); +} + +await main(); diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 1d7f06218..6c9992523 100755 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -14,9 +14,9 @@ try { * CLI entry point — registers all commands explicitly and delegates to the * lightweight CLI runner from pi-utils. */ -import { type CliConfig, run } from "@oh-my-pi/pi-utils/cli"; +import type { CliConfig } from "@oh-my-pi/pi-utils/cli"; import { APP_NAME, MIN_BUN_VERSION, VERSION } from "@oh-my-pi/pi-utils/dirs"; -import { commands, isSubcommand } from "./cli-commands"; +import { declareWorkerHostEntry } from "@oh-my-pi/pi-utils/env"; if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) { process.stderr.write( @@ -27,6 +27,12 @@ if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) { process.title = APP_NAME; +// Declare this module as the worker-host entry: Worker threads and worker +// subprocesses re-enter `Bun.main` with a hidden argv selector instead of +// loading separate worker entrypoints (single-entry contract across source, +// npm bundle, and compiled binary). +declareWorkerHostEntry(); + async function showHelp(config: CliConfig): Promise { const { renderRootHelp } = await import("@oh-my-pi/pi-utils/cli"); const { getExtraHelpText } = await import("./cli/args"); @@ -54,6 +60,45 @@ async function runSmokeTest(): Promise { process.stdout.write("smoke-test: ok\n"); } +const TINY_WORKER_ARGS = new Set(["--tiny-worker", "__tiny_worker"]); +const STATS_SYNC_WORKER_ARG = "__omp_stats_sync_worker"; +const TAB_WORKER_ARG = "__omp_tab_worker"; +const JS_EVAL_WORKER_ARG = "__omp_js_eval_worker"; + +async function runWorkerEntrypoint(arg: string | undefined): Promise { + if (arg === STATS_SYNC_WORKER_ARG) { + // The sync worker handles messages via `self.onmessage`, assigned during + // this *async* dynamic import. Bun flushes the worker's initial message + // buffer when the entry module's top-level evaluation finishes — before + // this dispatch completes — so anything the parent posted right after + // spawning (the smoke ping, the first parse request) would be dropped. + // Park early events and replay them once the module's handler is live. + // (The tab/eval workers are immune: `parentPort.on("message")` queues + // until a listener attaches.) + const scope = globalThis as unknown as { onmessage: ((event: MessageEvent) => void) | null }; + const pending: MessageEvent[] = []; + const buffer = (event: MessageEvent): void => { + pending.push(event); + }; + scope.onmessage = buffer; + await import("@oh-my-pi/omp-stats/sync-worker"); + const handler = scope.onmessage; + if (handler && handler !== buffer) { + for (const event of pending) handler.call(scope, event); + } + return true; + } + if (arg === TAB_WORKER_ARG) { + await import("./tools/browser/tab-worker-entry"); + return true; + } + if (arg === JS_EVAL_WORKER_ARG) { + await import("./eval/js/worker-entry"); + return true; + } + return false; +} + /** * Hidden subcommand that boots the tiny-model worker inside this process * over the parent's IPC channel. The agent's main process spawns the same @@ -90,11 +135,16 @@ async function runTinyWorker(): Promise { }; }, }); + const keepalive = setInterval(() => {}, 2 ** 30); // Parent went away (crashed, SIGKILL, etc.) — commit suicide so we don't // linger as an orphan. SIGKILL via `process.kill` keeps us symmetrical // with the parent's hard-kill on shutdown: skip every JS/native finalizer. process.on("disconnect", () => shutdown()); - await shuttingDown; + try { + await shuttingDown; + } finally { + clearInterval(keepalive); + } process.kill(process.pid, "SIGKILL"); } @@ -104,10 +154,17 @@ export async function runCli(argv: string[]): Promise { await runSmokeTest(); return; } - if (argv[0] === "--tiny-worker") { + if (TINY_WORKER_ARGS.has(argv[0] ?? "")) { await runTinyWorker(); return; } + if (await runWorkerEntrypoint(argv[0])) { + return; + } + const [{ run }, { commands, isSubcommand }] = await Promise.all([ + import("@oh-my-pi/pi-utils/cli"), + import("./cli-commands"), + ]); // --help and --version are handled by run() directly, don't rewrite those. // Everything else that isn't a known subcommand routes to "launch". const first = argv[0]; @@ -120,4 +177,11 @@ export async function runCli(argv: string[]): Promise { return run({ bin: APP_NAME, version: VERSION, argv: runArgv, commands, help: showHelp }); } -await runCli(process.argv.slice(2)); +// Floating call instead of top-level await: TLA forces `--bytecode` (CJS +// lowering) builds to fail, and the entrypoint needs nothing after this. +// The catch mirrors what an unhandled TLA rejection produced: error dump to +// stderr, exit code 1. Success paths resolve without touching the exit code. +runCli(process.argv.slice(2)).catch((err: unknown) => { + process.stderr.write(`${Bun.inspect(err, { colors: process.stderr.isTTY === true })}\n`); + process.exit(1); +}); diff --git a/packages/coding-agent/src/eval/js/context-manager.ts b/packages/coding-agent/src/eval/js/context-manager.ts index 8a7679935..e6f7b46e0 100644 --- a/packages/coding-agent/src/eval/js/context-manager.ts +++ b/packages/coding-agent/src/eval/js/context-manager.ts @@ -1,13 +1,10 @@ -import { isCompiledBinary, logger, Snowflake } from "@oh-my-pi/pi-utils"; +import { logger, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils"; import type { ToolSession } from "../../tools"; import { ToolAbortError, ToolError } from "../../tools/tool-errors"; import { callSessionTool, type JsStatusEvent } from "./tool-bridge"; import { WorkerCore } from "./worker-core"; -// Worker entry. See `tab-supervisor.ts` for the rationale behind the -// literal-string + `new URL(import.meta.url)` hybrid: the literal is what -// Bun's `--compile` bundler discovers, the `new URL` form is what makes dev -// runs portable across cwds. The worker is registered as an additional -// `--compile` entrypoint in `scripts/build-binary.ts`. +// Coding-agent binary/bundle workers route through the CLI entrypoint with a +// hidden argv mode, so compiled/npm builds only need one JavaScript entry. import type { JsDisplayOutput, RunErrorPayload, @@ -384,8 +381,9 @@ async function raceWithTimeout(promise: Promise, timeoutMs: number, reason async function spawnJsWorker(): Promise { try { - const worker = isCompiledBinary() - ? new Worker("./packages/coding-agent/src/eval/js/worker-entry.ts", { type: "module" }) + const hostEntry = workerHostEntry(); + const worker = hostEntry + ? new Worker(hostEntry, { type: "module", argv: ["__omp_js_eval_worker"] }) : new Worker(new URL("./worker-entry.ts", import.meta.url).href, { type: "module" }); return wrapBunWorker(worker); } catch (err) { 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 31c6da921..44b4bcd1f 100644 --- a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts @@ -110,9 +110,26 @@ function bunfsPath(...segments: string[]): string { return path.join(BUNFS_PACKAGE_ROOT, ...segments); } +function resolveBundledSelfPackageRoot(): string | undefined { + if (!process.env.PI_BUNDLED) return undefined; + try { + return path.dirname(Bun.resolveSync("@oh-my-pi/pi-coding-agent/package.json", import.meta.dir)); + } catch { + return undefined; + } +} + +const BUNDLED_SELF_PACKAGE_ROOT = resolveBundledSelfPackageRoot(); + +function sourceShimPath(file: string): string { + return BUNDLED_SELF_PACKAGE_ROOT + ? path.join(BUNDLED_SELF_PACKAGE_ROOT, "src", "extensibility", file) + : path.resolve(import.meta.dir, "..", file); +} + const TYPEBOX_SHIM_PATH = BUNFS_PACKAGE_ROOT ? bunfsPath("coding-agent", "src", "extensibility", "typebox.js") - : path.resolve(import.meta.dir, "../typebox.ts"); + : sourceShimPath("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` @@ -124,7 +141,7 @@ const TYPEBOX_SHIM_PATH = BUNFS_PACKAGE_ROOT // against the bundled pi-ai package. const LEGACY_PI_AI_SHIM_PATH = BUNFS_PACKAGE_ROOT ? bunfsPath("coding-agent", "src", "extensibility", "legacy-pi-ai-shim.js") - : path.resolve(import.meta.dir, "../legacy-pi-ai-shim.ts"); + : sourceShimPath("legacy-pi-ai-shim.ts"); // The coding-agent's own `./src/index.ts` cannot be listed as an extra // `bun --compile` entrypoint alongside the CLI entry without breaking binary @@ -133,7 +150,7 @@ const LEGACY_PI_AI_SHIM_PATH = BUNFS_PACKAGE_ROOT // avoids that collision while re-exporting the canonical package surface. const LEGACY_PI_CODING_AGENT_SHIM_PATH = BUNFS_PACKAGE_ROOT ? bunfsPath("coding-agent", "src", "extensibility", "legacy-pi-coding-agent-shim.js") - : path.resolve(import.meta.dir, "../legacy-pi-coding-agent-shim.ts"); + : sourceShimPath("legacy-pi-coding-agent-shim.ts"); // Package-root overrides. Shim entries are always applied because they replace // (or augment) the canonical surface even in non-compiled installs. The bunfs diff --git a/packages/coding-agent/src/tiny/title-client.ts b/packages/coding-agent/src/tiny/title-client.ts index d5479147c..0ffe83185 100644 --- a/packages/coding-agent/src/tiny/title-client.ts +++ b/packages/coding-agent/src/tiny/title-client.ts @@ -1,5 +1,5 @@ import * as path from "node:path"; -import { $env, isCompiledBinary, logger } from "@oh-my-pi/pi-utils"; +import { $env, isBunTestRuntime, isCompiledBinary, logger, workerHostEntry } from "@oh-my-pi/pi-utils"; import type { Subprocess } from "bun"; import { settings } from "../config/settings"; import { tinyModelDeviceSettingToEnv } from "./device"; @@ -108,17 +108,28 @@ function tinyWorkerEnv(): Record { for (const key in overlay) merged[key] = overlay[key]; return merged; } +interface TinyWorkerSpawnCommand { + cmd: string[]; + cwd?: string; +} /** - * Resolve the argv used to relaunch the agent CLI into tiny-worker mode. In a - * compiled binary the entry point is the binary itself; in dev/source the - * spawned `bun` needs the absolute path to `cli.ts` so it can resolve module - * imports against the on-disk source tree. + * Resolve the command used to relaunch the agent CLI into tiny-worker mode. + * In a compiled binary the entry point is the binary itself (no script arg). + * Otherwise re-enter the declared worker-host entry (source cli.ts or + * npm-bundle cli.js) with a cwd-relative script path — Bun's subprocess IPC + * is more reliable that way than with an absolute `.ts` entry under + * `bun test` — and fall back to this package's own `src/cli.ts` when no host + * entry is declared (bun test, SDK embedding). */ -function tinyWorkerSpawnCmd(): string[] { - if (isCompiledBinary()) return [process.execPath, TINY_WORKER_ARG]; - const cliPath = path.resolve(import.meta.dir, "..", "cli.ts"); - return [process.execPath, cliPath, TINY_WORKER_ARG]; +function tinyWorkerSpawnCmd(): TinyWorkerSpawnCommand { + if (isCompiledBinary()) return { cmd: [process.execPath, TINY_WORKER_ARG] }; + const hostEntry = workerHostEntry(); + if (hostEntry) { + return { cmd: [process.execPath, path.basename(hostEntry), TINY_WORKER_ARG], cwd: path.dirname(hostEntry) }; + } + const packageRoot = path.resolve(import.meta.dir, "..", ".."); + return { cmd: [process.execPath, "src/cli.ts", TINY_WORKER_ARG], cwd: packageRoot }; } interface SpawnedSubprocess { @@ -143,8 +154,10 @@ export function createTinyTitleSubprocess(): SpawnedSubprocess { const inbound = new Set<(message: TinyTitleWorkerOutbound) => void>(); const errors = new Set<(error: Error) => void>(); const intentionalExit = { value: false }; + const spawnCommand = tinyWorkerSpawnCmd(); const proc = Bun.spawn({ - cmd: tinyWorkerSpawnCmd(), + cmd: spawnCommand.cmd, + cwd: spawnCommand.cwd, env: tinyWorkerEnv(), stdin: "ignore", stdout: "ignore", @@ -175,7 +188,9 @@ export function createTinyTitleSubprocess(): SpawnedSubprocess { }); // Don't keep the parent event loop alive on account of an idle worker; the // agent dispose path calls `terminate()` explicitly when shutting down. - proc.unref(); + // Bun's test runner can starve IPC delivery for unref'd subprocesses, so + // keep it referenced only under tests that assert the ping/pong contract. + if (!isBunTestRuntime()) proc.unref(); return { proc, inbound, errors, intentionalExit }; } diff --git a/packages/coding-agent/src/tools/browser/tab-supervisor.ts b/packages/coding-agent/src/tools/browser/tab-supervisor.ts index b06649b43..86880b04f 100644 --- a/packages/coding-agent/src/tools/browser/tab-supervisor.ts +++ b/packages/coding-agent/src/tools/browser/tab-supervisor.ts @@ -1,4 +1,4 @@ -import { getPuppeteerDir, isCompiledBinary, logger, Snowflake } from "@oh-my-pi/pi-utils"; +import { getPuppeteerDir, logger, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils"; import type { Page, Target } from "puppeteer-core"; import { callSessionTool } from "../../eval/js/tool-bridge"; import type { ToolSession } from "../../sdk"; @@ -18,14 +18,8 @@ import type { WorkerOutbound, } from "./tab-protocol"; -// Worker entry. The literal string in `new Worker("./packages/coding-agent/src/tools/browser/tab-worker-entry.ts", …)` -// below is what Bun's `--compile` static analyzer needs to bundle the worker -// (registered as an additional entrypoint in `scripts/build-binary.ts`); in -// dev we resolve the same source via `import.meta.url`. Replaces the older -// `with { type: "file" }` pattern, which only copied the entry as a raw -// asset and could not resolve the worker's relative imports inside a -// compiled binary (issue #1011 was a false-positive fix — the regression -// test only checked emission, not actual worker startup). +// Coding-agent binary/bundle workers route through the CLI entrypoint with a +// hidden argv mode, so compiled/npm builds only need one JavaScript entry. interface WorkerHandle { send(msg: WorkerInbound, transferList?: Transferable[]): void; @@ -518,8 +512,9 @@ async function raceWithTimeout( async function spawnTabWorker(): Promise { try { - const worker = isCompiledBinary() - ? new Worker("./packages/coding-agent/src/tools/browser/tab-worker-entry.ts", { type: "module" }) + const hostEntry = workerHostEntry(); + const worker = hostEntry + ? new Worker(hostEntry, { type: "module", argv: ["__omp_tab_worker"] }) : new Worker(new URL("./tab-worker-entry.ts", import.meta.url).href, { type: "module" }); return wrapBunWorker(worker); } catch (err) { diff --git a/packages/coding-agent/test/issue-1011-repro.test.ts b/packages/coding-agent/test/issue-1011-repro.test.ts index 17821acff..7f11f3ed4 100644 --- a/packages/coding-agent/test/issue-1011-repro.test.ts +++ b/packages/coding-agent/test/issue-1011-repro.test.ts @@ -16,63 +16,35 @@ import * as path from "node:path"; * relative imports inside the compiled binary, so the worker still failed * to load (issue #1027 was the same root cause, retriggered). * - * The working pattern documented in AGENTS.md is a two-part contract: - * - * 1. `spawnTabWorker` branches on `isCompiledBinary()` and uses a literal - * string path under `--compile`. Bun's `--compile` analyzer discovers - * that literal at the `new Worker("...", ...)` call site. The path is - * `--root`-relative (`./packages/coding-agent/src/...`) because the - * build script passes `--root ../..`. - * 2. `scripts/build-binary.ts` lists the worker as an explicit additional - * `--compile` entrypoint. Without this, Bun sees the literal at the - * spawn site but never emits the worker module into bunfs. - * - * Either half alone is insufficient — both must agree on the exact path. - * Runtime end-to-end coverage lives in `omp --smoke-test` (via the stats - * sync worker). This test is the cheap static contract that catches an - * accidental regression of either half in code review / CI. + * The current contract is simpler: when the process was started from the omp + * CLI (source, npm bundle, or compiled binary), spawn sites re-enter the + * declared worker-host entry — `new Worker(workerHostEntry(), { argv })` — and + * the CLI dispatches the hidden argv selector. Outside a CLI host (bun test, + * SDK embedding) they load the worker module directly. No separate worker + * module is ever bundled or listed as a `--compile` entrypoint. */ -describe("issue #1011 — tab worker entry must survive `bun build --compile`", () => { +describe("issue #1011 — tab worker must re-enter the CLI entrypoint", () => { const packageDir = path.resolve(import.meta.dir, ".."); const supervisorPath = path.join(packageDir, "src/tools/browser/tab-supervisor.ts"); const buildBinaryPath = path.join(packageDir, "scripts/build-binary.ts"); - // `--root` is `../..` from packages/coding-agent, so the literal that - // matches at runtime inside the compiled bunfs is repo-relative. - const compiledLiteral = "./packages/coding-agent/src/tools/browser/tab-worker-entry.ts"; - // The build script's cwd is packages/coding-agent, so its entrypoint - // path is package-relative. - const buildEntrypoint = "./src/tools/browser/tab-worker-entry.ts"; + const workerArg = "__omp_tab_worker"; - it("tab-supervisor uses the isCompiledBinary() hybrid spawn pattern with a static literal", async () => { + it("tab-supervisor re-enters the worker-host entry with the argv selector", async () => { const source = await Bun.file(supervisorPath).text(); - // The exact literal at the `new Worker(...)` call must be present - // and discoverable to Bun's `--compile` static analyzer. expect( - source.includes(`new Worker("${compiledLiteral}"`), - `tab-supervisor.ts must spawn the worker with the literal "${compiledLiteral}" so Bun's --compile analyzer can embed it`, + source.includes("workerHostEntry()"), + "tab-supervisor.ts must spawn via the declared worker-host entry", ).toBe(true); - - // And the dev-mode branch should keep the portable import.meta.url - // form so spawns work outside of compiled binaries too. + expect(source).toContain(`argv: ["${workerArg}"]`); expect( - /new Worker\(\s*new URL\("\.\/tab-worker-entry\.ts",\s*import\.meta\.url\)/.test(source), - "tab-supervisor.ts must keep a `new URL('./tab-worker-entry.ts', import.meta.url)` branch for dev/source spawns", - ).toBe(true); - - // And the branching must come from `isCompiledBinary()` — not, say, - // a hard-coded check, an env var, or a renamed helper. - expect( - source.includes("isCompiledBinary()"), - "tab-supervisor.ts must select the spawn pattern via isCompiledBinary()", + source.includes('new URL("./tab-worker-entry.ts", import.meta.url)'), + "tab-supervisor.ts must keep the direct-module fallback for non-CLI hosts", ).toBe(true); }); - it("build-binary.ts lists tab-worker-entry as an explicit --compile entrypoint", async () => { + it("build-binary.ts no longer lists tab-worker-entry as a separate --compile entrypoint", async () => { const source = await Bun.file(buildBinaryPath).text(); - expect( - source.includes(`"${buildEntrypoint}"`), - `scripts/build-binary.ts must include "${buildEntrypoint}" as an explicit --compile entrypoint so Bun emits the worker into bunfs`, - ).toBe(true); + expect(source).not.toContain("./src/tools/browser/tab-worker-entry.ts"); }); }); diff --git a/packages/coding-agent/test/issue-1150-repro.test.ts b/packages/coding-agent/test/issue-1150-repro.test.ts index 777c58869..15a65c029 100644 --- a/packages/coding-agent/test/issue-1150-repro.test.ts +++ b/packages/coding-agent/test/issue-1150-repro.test.ts @@ -16,52 +16,36 @@ import * as path from "node:path"; * the worker module was never emitted into bunfs and the runtime tried to * bundle it on the fly, which fails in `$bunfs`. * - * The contract from AGENTS.md is symmetric: **every** worker spawned via - * the `isCompiledBinary()` hybrid pattern must be listed as an extra - * `--compile` entry in **both** scripts. This test pins that contract for - * the release script; the dev script is covered by `issue-1011-repro` for - * the tab worker entry. Runtime coverage lives in `omp --smoke-test`, - * which the release-binary CI step now invokes. + * The current contract is simpler: every Worker re-enters the CLI entrypoint + * and selects its worker body via `WorkerOptions.argv`, so release builds no + * longer need to list the worker modules as extra `--compile` entrypoints. + * Runtime coverage lives in `omp --smoke-test`. */ -describe("issue #1150 — release-build script must list all worker --compile entrypoints", () => { +describe("issue #1150 — release/dev builds route workers through the CLI entrypoint", () => { const repoRoot = path.resolve(import.meta.dir, "../../.."); const ciScriptPath = path.join(repoRoot, "scripts/ci-release-build-binaries.ts"); const devScriptPath = path.join(repoRoot, "packages/coding-agent/scripts/build-binary.ts"); - // Repo-root-relative literals — both the runtime `new Worker(...)` - // spawn site and the `--compile` entry must use this exact string for - // Bun's static analyzer to match them up. + // Repo-root-relative CLI literal — every runtime worker spawn site uses this + // same entry plus a hidden argv selector. const workerEntrypoints = [ "./packages/stats/src/sync-worker.ts", "./packages/coding-agent/src/tools/browser/tab-worker-entry.ts", "./packages/coding-agent/src/eval/js/worker-entry.ts", ]; - it("scripts/ci-release-build-binaries.ts lists every worker as an explicit --compile entrypoint", async () => { - const source = await Bun.file(ciScriptPath).text(); + it("release/dev build scripts do not list worker modules as explicit --compile entrypoints", async () => { + const releaseSource = await Bun.file(ciScriptPath).text(); + const devSource = await Bun.file(devScriptPath).text(); for (const entry of workerEntrypoints) { - expect( - source.includes(`"${entry}"`), - `scripts/ci-release-build-binaries.ts must include "${entry}" as a --compile entrypoint so Bun emits the worker into bunfs in the published binary`, - ).toBe(true); + expect(releaseSource).not.toContain(`"${entry}"`); } - }); - - it("packages/coding-agent/scripts/build-binary.ts lists every worker as an explicit --compile entrypoint", async () => { - // Dev script's cwd is packages/coding-agent and its `--root ../..` - // resolves to repo root, so its entry strings are package-relative - // (not repo-relative) but produce the same bunfs path. - const devEntrypoints = [ + for (const entry of [ "../stats/src/sync-worker.ts", "./src/tools/browser/tab-worker-entry.ts", "./src/eval/js/worker-entry.ts", - ]; - const source = await Bun.file(devScriptPath).text(); - for (const entry of devEntrypoints) { - expect( - source.includes(`"${entry}"`), - `packages/coding-agent/scripts/build-binary.ts must include "${entry}" as a --compile entrypoint so dev binaries match release binaries`, - ).toBe(true); + ]) { + expect(devSource).not.toContain(`"${entry}"`); } }); }); diff --git a/packages/coding-agent/test/issue-1606-repro.test.ts b/packages/coding-agent/test/issue-1606-repro.test.ts index d3a3e88ac..9a0ea3d00 100644 --- a/packages/coding-agent/test/issue-1606-repro.test.ts +++ b/packages/coding-agent/test/issue-1606-repro.test.ts @@ -15,22 +15,30 @@ * the original crash again. */ import { describe, expect, it } from "bun:test"; -import { - createTinyTitleSubprocess, - smokeTestTinyTitleWorker, - TINY_WORKER_ARG, -} from "@oh-my-pi/pi-coding-agent/tiny/title-client"; +import * as path from "node:path"; +import { createTinyTitleSubprocess, TINY_WORKER_ARG } from "@oh-my-pi/pi-coding-agent/tiny/title-client"; describe("issue #1606 — tiny model lives in an isolated subprocess", () => { it("ping/pongs through the spawned worker subprocess and tears it down cleanly", async () => { // `smokeTestTinyTitleWorker` is the runtime probe wired into - // `omp --smoke-test`: it spawns the worker subprocess via - // `Bun.spawn`, sends a ping over the IPC channel, awaits the pong, - // then SIGKILLs the child. If anyone reverts the worker to an - // in-process `new Worker(...)` thread or drops the `--tiny-worker` - // CLI dispatch, the spawn either picks up the wrong entrypoint or - // the ping never round-trips, and this test fails. - await expect(smokeTestTinyTitleWorker({ timeoutMs: 15_000 })).resolves.toBeUndefined(); + // `omp --smoke-test`. Run it in a child Bun process instead of this + // Bun-test worker: the test runner owns its own IPC channel and can + // starve nested Bun subprocess IPC on some Bun builds. + const repoRoot = path.resolve(import.meta.dir, "../../.."); + const script = + 'const { smokeTestTinyTitleWorker } = await import("@oh-my-pi/pi-coding-agent/tiny/title-client"); await smokeTestTinyTitleWorker({ timeoutMs: 15000 });'; + const proc = Bun.spawn([process.execPath, "-e", script], { + cwd: repoRoot, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + expect(`${stdout}${stderr}`).toBe(""); + expect(exitCode).toBe(0); }, 30_000); it("CLI dispatches the flag that `title-client.ts` passes to the spawned child", async () => { @@ -39,7 +47,7 @@ describe("issue #1606 — tiny model lives in an isolated subprocess", () => { // `argv` and there is no fallback path that "re-routes" the worker // on misnamed flags. Pin the spelling on both ends. const cliSource = await Bun.file(new URL("../src/cli.ts", import.meta.url)).text(); - expect(cliSource).toContain(`argv[0] === "${TINY_WORKER_ARG}"`); + expect(cliSource).toContain(`"${TINY_WORKER_ARG}"`); expect(cliSource).toContain("runTinyWorker"); }); diff --git a/packages/stats/CHANGELOG.md b/packages/stats/CHANGELOG.md index 25794560d..fd76b5cde 100644 --- a/packages/stats/CHANGELOG.md +++ b/packages/stats/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- The session-sync worker re-enters the host CLI entry (`workerHostEntry()` + `__omp_stats_sync_worker` argv selector) when running inside omp — source, npm bundle, or compiled binary — and keeps loading its own `sync-worker.ts` module directly for standalone `omp-stats`, bun test, and SDK hosts + ## [15.1.6] - 2026-05-19 ### Fixed diff --git a/packages/stats/src/aggregator.ts b/packages/stats/src/aggregator.ts index 97fba24df..af697ed68 100644 --- a/packages/stats/src/aggregator.ts +++ b/packages/stats/src/aggregator.ts @@ -1,5 +1,5 @@ import * as fs from "node:fs"; -import { isCompiledBinary } from "@oh-my-pi/pi-utils"; +import { workerHostEntry } from "@oh-my-pi/pi-utils"; import { getRecentErrors as dbGetRecentErrors, getRecentRequests as dbGetRecentRequests, @@ -24,15 +24,10 @@ import { } from "./db"; import { getSessionEntry, listAllSessionFiles, type ParseSessionResult } from "./parser"; import type { SyncWorkerRequest, SyncWorkerResponse } from "./sync-worker"; -// Worker entry. Bun's `--compile` bundler statically discovers the string -// literal in `new Worker("./packages/stats/src/sync-worker.ts", …)` below and -// emits the worker as an additional entrypoint (registered in -// `packages/coding-agent/scripts/build-binary.ts`). In dev runs we resolve -// the same source file through `import.meta.url`, so the literal only has to -// be valid relative to the `--root` directory (repo root). Importing the -// source as `with { type: "file" }` is NOT sufficient — that copies the file -// as a raw asset and does not bundle the worker's relative imports, so the -// worker would crash on first `import` (issue #1011, PR #1027). +// Coding-agent binary/bundle workers route through the CLI entrypoint with a +// hidden argv mode, so the compiled binary and npm bundle only need one +// JavaScript entry. Standalone source `omp-stats` keeps using this package's +// own sync-worker source file. import type { BehaviorDashboardStats, DashboardStats, MessageStats, RequestDetails } from "./types"; /** @@ -89,17 +84,18 @@ interface WorkerHandle { } /** - * Create a fresh sync worker. In a `--compile` binary the literal-string - * specifier is what Bun's static analyzer needs (the file is also listed as - * an additional `--compile` entrypoint in - * `packages/coding-agent/scripts/build-binary.ts`). In dev runs we resolve - * the source URL via `import.meta.url` so the worker survives `cwd` changes - * by callers. + * Create a fresh sync worker. When the process was started from a + * self-dispatching CLI entry (omp in source, npm-bundle, or compiled form), + * re-enter that entry with a worker argv selector; otherwise (standalone + * omp-stats, bun test, SDK embedding) load the worker module directly, so this + * package keeps zero runtime dependency on `@oh-my-pi/pi-coding-agent`. */ function createSyncWorker(): Worker { - return isCompiledBinary() - ? new Worker("./packages/stats/src/sync-worker.ts", { type: "module" }) - : new Worker(new URL("./sync-worker.ts", import.meta.url).href, { type: "module" }); + const hostEntry = workerHostEntry(); + if (hostEntry) { + return new Worker(hostEntry, { type: "module", argv: ["__omp_stats_sync_worker"] }); + } + return new Worker(new URL("./sync-worker.ts", import.meta.url).href, { type: "module" }); } function spawnWorker(): WorkerHandle { diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index 9b4c3f511..c0e4849c3 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -5,6 +5,7 @@ - Restored `PI_DEBUG_STARTUP` streaming startup markers: `logger.time` now writes a synchronous `[startup] :start` / `:done` / `:fail` stderr line per phase (independent of `PI_TIMING`), so a startup that hangs hard still names the phase it is stuck in — the `PI_TIMING` tree only prints after startup completes and is structurally unable to diagnose a hang. The CLI runner emits `cli:load:` markers around each lazily-imported command module for the same reason. - Added `logger.openSpanPath()`: ops of the currently-open timing-span chain (root → deepest), used by the coding agent's startup watchdog to name the in-flight phase of a stalled startup. +- Added `declareWorkerHostEntry()` / `workerHostEntry()` (env): self-dispatching CLI entrypoints declare `Bun.main` as the worker host so worker spawn sites can re-enter the single entry module with `WorkerOptions.argv` selectors across source, npm-bundle, and compiled distributions ### Changed @@ -14,6 +15,7 @@ ### Fixed - Fixed `prompt.format()` so ASCII symbol replacements such as `-->` and `!=` still run on lines containing a closing HTML comment token when not inside a comment +- `isCompiledBinary()` now also honors a define-folded `process.env.PI_COMPILED` (only `Bun.env` was checked), so builds that constant-fold `process.env` keep compiled-binary detection without relying on `import.meta.url` bunfs markers - `omp --help` now loads only the requested command module instead of the entire command table, so an unrelated command whose import graph hangs or crashes can no longer take down every per-command help invocation. ## [15.10.8] - 2026-06-09 diff --git a/packages/utils/src/env.ts b/packages/utils/src/env.ts index 39f429cfa..50ae7e999 100644 --- a/packages/utils/src/env.ts +++ b/packages/utils/src/env.ts @@ -156,11 +156,31 @@ export function isBunTestRuntime(): boolean { * first for cheap fast-path detection. */ export function isCompiledBinary(): boolean { - if (Bun.env.PI_COMPILED) return true; + if (process.env.PI_COMPILED || Bun.env.PI_COMPILED) return true; const url = import.meta.url; return url.includes("$bunfs") || url.includes("~BUN") || url.includes("%7EBUN"); } +/** + * Main-module path declared by self-dispatching CLI entrypoints — entries + * whose top-level argv handling routes hidden `__omp_*` worker selectors. + * Worker spawn sites re-enter this module via `new Worker(entry, { argv })`, + * so every distribution (source, npm bundle, compiled binary) needs exactly + * one JavaScript entrypoint. Never set under `bun test`, SDK embedding, or + * standalone package bins — those hosts load worker modules directly. + */ +let workerHostMain: string | null = null; + +/** Called by CLI entrypoints whose main module dispatches worker argv selectors. */ +export function declareWorkerHostEntry(): void { + workerHostMain = Bun.main; +} + +/** Main-module path of the self-dispatching CLI host, or null outside it. */ +export function workerHostEntry(): string | null { + return workerHostMain; +} + const TRUTHY: Dict = { "1": true, Y: true, diff --git a/scripts/ci-release-build-binaries.ts b/scripts/ci-release-build-binaries.ts index ab72a8ec1..d69241eb8 100644 --- a/scripts/ci-release-build-binaries.ts +++ b/scripts/ci-release-build-binaries.ts @@ -14,20 +14,10 @@ interface BinaryTarget { const repoRoot = path.join(import.meta.dir, ".."); const binariesDir = path.join(repoRoot, "packages", "coding-agent", "binaries"); const entrypoint = "./packages/coding-agent/src/cli.ts"; -// Worker entrypoints. Bun's `--compile` static analyzer discovers the -// literal in `new Worker("…", …)` at each spawn site, but only actually -// emits the worker into the bunfs root when it is also listed here as an -// explicit additional entry. Paths are repo-root-relative (matching -// `--root .` below) so the workers land at -// `/$bunfs/root/packages//src/.js`, which is exactly what the -// literals at the spawn sites resolve to. Keep this in sync with the dev -// script at `packages/coding-agent/scripts/build-binary.ts`; the -// `issue-1150-repro` test pins both halves of the contract. -const workerEntrypoints = [ - "./packages/stats/src/sync-worker.ts", - "./packages/coding-agent/src/tools/browser/tab-worker-entry.ts", - "./packages/coding-agent/src/eval/js/worker-entry.ts", -]; +// Legacy extension shims and package barrels in the build argv below are still +// explicit `--compile` entrypoints because they are reached via computed bunfs +// paths. Worker threads spawn `new Worker(Bun.main, { argv })` — they re-enter +// the binary's own entry module — so no separate worker modules are compiled. const isDryRun = process.argv.includes("--dry-run"); const targets: BinaryTarget[] = [ { @@ -120,7 +110,7 @@ async function buildBinary(target: BinaryTarget): Promise { console.log(`Building ${target.outfile}...`); await embedNative(target); if (isDryRun) { - console.log(`DRY RUN bun build --compile --no-compile-autoload-bunfig --no-compile-autoload-dotenv --no-compile-autoload-tsconfig --no-compile-autoload-package-json --keep-names --define process.env.PI_COMPILED="true" --root . --external mupdf --target=${target.target} ${entrypoint} ${workerEntrypoints.join(" ")} --outfile ${target.outfile}`); + console.log(`DRY RUN bun build --compile --no-compile-autoload-bunfig --no-compile-autoload-dotenv --no-compile-autoload-tsconfig --no-compile-autoload-package-json --keep-names --define process.env.PI_COMPILED="true" --root . --external mupdf --target=${target.target} ${entrypoint} --outfile ${target.outfile}`); return; } @@ -146,7 +136,6 @@ async function buildBinary(target: BinaryTarget): Promise { "--target", target.target, entrypoint, - ...workerEntrypoints, "--outfile", target.outfile, ],