* feat(telegram): support custom apiRoot for alternative API endpoints
Add `apiRoot` config option to allow users to specify custom Telegram Bot
API endpoints (e.g., self-hosted Bot API servers). Threads the configured
base URL through all Telegram API call sites: bot creation, send, probe,
audit, media download, and api-fetch. Extends SSRF policy to dynamically
trust custom apiRoot hostname for media downloads.
Closes#28535
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(telegram): thread apiRoot through allowFrom lookups
* fix(telegram): honor lookup transport and local file paths
* refactor(telegram): unify username lookup plumbing
* fix(telegram): restore doctor lookup imports
* fix: document Telegram apiRoot support (#48842) (thanks @Cypherm)
---------
Co-authored-by: Cypherm <28184436+Cypherm@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* test: align extension runtime mocks with plugin-sdk
Update stale extension tests to mock the plugin-sdk runtime barrels that production code now imports, and harden the Signal tool-result harness around system-event assertions so the channels lane matches current extension boundaries.
Regeneration-Prompt: |
Verify the failing channels-lane tests against current origin/main in an isolated worktree before changing anything. If the failures reproduce on main, keep the fix test-only unless production behavior is clearly wrong. Recent extension refactors moved Telegram, WhatsApp, and Signal code onto plugin-sdk runtime barrels, so update stale tests that still mock old core module paths to intercept the seams production code now uses. For Signal reaction notifications, avoid brittle assertions that depend on shared queued system-event state when a direct harness spy on enqueue behavior is sufficient. Preserve scope: only touch the failing tests and their local harness, then rerun the reproduced targeted tests plus the full channels lane and repo check gate.
* test: fix extension test drift on main
* fix: lazy-load bundled web search plugin registry
* test: make matrix sweeper failure injection portable
* fix: split heavy matrix runtime-api seams
* fix: simplify bundled web search id lookup
* test: tolerate windows env key casing
Reuse pi-ai's Anthropic client injection seam for streaming, and add
the OpenClaw-side provider discovery, auth, model catalog, and tests
needed to expose anthropic-vertex cleanly.
Signed-off-by: sallyom <somalley@redhat.com>
When a non-default accountId is specified but not found in the accounts
config, resolveTelegramToken() falls through to channel-level defaults
(botToken, tokenFile, env) — silently routing messages via the wrong
bot's token. This is a cross-bot message leak with no error or warning.
Root cause: extensions/telegram/src/token.ts:44-46, resolveAccountCfg()
returns undefined for unknown accountIds but code continues to fallbacks.
Introduced in e5bca0832f when Telegram moved to extensions/.
Fix: return { token: "", source: "none" } with a diagnostic log when
a non-default accountId is not found. Existing behavior for known
accounts (with or without per-account tokens) preserved.
Test: added "does not fall through when non-default accountId not in
config" — 1/1 new, 10/10 existing unaffected.
Closes#49383
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: HCL <chenglunhu@gmail.com>
[[reply_to_current]] sets payload.replyToId to the child-post id inside the thread,
not the thread root. The raw !== comparison triggered false divergence: child-post !=
thread-root, even though they both resolve to the same Mattermost thread.
Fix: compute baseReplyToId via resolveMattermostReplyRootId without the payload, then
compare finalReplyToId against baseReplyToId. Both paths normalize child-post ids to
the thread root before comparison, so [[reply_to_current]] and explicit child-post
targets no longer falsely trigger the divergent-target path.
Fixes#35822 — Bot Framework conversation.id format is incompatible with
Graph API /chats/{chatId}. Added resolveGraphChatId() to look up the
Graph-native chat ID via GET /me/chats, cached in the conversation store.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add updateActivity/deleteActivity to MSTeamsAdapter
- Add onReactionsAdded/onReactionsRemoved to MSTeamsActivityHandler
- Implement directory self() to return bot identity from appId credential
- Add tests for self() in channel.directory.test.ts
When a route-level (teams/channel) allowlist was configured but the sender
allowlist (allowFrom/groupAllowFrom) was empty, resolveSenderScopedGroupPolicy
would downgrade the effective group policy from "allowlist" to "open", allowing
any Teams user to interact with the bot.
The fix: when channelGate.allowlistConfigured is true and effectiveGroupAllowFrom
is empty, preserve the configured groupPolicy ("allowlist") rather than letting
it be downgraded to "open". This ensures an empty sender allowlist with an active
route allowlist means deny-all rather than allow-all.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
resolveMattermostReplyRootId always returns threadRootId when it is set, so
comparing finalReplyToId against effectiveReplyToId was always false when the
streaming preview was created in a thread. Explicit reply directives like
[[reply_to:...]] could therefore never trigger the divergent-target path.
Fix: compare payload.replyToId directly against effectiveReplyToId instead of
going through the resolver. The resolver is still used to compute finalReplyToId
for the actual delivery call.
flushPendingPatch and the divergent-target branch were still using a 2-second
busy-wait on patchSending, which has the same race as the onSettled wait that was
already fixed: patchSending clears in the finally block before the network request
actually settles. Both paths now await patchInflight directly. (ID=2965256849)
Patching preview text then delivering media separately splits a captioned-file
reply into two posts: a text-only preview + captionless file attachment.
New logic in the isFinal branch:
- Text-only payload: patch in place as before (no change for common case)
- Media payload: skip the patch, reset state, deliver full payload via
deliverMattermostReplyPayload (text+media together), then delete preview.
- Patch failure: same fallback as media payload — full delivery + delete.
After a successful patchMattermostPost in the isFinal branch, the code returned
immediately without delivering any media attachments. deliverMattermostReplyPayload
is the only path that uploads/sends media, so caption+image/file/audio payloads
were silently dropping the attachment whenever streaming was active and the patch
succeeded.
Fix: after a successful patch, check whether the payload has mediaUrls/mediaUrl.
If so, call deliverMattermostReplyPayload with text=undefined to deliver only the
media through the normal attachment path.
Add patchInflight Promise tracking to P4 (feat/mattermost-block-streaming-rebased),
mirroring the existing P5 approach. The onSettled cleanup previously used a 3-second
busy-wait on patchSending, which would race on slow Mattermost links: if the first
preview POST takes longer than 3s the cleanup exits early, patchSending is forced false,
and when the POST later resolves it creates an orphan post that is never deleted.
Fix: track the interval tick's async function as patchInflight. onSettled awaits it
directly so the cleanup always captures the final streamMessageId, regardless of how
long the POST takes. (Codex ID=2964616785)
Two fixes:
1. Failure latch (Codex ID=2964357928): add previewSendFailed boolean that is set true
in both the initial-send and patch-edit catch blocks (alongside stopPatchInterval).
schedulePatch() checks it before re-arming the interval, so subsequent onPartialReply
calls during a run with a permanent failure (missing permission, DM-creation error)
no longer recreate the timer and retry indefinitely.
2. patchInterval in onSettled guard (Codex ID=2964357925): onSettled now triggers
cleanup when patchInterval is non-null, even if streamMessageId and patchSending
are still falsy. This covers the window between schedulePatch arming the interval
and the first 200ms tick flipping patchSending — if the run ends in that window
(same-target messaging-tool sends, empty/heartbeat replies), the interval is now
stopped and the pending state is cleared.
When the first preview POST is still in flight (patchSending=true, streamMessageId=null),
the previous onSettled check was skipped entirely — the POST would resolve after cleanup
and leave an orphaned preview post with no interval to clear it.
Fix: trigger cleanup when either streamMessageId is set OR patchSending is true.
Stop the interval immediately, clear pending state, then wait up to 3s for patchSending
to clear before capturing the final streamMessageId and deleting the post.
Two fixes:
1. onSettled orphan cleanup (Codex ID=2963834802): add cleanup in the streaming
dispatcher's onSettled callback for cases where the reply pipeline produces no
final payload — e.g. messaging-tool sends suppressed by agent-runner-payloads.ts,
or empty/heartbeat responses. Without this, onPartialReply could create a preview
post that is never finalized or deleted. The cleanup mirrors the existing logic in
#43041 (P5).
2. Initial-send retry storm (Codex ID=2963834806): call stopPatchInterval() in the
sendMessageMattermost catch block, mirroring the existing fix for patchMattermostPost
failures. Without this, a failed initial post attempt (missing post permission, DM
creation failure, etc.) causes the 200ms interval to retry indefinitely for the rest
of the response, flooding the API and gateway logs.