Commit graph

1069 commits

Author SHA1 Message Date
Teknium
a1f51feb72
fix(telegram): avoid rich final duplicate previews (#46206) 2026-06-14 11:13:38 -07:00
Teknium
efbe1635dd
fix(gateway): include replied-to media attachments (#46107) 2026-06-14 04:51:50 -07:00
Teknium
9459057d7f
fix(telegram): guard rich details math crash (#46102) 2026-06-14 04:22:22 -07:00
Teknium
cf7d5932f8 fix(email): make IPv4 SMTP fallback use supported sockets 2026-06-14 04:16:26 -07:00
liuhao1024
04d4471d79 fix(email): use SMTP_SSL for port 465 and fall back to IPv4 on timeout
Port 465 expects implicit TLS (SMTP_SSL) from the first byte. The email
adapter always used SMTP() + starttls(), which is correct for port 587
but hangs/fails on port 465 providers (e.g., Swiss ISPs).

Additionally, when the SMTP host has AAAA DNS records but IPv6 is
unreachable, socket.create_connection() tries IPv6 first and hangs
until timeout. Add an IPv4 fallback via AF_INET socket.

Extract _connect_smtp() helper to consolidate the 4 duplicate SMTP
connection sites into a single method with correct protocol selection
and IPv6 fallback logic.
2026-06-14 04:16:26 -07:00
Teknium
5105c3651a
perf(api-server): normalize chat content linearly (#46079) 2026-06-14 03:25:49 -07:00
Teknium
afc8615509
perf(webhook): prune request caches incrementally (#46065) 2026-06-14 02:40:54 -07:00
Justin Sunseri
12682d96b9 feat(telegram): restore rich messages opt-out
Salvages PR #45840's client-compatibility opt-out while keeping rich messages enabled by default via telegram.extra.rich_messages: true.
2026-06-13 21:45:49 -07:00
ITheEqualizer
57c2a55be4 fix(telegram): harden rich message fallback handling
Carry forward focused follow-ups from PR #45741: treat PTB's raw Bot API 10.1 response shapes safely, recognize real missing-endpoint errors, preserve link preview settings on rich sends, and lock the rich limit to Telegram's character-based cap.
2026-06-13 14:34:53 -07:00
ITheEqualizer
7c0605bf22 fix(telegram): preserve rich formatting on stream final 2026-06-13 13:44:45 -07:00
Que0x
fc46354580 fix(security): fail closed when an own-policy gateway adapter has no allowlist
Own-policy adapters (WhatsApp, WeCom, Weixin, QQBot, Yuanbao) default dm_policy/group_policy to "open", which forwards every sender. The gateway's adapter-trust shortcut in _is_user_authorized blanket-trusted those platforms when no env allowlist was set, so an operator who enabled one with only credentials authorized the entire external network -- the fail-open SECURITY.md section 2.6 forbids ("an allowlist is required for every enabled network-exposed adapter").

Trust the adapter only when its effective policy for the chat type is an actual "allowlist" restriction (the case #34515 was protecting). "open"/"pairing"/anything else falls through to default-deny, where {PLATFORM}_ALLOW_ALL_USERS / GATEWAY_ALLOW_ALL_USERS and the pairing flow remain the explicit opt-ins.
2026-06-13 07:18:54 -07:00
Clayton Chew
f82cb48120 fix(platform): add .xls, .doc, .ppt to SUPPORTED_DOCUMENT_TYPES
Old Office formats (.xls, .doc, .ppt) were missing from the
SUPPORTED_DOCUMENT_TYPES dict in gateway/platforms/base.py while their
newer counterparts (.xlsx, .docx, .pptx) were included.

Sending an .xls file via Telegram triggers 'Unsupported document type'
and the file is silently dropped instead of being cached and forwarded
to the agent.

Add the three legacy MIME types so these files are handled the same way
as their modern equivalents.
2026-06-13 07:18:37 -07:00
Sarvesh
45f9099e51 fix(matrix): preserve markdown table structure 2026-06-13 06:57:08 -07:00
Teknium
a59d5e37e8
feat(telegram): make rich messages always on (#45584)
Remove the rich_messages config toggle entirely so Telegram replies always try the Bot API 10.1 rich-message path first, with the existing MarkdownV2 fallback/latch behavior for unsupported endpoints and per-message failures.

Restore the Telegram platform hint to encourage rich Markdown tables/task lists/math now that the rich path is the default, and remove the config/docs surface for the old toggle.
2026-06-13 05:45:11 -07:00
Teknium
2a5dc0ef3d
fix(slack): make video attachments available to agents (#45512) 2026-06-13 03:33:27 -07:00
Flownium
331cb38e21 fix: stop Discord typing after replies 2026-06-12 12:02:41 -07:00
Teknium
652dd9c9f2 fix: rich messages follow-ups — reply_parameters, send latch, opt-in default
- Use reply_parameters per the sendRichMessage spec instead of the
  undocumented reply_to_message_id scalar (silently ignored -> reply
  anchor quietly dropped).
- Latch rich sends off after an endpoint-capability failure (old PTB /
  server without sendRichMessage) so every later reply doesn't pay a
  doomed extra roundtrip; per-message BadRequests do NOT latch.
- Default rich_messages to OFF (opt-in) while the day-old Bot API 10.1
  endpoint is validated live; revert the prompt-hint table guidance
  until the default flips on.
- Tests: reply_parameters shape, send-latch behavior, BadRequest
  non-latch; rich tests opt in explicitly via extra.
2026-06-12 11:47:54 -07:00
ITheEqualizer
05b9c84ca4 Add Telegram Bot API 10.1 rich message support
Introduce opportunistic support for Telegram Bot API 10.1 rich messages by sending raw agent Markdown via sendRichMessage and streaming previews via sendRichMessageDraft. Implements a rich-path fast‑path in gateway/platforms/telegram.py (RICH_MESSAGE_MAX_BYTES=32768, feature gate platforms.telegram.extra.rich_messages, bot capability checks, routing/thread handling, and conservative fallback rules: permanent/capability errors fall back to the legacy MarkdownV2 path, transient/network errors are surfaced without legacy-resend). Also add a latch for draft capability failures (_rich_draft_disabled) and preserve legacy chunking and draft behavior when needed. Update agent prompt hints (telegram encourages rich Markdown/tables), add CLI config example option, update English and Chinese docs to describe rich messages and fallbacks, and add/adjust tests for rich send and draft behavior.
2026-06-12 11:47:54 -07:00
loongfay
e20e0bd744
feat(Yuanbao): support wechat forward msg (#43508)
* feat(yuanbao): support wechat forward msg

* feat(yuanbao): support wechat forward msg

---------

Co-authored-by: loongfay <izhaolongfei@gmail.com>
2026-06-12 02:06:47 -07:00
Teknium
f03f161b39 fix(gateway): classify email document attachments as DOCUMENT
Email cached document attachments and placed them in media_urls, but
msg_type only flipped on image attachments — documents stayed TEXT and
run.py's document-context injection (gated on MessageType.DOCUMENT)
silently dropped them. Same bug class as Signal #12845. DOCUMENT wins
over PHOTO for mixed attachments since image handling keys off per-path
mime types while document injection gates strictly on message_type.
2026-06-12 01:07:50 -07:00
Teknium
1e29ab38c7 fix(gateway): classify Signal video attachments + catch-all DOCUMENT fallback
Widen the salvaged #12851 fix to match the established classification
pattern (WhatsApp/Slack/BlueBubbles/Mattermost): video/* -> VIDEO, and
any remaining MIME type falls through to DOCUMENT instead of TEXT, so
exotic types still trigger run.py's document-context injection.
2026-06-12 01:07:50 -07:00
Kyle Dunn
8207ae888d fix(gateway): add Signal message type classification for documents 2026-06-12 01:07:50 -07:00
Teknium
db7714d5f1
Merge pull request #44331 from NousResearch/hermes/hermes-6b48295e
feat(whatsapp): WhatsApp Business Cloud API adapter (salvage #43921)
2026-06-11 22:48:06 -07:00
Veritas-7
82d570165e fix(slack): ack reaction lifecycle events
Register no-op Slack event handlers for inbound reaction_added and reaction_removed events so Slack Bolt does not log unhandled-request warnings for events Hermes does not consume.
2026-06-12 10:54:07 +05:30
Brad Smith
08e8bedae8 fix(gateway): keep plugin action wrapper signature to (ack, body, action)
The previous implementation captured loop vars via default arguments::

    async def _wrapped(ack, body, action, _cb=_cb, _plugin_name=_plugin_name):

slack_bolt's ``kwargs_injection`` introspects each listener's signature
via ``inspect.signature`` and passes ``None`` for any parameter name it
doesn't recognise (see ``slack_bolt/kwargs_injection/async_utils.py``
``build_async_required_kwargs``). That clobbered ``_cb`` to ``None`` at
dispatch time, so the wrapped plugin handler became ``NoneType`` —
``await _cb(...)`` then raised ``'NoneType' object is not callable`` and
no plugin action handler ever fired.

Replace the default-arg trick with a small closure factory so the
wrapper's public signature is exactly ``(ack, body, action)``. Add a
regression test that introspects the wrapped function's signature.

Found via real Slack click on a Block Kit button registered through
``ctx.register_slack_action_handler`` — gateway log showed
``[Slack] Plugin 'None' action handler raised: 'NoneType' object is
not callable`` despite the registration log line confirming the
handler was wired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 10:36:14 +05:30
Brad Smith
62e937bf2b feat(plugins): expose register_slack_action_handler API
Plugins that post Block Kit messages with interactive elements (buttons,
overflow menus, datepickers, etc.) had no documented way to receive the
resulting click events. The plugin API exposed register_tool, register_hook,
register_command, register_platform, and register_context_engine, but
nothing for slack_bolt action handlers. The only workaround was to
monkey-patch SlackAdapter.connect from inside register(), which is
fragile and breaks on every Hermes update.

This change adds:

* PluginContext.register_slack_action_handler(action_id, callback) —
  validates inputs and queues the handler on the PluginManager.
  action_id accepts whatever slack_bolt.App.action() accepts (literal
  string, compiled re.Pattern, or constraint dict).
* PluginManager.get_slack_action_handlers() — accessor used by the
  Slack adapter at connect time.
* SlackAdapter.connect — after wiring its built-in approval and
  slash-confirm buttons, iterates the plugin-registered handlers
  and registers each via self._app.action(matcher)(callback). Each
  callback is wrapped defensively so a misbehaving plugin cannot
  crash slack_bolt's dispatch loop, with a best-effort ack on
  exception so Slack stops retrying the click.
* Defensive fallback when the plugin layer is unhealthy: a
  RuntimeError from get_plugin_manager() is logged and swallowed
  rather than blocking the gateway from starting.
* Test coverage in tests/gateway/test_slack_plugin_action_handlers.py
  for input validation, multi-plugin registration, the connect-time
  wiring, defensive exception handling, and the plugin-loader-
  failure fallback path.
* Documentation in website/docs/guides/build-a-hermes-plugin.md
  describing the new API alongside the existing register_command /
  dispatch_tool documentation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 10:36:14 +05:30
teknium1
52c7976f40
fix(whatsapp-cloud): review follow-ups for #43921
- nous_subscription: gate the STT managed-default flip on openai-audio
  entitlement and skip when a local backend (faster-whisper or custom
  command) works; new _local_stt_backend_available() helper + tests
- whatsapp_cloud: WHATSAPP_CLOUD_{DM_POLICY,ALLOW_FROM,GROUP_POLICY,
  GROUP_ALLOW_FROM} env overrides so both adapters can run in parallel;
  normalize allowlist entries (JID/punctuation) to bare wa_id
- whatsapp_cloud: wrap per-message event build in try/except (dedup-marked
  wamids would be silently dropped on Meta's batch retry otherwise)
- whatsapp_cloud: validate media_id before URL/filename interpolation,
  delete transient .ogg after voice upload, FIFO-cap interactive-button
  state dicts and per-chat wamid cache
- whatsapp_common: '# **Title**' headers no longer double-wrap asterisks
- setup wizard: read access token / app secret via getpass on TTYs
- docs: new WHATSAPP_CLOUD_* gating env vars
2026-06-11 07:51:01 -07:00
Teknium
2ecb4e62bb
Merge remote-tracking branch 'origin/main' into hermes/hermes-6b48295e 2026-06-11 07:38:25 -07:00
Austin Pickett
d0e017bac8
fix(gateway): gate oversized Telegram voice/audio before download (#44245)
* fix(gateway): gate oversized Telegram voice/audio before download

Adds a pre-download size check to the Telegram voice and audio inbound
paths. Files that exceed _max_doc_bytes (default 20 MB) are rejected
before get_file() is called, preventing silent OOM-style stalls on large
uploads. A human-readable note is appended to the event text so the
model can explain the limit to the user.

Also extends 403 entitlement detection in recover_with_credential_pool
to cover two additional cases: 'oauth authentication is currently not
allowed for this organization' and Anthropic anthropic_messages-mode 403s,
both of which should be treated as entitlement failures rather than
transient errors.

Tests: 7 new cases in test_telegram_voice_v0_regressions.py covering
the size gate (accept, reject, note text) and the STT-failure notice path.

Salvaged from #40487 (cryptopafi) — cherry-picked the Telegram voice
policy and 403 entitlement fixes; LiveKit/Discord/uv.lock workstreams
left for separate PRs.

* test(gateway): drop orphaned voice tests not backed by this PR

The cherry-picked test file from #40487 included 3 tests for STT-failure
notice and voice-mode (_handle_voice_command 'on' -> voice_only) behavior
that this PR intentionally does NOT salvage (those belong to the LiveKit/
voice-policy workstreams left in #40487). They fail on both this branch
and clean main because the feature code isn't present.

Keep only the 2 tests backed by code actually in this PR:
- test_telegram_audio_size_gate_rejects_oversized_media_before_download
  (covers the _telegram_media_size_allowed guard this PR adds)
- test_voice_tts_is_explicit_audio_reply_opt_in (matches current main)

Removed now-unused imports (MessageEvent, MessageType, AsyncMock).
2026-06-11 10:01:51 -04:00
Chris
4717989c10
fix(matrix): isolate room context and restore reliable inbound dispatch (#18505)
* fix(matrix): isolate room context and inbound dispatch

* test(matrix): cover room isolation and dispatch regressions

* docs(matrix): document room isolation and session scope

* fix(matrix): stabilize CI requirement checks

* test(matrix): isolate mautrix stubs in requirements tests

* fix(matrix): port room-scoped status and resume to slash commands mixin

Move Matrix /status scope output and /resume same-room guards from the
pre-refactor gateway/run.py into gateway/slash_commands.py so PR #18505
foundation behavior survives the upstream god-file decomposition.

Uses i18n keys for Matrix resume/status messages. Preserves upstream
session.py fixes (role_authorized, DM user_id isolation).

* docs(matrix): explain inbound dispatch via handle_sync loop

Document why Hermes uses an explicit sync loop with handle_sync() rather than
client.start(), aligning with upstream #7914 diagnostics while preserving
Hermes background maintenance tasks.

* fix(i18n): add Matrix resume/status keys to all locale catalogs

The Matrix /resume and /status slash-command keys added in the foundation
PR must exist in every supported locale file. tests/agent/test_i18n.py
asserts key and placeholder parity across catalogs.

Non-English locales use English strings as interim placeholders until
community translators can localize them.

* fix(matrix): restore gateway authz for allowed_users; honor config require_mention

Revert the early MATRIX_ALLOWED_USERS gate in _on_room_message so inbound
sender authorization stays in gateway authz like main. Parse require_mention
from config.extra (platforms.matrix / top-level matrix yaml) with env fallback,
matching thread_require_mention and fixing Forge when require_mention is set
only in profile config.yaml.

* fix(matrix): harden status scope and allowlisted DMs

* fix(matrix): use session store lookup for resume scope
2026-06-11 07:41:43 -04:00
Teknium
3edd09a46f
fix(whatsapp): restart stale bridge processes instead of silently reusing them (#44205)
A long-lived Baileys bridge survives gateway restarts AND hermes update:
connect() adopted any bridge already listening with status connected, and
disconnect() only kills bridges the adapter spawned itself. Users who
updated to get inbound media support kept talking to a bridge process
serving months-old bridge.js — images and voice notes still arrived as
placeholders with no cached file path (refs #19105 follow-up reports).

Three fixes in the same stale-bridge class:

- Staleness handshake: bridge.js reports a sha256 self-hash in /health
  (scriptHash); connect() compares it against bridge.js on disk and
  restarts the bridge on mismatch. Pre-handshake bridges report no hash
  and are treated as stale, so every existing stale bridge gets recycled
  exactly once on the next gateway start.
- npm dep refresh: deps reinstall when package.json changes (stamp file
  in node_modules), not only when node_modules is missing — a Baileys
  pin bump now actually lands.
- Cache-dir passthrough: the gateway passes profile-aware
  HERMES_{IMAGE,AUDIO,DOCUMENT}_CACHE_DIR to the bridge instead of the
  bridge hardcoding ~/.hermes/image_cache etc., fixing media paths under
  HERMES_HOME overrides, profiles, and the new cache/ layout.
2026-06-11 03:47:29 -07:00
emozilla
bfcc9f92b4 Merge commit '6110aed9b' into feat/whatsapp-cloud-api 2026-06-10 21:39:22 -04:00
Teknium
3b4c715e1c fix(telegram): stripped-text fallbacks, re-finalize skip, and tail-only delete guard
Follow-ups on top of the two salvaged GodsBoy commits, all live-validated
against the real Telegram Bot API:

- _edit_overflow_split finalize fallbacks degrade to _strip_mdv2() clean
  text instead of putting raw **markdown** markers on screen (salvaged
  from PR #43463 minus its format-first sizing — live probes show
  Telegram's 4096 limit counts PARSED text, so MarkdownV2 escape
  inflation cannot cause MESSAGE_TOO_LONG and sizing against formatted
  wire length only causes premature splits and fragment messages).
- Skip the redundant requires-finalize edit after a got_done edit that
  split-and-delivered (salvaged from PR #43463): re-finalizing re-splits
  the full text into the adopted continuation and duplicates chunks.
- _send_fallback_final only deletes the stale partial message when the
  fallback re-sent the COMPLETE final text. When the prefix dedup sent
  only the missing tail, the partial IS the head of the answer; deleting
  it left users with only the second half of long responses (live-
  reproduced: flood-control during a long stream -> head deleted,
  ratio 0.54 of content visible). This is the third bug behind the
  'Telegram cut messages' reports and was present on main and both PRs.
2026-06-10 15:09:35 -07:00
GodsBoy
590b3c0d7e fix(gateway): recover partial Telegram overflow streams 2026-06-10 15:09:35 -07:00
Teknium
cd9a9cd8e5
fix(gateway): Slack approval UX in threads — block-size overflow + typed-prefix instruction text (#43444)
Two fixes for the reported Slack thread approval UX:

1. Slack Block Kit approval/confirm sends silently overflowed the
   3000-char section-block cap (flat 2900-char truncation + header +
   reason), so long execute_code approvals failed with invalid_blocks
   and fell back to the plain-text prompt with no buttons. Budget the
   command preview against the rendered fixed parts so blocks never
   exceed the cap (send_exec_approval + send_slash_confirm).

2. The text fallbacks told users to reply /approve — which Slack blocks
   inside threads and Matrix clients reserve client-side. Add a
   typed_command_prefix capability flag on BasePlatformAdapter
   (default "/"; Slack and Matrix set "!" to match their existing
   bang-prefix rewrite) and use it in the shared fallback prompt
   builders (exec approval, update prompt, destructive slash confirm,
   expensive-model confirm) plus Matrix's reaction-prompt text.
   The slash-confirm text-intercept now also accepts bang-prefixed
   replies (!always, !cancel) since those keywords aren't registered
   commands and the adapters' rewrite doesn't touch them.
2026-06-10 02:30:01 -07:00
konsisumer
6a30cfca82
fix(gateway): stop typing before post-delivery callbacks (#37556) 2026-06-10 00:46:00 -07:00
Teknium
243cada157 fix(model): cover typed gateway /model path + async-safe pricing lookups
Follow-ups on top of #26016's expensive-model guard:

- gateway/slash_commands.py: typed '/model <name>' now routes through the
  expensive-model confirmation gate (slash-confirm buttons / text fallback)
  instead of bypassing the guard the pickers enforce. Cancel leaves the
  session override and --global config untouched.
- telegram/discord/web_server: run expensive_model_warning() via
  asyncio.to_thread — it can hit models.dev or a /models endpoint on a
  cache miss, which would otherwise block the event loop.
- telegram: picker callback no longer toasts 'Model switched!' when the
  switch callback raised (both mm: and mc: paths).
- tests: new tests/gateway/test_model_command_expensive_confirm.py pins
  the typed-path gate (prompt, confirm-once, cancel, cheap-model no-op).
2026-06-10 00:24:06 -07:00
Robin Fernandes
af978ecb17 fix(model): require confirmation for expensive model selections
Rebased onto current main and re-ported across the restructured
surfaces: model flows now thread confirm_provider/base_url/api_key
through hermes_cli/model_setup_flows.py, the Discord picker lives in
plugins/platforms/discord/adapter.py, and the web dashboard picker
applies chat-mode switches via config.set so the expensive-model
confirmation can ride the response.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 00:24:06 -07:00
Joel Chan
e5580f43c2 fix(discord): propagate role_authorized flag so DISCORD_ALLOWED_ROLES works end-to-end
DISCORD_ALLOWED_ROLES was checked by the Discord adapter (_is_allowed_user)
but gateway._is_user_authorized only read DISCORD_ALLOWED_USERS, so
role-authorized users were rejected with "Unauthorized user" at the
gateway layer despite passing the adapter gate.

- Add role_authorized: bool = False to SessionSource
- Add role_authorized param to build_source (base.py)
- Compute _role_authorized in on_message when user passes via role not user ID
- Thread _role_authorized through _handle_message -> build_source
- Check source.role_authorized early in _is_user_authorized (run.py)

Fixes #33952
2026-06-10 00:18:11 -07:00
loongzhao
ffcd9d7ac7 refactor(yuanbao): consolidate media resolution into dedicated pipeline middlewares 2026-06-09 03:17:00 -07:00
Teknium
3705625b74
feat(gateway): render terminal commands as bare fenced code blocks in chat (#42576)
Terminal tool progress on markdown-capable gateways (Telegram, Slack,
Discord, WhatsApp, Matrix, Weixin, Feishu) renders the full command in a
fenced code block again, in all/new AND verbose modes — gated on the
adapter's supports_code_blocks capability. Plain-text platforms keep the
short truncated preview.

No language tag is emitted: Slack mrkdwn renders a '```bash' fence with
'bash' as a literal first code line, so a bare '```' fence is used, which
renders correctly on every platform that supports blocks.

This restores the #41215 feature (removed in #41950 due to the command
showing in group chats) as the default. For a personal assistant the
command display is desired; the group-chat concern is a preference, not a
vulnerability.
2026-06-08 21:19:05 -07:00
helix4u
b23184cad4 fix(api-server): bind request session context for tools 2026-06-08 20:52:08 -07:00
ruangraung
f4531feee8 fix(telegram): improve MarkdownV2 edit fallback and fix _strip_mdv2 bold handling
When edit_message(finalize=True) fails with a MarkdownV2 parse error,
the silent fallback previously sent raw content with escape sequences.
Now it logs the error and strips markdown formatting via _strip_mdv2()
for clean plain-text fallback.

Also fixes _strip_mdv2 to handle standard markdown bold (\*\*text\*\*)
before MarkdownV2 bold (\*text\*), preventing half-stripped asterisks.

Refs: #41955, #41732
2026-06-08 15:53:16 -07:00
GodsBoy
421226e404 fix(gateway): stop terminal progress from posting the full command to messaging chats
#41215 rendered a terminal tool call as a native ```bash fenced block on
markdown platforms (Telegram, WhatsApp, Slack, and others), showing the full
command with no truncation, in both all/new and verbose modes. That posted
complete shell commands (heredocs, internal paths, destructive commands) into
the chat before the final answer, visible to everyone in it.

This restores the prior behavior: terminal progress shows the short, truncated
preview line that every other tool already uses, capped at tool_preview_length.
The supports_code_blocks capability flag is left in place for future use.
CLI/TUI rendering is a separate path and was unaffected.

Adds a regression test asserting terminal progress renders as a truncated
preview, not a fenced bash block, even on a markdown-capable gateway.

Fixes #41955
2026-06-08 15:53:00 -07:00
konsisumer
3714caa1b9 fix(session): follow compression continuations for transcript reads 2026-06-07 23:57:20 -07:00
Hariharan Ayappane
b8469a81e3 fix(weixin): add rate-limit circuit breaker 2026-06-07 22:10:17 -07:00
Teknium
2e62862784
fix(telegram): use get_running_loop in polling-conflict retry reschedule (#41716)
The conflict-retry path called asyncio.get_event_loop() to reschedule
itself when a retry's start_polling raised. On Python 3.11+ (our floor)
that raises 'RuntimeError: There is no current event loop in thread
MainThread' when no loop is attached to the thread, which is what
happens when PTB dispatches this error callback. The retry never gets
scheduled, the adapter goes silent-but-alive, and gateway --replace
keeps spawning fresh instances that hit the same wall — the crash loop
reported in #19471 (worse under multi-profile, where two bots hold the
same conflict open).

We are inside a coroutine here, so asyncio.get_running_loop() is the
correct, guaranteed-valid replacement. Only get_event_loop() call in
any platform adapter, so no sibling sites.

Fixes #19471
2026-06-07 22:10:03 -07:00
islam666
09a5548628 fix(weixin): refresh typing ticket on expiry to prevent stuck indicator (#38085)
The WeChat iLink typing ticket has a 600-second TTL. When a long-running
session exceeds that window, the cached ticket evicts from TypingTicketCache.
Both send_typing and stop_typing silently returned early when the ticket was
None, meaning the TYPING_STOP=2 signal was never sent to iLink. The WeChat
client then showed the typing indicator indefinitely.

Fix: add _ensure_typing_ticket() that transparently refreshes the ticket
via getConfig when the cached one has expired or is missing. Both send_typing
and stop_typing now call this method instead of silently no-oping.

Fixes #38085
2026-06-07 21:50:57 -07:00
Brian D. Evans
ab0a6270c3 fix(slack): align thread_ts check with is_thread_reply invariant (Copilot #15464)
Two findings from Copilot's review on #15464, both addressed:

1. ``event.get("thread_ts")`` truthy vs
   ``event_thread_ts != ts``: the new channel branch treated ANY
   truthy ``thread_ts`` as a real thread reply, but three lines below
   ``is_thread_reply`` is defined with the stricter
   ``event_thread_ts and event_thread_ts != ts`` invariant.  If Slack
   ever ships a payload where ``thread_ts == ts`` on a thread root,
   the stricter check would treat it as a top-level message for the
   ``is_thread_reply`` path but as a thread reply for session keying
   — divergent behaviour.  Aligned this branch to the same
   ``and event_thread_ts_raw != ts`` invariant.

2. ``test_top_level_reply_to_id_stays_none_when_shared`` docstring
   had the ternary logic backwards ("None != ts → reply_to_message_id
   IS set").  The code reads
   ``reply_to_message_id = thread_ts if thread_ts != ts else None`` —
   with ``thread_ts = None``, the condition is True so the expression
   evaluates to ``thread_ts`` itself (None), meaning the reply stays
   un-threaded.  The test asserted the correct end-state; only the
   explanatory docstring was wrong.  Rewrote the docstring to match
   the actual code flow, with the note that Copilot caught the
   reversal.

7/7 tests still pass.  No behaviour change for the existing
test_thread_reply_scopes_by_thread_even_when_shared case because
``event_thread_ts_raw = "1700000000.000000"`` and ``ts =
"1700000000.000005"`` are distinct — the new
``!= ts`` guard is a no-op there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 21:19:59 -07:00
Brian D. Evans
133e0271e2 fix(slack): scope top-level channel messages by channel-only when reply_in_thread=false (#15421)
Top-level Slack channel messages previously fell back to the message's
own ``ts`` as a synthetic ``thread_ts``:

    thread_ts = event.get("thread_ts") or ts  # ts fallback for channels

That value flows into ``build_source(thread_id=thread_ts)`` at
line 1247.  The gateway session store keys sessions by
``(platform, channel_id, thread_id)``, so every top-level channel
message ended up on a unique session.  Operators who set
``reply_in_thread: false`` in ``config.yaml`` expected all top-level
channel messages to share one session (the whole point of that flag)
— instead each one spawned a fresh conversation with no context
carry-over.

### Fix

Three explicit cases in the channel branch:

| event.thread_ts | reply_in_thread | thread_ts for session keying |
|---|---|---|
| non-null (real thread reply) | either | event.thread_ts |
| null (top-level) | true (default) | ts (legacy: own-thread sessions) |
| null (top-level) | false | **None** (shared channel session) |

The outbound-reply gate at line 1264 (``reply_to_message_id =
thread_ts if thread_ts != ts else None``) still works correctly in
all three cases without further changes: ``None != ts`` is True, so
shared-channel top-level messages don't get their reply threaded
either — matching the operator's ``reply_in_thread=false`` intent
end-to-end.

Genuine thread replies still scope per-thread under both modes so
multi-person threaded conversations can't collide with unrelated
channel chatter.

### Tests (7 new in ``tests/gateway/test_slack_channel_session_scope.py``)

All drive the real ``SlackAdapter._handle_slack_message`` code path
(not a re-implementation) via the standard pytest fixture pattern
used by ``tests/gateway/test_slack.py``.  Messages @mention the bot
so the mention gate doesn't drop them — the tests are specifically
about what happens once the handler decides to emit a ``MessageEvent``.

* ``TestChannelSessionScopeDefault`` (2 cases):
  - Explicit ``reply_in_thread: true`` keeps ``thread_id = ts``
    (legacy behaviour — regression guard)
  - Unset config behaves like ``reply_in_thread: true`` (pins the
    default)
* ``TestChannelSessionScopeShared`` (3 cases):
  - ``reply_in_thread: false`` + top-level → ``thread_id is None``
    (the #15421 bug 1 fix)
  - ``reply_to_message_id is None`` in the same case (no threaded
    outbound reply)
  - Genuine thread reply still scopes per-thread when shared mode is
    on — only TOP-LEVEL messages collapse to the channel session
* ``TestThreadReplyAlwaysScopesByThread`` (2 parametrised cases):
  - Thread replies get ``thread_id = event.thread_ts`` regardless of
    ``reply_in_thread`` — critical invariant for multi-thread
    channels; a regression here would leak per-thread context across
    threads

**Regression guard verified**: reverted the else-branch to the legacy
``thread_ts = event.get("thread_ts") or ts`` one-liner;
``test_top_level_maps_to_none_when_reply_in_thread_false`` correctly
failed (asserts ``thread_id is None`` but got ``"1700000000.000003"``).
Restored → 182 slack tests pass (175 existing + 7 new).

Scope: this fixes #15421 bug 1 only.  Bug 2 (sessions.json not
persisting across compression) lives elsewhere in the session
manager and is left for a separate diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 21:19:59 -07:00