From c13011e2d0e66228a9a1fe5b61a01f7941e83a52 Mon Sep 17 00:00:00 2001 From: 1alyx Date: Thu, 29 Jan 2026 23:05:15 -0500 Subject: [PATCH 1/2] add newline fix task --- NEWLINE-FIX-TASK.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 NEWLINE-FIX-TASK.md diff --git a/NEWLINE-FIX-TASK.md b/NEWLINE-FIX-TASK.md new file mode 100644 index 000000000..2a8860c4f --- /dev/null +++ b/NEWLINE-FIX-TASK.md @@ -0,0 +1,45 @@ +# Task: Fix Leading Newline Bug + +## Problem +iMessage responses have 1-2 blank lines prepended. Root cause: Anthropic models in thinking mode emit a `\n\n` text block before the thinking block, and the streaming/flush path doesn't trim leading whitespace. + +## Fixes Required (apply ALL THREE for defense in depth) + +### Fix 1: `emitBlockChunk` in `src/agents/pi-embedded-subscribe.ts` +Find the line that does `.trimEnd()` on the chunk text and change it to `.trim()`: +``` +// Find something like: +const chunk = stripBlockTags(text, state.blockState).trimEnd(); +// Change to: +const chunk = stripBlockTags(text, state.blockState).trim(); +``` + +### Fix 2: `collapseConsecutiveDuplicateBlocks` in `src/agents/pi-embedded-helpers/errors.ts` +Fix the early return to use trimmed text instead of original: +``` +// Find: +if (blocks.length < 2) return text; +// Change to: +if (blocks.length < 2) return trimmed; +``` + +### Fix 3: Safety net in `sanitizeUserFacingText` in same file (`errors.ts`) +Make sure the final return trims: +``` +// Find the final return of collapseConsecutiveDuplicateBlocks(stripped) +// Change to: +return collapseConsecutiveDuplicateBlocks(stripped).trim(); +``` + +## Rules +- Edit the TypeScript SOURCE files in `src/`, NOT the compiled `dist/` files +- Build after changes: `npm run build` or `npx tsc` +- Commit with message: `fix: trim leading newlines from streaming/block-flush text output` +- Co-author: `Co-authored-by: Zach Canepa ` +- Push to branch: `fix/leading-newline-trim` +- Do NOT create a PR, just push + +## Verification +After building, check that the compiled JS in `dist/` reflects your changes: +- `dist/agents/pi-embedded-subscribe.js` should have `.trim()` not `.trimEnd()` +- `dist/agents/pi-embedded-helpers/errors.js` should return `trimmed` not `text` in early return From 38ba14ea9f9fbb32d5bfb9b103be12a7b47f4d94 Mon Sep 17 00:00:00 2001 From: 1alyx Date: Thu, 29 Jan 2026 23:07:12 -0500 Subject: [PATCH 2/2] fix: trim leading newlines from streaming/block-flush text output Co-authored-by: Zach Canepa --- src/agents/pi-embedded-helpers/errors.ts | 4 ++-- src/agents/pi-embedded-subscribe.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 849c4293e..3b2f47f56 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -81,7 +81,7 @@ function collapseConsecutiveDuplicateBlocks(text: string): string { const trimmed = text.trim(); if (!trimmed) return text; const blocks = trimmed.split(/\n{2,}/); - if (blocks.length < 2) return text; + if (blocks.length < 2) return trimmed; const normalizeBlock = (value: string) => value.trim().replace(/\s+/g, " "); const result: string[] = []; @@ -344,7 +344,7 @@ export function sanitizeUserFacingText(text: string): string { return formatRawAssistantErrorForUi(trimmed); } - return collapseConsecutiveDuplicateBlocks(stripped); + return collapseConsecutiveDuplicateBlocks(stripped).trim(); } export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean { diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/pi-embedded-subscribe.ts index a4a4b906a..978f60d3a 100644 --- a/src/agents/pi-embedded-subscribe.ts +++ b/src/agents/pi-embedded-subscribe.ts @@ -359,7 +359,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar const emitBlockChunk = (text: string) => { if (state.suppressBlockChunks) return; // Strip and blocks across chunk boundaries to avoid leaking reasoning. - const chunk = stripBlockTags(text, state.blockState).trimEnd(); + const chunk = stripBlockTags(text, state.blockState).trim(); if (!chunk) return; if (chunk === state.lastBlockReplyText) return;