fix(agents): make image resize logs single-line with size

This commit is contained in:
Peter Steinberger
2026-02-18 01:58:25 +01:00
parent 3459200444
commit 414b996b0c
2 changed files with 57 additions and 23 deletions

View File

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

View File

@@ -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)`);
}