diff --git a/src/agents/sessions/tools/find.ts b/src/agents/sessions/tools/find.ts index 1c926f457f2..0c2e67742a6 100644 --- a/src/agents/sessions/tools/find.ts +++ b/src/agents/sessions/tools/find.ts @@ -8,7 +8,7 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js" import type { AgentTool } from "../../runtime/index.js"; import { ensureTool } from "../../utils/tools-manager.js"; import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; -import { normalizePositiveLimit } from "./limits.js"; +import { appendBoundedTextTail, normalizePositiveLimit } from "./limits.js"; import { resolveToCwd } from "./path-utils.js"; import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js"; import type { FindToolDetails } from "./tool-contracts.js"; @@ -277,7 +277,7 @@ export function createFindToolDefinition( }; child.stderr?.on("data", (chunk) => { - stderr += chunk.toString(); + stderr = appendBoundedTextTail(stderr, chunk); }); rl.on("line", (line) => { diff --git a/src/agents/sessions/tools/grep.ts b/src/agents/sessions/tools/grep.ts index ddc04ab20c3..2848d33bb37 100644 --- a/src/agents/sessions/tools/grep.ts +++ b/src/agents/sessions/tools/grep.ts @@ -8,7 +8,7 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js" import type { AgentTool } from "../../runtime/index.js"; import { ensureTool } from "../../utils/tools-manager.js"; import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; -import { normalizePositiveLimit } from "./limits.js"; +import { appendBoundedTextTail, normalizePositiveLimit } from "./limits.js"; import { resolveToCwd } from "./path-utils.js"; import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js"; import type { GrepToolDetails } from "./tool-contracts.js"; @@ -270,7 +270,7 @@ export function createGrepToolDefinition( }; signal?.addEventListener("abort", onAbort, { once: true }); child.stderr?.on("data", (chunk) => { - stderr += chunk.toString(); + stderr = appendBoundedTextTail(stderr, chunk); }); const formatBlock = async (filePath: string, lineNumber: number): Promise => { diff --git a/src/agents/sessions/tools/limits.test.ts b/src/agents/sessions/tools/limits.test.ts index aff3b858b78..a28245c19cb 100644 --- a/src/agents/sessions/tools/limits.test.ts +++ b/src/agents/sessions/tools/limits.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { normalizePositiveLimit } from "./limits.js"; +import { + appendBoundedTextTail, + normalizePositiveLimit, + SESSION_TOOL_STDERR_TAIL_BYTES, +} from "./limits.js"; describe("session tool limits", () => { it.each([ @@ -13,4 +17,25 @@ describe("session tool limits", () => { ])("normalizes %s to %s", (input, expected) => { expect(normalizePositiveLimit(input, 500)).toBe(expected); }); + + it("keeps a bounded tail of accumulated child output", () => { + let output = appendBoundedTextTail("old-", "middle-", 12); + output = appendBoundedTextTail(output, "recent", 12); + + expect(output).toBe("iddle-recent"); + expect(Buffer.byteLength(output, "utf8")).toBeLessThanOrEqual(12); + }); + + it("clips oversized chunks to the configured tail bytes", () => { + const output = appendBoundedTextTail("ignored", Buffer.from("x".repeat(128)), 16); + + expect(output).toBe("x".repeat(16)); + expect(Buffer.byteLength(output, "utf8")).toBe(16); + }); + + it("uses the session stderr tail limit by default", () => { + const output = appendBoundedTextTail("", "x".repeat(SESSION_TOOL_STDERR_TAIL_BYTES + 1)); + + expect(Buffer.byteLength(output, "utf8")).toBe(SESSION_TOOL_STDERR_TAIL_BYTES); + }); }); diff --git a/src/agents/sessions/tools/limits.ts b/src/agents/sessions/tools/limits.ts index 128a56e56c5..2f420c62bdf 100644 --- a/src/agents/sessions/tools/limits.ts +++ b/src/agents/sessions/tools/limits.ts @@ -4,3 +4,27 @@ export function normalizePositiveLimit(value: number | undefined, fallback: numb } return Math.max(1, Math.floor(value)); } + +export const SESSION_TOOL_STDERR_TAIL_BYTES = 64 * 1024; + +export function appendBoundedTextTail( + current: string, + chunk: Buffer | string, + maxBytes = SESSION_TOOL_STDERR_TAIL_BYTES, +): string { + const effectiveMaxBytes = normalizePositiveLimit(maxBytes, SESSION_TOOL_STDERR_TAIL_BYTES); + const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + if (chunkBuffer.byteLength >= effectiveMaxBytes) { + return chunkBuffer.subarray(chunkBuffer.byteLength - effectiveMaxBytes).toString("utf8"); + } + + const currentBuffer = Buffer.from(current); + const nextBytes = currentBuffer.byteLength + chunkBuffer.byteLength; + if (nextBytes <= effectiveMaxBytes) { + return `${current}${chunkBuffer.toString("utf8")}`; + } + + const currentTailBytes = Math.max(0, effectiveMaxBytes - chunkBuffer.byteLength); + const currentTail = currentBuffer.subarray(currentBuffer.byteLength - currentTailBytes); + return Buffer.concat([currentTail, chunkBuffer], effectiveMaxBytes).toString("utf8"); +}