← Back to PRs

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

by 1kuna open 2026-02-06 18:39 View on GitHub →
agents stale
## 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) ```ts 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) ```ts 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_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> - 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. <h3>Confidence Score: 3/5</h3> - 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 <!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Most Similar PRs