← Back to PRs

#14274: feat: add collapseReplyPayloads to collapse multi-message replies

by Henry-Roff-AI open 2026-02-11 21:25 View on GitHub →
agents stale
## Problem When an LLM produces text between tool calls (common with agentic models like Claude), each text block becomes a separate entry in `assistantTexts[]`. At the end of the agent turn, `buildEmbeddedRunPayloads` converts each entry into a separate reply payload, resulting in multiple messages being sent to the user's channel. This creates a "spam" effect where users receive 3-8 separate messages per agent turn. This is consistently reported as one of the biggest UX pain points across all messaging channels (Telegram, Slack, Discord, Signal, iMessage). **Related issues:** #12474 ## Solution New config option: `agents.defaults.collapseReplyPayloads` (boolean, default: `false`). When enabled, all `assistantTexts[]` entries are joined with `\n\n` into a single text before constructing reply payloads. One agent turn = one message. ```json { "agents": { "defaults": { "collapseReplyPayloads": true } } } ``` ## Changes - `src/config/types.agent-defaults.ts` — type addition (8 lines) - `src/config/zod-schema.agent-defaults.ts` — schema addition (1 line) - `src/agents/pi-embedded-runner/run/payloads.ts` — core logic (5 lines) Total: **+15 lines, -1 line** ## Behavior - Default `false` — backward compatible, no change in behavior - When `true`: multiple `assistantTexts` entries joined with `\n\n` separator - Single text blocks are unaffected - Error text suppression happens before collapsing - Media directives (`MEDIA:`) are preserved through collapsing - Works with all channels (Telegram, Slack, Discord, WhatsApp, Signal, iMessage) - No interaction with block streaming (which operates on a separate delivery path) ## Testing Tested on a live OpenClaw deployment (Telegram DM channel) with Claude Opus 4.6 performing multi-tool-call agent tasks: - **Before:** 5-8 separate messages per agent turn - **After:** 1-2 messages per turn (2 only when a webhook interrupts mid-turn, creating separate agent turns) The behavioral alternative (instructing the LLM to write zero text between tool calls) is fragile — it works until context compaction loses the instruction, then the spam returns. A config-level fix is more reliable. <!-- greptile_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> Adds an opt-in `agents.defaults.collapseReplyPayloads` boolean config option that joins multiple `assistantTexts[]` entries with `\n\n` into a single reply payload, reducing multi-message spam when LLMs produce intermediate text between tool calls. - Type, Zod schema, and core logic changes are minimal (+15 lines) and well-scoped - Default `false` preserves backward compatibility; strict `=== true` comparison ensures explicit opt-in - Error text suppression runs before collapsing, and `parseReplyDirectives` correctly handles all directives (`MEDIA:`, `[[reply_to:]]`, `[[audio_as_voice]]`) in concatenated text - No unit tests added for the new feature, though existing tests continue to pass since they don't set the config <h3>Confidence Score: 5/5</h3> - This PR is safe to merge — minimal, opt-in change with no impact on existing behavior when disabled. - Score reflects the small scope (+15 lines across 3 files), backward-compatible default (false), strict opt-in check (=== true), correct ordering of error suppression before collapsing, and verified compatibility with parseReplyDirectives for all directive types. No logical issues found. - No files require special attention. <!-- greptile_other_comments_section --> <sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub> <!-- /greptile_comment -->

Most Similar PRs