#14274: feat: add collapseReplyPayloads to collapse multi-message replies
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
#15864: feat: add deliverOnlyToolMessages config for clean messaging channe...
by gandalf-the-engineer · 2026-02-14
80.0%
#12180: fix: merge multi-block assistant texts into single reply
by 1960697431 · 2026-02-08
77.3%
#15853: feat: add option to suppress media placeholder text
by MisterGuy420 · 2026-02-14
77.2%
#19932: feat(agents): suppressPreToolText config + onBlockReply buffering
by Milofax · 2026-02-18
76.8%
#21547: feat: add compaction.announce config to notify users of compaction ...
by jlwestsr · 2026-02-20
75.6%
#15051: feat: Zulip channel plugin with concurrent message processing
by FtlC-ian · 2026-02-12
75.1%
#21699: feat(agents): add opt-in OpenAI payload logging for embedded runs
by jcp · 2026-02-20
74.9%
#10943: fix(config): resolve Control UI "Unsupported schema node" for confi...
by kraftbj · 2026-02-07
74.9%
#17552: fix(agents): suppress tool error warnings when assistant already re...
by AytuncYildizli · 2026-02-15
74.8%
#8271: feat(signal): Add full quoted message context support
by ProofOfReach · 2026-02-03
74.7%