Skip to content

fix: strip leading whitespace from sanitizeUserFacingText output#16158

Merged
Takhoffman merged 2 commits intoopenclaw:mainfrom
mcinteerj:fix/trim-leading-whitespace-reply
Feb 14, 2026
Merged

fix: strip leading whitespace from sanitizeUserFacingText output#16158
Takhoffman merged 2 commits intoopenclaw:mainfrom
mcinteerj:fix/trim-leading-whitespace-reply

Conversation

@mcinteerj
Copy link
Copy Markdown
Contributor

@mcinteerj mcinteerj commented Feb 14, 2026

Summary

LLM responses frequently begin with \n\n, causing visible blank lines at the top of messages on WhatsApp (and other channels). This fixes the root cause in sanitizeUserFacingText rather than scattering trimStart() across multiple delivery paths.

lobster-biscuit

Repro Steps

  1. Send a message that triggers an LLM response beginning with \n\n (common with Claude Opus 4.6)
  2. Observe blank lines at the top of the WhatsApp message

Root Cause

sanitizeUserFacingText in src/agents/pi-embedded-helpers/errors.ts uses trimmed for empty-checks but returns the untrimmed stripped variable:

const stripped = stripFinalTagsFromText(text);
const trimmed = stripped.trim();
if (!trimmed) {
  return stripped;  // ← returns whitespace-only string instead of ""
}
// ...
return collapseConsecutiveDuplicateBlocks(stripped);  // ← returns untrimmed

Fix

Two one-line changes:

  1. Empty input path: return "" instead of whitespace-only stripped
  2. Final return: apply .trimStart() after collapseConsecutiveDuplicateBlocks

This is the single funnel point — all reply paths (auto-reply, heartbeat, outbound) flow through sanitizeUserFacingText via normalizeReplyPayload, so the fix applies universally without touching delivery code.

Behavior Changes

  • Messages with leading \n or \n\n will have that whitespace stripped before delivery
  • Whitespace-only text now returns "" instead of the whitespace string
  • No change to trailing whitespace or internal newlines

Codebase and GitHub Search

Tests

4 new test cases added to pi-embedded-helpers.sanitizeuserfacingtext.e2e.test.ts:

Test Status
strips leading newlines from LLM output
strips leading whitespace and newlines combined
preserves trailing whitespace and internal newlines
returns empty for whitespace-only input

All 15 tests pass (11 existing + 4 new).

pnpm lint    ✅ 0 warnings, 0 errors
pnpm build   ✅ clean
pnpm test:e2e (sanitizeuserfacingtext) ✅ 15/15 passed

Manual Testing

Tested by observing WhatsApp message output before/after the change — leading blank lines no longer appear.

Sign-Off

  • Models used: Claude Opus 4.6 (analysis + implementation)
  • Submitter effort: High — traced through normalize-reply → sanitizeUserFacingText → errors.ts, identified root cause, checked for duplicate PRs, wrote focused fix with tests
  • Agent notes: Bug independently discovered by two agents (Keith 🪿 and Kev 🦈) experiencing identical symptoms on the same model. Root cause confirmed by code inspection, not speculation.

Greptile Overview

Greptile Summary

Fixes leading whitespace in LLM responses by modifying sanitizeUserFacingText in src/agents/pi-embedded-helpers/errors.ts. The function now returns an empty string for whitespace-only input and strips leading whitespace from the final output using .trimStart(). This addresses the root cause of blank lines appearing at the top of WhatsApp messages when Claude Opus 4.6 responses begin with \n\n.

Changes:

  • Returns "" instead of whitespace-only string when input is empty after trimming
  • Applies .trimStart() to the final return value after collapseConsecutiveDuplicateBlocks
  • Adds 4 new test cases covering leading newlines, combined whitespace/newlines, trailing whitespace preservation, and empty input

The fix is well-targeted at the single funnel point where all reply paths flow through normalizeReplyPayload, avoiding the need to patch multiple delivery paths.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The changes are minimal (two one-line modifications), well-tested with 4 new test cases, and address a real bug at the correct abstraction level. The fix applies .trimStart() uniformly to all non-error output paths without affecting error message formatting. All existing tests continue to pass, and the new tests verify the specific behavior changes.
  • No files require special attention

Last reviewed commit: efdcf74

@steipete steipete self-assigned this Feb 14, 2026
mcinteerj and others added 2 commits February 14, 2026 09:20
LLM responses frequently begin with \n\n, which survives through
sanitizeUserFacingText and reaches the channel as visible blank lines.

