Skip to content

feat: Add errorPolicy support to WhatsApp channel#59801

Closed
smccaffrey wants to merge 3 commits into
openclaw:mainfrom
smccaffrey:feat/whatsapp-error-policy
Closed

feat: Add errorPolicy support to WhatsApp channel#59801
smccaffrey wants to merge 3 commits into
openclaw:mainfrom
smccaffrey:feat/whatsapp-error-policy

Conversation

@smccaffrey

Copy link
Copy Markdown

Summary

Adds errorPolicy configuration support to WhatsApp channels, matching the existing functionality available in Telegram channels.

Changes

  • Config schema: Add errorPolicy (always|once|silent) and errorCooldownMs to WhatsApp channel config
  • Runtime implementation: Wire up error suppression logic in WhatsApp auto-reply monitor
  • Error handling: Implements proper cooldown tracking for once policy, complete silence for silent policy
  • Tests: Full unit test suite for error policy resolution, suppression, and scope isolation
  • Backward compatibility: Default behavior unchanged (errorPolicy: always)

Motivation

WhatsApp channels currently have no way to suppress repetitive error messages during rate limits or other transient failures. Users can now configure:

  • errorPolicy: silent - suppress all error replies
  • errorPolicy: once - show error once per cooldown window (default 4hrs)
  • errorPolicy: always - current behavior (default)

Testing

Full unit test suite added (extensions/whatsapp/src/error-policy.test.ts) covering:

  • Default policy resolution
  • Config-driven policy and cooldown
  • Silent policy helper
  • Scope key generation
  • Suppression within cooldown windows
  • Cooldown expiry
  • Cross-account and cross-chat isolation

Adds errorPolicy and errorCooldownMs to WhatsApp channel config,
matching the existing Telegram channel pattern. This allows operators
to suppress or throttle error messages sent to WhatsApp users when
agent runs fail.

Closes openclaw#59782
- Tests for resolveWhatsAppErrorPolicy defaults and config resolution
- Tests for isSilentErrorPolicy helper
- Tests for buildWhatsAppErrorScopeKey
- Tests for shouldSuppressWhatsAppError:
  - First occurrence not suppressed
  - Repeated errors suppressed within cooldown
  - Different error messages not cross-suppressed
  - Cooldown expiry allows errors through again
  - No leaking across accounts or chats

Mirrors the existing Telegram error policy test suite.
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: whatsapp-web Channel integration: whatsapp-web size: M labels Apr 2, 2026
@greptile-apps

greptile-apps Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds errorPolicy (always | once | silent) and errorCooldownMs configuration to WhatsApp channels, mirroring the existing Telegram feature. The implementation — schema, type definitions, runtime resolver, and cooldown store — is clean and well-tested, with proper backward-compatible defaults.

Confidence Score: 5/5

  • Safe to merge; all remaining findings are minor P2 suggestions that don't affect the primary error-policy behavior.
  • No P0 or P1 issues found. The implementation is correct for typical usage. The inconsistency between msg.accountId and route.accountId for the scope key is theoretical (the route is constructed from msg.accountId), and the scope key collision concern is unlikely given real-world account/chat ID formats.
  • extensions/whatsapp/src/auto-reply/monitor/process-message.ts — minor inconsistency between the account ID used for config lookup vs. cooldown scope key.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/whatsapp/src/auto-reply/monitor/process-message.ts
Line: 430-435

Comment:
**`msg.accountId` vs `route.accountId` inconsistency**

The error policy config is resolved using `params.msg.accountId`, but the scope key passed to `shouldSuppressWhatsAppError` uses `params.route.accountId`. If `resolveAgentRoute` normalises or transforms the incoming account ID (e.g. aliases it), these two values can differ — causing the `once` cooldown to be recorded under a different key than the account whose config was read. The same pattern repeats in the `onError` block (lines 486–504). Consider reading the account once and using its `accountId` field for the scope key, or explicitly use `params.msg.accountId` for both.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/whatsapp/src/auto-reply/monitor/process-message.ts
Line: 429-455

Comment:
**Duplicate `resolveWhatsAppAccount` calls per message**

The account config is resolved independently inside both the `deliver` callback (line ~430) and the `onError` callback (line ~486). For every message that triggers an error, this causes two config lookups. Consider resolving the account config once before `createChannelReplyPipeline` and closing over it in both callbacks.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/whatsapp/src/error-policy.ts
Line: 29-31

Comment:
**Scope key separator can collide**

Using `:` as a separator means `accountId = "a:b"` + `chatId = "c"` produces the same key as `accountId = "a"` + `chatId = "b:c"`. If account IDs or chat IDs can contain colons (e.g. JID-style values), two different scopes would share a cooldown store. A simple mitigation is to escape or use a less common separator.

