Commit Graph

1193 Commits

Author SHA1 Message Date
kshitijk4poor
7b660e66ee fix(homeassistant): detect the supervised launch through is_supervised_gateway_launch
gateway.restart already answers "was this gateway launched by a generated
service" (HERMES_SUPERVISED_CHILD or launchd's XPC_SERVICE_NAME, so a plist
that predates the marker still counts); the hint reuses it instead of a
second env read. The negative branch (plain unreachable host without a
supervisor) is now an unmarked test so the Linux lane keeps covering the
function; the macOS-only test holds the positive branch.
2026-09-21 17:16:23 +05:30
kshitijk4poor
eda2432cce fix(gateway): scope the Local Network hint and keep the osascript wrapper out of gateway process scans
Review of the first cut:

- The Home Assistant errno hint matched a bare 65 and two error strings on
  every platform. 65 is ENOPKG on Linux and "No route to host" is Linux's
  errno 113, so a systemd gateway (or a Terminal-run gateway on macOS) with a
  genuinely unreachable HA host was told macOS was blocking launchd. Gate on
  darwin + errno.EHOSTUNREACH + the HERMES_SUPERVISED_CHILD marker the
  generated plist already sets; no new env var.
- With a `"` in the home path the wrapper's own ps line tokenized as
  `gateway run`, so `hermes gateway stop` would have signalled osascript
  alongside the gateway. The canonical matcher now bails on an exact
  argv[0] basename of osascript (the gateway is its child and is matched on
  its own command line). Asserted in the existing hostile-path test.
- Log paths spelled once; docstring now says what StandardOutPath still
  carries (osascript's own output) instead of implying it is redundant.
2026-09-21 17:16:23 +05:30
xxxigm
1d871110e1 fix(homeassistant): name the macOS Local Network block behind errno 65 under launchd (#71206)
A launchd-run gateway that macOS Local Network Privacy denies sees every LAN
connect fail with EHOSTUNREACH while the same URL works from Terminal.
Annotate the Home Assistant connect/reconnect log lines with the cause and
the remedy so the failure is actionable instead of a bare "No route to host".

Salvaged from #115196 (remedy text points at the regenerated launchd job).
2026-09-21 17:16:23 +05:30
chelsealong
7efacc8f62 fix(a2a): recover the streamed reply text instead of resolving empty
When streaming already delivered the final reply, gateway/run_turn.py's
_hmwa_deliver_turn_response suppresses the normal adapter.send() and returns
None, so A2AAdapter.send() — the only path that ever carries reply text —
never runs. on_processing_complete() then resolves the pending A2A task
future through its SUCCESS default, which was hardcoded to "", so every
streamed A2A reply lands as TASK_STATE_COMPLETED with no status.message and
no artifacts (#116944).

_hmwa_deliver_turn_response already stashes the true final text on
event._streamed_final_response for exactly this situation (the same stash
_final_text_for_post_turn_hooks reads for /goal and /loop). Read it as the
SUCCESS-path fallback text instead of "".

(cherry picked from commit 638041af046ab149a356a7e5107d52c2e6a3c9a8)
2026-09-20 18:45:42 -07:00
Forkbert
b113ab6de6 fix(photon): use GUID for liveness probe message id
(cherry picked from commit cdb6ccadb045e599db795823a100ae7b61d377ec)
2026-09-20 18:23:28 -07:00
teknium1
fd94fe9b93 fix(gateway): every adapter send_voice accepts the dispatch's is_voice kwarg
The base media dispatch passes is_voice= to send_voice; line, mattermost and
weixin had explicit signatures without it, so a non-image MEDIA attachment
routed as audio raised TypeError and was dropped — the same class as the
Matrix report (#102221, #116776). Adds a repo-wide signature invariant test.
2026-09-20 15:48:51 -07:00
xiaodu
b6505448f1 fix(matrix): accept the media dispatch's is_voice kwarg in send_voice
The base media dispatch calls send_voice(..., is_voice=is_voice) for every
audio MEDIA attachment (gateway/platforms/base.py _send_one). MatrixAdapter
.send_voice() accepted neither is_voice nor **kwargs, so every non-image
MEDIA delivery raised TypeError and the file was silently dropped — the
failure is visible in rotated logs since 2026-09-08 (never worked).

Accept the flag explicitly: is_voice=False -> plain m.audio in the original
format (no transcode); True or omitted (play_audio legacy callers) -> the
existing MSC3245 voice-bubble path with best-effort Ogg/Opus transcode.

Fixes #116776

(cherry picked from commit d4f89a725498e29a2ee0fe016f8bb98db08f358c)
2026-09-20 15:48:51 -07:00
fangliquan
45a701d8b0 fix(simplex): flatten alpha image thumbnails
(cherry picked from commit 97886801741f4367aadfe83ccb25b10cedb21149)
2026-09-20 15:48:01 -07:00
liuhao1024
7a23b00101 fix(whatsapp): refuse legacy-pidfile kills without a start-time fingerprint
The stale-bridge cleanup accepted a "node" + session-path cmdline substring
as kill evidence for legacy pidfiles (pid line only). A log tail, editor, or
grep that merely mentions the session path matches that same substring, so
the cleanup could SIGTERM a stranger process.

Require the kernel start-time fingerprint and fail closed when the pidfile
lacks one; the bridge-port scan (which verifies a node-executable listener)
reaps the orphan instead. The refusal reason in the warning now distinguishes
a fingerprint-less legacy pidfile from a recycled PID.

Flip the legacy-pidfile regression test to assert the fail-closed outcome.

Fixes #116883
2026-09-20 15:24:51 -07:00
teknium1
a57a5f90d2 fix: slot-busy skipped Telegram edits no longer count as shown text (review follow-up)
An interim edit skipped because the chat's shared send+edit slot was busy
returned a plain SendResult(success=True), so the stream consumer recorded
the never-shown text as _last_sent_text and reset _flood_strikes. A later
turn-final flood then saw _visible_prefix() == final text and either marked
the turn delivered or entered fallback with an empty continuation — the user
never saw the tail. The adapter now flags the skip in
raw_response={"skipped": True} and _edit_existing leaves the visible prefix
and flood state untouched, so the next tick retries and a flood fallback
re-sends exactly the unseen tail.
2026-09-20 12:17:23 -07:00
Haisam Abbas
fb2f3e213a fix(telegram): pace sends and edits against a shared per-chat slot
Telegram counts an editMessageText against the same per-chat allowance as a
sendMessage, but streaming previews paced only edits (DEFAULT_STREAMING_EDIT
_INTERVAL = 0.8s = 1.25 msg/s into one chat before any reply was sent) —
83% of measured flood penalties. One shared slot per chat: a send WAITS for
its slot (skipping would drop a message), an interim edit is SKIPPED (the
next tick shows the same text anyway), and the final edit is never gated
(the answer is never withheld). A per-adapter tuning knob keeps the slot
available to tests that model instantaneous bursts.
2026-09-20 12:17:23 -07:00
teknium1
1d6fbd0d1f fix: explicit empty backfill channel list disables the Discord scan (review follow-up)
`missed_message_backfill.channels: []` (YAML list or JSON-list string) returned
an empty set on main, and the backfill logged "no channels configured" and
skipped. The gate-CSV refactor treated an empty result as "unset" and fell
through to the allowed-union-free-response default, scanning channels the
operator had explicitly disabled. An explicit list is now authoritative even
when empty; only the default "" string still falls through to env/default.
2026-09-20 12:10:27 -07:00
teknium1
587bb10575 fix(platforms): Discord/WhatsApp/DingTalk gates honour allowlists stored as a JSON-list string
`hermes config set KEY '["-100","-200"]'` used to write the literal as one quoted
YAML string; the writer now emits a real list (acaac9a18, #88163) and the Telegram
gate decodes the legacy string shape (122ad719, #110213). The Discord, WhatsApp and
DingTalk gate parsers still comma-split that string into `{'["-100"', '"-200"]'}`,
so a config written before the writer fix silently locks every allowlisted chat,
channel or user out — with no warning.

Route every remaining comma-split gate through the shared
`gateway/platforms/_shared.py::decode_json_list_literal`:

- Discord `_gate_csv_set` (allowed/ignored/no-thread channels, allowed users/roles),
  `_discord_free_response_channels` and `_missed_message_backfill_channels` now share
  the one parser instead of three hand-rolled splits.
- WhatsApp `_coerce_allow_list` (allow_from, group_allow_from, free_response_chats).
- DingTalk `_csv_set` (allowed_users, allowed_chats, free_response_chats).

Plain CSV strings, YAML lists and malformed JSON keep their previous meaning.
2026-09-20 12:10:27 -07:00
Steve Hsu
97051e4194 fix(telegram): attach real video geometry and a thumbnail so large uploads aren't square
`sendVideo` gives back `width=320 height=320 duration=0` with no thumbnail once an
upload is large enough that Telegram skips its own video processing, and clients
then draw the message as a square tile — for portrait reels and 16:9 clips alike,
even though the delivered file itself is correct.

Measured on one 6 s 2560x1440 clip, inspecting the Bot API response: 4.9 MB and
9.8 MB keep `2560x1440` / duration 7 / a 320x180 thumbnail; 14.8 MB, 19.4 MB and
23.0 MB degrade to the square placeholder; the same 23.0 MB file sent with
`width`/`height`/`duration` plus a JPEG `thumbnail` comes back `2560x1440` with a
320x180 thumbnail.

Probe the local file with ffprobe and attach a 320px-wide JPEG frame from it, on
both Telegram send paths: the gateway adapter's `send_video` and the standalone
`hermes send` media sender. Both helpers return nothing when ffmpeg/ffprobe is
unavailable, which keeps the previous behaviour for hosts without them.
2026-09-20 11:20:26 -07:00
teknium1
c060a821d5 fix(discord): accept a pasted channel link at the _resolve_channel chokepoint too
HomeChannel normalization covers env and YAML homes, but cron
`deliver: discord:<link>` targets and thread metadata reach the adapter as
written and still died in int(). Share one helper
(gateway.config.discord_channel_id_from_link) between HomeChannel and the
resolver every outbound Discord target passes through; message links and
non-link strings keep their existing error path. Tests trimmed to two
invariants: config load (env + YAML) and the resolver entry point.
2026-09-20 10:49:49 -07:00
chelsealong
f45ed8e4ec docs(matrix): document the <=2-member DM auto-classification and its config bypass
_resolve_room_identity() classifies any room with <=2 joined members as a DM
regardless of m.direct or an explicit room name, so those rooms silently
bypass MATRIX_ALLOWED_ROOMS, MATRIX_FREE_RESPONSE_ROOMS, and
MATRIX_REQUIRE_MENTION, and use DM threading instead of
MATRIX_AUTO_THREAD/MATRIX_SESSION_SCOPE. This was previously only visible in
an inline code comment, not in the env-var docs an operator would read.

Fixes #114733
2026-09-20 10:42:25 -07:00
chelsealong
0c872b611c fix(telegram): point dm_topics prerequisite text at BotFather Threaded Mode
The dm_topics "not a forum" warning and the matching docs section told
users to tap the bot's name in the DM and toggle "Topics" in chat
settings. That toggle only exists for group forums; a bot DM has no
such control. The actual prerequisite is Threaded Mode, enabled by the
bot owner via the BotFather Mini App (Bot Settings -> Threads
Settings) -- already documented correctly a few sections later in the
same file, under the /topic prerequisites.

Fixes #115019
2026-09-20 10:39:37 -07:00
KeyArgo
e7509fa1a4 fix(telegram): observe sibling-bot wake-word messages dropped by the bot-to-bot gate
With `require_mention: true` + `bots_require_mention: true` + wake words in `mention_patterns`, a message
authored by another Hermes bot that addressed this bot by name matched no dispatch path (the
bot-to-bot loop breaker skips it) and was also refused by the observe gate (`mention_patterns`
matches are assumed dispatched), so it vanished from both paths with no log line.

Factor the loop-breaker predicate into `_bot_sender_suppressed` and consult it in
`_should_observe_unmentioned_group_message`, so every message the dispatcher drops because of
`bots_require_mention` is kept as observed context instead of being lost (#115119).
2026-09-20 10:30:11 -07:00
fangliquan
3cbd3cce0a fix(whatsapp): honor configured reply prefix in bridge replies
whatsapp.reply_prefix from config.yaml was written into the bridge env and then
popped again by the WHATSAPP_* passthrough loop (the key was in
_BRIDGE_PASSTHROUGH_ENV and the scoped env lookup came back empty), so bridge.js
always fell back to its built-in header and the documented reply_prefix: ""
could not disable it. Resolve the prefix once (scoped env first, then the
adapter value) and keep it out of the passthrough loop.

Fixes #116059
2026-09-20 10:20:40 -07:00
hardwork9047
a0e7fc4a9e fix(gateway): consult _in_bot_thread in the Discord admission gate
`_discord_message_admission()` drops a message that mentions someone other than
the bot when `DISCORD_IGNORE_NO_MENTION` is on (the default) and the channel is
not free-response. It did so without consulting `_in_bot_thread()`, unlike the
other two ingress paths — `_dispatch_recovered_message()` (adapter.py:2292) and
`_handle_message()` (adapter.py:5946). Admission runs on both and returns
`False` unconditionally, so it overrode the thread exemption they grant.

The result was an asymmetry with no obvious cause from the outside: in a thread
the bot had joined, a message with no mention at all was admitted (an empty
`message.mentions` skips the enclosing block), while the same message with one
mention of a third party was dropped. A thread the bot is a participant in is
the one place "addressed to someone else" is least likely to hold.

`thread_require_mention` still gates multi-bot threads, since that check lives
inside `_in_bot_thread()`.

The drop also emitted nothing at any log level, leaving `gateway.log` identical
whether the gate fired or the event never arrived; add a debug line so the two
can be told apart.

Fixes #116568
2026-09-20 10:20:03 -07:00
kshitijk4poor
de083436bd refactor(gateway): drop Telegram's shadowing _coerce_float_extra; module-level math import
Gate review: TelegramAdapter kept a same-named override with a weaker contract (no finite
guard, negatives clamped instead of reset), so the hierarchy had two parsers under one name.
Both Telegram callers pass explicit bounds; the base method is a strict superset for them.
The cadence test now pins the behaviour (≤ 0.5 s / ≤ 1.0 s) instead of echoing the constant.
2026-09-20 18:14:58 +05:30
kshitijk4poor
530fdb5867 refactor(gateway): one text-batch cadence and one extra-float parser on BasePlatformAdapter
Gate review: the fix left three adapter-private copies of `_coerce_float_extra` and the
0.3/2.0/1.0/4.0 cadence literals in three files. The parser and the cadence constants now
live on BasePlatformAdapter beside the delay attrs they configure; WhatsApp and Weixin call
`_configure_text_batch_delays()`, Telegram reads the same constants through its env helper.
The clamp test is parametrized over both adapters and the Weixin docs name the ceilings.
2026-09-20 18:14:58 +05:30
kshitijk4poor
130b596f6f fix(whatsapp,weixin): text-batch delays default to Telegram cadence
WhatsApp debounced text for 5s (10s near a split) and Weixin for 3s/5s
before dispatching, so every reply paid multiple seconds of idle latency
that Telegram never pays (0.3s/1.0s). Default both adapters to Telegram's
cadence and mirror its ceilings (2.0s / 4.0s, split >= base delay) via the
existing _coerce_float_extra seam. The config keys are unchanged; 0 still
dispatches immediately. Docs updated.

Spotted via #44896 (@liuhao1024). Fixes #44883, refs #25056.
2026-09-20 18:14:58 +05:30
kshitijk4poor
18f1706137 refactor(matrix): drop dead is_direct guard and unreachable sender warning
_schedule_invite_join already requires both is_direct and inviter before recording m.direct, so `is_direct and bool(inviter)` at the reconcile call site was redundant; pass is_direct through. The `if is_direct and not inviter` WARNING could only be reached with GATEWAY_ALLOW_ALL_USERS set and a spec-violating stripped m.room.member event lacking `sender`; the info log already prints is_direct, so drop the branch.
2026-09-20 18:00:06 +05:30
kshitijk4poor
b98dd99142 docs(matrix): reconcile gate comment states the real premise
The adapter comment and the test module docstring claimed a reconciled pending invite "never fires _on_invite". It does: _absorb_sync runs _dispatch_sync (which emits INVITE to _on_invite) and then the reconcile pass over rooms.invite, which joined every entry unconditionally — so a live invite _on_invite rejected was joined ms later, and invites that arrived while the gateway was down were joined on restart with no gate. Reword both to state that premise.
2026-09-20 18:00:06 +05:30
Iain Lane
439f2b1cf6 fix(matrix): apply the inviter allowlist to reconciled pending invites
_on_invite only auto-joins a room when the inviter is allow-listed (or
GATEWAY_ALLOW_ALL_USERS is set), so a live invite from an arbitrary
federated user is rejected. A pending invite that arrives while the
gateway is down takes a different path: _schedule_pending_invite_joins
reconciles it from rooms.invite in the sync response and scheduled the
join unconditionally. An unauthorized invite sent during downtime was
therefore auto-joined on restart, bypassing the allowlist.

Extract the gate from _on_invite into _is_authorized_inviter and apply
it during reconciliation too, reading the inviter from the stripped
invite state (the sender of the m.room.member event for our own user,
as _extract_invite_dm_signal already does for the DM signal). An
inviter that cannot be read from the invite state fails closed, exactly
like an empty sender in _on_invite: the invite is skipped with a
warning and left pending.
2026-09-20 18:00:06 +05:30
Iain Lane
0ce3e7b12b fix(matrix): record reconciled direct invites in m.direct
A direct invite that arrives while the gateway is running fires
_on_invite, which passes is_direct and the inviter through
_schedule_invite_join so the room is recorded in m.direct after the
join. An invite that is still pending across a gateway restart takes a
different path: _schedule_pending_invite_joins reconciles it from
rooms.invite in the sync response, but called _schedule_invite_join
without is_direct or inviter. The DM signal was dropped, the room was
never recorded in m.direct, and it was classified as a group until the
user's own client happened to update m.direct.

Read the signal from the stripped invite state instead: the
m.room.member event for our own user carries the original invite's
is_direct flag, and its sender is the inviter. Thread both through to
_schedule_invite_join so a reconciled direct invite is recorded in
m.direct, and thus lands in _dm_rooms, exactly like a live one.

This gap was surfaced by the triage of #62493.
2026-09-20 18:00:06 +05:30
kshitijk4poor
536ce859c4 fix(telegram): bounded sends do not arm the blocked-loop watchdog 2026-09-20 17:57:43 +05:30
kshitijk4poor
d37f59af4c fix(telegram): deadline label default no longer claims init at non-init sites 2026-09-20 17:57:43 +05:30
kshitijk4poor
e2a54239fc docs(telegram): deadline comment names what is bounded 2026-09-20 17:57:43 +05:30
kshitijk4poor
85e673b4a5 fix(telegram): give media uploads their own 300 s wall-clock deadline
Reusing `_MEDIA_SEND_READ_TIMEOUT` (60 s) as the whole-call deadline turned
httpx's per-phase stall budget into a bandwidth cap: a 20 MB video on a
~2 Mbit/s uplink (~80 s) that succeeds today would fail. `_MEDIA_SEND_DEADLINE`
= 300 s covers the 50 MB Bot API cap at ~2 Mbit/s plus connect and sendVideo
transcoding, and is >2x the summed httpx budgets (pool 8 + connect 10 +
media_write 60 + read 60 = 138 s), so it only fires on a socket that has
stopped raising. Abandon-inside-lock semantics documented at the constant.
2026-09-20 17:57:43 +05:30
jollyroger1480
04057250a3 fix(telegram): wall-clock deadline on every Bot API send (text + media)
No Bot-API write in the adapter was under a wall-clock cap — text sends,
edits, drafts and media uploads relied solely on httpx socket timeouts,
which do not fire when a shielded httpcore socket wedges (same class as the
getUpdates hang in #92991). A stuck send then pinned `_chat_send_lock` and
the loop. Wrap every send/edit/draft in `_await_with_thread_deadline`
(`_TEXT_SEND_DEADLINE`) and both media paths in
`_send_with_dm_topic_reply_anchor_retry`; the helper gains a `label` and a
descriptive TimeoutError message.

Half A of #115280; Half B superseded by #116134 / contradicts #75017.
2026-09-20 17:57:43 +05:30
kshitijk4poor
6d8badcc6e refactor(telegram): share the cold-boot queue decision and log it
The cold-boot queue policy was computed inline at both start paths, and the
default (drop) was silent — a command sent while the gateway was offline
vanished with no log line (the second half of #71811).

One helper now owns the decision and logs it on every cold boot, so an
operator can tell which policy applied. Behaviour is unchanged.
2026-09-20 00:19:28 +05:30
justcarlosm
a4a0447b04 feat(telegram): drop_pending_on_cold_boot knob to preserve offline queue 2026-09-20 00:19:28 +05:30
teknium1
4586cde64d chore: mark the deliberate /tmp literals and shrink the lint baseline to one code block
The seventeen remaining literals are container-side paths, AF_UNIX socket-path-limit
candidates on darwin, detection needles, guard regexes and guidance text that tells the
model to avoid /tmp. Each carries an inline `no-tmp: ok — <why>` so the reason lives
next to the line; the baseline keeps only a fenced tree listing where a marker would render.
2026-09-19 10:44:26 -07:00
Chinmayrawat15
1447c83b46 fix(acp): resolve reasoning_config for ACP and Feishu comment agents
`SessionManager._make_agent` and the Feishu doc-comment agent built their
`AIAgent` without `reasoning_config`, so `agent.reasoning_effort: none`
never reached those sessions: the transport applied its default effort,
which non-reasoning models such as gpt-4o-mini reject with HTTP 400 and
which silently re-enables thinking everywhere else. Both surfaces now go
through `hermes_constants.resolve_reasoning_config`, the same chokepoint
the CLI, gateway, TUI, cron and `hermes -p` already use, resolved against
the model the session actually runs so per-model overrides apply.

Ported from PR #85164 by @Chinmayrawat15 (oneshot hunk already on main).

Fixes #85153
2026-09-19 09:39:50 -07:00
kshitijk4poor
0c7738dfd6 refactor(discord): read the free-response auto-thread flag through a getter
Mirrors the _discord_require_mention()/_discord_max_attachment_bytes() shape, makes the
lookup lazy (it only runs on the free-channel path now), and keeps the in-code key
manifests in _handle_message and register() listing the new key.
2026-09-19 21:43:59 +05:30
Xunjin ZHENG
774e070731 feat(discord): add free_response_auto_thread opt-in
Free-response channels skip auto-threading by default so the bot replies
inline (lightweight chat mode). This prevented users who wanted BOTH
mention-free replies AND per-conversation threads from getting either.

Add a new opt-in `discord.free_response_auto_thread` (env:
`DISCORD_FREE_RESPONSE_AUTO_THREAD`, default false) that, when true,
re-enables auto-threading in free-response channels. Voice-linked
channels continue to skip auto-thread regardless, and the flag is
gated behind the global `DISCORD_AUTO_THREAD=true`.

Default behavior is unchanged; all 291 existing discord tests pass.
2026-09-19 21:43:59 +05:30
teknium1
1654575c6c fix(gateway): canonicalize identity first at every adapter ingress path
Every adapter-side lane (text/photo/album batch dicts, _active_sessions, the
busy guard, /stop /new /reset and clarify replies) derived its session key
before the receiving bot's identity was known, so two bots seeing the same
Telegram chat.id == user.id collided on one agent:main lane and a route to an
unserved profile still reached the default lane's running task.

BasePlatformAdapter._canonicalize pins the RoutingIdentity (via the new
session_identity.canonical_identity seam) as the FIRST statement of
handle_message, _enqueue_text_event, _handle_message_while_active, Telegram
_route_photo_event and every _source_session_key; _drop_unresolved drops a
rejected route at the first seam with one WARNING. Topic recovery copies the
source through replace_source so the identity travels; Slack thread sources go
through build_source so the thread key carries the same provenance.
2026-09-19 00:30:29 -07:00
teknium1
c07708671d fix(gateway): every adapter session key goes through one seam (+ lint)
A secondary-owned Yuanbao bot keyed its per-group dispatch queue and RecallGuard
entries with the free `build_session_key(source)` — no profile, so `agent:main:` —
while `handle_message` popped under `agent:<owner>:`. Two derivations of one
identity: the group queue was shared across bots and the RecallGuard entries
leaked. Weixin, Telegram's photo batch, Slack's thread key and Raft's wake key
each carried their own copy of the call as well.

Every adapter-side key now comes from `BasePlatformAdapter._source_session_key`
/ `_event_session_key` (owner namespace, runner-seeded isolation flags, and —
after the RoutingIdentity PR — the pinned identity). Weixin's `_text_batch_key`
override is deleted (the base does the same). Slack's thread key reads the
isolation flags from the adapter config the runner seeds, not the store's.

Lint: pattern P32 in `scripts/ci/profile_scope_patterns.json` flags
`build_session_key(` / `SessionSource(` under `gateway/platforms/**` and
`plugins/platforms/**` except `platforms/base.py`; the checker gains an optional
`path_regex` per pattern. Advisory, like every other pattern.

Phase 2 of #88715.
2026-09-18 22:04:43 -07:00
LeonSGP43
f59c451f81 fix(feishu): avoid threading regular replies 2026-09-19 03:31:00 +05:30
kshitijk4poor
35b38f1f4e fix(whatsapp): bridge gets the adapter's resolved group policy and group allowlist; one allowlist reader
The bridge now gates groups by WHATSAPP_GROUP_POLICY / WHATSAPP_GROUP_ALLOWED_USERS
(#73465), but only the DM policy and DM allowlist were exported from the adapter's
resolved config — a YAML group_allow_from never reached the bridge and, under a
multiplexed gateway, the launch process's WHATSAPP_GROUP_* values did. _bridge_env
exports both like it already did for the DM pair, so unlisted-group traffic is cut
before media download instead of after the POST to Python.

whatsapp_common._select_allowlist generalises the DM precedence reader (config key
presence wins, an explicit empty list stays authoritative, then the first truthy env
carrier); the adapter's group list and the Cloud sibling's group list use it instead
of hand-rolled `or` chains, so `group_allow_from: []` means the same everywhere.

Tests: bridge env carries group policy + group allowlist from the secondary profile's
YAML (scoped precedence file); env fallback test trimmed to the two invariants (red on
the merge base); bare test adapters seed _group_policy/_group_allow_from, which
_bridge_env now reads.
2026-09-19 03:15:40 +05:30
sal
477afd2420 fix(whatsapp): adapter reads WHATSAPP_GROUP_ALLOWED_USERS for the group allowlist (#72529)
The Node bridge receives WHATSAPP_GROUP_ALLOWED_USERS via
_BRIDGE_PASSTHROUGH_ENV and the YAML bridge maps config.yaml
whatsapp.group_allow_from onto the same env carrier, but
WhatsAppAdapter.__init__ seeded _group_allow_from from config.extra
alone — an install that set the group allowlist only through the
documented env var silently ran group gating with an empty allowlist:
with group_policy=allowlist every group chat was rejected at intake
(the second failure mode triaged on #72529, the half with 'no PR yet').

Mirror the DM path's precedence: config keys win by key presence
(explicit empty list stays authoritative), then the profile-scoped
WHATSAPP_GROUP_ALLOW_FROM / WHATSAPP_GROUP_ALLOWED_USERS env CSV —
multiplex-safe through _wenv, like every other WHATSAPP_* read.

Tests pin the full precedence matrix: env-only seeding, the
allow_from-style alias, config-wins-over-env, the legacy groupAllowFrom
key, empty-by-default, the scoped .env read under multiplex, and the
group-policy gating outcome for an env-seeded allowlist.
2026-09-19 03:15:40 +05:30
kshitijk4poor
8503ee4459 refactor(send): route WhatsApp mentions through the existing standalone chunker
Follow-up to the salvaged #92440 commit, shape-gate cleanup only; behaviour is unchanged
(one mention-bearing payload per logical send, captioned media keeps its caption).

- Fold `_send_whatsapp_with_mentions` into `_send_plugin_standalone` (it was a line-for-line
  copy of the caption split + `_send_chunks` loop); `mentions` is attached to the first
  payload only via a one-shot kwarg dict.
- Drop the `inspect.signature(sender)` probe: the only registered WhatsApp standalone sender
  is the in-tree `_standalone_send`, which gains `mentions` in the same change; a foreign
  sender already surfaces as a TypeError through `_handle_send`'s error path.
- Collapse `_normalize_outbound_mentions` to dedupe-only; its input is argparse `list[str]`
  already validated by the CLI.
- Revert the `\d` -> `[0-9]` edit to `_BARE_PHONE_RE` / `to_whatsapp_jid`: unrelated to the
  feature (`normalize_whatsapp_mention_jid` already rejects non-ASCII via `isascii()`) and it
  changed output for nine other `to_whatsapp_jid` callers.
- Split the single 137-line test into a `whatsapp_bridge` fixture + two invariants
  (rejections never reach the bridge; mentions ride the first payload only, stale bridge
  fails closed); drop the fabricated legacy-sender branch that only existed to cover the
  deleted probe. Mutation check: removing the first-payload gate turns the new test red.
2026-09-19 00:24:50 +05:30
Andrey
b34ebc084a feat(send): add WhatsApp native mentions
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>
(cherry picked from commit ba7fd43826f9e36d886cc294e072378d5d082aa5)
2026-09-19 00:24:50 +05:30
teknium1
595f3a289c fix(telegram): split replies resume from the refused chunk, never re-send the head; per-chat send order + flood cooldown
A reply past 4,096 chars goes out as several sendMessage calls. When chunk 2 was
refused by flood control (RetryAfter past the 5s inline cap) send() returned the
bare flood_control result, so _send_with_retry re-sent the WHOLE payload after
the wait: the user saw chunk 1 twice (reporter: 4 messages, 686 duplicated words),
and paths without a ledger row lost the tail outright.

- send() now reports a mid-split refusal through the existing partial_overflow
  contract (the key _edit_overflow_split already sets and the stream consumer
  reads): delivered_chunks / total_chunks / last_message_id, plus
  undelivered_chunks + delivered_message_ids ONLY when non-delivery is certain
  (flood cap, Bot API rejection, connect/pool timeout) — an ambiguous TimedOut may
  have reached Telegram and is never resumed from.
- BasePlatformAdapter._send_with_retry resumes from the remainder via a new
  _resume_partial_send hook (default None = keep the partial failure, never
  re-send the head; the plain-text fallback is skipped for partials too). The
  Telegram override sends the leftover formatted chunks, continuing the id sequence.
- Per-chat FIFO send gate, reentrant per asyncio task (media paths nest), held
  only around the API calls — never across the reconnect wait — on send() and the
  media funnel, so concurrent replies to one chat no longer interleave chunks.
- A flood refusal arms a per-chat cooldown (mirrors the sendChatAction cooldown,
  capped 300s); sends inside the window fail closed locally with the same
  flood_control:<s> result and no API call, so ledger recognition and redelivery
  timing are unchanged.
- Edit path: log "refusing (retry_after Ns > cap)" after the cap check instead of
  "waiting Ns" followed by no wait.

Live against a local fake Telegram Bot API with a fake token (RetryAfter=7 on
chunk 2 of a 3-chunk reply): before 4 messages / 324 duplicated words; after 3
messages, 853/853 words, 0 duplicated, 0 lost. Two concurrent 3-chunk sends:
before 9 source switches, after 1. Five sends inside a refused window: before 5
API calls, after 0.

Fixes #114396

Co-authored-by: AStrnbrg <45151087+AStrnbrg@users.noreply.github.com>
Co-authored-by: whyyagswhy <166958865+whyyagswhy@users.noreply.github.com>
2026-09-18 10:17:23 -07:00
teknium1
7f2b64f7a2 fix(platforms): set media_text_inlined in the sibling document paths
The Feishu fix on this branch populates MessageEvent.media_text_inlined so
run_inbound's document note stops claiming "Its content has been included
below" when a text attachment was NOT inlined (>100 KB gate or decode
failure); run_inbound treats a missing flag as inlined. Telegram, Discord,
Slack, the WhatsApp bridge adapter and whatsapp_cloud inline "[Content of
…]" the same way but never set the flag, so their notes lied on the
skip path. Mirror the Feishu/buzz per-attachment contract in each: False
for every cached attachment, flipped to True only when the text was
actually injected.

One parametrized (small→True / large→False) test per adapter in the
existing per-platform test files.
2026-09-18 10:14:38 -07:00
fangliquan
51fa2d51a2 fix(feishu): retain attachments in text batches 2026-09-18 10:14:38 -07:00
fangliquan
2e58dbc9c5 fix(feishu): retain attachment inline flags in batches 2026-09-18 10:14:38 -07:00
fangliquan
29490a0e66 fix(feishu): preserve post file attachments 2026-09-18 10:14:38 -07:00