Commit graph

326 commits

Author SHA1 Message Date
LeonSGP43
dbd8704673 fix(slack): clear stuck assistant status on /stop and via explicit metadata
Salvaged from #32340 by @LeonSGP43, adapted to the workspace-scoped
status tracking that landed in #63709:

- /stop with no running agent now best-effort clears the platform
  status indicator, so a phantom 'is thinking...' left by a gateway
  restart or a turn that died without a final send can always be
  dismissed (#32295).
- SlackAdapter.stop_typing clears an untracked thread when the caller
  names it explicitly in metadata — clearing an unset status is a
  harmless no-op on Slack's side. The fallback is skipped when multiple
  Slack Connect workspaces track the same channel+thread and no team_id
  is given, preserving #63709's cross-workspace safety guarantee.
2026-07-14 21:31:32 -07:00
HexLab98
f5f79ff1b4 fix(telegram): instrument getUpdates request via subclass re-tag, not slotted attr
PTB's HTTPXRequest/BaseRequest use __slots__. On Python 3.13 their
instances no longer carry a __dict__, so the getUpdates progress
instrumentation's `request.do_request = wrapper` monkey-patch raises
`AttributeError: 'HTTPXRequest' object attribute 'do_request' is
read-only`, failing every Telegram connect (#64482). It only worked on
Python 3.12, where the instance still had a __dict__ — the Docker image
ships Python 3.13, so the release broke Telegram outright.

Re-tag the request to a thin `__slots__ = ()` subclass that overrides
do_request instead of mutating the instance. This preserves the exact
progress-observation semantics, works on both 3.12 and 3.13, and covers
the real request and the test doubles alike.
2026-07-14 21:25:44 -07:00
Teknium
1f216de3a8 fix(slack): gate feedback buttons behind rich_blocks as documented
The docs state feedback_buttons requires rich_blocks: true, but
_maybe_blocks rendered full Block Kit whenever feedback_buttons alone
was enabled — implicitly turning on rich-block rendering the user never
opted into. Align the code with the documented contract and add a
regression test.
2026-07-14 13:58:36 -07:00
kshitijk4poor
fc8f8ad33f fix(slack): clear uniquely scoped assistant status 2026-07-14 13:58:36 -07:00
kshitijk4poor
38cfae9b54 fix(slack): scope Agent View workspace state 2026-07-14 13:58:36 -07:00
kshitijk4poor
4554fe128a fix(slack): complete agent view workspace routing 2026-07-14 13:58:36 -07:00
Edison42
f1328a6bfd feat(slack): cover agent view assistant APIs 2026-07-14 13:58:36 -07:00
Edison42
9a3b676fed feat(slack): support agent view manifests 2026-07-14 13:58:36 -07:00
Teknium
4e6e5181c6 refactor(telegram): drop dead _content_is_pipe_table_primary helper
After #53825's fix removed the auto-rich table bypass from
_rich_delivery_enabled(), _content_is_pipe_table_primary() had zero
callers. Remove it and simplify _rich_delivery_enabled() to the bare
rich_messages opt-in check (content param no longer used).
2026-07-14 06:48:53 -07:00
liuhao1024
34d07732dd fix(telegram): respect rich_messages config for pipe table routing
Remove the pipe-table bypass from _rich_delivery_enabled() so that
rich_messages: false is fully honoured.  Previously, pipe tables were
auto-routed to sendRichMessage regardless of the config flag, breaking
delivery on clients without Bot API 10.1 support (AyuGram, Telegram
Web, some desktop clients).

Fixes #53824
2026-07-14 06:48:53 -07:00
Roseyco-management
b8295cf6f7 fix(telegram): gate polling health on getUpdates progress 2026-07-14 17:32:41 +05:30
Roseyco-management
202be02ac9 test(telegram): define polling progress contract 2026-07-14 17:32:41 +05:30
Jaret Bottoms
e16743b0d5 fix(telegram): diagnose blocked-loop init hangs, unbind DoH from system DNS
The #63309 hang class — gateway stuck at 'Connecting to Telegram
(attempt 1/8)' with no retry, no timeout, for minutes — can only occur
when the event loop thread itself is blocked in a synchronous call:
_await_with_thread_deadline's timer fires off-loop, but its expiry
hand-off (call_soon_threadsafe) still needs the loop to run, and the
gateway's outer wait_for is a pure loop timer. When the loop is pinned,
every layer goes silent simultaneously and the process wedges with no
evidence of where.

Two changes:

1. Loop-blocked watchdog in _await_with_thread_deadline: a second
   daemon timer fires one grace period (5s) after the deadline; if the
   loop still hasn't processed the expiry, it logs a WARNING from the
   timer thread and faulthandler-dumps all thread stacks to stderr —
   converting the silent hang into a trace that names the exact
   blocking frame. A threading.Event set by the expiry callback (and on
   normal exit) keeps completed awaits from ever being misreported.

2. discover_fallback_ips: the system-resolver leg runs
   socket.getaddrinfo in a worker thread with no timeout, and
   asyncio.gather waited on it unboundedly — a wedged OS resolver
   stalled discovery for minutes between the two startup log lines. Its
   result only feeds a log message, so it no longer gates discovery:
   DoH legs (already client-bounded) are gathered alone and the system
   leg is awaited with a _DOH_TIMEOUT cap, best-effort.

Refs #63309

Tests: 3 watchdog regressions (blocked-loop dump fires; responsive-loop
timeout does not; completed await does not) + 2 hung-resolver
regressions (DoH results returned promptly; worst-case seed fallback
stays bounded).
2026-07-14 17:09:50 +05:30
SilentKnight87
2a0dd95ccf fix(telegram): classify and dedup post-reconnect probe failures (#63243) 2026-07-14 17:08:36 +05:30
Teknium
d48bf743f2 fix(approval): scope smart deny owner overrides to one operation
Co-authored-by: Sergei Ivanov <kavi@local.hermes>
2026-07-13 04:31:55 -07:00
Teknium
7550c594ce
feat(reasoning): add max and ultra effort levels (#62650) 2026-07-12 00:26:49 -07:00
kshitijk4poor
4aa499ff9f fix(telegram): harden flood fallback recovery
Keep empty-tail recovery scoped to the current stream segment and bound fallback flood retries. Preserve Telegram's server retry hint without blocking final delivery through a long cooldown.
2026-07-11 11:13:50 +05:30
Gille
04898631cb fix(telegram): recover final delivery after stream flood 2026-07-11 11:13:50 +05:30
liuhao1024
97fb9e1f62 fix(telegram): classify PTB heartbeat transport errors 2026-07-10 19:28:03 +05:30
luxuguang-leo
949e4cb72a fix(feishu): add extra_ua_tags=["channel"] to FeishuWSClient for group @mention delivery
Without this UA tag the Feishu server does not push group @mention events
over the WebSocket transport. The "channel" tag tells the server to use
the Channel protocol which enables group-message routing in addition to P2P
direct messages.

Root cause: FeishuWSClient was created without any UA signaling tag, so the
server defaulted to the basic DM-only push mode. Group @mention events were
silently dropped before reaching Hermes.

Fixes https://github.com/NousResearch/hermes-agent/issues/50656

Also adds a regression test verifying the UA tag is present in the
FeishuWSClient constructor call.
2026-07-09 20:31:49 -07:00
lemonwan
0f8603c571 test(gateway): regression: every adapter.connect() must accept is_reconnect
The gateway reconnect watcher forwards is_reconnect=True to every
adapter.connect() call on every retry. Adapters whose signature omits
the kwarg raise TypeError at every reconnect attempt and stay silently
disconnected — the exact bug that shipped for QQAdapter and only
surfaced after messages stopped flowing on the QQ channel for hours.

This test statically parses every adapter.py under gateway/platforms/
and plugins/platforms/ (via AST, so third-party SDKs like slack_sdk,
matrix-nio, aiohttp, telegram, etc. are NOT required in the test env)
and asserts every *Adapter class with an async connect() accepts
is_reconnect — either as a keyword-only argument or absorbed by
**kwargs.

Also fixes plugins/platforms/wecom/callback_adapter.py:WecomCallbackAdapter,
which the new test caught as a second offender. Same class of bug: bare
'async def connect(self)' signature would die on the first reconnect.

Companion to #59429 (which fixed the original QQAdapter offender).
2026-07-09 19:09:38 -07:00
kshitijk4poor
709da844b5 feat(gateway): attach MEDIA: caption to the media bubble on standalone sends
hermes send "MEDIA:/x.png This Caption" now arrives as one native captioned
bubble instead of a separate text message followed by an uncaptioned bubble.

Root cause: the standalone senders (hermes send / cron / send_message tool)
stripped the MEDIA: tag, sent the remaining text as its own message, and
called the media send with no caption -- even though hermes send's help
advertises the captioned form and the bridges/adapters already support a
caption. Signal already captioned correctly.

- tools/send_message_tool.py: new _media_caption_split() chokepoint decides
  caption-vs-separate-body (single captionable non-voice file within the
  platform's message-length cap). Wired into the Telegram, WhatsApp and
  Discord dispatch paths.
- Telegram/WhatsApp/Discord: when the single captioned file is missing, the
  caption text is delivered as a plain message so it is never silently lost.
- Telegram caption send gets a MarkdownV2->plain parse fallback.
- Tests: _media_caption_split unit tests + per-platform caption tests
  (ride, multi-file fallback, voice exclusion, over-limit fallback,
  missing-file text fallback); updated the 3 tests that asserted the old
  text-then-media split.

Closes the gap reported against #58911 (the MEDIA_CAPTION directive PR);
credit to @ferreiraesilva for surfacing the caption behavior.
2026-07-09 15:38:32 +05:30
kyssta-exe
e10c8eba00 fix(whatsapp): use windows_detach_popen_kwargs to prevent console window flash on Windows 2026-07-08 07:49:22 -07:00
Shannon Sands
4f620a0bbc Add WhatsApp dashboard pairing flow 2026-07-07 17:45:51 -07:00
yungchentang
3e7ade418d fix(discord): explain fail-closed allowlist default
Log a one-shot structured warning when Discord denies traffic because
no allowlist/policy is configured, and correct the setup wizard's
inverted warning text. The fail-closed default itself is unchanged.

Fixes #58682.
2026-07-07 17:01:08 -07:00
BROCCOLO1D
c3808cfc14 fix(discord): honor pairing grants for message auth 2026-07-07 17:00:58 -07:00
alex107ivanov
e0176cbd47 feat(discord): optionally mention approval owners on exec prompts
Opt-in discord.approval_mentions (config.yaml, bridged to
DISCORD_APPROVAL_MENTIONS) prepends <@id> mentions for numeric
allowlist entries to exec-approval prompts, with a scoped
AllowedMentions override (users only). Default off - no surprise
pings. Reapplied onto the content-mirror layout from #60245: mentions
prepend to the visible content block and its truncation budget.

Original implementation from PR #39719; commits arrived bot-authored,
re-attributed to the contributor.
2026-07-07 13:46:51 -07:00
teknium1
4c3a388cba fix(discord): widen expired-defer handling to /thread slash command
Same 10062 degrade-gracefully pattern as _run_simple_slash: create the
thread anyway, skip the ephemeral followups that need a live
interaction token. Non-expiry defer errors still raise.
2026-07-07 12:42:29 -07:00
Alix-007
b9d9b8aad6 fix(discord): handle expired slash defer interactions 2026-07-07 12:42:29 -07:00
teknium1
009b42d008 fix(discord): mirror all interactive prompt payloads into message content
Extends the send_exec_approval embed-invisibility fix to its three
sibling prompt surfaces — send_slash_confirm, send_clarify, and
send_update_prompt — via a shared _self_contained_prompt_content()
helper. All four interactive views now carry their payload in plain
content next to the buttons; the embed stays as progressive
enhancement for clients that render it. Adds gold to the conftest
discord Color mock (update prompt is the only gold user).
2026-07-07 06:25:23 -07:00
Jake Present
2acbdd1848 fix(discord): include approval command in message content 2026-07-07 06:25:23 -07:00
Teknium
afb5808d8c
feat(discord): make interactive view timeout configurable (#60230)
Discord's ExecApprovalView, SlashConfirmView, UpdatePromptView, and
ClarifyChoiceView hardcoded timeout=300, ignoring approval timeout
configuration. All four now read approvals.discord_prompt_timeout from
config.yaml (default 300s, clamped 30-900s — Discord interaction tokens
expire at ~15 min, so values beyond 900s would render dead buttons).

Surgical reapply of the timeout portion of PR #45904; the unrelated
channel-context changes bundled in that PR were intentionally excluded.

Co-authored-by: cruzanstx <cruzanstx@users.noreply.github.com>
2026-07-07 05:50:08 -07:00
teknium1
1deeaf71ab fix(discord): truncate thread titles by UTF-16 units + AUTHOR_MAP
Discord thread names share the same UTF-16 component budget as select
labels and buttons — route the sanitizers in gateway/run.py and the
adapter's rename_thread through utf16_len/_prefix_within_utf16_limit
instead of code-point slices. Adds rungmc357 to AUTHOR_MAP.
2026-07-07 05:11:59 -07:00
Georgio Constantinou
0d9ed9214d Add semantic titles for Discord auto-threads 2026-07-07 05:11:59 -07:00
teknium1
ba865e4038 refactor(setup): route dependency installs through the canonical uv→pip→ensurepip ladder
Replace the hand-rolled ensurepip bootstrap (and five other one-off
pip-install code paths) with hermes_cli.tools_config._pip_install, which
prefers the bundled uv (fast, needs no pip in the venv), falls back to
python -m pip, and bootstraps pip via ensurepip only when missing.

Sites unified:
- hermes_cli/setup.py: _install_neutts_deps, _install_kittentts_deps,
  modal SDK install, daytona SDK install
- hermes_cli/memory_setup.py: memory-plugin pip deps (previously dead-ended
  when uv AND pip binaries were both absent)
- hermes_cli/dingtalk_auth.py: qrcode auto-install (previously invoked
  'python -m uv' which is not how uv ships)
- agent/lsp/install.py: --target LSP server installs
- plugins/google_meet/cli.py, plugins/platforms/matrix/adapter.py,
  plugins/platforms/google_chat/oauth.py, plugins/memory/honcho/cli.py

Tests updated to assert the ladder behavior (uv-first, pip fallback,
ensurepip bootstrap) instead of the removed bespoke branches.
2026-07-07 04:09:35 -07:00
kshitijk4poor
aaeba213d9 fix(telegram): bound start_polling() at bootstrap and conflict-retry sites too; strengthen tests
Follow-up on the salvaged fix, which bounded start_polling() only in
_handle_polling_network_error. The same wedge (#59614) exists at the two
sibling call sites:

1. _start_polling_resilient (bootstrap): an exhausted pool hangs connect()
   forever. The TimeoutError from wait_for is a builtins TimeoutError
   (OSError subclass), so the existing except classifies it via
   _looks_like_network_error and schedules background recovery.
2. _handle_polling_conflict (conflict-retry ladder): identical hang wedges
   conflict attempt N forever; timeout now converts to RuntimeError and the
   existing except schedules the next attempt.

Tests replaced with a stronger suite: hung-network-ladder repro (RED without
the fix), bootstrap hang schedules recovery, success-path sanity, and a
bug-class contract test asserting EVERY updater.start_polling( call site is
wrapped in wait_for so a new unbounded site can't reintroduce the wedge.
Verified RED (3 failures) with the wrappers removed, GREEN with them.
2026-07-07 15:50:41 +05:30
liuhao1024
4aaaa206aa fix(telegram): add timeout to start_polling() in network error handler
When the connection pool is in a degraded state after
_drain_polling_connections(), start_polling() can hang indefinitely
when both primary and fallback Telegram endpoints are unreachable. The
httpx client may hold a stale socket that neither connects nor times out
within PTB's internal flow, causing the reconnect ladder to stall at
attempt 1/10 forever.

Wrap start_polling() in asyncio.wait_for() with a 30-second timeout so a
hung call raises asyncio.TimeoutError and feeds back into the existing
retry ladder. This unblocks:
- The 10-retry ladder advances to attempt 2, 3, ...
- The heartbeat loop sees _polling_error_task.done() and can trigger recovery
- The reconnect watcher gets the adapter in _failed_platforms

Fixes #59614
2026-07-07 15:50:41 +05:30
teknium1
f341cadb71 refactor(discord): detect streaming bodies structurally, not by mock-module sniffing
Replace the unittest.mock module-name check with an
inspect.iscoroutinefunction probe on content.read, and collapse the
duplicate read/iter_chunked reader paths into one. Non-streaming
objects (test doubles, proxy wrappers) fall back to the response's
native json()/text() as before.
2026-07-07 02:40:15 -07:00
ooiuuii
e0bca1cbe2 fix(discord): bound standalone response reads 2026-07-07 02:40:15 -07:00
ooiuuii
87be36c240 fix(discord): bound component labels by UTF-16 units 2026-07-07 02:40:12 -07:00
Teknium
a796e0b796
fix: cool down transient Telegram typing failures (#46355)
* fix: cool down transient Telegram typing failures

Port from openclaw/openclaw#93020: add per-chat cooldown for transient sendChatAction failures so keep-typing refreshes do not hammer Telegram during network blips or rate limits.

* fix: support bare Telegram adapters in typing cooldown

* test: update typing backoff imports for relocated Telegram adapter

The Telegram adapter moved from gateway/platforms/telegram.py to
plugins/platforms/telegram/adapter.py since this branch was created;
point the test imports and monkeypatch targets at the new module.
2026-07-07 02:39:31 -07:00
herbalizer404
4a80e27bba fix telegram explicit private thread routing 2026-07-06 03:18:33 -07:00
teknium1
2bcb893d87 fix(feishu): set client_max_size on the webhook Application
Follow-up to the salvaged #54938: the bounded reader gives a proper 413 +
anomaly telemetry for oversized chunked bodies; client_max_size makes
aiohttp enforce the same 1 MiB cap on every other read path
(#58536/#58902/#59180 pattern). Test fixture's fake Application now
accepts kwargs.
2026-07-05 17:38:36 -07:00
luyifan
a26680eb2d Enforce Feishu webhook body limit while reading 2026-07-05 17:38:36 -07:00
teknium1
3dd5ce236a fix(sms): set client_max_size on the Twilio webhook Application
Follow-up to the salvaged #54620: the post-read length check bounds
processing but a chunked body is still buffered by aiohttp first.
client_max_size enforces the same 64 KiB cap mid-read on every path
(#58536/#58902/#59180 pattern).
2026-07-05 17:38:36 -07:00
Alix-007
940b69b1a8 fix(sms): bound Twilio webhook body reads to prevent OOM
_handle_webhook() called request.read() with no size guard. Since the
endpoint is publicly reachable, an attacker can send an arbitrarily large
POST body to exhaust gateway memory.

Add _TWILIO_WEBHOOK_MAX_BODY_BYTES (64 KiB — well above any real Twilio
payload) and gate on both Content-Length and actual read size, returning
HTTP 413 with an empty TwiML Response on oversized requests. Mirrors the
guard already present in the Raft adapter.
2026-07-05 17:38:36 -07:00
teknium1
127d2ee87a fix(photon): bound the sidecar dep self-heal npm run with a timeout
Follow-up to the salvaged #57943: a wedged npm (dead registry, network
blackhole) ran unbounded inside asyncio.to_thread, holding the photon
connect path hostage. Cap npm ci / npm install at 600s; on timeout, log
and leave the stale deps in place so the readiness check reports the
real error and the next reconnect tick retries.
2026-07-05 17:38:32 -07:00
Jash Lee
3cd93f6aa8 fix(photon): auto-reinstall stale sidecar deps before start
A `hermes update` that bumps the spectrum-ts pin rewrites the Photon
sidecar's package-lock.json but never reinstalls node_modules. The sidecar
then spawns against the old install and the v8 postinstall patch throws
"@spectrum-ts/imessage dist not found", so the gateway retries the photon
platform every 300s forever without ever repairing the deps. Observed in
the wild: a June pin bump to spectrum-ts 8.0.0 left node_modules at 3.1.0,
and inbound/outbound iMessage stayed dead for days with the reconnect loop
faithfully restarting into the identical broken state.

_start_sidecar only checked that node_modules exists, not that it matches
the lockfile, so restart never became repair. Detect the skew with the same
signal npm ci uses: the top-level package-lock.json being newer than npm's
node_modules/.package-lock.json install marker. When stale, reinstall
(npm ci, falling back to npm install) before spawning. The reinstall runs
via asyncio.to_thread so a cold install can't block the event loop and stall
every other platform's traffic; worst case it heals on the next reconnect
tick instead. First-run "deps not installed" behavior is unchanged, and a
missing/unreadable marker fails safe to "not stale" so start is never
blocked.

Reuses the existing npm ci -> npm install fallback from
`hermes photon install-sidecar`. Adds unit tests for the staleness signal
(stale / fresh / missing-marker).
2026-07-05 17:38:32 -07:00
Teknium
8986981df4
security(gateway): set explicit client_max_size on 3 uncapped aiohttp servers (#59180)
Sibling sweep from the #58902 raft review found aiohttp servers still
running on the implicit 1 MiB default with no explicit body cap:

- bluebubbles webhook (127.0.0.1): 1 MiB explicit cap — events are small
  JSON/form payloads; attachments arrive via the REST API
- teams Bot Framework listener (0.0.0.0 bind — most exposed): 1 MiB cap;
  activities are JSON well under that
- hermes proxy server: 10 MB cap mirroring api_server's MAX_REQUEST_BYTES
  (chat-completion payloads can be large, but must stay bounded)

client_max_size bounds every read path including chunked transfer-encoding
requests that carry no Content-Length (#58536/#58902 pattern).

Deliberately excluded: feishu, whatsapp_cloud, sms, line, wecom, msgraph —
open contributor PRs (#54938, #54944, #54620, #54931, #54934, #25296)
already cover those; reviewing them separately preserves their credit.

3 regression tests pin the wiring.
2026-07-05 14:48:28 -07:00
Lohinth
1197d2bc96 fix(mattermost): accept leading-space slash commands 2026-07-05 14:41:57 -07:00