```suggestion
  return `${params.accountId}\x00${params.chatId}`;
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "test: Add unit tests for WhatsApp error ..." | Re-trigger Greptile

Comment on lines +430 to +435
const whatsAppAccount = resolveWhatsAppAccount({
cfg: params.cfg,
accountId: params.msg.accountId,
});
const errorPolicy = resolveWhatsAppErrorPolicy({
accountConfig: whatsAppAccount,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 msg.accountId vs route.accountId inconsistency

The error policy config is resolved using params.msg.accountId, but the scope key passed to shouldSuppressWhatsAppError uses params.route.accountId. If resolveAgentRoute normalises or transforms the incoming account ID (e.g. aliases it), these two values can differ — causing the once cooldown to be recorded under a different key than the account whose config was read. The same pattern repeats in the onError block (lines 486–504). Consider reading the account once and using its accountId field for the scope key, or explicitly use params.msg.accountId for both.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/whatsapp/src/auto-reply/monitor/process-message.ts
Line: 430-435

Comment:
**`msg.accountId` vs `route.accountId` inconsistency**

The error policy config is resolved using `params.msg.accountId`, but the scope key passed to `shouldSuppressWhatsAppError` uses `params.route.accountId`. If `resolveAgentRoute` normalises or transforms the incoming account ID (e.g. aliases it), these two values can differ — causing the `once` cooldown to be recorded under a different key than the account whose config was read. The same pattern repeats in the `onError` block (lines 486–504). Consider reading the account once and using its `accountId` field for the scope key, or explicitly use `params.msg.accountId` for both.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +429 to +455
if (payload.isError === true) {
const whatsAppAccount = resolveWhatsAppAccount({
cfg: params.cfg,
accountId: params.msg.accountId,
});
const errorPolicy = resolveWhatsAppErrorPolicy({
accountConfig: whatsAppAccount,
});
if (isSilentErrorPolicy(errorPolicy.policy)) {
logVerbose("Suppressed error reply (errorPolicy: silent)");
return;
}
if (
errorPolicy.policy === "once" &&
shouldSuppressWhatsAppError({
scopeKey: buildWhatsAppErrorScopeKey({
accountId: params.route.accountId,
chatId: conversationId ?? params.msg.from,
}),
cooldownMs: errorPolicy.cooldownMs,
errorMessage: payload.text,
})
) {
logVerbose("Suppressed error reply (errorPolicy: once, within cooldown)");
return;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate resolveWhatsAppAccount calls per message

The account config is resolved independently inside both the deliver callback (line ~430) and the onError callback (line ~486). For every message that triggers an error, this causes two config lookups. Consider resolving the account config once before createChannelReplyPipeline and closing over it in both callbacks.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/whatsapp/src/auto-reply/monitor/process-message.ts
Line: 429-455

Comment:
**Duplicate `resolveWhatsAppAccount` calls per message**

The account config is resolved independently inside both the `deliver` callback (line ~430) and the `onError` callback (line ~486). For every message that triggers an error, this causes two config lookups. Consider resolving the account config once before `createChannelReplyPipeline` and closing over it in both callbacks.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +29 to +31
export function buildWhatsAppErrorScopeKey(params: { accountId: string; chatId: string }): string {
return `${params.accountId}:${params.chatId}`;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Scope key separator can collide

Using : as a separator means accountId = "a:b" + chatId = "c" produces the same key as accountId = "a" + chatId = "b:c". If account IDs or chat IDs can contain colons (e.g. JID-style values), two different scopes would share a cooldown store. A simple mitigation is to escape or use a less common separator.

Suggested change
export function buildWhatsAppErrorScopeKey(params: { accountId: string; chatId: string }): string {
return `${params.accountId}:${params.chatId}`;
}
return `${params.accountId}\x00${params.chatId}`;
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/whatsapp/src/error-policy.ts
Line: 29-31

Comment:
**Scope key separator can collide**

Using `:` as a separator means `accountId = "a:b"` + `chatId = "c"` produces the same key as `accountId = "a"` + `chatId = "b:c"`. If account IDs or chat IDs can contain colons (e.g. JID-style values), two different scopes would share a cooldown store. A simple mitigation is to escape or use a less common separator.

```suggestion
  return `${params.accountId}\x00${params.chatId}`;
