Skip to content

Tool result size limit can return output larger than maxBytes for small limits #426

Description

@MicroMilo

Tool result size limit can return output larger than maxBytes for small limits

Summary

applyResultSizeLimit() treats maxBytes as disabled only when it is undefined or negative, so 0 and other small non-negative values are valid limits. When the limit is smaller than the fixed truncation suffix, the returned text can still contain the full suffix and exceed the requested cap.

For example, a one-byte text result with maxBytes = 0 returns a 46-byte truncation notice, and metadata.returnedBytes records 46.

Affected revision

Observed on main at 9ad10eb0e1ed75f864ca8cbda7f659f7c3b163e9.

Affected code

  • src/tool/protocol/result.ts:140-143 defines applyResultSizeLimit(content, maxBytes).
  • src/tool/protocol/result.ts:144-146 treats only undefined and negative limits as disabled.
  • src/tool/protocol/result.ts:162-167 appends the fixed truncation suffix after calculating a zero budget.

Reproduction

From the repository root:

cat > repro-tool-result-limit.mts <<'EOF'
import { applyResultSizeLimit } from "./src/tool/protocol/result.ts";

const result = applyResultSizeLimit([{ type: "text", text: " " }], 0);
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
const bytes = Buffer.byteLength(text, "utf8");

console.log(JSON.stringify({ bytes, metadata: result.metadata, text }, null, 2));
EOF

pnpm exec tsx repro-tool-result-limit.mts
rm repro-tool-result-limit.mts

Observed output:

{
  "bytes": 46,
  "metadata": {
    "truncated": true,
    "originalBytes": 1,
    "returnedBytes": 46
  },
  "text": "\n[Tool output truncated: head and tail shown.]"
}

Expected behavior

When maxBytes is a non-negative limit and the original non-media content is larger than that limit, the returned text and metadata.returnedBytes should not exceed maxBytes.

For tiny limits, returning an empty string or a shortened truncation notice would both preserve the limit. The key invariant is that the visible returned content stays within the requested byte cap.

Actual behavior

The function computes:

const budget = Math.max(0, maxBytes - suffixBytes);
const truncatedText = headTailTruncateUtf8(text, budget) + suffix;

When maxBytes < suffixBytes, budget becomes 0, but the full suffix is still appended. This makes the returned text longer than the requested cap.

Impact

applyResultSizeLimit() is used for bounded tool-result previews. Default limits are larger than this minimized case, but the helper does not preserve its own boundary contract for small configured caps or future callers. The metadata also reports truncated: true while recording a returnedBytes value larger than the requested maximum.

Existing coverage

I could not find an existing issue or pull request covering this small-maxBytes result-size boundary.

Suggested fix

Apply the byte cap to the final returned string, including the suffix. In the maxBytes <= suffixBytes case, either truncate the suffix itself or return an empty preview, then set metadata.returnedBytes from the actual returned string.

Suggested regression tests:

  • maxBytes = 0
  • maxBytes = suffixBytes - 1
  • multibyte text around the boundary, to preserve UTF-8 safety

Submitted with Codex.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions