Commit graph

334 commits

Author SHA1 Message Date
Teknium
a04fcbf779 fix(telegram): widen transport-error redaction to all remaining raw exception sites
Extends @AlexFucuson9's 3-site fix (#58594) across the full adapter:
every logger call and SendResult.error that interpolates a raw PTB
exception now routes through _redact_telegram_error_text(). Covers
polling conflict/retry/network ladders, overflow-split edits, draft
sends, prompt/approval/clarify/picker sends, media send fallbacks,
media cache failures, reactions, and chat-info lookups (48 additional
sites). Telegram Bot API exceptions embed the token in the request URL
(/bot<TOKEN>/<method>), so any raw str(exc) is a leak surface.

Adds regression tests for SendResult.error redaction (update prompt,
clarify) and delete_message debug-log redaction.
2026-07-16 04:25:54 -07:00
AlexFucuson9
6e96b745d8 fix(telegram): redact bot tokens from transport error logs
Telegram Bot API URLs carry credentials in the path as
/bot<TOKEN>/<method>. Three error-handling paths logged raw exception
text that could include these URLs:

- sendRichMessage fallback (line 1603)
- editMessageText fallback (line 1709)
- polling reconnect warning (line 1902)

Replace raw / with  which
uses the existing redact_sensitive_text(force=True) pipeline. This
matches the pattern already used by transient send failures, retry
errors, and legacy edit paths.

Fixes #58376
2026-07-16 04:25:54 -07:00
aeyeopsdev
fce298f700 fix(google-chat): don't flip clarify to text-capture at send time
mark_awaiting_text is the 'Other (type answer)' mode-flip, not a send-time
setup call — invoking it in send_clarify forces the user's next message to
be captured as the clarify response, racing the button-click path and
bypassing the buttons entirely. Telegram calls it only in the 'other'
callback branch; do the same here.
2026-07-16 04:24:47 -07:00
aeyeopsdev
09505a393e feat(google-chat): render clarify prompts as cards 2026-07-16 04:24:47 -07:00
Teknium
f8bf40b18b fix(photon): hide the npm dep self-heal console flashes on Windows too
Widen @lEWFkRAD's sidecar-headless fix (PR #54565) to the sibling spawn
sites: the npm ci / npm install self-heal runs in _reinstall_sidecar_deps
also popped a brief console window per run on Windows. Same
windows_hide_flags() helper (CREATE_NO_WINDOW only, so capture_output
stays usable).
2026-07-16 01:03:43 -07:00
Jeff Watts
d8f7b608c9 fix(photon): launch the iMessage sidecar headless on Windows
plugins/platforms/photon/adapter.py launches the Node sidecar (and the
spectrum-ts mixed-attachment patch run) via subprocess without creationflags.
On Windows this opens a visible console window on every sidecar (re)start --
and because a failed sidecar is retried on a timer, it flashes repeatedly.

Wire windows_hide_flags() (hermes_cli/_subprocess_compat) into both spawns,
the same helper the discord and whatsapp adapters already use for their
sidecar spawns -- photon was the one platform adapter this pattern missed.
CREATE_NO_WINDOW only (no DETACHED_PROCESS) so the persistent sidecar's
stdin/stdout pipes stay usable for the supervisor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 01:03:43 -07:00
Teknium
647520f83e fix(gateway): gate profile routing on multiplex_profiles + widen batch-key routing to all adapters
Follow-ups on the salvaged #20096 profile-routing feature:

- _profile_name_for_source now returns None unless gateway.multiplex_profiles
  is on. Routing stamps source.profile, which namespaces session/batch keys,
  but the profile-scoped agent run only activates under multiplexing — without
  the gate, configured routes with multiplexing off split batch/session keys
  into agent:<profile> while the agent still ran from agent:main.
- Widen the profile-aware _text_batch_key fix from Discord to every adapter
  that builds batch keys via build_session_key (telegram, whatsapp, matrix,
  feishu, wecom, weixin) — routing is platform-generic, so the batch-key
  namespace fix must be too.
- Downgrade the no-route-matched log from INFO to DEBUG (fired on every
  unrouted inbound message).
- GatewayConfig.to_dict(): serialize profile_routes as plain dicts
  (ProfileRoute dataclasses are not JSON-safe).
- Docs: correct the 'independent of multiplexing' claim in
  docs/profile-routing.md (routing requires multiplexing), fix the
  platform-only specificity row (0, not 1), and document profile_routes in
  website/docs/user-guide/multi-profile-gateways.md.
- Tests: pin the multiplex gate (routes ignored when off, active when on,
  build_source end-to-end stays in agent:main when off).
2026-07-15 09:50:05 -07:00
Burgunthy
e8b7ce8c19 fix(session): persist profile_name and route batch key by profile
Two follow-ups observed after deploying profile routing:

1. sessions.profile_name was NULL even when the agent ran inside the
   routed profile scope. _insert_session_row never wrote it,
   get_or_create_session / reset_session never passed it through, and
   the agent-side _ensure_db_session fallback had no way to read it.
   - Declare profile_name TEXT in SCHEMA_SQL so _reconcile_columns
     auto-adds it on existing DBs.
   - _insert_session_row takes profile_name and writes it.
   - SessionStore passes source.profile (or old_entry.origin.profile
     on reset) into db_create_kwargs.
   - _ensure_db_session reads the active profile via
     get_active_profile_name() inside _profile_runtime_scope.

2. DiscordAdapter._text_batch_key called build_session_key without
   profile=, so the batch key always landed in agent:main even when
   the routed profile differed — diverging from the agent session
   key namespace (agent:crypto-trader, agent:ai-expert, ...).
   Pass event.source.profile through so both namespaces agree.

Live verification (jth-server-2, 2026-06-28): a test message in a
routed #coin thread produced agent:crypto-trader:discord🧵...
in the batch log and profile_name=crypto-trader in the sessions
row. Default-routed chat still produced agent:main / NULL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-15 09:50:05 -07:00
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