diff --git a/CHANGELOG.md b/CHANGELOG.md index 891562c02b6..da57a44180c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/Image: collapse resize diagnostics to one line per image and include visible pixel/byte size details in the log message for faster triage. - Agents/Subagents: preemptively guard accumulated tool-result context before model calls by truncating oversized outputs and compacting oldest tool-result messages to avoid context-window overflow crashes. Thanks @tyler6204. - Agents/Subagents: add explicit subagent guidance to recover from `[compacted: tool output removed to free context]` / `[truncated: output exceeded context limit]` markers by re-reading with smaller chunks instead of full-file `cat`. Thanks @tyler6204. - Agents/Tools: make `read` auto-page across chunks (when no explicit `limit` is provided) and scale its per-call output budget from model `contextWindow`, so larger contexts can read more before context guards kick in. Thanks @tyler6204. diff --git a/src/agents/tool-images.ts b/src/agents/tool-images.ts index 4851d99ad63..52d1ad8e726 100644 --- a/src/agents/tool-images.ts +++ b/src/agents/tool-images.ts @@ -55,6 +55,16 @@ function inferMimeTypeFromBase64(base64: string): string | undefined { return undefined; } +function formatBytesShort(bytes: number): string { + if (!Number.isFinite(bytes) || bytes < 1024) { + return `${Math.max(0, Math.round(bytes))}B`; + } + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(1)}KB`; + } + return `${(bytes / (1024 * 1024)).toFixed(2)}MB`; +} + async function resizeImageBase64IfNeeded(params: { base64: string; mimeType: string; @@ -74,6 +84,8 @@ async function resizeImageBase64IfNeeded(params: { const height = meta?.height; const overBytes = buf.byteLength > params.maxBytes; const hasDimensions = typeof width === "number" && typeof height === "number"; + const overDimensions = + hasDimensions && (width > params.maxDimensionPx || height > params.maxDimensionPx); if ( hasDimensions && !overBytes && @@ -88,18 +100,6 @@ async function resizeImageBase64IfNeeded(params: { height, }; } - if ( - hasDimensions && - (width > params.maxDimensionPx || height > params.maxDimensionPx || overBytes) - ) { - log.warn("Image exceeds limits; resizing", { - label: params.label, - width, - height, - maxDimensionPx: params.maxDimensionPx, - maxBytes: params.maxBytes, - }); - } const qualities = [85, 75, 65, 55, 45, 35]; const maxDim = hasDimensions ? Math.max(width ?? 0, height ?? 0) : params.maxDimensionPx; @@ -122,17 +122,33 @@ async function resizeImageBase64IfNeeded(params: { smallest = { buffer: out, size: out.byteLength }; } if (out.byteLength <= params.maxBytes) { - log.info("Image resized", { - label: params.label, - width, - height, - maxDimensionPx: params.maxDimensionPx, - maxBytes: params.maxBytes, - originalBytes: buf.byteLength, - resizedBytes: out.byteLength, - quality, - side, - }); + const sourcePixels = + typeof width === "number" && typeof height === "number" + ? `${width}x${height}px` + : "unknown"; + const byteReductionPct = + buf.byteLength > 0 + ? Number((((buf.byteLength - out.byteLength) / buf.byteLength) * 100).toFixed(1)) + : 0; + log.info( + `Image resized to fit limits: ${sourcePixels} ${formatBytesShort(buf.byteLength)} -> ${formatBytesShort(out.byteLength)} (-${byteReductionPct}%)`, + { + label: params.label, + sourceMimeType: params.mimeType, + sourceWidth: width, + sourceHeight: height, + sourceBytes: buf.byteLength, + maxBytes: params.maxBytes, + maxDimensionPx: params.maxDimensionPx, + triggerOverBytes: overBytes, + triggerOverDimensions: overDimensions, + outputMimeType: "image/jpeg", + outputBytes: out.byteLength, + outputQuality: quality, + outputMaxSide: side, + byteReductionPct, + }, + ); return { base64: out.toString("base64"), mimeType: "image/jpeg", @@ -147,6 +163,23 @@ async function resizeImageBase64IfNeeded(params: { const best = smallest?.buffer ?? buf; const maxMb = (params.maxBytes / (1024 * 1024)).toFixed(0); const gotMb = (best.byteLength / (1024 * 1024)).toFixed(2); + const sourcePixels = + typeof width === "number" && typeof height === "number" ? `${width}x${height}px` : "unknown"; + log.warn( + `Image resize failed to fit limits: ${sourcePixels} best=${formatBytesShort(best.byteLength)} limit=${formatBytesShort(params.maxBytes)}`, + { + label: params.label, + sourceMimeType: params.mimeType, + sourceWidth: width, + sourceHeight: height, + sourceBytes: buf.byteLength, + maxDimensionPx: params.maxDimensionPx, + maxBytes: params.maxBytes, + smallestCandidateBytes: best.byteLength, + triggerOverBytes: overBytes, + triggerOverDimensions: overDimensions, + }, + ); throw new Error(`Image could not be reduced below ${maxMb}MB (got ${gotMb}MB)`); }