惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
D
Docker
GbyAI
GbyAI
宝玉的分享
宝玉的分享
Jina AI
Jina AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Vercel News
Vercel News
博客园_首页
Recent Announcements
Recent Announcements
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
S
SegmentFault 最新的问题
Microsoft Security Blog
Microsoft Security Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
美团技术团队
V
V2EX
WordPress大学
WordPress大学
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
Visual Studio Blog
IT之家
IT之家
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(agent-sessions): fail oversized exec output · opencla...
vincentkoc · 2026-05-28 · via Recent Commits to openclaw:main

@@ -5,6 +5,9 @@

55

import { spawn } from "node:child_process";

66

import { waitForChildProcess } from "../utils/child-process.js";

778+

const DEFAULT_OUTPUT_LIMIT_CHARS = 16 * 1024 * 1024;

9+

const FORCE_KILL_GRACE_MS = 5000;

10+811

/**

912

* Options for executing shell commands.

1013

*/

@@ -15,6 +18,8 @@ export interface ExecOptions {

1518

timeout?: number;

1619

/** Working directory */

1720

cwd?: string;

21+

/** Optional maximum retained stdout/stderr characters per stream. */

22+

maxOutputChars?: number;

1823

}

19242025

/**

@@ -23,10 +28,46 @@ export interface ExecOptions {

2328

export interface ExecResult {

2429

stdout: string;

2530

stderr: string;

31+

stdoutTruncatedChars?: number;

32+

stderrTruncatedChars?: number;

33+

outputLimitExceeded?: "stdout" | "stderr";

2634

code: number;

2735

killed: boolean;

2836

}

293738+

type OutputCapture = {

39+

text: string;

40+

truncatedChars: number;

41+

};

42+43+

function clampMaxOutputChars(value: number | undefined): number {

44+

if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {

45+

return DEFAULT_OUTPUT_LIMIT_CHARS;

46+

}

47+

return Math.max(1, Math.floor(value));

48+

}

49+50+

function appendCapturedOutput(

51+

current: OutputCapture,

52+

chunk: Buffer | string,

53+

maxOutputChars: number,

54+

truncateTail: boolean,

55+

): OutputCapture {

56+

const text = String(chunk);

57+

const combined = `${current.text}${text}`;

58+

const overflowChars = Math.max(0, combined.length - maxOutputChars);

59+

if (overflowChars === 0) {

60+

return {

61+

text: combined,

62+

truncatedChars: current.truncatedChars,

63+

};

64+

}

65+

return {

66+

text: truncateTail ? combined.slice(overflowChars) : combined.slice(0, maxOutputChars),

67+

truncatedChars: current.truncatedChars + overflowChars,

68+

};

69+

}

70+3071

/**

3172

* Execute a shell command and return stdout/stderr/code.

3273

* Supports timeout and abort signal.

@@ -44,21 +85,64 @@ export async function execCommand(

4485

stdio: ["ignore", "pipe", "pipe"],

4586

});

468747-

let stdout = "";

48-

let stderr = "";

88+

let stdout: OutputCapture = { text: "", truncatedChars: 0 };

89+

let stderr: OutputCapture = { text: "", truncatedChars: 0 };

4990

let killed = false;

5091

let timeoutId: NodeJS.Timeout | undefined;

92+

let forceKillTimer: NodeJS.Timeout | undefined;

93+

let settled = false;

94+

const maxOutputChars = clampMaxOutputChars(options?.maxOutputChars);

95+

const truncateOutput = options?.maxOutputChars !== undefined;

96+

let outputLimitExceeded: "stdout" | "stderr" | undefined;

97+

const markOutputLimitExceeded = (stream: "stdout" | "stderr") => {

98+

if (!truncateOutput && !outputLimitExceeded) {

99+

outputLimitExceeded = stream;

100+

killProcess();

101+

}

102+

};

103+

const finish = (code: number) => {

104+

if (settled) {

105+

return;

106+

}

107+

settled = true;

108+

if (timeoutId) {

109+

clearTimeout(timeoutId);

110+

}

111+

if (forceKillTimer) {

112+

clearTimeout(forceKillTimer);

113+

}

114+

if (options?.signal) {

115+

options.signal.removeEventListener("abort", killProcess);

116+

}

117+

if (outputLimitExceeded) {

118+

stderr = appendCapturedOutput(

119+

stderr,

120+

`${stderr.text ? "\n" : ""}exec ${outputLimitExceeded} exceeded output limit ${maxOutputChars} chars`,

121+

maxOutputChars,

122+

true,

123+

);

124+

}

125+

resolve({

126+

stdout: stdout.text,

127+

stderr: stderr.text,

128+

stdoutTruncatedChars: stdout.truncatedChars || undefined,

129+

stderrTruncatedChars: stderr.truncatedChars || undefined,

130+

outputLimitExceeded,

131+

code: outputLimitExceeded ? 1 : code,

132+

killed,

133+

});

134+

};

5113552136

const killProcess = () => {

53137

if (!killed) {

54138

killed = true;

55139

proc.kill("SIGTERM");

56-

// Force kill after 5 seconds if SIGTERM doesn't work

57-

setTimeout(() => {

58-

if (!proc.killed) {

140+

forceKillTimer = setTimeout(() => {

141+

if (!settled) {

59142

proc.kill("SIGKILL");

60143

}

61-

}, 5000);

144+

}, FORCE_KILL_GRACE_MS);

145+

forceKillTimer.unref?.();

62146

}

63147

};

64148

@@ -79,33 +163,29 @@ export async function execCommand(

79163

}

8016481165

proc.stdout?.on("data", (data) => {

82-

stdout += data.toString();

166+

const before = stdout.truncatedChars;

167+

stdout = appendCapturedOutput(stdout, data, maxOutputChars, truncateOutput);

168+

if (stdout.truncatedChars > before) {

169+

markOutputLimitExceeded("stdout");

170+

}

83171

});

8417285173

proc.stderr?.on("data", (data) => {

86-

stderr += data.toString();

174+

const before = stderr.truncatedChars;

175+

stderr = appendCapturedOutput(stderr, data, maxOutputChars, truncateOutput);

176+

if (stderr.truncatedChars > before) {

177+

markOutputLimitExceeded("stderr");

178+

}

87179

});

8818089181

// Wait for process termination without hanging on inherited stdio handles

90182

// held open by detached descendants.

91183

waitForChildProcess(proc)

92184

.then((code) => {

93-

if (timeoutId) {

94-

clearTimeout(timeoutId);

95-

}

96-

if (options?.signal) {

97-

options.signal.removeEventListener("abort", killProcess);

98-

}

99-

resolve({ stdout, stderr, code: code ?? 0, killed });

185+

finish(code ?? 0);

100186

})

101187

.catch(() => {

102-

if (timeoutId) {

103-

clearTimeout(timeoutId);

104-

}

105-

if (options?.signal) {

106-

options.signal.removeEventListener("abort", killProcess);

107-

}

108-

resolve({ stdout, stderr, code: 1, killed });

188+

finish(1);

109189

});

110190

});

111191

}