Skip to content

Commit 1ce0049

Browse files
fix(agents): keep cleanup timeout details UTF-16 safe
1 parent 7698783 commit 1ce0049

2 files changed

Lines changed: 49 additions & 23 deletions

File tree

src/agents/run-cleanup-timeout.test.ts

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { runAgentCleanupStep } from "./run-cleanup-timeout.js";
44

55
const AGENT_CLEANUP_STEP_TIMEOUT_MS = 10_000;
66
const CLEANUP_TIMEOUT_DETAILS_MAX_CHARS = 512;
7+
const CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX = "...[truncated]";
78

89
describe("agent cleanup timeout", () => {
910
const log = {
@@ -115,6 +116,33 @@ describe("agent cleanup timeout", () => {
115116
);
116117
});
117118

119+
it("keeps truncated cleanup timeout details UTF-16 safe", async () => {
120+
const cleanup = vi.fn(async () => new Promise<never>(() => {}));
121+
const prefixLength =
122+
CLEANUP_TIMEOUT_DETAILS_MAX_CHARS - CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX.length;
123+
const detailsPrefix = "a".repeat(prefixLength - 1);
124+
const oversizedDetails = `${detailsPrefix}😀${"b".repeat(CLEANUP_TIMEOUT_DETAILS_MAX_CHARS)}`;
125+
126+
const result = runAgentCleanupStep({
127+
runId: "run-trajectory",
128+
sessionId: "session-trajectory",
129+
step: "agent-trajectory-flush",
130+
cleanup,
131+
log,
132+
timeoutMs: 5,
133+
getTimeoutDetails: () => oversizedDetails,
134+
});
135+
136+
await vi.advanceTimersByTimeAsync(5);
137+
await expect(result).resolves.toBeUndefined();
138+
139+
const message = String(log.warn.mock.calls.at(-1)?.[0] ?? "");
140+
expect(message).toContain(
141+
` details=${detailsPrefix}${CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX}`,
142+
);
143+
expect(message).not.toContain("�");
144+
});
145+
118146
it("does not fail cleanup when timeout details throw", async () => {
119147
const cleanup = vi.fn(async () => new Promise<never>(() => {}));
120148

@@ -257,31 +285,28 @@ describe("agent cleanup timeout", () => {
257285
OPENCLAW_AGENT_CLEANUP_TIMEOUT_MS: "0x10",
258286
},
259287
},
260-
])(
261-
"ignores invalid cleanup timeout environment values",
262-
async ({ runId, sessionId, env }) => {
263-
const cleanup = vi.fn(async () => new Promise<never>(() => {}));
264-
265-
const result = runAgentCleanupStep({
266-
runId,
267-
sessionId,
268-
step: "openclaw-trajectory-flush",
269-
cleanup,
270-
log,
271-
env,
272-
});
288+
])("ignores invalid cleanup timeout environment values", async ({ runId, sessionId, env }) => {
289+
const cleanup = vi.fn(async () => new Promise<never>(() => {}));
273290

274-
await vi.advanceTimersByTimeAsync(AGENT_CLEANUP_STEP_TIMEOUT_MS - 1);
275-
expect(log.warn).not.toHaveBeenCalled();
291+
const result = runAgentCleanupStep({
292+
runId,
293+
sessionId,
294+
step: "openclaw-trajectory-flush",
295+
cleanup,
296+
log,
297+
env,
298+
});
276299

277-
await vi.advanceTimersByTimeAsync(1);
278-
await expect(result).resolves.toBeUndefined();
300+
await vi.advanceTimersByTimeAsync(AGENT_CLEANUP_STEP_TIMEOUT_MS - 1);
301+
expect(log.warn).not.toHaveBeenCalled();
279302

280-
expect(log.warn).toHaveBeenCalledWith(
281-
`agent cleanup timed out: runId=${runId} sessionId=${sessionId} step=openclaw-trajectory-flush timeoutMs=10000`,
282-
);
283-
},
284-
);
303+
await vi.advanceTimersByTimeAsync(1);
304+
await expect(result).resolves.toBeUndefined();
305+
306+
expect(log.warn).toHaveBeenCalledWith(
307+
`agent cleanup timed out: runId=${runId} sessionId=${sessionId} step=openclaw-trajectory-flush timeoutMs=10000`,
308+
);
309+
});
285310

286311
it("logs cleanup rejection without throwing", async () => {
287312
await expect(

src/agents/run-cleanup-timeout.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Bounds cleanup steps so run completion cannot hang forever while preserving late-failure diagnostics.
55
*/
66
import { resolveOptionalIntegerOption } from "@openclaw/normalization-core/number-coercion";
7+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
78
import { formatErrorMessage } from "../infra/errors.js";
89
import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js";
910

@@ -47,7 +48,7 @@ function truncateCleanupTimeoutDetails(value: string): string {
4748
0,
4849
CLEANUP_TIMEOUT_DETAILS_MAX_CHARS - CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX.length,
4950
);
50-
return `${value.slice(0, prefixLength)}${CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX}`;
51+
return `${truncateUtf16Safe(value, prefixLength)}${CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX}`;
5152
}
5253

5354
function resolveAgentCleanupStepTimeoutMs(params: {

0 commit comments

Comments
 (0)