Skip to content

fix: trim leading newlines from streaming/block-flush text output#9285

Closed
1kuna wants to merge 1 commit intoopenclaw:mainfrom
1kuna:fix/trim-leading-newlines
Closed

fix: trim leading newlines from streaming/block-flush text output#9285
1kuna wants to merge 1 commit intoopenclaw:mainfrom
1kuna:fix/trim-leading-newlines

Conversation

@1kuna
Copy link
Copy Markdown

@1kuna 1kuna commented Feb 5, 2026

Problem

iMessage (and likely other channel) responses have 1-2 blank lines prepended before the actual text content when using extended thinking mode.

Root Cause

Anthropic models in extended thinking mode emit a \n\n text content block before the thinking block. The streaming/block-flush path preserves these leading newlines because:

  1. emitBlockChunk() in pi-embedded-subscribe.ts uses .trimEnd() instead of .trim() — preserves leading newlines
  2. collapseConsecutiveDuplicateBlocks() in pi-embedded-helpers/errors.ts returns the original untrimmed text when there's only one paragraph block (early return if (blocks.length < 2) return text;)
  3. sanitizeUserFacingText() doesn't trim the final output

The final payloads path (extractAssistantText()) is NOT affected because it trims individual content blocks and filters empties.

Fixes (defense in depth)

  1. emitBlockChunk: .trimEnd().trim() — stops leading newlines at the source
  2. collapseConsecutiveDuplicateBlocks: early return now returns trimmed instead of text — fixes the sanitizer
  3. sanitizeUserFacingText: added .trim() to final return — safety net for all paths

Testing

  • Verified compiled JS reflects all three changes
  • No behavior change for well-formed text (already trimmed)
  • Only affects text with leading whitespace, which should never be user-facing

Fixes #7689

Greptile Overview

Greptile Summary

This PR addresses leading blank lines in streaming/block-flush output (notably in iMessage) by trimming whitespace earlier and adding a defensive trim in the user-facing sanitizer.

Changes are localized to the embedded Pi streaming path (src/agents/pi-embedded-subscribe.ts) and the shared error/text sanitization helpers (src/agents/pi-embedded-helpers/errors.ts), so the fix should apply across any channels that consume those paths.

Confidence Score: 3/5

  • This PR is close to safe to merge, but has one edge-case that can still preserve whitespace-only output in the sanitizer path.
  • The core trimEnd→trim change in the streaming path is straightforward, but the helper collapseConsecutiveDuplicateBlocks() still returns the untrimmed original text for the all-whitespace case, which undermines the stated ‘defense in depth’ intent for leading-newline removal in some scenarios.
  • src/agents/pi-embedded-helpers/errors.ts

Context used:

  • Context from dashboard - CLAUDE.md (source)
  • Context from dashboard - AGENTS.md (source)

@openclaw-barnacle openclaw-barnacle bot added the agents Agent runtime and tooling label Feb 5, 2026
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 5, 2026

Additional Comments (1)

src/agents/pi-embedded-helpers/errors.ts
Inconsistent trimming on empty

collapseConsecutiveDuplicateBlocks() now trims for the 1-block case, but for the empty/whitespace-only case it still returns the original text (if (!trimmed) return text;). That means callers like sanitizeUserFacingText() can still emit leading newlines/spaces when text is only whitespace after tag-stripping (e.g., "\n\n" or " "). If the intent is “no user-facing leading whitespace ever”, this early return should be consistent with the new behavior.

Also note this affects any other callers of collapseConsecutiveDuplicateBlocks().

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-helpers/errors.ts
Line: 93:97

Comment:
**Inconsistent trimming on empty**

`collapseConsecutiveDuplicateBlocks()` now trims for the 1-block case, but for the empty/whitespace-only case it still returns the *original* `text` (`if (!trimmed) return text;`). That means callers like `sanitizeUserFacingText()` can still emit leading newlines/spaces when `text` is only whitespace after tag-stripping (e.g., `"\n\n"` or `"  "`). If the intent is “no user-facing leading whitespace ever”, this early return should be consistent with the new behavior.

Also note this affects any other callers of `collapseConsecutiveDuplicateBlocks()`.


How can I resolve this? If you propose a fix, please make it concise.

@1kuna
Copy link
Copy Markdown
Author

1kuna commented Feb 6, 2026

Superseded by #10612, which uses a more targeted approach: strips leading blank lines only on the first emitted chunk (via state.lastBlockReplyText === undefined) instead of .trim() on every chunk. This preserves meaningful indentation at chunk boundaries while still fixing the leading newline issue.

@1kuna
Copy link
Copy Markdown
Author

1kuna commented Feb 6, 2026

Closing in favor of #10612 (targeted first-chunk-only fix).

@1kuna 1kuna closed this Feb 6, 2026
@1kuna
Copy link
Copy Markdown
Author

1kuna commented Feb 6, 2026

Superseded by #10612 (targeted first-chunk blank-line trim).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming replies can start with leading blank lines

1 participant