diff --git a/packages/codemode/CHANGELOG.md b/packages/codemode/CHANGELOG.md index 938d6400e..8e7e6798d 100644 --- a/packages/codemode/CHANGELOG.md +++ b/packages/codemode/CHANGELOG.md @@ -12,6 +12,10 @@ - The VM now polls for interrupts while it drains promise jobs, so a script such as `while (true) await null` is stopped at the same points as a synchronous loop +### Fixed + +- Fixed `CodemodeSandbox` failing with a broken bridge error when the host runs under `node --watch` on Node 24 or 26, which posts `watch:import`/`watch:require` messages on the worker channel ([#10725](https://github.com/earendil-works/pi/issues/10725)) + ## [1.1.0] - 2026-10-07 ### Added diff --git a/packages/codemode/src/runtime/host.ts b/packages/codemode/src/runtime/host.ts index c618b07a2..70e44963d 100644 --- a/packages/codemode/src/runtime/host.ts +++ b/packages/codemode/src/runtime/host.ts @@ -35,7 +35,16 @@ function workerLauncher( } catch (error) { throw new Error(`Failed to start worker: ${errorMessage(error)}`); } - worker.on("message", (message: unknown) => events.message(message)); + worker.on("message", (message: unknown) => { + // Node 24/26 --watch leaks dependency reports onto the worker channel (nodejs/node#65044). + if ( + typeof message === "object" && + message !== null && + ("watch:import" in message || "watch:require" in message) + ) + return; + events.message(message); + }); worker.on("error", (error: unknown) => { events.failure({ kind: "sandbox", diff --git a/packages/codemode/test/fixtures/raw-worker.ts b/packages/codemode/test/fixtures/raw-worker.ts index dacf83836..6e617f247 100644 --- a/packages/codemode/test/fixtures/raw-worker.ts +++ b/packages/codemode/test/fixtures/raw-worker.ts @@ -1,7 +1,8 @@ /** - * Stands in for the real worker in host tests: posts the JSON message passed as the script source, - * so tests can send the host payloads the real prelude never produces. + * Stands in for the real worker in host tests: posts the JSON message (or array of messages) passed + * as the script source, so tests can send the host payloads the real prelude never produces. */ import { parentPort, workerData } from "node:worker_threads"; -parentPort?.postMessage(JSON.parse((workerData as { code: string }).code)); +const messages: unknown = JSON.parse((workerData as { code: string }).code); +for (const message of Array.isArray(messages) ? messages : [messages]) parentPort?.postMessage(message); diff --git a/packages/codemode/test/sandbox.test.ts b/packages/codemode/test/sandbox.test.ts index ed45c8d4e..0181457ab 100644 --- a/packages/codemode/test/sandbox.test.ts +++ b/packages/codemode/test/sandbox.test.ts @@ -835,6 +835,20 @@ describe("worker host", () => { }); }); + // #10725: Node 24/26 `--watch` posts dependency reports on the worker channel. + it("ignores Node --watch notifications from the worker", async () => { + const sandbox = new CodemodeSandbox({ workerUrl: new URL("./fixtures/raw-worker.ts", import.meta.url) }); + sandboxes.push(sandbox); + const result = await sandbox.execute( + JSON.stringify([ + { "watch:require": ["/worker.js"] }, + { "watch:import": ["file:///worker.js"] }, + { type: "done", ok: true, value: "42", writes: "[]" }, + ]), + ); + expect(result).toMatchObject({ ok: true, value: 42 }); + }); + // #10444: a malformed payload from the worker must fail the run as a sandbox error instead of // throwing in the host's message listener and never settling. it.each([