fix(coding-agent): ignore Node's own worker messages in image resize

Under `node --watch`, Node 24.19+ and 26.x post `{ "watch:require": [...] }`
on the worker message channel (nodejs/node#65044). The image resize code
treated the first message as the worker's reply, resolved it to `null`, and
dropped the image as "could not be resized below the inline image size limit".

Worker replies now carry a `type` marker, and the main thread ignores any
message without it. The reply type is now either a result or an error.

fixes #10527
This commit is contained in:
David Brailovsky 2026-10-07 12:26:43 +02:00
parent 7fb59f995b
commit b30a6dd779
4 changed files with 35 additions and 24 deletions

View file

@ -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 `<console_output>` 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

View file

@ -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;

View file

@ -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<string, unknown>;
@ -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);

View file

@ -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<ArrayBuffer> {
// 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<ArrayBuffer> {
}
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) => {