mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): track discarded user bash output (#10165)
Closes #10164.
A user `!` command can discard earlier chunks but report `truncated:
false` when the retained tail fits within the output limits. The model
then receives no truncation notice or full-log path.
Track discarded chunks and include that flag in the executor's result
for both completion and cancellation. This applies to the current shared
finalization path and preserves streaming ANSI handling. Output limits
and message format stay unchanged.
Related: #2852. Here the complete log is already saved; the incorrect
flag hides its path from the model.
Validation on main `f1b2e77f5`:
- Two deterministic 64 KiB + 44 KiB cases fail without the fix. All
three regressions pass with it, checking completion, cancellation, the
complete log, model-facing path, and short-output behavior.
- Regressions are in `test/bash-executor-truncation.test.ts`; that file
and `test/tools.test.ts` pass all 88 tests.
- `npm run check`, offline build, and `./test.sh` pass.
This commit is contained in:
parent
3b57ed7e27
commit
66c6556771
2 changed files with 62 additions and 1 deletions
|
|
@ -53,6 +53,7 @@ export async function executeBashWithOperations(
|
|||
): Promise<BashResult> {
|
||||
const outputChunks: string[] = [];
|
||||
let outputBytes = 0;
|
||||
let discardedOutput = false;
|
||||
const maxOutputBytes = DEFAULT_MAX_BYTES * 2;
|
||||
|
||||
let tempFilePath: string | undefined;
|
||||
|
|
@ -95,6 +96,7 @@ export async function executeBashWithOperations(
|
|||
while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
|
||||
const removed = outputChunks.shift()!;
|
||||
outputBytes -= removed.length;
|
||||
discardedOutput = true;
|
||||
}
|
||||
|
||||
// Stream to callback
|
||||
|
|
@ -140,7 +142,7 @@ export async function executeBashWithOperations(
|
|||
output: truncationResult.truncated ? truncationResult.content : fullOutput,
|
||||
exitCode: cancelled ? undefined : (exitCode ?? undefined),
|
||||
cancelled,
|
||||
truncated: truncationResult.truncated,
|
||||
truncated: discardedOutput || truncationResult.truncated,
|
||||
fullOutputPath: tempFilePath,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
59
packages/coding-agent/test/bash-executor-truncation.test.ts
Normal file
59
packages/coding-agent/test/bash-executor-truncation.test.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { readFile, rm } from "node:fs/promises";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { executeBashWithOperations } from "../src/core/bash-executor.ts";
|
||||
import { bashExecutionToText } from "../src/core/messages.ts";
|
||||
|
||||
describe("user bash output truncation (#10164)", () => {
|
||||
const tempFiles: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempFiles.splice(0).map((path) => rm(path, { force: true })));
|
||||
});
|
||||
|
||||
// #10164: the retained tail can fit even though earlier output was discarded.
|
||||
it.each([false, true])("reports discarded chunks when cancelled=%s", async (cancelled) => {
|
||||
const first = "error: missing build dependency\n".padEnd(64 * 1024, "x");
|
||||
const last = "remaining build output\n".padEnd(44 * 1024, "y");
|
||||
const controller = new AbortController();
|
||||
const result = await executeBashWithOperations(
|
||||
"cat build.log",
|
||||
process.cwd(),
|
||||
{
|
||||
exec: async (_command, _cwd, { onData }) => {
|
||||
onData(Buffer.from(first));
|
||||
onData(Buffer.from(last));
|
||||
if (cancelled) {
|
||||
controller.abort();
|
||||
throw new Error("aborted");
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
},
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (result.fullOutputPath) tempFiles.push(result.fullOutputPath);
|
||||
// The executor closes its file stream without awaiting its finish event.
|
||||
await vi.waitFor(async () => {
|
||||
expect(await readFile(result.fullOutputPath!, "utf-8")).toBe(first + last);
|
||||
});
|
||||
|
||||
expect(result.cancelled).toBe(cancelled);
|
||||
expect(result.output).toBe(last);
|
||||
expect(result.truncated).toBe(true);
|
||||
expect(
|
||||
bashExecutionToText({ role: "bashExecution", command: "cat build.log", timestamp: 0, ...result }),
|
||||
).toContain(`[Output truncated. Full output: ${result.fullOutputPath}]`);
|
||||
});
|
||||
|
||||
it("does not mark short output as truncated", async () => {
|
||||
const result = await executeBashWithOperations("echo done", process.cwd(), {
|
||||
exec: async (_command, _cwd, { onData }) => {
|
||||
onData(Buffer.from("done\n"));
|
||||
return { exitCode: 0 };
|
||||
},
|
||||
});
|
||||
expect(result.output).toBe("done\n");
|
||||
expect(result.truncated).toBe(false);
|
||||
expect(result.fullOutputPath).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue