fix(release): bound cross-os agent log fallback reads

This commit is contained in:
Vincent Koc
2026-06-04 10:14:31 +02:00
parent ee282c6de5
commit 10b9df6d8a
2 changed files with 99 additions and 20 deletions

View File

@@ -10,7 +10,10 @@ import {
createWriteStream, createWriteStream,
existsSync, existsSync,
mkdirSync, mkdirSync,
closeSync,
openSync,
readFileSync, readFileSync,
readSync,
readdirSync, readdirSync,
realpathSync, realpathSync,
rmSync, rmSync,
@@ -53,6 +56,7 @@ export const CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS = parsePositiveIntegerEnv(
600, 600,
); );
export const CROSS_OS_COMMAND_CAPTURE_TAIL_BYTES = 16 * 1024 * 1024; 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( const CROSS_OS_PROCESS_TREE_KILL_AFTER_MS = parsePositiveIntegerEnv(
"OPENCLAW_CROSS_OS_PROCESS_TREE_KILL_AFTER_MS", "OPENCLAW_CROSS_OS_PROCESS_TREE_KILL_AFTER_MS",
15_000, 15_000,
@@ -3276,12 +3280,8 @@ export function shouldSkipOptionalCrossOsAgentTurnError(error, logPath) {
if (!/Agent output did not contain the expected OK marker/u.test(message)) { if (!/Agent output did not contain the expected OK marker/u.test(message)) {
return false; return false;
} }
try { const log = readLogTextTail(logPath);
const log = readFileSync(logPath, "utf8"); return /"status"\s*:\s*"timeout"|Request timed out before a response was generated/u.test(log);
return /"status"\s*:\s*"timeout"|Request timed out before a response was generated/u.test(log);
} catch {
return false;
}
} }
function buildReleaseAgentTurnArgs(sessionId) { function buildReleaseAgentTurnArgs(sessionId) {
@@ -3313,7 +3313,7 @@ export function agentTurnUsedEmbeddedFallback(result, options = {}) {
typeof options.logText === "string" typeof options.logText === "string"
? options.logText ? options.logText
: typeof options.logPath === "string" : typeof options.logPath === "string"
? safeReadTextFile(options.logPath) ? readLogTextTail(options.logPath)
: ""; : "";
return /EMBEDDED FALLBACK:/u.test(`${result.stdout ?? ""}\n${result.stderr ?? ""}\n${logText}`); 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") { if (typeof options.logPath !== "string") {
return false; return false;
} }
try { const logTexts = parseAgentPayloadTexts(readLogTextTail(options.logPath));
const logTexts = parseAgentPayloadTexts(readFileSync(options.logPath, "utf8")); return logTexts.some((text) => text.trim() === "OK");
return logTexts.some((text) => text.trim() === "OK");
} catch {
return false;
}
} }
function readLogFileSize(logPath) { function readLogFileSize(logPath) {
@@ -3347,19 +3343,52 @@ function readLogFileSize(logPath) {
} }
function readLogTextSince(logPath, offsetBytes) { 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 { try {
return readFileSync(logPath).subarray(offsetBytes).toString("utf8"); stat = statSync(logPath);
} catch { } catch {
return ""; return "";
} }
} if (!stat.isFile() || stat.size <= 0) {
function safeReadTextFile(logPath) {
try {
return readFileSync(logPath, "utf8");
} catch {
return ""; 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) { function parseAgentPayloadTexts(stdout) {

View File

@@ -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", () => { it("retries transient agent-turn failures", () => {
expect( expect(
shouldRetryCrossOsAgentTurnError( 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", () => { it("only skips opted-in cross-OS live agent turns after retry exhaustion", () => {
const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-agent-skip-retry-")); const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-agent-skip-retry-"));
try { try {