diff --git a/packages/core/src/tool/plugin/shell.ts b/packages/core/src/tool/plugin/shell.ts index 35e1a265e4c..384f60e461a 100644 --- a/packages/core/src/tool/plugin/shell.ts +++ b/packages/core/src/tool/plugin/shell.ts @@ -205,8 +205,12 @@ export const Plugin = { yield* context.progress({ shellID: info.id }) const captureShell = Effect.fn("ShellTool.captureShell")(function* () { - const page = yield* shell.output(info.id, { limit: MAX_CAPTURE_BYTES }) - const truncated = page.size > page.cursor + const latest = yield* shell.output(info.id, { cursor: Number.MAX_SAFE_INTEGER }) + const truncated = latest.size > MAX_CAPTURE_BYTES + const page = yield* shell.output(info.id, { + cursor: Math.max(0, latest.size - MAX_CAPTURE_BYTES), + limit: MAX_CAPTURE_BYTES, + }) const notice = truncated ? `\n\n[output truncated; full output saved to: ${info.file}]` : "" return { output: `${page.output || "(no output)"}${notice}`, diff --git a/packages/core/test/tool-shell.test.ts b/packages/core/test/tool-shell.test.ts index 62c25c24776..df53ae94036 100644 --- a/packages/core/test/tool-shell.test.ts +++ b/packages/core/test/tool-shell.test.ts @@ -165,8 +165,8 @@ const bodyExitCommand = isWindows : "printf body && exit 7" const overflowCommand = (bytes: number) => isWindows - ? `[Console]::Out.Write(('x' * ${bytes})); Start-Sleep -Milliseconds 100` - : `head -c ${bytes} /dev/zero | tr '\\0' 'x'` + ? `[Console]::Out.Write('output-start' + ('x' * ${bytes}) + 'output-end'); Start-Sleep -Milliseconds 100` + : `printf output-start; head -c ${bytes} /dev/zero | tr '\\0' 'x'; printf output-end` const progressOverflowCommand = (bytes: number, release: string) => isWindows ? `[Console]::Out.Write(('x' * ${bytes})); while (!(Test-Path -LiteralPath '${release}')) { Start-Sleep -Milliseconds 50 }` @@ -410,7 +410,11 @@ describe("ShellTool", () => { Effect.andThen((settled) => Effect.sync(() => { expect(settled.metadata).toMatchObject({ exit: 0, truncated: true }) - expect(settled.content?.[0]).toMatchObject({ + const content = settled.content?.[0] + if (!content || content.type !== "text") throw new Error("Expected text content") + expect(content.text.includes("output-start")).toBe(false) + expect(content.text.includes("output-end")).toBe(true) + expect(content).toMatchObject({ type: "text", text: expect.stringContaining("output truncated; full output saved to:"), })