Five seconds to answer a one-word reply: three silent waits hiding in Hermes message delivery

You ask your assistant one short question on WhatsApp — “where do the logs live?” — and it answers one word: ~/.hermes/logs. You waited five seconds for that. On the same machine, the same exchange over Telegram comes back instantly. There is a worse version too: you close the laptop and take it home, send it three jobs from your phone on the way, and the next morning nothing happened. Your messages are still sitting in Telegram; Hermes’ log is completely empty.
Neither of those is a “failure” in the strict sense. One is just slow; the other is a message silently discarded. Three changes merged on 19-20 September (#117104, #116134, #116168) deal with exactly this class of problem: the ones with no error message.
Why a short reply had to wait five full seconds
Messaging adapters buffer text: a burst of fragments arriving within a moment is combined into one request, so a rapid paste-split does not trigger seven separate agent invocations. The catch was in the defaults and in how the delay was computed.
The WhatsApp adapter defaulted text_batch_delay_seconds to 5.0 s and Weixin to 3.0 s; and base.py::_text_batch_delay_for returns that flat delay for every fragment under the split threshold. A two-word answer therefore paid the full five seconds, because the debounce window has nothing that would end it early. The cost of that wait is entirely on the user.
PR #117104 aligns both adapters with the Telegram cadence:
| Adapter | Short-text delay (before) | Short-text delay (now) | Split delay (before → now) |
|---|---|---|---|
| 5.0 s | 0.3 s | 10.0 s → 1.0 s | |
| Weixin | 3.0 s | 0.3 s | 5.0 s → 1.0 s |
Configured values now have ceilings too: the delay clamps at 2.0 s and the split delay at 4.0 s (matching Telegram’s implementation), and the split delay may never be lower than the delay itself. One thing to judge for yourself: if you deliberately configured something like 7 seconds, it now clamps to 2.0.
gateway:
platforms:
whatsapp:
extra:
text_batch_delay_seconds: 0.5 # ceiling 2.0, floor 0
text_batch_split_delay_seconds: 1.5 # ceiling 4.0, never below the delay above
Setting the delay to 0 still disables batching, dispatching each message immediately.
A cold boot no longer throws away what arrived while you were offline
The second one is sneakier. The Telegram adapter’s connect() passed drop_pending_updates=not is_reconnect — so any fresh process told Telegram to discard every update queued during the outage.
That path is exactly the common case: a laptop powering on, a container restart, hermes gateway restart. You send instructions from your phone at night while the machine is off; the next boot drops them. No log, no error — the command simply never ran, which is the “silent” half of the report.
There is now a switch:
gateway:
platforms:
telegram:
extra:
drop_pending_on_cold_boot: false # default true = today's behaviour
With false, the first connection collects the backlog Telegram retained (bounded by its roughly 24-hour window). And the decision is logged either way (Cold boot: dropping|preserving …), so a command that never ran becomes traceable.
Two things deliberately do not change: the 409-conflict recovery ladder still drops pending updates (it has to, to evict the competing getUpdates session), and watcher reconnects always preserve them. The key applies to both startup paths — polling and webhook — since the old inline comment claiming the webhook flag was a no-op was wrong: PTB hands it to delete_webhook / set_webhook, which do drop Telegram’s stored updates.
A mention-free channel can thread again
The third one is an accidental either/or. free_response_channels decides whether an @mention is required; auto_thread decides whether each conversation gets its own thread. They are independent concerns — but once the inline default landed, the first silently disabled the second. A channel could therefore be mention-free (with every topic piled into one thread) or threaded (but requiring a mention every time). Community and support channels on Discord hit this first.
There is now an explicit opt-in:
# config.yaml top level (not under platforms:)
discord:
free_response_auto_thread: true # default false — nothing changes unless you set it
It affects only the free-response path, and the existing precedence is untouched: no_thread_channels still wins outright, voice-linked channels stay inline, and a global auto_thread: false still disables threading everywhere. The matching environment variable is DISCORD_FREE_RESPONSE_AUTO_THREAD, and the YAML key is bridged to it.
The common thread: silence
Put the three together and the failure mode is identical: no exception, no warning, only “why is nothing happening?” The five-second reply is a pure wait; the offline messages were actively discarded; the unthreaded channel was two independent defaults cancelling each other out. None of it is visible from the UI. So when you debug this class of problem, a workable order is:
- Confirm the message even reached Hermes — look for the inbound record in the gateway log.
- If it arrived but the reply is slow, suspect batching: write
text_batch_delay_secondsout explicitly instead of relying on the default. - If “nothing happened” after a cold boot, check
drop_pending_on_cold_bootand look for thatCold boot:line.
Is this available today?
Honest answer again: no release. The three changes merged on 19-20 September 2026, the newest release is v0.21.3 (tag v2026.9.14), so they are on main only — hermes update still gives you the old behaviour and will not recognise the keys above. The docs (messaging/whatsapp.md, messaging/weixin.md, messaging/telegram.md, messaging/discord.md) were updated alongside them.
The pattern
All three fixes correct the same habit: defaults deciding on the user’s behalf, without leaving a trace. A delay default is a number that suited someone else’s machine; dropping the backlog is mandatory on some deployments and eats your instructions on a personal laptop; two keys each have a sensible default that cancels the other out when combined. The remedies are plain — align a default with a cadence that has already been proven and clamp it, hand the choice back and log it every time, and add an explicit opt-in rather than flipping a default.
If you are wiring up messaging platforms, the Telegram inline picker and media delivery from a sandbox pair well with this post; for how messages in a multi-bot room get threaded and turn-taken, see the group chat write-up.