From 10b9df6d8ac6cc289a3728e4737c9faeae3531eb Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 4 Jun 2026 10:14:31 +0200 Subject: [PATCH] fix(release): bound cross-os agent log fallback reads --- scripts/openclaw-cross-os-release-checks.ts | 69 +++++++++++++------ .../openclaw-cross-os-release-checks.test.ts | 50 ++++++++++++++ 2 files changed, 99 insertions(+), 20 deletions(-) diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index 7a42d26f224a..68d995c5d9a2 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -10,7 +10,10 @@ import { createWriteStream, existsSync, mkdirSync, + closeSync, + openSync, readFileSync, + readSync, readdirSync, realpathSync, rmSync, @@ -53,6 +56,7 @@ export const CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS = parsePositiveIntegerEnv( 600, ); export const CROSS_OS_COMMAND_CAPTURE_TAIL_BYTES = 16 * 1024 * 1024; +const CROSS_OS_AGENT_LOG_FALLBACK_TAIL_BYTES = 2 * 1024 * 1024; const CROSS_OS_PROCESS_TREE_KILL_AFTER_MS = parsePositiveIntegerEnv( "OPENCLAW_CROSS_OS_PROCESS_TREE_KILL_AFTER_MS", 15_000, @@ -3276,12 +3280,8 @@ export function shouldSkipOptionalCrossOsAgentTurnError(error, logPath) { if (!/Agent output did not contain the expected OK marker/u.test(message)) { return false; } - try { - const log = readFileSync(logPath, "utf8"); - return /"status"\s*:\s*"timeout"|Request timed out before a response was generated/u.test(log); - } catch { - return false; - } + const log = readLogTextTail(logPath); + return /"status"\s*:\s*"timeout"|Request timed out before a response was generated/u.test(log); } function buildReleaseAgentTurnArgs(sessionId) { @@ -3313,7 +3313,7 @@ export function agentTurnUsedEmbeddedFallback(result, options = {}) { typeof options.logText === "string" ? options.logText : typeof options.logPath === "string" - ? safeReadTextFile(options.logPath) + ? readLogTextTail(options.logPath) : ""; return /EMBEDDED FALLBACK:/u.test(`${result.stdout ?? ""}\n${result.stderr ?? ""}\n${logText}`); } @@ -3330,12 +3330,8 @@ export function agentOutputHasExpectedOkMarker(stdout, options = {}) { if (typeof options.logPath !== "string") { return false; } - try { - const logTexts = parseAgentPayloadTexts(readFileSync(options.logPath, "utf8")); - return logTexts.some((text) => text.trim() === "OK"); - } catch { - return false; - } + const logTexts = parseAgentPayloadTexts(readLogTextTail(options.logPath)); + return logTexts.some((text) => text.trim() === "OK"); } function readLogFileSize(logPath) { @@ -3347,19 +3343,52 @@ function readLogFileSize(logPath) { } function readLogTextSince(logPath, offsetBytes) { + return readLogTextWindow(logPath, { + offsetBytes, + maxBytes: CROSS_OS_AGENT_LOG_FALLBACK_TAIL_BYTES, + }); +} + +function readLogTextTail(logPath) { + return readLogTextWindow(logPath, { + maxBytes: CROSS_OS_AGENT_LOG_FALLBACK_TAIL_BYTES, + }); +} + +function readLogTextWindow(logPath, options = {}) { + const maxBytes = Math.max( + 1, + Math.floor(options.maxBytes ?? CROSS_OS_AGENT_LOG_FALLBACK_TAIL_BYTES), + ); + const offsetBytes = + typeof options.offsetBytes === "number" && Number.isFinite(options.offsetBytes) + ? Math.max(0, Math.floor(options.offsetBytes)) + : 0; + let stat; try { - return readFileSync(logPath).subarray(offsetBytes).toString("utf8"); + stat = statSync(logPath); } catch { return ""; } -} - -function safeReadTextFile(logPath) { - try { - return readFileSync(logPath, "utf8"); - } catch { + if (!stat.isFile() || stat.size <= 0) { return ""; } + + const tailStart = Math.max(0, stat.size - maxBytes); + const start = Math.min(stat.size, Math.max(offsetBytes, tailStart)); + const length = stat.size - start; + if (length <= 0) { + return ""; + } + + const fd = openSync(logPath, "r"); + try { + const buffer = Buffer.alloc(length); + const bytesRead = readSync(fd, buffer, 0, length, start); + return buffer.subarray(0, bytesRead).toString("utf8"); + } finally { + closeSync(fd); + } } function parseAgentPayloadTexts(stdout) { diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index ce139e31ca1f..b3193e14160b 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -255,6 +255,29 @@ describe("scripts/openclaw-cross-os-release-checks", () => { } }); + it("ignores stale OK markers outside the recent agent log tail", () => { + const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-agent-output-tail-")); + try { + const logPath = join(dir, "agent.log"); + writeFileSync( + logPath, + [ + JSON.stringify({ + payloads: [{ type: "text", text: "OK" }], + }), + "x".repeat(2_200_000), + JSON.stringify({ + payloads: [{ type: "text", text: "still working" }], + }), + ].join("\n"), + ); + + expect(agentOutputHasExpectedOkMarker("", { logPath })).toBe(false); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("retries transient agent-turn failures", () => { expect( shouldRetryCrossOsAgentTurnError( @@ -365,6 +388,33 @@ describe("scripts/openclaw-cross-os-release-checks", () => { } }); + it("does not classify stale timeout logs as current optional agent-turn failures", () => { + const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-agent-skip-tail-")); + try { + const logPath = join(dir, "agent.log"); + writeFileSync( + logPath, + [ + JSON.stringify({ + status: "timeout", + result: { payloads: [{ text: "Request timed out before a response was generated." }] }, + }), + "x".repeat(2_200_000), + JSON.stringify({ status: "error", message: "document-extract failed" }), + ].join("\n"), + ); + + expect( + shouldSkipOptionalCrossOsAgentTurnError( + new Error("Agent output did not contain the expected OK marker."), + logPath, + ), + ).toBe(false); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("only skips opted-in cross-OS live agent turns after retry exhaustion", () => { const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-agent-skip-retry-")); try {