feat: rerouted worker subprocesses through the bundled CLI host entrypoint
- Rerouted sync, tab, js-eval, and tiny workers to re-enter CLI modes via `__omp_*` selectors. - Adjusted `cli.ts` startup to dispatch worker entrypoints before parsing and exit 1 on uncaught errors. - Bundled CLI as `dist/cli.js` in prepack, switching `omp` binary and published files. - Removed explicit Bun `--compile` worker entrypoints from build/release scripts in favor of host-entry dispatch. - Added `declareWorkerHostEntry()` and `workerHostEntry()` environment helpers and `PI_COMPILED` binary detection.
This commit is contained in:
@@ -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/<pkg>/src/<worker>.ts", { type: "module" })
|
||||
import { workerHostEntry } from "@oh-my-pi/pi-utils";
|
||||
const hostEntry = workerHostEntry();
|
||||
const worker = hostEntry
|
||||
? new Worker(hostEntry, { type: "module", argv: ["__omp_<name>_worker"] })
|
||||
: new Worker(new URL("./<worker>.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
|
||||
|
||||
|
||||
@@ -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 `<PLAN_TITLE>` 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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<void> {
|
||||
async function runCommand(
|
||||
command: string[],
|
||||
env: NodeJS.ProcessEnv = Bun.env,
|
||||
cwd: string = packageDir,
|
||||
): Promise<void> {
|
||||
const proc = Bun.spawn(command, {
|
||||
cwd: packageDir,
|
||||
cwd,
|
||||
env,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
@@ -55,19 +60,8 @@ async function main(): Promise<void> {
|
||||
"--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/<pkg>/src/<worker>.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<void> {
|
||||
// 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.
|
||||
|
||||
Executable
+81
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
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();
|
||||
@@ -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<void> {
|
||||
const { renderRootHelp } = await import("@oh-my-pi/pi-utils/cli");
|
||||
const { getExtraHelpText } = await import("./cli/args");
|
||||
@@ -54,6 +60,45 @@ async function runSmokeTest(): Promise<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
};
|
||||
},
|
||||
});
|
||||
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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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<T>(promise: Promise<T>, timeoutMs: number, reason
|
||||
|
||||
async function spawnJsWorker(): Promise<WorkerHandle> {
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, string> {
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T>(
|
||||
|
||||
async function spawnTabWorker(): Promise<WorkerHandle> {
|
||||
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) {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}"`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
- Restored `PI_DEBUG_STARTUP` streaming startup markers: `logger.time` now writes a synchronous `[startup] <op>: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:<name>` 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 <cmd> --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
|
||||
|
||||
@@ -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<boolean> = {
|
||||
"1": true,
|
||||
Y: true,
|
||||
|
||||
@@ -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/<pkg>/src/<worker>.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<void> {
|
||||
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<void> {
|
||||
"--target",
|
||||
target.target,
|
||||
entrypoint,
|
||||
...workerEntrypoints,
|
||||
"--outfile",
|
||||
target.outfile,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user