Summary
Running doctor on a config that still uses the legacy channels.whatsapp.ackReaction object silently drops DM acknowledgement reactions whenever the legacy settings mean "ack DMs plus mentioned groups" (direct: true with group: "mentions"). That combination is also the legacy default, so a plain ackReaction: { "emoji": "..." } entry is affected too. After migration the config ends up with messages.ackReactionScope: "group-mentions", which the WhatsApp runtime interprets as DMs disabled, and the legacy object is deleted so the original intent is unrecoverable. Doctor reports the migration as a clean move.
Environment
- Commit: 62c148d7d003dc67e4b1d6fea71737b9dca61e14 (origin/main at time of filing)
- Surface: doctor legacy config migration for WhatsApp ack reactions, plus the WhatsApp auto-reply ack reaction monitor
Steps to reproduce
- Create an isolated state dir with a legacy WhatsApp ack config:
{
"channels": {
"whatsapp": {
"ackReaction": { "direct": true, "group": "mentions", "emoji": "👀" }
}
}
}
- Run doctor against it:
OPENCLAW_STATE_DIR=<dir> node scripts/run-node.mjs doctor --non-interactive --fix
- Inspect the rewritten
openclaw.json: the legacy object is gone and messages.ackReactionScope is "group-mentions".
- Deliver a DM through the WhatsApp auto-reply monitor (
maybeSendAckReaction) with the migrated config: no ack reaction is sent. A group message with a mention still gets one.
Expected
A legacy config that acked DMs keeps acking DMs after migration. If the migration cannot represent the legacy combination, it should say so (doctor note or warning) instead of deleting the legacy object and reporting a clean move.
Actual
The migration writes no explicit scope for direct: true plus group: "mentions", deletes channels.whatsapp.ackReaction, and logs "Moved translatable channels.whatsapp.ackReaction settings to messages ack settings." Default materialization then persists ackReactionScope: "group-mentions". The runtime treats that scope as directEnabled = false, so DM acknowledgements stop with no signal to the user.
Root cause (if known)
src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts:356-375. The mapping ladder covers five of the six legacy combinations and yields undefined for direct && group === "mentions":
const direct = ack.direct !== false;
const group = ack.group ?? "mentions";
const scope =
direct && group === "always"
? "all"
: direct && group === "never"
? "direct"
: !direct && group === "always"
? "group-all"
: !direct && group === "mentions"
? "group-mentions"
: !direct && group === "never"
? "off"
: undefined;
if (scope) {
messages.ackReactionScope = scope;
}
// ...
delete entry.ackReaction;
The unmapped combination is exactly the legacy default (direct defaults to true, group defaults to "mentions"). The shared ackReactionScope enum has no value meaning "DMs plus mentioned groups": all widens groups to every message, direct drops groups, group-mentions drops DMs (extensions/whatsapp/src/auto-reply/monitor/ack-reaction.ts:43-55 derives directEnabled = scope === "all" || scope === "direct"). So a lossless 1:1 mapping is impossible with the current enum; the bug is that the migration silently picks the lossy default and destroys the legacy source.
Sibling surfaces
The messages.ackReactionScope interpretation is shared with other channels, but the documented DM gotcha there (#84233) applies to users who chose a scope explicitly. Only WhatsApp had the legacy per-channel {direct, group} object, so the silent-migration loss is WhatsApp-specific. #91347 (whether doctor should create channels.whatsapp at all during this migration) and #109979 (LID sender identities in group acks) do not touch the scope mapping.
Real behavior proof
Behavior addressed: doctor migration of legacy channels.whatsapp.ackReaction with direct: true, group: "mentions" loses DM acknowledgement reactions
Real environment tested: real doctor CLI (node scripts/run-node.mjs doctor --non-interactive --fix) against an isolated state dir, then the real WhatsApp auto-reply monitor (maybeSendAckReaction with the real config loader and real admission builder), at commit 62c148d7d003dc67e4b1d6fea71737b9dca61e14
Exact steps or command run after this patch: OPENCLAW_STATE_DIR=<dir> node scripts/run-node.mjs doctor --non-interactive --fix on the legacy config above, then drove maybeSendAckReaction with the migrated config for a DM and for a group message with a mention
Evidence after fix:
# OBSERVED (buggy, origin/main 62c148d7d003)
# doctor change note:
Moved translatable channels.whatsapp.ackReaction settings to messages
# migrated openclaw.json (legacy channels.whatsapp.ackReaction deleted):
"messages": {
"ackReaction": "👀",
"ackReactionScope": "group-mentions"
}
# real WhatsApp monitor decisions with the migrated config:
DM from paired user: ack SKIPPED (no reaction)
Group message with @mention: ack SENT (emoji 👀)
# same DM when the scope permits directs (shows only the migrated scope suppresses it):
DM from paired user: ack SENT (emoji 👀)
# EXPECTED (correct behavior on identical inputs)
The migrated config preserves DM acknowledgements, or doctor surfaces a warning that
the legacy direct-plus-group-mentions combination cannot be represented, instead of
deleting the legacy object and reporting a clean move.
Observed result after fix: the migration discards the DM half of the legacy ack scope, the rewritten config suppresses DM ack reactions, and the same DM is acked again the moment the scope permits directs
What was not tested: delivery of the reaction over a live WhatsApp socket (no linked account on this machine; the send was attempted and only failed at the socket layer), other channels' runtime interpretation of the defaulted scope, and the interactive doctor flow (the non-interactive flag was used)
Additional notes
The emoji-only legacy form "ackReaction": { "emoji": "👀" } (legacy defaults for both direct and group) migrates to the same "group-mentions" scope and loses DM acks identically, verified with a second doctor run on a fresh state dir.
Originally posted by @yetval in openclaw/openclaw#112796
Summary
Running doctor on a config that still uses the legacy
channels.whatsapp.ackReactionobject silently drops DM acknowledgement reactions whenever the legacy settings mean "ack DMs plus mentioned groups" (direct: truewithgroup: "mentions"). That combination is also the legacy default, so a plainackReaction: { "emoji": "..." }entry is affected too. After migration the config ends up withmessages.ackReactionScope: "group-mentions", which the WhatsApp runtime interprets as DMs disabled, and the legacy object is deleted so the original intent is unrecoverable. Doctor reports the migration as a clean move.Environment
Steps to reproduce
{ "channels": { "whatsapp": { "ackReaction": { "direct": true, "group": "mentions", "emoji": "👀" } } } }OPENCLAW_STATE_DIR=<dir> node scripts/run-node.mjs doctor --non-interactive --fixopenclaw.json: the legacy object is gone andmessages.ackReactionScopeis"group-mentions".maybeSendAckReaction) with the migrated config: no ack reaction is sent. A group message with a mention still gets one.Expected
A legacy config that acked DMs keeps acking DMs after migration. If the migration cannot represent the legacy combination, it should say so (doctor note or warning) instead of deleting the legacy object and reporting a clean move.
Actual
The migration writes no explicit scope for
direct: trueplusgroup: "mentions", deleteschannels.whatsapp.ackReaction, and logs "Moved translatable channels.whatsapp.ackReaction settings to messages ack settings." Default materialization then persistsackReactionScope: "group-mentions". The runtime treats that scope asdirectEnabled = false, so DM acknowledgements stop with no signal to the user.Root cause (if known)
src/commands/doctor/shared/legacy-config-migrations.runtime.retired.ts:356-375. The mapping ladder covers five of the six legacy combinations and yieldsundefinedfordirect && group === "mentions":The unmapped combination is exactly the legacy default (
directdefaults to true,groupdefaults to "mentions"). The sharedackReactionScopeenum has no value meaning "DMs plus mentioned groups":allwidens groups to every message,directdrops groups,group-mentionsdrops DMs (extensions/whatsapp/src/auto-reply/monitor/ack-reaction.ts:43-55derivesdirectEnabled = scope === "all" || scope === "direct"). So a lossless 1:1 mapping is impossible with the current enum; the bug is that the migration silently picks the lossy default and destroys the legacy source.Sibling surfaces
The
messages.ackReactionScopeinterpretation is shared with other channels, but the documented DM gotcha there (#84233) applies to users who chose a scope explicitly. Only WhatsApp had the legacy per-channel{direct, group}object, so the silent-migration loss is WhatsApp-specific. #91347 (whether doctor should createchannels.whatsappat all during this migration) and #109979 (LID sender identities in group acks) do not touch the scope mapping.Real behavior proof
Behavior addressed: doctor migration of legacy
channels.whatsapp.ackReactionwithdirect: true, group: "mentions"loses DM acknowledgement reactionsReal environment tested: real doctor CLI (
node scripts/run-node.mjs doctor --non-interactive --fix) against an isolated state dir, then the real WhatsApp auto-reply monitor (maybeSendAckReactionwith the real config loader and real admission builder), at commit 62c148d7d003dc67e4b1d6fea71737b9dca61e14Exact steps or command run after this patch:
OPENCLAW_STATE_DIR=<dir> node scripts/run-node.mjs doctor --non-interactive --fixon the legacy config above, then drovemaybeSendAckReactionwith the migrated config for a DM and for a group message with a mentionEvidence after fix:
Observed result after fix: the migration discards the DM half of the legacy ack scope, the rewritten config suppresses DM ack reactions, and the same DM is acked again the moment the scope permits directs
What was not tested: delivery of the reaction over a live WhatsApp socket (no linked account on this machine; the send was attempted and only failed at the socket layer), other channels' runtime interpretation of the defaulted scope, and the interactive doctor flow (the non-interactive flag was used)
Additional notes
The emoji-only legacy form
"ackReaction": { "emoji": "👀" }(legacy defaults for bothdirectandgroup) migrates to the same"group-mentions"scope and loses DM acks identically, verified with a second doctor run on a fresh state dir.Originally posted by @yetval in openclaw/openclaw#112796