Root cause: the function used trimmed text for empty-checks but returned
the untrimmed 'stripped' variable. Two one-line fixes:
1. Return empty string (not whitespace-only 'stripped') for blank input
2. Apply trimStart() to the final return value

Fixes the same issue as openclaw#8052 and openclaw#10612 but at the root cause
(sanitizeUserFacingText) rather than scattering trimStart across
multiple delivery paths.
@Takhoffman Takhoffman force-pushed the fix/trim-leading-whitespace-reply branch from 72e2b92 to a9db342 Compare February 14, 2026 15:22
@Takhoffman Takhoffman merged commit 3881af5 into openclaw:main Feb 14, 2026
9 checks passed
steipete added a commit that referenced this pull request Feb 14, 2026
* fix: strip leading empty lines in sanitizeUserFacingText (#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (#16158) (thanks @mcinteerj)
hamidzr pushed a commit to hamidzr/openclaw that referenced this pull request Feb 14, 2026
…nclaw#16158)

* fix: strip leading whitespace from sanitizeUserFacingText output

LLM responses frequently begin with \n\n, which survives through
sanitizeUserFacingText and reaches the channel as visible blank lines.

Root cause: the function used trimmed text for empty-checks but returned
the untrimmed 'stripped' variable. Two one-line fixes:
1. Return empty string (not whitespace-only 'stripped') for blank input
2. Apply trimStart() to the final return value

Fixes the same issue as openclaw#8052 and openclaw#10612 but at the root cause
(sanitizeUserFacingText) rather than scattering trimStart across
multiple delivery paths.

* Changelog: note sanitizeUserFacingText whitespace normalization

Co-authored-by: Tak Hoffman <[email protected]>

---------

Co-authored-by: Tak Hoffman <[email protected]>
hamidzr pushed a commit to hamidzr/openclaw that referenced this pull request Feb 14, 2026
)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)
mcinteerj added a commit to mcinteerj/openclaw that referenced this pull request Feb 14, 2026
The block streaming delivery path (onBlockReply) bypasses
sanitizeUserFacingText, so the trimStart fix from openclaw#16158/openclaw#16280
doesn't apply to users with blockStreaming enabled (the default).

Adds trimStart() to the cleaned text in the block reply payload
construction, consistent with the non-streaming path.
mcinteerj added a commit to mcinteerj/openclaw that referenced this pull request Feb 14, 2026
The block streaming delivery path (onBlockReply) bypasses
sanitizeUserFacingText, so the trimStart fix from openclaw#16158/openclaw#16280
doesn't apply to users with blockStreaming enabled (the default).

Adds trimStart() to the cleaned text in the block reply payload
construction, consistent with the non-streaming path.
mverrilli pushed a commit to mverrilli/openclaw that referenced this pull request Feb 14, 2026
…nclaw#16158)

* fix: strip leading whitespace from sanitizeUserFacingText output

LLM responses frequently begin with \n\n, which survives through
sanitizeUserFacingText and reaches the channel as visible blank lines.

Root cause: the function used trimmed text for empty-checks but returned
the untrimmed 'stripped' variable. Two one-line fixes:
1. Return empty string (not whitespace-only 'stripped') for blank input
2. Apply trimStart() to the final return value

Fixes the same issue as openclaw#8052 and openclaw#10612 but at the root cause
(sanitizeUserFacingText) rather than scattering trimStart across
multiple delivery paths.

* Changelog: note sanitizeUserFacingText whitespace normalization

Co-authored-by: Tak Hoffman <[email protected]>

---------

Co-authored-by: Tak Hoffman <[email protected]>
mverrilli pushed a commit to mverrilli/openclaw that referenced this pull request Feb 14, 2026
)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)
akoscz pushed a commit to akoscz/openclaw that referenced this pull request Feb 15, 2026
)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)
sebslight pushed a commit to mcinteerj/openclaw that referenced this pull request Feb 15, 2026
The block streaming delivery path (onBlockReply) bypasses
sanitizeUserFacingText, so the trimStart fix from openclaw#16158/openclaw#16280
doesn't apply to users with blockStreaming enabled (the default).

Adds trimStart() to the cleaned text in the block reply payload
construction, consistent with the non-streaming path.
GwonHyeok pushed a commit to learners-superpumped/openclaw that referenced this pull request Feb 15, 2026
…nclaw#16158)

