diff --git a/CHANGELOG.md b/CHANGELOG.md index 4711346d5e4..c02fb1d6c0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ Docs: https://docs.openclaw.ai - Security/Audit: warn when `gateway.tools.allow` re-enables default-denied tools over HTTP `POST /tools/invoke`, since this can increase RCE blast radius if the gateway is reachable. - Security/Plugins/Hooks: harden npm-based installs by restricting specs to registry packages only, passing `--ignore-scripts` to `npm pack`, and cleaning up temp install directories. - Feishu: stop persistent Typing reaction on NO_REPLY/suppressed runs by wiring reply-dispatcher cleanup to remove typing indicators. (#15464) Thanks @arosstale. +- Agents: strip leading whitespace/newlines from `sanitizeUserFacingText` output and normalize whitespace-only outputs to empty text. (#16158) Thanks @mcinteerj. - BlueBubbles: gracefully degrade when Private API is disabled by filtering private-only actions, skipping private-only reactions/reply effects, and avoiding private reply markers so non-private flows remain usable. (#16002) Thanks @L-U-C-K-Y. - Outbound: add a write-ahead delivery queue with crash-recovery retries to prevent lost outbound messages after gateway restarts. (#15636) Thanks @nabbilkhan, @thewilloftheshadow. - Auto-reply/Threading: auto-inject implicit reply threading so `replyToMode` works without requiring model-emitted `[[reply_to_current]]`, while preserving `replyToMode: "off"` behavior for implicit Slack replies and keeping block-streaming chunk coalescing stable under `replyToMode: "first"`. (#14976) Thanks @Diaspar4u. diff --git a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.e2e.test.ts b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.e2e.test.ts index bde06a285c3..cd8b13c3ee5 100644 --- a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.e2e.test.ts +++ b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.e2e.test.ts @@ -69,4 +69,25 @@ describe("sanitizeUserFacingText", () => { const text = "Hello there!\n\nDifferent line."; expect(sanitizeUserFacingText(text)).toBe(text); }); + + it("strips leading newlines from LLM output", () => { + expect(sanitizeUserFacingText("\n\nHello there!")).toBe("Hello there!"); + expect(sanitizeUserFacingText("\nHello there!")).toBe("Hello there!"); + expect(sanitizeUserFacingText("\n\n\nMultiple newlines")).toBe("Multiple newlines"); + }); + + it("strips leading whitespace and newlines combined", () => { + expect(sanitizeUserFacingText("\n \n Hello")).toBe("Hello"); + expect(sanitizeUserFacingText(" \n\nHello")).toBe("Hello"); + }); + + it("preserves trailing whitespace and internal newlines", () => { + expect(sanitizeUserFacingText("Hello\n\nWorld\n")).toBe("Hello\n\nWorld\n"); + expect(sanitizeUserFacingText("Line 1\nLine 2")).toBe("Line 1\nLine 2"); + }); + + it("returns empty for whitespace-only input", () => { + expect(sanitizeUserFacingText("\n\n")).toBe(""); + expect(sanitizeUserFacingText(" \n ")).toBe(""); + }); }); diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index d4d0f34e40a..02332145ad7 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -488,7 +488,7 @@ export function sanitizeUserFacingText(text: string, opts?: { errorContext?: boo const stripped = stripFinalTagsFromText(text); const trimmed = stripped.trim(); if (!trimmed) { - return stripped; + return ""; } // Only apply error-pattern rewrites when the caller knows this text is an error payload. @@ -527,7 +527,7 @@ export function sanitizeUserFacingText(text: string, opts?: { errorContext?: boo } } - return collapseConsecutiveDuplicateBlocks(stripped); + return collapseConsecutiveDuplicateBlocks(stripped).trimStart(); } export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean {