#17552: fix(agents): suppress tool error warnings when assistant already replied
channel: voice-call
gateway
commands
agents
size: S
Cluster:
Tool Execution and Error Handling
## Summary
Fixes #9651
When the assistant composes a user-facing reply, `shouldShowToolErrorWarning` now returns `false` — the error is **not** surfaced as a separate `⚠️ … failed` payload. Previously, the function unconditionally returned `true` for any tool in `MUTATING_TOOL_NAMES`, bypassing the `hasUserFacingReply` check entirely.
Because `exec` and `bash` are in `MUTATING_TOOL_NAMES`, every non-zero exit — including perfectly normal ones like `grep` returning code 1 (no match) or `find` with no results — produced a warning message that leaked to messaging channels (Telegram, WhatsApp, Discord) as a separate chat bubble.
### Root cause
```typescript
// Before (payloads.ts — shouldShowToolErrorWarning)
const isMutatingToolError = params.lastToolError.mutatingAction
?? isLikelyMutatingToolName(params.lastToolError.toolName);
if (isMutatingToolError) {
return true; // ← bypasses hasUserFacingReply entirely
}
```
`isMutatingToolCall` unconditionally returns `true` for `exec`/`bash` (tool-mutation.ts:107-108), so `buildToolMutationState` always sets `mutatingAction = true`, and the early return fires on every failed command.
### Fix
- **`payloads.ts`**: Move the `hasUserFacingReply` check **above** the mutating-tool branch. When the assistant already addressed the error in its reply, duplicating it as a separate chat bubble is noise. When no reply exists, mutating-tool errors are still always surfaced as the sole failure signal.
- **`handlers.tools.ts`**: Guard `emitToolOutput` with `!isToolError` so error payloads are not emitted as regular tool output on verbose channels — matching the existing guard already used for media delivery (line 279).
### What changes
| Scenario | Before | After |
|----------|--------|-------|
| `exec` fails, assistant replies with explanation | `⚠️ Exec: … failed` **leaked** as separate message | Suppressed — assistant reply is sufficient |
| `write` fails, assistant replies "Done." | Duplicate error bubble | Suppressed — assistant already replied |
| `write` fails, **no** assistant reply | Error shown | Error shown (unchanged) |
| `message send` fails, **no** assistant reply | Error shown | Error shown (unchanged) |
| Non-mutating tool fails, no reply | Error shown | Error shown (unchanged) |
| `suppressToolErrors` enabled, mutating, no reply | Error shown | Error shown (unchanged) |
### Local validation
- `pnpm build` — clean ✅
- `pnpm check` — clean ✅
- `pnpm test` — all **5,136 unit tests** pass ✅
- **21 e2e tests** pass ✅
- Tested locally on a production OpenClaw instance (Mac Studio, WhatsApp channel) — the duplicate `⚠️ Exec: … failed` bubbles no longer appear when the assistant has already replied with an explanation.
### Tests
- Updated the existing test that validated the old behavior ("shows mutating tool errors even when assistant output exists" → "suppresses mutating tool errors when the assistant already composed a reply")
- Added test: "shows mutating tool errors when the assistant has no user-facing reply"
- Added test: "suppresses exec tool errors when the assistant already replied" — reproduces the exact scenario from #9651
### Scope
Single focused change: fix the duplicate error-message leak on messaging channels. Two files modified (`payloads.ts`, `handlers.tools.ts`), both addressing the same root cause from different angles.
### AI-assistance transparency
- [x] **AI-assisted**: This PR was authored with the help of Claude (Anthropic) via [OpenClaw](https://github.com/openclaw/openclaw) — specifically, the `exec` tool error was discovered during daily usage of an OpenClaw agent on WhatsApp, and the fix was developed interactively.
- [x] **Testing level**: Fully tested — all existing unit tests updated, 3 new targeted tests added, full `pnpm build && pnpm check && pnpm test` suite passes, and the fix was validated on a live production instance.
- [x] **Understanding**: The contributor ([@AytuncYildizli](https://github.com/AytuncYildizli)) understands the logic change and has been running OpenClaw with this patch in production since Feb 14, 2026 without regressions.
### Related
- #9651 — Exec tool error output sent as message to WhatsApp/Telegram (this PR fixes it)
- #11111 — Same issue on Discord (closed as dup of #9651)
- #10261 — Prior partial fix attempt (addresses `emitToolOutput` but not `shouldShowToolErrorWarning`)
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Fixes duplicate error messages on messaging channels by prioritizing assistant replies over raw tool errors. Previously, mutating tool errors (like `exec` or `bash` non-zero exits) always generated separate warning bubbles even when the assistant already explained the situation - this was especially noisy on WhatsApp/Telegram/Discord where each payload becomes a distinct chat bubble.
**Key changes:**
- `payloads.ts`: Reordered `shouldShowToolErrorWarning` logic to check `hasUserFacingReply` before the mutating-tool branch, suppressing duplicate error warnings when assistant has already replied
- `handlers.tools.ts`: Added `!isToolError` guard to `emitToolOutput` preventing error payloads from being emitted as regular tool output (matches existing guard on line 294 for media delivery)
- Tests updated to reflect new behavior with 3 new test cases covering the exact scenarios from issue #9651
The fix maintains the safety guarantee: mutating tool errors are still surfaced when there's NO assistant reply (the only failure signal).
<h3>Confidence Score: 5/5</h3>
- Safe to merge - focused fix with comprehensive test coverage and production validation
- The logic change is well-reasoned and addresses the root cause correctly. All existing tests pass (5,136 unit + 21 e2e), new tests cover the exact scenarios, and the author has run this in production since Feb 14. The change maintains safety by still showing errors when there's no assistant reply.
- No files require special attention
<sub>Last reviewed commit: 9f7eca5</sub>
<!-- 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
#19632: fix: suppressToolErrors now suppresses exec tool failure notifications
by Gitjay11 · 2026-02-18
87.8%
#22087: Preserve assistant reply when exec fails under suppressToolErrors
by graysurf · 2026-02-20
86.3%
#19235: fix(telegram): tool error warnings no longer overwrite streamed rep...
by gatewaybuddy · 2026-02-17
86.0%
#18466: fix: suppress recoverable mutating tool errors when agent already r...
by stijnhoste · 2026-02-16
85.7%
#20382: fix: move suppressToolErrors check before mutating tool check
by klawdius-noodle · 2026-02-18
84.5%
#14328: fix: strip incomplete tool_use blocks from errored/aborted messages...
by Kropiunig · 2026-02-12
82.5%
#18908: fix(payloads): place tool warnings before assistant reply
by BinHPdev · 2026-02-17
82.2%
#16733: fix(ui): avoid injected newlines when tool output is hidden
by jp117 · 2026-02-15
81.3%
#19399: telegram: fix MEDIA false positives and partial final drop
by HOYALIM · 2026-02-17
81.2%
#18992: fix: suppress spurious tool error warnings for read-only exec commands
by Phineas1500 · 2026-02-17
80.9%