* fix: strip leading whitespace from sanitizeUserFacingText output

LLM responses frequently begin with \n\n, which survives through
sanitizeUserFacingText and reaches the channel as visible blank lines.

Root cause: the function used trimmed text for empty-checks but returned
the untrimmed 'stripped' variable. Two one-line fixes:
1. Return empty string (not whitespace-only 'stripped') for blank input
2. Apply trimStart() to the final return value

Fixes the same issue as openclaw#8052 and openclaw#10612 but at the root cause
(sanitizeUserFacingText) rather than scattering trimStart across
multiple delivery paths.

* Changelog: note sanitizeUserFacingText whitespace normalization

Co-authored-by: Tak Hoffman <[email protected]>

---------

Co-authored-by: Tak Hoffman <[email protected]>
GwonHyeok pushed a commit to learners-superpumped/openclaw that referenced this pull request Feb 15, 2026
)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)
snowzlm pushed a commit to snowzlm/openclaw that referenced this pull request Feb 15, 2026
)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)
hughdidit pushed a commit to hughdidit/DAISy-Agency that referenced this pull request Mar 1, 2026
…nclaw#16158)

* fix: strip leading whitespace from sanitizeUserFacingText output

LLM responses frequently begin with \n\n, which survives through
sanitizeUserFacingText and reaches the channel as visible blank lines.

Root cause: the function used trimmed text for empty-checks but returned
the untrimmed 'stripped' variable. Two one-line fixes:
1. Return empty string (not whitespace-only 'stripped') for blank input
2. Apply trimStart() to the final return value

Fixes the same issue as openclaw#8052 and openclaw#10612 but at the root cause
(sanitizeUserFacingText) rather than scattering trimStart across
multiple delivery paths.

* Changelog: note sanitizeUserFacingText whitespace normalization

Co-authored-by: Tak Hoffman <[email protected]>

---------

Co-authored-by: Tak Hoffman <[email protected]>
(cherry picked from commit 3881af5)

# Conflicts:
#	CHANGELOG.md
hughdidit pushed a commit to hughdidit/DAISy-Agency that referenced this pull request Mar 1, 2026
)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

(cherry picked from commit 50a6e0e)

# Conflicts:
#	CHANGELOG.md
hughdidit pushed a commit to hughdidit/DAISy-Agency that referenced this pull request Mar 3, 2026
…nclaw#16158)

* fix: strip leading whitespace from sanitizeUserFacingText output

LLM responses frequently begin with \n\n, which survives through
sanitizeUserFacingText and reaches the channel as visible blank lines.

Root cause: the function used trimmed text for empty-checks but returned
the untrimmed 'stripped' variable. Two one-line fixes:
1. Return empty string (not whitespace-only 'stripped') for blank input
2. Apply trimStart() to the final return value

Fixes the same issue as openclaw#8052 and openclaw#10612 but at the root cause
(sanitizeUserFacingText) rather than scattering trimStart across
multiple delivery paths.

* Changelog: note sanitizeUserFacingText whitespace normalization

Co-authored-by: Tak Hoffman <[email protected]>

---------

Co-authored-by: Tak Hoffman <[email protected]>
(cherry picked from commit 3881af5)

# Conflicts:
#	CHANGELOG.md
#	src/agents/pi-embedded-helpers/errors.ts
hughdidit pushed a commit to hughdidit/DAISy-Agency that referenced this pull request Mar 3, 2026
)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

(cherry picked from commit 50a6e0e)

# Conflicts:
#	CHANGELOG.md
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
…nclaw#16158)

* fix: strip leading whitespace from sanitizeUserFacingText output

LLM responses frequently begin with \n\n, which survives through
sanitizeUserFacingText and reaches the channel as visible blank lines.

Root cause: the function used trimmed text for empty-checks but returned
the untrimmed 'stripped' variable. Two one-line fixes:
1. Return empty string (not whitespace-only 'stripped') for blank input
2. Apply trimStart() to the final return value

Fixes the same issue as openclaw#8052 and openclaw#10612 but at the root cause
(sanitizeUserFacingText) rather than scattering trimStart across
multiple delivery paths.

* Changelog: note sanitizeUserFacingText whitespace normalization

Co-authored-by: Tak Hoffman <[email protected]>

---------

Co-authored-by: Tak Hoffman <[email protected]>
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)

* fix: strip leading empty lines in sanitizeUserFacingText (openclaw#16158) (thanks @mcinteerj)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants