diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index 073da131a27..6a2c30414bd 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -3185,6 +3185,31 @@ describe("matrix monitor handler draft streaming", () => { await finish(); }); + it("keeps truncated Matrix tool progress UTF-16 safe", async () => { + const { dispatch } = createStreamingHarness({ + streaming: "progress", + previewToolProgressEnabled: true, + accountConfig: { + streaming: { + mode: "progress", + progress: { label: false, maxLineChars: 500 }, + }, + } as never, + }); + const { opts, finish } = await dispatch(); + const progressPrefix = "x".repeat(298); + + const progressText = `${progressPrefix}🎉tail`; + await opts.onItemEvent?.({ progressText }); + await opts.onItemEvent?.({ progressText }); + + await vi.waitFor(() => { + expect(sendSingleTextMessageMatrixMock).toHaveBeenCalledTimes(1); + }); + expect(singleTextMessageBody()).toBe(`- \`${progressPrefix}...\``); + await finish(); + }); + it("replaces recovered Matrix command progress instead of leaving stale failed text", async () => { const { dispatch } = createStreamingHarness({ streaming: "progress", diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 49243ed79a8..0386c7f56a0 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -44,6 +44,7 @@ import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing"; import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime"; import { getSessionEntry } from "openclaw/plugin-sdk/session-store-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { CoreConfig, MatrixConfig, @@ -434,7 +435,7 @@ function formatMatrixToolProgressMarkdownCode(text: string): string { const clipped = text.length <= MATRIX_TOOL_PROGRESS_MAX_CHARS ? text - : `${text.slice(0, MATRIX_TOOL_PROGRESS_MAX_CHARS - 1).trimEnd()}...`; + : `${truncateUtf16Safe(text, MATRIX_TOOL_PROGRESS_MAX_CHARS - 1).trimEnd()}...`; const safe = clipped.replaceAll("`", "'"); return `\`${safe}\``; } diff --git a/extensions/matrix/src/matrix/sdk/event-helpers.test.ts b/extensions/matrix/src/matrix/sdk/event-helpers.test.ts index 6b259835ba7..c187cc893c5 100644 --- a/extensions/matrix/src/matrix/sdk/event-helpers.test.ts +++ b/extensions/matrix/src/matrix/sdk/event-helpers.test.ts @@ -41,6 +41,14 @@ describe("event-helpers", () => { expect(fromText.statusCode).toBe(500); }); + it("keeps truncated HTTP error bodies UTF-16 safe", () => { + const parsedPrefix = `{"detail":"${"a".repeat(488)}`; + const invalidPrefix = `not-json ${"b".repeat(490)}`; + + expect(buildHttpError(500, `${parsedPrefix}😀"}`).message).toBe(parsedPrefix); + expect(buildHttpError(502, `${invalidPrefix}🎉tail`).message).toBe(invalidPrefix); + }); + it("serializes Matrix events and resolves state key from available sources", () => { const viaGetter = { getId: () => "$1", diff --git a/extensions/matrix/src/matrix/sdk/event-helpers.ts b/extensions/matrix/src/matrix/sdk/event-helpers.ts index 61047096a2b..06018408a27 100644 --- a/extensions/matrix/src/matrix/sdk/event-helpers.ts +++ b/extensions/matrix/src/matrix/sdk/event-helpers.ts @@ -1,5 +1,6 @@ // Matrix helper module supports event helpers behavior. import type { MatrixEvent } from "matrix-js-sdk/lib/matrix.js"; +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { MatrixRawEvent } from "./types.js"; type MatrixEventContentMode = "current" | "original"; @@ -90,10 +91,10 @@ export function buildHttpError( if (typeof parsed.error === "string" && parsed.error.trim()) { message = parsed.error.trim(); } else { - message = bodyText.slice(0, 500); + message = truncateUtf16Safe(bodyText, 500); } } catch { - message = bodyText.slice(0, 500); + message = truncateUtf16Safe(bodyText, 500); } } return Object.assign(new Error(message), { statusCode });