Files
openclaw/src/agents/run-cleanup-timeout.ts
Val Alexander 5d4a8b0072 fix(agents): make trajectory cleanup timeout configurable
Refs #75839.\n\nAdds OPENCLAW_TRAJECTORY_FLUSH_TIMEOUT_MS and OPENCLAW_AGENT_CLEANUP_TIMEOUT_MS for agent cleanup steps while preserving the 10s default. Includes focused timeout precedence tests, trajectory docs, and changelog coverage.\n\nVerification:\n- pnpm test src/agents/run-cleanup-timeout.test.ts\n- pnpm exec oxfmt --check --threads=1 src/agents/run-cleanup-timeout.ts src/agents/run-cleanup-timeout.test.ts\n- pnpm format:docs:check docs/tools/trajectory.md\n- git diff --check\n- pnpm check:changed\n- GitHub PR checks: 88 passing, CodeQL neutral, 21 skipped
2026-05-13 23:09:56 -05:00

101 lines
3.1 KiB
TypeScript

import { formatErrorMessage } from "../infra/errors.js";
export const AGENT_CLEANUP_STEP_TIMEOUT_MS = 10_000;
export const AGENT_CLEANUP_STEP_TIMEOUT_ENV = "OPENCLAW_AGENT_CLEANUP_TIMEOUT_MS";
export const TRAJECTORY_FLUSH_TIMEOUT_ENV = "OPENCLAW_TRAJECTORY_FLUSH_TIMEOUT_MS";
type AgentCleanupLogger = {
warn: (message: string) => void;
};
function normalizeExplicitTimeoutMs(value: unknown): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value)) {
return undefined;
}
return Math.max(1, Math.floor(value));
}
function parseTimeoutEnvValue(value: string | undefined): number | undefined {
const trimmed = value?.trim();
if (!trimmed) {
return undefined;
}
const timeoutMs = Number(trimmed);
if (!Number.isFinite(timeoutMs)) {
return undefined;
}
const normalized = Math.floor(timeoutMs);
return normalized > 0 ? normalized : undefined;
}
export function resolveAgentCleanupStepTimeoutMs(params: {
step: string;
timeoutMs?: number;
env?: NodeJS.ProcessEnv;
}): number {
const explicitTimeoutMs = normalizeExplicitTimeoutMs(params.timeoutMs);
if (explicitTimeoutMs !== undefined) {
return explicitTimeoutMs;
}
const env = params.env ?? process.env;
if (params.step === "pi-trajectory-flush") {
const trajectoryTimeoutMs = parseTimeoutEnvValue(env[TRAJECTORY_FLUSH_TIMEOUT_ENV]);
if (trajectoryTimeoutMs !== undefined) {
return trajectoryTimeoutMs;
}
}
return parseTimeoutEnvValue(env[AGENT_CLEANUP_STEP_TIMEOUT_ENV]) ?? AGENT_CLEANUP_STEP_TIMEOUT_MS;
}
export async function runAgentCleanupStep(params: {
runId: string;
sessionId: string;
step: string;
cleanup: () => Promise<void>;
log: AgentCleanupLogger;
env?: NodeJS.ProcessEnv;
timeoutMs?: number;
}): Promise<void> {
const timeoutMs = resolveAgentCleanupStepTimeoutMs({
step: params.step,
timeoutMs: params.timeoutMs,
env: params.env,
});
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
let timedOut = false;
const cleanupPromise = Promise.resolve().then(params.cleanup);
const observedCleanupPromise = cleanupPromise.catch((error) => {
if (!timedOut) {
params.log.warn(
`agent cleanup failed: runId=${params.runId} sessionId=${params.sessionId} step=${params.step} error=${formatErrorMessage(error)}`,
);
}
});
const timeoutPromise = new Promise<"timeout">((resolve) => {
timeoutHandle = setTimeout(() => {
timedOut = true;
resolve("timeout");
}, timeoutMs);
timeoutHandle.unref?.();
});
const result = await Promise.race([
observedCleanupPromise.then(() => "done" as const),
timeoutPromise,
]);
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
if (result === "timeout") {
params.log.warn(
`agent cleanup timed out: runId=${params.runId} sessionId=${params.sessionId} step=${params.step} timeoutMs=${timeoutMs}`,
);
void cleanupPromise.catch((error) => {
params.log.warn(
`agent cleanup rejected after timeout: runId=${params.runId} sessionId=${params.sessionId} step=${params.step} error=${formatErrorMessage(error)}`,
);
});
}
}