fix(agents): keep cleanup timeout details UTF-16 safe (#102565)

This commit is contained in:
xingzhou 2026-07-09 17:17:59 +08:00 committed by GitHub
parent 3f2466c4c3
commit 4ad94febcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 48 additions and 22 deletions

View file

@ -4,6 +4,7 @@ import { runAgentCleanupStep } from "./run-cleanup-timeout.js";
const AGENT_CLEANUP_STEP_TIMEOUT_MS = 10_000;
const CLEANUP_TIMEOUT_DETAILS_MAX_CHARS = 512;
const CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX = "...[truncated]";
describe("agent cleanup timeout", () => {
const log = {
@ -115,6 +116,33 @@ describe("agent cleanup timeout", () => {
);
});
it("keeps truncated cleanup timeout details UTF-16 safe", async () => {
const cleanup = vi.fn(async () => new Promise<never>(() => {}));
const prefixLength =
CLEANUP_TIMEOUT_DETAILS_MAX_CHARS - CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX.length;
const detailsPrefix = "a".repeat(prefixLength - 1);
const oversizedDetails = `${detailsPrefix}😀${"b".repeat(CLEANUP_TIMEOUT_DETAILS_MAX_CHARS)}`;
const result = runAgentCleanupStep({
runId: "run-trajectory",
sessionId: "session-trajectory",
step: "agent-trajectory-flush",
cleanup,
log,
timeoutMs: 5,
getTimeoutDetails: () => oversizedDetails,
});
await vi.advanceTimersByTimeAsync(5);
await expect(result).resolves.toBeUndefined();
const message = String(log.warn.mock.calls.at(-1)?.[0] ?? "");
expect(message).toContain(
` details=${detailsPrefix}${CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX}`,
);
expect(message).not.toContain("<22>");
});
it("does not fail cleanup when timeout details throw", async () => {
const cleanup = vi.fn(async () => new Promise<never>(() => {}));
@ -257,31 +285,28 @@ describe("agent cleanup timeout", () => {
OPENCLAW_AGENT_CLEANUP_TIMEOUT_MS: "0x10",
},
},
])(
"ignores invalid cleanup timeout environment values",
async ({ runId, sessionId, env }) => {
const cleanup = vi.fn(async () => new Promise<never>(() => {}));
])("ignores invalid cleanup timeout environment values", async ({ runId, sessionId, env }) => {
const cleanup = vi.fn(async () => new Promise<never>(() => {}));
const result = runAgentCleanupStep({
runId,
sessionId,
step: "openclaw-trajectory-flush",
cleanup,
log,
env,
});
const result = runAgentCleanupStep({
runId,
sessionId,
step: "openclaw-trajectory-flush",
cleanup,
log,
env,
});
await vi.advanceTimersByTimeAsync(AGENT_CLEANUP_STEP_TIMEOUT_MS - 1);
expect(log.warn).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(AGENT_CLEANUP_STEP_TIMEOUT_MS - 1);
expect(log.warn).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
await expect(result).resolves.toBeUndefined();
await vi.advanceTimersByTimeAsync(1);
await expect(result).resolves.toBeUndefined();
expect(log.warn).toHaveBeenCalledWith(
`agent cleanup timed out: runId=${runId} sessionId=${sessionId} step=openclaw-trajectory-flush timeoutMs=10000`,
);
},
);
expect(log.warn).toHaveBeenCalledWith(
`agent cleanup timed out: runId=${runId} sessionId=${sessionId} step=openclaw-trajectory-flush timeoutMs=10000`,
);
});
it("logs cleanup rejection without throwing", async () => {
await expect(

View file

@ -4,6 +4,7 @@
* Bounds cleanup steps so run completion cannot hang forever while preserving late-failure diagnostics.
*/
import { resolveOptionalIntegerOption } from "@openclaw/normalization-core/number-coercion";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { formatErrorMessage } from "../infra/errors.js";
import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js";
@ -47,7 +48,7 @@ function truncateCleanupTimeoutDetails(value: string): string {
0,
CLEANUP_TIMEOUT_DETAILS_MAX_CHARS - CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX.length,
);
return `${value.slice(0, prefixLength)}${CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX}`;
return `${truncateUtf16Safe(value, prefixLength)}${CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX}`;
}
function resolveAgentCleanupStepTimeoutMs(params: {