From ebbe66a3d62adad397f23e7735201e6c7e073e52 Mon Sep 17 00:00:00 2001 From: can1357 Date: Tue, 12 May 2026 06:15:14 +0200 Subject: [PATCH] fix(coding-agent/browser): bundle tab-worker-entry for prebuilt binaries (#1011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Worker spawn in spawnTabWorker used `new Worker(new URL(...).href, ...)` through a local variable, which Bun's --compile static analysis does not recognize. As a result, tab-worker-entry.ts was never embedded in single-file binaries (v14.5.13+), and the worker thread failed to load its entry at /$bunfs/root/tab-worker-entry.ts, surfacing as the user-visible "Timed out initializing browser tab worker". Switch to a `with { type: "file" }` import of the entry module so the bundler discovers and embeds it in both dev and compiled targets. Regression test asserts `Bun.build` emits tab-worker-entry as an asset alongside the tab-supervisor entry point — the same static-analysis pass that powers `bun build --compile`. --- packages/coding-agent/CHANGELOG.md | 1 + .../src/tools/browser/tab-supervisor.ts | 10 ++- .../test/issue-1011-repro.test.ts | 66 +++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/test/issue-1011-repro.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 73109ca32..e301c4cdf 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -33,6 +33,7 @@ - Fixed multi-entry edits silently rendering a fake success when every entry failed (e.g. all hit the auto-generated guard), by surfacing `isError: true` from the single-path edit orchestrator so the renderer takes the error branch instead of falling through to the streaming-preview fallback that displays the *proposed* diff - Fixed the auto-generated streaming guard being gated behind `edit.streamingAbort` (default false), so it now pre-empts streaming edit tool calls targeting auto-generated files regardless of that setting - Fixed subagents launched in the same parallel batch not seeing each other in their initial `# IRC Peers` system-prompt block by pre-registering the agent in the global `AgentRegistry` before `rebuildSystemPrompt` runs and attaching the live session afterwards +- Fixed `Timed out initializing browser tab worker` on prebuilt binaries by rewriting `spawnTabWorker` to import the worker entry with `with { type: "file" }` so Bun's `--compile` bundler statically discovers and embeds `tab-worker-entry.ts` in the single-file binary ([#1011](https://github.com/can1357/oh-my-pi/issues/1011)) ## [14.9.3] - 2026-05-10 ### Breaking Changes diff --git a/packages/coding-agent/src/tools/browser/tab-supervisor.ts b/packages/coding-agent/src/tools/browser/tab-supervisor.ts index 96bad2ae6..6afd56630 100644 --- a/packages/coding-agent/src/tools/browser/tab-supervisor.ts +++ b/packages/coding-agent/src/tools/browser/tab-supervisor.ts @@ -16,6 +16,13 @@ import type { WorkerInitPayload, WorkerOutbound, } from "./tab-protocol"; +// Imported with `type: "file"` so Bun's bundler statically discovers the +// worker entry and embeds it inside `bun build --compile` single-file +// binaries. Without this attribute the bundler cannot reach the entry through +// a `new URL(..., import.meta.url)` literal stored in a local variable, and +// the prebuilt binary surfaces `Timed out initializing browser tab worker` +// (issue #1011) because `/$bunfs/root/tab-worker-entry.ts` is missing. +import tabWorkerEntryUrl from "./tab-worker-entry.ts" with { type: "file" }; interface WorkerHandle { send(msg: WorkerInbound, transferList?: Transferable[]): void; @@ -364,8 +371,7 @@ async function raceWithTimeout( async function spawnTabWorker(): Promise { try { - const url = new URL("./tab-worker-entry.ts", import.meta.url); - const worker = new Worker(url.href, { type: "module" }); + const worker = new Worker(tabWorkerEntryUrl, { type: "module" }); return wrapBunWorker(worker); } catch (err) { logger.warn("Bun Worker spawn failed; using inline tab worker (no sync-loop guard)", { diff --git a/packages/coding-agent/test/issue-1011-repro.test.ts b/packages/coding-agent/test/issue-1011-repro.test.ts new file mode 100644 index 000000000..cffd0977e --- /dev/null +++ b/packages/coding-agent/test/issue-1011-repro.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "bun:test"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; + +/** + * Regression for https://github.com/can1357/oh-my-pi/issues/1011 + * + * In v14.5.13 `spawnTabWorker` (in `src/tools/browser/tab-supervisor.ts`) was + * introduced to host browser tabs in a `Worker`. The worker URL was assembled + * as: + * + * ```ts + * const url = new URL("./tab-worker-entry.ts", import.meta.url); + * const worker = new Worker(url.href, { type: "module" }); + * ``` + * + * Bun's `--compile` bundler does NOT statically discover that pattern (the + * worker entry is hidden behind a local variable and `.href`), so the entry + * file is never embedded in the single-file binary. At runtime the worker + * thread tries to load `/$bunfs/root/tab-worker-entry.ts`, the module is + * missing, and the supervisor surfaces the symptom from the issue: + * `Timed out initializing browser tab worker`. + * + * `Bun.build` exposes the same static-analysis pass that drives `--compile`. + * If `tab-worker-entry.ts` is reachable to the bundler, it appears in the + * outputs as a separate `asset` chunk. If the spawn pattern hides it from + * the bundler, only the entry point is emitted. + * + * The bundler is driven through a `bun -e` subprocess that writes its report + * to a tmp file: invoking `Bun.build` directly from inside `bun test` does + * not auto-resolve TypeScript imports the way the real build pipeline does. + */ +describe("issue #1011 — tab worker entry must survive `bun build --compile`", () => { + it("bundles tab-worker-entry.ts as a discoverable asset of tab-supervisor.ts", async () => { + const supervisor = path.resolve(import.meta.dir, "../src/tools/browser/tab-supervisor.ts"); + const packageDir = path.resolve(import.meta.dir, ".."); + const reportPath = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "issue-1011-")), "report.json"); + const script = `const r = await Bun.build({ entrypoints: [${JSON.stringify(supervisor)}], target: "bun" }); await Bun.write(${JSON.stringify(reportPath)}, JSON.stringify({ success: r.success, outputs: r.outputs.map(o => ({ path: o.path, kind: o.kind })), logs: r.logs.map(l => l.message) }));`; + const proc = Bun.spawnSync(["bun", "-e", script], { + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + }); + const stderr = proc.stderr.toString(); + expect(proc.exitCode, `bun -e exited with ${proc.exitCode}; stderr=${stderr}`).toBe(0); + + const report = (await Bun.file(reportPath).json()) as { + success: boolean; + outputs: { path: string; kind: string }[]; + logs: string[]; + }; + expect(report.success, `bundler logs: ${report.logs.join("; ")}`).toBe(true); + + const workerAssets = report.outputs.filter(out => out.kind === "asset" && out.path.includes("tab-worker-entry")); + if (workerAssets.length === 0) { + const summary = report.outputs.map(o => `${o.kind}:${o.path}`).join(", "); + throw new Error( + `tab-worker-entry.ts was not bundled as an asset of tab-supervisor.ts. ` + + `Bun's --compile bundler cannot embed the worker because the Worker ` + + `constructor argument is not a statically-analyzable URL literal. ` + + `Bundler outputs were: [${summary}]`, + ); + } + }); +});