Skip to content

Commit 10b9df6

Browse files
committed
fix(release): bound cross-os agent log fallback reads
1 parent ee282c6 commit 10b9df6

2 files changed

Lines changed: 98 additions & 19 deletions

File tree

scripts/openclaw-cross-os-release-checks.ts

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import {
1010
createWriteStream,
1111
existsSync,
1212
mkdirSync,
13+
closeSync,
14+
openSync,
1315
readFileSync,
16+
readSync,
1417
readdirSync,
1518
realpathSync,
1619
rmSync,
@@ -53,6 +56,7 @@ export const CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS = parsePositiveIntegerEnv(
5356
600,
5457
);
5558
export const CROSS_OS_COMMAND_CAPTURE_TAIL_BYTES = 16 * 1024 * 1024;
59+
const CROSS_OS_AGENT_LOG_FALLBACK_TAIL_BYTES = 2 * 1024 * 1024;
5660
const CROSS_OS_PROCESS_TREE_KILL_AFTER_MS = parsePositiveIntegerEnv(
5761
"OPENCLAW_CROSS_OS_PROCESS_TREE_KILL_AFTER_MS",
5862
15_000,
@@ -3276,12 +3280,8 @@ export function shouldSkipOptionalCrossOsAgentTurnError(error, logPath) {
32763280
if (!/Agent output did not contain the expected OK marker/u.test(message)) {
32773281
return false;
32783282
}
3279-
try {
3280-
const log = readFileSync(logPath, "utf8");
3281-
return /"status"\s*:\s*"timeout"|Request timed out before a response was generated/u.test(log);
3282-
} catch {
3283-
return false;
3284-
}
3283+
const log = readLogTextTail(logPath);
3284+
return /"status"\s*:\s*"timeout"|Request timed out before a response was generated/u.test(log);
32853285
}
32863286

32873287
function buildReleaseAgentTurnArgs(sessionId) {
@@ -3313,7 +3313,7 @@ export function agentTurnUsedEmbeddedFallback(result, options = {}) {
33133313
typeof options.logText === "string"
33143314
? options.logText
33153315
: typeof options.logPath === "string"
3316-
? safeReadTextFile(options.logPath)
3316+
? readLogTextTail(options.logPath)
33173317
: "";
33183318
return /EMBEDDED FALLBACK:/u.test(`${result.stdout ?? ""}\n${result.stderr ?? ""}\n${logText}`);
33193319
}
@@ -3330,12 +3330,8 @@ export function agentOutputHasExpectedOkMarker(stdout, options = {}) {
33303330
if (typeof options.logPath !== "string") {
33313331
return false;
33323332
}
3333-
try {
3334-
const logTexts = parseAgentPayloadTexts(readFileSync(options.logPath, "utf8"));
3335-
return logTexts.some((text) => text.trim() === "OK");
3336-
} catch {
3337-
return false;
3338-
}
3333+
const logTexts = parseAgentPayloadTexts(readLogTextTail(options.logPath));
3334+
return logTexts.some((text) => text.trim() === "OK");
33393335
}
33403336

33413337
function readLogFileSize(logPath) {
@@ -3347,19 +3343,52 @@ function readLogFileSize(logPath) {
33473343
}
33483344

33493345
function readLogTextSince(logPath, offsetBytes) {
3346+
return readLogTextWindow(logPath, {
3347+
offsetBytes,
3348+
maxBytes: CROSS_OS_AGENT_LOG_FALLBACK_TAIL_BYTES,
3349+
});
3350+
}
3351+
3352+
function readLogTextTail(logPath) {
3353+
return readLogTextWindow(logPath, {
3354+
maxBytes: CROSS_OS_AGENT_LOG_FALLBACK_TAIL_BYTES,
3355+
});
3356+
}
3357+
3358+
function readLogTextWindow(logPath, options = {}) {
3359+
const maxBytes = Math.max(
3360+
1,
3361+
Math.floor(options.maxBytes ?? CROSS_OS_AGENT_LOG_FALLBACK_TAIL_BYTES),
3362+
);
3363+
const offsetBytes =
3364+
typeof options.offsetBytes === "number" && Number.isFinite(options.offsetBytes)
3365+
? Math.max(0, Math.floor(options.offsetBytes))
3366+
: 0;
3367+
let stat;
33503368
try {
3351-
return readFileSync(logPath).subarray(offsetBytes).toString("utf8");
3369+
stat = statSync(logPath);
33523370
} catch {
33533371
return "";
33543372
}
3355-
}
3373+
if (!stat.isFile() || stat.size <= 0) {
3374+
return "";
3375+
}
33563376

3357-
function safeReadTextFile(logPath) {
3358-
try {
3359-
return readFileSync(logPath, "utf8");
3360-
} catch {
3377+
const tailStart = Math.max(0, stat.size - maxBytes);
3378+
const start = Math.min(stat.size, Math.max(offsetBytes, tailStart));
3379+
const length = stat.size - start;
3380+
if (length <= 0) {
33613381
return "";
33623382
}
3383+
3384+
const fd = openSync(logPath, "r");
3385+
try {
3386+
const buffer = Buffer.alloc(length);
3387+
const bytesRead = readSync(fd, buffer, 0, length, start);
3388+
return buffer.subarray(0, bytesRead).toString("utf8");
3389+
} finally {
3390+
closeSync(fd);
3391+
}
33633392
}
33643393

33653394
function parseAgentPayloadTexts(stdout) {

test/scripts/openclaw-cross-os-release-checks.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,29 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
255255
}
256256
});
257257

258+
it("ignores stale OK markers outside the recent agent log tail", () => {
259+
const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-agent-output-tail-"));
260+
try {
261+
const logPath = join(dir, "agent.log");
262+
writeFileSync(
263+
logPath,
264+
[
265+
JSON.stringify({
266+
payloads: [{ type: "text", text: "OK" }],
267+
}),
268+
"x".repeat(2_200_000),
269+
JSON.stringify({
270+
payloads: [{ type: "text", text: "still working" }],
271+
}),
272+
].join("\n"),
273+
);
274+
275+
expect(agentOutputHasExpectedOkMarker("", { logPath })).toBe(false);
276+
} finally {
277+
rmSync(dir, { recursive: true, force: true });
278+
}
279+
});
280+
258281
it("retries transient agent-turn failures", () => {
259282
expect(
260283
shouldRetryCrossOsAgentTurnError(
@@ -365,6 +388,33 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
365388
}
366389
});
367390

391+
it("does not classify stale timeout logs as current optional agent-turn failures", () => {
392+
const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-agent-skip-tail-"));
393+
try {
394+
const logPath = join(dir, "agent.log");
395+
writeFileSync(
396+
logPath,
397+
[
398+
JSON.stringify({
399+
status: "timeout",
400+
result: { payloads: [{ text: "Request timed out before a response was generated." }] },
401+
}),
402+
"x".repeat(2_200_000),
403+
JSON.stringify({ status: "error", message: "document-extract failed" }),
404+
].join("\n"),
405+
);
406+
407+
expect(
408+
shouldSkipOptionalCrossOsAgentTurnError(
409+
new Error("Agent output did not contain the expected OK marker."),
410+
logPath,
411+
),
412+
).toBe(false);
413+
} finally {
414+
rmSync(dir, { recursive: true, force: true });
415+
}
416+
});
417+
368418
it("only skips opted-in cross-OS live agent turns after retry exhaustion", () => {
369419
const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-agent-skip-retry-"));
370420
try {

0 commit comments

Comments
 (0)