fix(meshtastic): stop auto-acknowledging tapbacks - #4574
Conversation
`checkAutoAcknowledge` read hopStart/hopLimit/viaMqtt/text off the incoming message but never `emoji` — the field that marks a TEXT_MESSAGE_APP packet as a reaction. An inbound tapback therefore flowed through the normal auto-ack path and drew a response, either a text reply or the hop-count tapback. That response is pure waste: clients don't render a reaction to a reaction, so the sender never sees it. It only burns airtime. The default `^(test|ping)` regex hides the bug — a bare emoji can't match it. Operators running a permissive pattern to ack everything saw every reaction on the channel draw a reply, which is how it was found. Skip any message carrying an `emoji` value. The check sits ahead of the per-packet dedup guard so a tapback never consumes a slot in the bounded `autoAckProcessedPackets` set. Verified red/green: with the guard removed, the three tapback assertions in the new suite fail while both control assertions (ordinary message still acked, explicit-null emoji still acked) keep passing. Reported by Snayler. Fixes #4569 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EtJnjbUgYwJfNU6XXACbFf
|
Claude finished @Yeraze's task —— View job PR Review: fix(meshtastic): stop auto-acknowledging tapbacks
Overall: LGTM — clean, well-targeted, well-tested fix. Code Quality
if (message?.emoji != null) {The const emoji = (decodedEmoji !== undefined && decodedEmoji > 0) ? decodedEmoji : undefined;Zero and absent both normalize to Placement — Putting this check before the dedup guard (line 10201+) is the right call. As the comment explains, a rejected tapback shouldn't consume a slot in Comment quality — Unusually thorough inline documentation. The explanation of why this went unreported (default regex can't match bare emoji) is genuinely useful operational context. Test CoverageFive tests in the new file are well-chosen:
The explicit note about using a permissive regex ( The Minor Observations (non-blocking)
Security / PerformanceNo concerns. This is strictly a guard that exits early — it can only reduce airtime usage, never increase it. The fix is minimal, correct, and well-documented. The test suite exercises the exact failure modes described in the PR body and includes the controls needed to prevent vacuous passes. |
…nce (#4589) Ported from #4571, which had this right and was closed as superseded by #4574 after arriving first. Credit where due. The merged guard read `message?.emoji != null`. Meshtastic's `emoji` is a FLAG whose zero value means "not a reaction", so `!= null` classified a literal `emoji: 0` as a tapback and would have skipped acking an ordinary message. Unreachable today: the TEXT_MESSAGE_APP decode normalizes 0 to undefined (`decodedEmoji > 0 ? decodedEmoji : undefined`), so both spellings behave identically on the live path. But that made the guard silently dependent on a normalization several thousand lines away — move it, or add an ingestion path that skips it, and auto-ack quietly stops responding. Keying on the flag's own semantics removes the coupling. Verified red/green: the new `emoji: 0` case fails against `!= null` and passes with the truthiness test, while the other five assertions (including the explicit-null control) are unchanged either way. Claude-Session: https://claude.ai/code/session_01EtJnjbUgYwJfNU6XXACbFf Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Fixes #4569. Reported by Snayler.
checkAutoAcknowledge(meshtasticManager.ts) readshopStart,hopLimit,viaMqtt,relayNodeandtextoff the incoming message — but neveremoji, the field that marks aTEXT_MESSAGE_APPpacket as a reaction. The value is already extracted and set on the message object upstream (normalized toundefinedwhen absent or zero); it was simply never consulted. So an inbound tapback flowed through the normal auto-ack path and drew a response, either a text reply or the hop-count tapback.That response is pure waste, exactly as reported: clients don't render a reaction to a reaction, so the sender never sees it. It only costs airtime.
Why this went unreported for so long: the default regex is
^(test|ping), which a bare emoji can't match. The bug only fires for operators running a permissive pattern (.,.*) to ack everything — where every reaction on the channel drew a reply.The fix
One guard at the top of
checkAutoAcknowledge: skip any message carrying anemojivalue.It sits ahead of the per-packet dedup guard on purpose, so a skipped tapback never consumes a slot in the bounded
autoAckProcessedPacketsset (capped at 1000, trimmed to 500). There's a test for that ordering.MQTT-ingested messages don't call this function, so this single site covers the feature.
Test plan
npx tsc --noEmitandnpx tsc -p tsconfig.server.json --noEmit— cleannpm run lint:ci— clean, no baseline growthnpx vitest run: 13,504 passed, 0 failedsrc/server/meshtasticManager.autoAckTapbackSkip.test.ts(5 tests): tapback skipped under a match-everything regex; any non-zero emoji counts, not just1; ordinary message still acked and explicit-nullemoji still acked (the two controls that stop an over-broad guard from passing vacuously); and the dedup-slot ordering above.autoAckTapbackEmoji.test.tsstill green — the hop-count emoji MeshMonitor sends is unaffected.Red/green verified: with the guard commented out, exactly the three tapback assertions fail and both controls keep passing. The tests fail for the right reason, not by accident.
Note on the test harness
The suite enables the tapback cells and leaves the reply cells off, matching the sibling
autoAckTapbackEmojisuite. Enabling the reply cells needs channel/token plumbing this harness doesn't stub — my first draft did that and the control tests failed silently because the reply path threw inside the outertry. The tapback path alone proves the guard, since it's the response an ordinary message produces under these settings.Generated by Claude Code
https://claude.ai/code/session_01EtJnjbUgYwJfNU6XXACbFf