mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-01 11:03:32 +00:00
fix(supervisor): bound captured process output
This commit is contained in:
@@ -290,4 +290,42 @@ describe("process supervisor", () => {
|
||||
expect(streamed).toBe("streamed");
|
||||
expect(exit.stdout).toBe("");
|
||||
});
|
||||
|
||||
it("bounds retained stdout and stderr while streaming full chunks", async () => {
|
||||
const adapter = createStubChildAdapter();
|
||||
createChildAdapterMock.mockResolvedValue(adapter);
|
||||
|
||||
const supervisor = createProcessSupervisor();
|
||||
let streamedStdout = "";
|
||||
let streamedStderr = "";
|
||||
const stdoutChunk = `${"a".repeat(300)}stdout-tail`;
|
||||
const stderrChunk = `${"b".repeat(300)}stderr-tail`;
|
||||
const run = await spawnChild(supervisor, {
|
||||
sessionId: "s-capture-cap",
|
||||
argv: createWriteStdoutArgv(stdoutChunk),
|
||||
timeoutMs: 1_000,
|
||||
stdinMode: "pipe-closed",
|
||||
maxCapturedOutputChars: 256,
|
||||
onStdout: (chunk) => {
|
||||
streamedStdout += chunk;
|
||||
},
|
||||
onStderr: (chunk) => {
|
||||
streamedStderr += chunk;
|
||||
},
|
||||
});
|
||||
|
||||
adapter.emitStdout(stdoutChunk);
|
||||
adapter.emitStderr(stderrChunk);
|
||||
adapter.settle(0);
|
||||
|
||||
const exit = await run.wait();
|
||||
expect(streamedStdout).toBe(stdoutChunk);
|
||||
expect(streamedStderr).toBe(stderrChunk);
|
||||
expect(exit.stdout.length).toBeLessThanOrEqual(256);
|
||||
expect(exit.stderr.length).toBeLessThanOrEqual(256);
|
||||
expect(exit.stdout).toContain("captured stdout truncated");
|
||||
expect(exit.stderr).toContain("captured stderr truncated");
|
||||
expect(exit.stdout.endsWith("stdout-tail")).toBe(true);
|
||||
expect(exit.stderr.endsWith("stderr-tail")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ type ActiveRun = {
|
||||
};
|
||||
|
||||
const GRACEFUL_CANCEL_TIMEOUT_MS = 5000;
|
||||
const DEFAULT_MAX_CAPTURED_OUTPUT_CHARS = 1024 * 1024;
|
||||
|
||||
let supervisorLogRuntimePromise: Promise<SupervisorLogRuntime> | undefined;
|
||||
|
||||
@@ -36,6 +37,28 @@ function clampTimeout(value?: number): number | undefined {
|
||||
return Math.max(1, Math.floor(value));
|
||||
}
|
||||
|
||||
function clampCapturedOutputChars(value?: number): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
||||
return DEFAULT_MAX_CAPTURED_OUTPUT_CHARS;
|
||||
}
|
||||
return Math.max(256, Math.floor(value));
|
||||
}
|
||||
|
||||
function appendCapturedOutput(
|
||||
current: string,
|
||||
chunk: string,
|
||||
stream: "stdout" | "stderr",
|
||||
maxChars: number,
|
||||
) {
|
||||
const next = current + chunk;
|
||||
if (next.length <= maxChars) {
|
||||
return next;
|
||||
}
|
||||
const marker = `[openclaw: captured ${stream} truncated to last ${maxChars} chars]\n`;
|
||||
const tailChars = Math.max(0, maxChars - marker.length);
|
||||
return `${marker}${next.slice(-tailChars)}`;
|
||||
}
|
||||
|
||||
function isTimeoutReason(reason: TerminationReason) {
|
||||
return reason === "overall-timeout" || reason === "no-output-timeout";
|
||||
}
|
||||
@@ -95,6 +118,7 @@ export function createProcessSupervisor(): ProcessSupervisor {
|
||||
let noOutputTimer: NodeJS.Timeout | null = null;
|
||||
let forceKillTimer: NodeJS.Timeout | null = null;
|
||||
const captureOutput = input.captureOutput !== false;
|
||||
const maxCapturedOutputChars = clampCapturedOutputChars(input.maxCapturedOutputChars);
|
||||
|
||||
const overallTimeoutMs = clampTimeout(input.timeoutMs);
|
||||
const noOutputTimeoutMs = clampTimeout(input.noOutputTimeoutMs);
|
||||
@@ -198,14 +222,14 @@ export function createProcessSupervisor(): ProcessSupervisor {
|
||||
|
||||
adapter.onStdout((chunk) => {
|
||||
if (captureOutput) {
|
||||
stdout += chunk;
|
||||
stdout = appendCapturedOutput(stdout, chunk, "stdout", maxCapturedOutputChars);
|
||||
}
|
||||
input.onStdout?.(chunk);
|
||||
touchOutput();
|
||||
});
|
||||
adapter.onStderr((chunk) => {
|
||||
if (captureOutput) {
|
||||
stderr += chunk;
|
||||
stderr = appendCapturedOutput(stderr, chunk, "stderr", maxCapturedOutputChars);
|
||||
}
|
||||
input.onStderr?.(chunk);
|
||||
touchOutput();
|
||||
|
||||
@@ -81,6 +81,11 @@ type SpawnBaseInput = {
|
||||
* When false, stdout/stderr are streamed via callbacks only and not retained in RunExit payload.
|
||||
*/
|
||||
captureOutput?: boolean;
|
||||
/**
|
||||
* Maximum retained stdout/stderr characters per stream when captureOutput is enabled.
|
||||
* Streaming callbacks still receive full chunks.
|
||||
*/
|
||||
maxCapturedOutputChars?: number;
|
||||
onStdout?: (chunk: string) => void;
|
||||
onStderr?: (chunk: string) => void;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user