From c13e5474ddb1c1df622372dd55c4314329780cb3 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 19:33:16 -0600 Subject: [PATCH] =?UTF-8?q?forge:=20add=20patch=207=20=E2=80=94=20request?= =?UTF-8?q?=20token=20prefix=20for=20inter-bot=20suppression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bots now prefix every response with a 4-hex request token derived from the request fingerprint. Peers parsing the prefix can suppress their own pending response for the same request, fixing duplicate replies on #bot even for commands whose response text varies per bot (ack/test/hello/ status/roll/dice/path/trace/stats/time/lora/id/neighbors). Patches 0001-0006 re-exported (cosmetic series-count bump 1/6 -> 1/7). --- ...on-radio-firmware-bot-command-parity.patch | 2 +- ...n-firmware-bot-response-coordination.patch | 2 +- ...03-Allow-prefixless-firmware-bot-DMs.patch | 2 +- ...-bot-commands-with-upstream-behavior.patch | 2 +- ...t-coordination-utility-commands-and-.patch | 2 +- ...-speed-up-initial-advert-default-con.patch | 2 +- ...nses-with-request-token-for-inter-bo.patch | 256 ++++++++++++++++++ vendor/MeshCore | 2 +- 8 files changed, 263 insertions(+), 7 deletions(-) create mode 100644 patches/meshcore/0007-Prefix-bot-responses-with-request-token-for-inter-bo.patch diff --git a/patches/meshcore/0001-Add-companion-radio-firmware-bot-command-parity.patch b/patches/meshcore/0001-Add-companion-radio-firmware-bot-command-parity.patch index 0da6816..755ede2 100644 --- a/patches/meshcore/0001-Add-companion-radio-firmware-bot-command-parity.patch +++ b/patches/meshcore/0001-Add-companion-radio-firmware-bot-command-parity.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Fri, 15 May 2026 11:44:21 -0600 -Subject: [PATCH 1/6] Add companion radio firmware bot command parity +Subject: [PATCH 1/7] Add companion radio firmware bot command parity --- .../companion_radio/BotCommandRegistry.cpp | 121 ++ diff --git a/patches/meshcore/0002-Harden-firmware-bot-response-coordination.patch b/patches/meshcore/0002-Harden-firmware-bot-response-coordination.patch index 80e06e5..03443d9 100644 --- a/patches/meshcore/0002-Harden-firmware-bot-response-coordination.patch +++ b/patches/meshcore/0002-Harden-firmware-bot-response-coordination.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Fri, 15 May 2026 19:58:42 -0600 -Subject: [PATCH 2/6] Harden firmware bot response coordination +Subject: [PATCH 2/7] Harden firmware bot response coordination --- examples/companion_radio/BotCommands.cpp | 2 +- diff --git a/patches/meshcore/0003-Allow-prefixless-firmware-bot-DMs.patch b/patches/meshcore/0003-Allow-prefixless-firmware-bot-DMs.patch index 36e4523..63f36d1 100644 --- a/patches/meshcore/0003-Allow-prefixless-firmware-bot-DMs.patch +++ b/patches/meshcore/0003-Allow-prefixless-firmware-bot-DMs.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Fri, 15 May 2026 20:09:58 -0600 -Subject: [PATCH 3/6] Allow prefixless firmware bot DMs +Subject: [PATCH 3/7] Allow prefixless firmware bot DMs --- examples/companion_radio/BotPolicy.cpp | 2 +- diff --git a/patches/meshcore/0004-Align-firmware-bot-commands-with-upstream-behavior.patch b/patches/meshcore/0004-Align-firmware-bot-commands-with-upstream-behavior.patch index 0c50774..0be6171 100644 --- a/patches/meshcore/0004-Align-firmware-bot-commands-with-upstream-behavior.patch +++ b/patches/meshcore/0004-Align-firmware-bot-commands-with-upstream-behavior.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 04:12:02 +0000 -Subject: [PATCH 4/6] Align firmware bot commands with upstream behavior +Subject: [PATCH 4/7] Align firmware bot commands with upstream behavior --- .../companion_radio/BotCommandRegistry.cpp | 25 +- diff --git a/patches/meshcore/0005-Add-hop-aware-bot-coordination-utility-commands-and-.patch b/patches/meshcore/0005-Add-hop-aware-bot-coordination-utility-commands-and-.patch index 6cf6831..63fb34f 100644 --- a/patches/meshcore/0005-Add-hop-aware-bot-coordination-utility-commands-and-.patch +++ b/patches/meshcore/0005-Add-hop-aware-bot-coordination-utility-commands-and-.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 12:38:13 -0600 -Subject: [PATCH 5/6] Add hop-aware bot coordination, utility commands, and +Subject: [PATCH 5/7] Add hop-aware bot coordination, utility commands, and bounded delays Adds new utility commands (TIME, LORA, ID, NEIGHBORS), neighbor tracking, diff --git a/patches/meshcore/0006-Mark-bot-adverts-speed-up-initial-advert-default-con.patch b/patches/meshcore/0006-Mark-bot-adverts-speed-up-initial-advert-default-con.patch index 9b66084..4183b73 100644 --- a/patches/meshcore/0006-Mark-bot-adverts-speed-up-initial-advert-default-con.patch +++ b/patches/meshcore/0006-Mark-bot-adverts-speed-up-initial-advert-default-con.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 18:58:28 -0600 -Subject: [PATCH 6/6] Mark bot adverts, speed up initial advert, default +Subject: [PATCH 6/7] Mark bot adverts, speed up initial advert, default contacts auto-overwrite Three coordination/discoverability fixes: diff --git a/patches/meshcore/0007-Prefix-bot-responses-with-request-token-for-inter-bo.patch b/patches/meshcore/0007-Prefix-bot-responses-with-request-token-for-inter-bo.patch new file mode 100644 index 0000000..61f78fc --- /dev/null +++ b/patches/meshcore/0007-Prefix-bot-responses-with-request-token-for-inter-bo.patch @@ -0,0 +1,256 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: cj-vana +Date: Sat, 16 May 2026 19:33:04 -0600 +Subject: [PATCH 7/7] Prefix bot responses with request token for inter-bot + suppression + +Adds a 4-hex request token (low 16 bits of the request fingerprint) as a +leading "[XXXX] " prefix on every bot response. Two bots receiving the +same channel request compute the same fingerprint -> same token, so a +peer bot can correlate an observed response back to its own pending +response even when the response text differs (per-bot data like hop +count, SNR, recv time, etc.). + +New FirmwareBot helpers: +- requestToken(BotFingerprint) -> uint16_t +- formatRequestToken(uint16_t, char[5]) +- parseRequestTokenPrefix(text, len, *token, *prefix_len) +- prependRequestToken(request_fingerprint, text, text_len, buf_len, *new_len) + +New ResponseCoordinator function: +- suppressByRequestToken(pending, count, token16) suppresses entries + where (request_fingerprint.value & 0xFFFF) == token16 + +Wiring: +- MyMesh::enqueueBotResponse prepends the token to every queued response +- MyMesh::sendBotTraceText prepends the token to async trace results +- MyMesh::observeBotGroupResponse parses the token from incoming peer + responses and suppresses matching pending responses, in addition to + the existing response-fingerprint match (which still handles deterministic + commands like ping->Pong where both bots produce identical text) + +Fixes inter-bot duplication on #bot for ack/test/hello/status/roll/dice/ +path/trace/tracer/stats/time/lora/id/neighbors observed on the Colorado +mesh when two bots both queued responses to the same command. +--- + examples/companion_radio/FirmwareBot.cpp | 65 +++++++++++++++++++ + examples/companion_radio/FirmwareBot.h | 5 ++ + examples/companion_radio/MyMesh.cpp | 40 +++++++++--- + .../companion_radio/ResponseCoordinator.cpp | 15 +++++ + .../companion_radio/ResponseCoordinator.h | 1 + + 5 files changed, 118 insertions(+), 8 deletions(-) + +diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp +index 6aac8a1b..24c79e81 100644 +--- a/examples/companion_radio/FirmwareBot.cpp ++++ b/examples/companion_radio/FirmwareBot.cpp +@@ -422,4 +422,69 @@ BotFingerprint responseFingerprintFor(const BotMessage& message, const char* res + return fingerprint; + } + ++uint16_t requestToken(BotFingerprint request_fingerprint) { ++ // Take the low 16 bits of the request fingerprint. Two bots computing ++ // fingerprintFor() against the same request derive the same token, so ++ // a bot can recognise another bot's response to a request it also queued ++ // even when the response text differs (different hop count, SNR, recv time). ++ return (uint16_t)(request_fingerprint.value & 0xFFFFu); ++} ++ ++void formatRequestToken(uint16_t token, char out[5]) { ++ if (!out) return; ++ static const char hex[] = "0123456789abcdef"; ++ out[0] = hex[(token >> 12) & 0xF]; ++ out[1] = hex[(token >> 8) & 0xF]; ++ out[2] = hex[(token >> 4) & 0xF]; ++ out[3] = hex[token & 0xF]; ++ out[4] = 0; ++} ++ ++static int hexNibble(char c) { ++ if (c >= '0' && c <= '9') return c - '0'; ++ if (c >= 'a' && c <= 'f') return 10 + (c - 'a'); ++ if (c >= 'A' && c <= 'F') return 10 + (c - 'A'); ++ return -1; ++} ++ ++bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len) { ++ if (token) *token = 0; ++ if (prefix_len) *prefix_len = 0; ++ if (!text || text_len < 7) return false; // "[XXXX] " = 7 chars minimum ++ if (text[0] != '[' || text[5] != ']' || text[6] != ' ') return false; ++ uint16_t value = 0; ++ for (int i = 0; i < 4; i++) { ++ int n = hexNibble(text[1 + i]); ++ if (n < 0) return false; ++ value = (uint16_t)((value << 4) | (uint16_t)n); ++ } ++ if (token) *token = value; ++ if (prefix_len) *prefix_len = 7; ++ return true; ++} ++ ++BotWriteResult prependRequestToken(BotFingerprint request_fingerprint, char* text, size_t text_len, size_t buf_len, ++ size_t* new_len) { ++ if (new_len) *new_len = text_len; ++ if (!text || buf_len == 0) return BOT_WRITE_NO_SPACE; ++ if (text_len + 7 + 1 > buf_len) { ++ // No room for "[XXXX] " prefix + null terminator without dropping content. ++ return BOT_WRITE_NO_SPACE; ++ } ++ char hex[5]; ++ formatRequestToken(requestToken(request_fingerprint), hex); ++ memmove(text + 7, text, text_len); ++ text[0] = '['; ++ text[1] = hex[0]; ++ text[2] = hex[1]; ++ text[3] = hex[2]; ++ text[4] = hex[3]; ++ text[5] = ']'; ++ text[6] = ' '; ++ size_t total = text_len + 7; ++ if (total < buf_len) text[total] = 0; ++ if (new_len) *new_len = total; ++ return BOT_WRITE_OK; ++} ++ + } +diff --git a/examples/companion_radio/FirmwareBot.h b/examples/companion_radio/FirmwareBot.h +index 9eff340b..6e5cfd9a 100644 +--- a/examples/companion_radio/FirmwareBot.h ++++ b/examples/companion_radio/FirmwareBot.h +@@ -20,6 +20,11 @@ BotWriteResult writeBotAdvertName(const char* node_name, char* output, size_t ou + bool isBotAdvertName(const char* name, size_t name_len); + BotFingerprint fingerprintFor(const BotMessage& message); + BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len); ++uint16_t requestToken(BotFingerprint request_fingerprint); ++void formatRequestToken(uint16_t token, char out[5]); ++bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len); ++BotWriteResult prependRequestToken(BotFingerprint request_fingerprint, char* text, size_t text_len, size_t buf_len, ++ size_t* new_len); + BotCommandId commandIdForName(const char* name, size_t len); + size_t maxResponseLenForChannel(BotChannelKind channel_kind); + bool isCommandOnCooldown(const BotCommandCooldown* cooldowns, size_t cooldown_count, BotCommandId command_id, +diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp +index 9209dd86..02339124 100644 +--- a/examples/companion_radio/MyMesh.cpp ++++ b/examples/companion_radio/MyMesh.cpp +@@ -1209,6 +1209,11 @@ bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *di + if (!botFormatResponseForChannel(message, text, text_len, pending->text, sizeof(pending->text), &pending->text_len)) { + return false; + } ++ // Prefix every bot response with the request token so peer bots can correlate ++ // their pending response to ours (their response text may differ from ours when ++ // the command includes per-bot data like hop count, SNR, recv time, etc.). ++ FirmwareBot::prependRequestToken(request_fingerprint, pending->text, pending->text_len, sizeof(pending->text), ++ &pending->text_len); + pending->active = true; + return true; + } +@@ -1613,6 +1618,17 @@ BotFingerprint MyMesh::traceResponseFingerprintFor(const PendingBotTrace &pendin + + bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, size_t text_len, + BotFingerprint response_fingerprint, uint32_t now_millis) { ++ // Prefix with the request token so peer bots can correlate this trace result ++ // back to their own pending trace for the same request and suppress duplicates. ++ char tokened[BOT_MAX_RESPONSE_LEN + 1]; ++ size_t tokened_len = text_len; ++ if (tokened_len > BOT_MAX_RESPONSE_LEN) tokened_len = BOT_MAX_RESPONSE_LEN; ++ if (tokened_len > 0) memcpy(tokened, text, tokened_len); ++ tokened[tokened_len] = 0; ++ FirmwareBot::prependRequestToken(pending.request_fingerprint, tokened, tokened_len, sizeof(tokened), &tokened_len); ++ const char *send_text = tokened; ++ size_t send_len = tokened_len; ++ + bool success = false; + if (pending.direct) { + ContactInfo *recipient = lookupContactByPubKey(pending.recipient_pub_key, PUB_KEY_SIZE); +@@ -1620,12 +1636,7 @@ bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, + uint32_t expected_ack = 0; + uint32_t est_timeout = 0; + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); +- char response[BOT_MAX_RESPONSE_LEN + 1]; +- size_t copy_len = text_len; +- if (copy_len > BOT_MAX_RESPONSE_LEN) copy_len = BOT_MAX_RESPONSE_LEN; +- if (copy_len > 0) memcpy(response, text, copy_len); +- response[copy_len] = 0; +- int result = sendMessage(*recipient, timestamp, 0, response, expected_ack, est_timeout); ++ int result = sendMessage(*recipient, timestamp, 0, send_text, expected_ack, est_timeout); + success = result != MSG_SEND_FAILED; + if (success && expected_ack) { + expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); +@@ -1638,7 +1649,7 @@ bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, + ChannelDetails channel; + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + success = getChannel(pending.channel_idx, channel) && +- sendGroupMessage(timestamp, channel.channel, _prefs.node_name, text, text_len); ++ sendGroupMessage(timestamp, channel.channel, _prefs.node_name, send_text, send_len); + } + + if (success) { +@@ -1744,6 +1755,19 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati + + bool MyMesh::observeBotGroupResponse(const BotMessage &message) { + if (!BotPolicy::isPrefixlessCommandAllowed(message.channel_kind)) return false; ++ ++ // If the incoming channel message carries a request token from a peer bot, ++ // suppress our own pending response for that same request even when the ++ // response text differs (hop count, SNR, recv time, etc.). ++ uint16_t token = 0; ++ size_t token_prefix_len = 0; ++ bool token_suppressed = false; ++ if (FirmwareBot::parseRequestTokenPrefix(message.text, message.text_len, &token, &token_prefix_len)) { ++ if (ResponseCoordinator::suppressByRequestToken(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, token)) { ++ token_suppressed = true; ++ } ++ } ++ + BotFingerprint fingerprint = FirmwareBot::responseFingerprintFor(message, message.text, message.text_len); + if (ResponseCoordinator::recentlySent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, fingerprint, _ms->getMillis())) { + return true; +@@ -1751,7 +1775,7 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) { + if (ResponseCoordinator::suppress(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint)) { + return true; + } +- return false; ++ return token_suppressed; + } + + void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx) { +diff --git a/examples/companion_radio/ResponseCoordinator.cpp b/examples/companion_radio/ResponseCoordinator.cpp +index 7afdab24..3640f8b6 100644 +--- a/examples/companion_radio/ResponseCoordinator.cpp ++++ b/examples/companion_radio/ResponseCoordinator.cpp +@@ -153,6 +153,21 @@ bool suppress(BotCoordinatorPending pending[], size_t pending_count, BotFingerpr + return suppressed; + } + ++bool suppressByRequestToken(BotCoordinatorPending pending[], size_t pending_count, uint16_t request_token) { ++ if (!pending) return false; ++ bool suppressed = false; ++ for (size_t i = 0; i < pending_count; i++) { ++ if (!pending[i].active) continue; ++ if (pending[i].request_fingerprint.value == 0) continue; ++ uint16_t token = (uint16_t)(pending[i].request_fingerprint.value & 0xFFFFu); ++ if (token == request_token) { ++ pending[i].suppressed = true; ++ suppressed = true; ++ } ++ } ++ return suppressed; ++} ++ + bool cancel(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint request_fingerprint) { + if (!pending || request_fingerprint.value == 0) return false; + for (size_t i = 0; i < pending_count; i++) { +diff --git a/examples/companion_radio/ResponseCoordinator.h b/examples/companion_radio/ResponseCoordinator.h +index f549b887..7e053b44 100644 +--- a/examples/companion_radio/ResponseCoordinator.h ++++ b/examples/companion_radio/ResponseCoordinator.h +@@ -32,6 +32,7 @@ BotCoordinatorScheduleResult schedule(BotCoordinatorPending pending[], size_t pe + uint8_t queue_depth, uint16_t base_delay_millis, uint16_t jitter_millis, + uint16_t hop_step_millis, BotFingerprint* fingerprint, uint32_t* due_at_millis); + bool suppress(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint response_fingerprint); ++bool suppressByRequestToken(BotCoordinatorPending pending[], size_t pending_count, uint16_t request_token); + bool cancel(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint request_fingerprint); + BotCoordinatorReady poll(BotCoordinatorPending pending[], size_t pending_count, uint32_t now_millis); + void recordRecent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint, diff --git a/vendor/MeshCore b/vendor/MeshCore index 2252de4..a358fc5 160000 --- a/vendor/MeshCore +++ b/vendor/MeshCore @@ -1 +1 @@ -Subproject commit 2252de4e49f65565bd1ed7a02435c90132e63874 +Subproject commit a358fc5a40771b44fd16af819d744663f9ec82bb