fix(release): fail closed on cross-os agent turns

This commit is contained in:
Vincent Koc
2026-05-26 14:06:02 +02:00
parent 419178b9bc
commit dfe94ff048
2 changed files with 76 additions and 7 deletions

View File

@@ -41,7 +41,7 @@ export const CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS = parsePositiveIntegerEnv(
"OPENCLAW_CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS",
600,
);
const CROSS_OS_AGENT_TURN_OPTIONAL = parseBooleanEnv("OPENCLAW_CROSS_OS_AGENT_TURN_OPTIONAL", true);
const CROSS_OS_AGENT_TURN_OPTIONAL = resolveCrossOsAgentTurnOptional();
const providerConfig = {
openai: {
@@ -180,8 +180,8 @@ function parsePositiveIntegerEnv(name, fallback) {
return value;
}
function parseBooleanEnv(name, fallback) {
const raw = process.env[name]?.trim();
function parseBooleanEnv(name, fallback, env = process.env) {
const raw = env[name]?.trim();
if (!raw) {
return fallback;
}
@@ -194,6 +194,10 @@ function parseBooleanEnv(name, fallback) {
throw new Error(`${name} must be a boolean. Got: ${JSON.stringify(raw)}`);
}
export function resolveCrossOsAgentTurnOptional(env = process.env) {
return parseBooleanEnv("OPENCLAW_CROSS_OS_AGENT_TURN_OPTIONAL", false, env);
}
export function looksLikeReleaseVersionRef(ref) {
const trimmed = normalizeRequestedRef(ref);
return /^v?[0-9]{4}\.[0-9]+\.[0-9]+(?:-(?:[1-9][0-9]*)|[-.](?:alpha|beta|rc)[-.]?[0-9]+)?$/iu.test(
@@ -2274,7 +2278,10 @@ async function runInstalledAgentTurn(params) {
return result;
} catch (error) {
lastError = error;
const skipped = maybeBuildOptionalAgentTurnSkipResult(error, params.logPath);
const skipped = maybeBuildOptionalAgentTurnSkipResult(error, params.logPath, {
attempt,
maxAttempts: 2,
});
if (skipped) {
return skipped;
}
@@ -3090,7 +3097,10 @@ async function runAgentTurn(params) {
return result;
} catch (error) {
lastError = error;
const skipped = maybeBuildOptionalAgentTurnSkipResult(error, params.logPath);
const skipped = maybeBuildOptionalAgentTurnSkipResult(error, params.logPath, {
attempt,
maxAttempts: 2,
});
if (skipped) {
return skipped;
}
@@ -3108,8 +3118,15 @@ async function runAgentTurn(params) {
throw lastError;
}
function maybeBuildOptionalAgentTurnSkipResult(error, logPath) {
if (!CROSS_OS_AGENT_TURN_OPTIONAL || !shouldSkipOptionalCrossOsAgentTurnError(error, logPath)) {
export function maybeBuildOptionalAgentTurnSkipResult(error, logPath, options = {}) {
const attempt = options.attempt ?? 1;
const maxAttempts = options.maxAttempts ?? 2;
const optional = options.optional ?? CROSS_OS_AGENT_TURN_OPTIONAL;
if (
attempt < maxAttempts ||
!optional ||
!shouldSkipOptionalCrossOsAgentTurnError(error, logPath)
) {
return null;
}
const message = error instanceof Error ? error.message : String(error);

View File

@@ -45,11 +45,13 @@ import {
normalizeRequestedRef,
normalizeWindowsCommandShimPath,
normalizeWindowsInstalledCliPath,
maybeBuildOptionalAgentTurnSkipResult,
parseCrossOsSuiteFilter,
parseArgs,
packageHasScript,
readInstalledVersion,
readRunnerOverrideEnv,
resolveCrossOsAgentTurnOptional,
runCommand,
resolveCommandSpawnInvocation,
resolveExplicitBaselineVersion,
@@ -174,6 +176,16 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
).toBe(true);
});
it("requires explicit opt-in before cross-OS agent turns become optional", () => {
expect(resolveCrossOsAgentTurnOptional({})).toBe(false);
expect(resolveCrossOsAgentTurnOptional({ OPENCLAW_CROSS_OS_AGENT_TURN_OPTIONAL: "1" })).toBe(
true,
);
expect(resolveCrossOsAgentTurnOptional({ OPENCLAW_CROSS_OS_AGENT_TURN_OPTIONAL: "false" })).toBe(
false,
);
});
it("detects embedded fallback agent turns as non-gateway proof", () => {
const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-agent-fallback-"));
const logPath = join(dir, "agent.log");
@@ -244,6 +256,46 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
}
});
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 {
const logPath = join(dir, "agent.log");
const error = new Error("gateway request timeout for agent after 210000ms");
expect(
maybeBuildOptionalAgentTurnSkipResult(error, logPath, {
attempt: 1,
maxAttempts: 2,
optional: true,
}),
).toBeNull();
expect(
maybeBuildOptionalAgentTurnSkipResult(error, logPath, {
attempt: 2,
maxAttempts: 2,
optional: false,
}),
).toBeNull();
const skipped = maybeBuildOptionalAgentTurnSkipResult(error, logPath, {
attempt: 2,
maxAttempts: 2,
optional: true,
});
expect(skipped?.status).toBe(0);
expect(JSON.parse(skipped?.stdout ?? "{}")).toEqual({
status: "skipped",
reason: "cross-os live agent turn unavailable after retry",
});
expect(readFileSync(logPath, "utf8")).toContain(
"skipping optional cross-OS live agent turn after retryable failure",
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("allows cross-OS provider smoke models to use faster CI overrides", () => {
expect(
resolveProviderConfig("openai", {