```

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90f76f3087

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +442 to +446
errorPolicy.policy === "once" &&
shouldSuppressWhatsAppError({
scopeKey: buildWhatsAppErrorScopeKey({
accountId: params.route.accountId,
chatId: conversationId ?? params.msg.from,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Start once-policy cooldown only after successful send

shouldSuppressWhatsAppError is called before deliverWebReply, and that helper mutates cooldown state on the first attempt. If the first error reply fails to send (for example, transient WhatsApp send failure), the cooldown is still recorded, so subsequent error replies in the window are suppressed even though the user never received one. This breaks the documented "once per cooldown window" behavior in failure scenarios.

Useful? React with 👍 / 👎.

Comment on lines +493 to +497
if (isSilentErrorPolicy(errorPolicy.policy)) {
return;
}
if (
errorPolicy.policy === "once" &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep delivery-failure logging independent of errorPolicy

This early return applies errorPolicy to the dispatcher onError path, but onError is invoked for any exception thrown during delivery, including normal (non-error) final replies. With errorPolicy: "silent" (or repeated "once" matches), outbound delivery failures stop being logged entirely, which can hide real send outages and makes operational debugging much harder.

Useful? React with 👍 / 👎.

- Hoist resolveWhatsAppAccount + errorPolicy resolution before pipeline
  to avoid duplicate config lookups per message (greptile, codex)
- Use params.msg.accountId consistently for scope key instead of mixing
  with params.route.accountId (greptile)
- Use null byte separator in scope key to prevent collisions when
  accountId or chatId contains colons (greptile)
- Remove errorPolicy suppression from onError callback — always log
  delivery failures regardless of policy to preserve operational
  visibility (codex)
- Note: shouldSuppressWhatsAppError is intentionally called before
  deliverWebReply (not after) to match the existing Telegram pattern
  and avoid complexity of rollback on send failure
@vincentkoc vincentkoc added the triage: dirty-candidate Candidate: broad unrelated surfaces; may need splitting or cleanup. label Apr 26, 2026
@clawsweeper

clawsweeper Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I did a careful shell check against current main, and the useful part of this older PR is already implemented there.

Close as mostly implemented on main: current main and v2026.6.6 already suppress WhatsApp error payload text by default, while the remaining configurable always/once surface is stale, conflicting, and reopens the broader error-disclosure policy tracked elsewhere.

So I’m closing this older PR as already covered on main rather than keeping a mostly-duplicated branch open.

Review details

Best possible solution:

Keep the shipped WhatsApp error suppression and use the canonical open issue for any future opt-in, custom-message, or owner-only error policy instead of landing this stale branch.

Do we have a high-confidence way to reproduce the issue?

Not applicable as a feature PR. Source inspection shows current main already suppresses WhatsApp isError payload delivery, while this branch proposes a new configurable policy surface.

Is this the best way to solve the issue?

No. The central suppression behavior is already shipped, and the branch's default-always config path conflicts with the current safer WhatsApp delivery behavior and should be replaced by a maintainer-approved policy if more configurability is needed.

Security review:

Security review needs attention: The diff can re-enable user-visible WhatsApp error text by default, which conflicts with the shipped suppression behavior and can expose provider/operator diagnostics to recipients.

  • [medium] Do not default WhatsApp back to raw error delivery — extensions/whatsapp/src/error-policy.ts:20
    The PR defaults errorPolicy to always and only suppresses silent or repeated once errors; current main and v2026.6.6 suppress WhatsApp error payloads before delivery, so porting this default would reopen an error-disclosure boundary.
    Confidence: 0.86

AGENTS.md: found and applied where relevant.

What I checked:

Likely related people:

  • mcaxtr: Recent current-main commits removed the earlier exposeErrorText config, made WhatsApp error suppression unconditional, and later refactored WhatsApp inbound dispatch around the same delivery path. (role: recent WhatsApp behavior contributor; confidence: high; commits: 4cba08df01ea, eebcb100b835, 458a52610a4d; files: extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts, src/config/types.whatsapp.ts, src/config/zod-schema.providers-whatsapp.ts)
  • steipete: Recent history shows repeated channel-message lifecycle and WhatsApp/Telegram dispatch refactors that shaped the current owner boundary this PR would need to target. (role: adjacent channel/message lifecycle contributor; confidence: medium; commits: 05eda57b3c72, 1507a9701b83, 77d9ac30bb8d; files: extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts, extensions/whatsapp/src/channel-outbound.ts, extensions/telegram/src/error-policy.ts)
  • rubencu: Authored the earlier WhatsApp exposeErrorText and sanitizer change that was later narrowed to unconditional suppression, so this history is relevant to the superseded config surface. (role: adjacent prior implementation author; confidence: medium; commits: 652f34103a4d; files: extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts, extensions/whatsapp/src/outbound-base.ts, src/config/types.whatsapp.ts)
  • chinar-amrutkar: Authored the Telegram error suppression controls that this branch explicitly mirrors, making them a useful sibling-surface routing candidate if maintainers revisit cross-channel policy. (role: sibling feature author; confidence: medium; commits: 74b9f22a424c; files: extensions/telegram/src/error-policy.ts, extensions/telegram/src/bot-message-dispatch.ts)

Codex review notes: model internal, reasoning high; reviewed against 37e3e895b020; fix evidence: release v2026.6.6, commit 4cba08df01ea.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 19, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 19, 2026
@clawsweeper clawsweeper Bot added P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels May 19, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jun 10, 2026
@clawsweeper clawsweeper Bot added rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 10, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Jun 11, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. labels Jun 14, 2026
@clawsweeper clawsweeper Bot added the status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. label Jun 14, 2026
@clawsweeper

clawsweeper Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

@clawsweeper clawsweeper Bot closed this Jun 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: whatsapp-web Channel integration: whatsapp-web docs Improvements or additions to documentation merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: M status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: dirty-candidate Candidate: broad unrelated surfaces; may need splitting or cleanup. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants