Skip to content

fix: trim leading blank lines on first emitted chunk only (#5530)#10612

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

fix: trim leading blank lines on first emitted chunk only (#5530)#10612
1kuna wants to merge 1 commit intoopenclaw:mainfrom
1kuna:fix/trim-leading-newlines-v2

Conversation

@1kuna
Copy link
Copy Markdown

@1kuna 1kuna commented Feb 6, 2026

Problem

Anthropic models emit a "\n\n" text block before thinking blocks. The existing .trimEnd() in emitBlockChunk only strips trailing whitespace, so leading newlines pass through to iMessage output as blank lines before the agent's response.

Root Cause

  1. Streaming path (emitBlockChunk): stripBlockTags(text, state.blockState).trimEnd() preserves leading \n\n.
  2. Single-block path (sanitizeUserFacingText): collapseConsecutiveDuplicateBlocks() returns text (untrimmed) when blocks.length < 2.

Fix

Targeted first-chunk-only trim instead of the broader .trim() on every chunk (which risks stripping meaningful indentation at chunk boundaries):

emitBlockChunk (streaming path)

const raw = stripBlockTags(text, state.blockState);
const cleaned =
  state.lastBlockReplyText === undefined
    ? raw.replace(/^(?:[ \t]*\n)+/, "")
    : raw;
const chunk = cleaned.trimEnd();

Only the first emitted chunk (when lastBlockReplyText is still undefined) gets leading blank lines stripped. Subsequent chunks use .trimEnd() as before, preserving indentation in code blocks.

sanitizeUserFacingText (final text path)

const cleaned = stripped.replace(/^(?:[ \t]*\n)+/, "");

Strips leading blank lines after removing <final> tags but before error detection and collapse logic.

Why /^(?:[ \t]*\n)+/ instead of .trim()

  • Removes only leading blank lines (lines containing only spaces/tabs followed by newline)
  • Preserves indentation on the first content line (e.g. \n\n indented code indented code)
  • Only applied on the first chunk, not every streaming chunk

Testing

  • Added test case for sanitizeUserFacingText verifying leading blank line stripping
  • All 8 existing tests pass
  • No type errors

Supersedes #9285. Closes #5530.

Greptile Overview

Greptile Summary

  • Trims leading blank lines only on the first emitted streaming chunk (to avoid Anthropic pre-thinking \n\n showing up as empty lines), while keeping .trimEnd() behavior for subsequent chunks.
  • Extends sanitizeUserFacingText() to strip leading blank lines after removing <final> tags and adds a unit test covering this behavior.
  • Adds an auto-compaction retry safeguard that can downgrade the system prompt (drop injected workspace files) when Pi’s internal retry would still overflow.
  • Updates compaction lifecycle events to propagate a user-facing “retry canceled” message when compaction succeeds but retry is blocked by prompt size.

Confidence Score: 3/5

  • This PR is close to mergeable, but dependency range changes and internal session-field restoration behavior need attention.
  • Core trimming/sanitization changes look targeted and covered by tests, but switching pi-* deps to caret ranges reduces reproducibility and the new retry hook logic mutates/restores private session fields in a way that can subtly change session behavior.
  • package.json, src/agents/pi-embedded-runner/run/attempt.ts

@openclaw-barnacle openclaw-barnacle bot added the agents Agent runtime and tooling label Feb 6, 2026
@1kuna 1kuna force-pushed the fix/trim-leading-newlines-v2 branch from 263309d to a3ea5b5 Compare February 6, 2026 18:47
@1kuna 1kuna changed the title fix: trim leading blank lines on first emitted chunk only fix: trim leading blank lines on first emitted chunk only (#5530) Feb 6, 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.

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 6, 2026

Additional Comments (2)

package.json
Caret dependency range

This PR changes several @mariozechner/pi-* dependencies from exact 0.52.6 to ^0.52.6. That makes installs non-reproducible and can pull in newer versions with API/behavior changes, which is particularly risky here since the PR also relies on setAutoCompactionRetryHook/internal session fields. If the intent is just to pick up the current version, keep these pinned to an exact version instead of a caret range.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: package.json
Line: 113:120

Comment:
**Caret dependency range**

This PR changes several `@mariozechner/pi-*` dependencies from exact `0.52.6` to `^0.52.6`. That makes installs non-reproducible and can pull in newer versions with API/behavior changes, which is particularly risky here since the PR also relies on `setAutoCompactionRetryHook`/internal session fields. If the intent is just to pick up the current version, keep these pinned to an exact version instead of a caret range.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

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

src/agents/pi-embedded-runner/run/attempt.ts
Restoring internal session fields

downgradeSystemPromptOneShot() captures previousBasePrompt/previousRebuild from mutableSession._baseSystemPrompt and later restores them, but those fields are not guaranteed to exist. When they were originally undefined, the restore callback sets them back to undefined (explicitly), which can change session behavior vs leaving the properties unset (and may break _rebuildSystemPrompt lookups if the library expects absence vs undefined). Since this code is intentionally poking internal/private fields, the restore should avoid writing the properties back when they were not present originally (or use the session’s public API only).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 523:534

Comment:
**Restoring internal session fields**

`downgradeSystemPromptOneShot()` captures `previousBasePrompt`/`previousRebuild` from `mutableSession._baseSystemPrompt` and later restores them, but those fields are not guaranteed to exist. When they were originally `undefined`, the restore callback sets them back to `undefined` (explicitly), which can change session behavior vs leaving the properties unset (and may break `_rebuildSystemPrompt` lookups if the library expects absence vs undefined). Since this code is intentionally poking internal/private fields, the restore should avoid writing the properties back when they were not present originally (or use the session’s public API only).

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

@1kuna 1kuna force-pushed the fix/trim-leading-newlines-v2 branch from a3ea5b5 to 6f5f4ee Compare February 7, 2026 03:48
@1kuna 1kuna force-pushed the fix/trim-leading-newlines-v2 branch from 6f5f4ee to 24635ee Compare February 12, 2026 00:26
mcinteerj added a commit to mcinteerj/openclaw that referenced this pull request Feb 14, 2026
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 pushed a commit to mcinteerj/openclaw that referenced this pull request Feb 14, 2026
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 added a commit that referenced this pull request Feb 14, 2026
)

* 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 #8052 and #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
…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
…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
…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]>
@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added stale Marked as stale due to inactivity and removed stale Marked as stale due to inactivity labels Feb 21, 2026
@steipete
Copy link
Copy Markdown
Contributor

Closing as AI-assisted stale-fix triage.

Linked issue #5530 ("iMessage: leading blank lines in streamed messages") is currently CLOSED and was closed on 2026-02-13T03:32:44Z with state reason NOT_PLANNED.
Given that issue state, this fix PR is no longer needed in the active queue and is being closed as stale.

If the underlying bug is still reproducible on current main, please reopen this PR (or open a new focused fix PR) and reference both #5530 and #10612 for fast re-triage.

@steipete
Copy link
Copy Markdown
Contributor

Closed after AI-assisted stale-fix triage (closed issue duplicate/stale fix).

@steipete steipete closed this Feb 24, 2026
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 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
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]>
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.

iMessage: leading blank lines in streamed messages

3 participants