






















@@ -7,6 +7,10 @@ const agentTurnTimeoutSeconds = Number.parseInt(
77process.env.OPENCLAW_LIVE_PLUGIN_TOOL_TIMEOUT_SECONDS ?? "300",
8810,
99);
10+const SCAN_CHUNK_BYTES = 64 * 1024;
11+const SCAN_CARRY_CHARS = 256;
12+const ERROR_DETAIL_TAIL_BYTES = 16 * 1024;
13+const SESSION_FILE_LIST_LIMIT = 20;
10141115function requireEnv(name) {
1216const value = process.env[name];
@@ -24,6 +28,124 @@ function configPath() {
2428return process.env.OPENCLAW_CONFIG_PATH || path.join(stateDir(), "openclaw.json");
2529}
263031+function agentOutputPath() {
32+return process.env.OPENCLAW_LIVE_PLUGIN_TOOL_AGENT_OUTPUT_PATH || "/tmp/openclaw-agent.json";
33+}
34+35+function agentErrorPath() {
36+return process.env.OPENCLAW_LIVE_PLUGIN_TOOL_AGENT_ERROR_PATH || "/tmp/openclaw-agent.err";
37+}
38+39+function tailText(text, maxBytes = ERROR_DETAIL_TAIL_BYTES) {
40+if (Buffer.byteLength(text, "utf8") <= maxBytes) {
41+return text;
42+}
43+return Buffer.from(text, "utf8").subarray(-maxBytes).toString("utf8");
44+}
45+46+function readTextFileTail(file, maxBytes = ERROR_DETAIL_TAIL_BYTES) {
47+let stat;
48+try {
49+stat = fs.statSync(file);
50+} catch {
51+return "";
52+}
53+if (!stat.isFile() || stat.size <= 0) {
54+return "";
55+}
56+57+const length = Math.min(maxBytes, stat.size);
58+const start = stat.size - length;
59+const fd = fs.openSync(file, "r");
60+try {
61+const buffer = Buffer.alloc(length);
62+const bytesRead = fs.readSync(fd, buffer, 0, length, start);
63+return buffer.subarray(0, bytesRead).toString("utf8");
64+} finally {
65+fs.closeSync(fd);
66+}
67+}
68+69+function scanFileForNeedles(file, pendingNeedles) {
70+let stat;
71+try {
72+stat = fs.statSync(file);
73+} catch {
74+return;
75+}
76+if (!stat.isFile() || stat.size <= 0 || pendingNeedles.size === 0) {
77+return;
78+}
79+80+const maxNeedleLength = Math.max(...Array.from(pendingNeedles, (needle) => needle.length));
81+const carryChars = Math.max(SCAN_CARRY_CHARS, maxNeedleLength - 1);
82+const fd = fs.openSync(file, "r");
83+try {
84+const buffer = Buffer.alloc(Math.min(SCAN_CHUNK_BYTES, stat.size));
85+let carry = "";
86+let offset = 0;
87+while (offset < stat.size && pendingNeedles.size > 0) {
88+const bytesToRead = Math.min(buffer.length, stat.size - offset);
89+const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, offset);
90+if (bytesRead <= 0) {
91+break;
92+}
93+offset += bytesRead;
94+const text = carry + buffer.subarray(0, bytesRead).toString("utf8");
95+for (const needle of Array.from(pendingNeedles)) {
96+if (text.includes(needle)) {
97+pendingNeedles.delete(needle);
98+}
99+}
100+carry = text.slice(-carryChars);
101+}
102+} finally {
103+fs.closeSync(fd);
104+}
105+}
106+107+function scanSessionTranscripts(sessionsDir, needles) {
108+const pendingNeedles = new Set(needles);
109+const checkedFiles = [];
110+let filesChecked = 0;
111+let stat;
112+try {
113+stat = fs.statSync(sessionsDir);
114+} catch {
115+return { checkedFiles, filesChecked, missingDir: true, pendingNeedles };
116+}
117+if (!stat.isDirectory()) {
118+return { checkedFiles, filesChecked, missingDir: true, pendingNeedles };
119+}
120+121+const pendingDirs = [sessionsDir];
122+while (pendingDirs.length > 0 && pendingNeedles.size > 0) {
123+const dir = pendingDirs.pop();
124+const entries = fs
125+.readdirSync(dir, { withFileTypes: true })
126+.toSorted((left, right) => left.name.localeCompare(right.name));
127+for (const entry of entries) {
128+const entryPath = path.join(dir, entry.name);
129+if (entry.isDirectory()) {
130+pendingDirs.push(entryPath);
131+continue;
132+}
133+if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
134+continue;
135+}
136+filesChecked += 1;
137+if (checkedFiles.length < SESSION_FILE_LIST_LIMIT) {
138+checkedFiles.push(path.relative(sessionsDir, entryPath));
139+}
140+scanFileForNeedles(entryPath, pendingNeedles);
141+if (pendingNeedles.size === 0) {
142+break;
143+}
144+}
145+}
146+return { checkedFiles, filesChecked, missingDir: false, pendingNeedles };
147+}
148+27149function realPathMaybe(filePath) {
28150try {
29151return fs.realpathSync(filePath);
@@ -235,26 +357,24 @@ function assertInstalled() {
235357function assertAgentTurn() {
236358const expected = requireEnv("EXPECTED_SLUG");
237359const toolName = requireEnv("TOOL_NAME");
238-const stdout = fs.readFileSync("/tmp/openclaw-agent.json", "utf8");
239-const stderr = fs.existsSync("/tmp/openclaw-agent.err")
240- ? fs.readFileSync("/tmp/openclaw-agent.err", "utf8")
241- : "";
360+const outputPath = agentOutputPath();
361+const errorPath = agentErrorPath();
362+const stdout = fs.readFileSync(outputPath, "utf8");
242363const response = JSON.parse(stdout);
243364const text = (response.payloads || []).map((payload) => payload?.text || "").join("\n");
244365if (!text.includes(expected)) {
366+const stderrTail = readTextFileTail(errorPath);
245367throw new Error(
246-`live agent reply did not contain tool slug ${expected}:\nstdout=${stdout}\nstderr=${stderr}`,
368+`live agent reply did not contain tool slug ${expected}:\nstdout tail=${tailText(stdout)}\nstderr tail=${stderrTail}`,
247369);
248370}
249371const sessionsDir = path.join(stateDir(), "agents", "main", "sessions");
250-const sessionFiles = fs
251-.readdirSync(sessionsDir, { recursive: true })
252-.map((entry) => path.join(sessionsDir, String(entry)))
253-.filter((entry) => entry.endsWith(".jsonl") && fs.existsSync(entry));
254-const transcript = sessionFiles.map((file) => fs.readFileSync(file, "utf8")).join("\n");
255-if (!transcript.includes(toolName) || !transcript.includes(expected)) {
372+const scan = scanSessionTranscripts(sessionsDir, [toolName, expected]);
373+if (scan.pendingNeedles.size > 0) {
374+const checkedFiles = scan.checkedFiles.length > 0 ? scan.checkedFiles.join(", ") : "<none>";
375+const missingDir = scan.missingDir ? " sessions directory was missing." : "";
256376throw new Error(
257-`session transcript did not show ${toolName} returning ${expected}; checked ${sessionFiles.join(", ")}`,
377+`session transcript did not show ${toolName} returning ${expected}; missing ${Array.from(scan.pendingNeedles).join(", ")} after checking ${scan.filesChecked} jsonl file(s): ${checkedFiles}.${missingDir}`,
258378);
259379}
260380}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。