fix(agents): keep failover detail truncation UTF-16 safe (#104487)

* fix(agents): keep failover detail truncation UTF-16 safe

Replace raw .slice(0, MAX_FAILOVER_DETAIL_CHARS) with truncateUtf16Safe
so provider error details containing emoji or other non-BMP characters
are not split at a surrogate-pair boundary before failover classification.

Adds a regression test via extractFailoverSignalDetails.

* test(agents): tighten failover UTF-16 coverage

---------

Co-authored-by: chengzhichao-xydt <chengzhichao-xydt@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
chengzhichao-xydt
2026-07-12 06:43:52 +08:00
committed by GitHub
parent 1bd4887a5e
commit aa8f78788c
2 changed files with 25 additions and 2 deletions

View File

@@ -4,7 +4,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE } from "../../shared/assistant-error-format.js";
import { makeAssistantMessageFixture } from "../test-helpers/assistant-message-fixtures.js";
import { formatAssistantErrorText, isLikelyContextOverflowError } from "./errors.js";
import {
extractFailoverSignalDetails,
formatAssistantErrorText,
isLikelyContextOverflowError,
} from "./errors.js";
const { toolPolicyAuditInfo } = vi.hoisted(() => ({
toolPolicyAuditInfo: vi.fn(),
@@ -98,6 +102,25 @@ describe("formatAssistantErrorText streaming JSON parse classification", () => {
});
});
describe("extractFailoverSignalDetails", () => {
it.each([
{
name: "backs off before a split surrogate pair",
input: `${"a".repeat(999)}🎉!`,
expected: "a".repeat(999),
},
{
name: "keeps the full ASCII budget",
input: "a".repeat(1001),
expected: "a".repeat(1000),
},
])("$name in nested provider details", ({ input, expected }) => {
const details = extractFailoverSignalDetails({ error: { body: { detail: input } } });
expect(details).toEqual([expected]);
});
});
describe("isLikelyContextOverflowError", () => {
it("detects Codex promptError wording for a full context window", () => {
expect(

View File

@@ -308,7 +308,7 @@ function normalizeFailoverDetailString(value: string | undefined): string | unde
return undefined;
}
return trimmed.length > MAX_FAILOVER_DETAIL_CHARS
? trimmed.slice(0, MAX_FAILOVER_DETAIL_CHARS)
? truncateUtf16Safe(trimmed, MAX_FAILOVER_DETAIL_CHARS)
: trimmed;
}