From 210be4b749206de1a6cccba245484930311a9f48 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:31:26 -0500 Subject: [PATCH] fix(core): preserve shell output on timeout (#39559) --- packages/core/src/tool/plugin/shell.ts | 6 +++--- packages/core/test/tool-shell.test.ts | 23 +++++++++++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/core/src/tool/plugin/shell.ts b/packages/core/src/tool/plugin/shell.ts index b663140688..89ad765a83 100644 --- a/packages/core/src/tool/plugin/shell.ts +++ b/packages/core/src/tool/plugin/shell.ts @@ -198,20 +198,20 @@ export const Plugin = { const settleShell = Effect.fn("ShellTool.settleShell")(function* () { const final = yield* shell.wait(info.id) + const capture = yield* captureShell() // `exit` is optionalKey in the Output schema; a present-but-undefined key // fails output encoding, so omit it when the process has no exit code. if (final.status === "timeout") { return { ...(final.exit !== undefined ? { exit: final.exit } : {}), - output: `Command exceeded timeout of ${finalTimeout} ms. Retry with a larger timeout if the command is expected to take longer.`, - truncated: false, + output: `${capture.output}\n\nCommand exceeded timeout of ${finalTimeout} ms. Retry with a larger timeout if the command is expected to take longer.`, + truncated: capture.truncated, timeout: true, status: "completed" as const, } } - const capture = yield* captureShell() return { ...(final.exit !== undefined ? { exit: final.exit } : {}), output: capture.output, diff --git a/packages/core/test/tool-shell.test.ts b/packages/core/test/tool-shell.test.ts index 2b540d7538..33f81161ce 100644 --- a/packages/core/test/tool-shell.test.ts +++ b/packages/core/test/tool-shell.test.ts @@ -157,6 +157,9 @@ const mixedOutputCommand = isWindows ? "[Console]::Out.Write('stdout'); Start-Sleep -Milliseconds 50; [Console]::Error.Write('stderr'); Start-Sleep -Milliseconds 100" : "printf stdout; sleep 0.05; printf stderr >&2" const idleCommand = isWindows ? "Start-Sleep -Seconds 60" : "sleep 60" +const timeoutOutputCommand = isWindows + ? "[Console]::Out.Write('before timeout'); Start-Sleep -Seconds 60" + : "printf 'before timeout'; sleep 60" const steadyProgressCommand = isWindows ? "[Console]::Out.Write('steady'); Start-Sleep -Milliseconds 3400" : "printf steady; sleep 3.4" @@ -461,14 +464,18 @@ describe("ShellTool", () => { Effect.acquireUseRelease( Effect.promise(() => tmpdir()), (tmp) => { - reset() - return withSession(tmp.path, (registry) => - executeTool(registry, call({ command: idleCommand, timeout: 50 })), - ).pipe( - Effect.andThen((settled) => - Effect.sync(() => { - expect(settled.metadata).toMatchObject({ timeout: true, truncated: false }) - expect(settled.content?.[1]).toMatchObject({ + reset() + return withSession(tmp.path, (registry) => + executeTool(registry, call({ command: timeoutOutputCommand, timeout: 50 })), + ).pipe( + Effect.andThen((settled) => + Effect.sync(() => { + expect(settled.metadata).toMatchObject({ timeout: true, truncated: false }) + expect(settled.content?.[0]).toMatchObject({ + type: "text", + text: expect.stringContaining("before timeout"), + }) + expect(settled.content?.[1]).toMatchObject({ type: "text", text: expect.stringContaining("Command timed out"), })