diff --git a/src/agents/embedded-agent-helpers/errors.test.ts b/src/agents/embedded-agent-helpers/errors.test.ts index ad94a3941c37..51e657c96b36 100644 --- a/src/agents/embedded-agent-helpers/errors.test.ts +++ b/src/agents/embedded-agent-helpers/errors.test.ts @@ -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( diff --git a/src/agents/embedded-agent-helpers/errors.ts b/src/agents/embedded-agent-helpers/errors.ts index 47ce868e33f3..1c63aa499705 100644 --- a/src/agents/embedded-agent-helpers/errors.ts +++ b/src/agents/embedded-agent-helpers/errors.ts @@ -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; }