diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0003900a6..8647033f1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -23,6 +23,7 @@ - Fixed the codemode description not marking `searchTools()`, `describeTool()`, and `describeNamespace()` as async, which led models to serialize the unawaited promise as `{}` ([#10555](https://github.com/earendil-works/pi/issues/10555)) - Fixed codemode output items running together, so models could not tell where one `text()` or `console.log()` output ended and the next began. With several text items, each now starts with a `==> text N/M <==` line, and `console` calls follow the other output in one `` block with one line per call - Fixed `/mcp` waiting for all servers to connect before opening; the manager now updates live and remains usable while enabling, reconnecting, or disabling servers ([#10562](https://github.com/earendil-works/pi/issues/10562)) +- Fixed images being dropped as "could not be resized" when running under `node --watch` on Node 24.19+ and 26.x, where Node posts its own messages on the image resize worker channel ([#10527](https://github.com/earendil-works/pi/issues/10527)) ## [1.0.4] - 2026-10-05 diff --git a/packages/coding-agent/src/utils/image-resize-core.ts b/packages/coding-agent/src/utils/image-resize-core.ts index e60820c7b..3f80111ff 100644 --- a/packages/coding-agent/src/utils/image-resize-core.ts +++ b/packages/coding-agent/src/utils/image-resize-core.ts @@ -18,6 +18,16 @@ export interface ResizedImage { wasResized: boolean; } +/** + * Tag on image resize worker replies. Node can post its own messages on the worker channel + * (for example `{ "watch:require": [...] }` under `node --watch`), so replies must be identifiable. + */ +export const IMAGE_RESIZE_WORKER_RESPONSE_TYPE = "pi:image-resize-response"; + +export type ResizeImageWorkerResponse = + | { type: typeof IMAGE_RESIZE_WORKER_RESPONSE_TYPE; result: ResizedImage | null } + | { type: typeof IMAGE_RESIZE_WORKER_RESPONSE_TYPE; error: string }; + // 4.5MB of base64 payload. Provides headroom below Anthropic's 5MB limit. const DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024; diff --git a/packages/coding-agent/src/utils/image-resize-worker.ts b/packages/coding-agent/src/utils/image-resize-worker.ts index ee881b3d9..7fdc581a5 100644 --- a/packages/coding-agent/src/utils/image-resize-worker.ts +++ b/packages/coding-agent/src/utils/image-resize-worker.ts @@ -1,5 +1,10 @@ import { parentPort } from "node:worker_threads"; -import { type ImageResizeOptions, type ResizedImage, resizeImageInProcess } from "./image-resize-core.ts"; +import { + IMAGE_RESIZE_WORKER_RESPONSE_TYPE, + type ImageResizeOptions, + type ResizeImageWorkerResponse, + resizeImageInProcess, +} from "./image-resize-core.ts"; interface ResizeImageWorkerRequest { inputBytes: Uint8Array; @@ -7,11 +12,6 @@ interface ResizeImageWorkerRequest { options?: ImageResizeOptions; } -interface ResizeImageWorkerResponse { - result?: ResizedImage | null; - error?: string; -} - function isResizeImageWorkerRequest(value: unknown): value is ResizeImageWorkerRequest { if (!value || typeof value !== "object") return false; const record = value as Record; @@ -30,10 +30,11 @@ port.once("message", (message: unknown) => { throw new Error("Invalid image resize worker request"); } const result = await resizeImageInProcess(message.inputBytes, message.mimeType, message.options); - const response: ResizeImageWorkerResponse = { result }; + const response: ResizeImageWorkerResponse = { type: IMAGE_RESIZE_WORKER_RESPONSE_TYPE, result }; port.postMessage(response); } catch (error) { const response: ResizeImageWorkerResponse = { + type: IMAGE_RESIZE_WORKER_RESPONSE_TYPE, error: error instanceof Error ? error.message : String(error), }; port.postMessage(response); diff --git a/packages/coding-agent/src/utils/image-resize.ts b/packages/coding-agent/src/utils/image-resize.ts index 516a1e57a..ccdb98015 100644 --- a/packages/coding-agent/src/utils/image-resize.ts +++ b/packages/coding-agent/src/utils/image-resize.ts @@ -1,13 +1,14 @@ import { Worker } from "node:worker_threads"; -import { type ImageResizeOptions, type ResizedImage, resizeImageInProcess } from "./image-resize-core.ts"; +import { + IMAGE_RESIZE_WORKER_RESPONSE_TYPE, + type ImageResizeOptions, + type ResizedImage, + type ResizeImageWorkerResponse, + resizeImageInProcess, +} from "./image-resize-core.ts"; export type { ImageResizeOptions, ResizedImage } from "./image-resize-core.ts"; -interface ResizeImageWorkerResponse { - result?: ResizedImage | null; - error?: string; -} - function toTransferableBytes(input: Uint8Array): Uint8Array { // Transfer detaches the buffer, so transfer a worker-owned copy and leave the // caller's bytes intact. @@ -15,7 +16,9 @@ function toTransferableBytes(input: Uint8Array): Uint8Array { } function isResizeImageWorkerResponse(value: unknown): value is ResizeImageWorkerResponse { - return value !== null && typeof value === "object"; + return ( + value !== null && typeof value === "object" && "type" in value && value.type === IMAGE_RESIZE_WORKER_RESPONSE_TYPE + ); } function createResizeWorker(workerSpecifier: string | URL): Worker { @@ -44,16 +47,12 @@ async function resizeImageInWorker( reject(error); }; - worker.once("message", (message: unknown) => { - if (!isResizeImageWorkerResponse(message)) { - fail(new Error("Invalid image resize worker response")); - return; - } - if (message.error) { - fail(new Error(message.error)); - return; - } - settle(message.result ?? null); + worker.on("message", (message: unknown) => { + // Ignore messages Node posts on the worker channel itself, such as + // `{ "watch:require": [...] }` under `node --watch` (nodejs/node#65044). + if (!isResizeImageWorkerResponse(message)) return; + if ("error" in message) fail(new Error(message.error)); + else settle(message.result); }); worker.once("error", fail); worker.once("exit", (code) => {