diff --git a/src/auto-reply/fallback-state.test.ts b/src/auto-reply/fallback-state.test.ts index 67dbfa9728b..cc60b18b050 100644 --- a/src/auto-reply/fallback-state.test.ts +++ b/src/auto-reply/fallback-state.test.ts @@ -113,6 +113,15 @@ describe("fallback-state", () => { expect(resolved.reasonSummary).toContain("Claude Max usage limit reached"); }); + it("keeps truncated transient error details UTF-16 safe", () => { + const detail = "x".repeat(68); + const resolved = resolveDemoFallbackTransition({ + attempts: [{ ...baseAttempt, error: `429 ${detail}😀tail` }], + }); + + expect(resolved.reasonSummary).toBe(`HTTP 429: ${detail}…`); + }); + it("refreshes reason when fallback remains active with same model pair", () => { const resolved = resolveDemoFallbackTransition({ attempts: [{ ...baseAttempt, reason: "timeout" }], diff --git a/src/auto-reply/fallback-state.ts b/src/auto-reply/fallback-state.ts index 9f3146b210a..e443c243f4d 100644 --- a/src/auto-reply/fallback-state.ts +++ b/src/auto-reply/fallback-state.ts @@ -1,5 +1,6 @@ /** Formats model-fallback notice state for UI/status messages and persisted transition tracking. */ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import { formatRawAssistantErrorForUi } from "../agents/embedded-agent-helpers.js"; import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -29,7 +30,7 @@ function truncateFallbackReasonPart(value: string, max = FALLBACK_REASON_PART_MA if (text.length <= max) { return text; } - return `${text.slice(0, Math.max(0, max - 1)).trimEnd()}…`; + return `${truncateUtf16Safe(text, max - 1).trimEnd()}…`; } function formatFallbackAttemptErrorPreview(attempt: RuntimeFallbackAttempt): string | undefined {