From b4766f8743b2bae3ec78903eac2d973f2910fdf0 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sat, 14 Feb 2026 18:12:50 -0500 Subject: [PATCH] fix(agents): restore log paging and strict bootstrap cap --- src/agents/bash-tools.e2e.test.ts | 23 +++++++++++ src/agents/bash-tools.process.ts | 12 ++++-- ...ers.buildbootstrapcontextfiles.e2e.test.ts | 22 +++++++++++ src/agents/pi-embedded-helpers/bootstrap.ts | 38 ++++++++++++++++--- 4 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/agents/bash-tools.e2e.test.ts b/src/agents/bash-tools.e2e.test.ts index 067ca8067ff..99f31b89b39 100644 --- a/src/agents/bash-tools.e2e.test.ts +++ b/src/agents/bash-tools.e2e.test.ts @@ -261,6 +261,29 @@ describe("exec tool backgrounding", () => { expect(normalizeText(textBlock?.text)).toBe("beta"); }); + it("keeps offset-only log requests unbounded by default tail mode", async () => { + const lines = Array.from({ length: 260 }, (_value, index) => `line-${index + 1}`); + const result = await execTool.execute("call1", { + command: echoLines(lines), + background: true, + }); + const sessionId = (result.details as { sessionId: string }).sessionId; + await waitForCompletion(sessionId); + + const log = await processTool.execute("call2", { + action: "log", + sessionId, + offset: 30, + }); + + const textBlock = log.content.find((c) => c.type === "text")?.text ?? ""; + const renderedLines = textBlock.split("\n"); + expect(renderedLines[0]?.trim()).toBe("line-31"); + expect(renderedLines[renderedLines.length - 1]?.trim()).toBe("line-260"); + expect(textBlock).not.toContain("showing last 200"); + expect((log.details as { totalLines?: number }).totalLines).toBe(260); + }); + it("scopes process sessions by scopeKey", async () => { const bashA = createExecTool({ backgroundMs: 10, scopeKey: "agent:alpha" }); const processA = createProcessTool({ scopeKey: "agent:alpha" }); diff --git a/src/agents/bash-tools.process.ts b/src/agents/bash-tools.process.ts index ca1ffca7e39..f066cfc154c 100644 --- a/src/agents/bash-tools.process.ts +++ b/src/agents/bash-tools.process.ts @@ -296,16 +296,18 @@ export function createProcessTool( }; } const effectiveOffset = params.offset; + const usingDefaultTail = params.offset === undefined && params.limit === undefined; const effectiveLimit = typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit - : DEFAULT_LOG_TAIL_LINES; + : usingDefaultTail + ? DEFAULT_LOG_TAIL_LINES + : undefined; const { slice, totalLines, totalChars } = sliceLogLines( scopedSession.aggregated, effectiveOffset, effectiveLimit, ); - const usingDefaultTail = params.offset === undefined && params.limit === undefined; const defaultTailNote = usingDefaultTail && totalLines > DEFAULT_LOG_TAIL_LINES ? `\n\n[showing last ${DEFAULT_LOG_TAIL_LINES} of ${totalLines} lines; pass offset/limit to page]` @@ -325,17 +327,19 @@ export function createProcessTool( } if (scopedFinished) { const effectiveOffset = params.offset; + const usingDefaultTail = params.offset === undefined && params.limit === undefined; const effectiveLimit = typeof params.limit === "number" && Number.isFinite(params.limit) ? params.limit - : DEFAULT_LOG_TAIL_LINES; + : usingDefaultTail + ? DEFAULT_LOG_TAIL_LINES + : undefined; const { slice, totalLines, totalChars } = sliceLogLines( scopedFinished.aggregated, effectiveOffset, effectiveLimit, ); const status = scopedFinished.status === "completed" ? "completed" : "failed"; - const usingDefaultTail = params.offset === undefined && params.limit === undefined; const defaultTailNote = usingDefaultTail && totalLines > DEFAULT_LOG_TAIL_LINES ? `\n\n[showing last ${DEFAULT_LOG_TAIL_LINES} of ${totalLines} lines; pass offset/limit to page]` diff --git a/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.e2e.test.ts b/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.e2e.test.ts index 7bf758e7981..64203efd8fe 100644 --- a/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.e2e.test.ts +++ b/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.e2e.test.ts @@ -67,4 +67,26 @@ describe("buildBootstrapContextFiles", () => { expect(result).toHaveLength(3); expect(result[2]?.content).toContain("[...truncated, read USER.md for full content...]"); }); + + it("enforces strict total cap even when truncation markers are present", () => { + const files = [ + makeFile({ name: "AGENTS.md", content: "a".repeat(1_000) }), + makeFile({ name: "SOUL.md", path: "/tmp/SOUL.md", content: "b".repeat(1_000) }), + ]; + const result = buildBootstrapContextFiles(files, { + maxChars: 100, + totalMaxChars: 150, + }); + const totalChars = result.reduce((sum, entry) => sum + entry.content.length, 0); + expect(totalChars).toBeLessThanOrEqual(150); + }); + + it("skips bootstrap injection when remaining total budget is too small", () => { + const files = [makeFile({ name: "AGENTS.md", content: "a".repeat(1_000) })]; + const result = buildBootstrapContextFiles(files, { + maxChars: 200, + totalMaxChars: 40, + }); + expect(result).toEqual([]); + }); }); diff --git a/src/agents/pi-embedded-helpers/bootstrap.ts b/src/agents/pi-embedded-helpers/bootstrap.ts index 90c1d2b102f..b2abbed7b4a 100644 --- a/src/agents/pi-embedded-helpers/bootstrap.ts +++ b/src/agents/pi-embedded-helpers/bootstrap.ts @@ -83,6 +83,7 @@ export function stripThoughtSignatures( export const DEFAULT_BOOTSTRAP_MAX_CHARS = 20_000; export const DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 24_000; +const MIN_BOOTSTRAP_FILE_BUDGET_CHARS = 64; const BOOTSTRAP_HEAD_RATIO = 0.7; const BOOTSTRAP_TAIL_RATIO = 0.2; @@ -144,6 +145,20 @@ function trimBootstrapContent( }; } +function clampToBudget(content: string, budget: number): string { + if (budget <= 0) { + return ""; + } + if (content.length <= budget) { + return content; + } + if (budget <= 3) { + return content.slice(0, budget); + } + const safe = Math.max(1, budget - 1); + return `${content.slice(0, safe)}…`; +} + export async function ensureSessionHeader(params: { sessionFile: string; sessionId: string; @@ -183,27 +198,40 @@ export function buildBootstrapContextFiles( if (remainingTotalChars <= 0) { break; } + if (remainingTotalChars < MIN_BOOTSTRAP_FILE_BUDGET_CHARS) { + opts?.warn?.( + `remaining bootstrap budget is ${remainingTotalChars} chars (<${MIN_BOOTSTRAP_FILE_BUDGET_CHARS}); skipping additional bootstrap files`, + ); + break; + } if (file.missing) { + const missingText = `[MISSING] Expected at: ${file.path}`; + const cappedMissingText = clampToBudget(missingText, remainingTotalChars); + if (!cappedMissingText) { + break; + } + remainingTotalChars = Math.max(0, remainingTotalChars - cappedMissingText.length); result.push({ path: file.path, - content: `[MISSING] Expected at: ${file.path}`, + content: cappedMissingText, }); continue; } const fileMaxChars = Math.max(1, Math.min(maxChars, remainingTotalChars)); const trimmed = trimBootstrapContent(file.content ?? "", file.name, fileMaxChars); - if (!trimmed.content) { + const contentWithinBudget = clampToBudget(trimmed.content, remainingTotalChars); + if (!contentWithinBudget) { continue; } - if (trimmed.truncated) { + if (trimmed.truncated || contentWithinBudget.length < trimmed.content.length) { opts?.warn?.( `workspace bootstrap file ${file.name} is ${trimmed.originalLength} chars (limit ${trimmed.maxChars}); truncating in injected context`, ); } - remainingTotalChars = Math.max(0, remainingTotalChars - trimmed.content.length); + remainingTotalChars = Math.max(0, remainingTotalChars - contentWithinBudget.length); result.push({ path: file.path, - content: trimmed.content, + content: contentWithinBudget, }); } return result;