Commit Graph

678 Commits

Author SHA1 Message Date
Nikita Barkov 5d3f75110a fix(kanban): key terminal-event wakes to the creator's workspace scope
Slack session keys include the workspace id since #70190, but the kanban
notifier rebuilds the wake source from a subscription row that has no scope
column, so every terminal-event wake keyed without the workspace.

The legacy-key adoption shipped in the same change (`_legacy_slack_session_key`,
`_recovered_row_matches_source_scope`) resolves that unscoped key onto the same
session_id, so the wake passes the busy guards that are keyed by routing key
(`_active_sessions`, `_running_agents`) and only collides afterwards, on session
id, under the per-session turn lease (#64934) — which serializes it behind the
live turn's flush. On a live Slack gateway that shows up as a duplicate run on
one task plus 400+s of waiting before the woken turn starts.

Same failure mode as #56580 / #72191 (chat_type), one field over, and it needs
no schema change: `_thread_metadata_for_source()` already stamps
`slack_team_id`, the notify subscription persists that dict as
`delivery_metadata`, and the notifier already unpacks it. Rows written by
`kanban_tools._maybe_auto_subscribe` carry no workspace, so fall back to the
adapter's channel → workspace map via `scope_id_for_chat()`, read with getattr
so adapters opt in and unscoped platforms' keys stay byte-identical. Slack
answers it from `_remember_channel_team`, which drops channels claimed by two
workspaces, so an unknown or ambiguous channel degrades to today's behavior
instead of guessing wrong.

Also adds the contributor email mapping the attribution check requires.

Co-authored-by: Junie <junie@jetbrains.com>
2026-08-13 11:41:19 -07:00
Simon van Laak eb137762ff feat(slack): render tool progress as native plan/task cards (opt-in)
Adds platforms.slack.extra.native_task_cards: when enabled, live tool
calls render as Slack-native plan/task cards via chat.startStream /
chat.appendStream (task_display_mode: plan, task_update chunks) instead
of text/edit progress bubbles. ID-bearing tool_start/tool_complete
callbacks correlate concurrent same-name tool calls correctly; any
native API failure falls back to one continuously edited text update.
The stream is stopped exactly once when the turn finalizes.

Salvaged from PR #29496 onto current main (TurnRunner/TurnContext seam);
closes #29483.
2026-08-13 11:12:19 -07:00
Arman 47ed5e4964 feat(slack): native streaming via chat.startStream/appendStream/stopStream
Slack's Agents & AI Apps feature ships a native streaming surface that
renders a live-typing message instead of the edit-based progressive
updates the adapter used until now.

The adapter now implements the existing draft-streaming interface:

- supports_draft_streaming() opts in whenever the app is connected and
  native streaming hasn't been detected as unavailable.
- send_draft() starts a stream on the first frame (chat.startStream,
  anchored to the resolved thread_ts, with recipient_team_id/user_id
  for channel streams) and appends only the delta on subsequent frames
  (chat.appendStream is append-only). The consumer's trailing cursor
  glyph is stripped before delta computation.
- Unlike Telegram drafts (ephemeral, replaced by a real sendMessage),
  a Slack stream IS the final message. send() therefore intercepts the
  turn-final delivery for a chat with an active stream whose streamed
  text is a prefix of the final content, and seals it via
  chat.stopStream with the remaining delta instead of posting a
  duplicate. Rich Block Kit (when enabled) is applied to the sealed
  message via chat_update, mirroring the finalize path in edit_message.
- Feature-gate errors from chat.startStream (not_allowed,
  missing_scope, unknown_method, ...) are cached on the adapter so
  subsequent runs skip straight to edit-based streaming with a single
  warning naming the fix (enable Agents & AI Apps for the app);
  transient errors only disable drafts for the current run via the
  consumer's existing send_draft failure handling.
- Segment breaks (new draft_id) and disconnect() seal any open stream
  so chats are never left with a dangling live-typing indicator.

No consumer or config changes: streaming.transport auto/draft now
lights up native streaming on Slack through the same interface
Telegram drafts use, and the edit-based path remains the fallback.
2026-08-13 11:12:19 -07:00
andyst-dev e49a7fe568 fix(google-chat): post cron deliveries as new top-level threads, not replies
_resolve_thread_id() falls back to _last_inbound_thread[chat_id] when no
explicit thread is present. That fallback exists for interactive DMs, where
Google Chat spawns a fresh thread per top-level user message and the adapter
drops thread_id to keep the session key stable. It also fired for cron
deliveries, which carry job_id in their metadata but no thread: the output
landed as a reply inside the last inbound thread instead of starting a new
top-level message.

Bypass the _last_inbound_thread fallback when metadata has a job_id (i.e. the
message is an automated cron delivery), so cron output posts at top level
unless an explicit thread is requested.
2026-08-13 11:05:40 -07:00
Nikita Barkov 9cf2cbd382 fix(slack): read real SDK responses instead of gating on isinstance dict
Slack Web API calls return `SlackResponse`/`AsyncSlackResponse`, which are
mapping-like but not `dict` subclasses, so every `isinstance(resp, dict)`
gate took its "unexpected shape" branch at runtime: user and channel names
collapsed to raw IDs, every user resolved as a non-bot (defeating the
allow_bots loop guard), ephemeral replies were reported as failures, and
uploads/caption fallbacks lost their message_id.

Normalize responses through a single `_slack_response_payload()` helper
(dict passes through, SDK response yields `.data`, anything else yields
`{}` so callers keep their fallbacks) and use it at every call site.

Existing Slack tests injected plain dicts, which is why the defect was
invisible; the new tests run each behavioral case against a real
`AsyncSlackResponse` as well.
2026-08-13 10:32:07 -07:00
Christopher 136a911065 fix(whatsapp): classify npm install failures as non-retryable fatal errors (#80095) 2026-08-13 02:37:12 -07:00
Teknium f4749a77a5 fix(mattermost): escalate genuine WS auth failures through the fatal-error hook
Follow-up to the salvaged #80489 substring-fallback removal: the
structured 401/403 branch still exited with a bare return, leaving
_running True — dead listener, healthy-looking is_connected(), gateway
never told (the zombie half of the bug, OOF-156 class). It now sets a
non-retryable mattermost_auth_error with token guidance and notifies
the gateway fatal handler.

Also: pytest.importorskip for aiohttp in the verifier probe file
(module-level import crashed collection in envs without the optional
dep), and probe fixtures updated for the escalation attributes.
2026-08-13 01:51:13 -07:00
Stephen Chin d184d68f37 fix(mattermost): stop misclassifying transient errors as auth failures
The WS reconnect loop had a fallback check that looked for "401", "403",
or "unauthorized" as substrings anywhere in an exception's string form.
A transient error whose message happens to contain those digits (a proxy
body, a stack trace, anything) got treated as a permanent auth failure
and stopped reconnection for good.

I removed the substring fallback and kept only the structured check:
aiohttp.WSServerHandshakeError with status in {401, 403}. That's the only
signal that reliably means the server rejected our credentials.

Added two regression tests: one proving a transient error containing
"401" in its text still retries, and one confirming the existing
_closing early-return path is untouched by the removal.
2026-08-13 01:51:13 -07:00
Teknium a7f0abc845 fix(email): dispatch partial batches, seen-after-fetch UIDs, reconnect UID baseline restore
Follow-ups to the salvaged #80032 fatal-error escalation, closing the
gaps its review thread identified plus a sibling of the same class:

1. Partial-batch loss: _check_inbox now dispatches whatever the fetch
   returned BEFORE escalating a failure — the early-return dropped
   already-fetched messages whose UIDs were marked seen.
2. Seen-after-fetch: UIDs enter _seen_uids only after their fetch
   returns a response, so a mid-batch connection failure leaves the
   remaining UIDs eligible for the next poll. Per-message processing
   moved to _parse_fetched_message behind a poison guard: a message
   that fails parsing/auth-verification is marked seen, logged with
   its UID, and skipped once — never an eternal crash loop.
3. Reconnect mail loss: connect(is_reconnect=True) restores the
   account's seen-UID baseline from a class-level snapshot instead of
   re-marking the entire mailbox seen — mail that arrived during an
   outage is now processed after the reconnect the escalation triggers.

7 new regression tests.
2026-08-13 01:24:54 -07:00
kyssta-exe 9b8da52f41 fix(email): surface IMAP fetch failures through the fatal-error hook (#80016)
_fetch_new_messages() wrapped the whole IMAP connect/login/select/search/
fetch sequence in a bare except that logged and returned an empty list —
indistinguishable from a genuinely empty inbox. The adapter never invoked
its fatal-error handler, so the gateway's reconnect/backoff/status
machinery never learned the mailbox was unreachable; outages lasted until
a manual restart.

Track fetch failure on the adapter and, when the poll loop observes it,
set a retryable fatal error (email_imap_fetch_failed) and notify the
gateway handler so the platform enters the reconnect queue just like a
startup connection failure.
2026-08-13 01:24:54 -07:00
Teknium fe5e7799f2 refactor: fold tailored intents guidance into the connect classifier
The cherry-picked #79448 predated #85049's _classify_connect_exception,
so it added a parallel PrivilegedIntentsRequired branch ahead of the
classifier (plus its own _is_privileged_intents_required detector).
Fold the tailored guidance into the classifier's existing intents arm
instead: one classification path, one error code (discord_intents_required),
and the message now names exactly the intents Hermes requested (Message
Content always; Server Members only when username/role allowlists need it).
Wizard callout, docs corrections, and tests from #79448 kept as-is.
2026-08-13 00:10:30 -07:00
rainbowgits b10e7890b6 fix(discord): name missing privileged intents and stop reconnect loop
PrivilegedIntentsRequired is a Developer Portal config error; surface which
intents Hermes requested as a non-retryable fatal and teach setup/docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 00:10:30 -07:00
Teknium 6397776fe8 refactor: simplify discord classifier + move attention threshold to config.yaml
- Collapse the duplicated discord LoginFailure/PrivilegedIntentsRequired
  classification (name-match + isinstance blocks repeated the same code/
  message tuples) into a single _is() helper — one message per failure.
- Replace the user-facing HERMES_RECONNECT_ATTENTION_AFTER_SECONDS env var
  with agent.reconnect_attention_after in config.yaml (default 7200, 0
  disables), bridged internally like gateway_timeout. .env is for secrets.
- Use _float_env for robust parsing instead of bare int(os.getenv(...)).
- Document terminal classification + needs_attention escalation in
  website/docs/user-guide/configuration.md.
2026-08-12 22:16:12 -07:00
Shannon Sands 91bc822330 fix(gateway): classify terminal adapter connect failures + escalate long-lived retry loops (OOF-156)
Fleet triage after the 2026-08-11 storm resolution found agents whose sole
platform had been silently 'retrying' for weeks: revoked Telegram tokens,
Discord privileged-intent rejections, and Photon sidecars that can never
start were all funnelled into the indefinite reconnect queue with no owner
signal (OOF-151/152/153, epic OOF-156).

Two-part fix:

1. Per-adapter classification — by exception TYPE only, never message text:
   - telegram: InvalidToken/Forbidden -> telegram_auth_error, retryable=False
     (new _looks_like_auth_error, mirrors _looks_like_network_error)
   - discord: LoginFailure -> discord_auth_error, PrivilegedIntentsRequired
     -> discord_intents_required (both retryable=False); every other path now
     sets an explicit code (previously the generic branch set NO fatal info,
     which the gateway read as 'probably transient')
   - photon: new typed PhotonSidecarStartupError; deps-install failure ->
     SIDECAR_DEPS_MISSING and missing node binary -> SIDECAR_NODE_MISSING
     (retryable=False); ambiguous startup crashes stay retryable
   - email: IMAP/SMTP failures now always set a fatal code;
     SMTPAuthenticationError -> email_auth_error, retryable=False (IMAP4.error
     is type-ambiguous between bad creds and transient NOs, so IMAP stays
     retryable)

2. Gateway escalation — platforms continuously in the reconnect queue past
   HERMES_RECONNECT_ATTENTION_AFTER_SECONDS (default 2h, 0 disables) get
   needs_attention=true + retrying_since stamped into runtime status, once
   per episode, cleared on successful reconnect.

Deliberately NOT a circuit breaker: retries never stop. The auto-pause
mechanism was removed for good reason (transient DNS outages left bots
silently dead); this preserves that and only adds visibility. No new
platform_state enum values — NAS's status schema is strict — only additive
fields.

Unknown exception types always stay retryable: a false terminal recreates
the silently-dead-bot problem, and the escalation path covers
misclassified permanent failures.
2026-08-12 22:16:12 -07:00
Teknium 3b7c940208 feat(gateway): more normalized gateway_platform_event types (#64176)
Extend the normalized-envelope pipeline shipped in #82063 with new event
types, each with its own versioned, event-local payload contract:

- Telegram: message_edited (edited_message updates; editor-identity auth
  extraction, forum topic thread_id, bounded text/caption, ISO edited_at)
- Discord: message_edited, message_deleted, thread_created, thread_renamed
  (on_message_edit/delete, on_thread_create/update fire-sites with has_hook
  no-subscriber fast-paths, bot-authored events dropped, rename-only
  filtering on thread updates)

All events flow through the same gateway-owned post-auth boundary; malformed
or unauthorized events drop, fail closed. Raw SDK payload access is
deliberately NOT shipped (round-2 correction: needs its own
gateway.raw_events capability and design).

The Discord fire-site machinery (no-subscriber fast-path, observer isolation,
connect-time wiring) adapts the observer-hook design from PR #62584
(@paoloantinori) onto the normalized-envelope contract; PR #36875's raw
telegram update hook is superseded by the same correction.

Docs: hooks.md gains per-event payload contract tables.

Co-authored-by: Paolo Antinori <pantinor@redhat.com>
2026-08-12 20:10:51 -07:00
Teknium 1e46e09bbd fix(gateway): scope reaction observers to routed profiles 2026-08-12 16:42:28 -07:00
Teknium 9994bc9ec9 fix(gateway): enforce post-auth normalized reaction observer
Builds on Paolo Antinori's #68431 salvage for #64176. Move plugin dispatch behind the profile-scoped runner authorization boundary, fail closed on malformed reaction identities, preserve observer registration across Telegram app rebuilds, and document the deliberately observer-only contract.

Co-authored-by: Paolo Antinori <pantinor@redhat.com>
2026-08-12 16:42:28 -07:00
Paolo Antinori 24e3aa180f fix(plugins): gateway_platform_event error logs include traceback; pin handler groups
- _on_platform_update: log the normalize and auth errors with exc_info=True so
  a regression that silently drops reactions leaves a traceback, not just a
  one-line message (matches the intake auth fallback's exc_info usage).
- TestRegisterHandlers: also assert five core handlers land in the default
  group and only the observer is in group 99.

DoD: hook + auth tests green (34 passed). For a log-line + assertion change
the substantive gate is the test run; /simplify and /code-review were applied
proportionately.
2026-08-12 16:42:28 -07:00
Paolo Antinori c0a4535a26 fix(plugins): address #64176 review on gateway_platform_event (#68431)
Response to teknium1's hermes-sweeper review (keep_open, salvageability=medium).

1. Post-auth gate. The group-99 catch-all fired gateway_platform_event before
   the authorization boundary. Extract _is_source_authorized(source) from
   _is_user_authorized_from_message and add _source_from_reaction_for_auth;
   reactions whose actor the intake would reject no longer reach plugins.
   Fails closed if source extraction raises, so a future non-reaction event
   type cannot silently bypass auth before its own extraction is wired.

2. Shared registration. Extract _register_handlers(app) from connect() so the
   gateway_platform_event observer (group 99) is re-registered alongside the
   core handlers on any rebuild path.

3. Trim inert hook surface. Drop the three reserved gateway_* names from
   VALID_HOOKS (keep only gateway_platform_event). The others land with their
   real contracts and fire-sites when #64231 is finalized.

Tests: unauthorized/authorized/open reaction gating, fail-closed for a future
non-reaction event type, and _register_handlers re-registration.

Ran /simplify and /code-review (high) before pushing.
2026-08-12 16:42:28 -07:00
Paolo Antinori 929be4d1aa feat(plugins): gateway_platform_event observer hook (normalized envelopes)
First slice of #64176's observer-hook half — a normalized-envelope inbound
event hook, replacing raw-SDK handler args with a stable contract (per #64176's
"normalized versioned envelopes only; raw SDK gated behind a capability" rule).

- VALID_HOOKS: register the four gateway_* names from #64176
  (gateway_platform_event fires today; gateway_session_titled /
  gateway_message_delivered / gateway_thread_created reserved pending #64176's
  fire-sites).
- BasePlatformAdapter._fire_gateway_hook: reusable, has_hook-guarded,
  per-call-isolated fire helper (the no-subscriber common case short-circuits).
- TelegramAdapter: a group-99 catch-all TypeHandler normalizes inbound updates
  into gateway_platform_event envelopes. message_reaction -> {platform,
  event_type:"reaction", payload{emojis, custom_emoji_ids, chat_id, message_id,
  thread_id}} (custom-emoji reactions captured via custom_emoji_id; standard via
  .emoji — no None in consumer-facing lists). Other update types return None
  pending #64176's taxonomy (#64231). Normalization is wrapped so a malformed
  update can't raise into PTB dispatch.

Observer-only — zero behavioral change to core dispatch. Supersedes the raw
inbound half of #62584 (telegram:update -> normalized gateway_platform_event).

Tests: VALID_HOOKS registration; _fire_gateway_hook routing/has_hook/isolation;
_normalize for standard, custom-emoji, and mixed reactions + non-reaction;
_on_platform_update firing + normalize-error isolation.

Ran code-review (high) + simplify before pushing.
2026-08-12 16:42:28 -07:00
ethernet 8789cf9f0c fix(sec): patch the npm advisories main left open
Main (7537de9e7) moved most of the vulnerable locked versions, but some
fixes live only in the lockfiles and some advisories stayed open. This
commit closes the rest:

website/package.json gets durable overrides for js-yaml 4.3.1,
dompurify 3.4.13, mermaid 11.16.1, and tar 7.5.22. The root workspace
gets the same tar override, which moves the tar 6.2.1 copies under
get-windows and @mapbox/node-pre-gyp past twelve open advisories.
Without an override, a reinstall can pull an old transitive copy back
in.

image-size <=2.0.2 has two infinite-loop DoS advisories and no fixed
release upstream. An override points it at @nous-research/image-size
2.0.3, our maintained fork of the real repo. The OSV scanner resolves
the aliased fork cleanly, so no ignore entries are needed.

The photon sidecar moves @opentelemetry/core to 2.10.0. The
whatsapp-bridge gets a body-parser 1.20.6 override, so the lockfile-only
fix from main cannot regress on reinstall.

website/.npmrc gets matching min-release-age exclusions for the fix
releases that are less than two weeks old.

electron stays at 40.10.2. The 41.x fix for GHSA-9f4c-93c8-jc8g brings
back the install failure that bb8280b75 reverted: install.js in 40.10.3+
extracts with an MSVC native binding, which fails on Windows machines
without the VC++ Redistributable. Upstream tracks this in
electron/electron#52481, with no fix released.
2026-08-10 13:49:37 -04:00
joaomarcos 82255fa8ef fix(telegram): reset failed primary transport pool
Retryable primary errors can leave pooled sockets in CLOSE_WAIT while fallback retries continue. Replace and close failed primary generation before fallback selection.\n\nRefs #82920
2026-08-10 13:46:48 +05:30
aameobius 100219f664 fix(cron): surface exception type and traceback for standalone Discord delivery errors 2026-08-10 10:58:05 +05:30
Slobaka 3332ad4dbf fix(telegram): avoid MDV2 draft preview when rich_messages lacks rich_drafts
When rich_messages is on and rich_drafts is off, transport=auto used
sendMessageDraft (MarkdownV2 tables→bullets) then finalized via
sendRichMessage. Users saw a crooked first bubble and a second wiki-style
final. Decline drafts in that config so auto uses edit-in-place + rich
finalize on one message.

Fixes #78524
2026-08-09 12:53:45 +05:30
Teknium 7537de9e74 fix(deps): patch 31 known CVEs across Python and npm lockfiles
OSV weekly scan reported 50 known vulnerabilities in pinned deps.
This bumps everything with a released, semver-compatible fix:

Python (uv.lock):
- aiohttp 3.14.1 -> 3.14.3 (GHSA-cq5v-8q36-5273, GHSA-mfx4-hv73-q22v,
  GHSA-mq44-7p77-q5h7)
- h2 4.3.0 -> 4.4.1 (CVE-2026-71554 request smuggling; exclude-newer
  exception documented in pyproject, remove after 2026-08-17)

npm (root workspace):
- brace-expansion 5.0.8 -> 5.0.9, undici 6.27->6.28 / 7.28->7.29,
  js-yaml 4.3.1, nanoid 3.3.17/3.3.18, ip-address 10.4.0,
  mermaid 11.16.1 + dompurify 3.4.13 (root overrides so the
  streamdown transitive copy is pinned too)
- electron 40.10.2 -> 40.10.6 (GHSA-r4w5-6pfg-jxp5; the 41.x major
  for GHSA-9f4c-93c8-jc8g is deferred to its own PR)

npm (website): mermaid, dompurify, js-yaml, nanoid, fast-uri 3.1.5,
postcss 8.5.23, undici 7.29.0
npm (photon sidecar): @opentelemetry/core 2.8.0 via override, undici
npm (whatsapp-bridge): body-parser 1.20.6

min-release-age excludes added to .npmrc/website/.npmrc for the
sub-2wk CVE-fix releases, each with a removal date.

Remaining findings are blocked upstream: cryptography <49 cap
(alibabacloud-tea-openapi), image-size (no fixed release), tar 6.x
transitive majors, electron 41.

Local rescan: 50 -> 19 known vulns, 0 introduced.
2026-08-08 14:06:48 -07:00
Teknium 65f407184d fix(email): never let unknown or malformed charsets abort the IMAP fetch
Unknown charset labels (QQ Mail's RFC 1428 'unknown-8bit' placeholder,
misspelled names, garbage encoded-word charsets) raised LookupError from
bytes.decode — errors='replace' only guards decode errors, not a missing
codec — aborting the whole fetch batch. UIDs are marked seen before the
fetch, so the crash permanently dropped every message in the batch.

- _safe_decode(): alias table (unknown-8bit→utf-8, gb2312/gbk→gb18030,
  ks_c_5601-1987→cp949, ...) then utf-8, then latin-1 last resort.
- _decode_header_value(): wraps decode_header() so a malformed RFC 2047
  header degrades to the raw string instead of crashing.
- _extract_text_body(): all three decode sites now use _safe_decode.

Fixes #35901, fixes #55381, fixes #55383.
2026-08-08 12:30:19 -07:00
峯岸 亮 022d196f38 fix(telegram): honor UTF-16 entity offsets 2026-08-08 12:30:19 -07:00
Drexuxux 7c02bfce89 fix(slack): insert resolved display names literally when humanizing mentions
_humanize_user_mentions rewrites <@UID> to @DisplayName by passing the
resolved name as re.sub's replacement, where re parses it as a template.
A display name is arbitrary user-set text, so the escapes in it are the
user's characters, not regex syntax:

  dev\ops  -> re.error: bad escape \o
  a\1b     -> re.error: invalid group reference 1
  \g<0>    -> expands to the whole match, silently putting the opaque
              <@UID> back — the token this method exists to remove

The trigger-text call site sits in _handle_slack_message outside any try,
and both Bolt event handlers await it bare, so the raise takes the whole
inbound message down: every message mentioning that person is dropped.

Pass the replacement as a function instead — re does no template parsing
on the return value, so the name lands verbatim. Same shape the Matrix
adapter already uses for its outbound mention rewrite.
2026-08-08 05:58:43 -07:00
kshitij 0041fc6946 refactor(simplex): hoist json.dumps above branch in _standalone_send
Deduplicate the composed JSON payload construction — both the group
and DM branches build the identical json.dumps result, so hoist it
above the if/else to match the pattern already used in send().
2026-08-08 13:56:12 +05:30
liuhao1024 eb048772f6 fix(simplex): use structured /_send for standalone DM text sends
The _standalone_send() function (used by the send_message tool for
proactive/scheduled sends) has the same bare `@<id> text` bug that
send() had before PR #44444. SimpleX's `@<x>` syntax resolves x as a
display name, not a contactId — the daemon silently drops messages
when it cannot find a contact named "6".

Use the structured `/_send @<id> json [...]` form, matching what
send_image, send_document, and the send() fix already use.

Fixes #46265
2026-08-08 13:56:12 +05:30
liuhao1024 3a04d9c4d7 fix(simplex): use structured /_send for DM text messages to prevent silent drops 2026-08-08 13:56:12 +05:30
kshitij e5e96e8bb5 fix: harden _await_disconnect_step against outer cancellation + add claim keys
Follow-up to #80700:

1. _await_disconnect_step was missing the try/except CancelledError around
   asyncio.wait() that _await_adapter_cleanup_with_timeout already has.
   When the outer fatal-handler timeout cancels disconnect() mid-step,
   asyncio.wait does NOT cancel its inner task — the task was orphaned
   with no observer. Add the same cancel+detach+re-raise pattern.

2. _queue_retryable_fatal_platform omitted credential_claim/listener_claim
   keys that all 3 startup-path queue sites include. These are consumed by
   the multiplex reservation logic to prevent secondary profiles from
   taking the endpoint while a primary is queued. Pre-existing latent bug
   — now fixed since the extraction makes it trivial.
2026-08-07 18:32:45 +05:30
HexLab98 7141a6dc3a fix(gateway): queue reconnect before fatal disconnect wedges (#80598)
After a network outage the Telegram fatal handler could hang inside
disconnect() and never populate _failed_platforms, so the reconnect
watcher had nothing to retry and the process stayed permanently deaf.
Queue retryable platforms before any disconnect await, bound the fatal
handler with an outer detach deadline, and release the Telegram token
lock / PTB close steps with detach-on-timeout so recovery cannot stall.
2026-08-07 18:32:45 +05:30
kshitij 6ab7528c33 refactor(matrix): extract _strip_reply_fallback to deduplicate text+media handlers
The reply fallback stripping loop was copy-pasted between _handle_text_message
and _handle_media_message. Extract into the module-level _strip_reply_fallback
helper, matching _extract_reply_fallback's existing pattern.
2026-08-07 14:34:55 +05:30
kshitij e8511efe75 fix(matrix): propagate sender + reply context to media MessageEvents too
_handle_media_message had the same gap as _handle_text_message: it didn't
set user_id/user_name or any reply_to_* fields on MessageEvent. A user
replying to a photo/video/audio in Matrix got null sender metadata and
null reply context — the exact bug PR #80293 fixes for text messages.

Mirrors the text handler's reply fallback parsing + sender propagation
into the media handler, and adds a test covering a media reply event.

Also fixes test env mutation: _make_adapter now accepts monkeypatch
and uses monkeypatch.setenv() instead of bare os.environ assignment,
preventing env var leakage across tests in the same xdist worker.
2026-08-07 14:34:55 +05:30
WintleChoung e245a98781 fix(matrix): propagate sender MXID + reply context to MessageEvent
The Matrix adapter built MessageEvent from inbound room events but dropped
the sender's MXID and display name on the event itself -- only 'source'
carried them. Other adapters (signal/slack/telegram/discord/mattermost/irc)
have the same gap; this PR fixes matrix and adds the supporting
top-level MessageEvent fields so the rest can follow.

Downstream effects for matrix specifically:
  - gateway prompt assembly can now read event.user_name (or source)
    without having to dig into source per platform
  - reply context (reply_to_text / reply_to_author_id /
    reply_to_author_name) is parsed from the inline > <@user:server> ...
    Matrix fallback format before stripping, instead of discarded
  - the gateway's existing [Replying to: "..."] renderer can now show
    who the user was replying to (was always anonymous for matrix)

MessageEvent gains two optional top-level fields (user_id, user_name,
both default None) so non-IM producers (cron/webhook/autonomous) remain
unaffected. Source still carries the same values for callers that
already read from there.

Tests cover:
  - non-reply message carries sender user_id/user_name on MessageEvent
  - different senders (alice, bob) both propagate
  - reply message carries reply_to_message_id + reply_to_text +
    reply_to_author_id + reply_to_author_name, parsed from the
    > <@carol:example.org> original question\n\nactual reply shape
  - non-reply message does NOT spuriously set reply_to_* fields

Sibling matrix tests (148 across test_matrix*.py) remain green.

Authored by WintleChoung <cwt@users.noreply.github.com>
Salvaged from PR #80293.
2026-08-07 14:34:55 +05:30
kshitij 99237a4444 refactor: derive teams install hint via feature_install_command(venv_pip=True)
Fold the remaining simplify-code reuse finding: teams' _install_hint()
duplicated lazy_deps' spec-fetch + quote + join (feature_install_command
already builds pip commands from LAZY_DEPS). Add a venv_pip=True variant
to feature_install_command — sys.executable -m pip targeting, correct in
every install layout and immune to PEP 668 — and shrink the teams helper
to a one-line call.  Also gives matrix and the other platforms a shared
derived hint to adopt later.  New test mutation-checked (fails when
venv_pip returns the uv form).
2026-08-07 13:28:43 +05:30
kshitij f5784617e8 refactor: fold simplify-code review findings
- matrix/dingtalk: extract deps-only installers (ensure_matrix_deps,
  ensure_dingtalk_deps) and register THOSE as ensure_deps_fn — the prior
  check_*_requirements combined credential env checks with the install,
  so a platform configured via PlatformConfig.extra (which is_connected
  accepts) would pass enablement, reach create_adapter(), and have the
  'installer' veto on env-var grounds before installing anything —
  re-creating the #79812 deadlock for extra-configured setups.  The
  combined deps+credentials functions remain for setup/status callers.
- matrix/feishu passive probes: use the existing lazy_deps.is_available()
  instead of hand-rolling 'not feature_missing(...)' (reuse finding).
- teams: module docstring no longer recommends bare system pip (the
  PEP 668 trap purged everywhere else); docs troubleshooting row updated
  to match the new hint text.
- wecom_callback: drop dead 'global ET, DEFUSEDXML_AVAILABLE'
  (ensure_and_bind mutates the module dict directly; nothing assigns).
- tests: parametrized wiring contract for all 8 lazy-installable
  platforms — ensure_deps_fn present and distinct from check_fn
  (behavior contract, not identity snapshot, so renames don't churn it).
2026-08-07 13:28:43 +05:30
kshitij a658dfe509 fix: address self-review findings on the check_fn/ensure_deps_fn split
- gateway/config.py: rewrite the stale enablement-pass header comment that
  still described check_fn as 'the single source of truth for are-my-env-
  vars-set' / 'lazy-installs it' — both false under the new contract.
- teams: check_requirements docstring wrongly claimed credential checks
  (body checks only SDK/aiohttp presence); derive install_hint from the
  canonical LAZY_DEPS pins + sys.executable instead of hardcoding
  '~/.hermes/hermes-agent/venv/bin/pip' and version pins (wrong under
  HERMES_HOME overrides / profile installs; pins go stale on CVE bumps);
  connect() fatal-error hints now point at the venv pip instead of bare
  system pip (the PEP 668 trap the docs warn about).
- teams docs: drop exact version pins from the two manual-install commands
  (LAZY_DEPS is the source of truth; unpinned installs still work and the
  text can't go stale).
- hermes_cli/status.py: per-entry exception guard around check_fn so one
  raising probe can't abort the listing of all remaining plugin platforms
  (aligns with the other three call sites).
- tests: rename test_register_check_fn_is_active_lazy_installer ->
  test_register_splits_passive_probe_from_active_installer (name said the
  opposite of what it verifies).
2026-08-07 13:28:43 +05:30
kshitij 0d32607c62 fix(gateway): split check_fn (passive probe) from ensure_deps_fn (active installer)
PlatformEntry.check_fn served three contradictory roles: adapter-creation
gate, config auto-enablement gate, and status display. Plugins had to pick
one function for all three:

- Active installer as check_fn (discord/slack/telegram/matrix/dingtalk/
  feishu): every status display could pip-install SDKs as a side effect
  (the desktop 94% boot-loop class).
- Passive probe as check_fn (teams, wecom_callback): create_adapter()
  returned None before connect() could lazy-install, so the SDK never
  installed (#79812 deadlock; wecom_callback's platform.wecom_callback
  LAZY_DEPS entry was dead code).

The split makes both call sites correct by construction:

- check_fn is now contractually PASSIVE (probe only, never installs).
- New optional PlatformEntry.ensure_deps_fn is the ACTIVE installer;
  create_adapter() runs it exactly when check_fn is False — the platform
  is enabled+configured and the gateway is about to connect it.
- Config enablement keeps a configured platform whose deps are missing
  but installable; the install itself is deferred to create_adapter().
- Status surfaces (_platform_status, hermes status) read only the
  passive probe and can never trigger pip.

Migrated all lazy-installable platform plugins to the split; platforms
with no optional deps (irc/ntfy/buzz/simplex/line/a2a/...) are unchanged
— no ensure_deps_fn means a False check_fn stays a hard block.
wecom_callback gains a working installer for the first time.

Builds on @xxxigm's #79812 (both commits cherry-picked with authorship
preserved), reworking the check_fn swap into the two-field split so the
Teams fix doesn't reintroduce install-on-status.
2026-08-07 13:28:43 +05:30
xxxigm 98408f713b fix(teams): lazy-install SDK via registry check_fn
Platform registry create_adapter() gates on check_fn before the adapter
exists, so wiring the passive probe permanently blocked connect() and
the existing check_teams_requirements() lazy-install never ran.
2026-08-07 13:28:43 +05:30
liuhao1024 358d55051e fix(plugins): use asyncio.wait_for instead of ClientTimeout in Matrix standalone send
Fixes #61495

When manually triggering cron jobs from a live Matrix session, delivery
would fail with "Timeout context manager should be used inside a task"
because the aiohttp.ClientTimeout context manager requires a proper asyncio
task context.

Use asyncio.wait_for() instead of aiohttp.ClientTimeout to avoid this error,
following the same pattern as the Weixin platform (gateway/platforms/weixin.py).

Changes:
- Remove aiohttp.ClientTimeout(total=30) from ClientSession constructor
- Wrap the send operation in a nested async function (_do_send)
- Use asyncio.wait_for(_do_send(), timeout=30) for timeout handling
- Catch asyncio.TimeoutError explicitly and return clear error message
2026-08-06 23:14:55 -07:00
brooklyn! b3e45a3d46
Discord drops an empty outbound message instead of sending it (#78815)
* fix(discord): reject empty outbound messages

* test(discord): cover empty final reply backfill state

Missed-message backfill decides what to replay from discord_messages, so
a dropped final reply must be recorded as failed by the new guard the
same way the exception path records one — otherwise the reply is both
never sent and never retried.

Co-authored-by: Jony <619963502@qq.com>

* chore: map 619963502@qq.com to zyz619963502zyz for PR #73449 salvage

---------

Co-authored-by: Jony <619963502@qq.com>
2026-08-04 18:23:04 +00:00
kshitij e05eba26a3 fix(telegram+sqlite): resolve polling conflict loop + misleading WAL warning
#75017: Telegram polling conflict retry used drop_pending_updates=False,
starting a new getUpdates session that immediately got 409'd by the
previous still-expiring session — creating the very conflict it was
trying to recover from. Switch to drop_pending_updates=True so Telegram
terminates stale sessions. Also add a recovery-generation guard so the
first transient getUpdates success after a retry doesn't reset the
conflict counter back to 0 (defense-in-depth from PR #75096).

#75153: The WAL-reset warning always said 'hermes update can repair'
even for git/pip/system Python installs where it can't. Now uses
detect_install_method() + recommended_update_command_for_method() to
give a context-appropriate hint (hermes update for git, docker pull for
docker, nix message for nix, generic install hint as fallback).
2026-08-04 14:34:50 +05:30
Brin Shadewater e6f1d613b6 fix(discord): leave voice channels before cancelling the bot task
`DiscordAdapter.disconnect()` cancelled the bot task before tearing down voice
clients. `leave_voice_channel()` ends in `await vc.disconnect()`, and discord.py
sends a voice state update over the main gateway websocket and then waits for the
voice socket to close. The bot task is the loop running that gateway connection,
so cancelling it first left the handshake with no transport: it could never
complete and blocked until the caller's shutdown timeout fired.

The effect was a fixed ~5s penalty on every shutdown with a voice connection
open, ending in "discord disconnect timed out after 5.0s - forcing continue",
with the voice disconnect abandoned rather than completed.

Measured on a live gateway with a voice connection open in both cases:

  before: timed out after 5.0s, all adapters disconnected at +5.29s
  after:  discord disconnected (0.12s), all adapters disconnected at +0.46s

Moving the voice-cleanup loop above `_cancel_bot_task()` preserves the
zombie-client protection its comment describes: the bot task is still cancelled
before `client.close()`, just after voice teardown rather than before it. Voice
teardown is the one step that still requires a live gateway.

Adds a regression test asserting the ordering. It fails on the previous ordering
at index 1 with `cancel_bot_task != leave_voice_channel:111`.

Fixes #76044
2026-08-03 22:47:14 +05:30
kshitijk4poor e80b7aeda1 fix(feishu): test SDK globals by None-ness, not globals() membership
The no-SDK fallback guards check '"Name" in globals()' — correct on
main where a failed module-level import leaves those names undefined,
but the deferred-import port pre-binds every SDK name to None, so the
guard was always true and the fallback paths called .builder() on None
(AttributeError) wherever lark_oapi isn't installed. Local runs passed
because lark IS installed here; CI's default env has no feishu extra.
Rewrote all 14 guards to 'is not None', which is correct under both
conditions. Verified by simulating CI with a lark-blocking meta_path
hook: 74 passed, 18 skipped (the skipUnless set), zero failures.
2026-08-03 17:24:11 +05:30
baau b51c4e6a78 fix(feishu): defer the lark_oapi import off the startup path
Salvage of #57657, ported onto the plugin layout (the adapter moved
from gateway/platforms/feishu.py to plugins/platforms/feishu/adapter.py
since the PR's base). lark_oapi takes seconds to import and holds the
GIL doing it; the module-level import made every gateway boot pay that
cost even with Feishu unconfigured.

- _load_lark_oapi() with double-checked locking binds the SDK globals
  on first use; connect() and _standalone_send() call it via
  asyncio.to_thread so the loop never blocks on the import.
- probe_bot() also calls _load_lark_oapi() (sync context) so the SDK
  probe path is preserved rather than silently degrading to the HTTP
  fallback before a first connect.
- check_feishu_requirements() is install-only and no longer rebinds
  globals; test_feishu.py gets a setUpModule that binds them eagerly
  for tests that inject fake clients.

Includes the dedicated lazy-import test file (check-does-not-import,
connect-loads-on-worker-thread).
2026-08-03 17:24:11 +05:30
ErnestHysa 425c54b51e fix(platforms/line): fix broken import of non-existent config functions
_adapter_config_interactive() imported get_env_var and set_env_var from
hermes_cli.config, but these do not exist — the actual functions are
get_env_value and save_env_value. This caused an ImportError at runtime,
breaking the entire LINE platform adapter setup.

Pain before: Any user who ran the LINE adapter setup function would get:
    ImportError: cannot import name 'get_env_var' from 'hermes_cli.config'

Fix: Import the correct functions with aliased local names:
    from hermes_cli.config import get_env_value as _get_env, save_env_value as _set_env

Also fixed an indentation bug introduced during the fix: the 'if value: _set_env()'
block was incorrectly nested inside the except clause.

PR: N32 (hermes-agent audit)
2026-08-03 16:54:02 +05:30
Teknium dd600d1ace fix(discord): suppress link embeds in tool preview markdown links
Wrap the masked-link destination in angle brackets so Discord does not
unfurl an OG-preview embed under every tool progress bubble. quote()
percent-encodes any <> inside the URL itself, so the wrapper cannot be
broken out of.
2026-08-02 21:48:06 -07:00
Coffee☕️ e599f100ec fix(discord): avoid truncated URL link targets 2026-08-02 21:48:06 -07:00