← Back to PRs

#16275: fix(agents): prevent rapid-fire duplicate messages during tool execution

by heyhudson open 2026-02-14 15:25 View on GitHub →
agents size: M trusted-contributor
## Summary Tool-heavy replies were being split into multiple outbound messages because block coalescing could idle-flush while a tool call was still running. This PR pauses idle-based coalescing during tool execution and resumes it when the tool ends, so pre-tool and post-tool text can remain in one coherent streamed reply. ## What Changed ### Block coalescer hold and resume during tool execution When a tool call takes longer than the coalescer idle timeout, buffered assistant text flushes mid-tool. Subsequent text then arrives as additional messages, which appears as fragmented or duplicate reply behavior across channels. - `src/auto-reply/reply/block-reply-coalescer.ts`: Added hold/resume semantics to the coalescer with new `hold()` and `resume()` APIs. - `src/auto-reply/reply/block-reply-coalescer.ts`: Added ref-counted `holdCount` so overlapping holds are handled safely. - `src/auto-reply/reply/block-reply-coalescer.ts`: `scheduleIdleFlush()` now skips scheduling while held. - `src/auto-reply/reply/block-reply-coalescer.ts`: `hold()` clears any active idle timer, and `resume()` reschedules only when all holds are released and text is buffered. - `src/auto-reply/reply/block-reply-pipeline.ts`: Exposed pass-through hold/resume on the pipeline. - `src/auto-reply/reply/agent-runner-execution.ts`, `src/agents/pi-embedded-runner/run.ts`, `src/agents/pi-embedded-runner/run/attempt.ts`, `src/agents/pi-embedded-runner/run/params.ts`, `src/agents/pi-embedded-subscribe.types.ts`, and `src/agents/pi-embedded-subscribe.handlers.types.ts`: Wired callbacks from auto-reply execution into embedded runner plumbing. - `src/agents/pi-embedded-subscribe.handlers.tools.ts`: `handleToolExecutionStart(...)` now flushes existing buffered block output, then calls `onBlockReplyHold()`. - `src/agents/pi-embedded-subscribe.handlers.tools.ts`: `handleToolExecutionEnd(...)` now calls `onBlockReplyResume()` before processing results. ## Design Notes - Ref-counted hold state (instead of a boolean) was chosen so parallel and overlapping tool execution cannot resume coalescing too early. - The change intentionally pauses only idle-timer flush behavior; force flush paths remain available for explicit boundaries and media payload behavior. - New callbacks are optional, preserving compatibility for call paths that do not enable block streaming. ## Testing - Added `src/auto-reply/reply/block-reply-coalescer.test.ts` with coverage for idle suppression while held, timer restart on resume, nested/parallel hold balancing, force flush behavior while held, and media enqueue behavior during hold. - Added `src/auto-reply/reply/block-reply-pipeline.test.ts` for hold/resume passthrough. - Updated `src/agents/pi-embedded-subscribe.handlers.tools.test.ts` to validate hold at tool start and resume at tool end. - Ran: `pnpm test src/auto-reply/reply/block-reply-coalescer.test.ts src/auto-reply/reply/block-reply-pipeline.test.ts src/agents/pi-embedded-subscribe.handlers.tools.test.ts` - Result: 3 files passed, 20 tests passed. ## AI Disclosure - [x] AI-assisted - [x] Fully tested ## Related Issues - #12659 - #13788 - #20226 --- Closes #3549 Closes #13944 Closes #15022 Closes #15968 Closes #20740 Closes #22258

Most Similar PRs