2026-02-15 20:43:53 +00:00
import type { Bot } from "grammy" ;
2026-02-16 00:39:59 +00:00
import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
2026-02-14 21:08:29 +00:00
import {
getTelegramSendTestMocks ,
importTelegramSendModule ,
installTelegramSendTestHooks ,
} from "./send.test-harness.js" ;
2026-02-16 00:39:59 +00:00
import { clearSentMessageCache , recordSentMessage , wasSentByBot } from "./sent-message-cache.js" ;
2026-01-06 02:22:09 +00:00
2026-02-14 21:08:29 +00:00
installTelegramSendTestHooks ( ) ;
2026-01-08 04:40:29 +01:00
2026-02-14 21:08:29 +00:00
const { botApi , botCtorSpy , loadConfig , loadWebMedia } = getTelegramSendTestMocks ( ) ;
2026-02-15 20:43:53 +00:00
const {
buildInlineKeyboard ,
feat(telegram): add forum topic creation support (#17035)
* Revert "fix(gateway): set explicit chat timeouts for mesh gateway calls"
This reverts commit c529e6005a3e9e112883d86a973abc1e59e22e1d.
* Revert "fix: capture init script exit codes instead of swallowing via pipe"
This reverts commit 8b14052ebe9d0ced947de214e2ac20ed384013c3.
* Revert "feat(docker): add init script support via /openclaw-init.d/"
This reverts commit 53af9f743767e7ffca3c95b572a7d5451cfc9e4a.
* Revert "Agents: improve Windows scaffold helpers for venture studio"
This reverts commit b6d934c2c7da1276ce5a001ef7f1d16773918f0c.
* chore: Fix types in tests 1/N.
* chore: Fix types in tests 2/N.
* Revert "fix: remove stderr suppression so install failures are visible in build logs"
This reverts commit 717caa97fbe023a0d2f14b8e2cbad456954d6d2c.
* Revert "fix(docker): ensure memory-lancedb deps installed in Docker image"
This reverts commit 2ab6313d99d5c794f2ebcac30534533b6b26755f.
* Revert "fix: add windowsHide: true to spawn in runCommandWithTimeout"
This reverts commit 32c66aff49fcdb295484dd94f56c9c8a8f0d9482.
* Revert "Onboarding: fix webchat URL loopback and canonical session"
This reverts commit 59e0e7e4ffeb6853e4be24f3c25c5ceb1a212f2e.
* Revert "feat(linq): add interactive onboarding adapter"
This reverts commit b91e43714b4e937f33f1c63fe781eb98e20d8756.
* Revert "feat: add Linq channel — real iMessage via API, no Mac required"
This reverts commit d4a142fd8f1cc2e2d5f634c919a20cc9bf864475.
* docs: clarify discord proxy scope for startup REST calls
* Revert "fix: flatten remaining anyOf/oneOf in Gemini schema cleaning"
This reverts commit 06b961b0371f1dbea621c73d25951b3b44113941.
* Revert "fix: session-memory hook finds previous session file after /new/reset"
This reverts commit d6acd71576cda5a319abbcc244614eb5be91045d.
* Revert "fix: respect OPENCLAW_HOME for isolated gateway instances"
This reverts commit 34b18ea9db116730de38eab19c74492518b8567d.
* fix(process): harden graceful kill-tree cancellation semantics
* fix(slack): scope attachment extraction to forwarded shares
* docs(changelog): note process kill-tree hotfix
* docs(changelog): note slack forwarded attachment hotfix
* fix(session-memory): harden reset transcript recovery
* revert(telegram): undo accidental merge of PR #18601
* fix(ui): preserve locale bootstrap and trusted-proxy overview behavior
* fix(scripts): harden Windows UI spawn behavior
* fix(slack): validate interaction payloads and handle malformed actions
* fix(mattermost): harden react remove flag parsing
* docs(changelog): record PR 18608 fixups
* fix(heartbeat): bound responsePrefix strip for ack detection
* chore: Fix types in tests 3/N.
* chore: chore: Fix types in tests 4/N.
* chore: Fix types in tests 5/N.
* chore: Fix types in tests 6/N.
* chore: Format files.
* chore: Fix types that were broken due to reverts.
* chore: Cleanup unused vars that were leftover from the reverts.
* fix(actions): layer per-account gate fallback
* fix(subagents): pass group context in /subagents spawn
* fix(failover): align abort timeout detection and regressions
* fix(models): sync auth-profiles before availability checks
* fix(ui): correct usage range totals and muted styles
* Revert "feat: show transcript file size in session status"
This reverts commit 15dd2cda209ccabc9febc25e16eec620137ae744.
* revert(doctor): undo accidental merge of PR #18591
* fix(agents): align session lock hold budget with run timeouts
* Revert "fix: resolve #12770 - update Antigravity default model and trim leading whitespace in BlueBubbles replies"
This reverts commit e179d453c7d8aa3ffdc2dbd67fa5f5fbf93aa016.
* revert(tools): undo accidental merge of PR #18584
* revert(tools): finish rollback of PR #18584
* chore: Fix Slack test.
* revert: remove accidentally merged video-quote-finder skill (#18550)
* revert: accidental merge of OC-09 sandbox env sanitization change
* fix(doctor): move forced exit to top-level command
* chore: Fix types in tests 7/N.
* chore: Fix types in tests 8/N.
* chore: Fix types in tests 9/N.
* chore: Fix types in tests 10/N.
* chore: Fix types in tests 11/N.
* chore: chore: Fix types in tests 12/N.
* chore: Fix type errors from reverts.
* fix(gateway): remove watch-mode build/start race (#18782)
* fix(doctor): repair googlechat open dm wildcard auto-fix
* test(extensions): cast fetch mocks to satisfy tsgo
* fix(gateway): harden channel health monitor recovery
* fix(reply): track messaging media aliases for dedupe
* refactor(plugins): split before-agent hooks by model and prompt phases
* revert(telegram): undo accidental merge of PR #18564
* fix(agents): restore multi-image image tool schema contract
* chore: Format files.
* fix(ui): gate sessions refresh on successful delete
* revert(docs): undo accidental merge of #18516
* revert(exec): undo accidental merge of PR #18521
* docs(cron): clarify webhook posting summary condition
* fix(gateway): preserve chat.history context under hard caps
* chore: Fix types in tests 13/N.
* chore: Fix types in tests 14/N.
* chore: Fix types in tests 15/N.
* chore: Fix types in tests 16/N.
* chore: Fix types in tests 17/N.
* chore: Fix types in tests 18/N.
* chore: Format files.
* revert(sandbox): revert SHA-1 slug restoration
* test(session): cover stale threadId fallback
* test(status): cover token summary variants
* test(telegram): cover getFile file-too-big errors
* test(voice-call): cover stream disconnect auto-end
* chore(format): fix test import order
* test(agents): cover tool result media placeholders
* chore: chore: Fix types in tests 19/N.
* chore: Fix types in tests 20/N.
* chore: Fix types in tests 21/N.
* chore: Fix types in tests 22/N.
* chore: Fix types in tests 23/N.
* docs(voice-call): document stale call reaper config
* fix(doctor): audit env-only gateway tokens
* fix(sessions): purge deleted transcript archives
* test(docker): cover browser install build arg
* revert(gateway): restore loopback auth setup
* revert(voice-call): undo cached greeting note
* revert(voice-call): undo oxfmt formatting
* revert(voice-call): undo oxfmt formatting pass
* revert(voice-call): remove cached inbound greeting
* test: stabilize infra tests
* fix(subagents): harden announce retry guards
* Revert "fix(whatsapp): allow per-message link preview override\n\nWhatsApp messages default to enabling link previews for URLs. This adds\nsupport for overriding this behavior per-message via the \nparameter (e.g. from tool options), consistent with Telegram.\n\nFix: Updated internal WhatsApp Web API layers to pass option\ndown to Baileys ."
This reverts commit 1bef2fc68bc512c886c25ef1dfba67f58c90e811.
* fix(telegram): clear offsets on token change
* test(agents): cover exec non-zero exits
* CI: use self-hosted for labeler/automation
* Revert "channels: migrate extension account listing to factory"
This reverts commit d24340d75bf2b8e69a60f8c122ea59eaec9b0833.
* chore(format)
* chore: wtf.
* chore: Fix types.
* chore: Fix types in tests 24/N.
* chore: Fix types in tests 25/N.
* chore: Fix types in tests 26/N.
* chore: Fix types in tests 27/N.
* chore: Fix types in tests 28/N.
* chore: Fix types in tests 29/N.
* chore: Fix types in tests 30/N.
* chore: Fix types in tests 31/N.
* chore: Fix types in tests 32/N.
* fix(telegram): add initial message debounce for better push notifications (#18147)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 5e2285b6a03943a483993b540f86a0fa49d7de39
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* style(telegram): format dispatch files
* chore: Fix types in tests 33/N.
* chore: Fix types in tests 34/N.
* chore: Fix types in tests 35/N.
* chore: Fix types in tests 36/N.
* chore: Fix types in tests 37/N.
* chore: Fix types in tests 38/N.
* chore: Fix types in tests 39/N.
* chore: Fix types in tests 40/N.
* chore: Fix types in tests 41/N.
* chore: Fix types in tests 42/N.
* chore: Fix types in tests 43/N.
* chore: Fix types in tests 44/N.
* chore: Fix types in tests 45/N.
* chore: Typecheck tests.
* chore: Fix broken test.
* chore: Fix hanging test.
* fix(telegram): avoid duplicate preview bubbles in partial stream mode (#18956)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: cf4eca71d46e0c5ef1ec46af90f978b3d454c34a
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* fix: before_tool_call hook double-fires with abort signal (#16852)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 6269d617f3ac811e03cd29d915f94657da922ba1
Co-authored-by: sreuter <550246+sreuter@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* Revert "Default Telegram polls to public"
This reverts commit c43e95e011cdbdd3be9446b12d257899b4a90201.
* Revert "Fix Telegram poll action wiring"
This reverts commit 556b531a140330540a10299cd6c4907750a2c0b6.
* Revert "Add Telegram polls action to config typing"
This reverts commit 5cbfaf5cc70d8d69a39d640eea147d70589eeaaf.
* Revert "fix(telegram): wire sendPollTelegram into channel action handler (#16977)"
This reverts commit 7bb9a7dcfc3194a953f0ca20f86f9caba8a22923.
* CI: remove formal models conformance workflow (#19007)
* fix: preserve telegram dm topic thread ids
* style: drop aidev-note prefix in telegram comments
* test: pass extensionContext in abort dedupe e2e
* fix: align tool execute arg parsing for hooks
* test: type telegram action mock passthrough args
* Configure: make model picker allowlist searchable
* Configure: improve searchable model picker token matching
* Docs: add screenshot showing model picker usability issue
* fix: searchable model picker in configure (#19010) (thanks @bjesuiter)
* fix(extensions): revert openai codex auth plugin (PR #18009)
* feat(telegram): add channel_post support for bot-to-bot communication (#17857)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 27a343cd4d9c778a6017ff666d8285ae60256bf4
Co-authored-by: theSamPadilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* Revert "fix: handle forum/topics in Telegram DM thread routing (#17980)"
This reverts commit e20b87f1ba332e2b7838d05e38e3bd7d991f460d.
* Revert: undo #17974 README change
* voice-call: harden closed-loop turn loop and transcript routing (#19140)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 14a3edb00572b20348f839bbafa56ca826cee362
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* iOS onboarding: stop auth step-3 retry loop churn (#19153)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: a38ec42bdd4cf1bf5743ecd3c1d1f2bcceea91e0
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* Revert: fully roll back #17974 zh-cn UI README
* chore(subagents): add regression coverage and changelog
* fix(daemon): scope token drift warnings
* test(web): fix baileys mock typing
* test(cron): cover webhook session rollover overrides
* docs(changelog): note webhook session reuse fix
* fix(discord): normalize command allowFrom prefixes
* fix(cli): honor update restart overrides
* fix(cron): add spin-loop regression coverage
* test(gateway): cover trusted proxy trimming
* test(discord): cover audioAsVoice replies
* test(feishu): cover post mentions for other users
* fix(discord): preserve DM lastRoute user target
* Revert "fix(browser): track original port mapping for EADDRINUSE fallback"
This reverts commit 8e55503d7785e741ce9995f989d944e09a8f22db.
* Revert "fix(browser): handle EADDRINUSE with automatic port fallback"
This reverts commit 0e6daa2e6e30240ce5191ee5c7954f24b4b56b42.
* test(discord): fix mock call arg typing
* Revert: fully roll back #17986 templates
* test: add fetch mock helper and reaction coverage
* CLI: approve latest pending device request
* docs(readme): remove Android install link
* revert(agents): remove llms.txt discovery prompt (#19192)
* fix(ui): revert PR #18093 directive tags (#19188)
* test(discord): cover auto-thread skip types
* test(update): cover restart gating
* docs(zai): document tool_stream defaults
* revert: per-model thinkingDefault override (#19195)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: fe2c59e222a71086864f8abfae05d2da03ad4066
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* fix(gateway): make stale token cleanup non-fatal
* Agents: add before_message_write persistence regression tests
* fix(mattermost): surface reactions support
* Tests: fix fetch mock typings for type-aware checks
* revert: fix models set catalog validation (#19194)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 7e3b2ff7afe052097c4414fc64d7e66191e8fcc3
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* test: cover cron telemetry and typed fetch mocks
* revert(agents): revert base64 image validation (#19221)
* docs(cli): add components send example
* test(sessions): add delivery info regression coverage
* fix(daemon): guard preferred node selection
* test(auto-reply): cover sender_id metadata
* revert: PR 18288 accidental merge (#19224)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 3cda31578ccf71365d5815a8374922e835efff0e
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* test(telegram): cover autoSelectFamily env precedence
* test(cron): add model fallback regression coverage
* test(release): add appcast regression coverage
* docs(changelog): remove revert entries
* docs: add maintainer application section
* docs: refine maintainer application guidance
* docs: add vision doc and link from README
* docs: add community plugins guide
* Update auto-response message for third-party extensions
* update my contributing list
* iOS: use operator session for ChatSheet RPCs (#19320)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 0753b3a1a22a1d23e871d210e317279627f9fc0f
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* fix: sanitize native command names for Telegram API (#19257)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: b608be348888505b23bb4b2f1c8c42058a28e64e
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* docs(slack): add assistant:write requirement for typing status
* chore: document sessions_spawn response note and subagent context prefix
* feat(ios): auto-select local signing team (#18421)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: bbb9c3aa48a542539dc37136e6542d1f3958f9c2
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
* fix(bluebubbles): recover outbound message IDs and include sender metadata
* fix cron announce routing and timeout handling
* changelog: add @tyler6204 credit for today's entries
* feat: share to openclaw ios app (#19424)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 0a7ab8589ac23d0743d4377683d60601a8c19e61
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* Docs: expand multi-agent routing
* docs(changelog): add missing 2026.2.16 entries and reorder by user impact
* chore(release): bump version to 2026.2.17
* fix(signal): canonicalize message targets in tool and inbound flows
* docs: tighten contribution guidance and vision links
* docs: tighten PR scope and review-size policy in vision
* fix(gateway): block cross-session fallback in node event delivery
* fix(gateway): make health monitor checks single-flight
* fix(ios): harden share relay routing and delivery guards
* fix(telegram): normalize topic-create targets and add regression tests
* feat(cron): add default stagger controls for scheduled jobs
* fix(cron): retry next-second schedule compute on undefined
* docs(security): harden gateway security guidance
* feat(models): support anthropic sonnet 4.6
* fix: wire agents.defaults.imageModel into media understanding auto-discovery
resolveAutoEntries only checked a hardcoded list of providers
(openai, anthropic, google, minimax) when looking for an image model.
agents.defaults.imageModel was never consulted by the media understanding
pipeline — it was only wired into the explicit `image` tool.
Add resolveImageModelFromAgentDefaults that reads the imageModel config
(primary + fallbacks) and inserts it into the auto-discovery chain before
the hardcoded provider list. runProviderEntry already falls back to
describeImageWithModel (via pi-ai) for providers not in the media
understanding registry, so no additional provider registration is needed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
(cherry picked from commit b381029ede72a57ef6d12d9413c98fa29501b797)
* docs: update AGENTS instructions
* fix(subagent): harden read-tool overflow guards and sticky reply threading (#19508)
* fix(gateway): avoid premature agent.wait completion on transient errors
* fix(agent): preemptively guard tool results against context overflow
* fix: harden tool-result context guard and add message_id metadata
* fix: use importOriginal in session-key mock to include DEFAULT_ACCOUNT_ID
The run.skill-filter test was mocking ../../routing/session-key.js with only
buildAgentMainSessionKey and normalizeAgentId, but the module also exports
DEFAULT_ACCOUNT_ID which is required transitively by src/web/auth-store.ts.
Switch to importOriginal pattern so all real exports are preserved alongside
the mocked functions.
* pi-runner: guard accumulated tool-result overflow in transformContext
* PI runner: compact overflowing tool-result context
* Subagent: harden tool-result context recovery
* Enhance tool-result context handling by adding support for legacy tool outputs and improving character estimation for message truncation. This includes a new function to create legacy tool results and updates to existing functions to better manage context overflow scenarios.
* Enhance iMessage handling by adding reply tag support in send functions and tests. This includes modifications to prepend or rewrite reply tags based on provided replyToId, ensuring proper message formatting for replies.
* Enhance message delivery across multiple channels by implementing sticky reply context for chunked messages. This includes preserving reply references in Discord, Telegram, and iMessage, ensuring that follow-up messages maintain their intended reply targets. Additionally, improve handling of reply tags in system prompts and tests to support consistent reply behavior.
* Enhance read tool functionality by implementing auto-paging across chunks when no explicit limit is provided, scaling output budget based on model context window. Additionally, add tests for adaptive reading behavior and capped continuation guidance for large outputs. Update related functions to support these features.
* Refine tool-result context management by stripping oversized read-tool details payloads during compaction, ensuring repeated read calls do not bypass context limits. Introduce new utility functions for handling truncation content and enhance character estimation for tool results. Add tests to validate the removal of excessive details in context overflow scenarios.
* Refine message delivery logic in Matrix and Telegram by introducing a flag to track if a text chunk was sent. This ensures that replies are only marked as delivered when a text chunk has been successfully sent, improving the accuracy of reply handling in both channels.
* fix: tighten reply threading coverage and prep fixes (#19508) (thanks @tyler6204)
* fix(hooks): backport internal message hook bridge with safe delivery semantics
* fix(subagent): update SUBAGENT_SPAWN_ACCEPTED_NOTE for clarity on auto-announcement behavior
* fix: follow-up slack streaming routing/tests (#9972) (thanks @natedenh)
* fix: reduce default image dimension from 2000px to 1200px
Large images (2000px) consume excessive context tokens when sent to LLMs.
1200px provides sufficient detail for most use cases while significantly
reducing token usage.
The 5MB byte limit remains unchanged as JPEG compression at 1200px
naturally produces smaller files.
(cherry picked from commit 40182123dd2673b4b65e6846fa066380f10b781f)
* fix(agents): make image sanitization dimension configurable
* docs(tokens): document image dimension token tradeoffs
* Whatsapp/add resolve outbound target tests (#19345)
* test(whatsapp): add resolveWhatsAppOutboundTarget test suite
* style: auto-format files
* fix(test): correct mock order for invalid allowList entry test
* feat(skills): Add 'Use when / Don't use when' routing blocks (#14521)
* feat(skills): add 'Use when / Don't use when' blocks to skill descriptions
Based on OpenAI's Shell + Skills + Compaction best practices article.
Key changes:
- Added clear routing logic to skill descriptions
- Added negative examples to prevent misfires
- Added templates/examples to github skill
- Included Blake's specific setup notes for openhue
Skills updated:
- apple-reminders: Clarify vs Clawdbot cron
- github: Clarify vs local git operations
- imsg: Clarify vs other messaging channels
- openhue: Add device inventory, room layout
- tmux: Clarify vs exec tool
- weather: Add location defaults, format codes
Reference: https://developers.openai.com/blog/skills-shell-tips
* fix(skills): restore metadata and generic CLI examples
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(agents): add generic provider api key rotation (#19587)
* feat(skills): improve descriptions with routing logic (#14577)
* feat(skills): improve descriptions with routing logic
Apply OpenAI's recommended pattern for skill descriptions:
- Add 'Use when' conditions for clear triggering
- Add 'NOT for' negative examples to reduce misfires
- Make descriptions act as routing logic, not marketing copy
Based on: https://developers.openai.com/blog/skills-shell-tips/
Skills updated:
- coding-agent: clarify when to delegate vs direct edit
- github: add boundaries vs browser/scripting
- weather: add scope limitations
Glean reported 20% drop in skill triggering without negative
examples, recovering after adding them. This change brings
Clawdbot skills in line with that pattern.
* docs(skills): clarify routing boundaries (openclaw#14577) (thanks @DylanWoodAkers)
* docs(changelog): add PR 14577 release note (openclaw#14577) (thanks @DylanWoodAkers)
---------
Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* Add frontend-design skill
* feat(telegram): add forum topic creation support (#10427)
Add `topic-create` action to the Telegram message adapter, enabling
programmatic creation of forum topics in supergroups.
Changes:
- Add `createForumTopicTelegram()` to `src/telegram/send.ts`
- Add `createForumTopic` handler in `telegram-actions.ts`
- Wire `topic-create` action in Telegram adapter
- Register `topic-create` in message action names and spec
The bot requires `can_manage_topics` permission in the target group.
Supports optional `iconColor` and `iconCustomEmojiId` parameters.
Closes #10427
* chore: fix formatting in frontend-design SKILL.md
* fix: add action gate check and config type for createForumTopic
Address review feedback:
- Add isActionEnabled() gate in telegram-actions.ts
- Add gate() check in telegram adapter listActions
- Add createForumTopic to TelegramActionConfig type
* fix(telegram): normalize topic-create targets and add regression tests
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
Co-authored-by: cpojer <christoph.pojer@gmail.com>
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
Co-authored-by: Josh Avant <830519+joshavant@users.noreply.github.com>
Co-authored-by: Shadow <hi@shadowing.dev>
Co-authored-by: Hongwei Ma <Marvae@users.noreply.github.com>
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <zaidi@uplause.io>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Sascha Reuter <s.reuter@geek-it.de>
Co-authored-by: sreuter <550246+sreuter@users.noreply.github.com>
Co-authored-by: Nimrod Gutman <nimrod.g@singular.net>
Co-authored-by: Vignesh <mailvgnsh@gmail.com>
Co-authored-by: Benjamin Jesuiter <bjesuiter@gmail.com>
Co-authored-by: Sam Padilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: Muhammed Mukhthar CM <mukhtharcm@gmail.com>
Co-authored-by: Mariano <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
Co-authored-by: Mariano Belinky <mbelinky@gmail.com>
Co-authored-by: Shadow <shadow@openclaw.ai>
Co-authored-by: Sk Akram <skcodewizard786@gmail.com>
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: Onur <onur@textcortex.com>
Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: Pablo Nunez <pnunfe@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Tyler Yust <64381258+tyler6204@users.noreply.github.com>
Co-authored-by: Han Xiao <han.xiao@jina.ai>
Co-authored-by: Verite Igiraneza <69280208+VeriteIgiraneza@users.noreply.github.com>
Co-authored-by: Blakeshannon <blake@blakeshannon.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: DylanWoodAkers <dylan@lec.com>
Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Claw <claw@openclaw.ai>
2026-02-18 00:38:44 +00:00
createForumTopicTelegram ,
2026-02-15 20:43:53 +00:00
editMessageTelegram ,
reactMessageTelegram ,
sendMessageTelegram ,
sendPollTelegram ,
sendStickerTelegram ,
} = await importTelegramSendModule ( ) ;
2026-01-09 20:46:11 +01:00
2026-02-16 00:39:59 +00:00
describe ( "sent-message-cache" , ( ) = > {
afterEach ( ( ) = > {
clearSentMessageCache ( ) ;
} ) ;
it ( "records and retrieves sent messages" , ( ) = > {
recordSentMessage ( 123 , 1 ) ;
recordSentMessage ( 123 , 2 ) ;
recordSentMessage ( 456 , 10 ) ;
expect ( wasSentByBot ( 123 , 1 ) ) . toBe ( true ) ;
expect ( wasSentByBot ( 123 , 2 ) ) . toBe ( true ) ;
expect ( wasSentByBot ( 456 , 10 ) ) . toBe ( true ) ;
expect ( wasSentByBot ( 123 , 3 ) ) . toBe ( false ) ;
expect ( wasSentByBot ( 789 , 1 ) ) . toBe ( false ) ;
} ) ;
it ( "handles string chat IDs" , ( ) = > {
recordSentMessage ( "123" , 1 ) ;
expect ( wasSentByBot ( "123" , 1 ) ) . toBe ( true ) ;
expect ( wasSentByBot ( 123 , 1 ) ) . toBe ( true ) ;
} ) ;
it ( "clears cache" , ( ) = > {
recordSentMessage ( 123 , 1 ) ;
expect ( wasSentByBot ( 123 , 1 ) ) . toBe ( true ) ;
clearSentMessageCache ( ) ;
expect ( wasSentByBot ( 123 , 1 ) ) . toBe ( false ) ;
} ) ;
} ) ;
2026-01-09 20:46:11 +01:00
describe ( "buildInlineKeyboard" , ( ) = > {
it ( "returns undefined for empty input" , ( ) = > {
expect ( buildInlineKeyboard ( ) ) . toBeUndefined ( ) ;
expect ( buildInlineKeyboard ( [ ] ) ) . toBeUndefined ( ) ;
} ) ;
it ( "builds inline keyboards for valid input" , ( ) = > {
const result = buildInlineKeyboard ( [
[ { text : "Option A" , callback_data : "cmd:a" } ] ,
[
{ text : "Option B" , callback_data : "cmd:b" } ,
{ text : "Option C" , callback_data : "cmd:c" } ,
] ,
] ) ;
expect ( result ) . toEqual ( {
inline_keyboard : [
[ { text : "Option A" , callback_data : "cmd:a" } ] ,
[
{ text : "Option B" , callback_data : "cmd:b" } ,
{ text : "Option C" , callback_data : "cmd:c" } ,
] ,
] ,
} ) ;
} ) ;
2026-02-16 22:48:47 +05:30
it ( "passes through button style" , ( ) = > {
const result = buildInlineKeyboard ( [
[
{
text : "Option A" ,
callback_data : "cmd:a" ,
style : "primary" ,
} ,
] ,
] ) ;
expect ( result ) . toEqual ( {
inline_keyboard : [
[
{
text : "Option A" ,
callback_data : "cmd:a" ,
style : "primary" ,
} ,
] ,
] ,
} ) ;
} ) ;
2026-01-09 20:46:11 +01:00
it ( "filters invalid buttons and empty rows" , ( ) = > {
const result = buildInlineKeyboard ( [
[
{ text : "" , callback_data : "cmd:skip" } ,
{ text : "Ok" , callback_data : "cmd:ok" } ,
] ,
[ { text : "Missing data" , callback_data : "" } ] ,
[ ] ,
] ) ;
expect ( result ) . toEqual ( {
inline_keyboard : [ [ { text : "Ok" , callback_data : "cmd:ok" } ] ] ,
} ) ;
} ) ;
} ) ;
2025-12-07 22:46:02 +01:00
describe ( "sendMessageTelegram" , ( ) = > {
2026-01-14 10:09:26 +00:00
it ( "passes timeoutSeconds to grammY client when configured" , async ( ) = > {
loadConfig . mockReturnValue ( {
channels : { telegram : { timeoutSeconds : 60 } } ,
} ) ;
await sendMessageTelegram ( "123" , "hi" , { token : "tok" } ) ;
expect ( botCtorSpy ) . toHaveBeenCalledWith (
"tok" ,
expect . objectContaining ( {
client : expect.objectContaining ( { timeoutSeconds : 60 } ) ,
} ) ,
) ;
} ) ;
2026-01-14 10:35:42 +00:00
it ( "prefers per-account timeoutSeconds overrides" , async ( ) = > {
loadConfig . mockReturnValue ( {
channels : {
telegram : {
timeoutSeconds : 60 ,
accounts : { foo : { timeoutSeconds : 61 } } ,
} ,
} ,
} ) ;
await sendMessageTelegram ( "123" , "hi" , { token : "tok" , accountId : "foo" } ) ;
expect ( botCtorSpy ) . toHaveBeenCalledWith (
"tok" ,
expect . objectContaining ( {
client : expect.objectContaining ( { timeoutSeconds : 61 } ) ,
} ) ,
) ;
} ) ;
2026-01-14 10:09:26 +00:00
2026-01-08 02:34:32 +01:00
it ( "falls back to plain text when Telegram rejects HTML" , async ( ) = > {
2025-12-10 15:55:20 +00:00
const chatId = "123" ;
const parseErr = new Error (
"400: Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 9" ,
2025-12-07 22:46:02 +01:00
) ;
2025-12-10 15:55:20 +00:00
const sendMessage = vi
. fn ( )
. mockRejectedValueOnce ( parseErr )
. mockResolvedValueOnce ( {
message_id : 42 ,
chat : { id : chatId } ,
} ) ;
2025-12-10 18:00:33 +00:00
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
2025-12-10 15:55:20 +00:00
const res = await sendMessageTelegram ( chatId , "_oops_" , {
token : "tok" ,
api ,
verbose : true ,
2025-12-07 22:46:02 +01:00
} ) ;
2025-12-10 15:55:20 +00:00
2026-01-08 02:34:32 +01:00
expect ( sendMessage ) . toHaveBeenNthCalledWith ( 1 , chatId , "<i>oops</i>" , {
parse_mode : "HTML" ,
2025-12-07 22:46:02 +01:00
} ) ;
2025-12-10 15:55:20 +00:00
expect ( sendMessage ) . toHaveBeenNthCalledWith ( 2 , chatId , "_oops_" ) ;
expect ( res . chatId ) . toBe ( chatId ) ;
expect ( res . messageId ) . toBe ( "42" ) ;
2025-12-07 22:46:02 +01:00
} ) ;
2025-12-20 14:21:49 +00:00
2026-01-25 07:55:39 +00:00
it ( "adds link_preview_options when previews are disabled in config" , async ( ) = > {
const chatId = "123" ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 7 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
loadConfig . mockReturnValue ( {
channels : { telegram : { linkPreview : false } } ,
} ) ;
await sendMessageTelegram ( chatId , "hi" , { token : "tok" , api } ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , "hi" , {
parse_mode : "HTML" ,
link_preview_options : { is_disabled : true } ,
} ) ;
} ) ;
it ( "keeps link_preview_options on plain-text fallback when disabled" , async ( ) = > {
const chatId = "123" ;
const parseErr = new Error (
"400: Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 9" ,
) ;
const sendMessage = vi
. fn ( )
. mockRejectedValueOnce ( parseErr )
. mockResolvedValueOnce ( {
message_id : 42 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
loadConfig . mockReturnValue ( {
channels : { telegram : { linkPreview : false } } ,
} ) ;
await sendMessageTelegram ( chatId , "_oops_" , {
token : "tok" ,
api ,
} ) ;
expect ( sendMessage ) . toHaveBeenNthCalledWith ( 1 , chatId , "<i>oops</i>" , {
parse_mode : "HTML" ,
link_preview_options : { is_disabled : true } ,
} ) ;
expect ( sendMessage ) . toHaveBeenNthCalledWith ( 2 , chatId , "_oops_" , {
link_preview_options : { is_disabled : true } ,
} ) ;
} ) ;
2026-01-08 04:40:29 +01:00
it ( "uses native fetch for BAN compatibility when api is omitted" , async ( ) = > {
const originalFetch = globalThis . fetch ;
2026-01-08 10:37:29 +01:00
const originalBun = ( globalThis as { Bun? : unknown } ) . Bun ;
2026-01-08 04:40:29 +01:00
const fetchSpy = vi . fn ( ) as unknown as typeof fetch ;
globalThis . fetch = fetchSpy ;
2026-01-08 10:37:29 +01:00
( globalThis as { Bun? : unknown } ) . Bun = { } ;
2026-01-08 04:40:29 +01:00
botApi . sendMessage . mockResolvedValue ( {
message_id : 1 ,
chat : { id : "123" } ,
} ) ;
try {
await sendMessageTelegram ( "123" , "hi" , { token : "tok" } ) ;
2026-01-21 16:46:49 +00:00
const clientFetch = ( botCtorSpy . mock . calls [ 0 ] ? . [ 1 ] as { client ? : { fetch? : unknown } } )
? . client ? . fetch ;
expect ( clientFetch ) . toBeTypeOf ( "function" ) ;
expect ( clientFetch ) . not . toBe ( fetchSpy ) ;
2026-01-08 04:40:29 +01:00
} finally {
globalThis . fetch = originalFetch ;
2026-01-08 10:37:29 +01:00
if ( originalBun === undefined ) {
delete ( globalThis as { Bun? : unknown } ) . Bun ;
} else {
( globalThis as { Bun? : unknown } ) . Bun = originalBun ;
}
2026-01-08 04:40:29 +01:00
}
} ) ;
2025-12-20 14:21:49 +00:00
it ( "normalizes chat ids with internal prefixes" , async ( ) = > {
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 1 ,
chat : { id : "123" } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
await sendMessageTelegram ( "telegram:123" , "hi" , {
token : "tok" ,
api ,
} ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( "123" , "hi" , {
2026-01-08 02:34:32 +01:00
parse_mode : "HTML" ,
2025-12-20 14:21:49 +00:00
} ) ;
} ) ;
it ( "wraps chat-not-found with actionable context" , async ( ) = > {
const chatId = "123" ;
const err = new Error ( "400: Bad Request: chat not found" ) ;
const sendMessage = vi . fn ( ) . mockRejectedValue ( err ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
2026-01-14 14:31:43 +00:00
await expect ( sendMessageTelegram ( chatId , "hi" , { token : "tok" , api } ) ) . rejects . toThrow (
/chat not found/i ,
) ;
await expect ( sendMessageTelegram ( chatId , "hi" , { token : "tok" , api } ) ) . rejects . toThrow (
/chat_id=123/ ,
) ;
2025-12-20 14:21:49 +00:00
} ) ;
2026-01-06 02:22:09 +00:00
2026-02-15 20:43:53 +00:00
it ( "preserves thread params in plain text fallback" , async ( ) = > {
const chatId = "-1001234567890" ;
const parseErr = new Error (
"400: Bad Request: can't parse entities: Can't find end of the entity" ,
) ;
const sendMessage = vi
. fn ( )
. mockRejectedValueOnce ( parseErr )
. mockResolvedValueOnce ( {
message_id : 60 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
const res = await sendMessageTelegram ( chatId , "_bad markdown_" , {
token : "tok" ,
api ,
messageThreadId : 271 ,
replyToMessageId : 100 ,
} ) ;
expect ( sendMessage ) . toHaveBeenNthCalledWith ( 1 , chatId , "<i>bad markdown</i>" , {
parse_mode : "HTML" ,
message_thread_id : 271 ,
reply_to_message_id : 100 ,
} ) ;
expect ( sendMessage ) . toHaveBeenNthCalledWith ( 2 , chatId , "_bad markdown_" , {
message_thread_id : 271 ,
reply_to_message_id : 100 ,
} ) ;
expect ( res . messageId ) . toBe ( "60" ) ;
} ) ;
it ( "includes thread params in media messages" , async ( ) = > {
const chatId = "-1001234567890" ;
const sendPhoto = vi . fn ( ) . mockResolvedValue ( {
message_id : 58 ,
chat : { id : chatId } ,
} ) ;
const api = { sendPhoto } as unknown as {
sendPhoto : typeof sendPhoto ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "fake-image" ) ,
contentType : "image/jpeg" ,
fileName : "photo.jpg" ,
} ) ;
await sendMessageTelegram ( chatId , "photo in topic" , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/photo.jpg" ,
messageThreadId : 99 ,
} ) ;
expect ( sendPhoto ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , {
caption : "photo in topic" ,
parse_mode : "HTML" ,
message_thread_id : 99 ,
} ) ;
} ) ;
it ( "splits long captions into media + text messages when text exceeds 1024 chars" , async ( ) = > {
const chatId = "123" ;
const longText = "A" . repeat ( 1100 ) ;
const sendPhoto = vi . fn ( ) . mockResolvedValue ( {
message_id : 70 ,
chat : { id : chatId } ,
} ) ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 71 ,
chat : { id : chatId } ,
} ) ;
const api = { sendPhoto , sendMessage } as unknown as {
sendPhoto : typeof sendPhoto ;
sendMessage : typeof sendMessage ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "fake-image" ) ,
contentType : "image/jpeg" ,
fileName : "photo.jpg" ,
} ) ;
const res = await sendMessageTelegram ( chatId , longText , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/photo.jpg" ,
} ) ;
expect ( sendPhoto ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , {
caption : undefined ,
} ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , longText , {
parse_mode : "HTML" ,
} ) ;
expect ( res . messageId ) . toBe ( "71" ) ;
} ) ;
it ( "uses caption when text is within 1024 char limit" , async ( ) = > {
const chatId = "123" ;
const shortText = "B" . repeat ( 1024 ) ;
const sendPhoto = vi . fn ( ) . mockResolvedValue ( {
message_id : 72 ,
chat : { id : chatId } ,
} ) ;
const sendMessage = vi . fn ( ) ;
const api = { sendPhoto , sendMessage } as unknown as {
sendPhoto : typeof sendPhoto ;
sendMessage : typeof sendMessage ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "fake-image" ) ,
contentType : "image/jpeg" ,
fileName : "photo.jpg" ,
} ) ;
const res = await sendMessageTelegram ( chatId , shortText , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/photo.jpg" ,
} ) ;
expect ( sendPhoto ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , {
caption : shortText ,
parse_mode : "HTML" ,
} ) ;
expect ( sendMessage ) . not . toHaveBeenCalled ( ) ;
expect ( res . messageId ) . toBe ( "72" ) ;
} ) ;
it ( "renders markdown in media captions" , async ( ) = > {
const chatId = "123" ;
const caption = "hi **boss**" ;
const sendPhoto = vi . fn ( ) . mockResolvedValue ( {
message_id : 90 ,
chat : { id : chatId } ,
} ) ;
const api = { sendPhoto } as unknown as {
sendPhoto : typeof sendPhoto ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "fake-image" ) ,
contentType : "image/jpeg" ,
fileName : "photo.jpg" ,
} ) ;
await sendMessageTelegram ( chatId , caption , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/photo.jpg" ,
} ) ;
expect ( sendPhoto ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , {
caption : "hi <b>boss</b>" ,
parse_mode : "HTML" ,
} ) ;
} ) ;
it ( "sends video as video note when asVideoNote is true" , async ( ) = > {
const chatId = "123" ;
const text = "ignored caption context" ;
const sendVideoNote = vi . fn ( ) . mockResolvedValue ( {
message_id : 101 ,
chat : { id : chatId } ,
} ) ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 102 ,
chat : { id : chatId } ,
} ) ;
const api = { sendVideoNote , sendMessage } as unknown as {
sendVideoNote : typeof sendVideoNote ;
sendMessage : typeof sendMessage ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "fake-video" ) ,
contentType : "video/mp4" ,
fileName : "video.mp4" ,
} ) ;
const res = await sendMessageTelegram ( chatId , text , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/video.mp4" ,
asVideoNote : true ,
} ) ;
expect ( sendVideoNote ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , { } ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , text , {
parse_mode : "HTML" ,
} ) ;
expect ( res . messageId ) . toBe ( "102" ) ;
} ) ;
it ( "sends regular video when asVideoNote is false" , async ( ) = > {
const chatId = "123" ;
const text = "my caption" ;
const sendVideo = vi . fn ( ) . mockResolvedValue ( {
message_id : 201 ,
chat : { id : chatId } ,
} ) ;
const api = { sendVideo } as unknown as {
sendVideo : typeof sendVideo ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "fake-video" ) ,
contentType : "video/mp4" ,
fileName : "video.mp4" ,
} ) ;
const res = await sendMessageTelegram ( chatId , text , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/video.mp4" ,
asVideoNote : false ,
} ) ;
expect ( sendVideo ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , {
caption : expect.any ( String ) ,
parse_mode : "HTML" ,
} ) ;
expect ( res . messageId ) . toBe ( "201" ) ;
} ) ;
it ( "adds reply_markup to separate text message for video notes" , async ( ) = > {
const chatId = "123" ;
const text = "Check this out" ;
const sendVideoNote = vi . fn ( ) . mockResolvedValue ( {
message_id : 301 ,
chat : { id : chatId } ,
} ) ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 302 ,
chat : { id : chatId } ,
} ) ;
const api = { sendVideoNote , sendMessage } as unknown as {
sendVideoNote : typeof sendVideoNote ;
sendMessage : typeof sendMessage ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "fake-video" ) ,
contentType : "video/mp4" ,
fileName : "video.mp4" ,
} ) ;
await sendMessageTelegram ( chatId , text , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/video.mp4" ,
asVideoNote : true ,
buttons : [ [ { text : "Btn" , callback_data : "dat" } ] ] ,
} ) ;
expect ( sendVideoNote ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , { } ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , text , {
parse_mode : "HTML" ,
reply_markup : {
inline_keyboard : [ [ { text : "Btn" , callback_data : "dat" } ] ] ,
} ,
} ) ;
} ) ;
it ( "threads video note and text message correctly" , async ( ) = > {
const chatId = "123" ;
const text = "Threaded reply" ;
const sendVideoNote = vi . fn ( ) . mockResolvedValue ( {
message_id : 401 ,
chat : { id : chatId } ,
} ) ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 402 ,
chat : { id : chatId } ,
} ) ;
const api = { sendVideoNote , sendMessage } as unknown as {
sendVideoNote : typeof sendVideoNote ;
sendMessage : typeof sendMessage ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "fake-video" ) ,
contentType : "video/mp4" ,
fileName : "video.mp4" ,
} ) ;
await sendMessageTelegram ( chatId , text , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/video.mp4" ,
asVideoNote : true ,
replyToMessageId : 999 ,
} ) ;
expect ( sendVideoNote ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , {
reply_to_message_id : 999 ,
} ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , text , {
parse_mode : "HTML" ,
reply_to_message_id : 999 ,
} ) ;
} ) ;
2026-01-07 17:48:19 +00:00
it ( "retries on transient errors with retry_after" , async ( ) = > {
vi . useFakeTimers ( ) ;
const chatId = "123" ;
const err = Object . assign ( new Error ( "429" ) , {
parameters : { retry_after : 0.5 } ,
} ) ;
const sendMessage = vi
. fn ( )
. mockRejectedValueOnce ( err )
. mockResolvedValueOnce ( {
message_id : 1 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
const setTimeoutSpy = vi . spyOn ( global , "setTimeout" ) ;
const promise = sendMessageTelegram ( chatId , "hi" , {
token : "tok" ,
api ,
retry : { attempts : 2 , minDelayMs : 0 , maxDelayMs : 1000 , jitter : 0 } ,
} ) ;
await vi . runAllTimersAsync ( ) ;
await expect ( promise ) . resolves . toEqual ( { messageId : "1" , chatId } ) ;
expect ( setTimeoutSpy . mock . calls [ 0 ] ? . [ 1 ] ) . toBe ( 500 ) ;
setTimeoutSpy . mockRestore ( ) ;
vi . useRealTimers ( ) ;
} ) ;
it ( "does not retry on non-transient errors" , async ( ) = > {
const chatId = "123" ;
2026-01-14 14:31:43 +00:00
const sendMessage = vi . fn ( ) . mockRejectedValue ( new Error ( "400: Bad Request" ) ) ;
2026-01-07 17:48:19 +00:00
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
await expect (
sendMessageTelegram ( chatId , "hi" , {
token : "tok" ,
api ,
retry : { attempts : 3 , minDelayMs : 0 , maxDelayMs : 0 , jitter : 0 } ,
} ) ,
) . rejects . toThrow ( /Bad Request/ ) ;
expect ( sendMessage ) . toHaveBeenCalledTimes ( 1 ) ;
} ) ;
2026-01-06 02:22:09 +00:00
it ( "sends GIF media as animation" , async ( ) = > {
const chatId = "123" ;
const sendAnimation = vi . fn ( ) . mockResolvedValue ( {
message_id : 9 ,
chat : { id : chatId } ,
} ) ;
const api = { sendAnimation } as unknown as {
sendAnimation : typeof sendAnimation ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "GIF89a" ) ,
fileName : "fun.gif" ,
} ) ;
const res = await sendMessageTelegram ( chatId , "caption" , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/fun" ,
} ) ;
expect ( sendAnimation ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( sendAnimation ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , {
caption : "caption" ,
2026-01-24 03:39:21 +00:00
parse_mode : "HTML" ,
2026-01-06 02:22:09 +00:00
} ) ;
expect ( res . messageId ) . toBe ( "9" ) ;
} ) ;
2026-01-07 03:24:56 -03:00
2026-01-08 03:13:54 +00:00
it ( "sends audio media as files by default" , async ( ) = > {
const chatId = "123" ;
const sendAudio = vi . fn ( ) . mockResolvedValue ( {
message_id : 10 ,
chat : { id : chatId } ,
} ) ;
const sendVoice = vi . fn ( ) . mockResolvedValue ( {
message_id : 11 ,
chat : { id : chatId } ,
} ) ;
const api = { sendAudio , sendVoice } as unknown as {
sendAudio : typeof sendAudio ;
sendVoice : typeof sendVoice ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "audio" ) ,
contentType : "audio/mpeg" ,
fileName : "clip.mp3" ,
} ) ;
await sendMessageTelegram ( chatId , "caption" , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/clip.mp3" ,
} ) ;
expect ( sendAudio ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , {
caption : "caption" ,
2026-01-24 03:39:21 +00:00
parse_mode : "HTML" ,
2026-01-08 03:13:54 +00:00
} ) ;
expect ( sendVoice ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( "sends voice messages when asVoice is true and preserves thread params" , async ( ) = > {
const chatId = "-1001234567890" ;
const sendAudio = vi . fn ( ) . mockResolvedValue ( {
message_id : 12 ,
chat : { id : chatId } ,
} ) ;
const sendVoice = vi . fn ( ) . mockResolvedValue ( {
message_id : 13 ,
chat : { id : chatId } ,
} ) ;
const api = { sendAudio , sendVoice } as unknown as {
sendAudio : typeof sendAudio ;
sendVoice : typeof sendVoice ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "voice" ) ,
contentType : "audio/ogg" ,
fileName : "note.ogg" ,
} ) ;
await sendMessageTelegram ( chatId , "voice note" , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/note.ogg" ,
asVoice : true ,
messageThreadId : 271 ,
replyToMessageId : 500 ,
} ) ;
expect ( sendVoice ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , {
caption : "voice note" ,
2026-01-24 03:39:21 +00:00
parse_mode : "HTML" ,
2026-01-08 03:13:54 +00:00
message_thread_id : 271 ,
reply_to_message_id : 500 ,
} ) ;
expect ( sendAudio ) . not . toHaveBeenCalled ( ) ;
} ) ;
2026-01-08 13:55:36 +00:00
it ( "falls back to audio when asVoice is true but media is not voice compatible" , async ( ) = > {
const chatId = "123" ;
const sendAudio = vi . fn ( ) . mockResolvedValue ( {
message_id : 14 ,
chat : { id : chatId } ,
} ) ;
const sendVoice = vi . fn ( ) . mockResolvedValue ( {
message_id : 15 ,
chat : { id : chatId } ,
} ) ;
const api = { sendAudio , sendVoice } as unknown as {
sendAudio : typeof sendAudio ;
sendVoice : typeof sendVoice ;
} ;
2026-02-14 02:03:02 +00:00
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "audio" ) ,
contentType : "audio/wav" ,
fileName : "clip.wav" ,
} ) ;
await sendMessageTelegram ( chatId , "caption" , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/clip.wav" ,
asVoice : true ,
} ) ;
expect ( sendAudio ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , {
caption : "caption" ,
parse_mode : "HTML" ,
} ) ;
expect ( sendVoice ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( "sends MP3 as voice when asVoice is true" , async ( ) = > {
const chatId = "123" ;
const sendAudio = vi . fn ( ) . mockResolvedValue ( {
message_id : 16 ,
chat : { id : chatId } ,
} ) ;
const sendVoice = vi . fn ( ) . mockResolvedValue ( {
message_id : 17 ,
chat : { id : chatId } ,
} ) ;
const api = { sendAudio , sendVoice } as unknown as {
sendAudio : typeof sendAudio ;
sendVoice : typeof sendVoice ;
} ;
2026-01-08 13:55:36 +00:00
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "audio" ) ,
contentType : "audio/mpeg" ,
fileName : "clip.mp3" ,
} ) ;
await sendMessageTelegram ( chatId , "caption" , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/clip.mp3" ,
asVoice : true ,
} ) ;
2026-02-14 02:03:02 +00:00
expect ( sendVoice ) . toHaveBeenCalledWith ( chatId , expect . anything ( ) , {
2026-01-08 13:55:36 +00:00
caption : "caption" ,
2026-01-24 03:39:21 +00:00
parse_mode : "HTML" ,
2026-01-08 13:55:36 +00:00
} ) ;
2026-02-14 02:03:02 +00:00
expect ( sendAudio ) . not . toHaveBeenCalled ( ) ;
2026-01-08 13:55:36 +00:00
} ) ;
2026-01-07 03:24:56 -03:00
it ( "includes message_thread_id for forum topic messages" , async ( ) = > {
const chatId = "-1001234567890" ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 55 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
await sendMessageTelegram ( chatId , "hello forum" , {
token : "tok" ,
api ,
messageThreadId : 271 ,
} ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , "hello forum" , {
2026-01-08 02:34:32 +01:00
parse_mode : "HTML" ,
2026-01-07 03:24:56 -03:00
message_thread_id : 271 ,
} ) ;
} ) ;
2026-02-17 13:03:20 +05:30
it ( "keeps message_thread_id for private chat topic sends (#18974)" , async ( ) = > {
2026-02-16 12:01:35 +01:00
const chatId = "123456789" ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 56 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
await sendMessageTelegram ( chatId , "hello private" , {
token : "tok" ,
api ,
messageThreadId : 271 ,
} ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , "hello private" , {
parse_mode : "HTML" ,
2026-02-17 13:03:20 +05:30
message_thread_id : 271 ,
2026-02-16 12:01:35 +01:00
} ) ;
} ) ;
it ( "keeps message_thread_id for group chat sends (#17242)" , async ( ) = > {
// Group/supergroup chats have negative IDs.
const chatId = "-1001234567890" ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 57 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
await sendMessageTelegram ( chatId , "hello group" , {
token : "tok" ,
api ,
messageThreadId : 271 ,
} ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , "hello group" , {
parse_mode : "HTML" ,
message_thread_id : 271 ,
} ) ;
} ) ;
2026-02-09 08:35:53 +05:30
it ( "retries without message_thread_id when Telegram reports missing thread" , async ( ) = > {
2026-02-16 12:01:35 +01:00
const chatId = "-100123" ;
2026-02-09 08:35:53 +05:30
const threadErr = new Error ( "400: Bad Request: message thread not found" ) ;
const sendMessage = vi
. fn ( )
. mockRejectedValueOnce ( threadErr )
. mockResolvedValueOnce ( {
message_id : 58 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
const res = await sendMessageTelegram ( chatId , "hello forum" , {
token : "tok" ,
api ,
messageThreadId : 271 ,
} ) ;
expect ( sendMessage ) . toHaveBeenNthCalledWith ( 1 , chatId , "hello forum" , {
parse_mode : "HTML" ,
message_thread_id : 271 ,
} ) ;
expect ( sendMessage ) . toHaveBeenNthCalledWith ( 2 , chatId , "hello forum" , {
parse_mode : "HTML" ,
} ) ;
expect ( res . messageId ) . toBe ( "58" ) ;
} ) ;
2026-02-17 13:03:20 +05:30
it ( "retries private chat sends without message_thread_id on thread-not-found" , async ( ) = > {
const chatId = "123456789" ;
const threadErr = new Error ( "400: Bad Request: message thread not found" ) ;
const sendMessage = vi
. fn ( )
. mockRejectedValueOnce ( threadErr )
. mockResolvedValueOnce ( {
message_id : 59 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
const res = await sendMessageTelegram ( chatId , "hello private" , {
token : "tok" ,
api ,
messageThreadId : 271 ,
} ) ;
expect ( sendMessage ) . toHaveBeenNthCalledWith ( 1 , chatId , "hello private" , {
parse_mode : "HTML" ,
message_thread_id : 271 ,
} ) ;
expect ( sendMessage ) . toHaveBeenNthCalledWith ( 2 , chatId , "hello private" , {
parse_mode : "HTML" ,
} ) ;
expect ( res . messageId ) . toBe ( "59" ) ;
} ) ;
2026-02-09 08:43:40 +05:30
it ( "does not retry thread-not-found when no message_thread_id was provided" , async ( ) = > {
const chatId = "123" ;
const threadErr = new Error ( "400: Bad Request: message thread not found" ) ;
const sendMessage = vi . fn ( ) . mockRejectedValueOnce ( threadErr ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
await expect (
sendMessageTelegram ( chatId , "hello forum" , {
token : "tok" ,
api ,
} ) ,
) . rejects . toThrow ( "message thread not found" ) ;
expect ( sendMessage ) . toHaveBeenCalledTimes ( 1 ) ;
} ) ;
2026-02-17 13:03:20 +05:30
it ( "does not retry without message_thread_id on chat-not-found" , async ( ) = > {
const chatId = "123456789" ;
const chatErr = new Error ( "400: Bad Request: chat not found" ) ;
const sendMessage = vi . fn ( ) . mockRejectedValueOnce ( chatErr ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
await expect (
sendMessageTelegram ( chatId , "hello private" , {
token : "tok" ,
api ,
messageThreadId : 271 ,
} ) ,
) . rejects . toThrow ( /chat not found/i ) ;
expect ( sendMessage ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , "hello private" , {
parse_mode : "HTML" ,
message_thread_id : 271 ,
} ) ;
} ) ;
2026-01-27 02:44:13 +05:30
it ( "sets disable_notification when silent is true" , async ( ) = > {
const chatId = "123" ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 1 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
await sendMessageTelegram ( chatId , "hi" , {
token : "tok" ,
api ,
silent : true ,
} ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , "hi" , {
parse_mode : "HTML" ,
disable_notification : true ,
} ) ;
} ) ;
2026-01-08 21:38:59 +01:00
it ( "parses message_thread_id from recipient string (telegram:group:...:topic:...)" , async ( ) = > {
const chatId = "-1001234567890" ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 55 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
2026-01-14 14:31:43 +00:00
await sendMessageTelegram ( ` telegram:group: ${ chatId } :topic:271 ` , "hello forum" , {
token : "tok" ,
api ,
} ) ;
2026-01-08 21:38:59 +01:00
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , "hello forum" , {
parse_mode : "HTML" ,
message_thread_id : 271 ,
} ) ;
} ) ;
2026-01-07 03:24:56 -03:00
it ( "includes reply_to_message_id for threaded replies" , async ( ) = > {
const chatId = "123" ;
const sendMessage = vi . fn ( ) . mockResolvedValue ( {
message_id : 56 ,
chat : { id : chatId } ,
} ) ;
const api = { sendMessage } as unknown as {
sendMessage : typeof sendMessage ;
} ;
await sendMessageTelegram ( chatId , "reply text" , {
token : "tok" ,
api ,
replyToMessageId : 100 ,
} ) ;
expect ( sendMessage ) . toHaveBeenCalledWith ( chatId , "reply text" , {
2026-01-08 02:34:32 +01:00
parse_mode : "HTML" ,
2026-01-07 03:24:56 -03:00
reply_to_message_id : 100 ,
} ) ;
} ) ;
2026-02-09 08:35:53 +05:30
it ( "retries media sends without message_thread_id when thread is missing" , async ( ) = > {
2026-02-16 12:01:35 +01:00
const chatId = "-100123" ;
2026-02-09 08:35:53 +05:30
const threadErr = new Error ( "400: Bad Request: message thread not found" ) ;
const sendPhoto = vi
. fn ( )
. mockRejectedValueOnce ( threadErr )
. mockResolvedValueOnce ( {
message_id : 59 ,
chat : { id : chatId } ,
} ) ;
const api = { sendPhoto } as unknown as {
sendPhoto : typeof sendPhoto ;
} ;
loadWebMedia . mockResolvedValueOnce ( {
buffer : Buffer.from ( "fake-image" ) ,
contentType : "image/jpeg" ,
fileName : "photo.jpg" ,
} ) ;
const res = await sendMessageTelegram ( chatId , "photo" , {
token : "tok" ,
api ,
mediaUrl : "https://example.com/photo.jpg" ,
messageThreadId : 271 ,
} ) ;
expect ( sendPhoto ) . toHaveBeenNthCalledWith ( 1 , chatId , expect . anything ( ) , {
caption : "photo" ,
parse_mode : "HTML" ,
message_thread_id : 271 ,
} ) ;
expect ( sendPhoto ) . toHaveBeenNthCalledWith ( 2 , chatId , expect . anything ( ) , {
caption : "photo" ,
parse_mode : "HTML" ,
} ) ;
expect ( res . messageId ) . toBe ( "59" ) ;
} ) ;
2026-01-07 04:10:13 +01:00
} ) ;
2026-01-26 22:07:43 +00:00
2026-02-15 20:43:53 +00:00
describe ( "reactMessageTelegram" , ( ) = > {
it ( "sends emoji reactions" , async ( ) = > {
const setMessageReaction = vi . fn ( ) . mockResolvedValue ( undefined ) ;
const api = { setMessageReaction } as unknown as {
setMessageReaction : typeof setMessageReaction ;
} ;
await reactMessageTelegram ( "telegram:123" , "456" , "✅" , {
token : "tok" ,
api ,
} ) ;
expect ( setMessageReaction ) . toHaveBeenCalledWith ( "123" , 456 , [ { type : "emoji" , emoji : "✅" } ] ) ;
} ) ;
it ( "removes reactions when emoji is empty" , async ( ) = > {
const setMessageReaction = vi . fn ( ) . mockResolvedValue ( undefined ) ;
const api = { setMessageReaction } as unknown as {
setMessageReaction : typeof setMessageReaction ;
} ;
await reactMessageTelegram ( "123" , 456 , "" , {
token : "tok" ,
api ,
} ) ;
expect ( setMessageReaction ) . toHaveBeenCalledWith ( "123" , 456 , [ ] ) ;
} ) ;
it ( "removes reactions when remove flag is set" , async ( ) = > {
const setMessageReaction = vi . fn ( ) . mockResolvedValue ( undefined ) ;
const api = { setMessageReaction } as unknown as {
setMessageReaction : typeof setMessageReaction ;
} ;
await reactMessageTelegram ( "123" , 456 , "✅" , {
token : "tok" ,
api ,
remove : true ,
} ) ;
expect ( setMessageReaction ) . toHaveBeenCalledWith ( "123" , 456 , [ ] ) ;
} ) ;
} ) ;
2026-01-26 22:07:43 +00:00
describe ( "sendStickerTelegram" , ( ) = > {
beforeEach ( ( ) = > {
loadConfig . mockReturnValue ( { } ) ;
botApi . sendSticker . mockReset ( ) ;
botCtorSpy . mockReset ( ) ;
} ) ;
it ( "sends a sticker by file_id" , async ( ) = > {
const chatId = "123" ;
const fileId = "CAACAgIAAxkBAAI...sticker_file_id" ;
const sendSticker = vi . fn ( ) . mockResolvedValue ( {
message_id : 100 ,
chat : { id : chatId } ,
} ) ;
const api = { sendSticker } as unknown as {
sendSticker : typeof sendSticker ;
} ;
const res = await sendStickerTelegram ( chatId , fileId , {
token : "tok" ,
api ,
} ) ;
expect ( sendSticker ) . toHaveBeenCalledWith ( chatId , fileId , undefined ) ;
expect ( res . messageId ) . toBe ( "100" ) ;
expect ( res . chatId ) . toBe ( chatId ) ;
} ) ;
2026-02-01 22:21:26 +00:00
it ( "throws error when fileId is blank" , async ( ) = > {
for ( const fileId of [ "" , " " ] ) {
await expect ( sendStickerTelegram ( "123" , fileId , { token : "tok" } ) ) . rejects . toThrow (
/file_id is required/i ,
) ;
}
2026-01-26 22:07:43 +00:00
} ) ;
2026-02-09 08:35:53 +05:30
it ( "retries sticker sends without message_thread_id when thread is missing" , async ( ) = > {
2026-02-16 12:01:35 +01:00
const chatId = "-100123" ;
2026-02-09 08:35:53 +05:30
const threadErr = new Error ( "400: Bad Request: message thread not found" ) ;
const sendSticker = vi
. fn ( )
. mockRejectedValueOnce ( threadErr )
. mockResolvedValueOnce ( {
message_id : 109 ,
chat : { id : chatId } ,
} ) ;
const api = { sendSticker } as unknown as {
sendSticker : typeof sendSticker ;
} ;
const res = await sendStickerTelegram ( chatId , "fileId123" , {
token : "tok" ,
api ,
messageThreadId : 271 ,
} ) ;
expect ( sendSticker ) . toHaveBeenNthCalledWith ( 1 , chatId , "fileId123" , {
message_thread_id : 271 ,
} ) ;
expect ( sendSticker ) . toHaveBeenNthCalledWith ( 2 , chatId , "fileId123" , undefined ) ;
expect ( res . messageId ) . toBe ( "109" ) ;
} ) ;
2026-01-26 22:07:43 +00:00
it ( "includes reply_to_message_id for threaded replies" , async ( ) = > {
const chatId = "123" ;
const fileId = "CAACAgIAAxkBAAI...sticker_file_id" ;
const sendSticker = vi . fn ( ) . mockResolvedValue ( {
message_id : 102 ,
chat : { id : chatId } ,
} ) ;
const api = { sendSticker } as unknown as {
sendSticker : typeof sendSticker ;
} ;
await sendStickerTelegram ( chatId , fileId , {
token : "tok" ,
api ,
replyToMessageId : 500 ,
} ) ;
expect ( sendSticker ) . toHaveBeenCalledWith ( chatId , fileId , {
reply_to_message_id : 500 ,
} ) ;
} ) ;
it ( "wraps chat-not-found with actionable context" , async ( ) = > {
const chatId = "123" ;
const err = new Error ( "400: Bad Request: chat not found" ) ;
const sendSticker = vi . fn ( ) . mockRejectedValue ( err ) ;
const api = { sendSticker } as unknown as {
sendSticker : typeof sendSticker ;
} ;
await expect ( sendStickerTelegram ( chatId , "fileId123" , { token : "tok" , api } ) ) . rejects . toThrow (
/chat not found/i ,
) ;
await expect ( sendStickerTelegram ( chatId , "fileId123" , { token : "tok" , api } ) ) . rejects . toThrow (
/chat_id=123/ ,
) ;
} ) ;
it ( "trims whitespace from fileId" , async ( ) = > {
const chatId = "123" ;
const sendSticker = vi . fn ( ) . mockResolvedValue ( {
message_id : 106 ,
chat : { id : chatId } ,
} ) ;
const api = { sendSticker } as unknown as {
sendSticker : typeof sendSticker ;
} ;
await sendStickerTelegram ( chatId , " fileId123 " , {
token : "tok" ,
api ,
} ) ;
expect ( sendSticker ) . toHaveBeenCalledWith ( chatId , "fileId123" , undefined ) ;
} ) ;
} ) ;
2026-02-15 20:43:53 +00:00
describe ( "editMessageTelegram" , ( ) = > {
beforeEach ( ( ) = > {
botApi . editMessageText . mockReset ( ) ;
botCtorSpy . mockReset ( ) ;
} ) ;
it ( "keeps existing buttons when buttons is undefined (no reply_markup)" , async ( ) = > {
botApi . editMessageText . mockResolvedValue ( { message_id : 1 , chat : { id : "123" } } ) ;
await editMessageTelegram ( "123" , 1 , "hi" , {
token : "tok" ,
cfg : { } ,
} ) ;
expect ( botCtorSpy ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( botCtorSpy . mock . calls [ 0 ] ? . [ 0 ] ) . toBe ( "tok" ) ;
expect ( botApi . editMessageText ) . toHaveBeenCalledTimes ( 1 ) ;
const params = ( botApi . editMessageText . mock . calls [ 0 ] ? ? [ ] ) [ 3 ] as Record < string , unknown > ;
expect ( params ) . toEqual ( expect . objectContaining ( { parse_mode : "HTML" } ) ) ;
expect ( params ) . not . toHaveProperty ( "reply_markup" ) ;
} ) ;
it ( "removes buttons when buttons is empty (reply_markup.inline_keyboard = [])" , async ( ) = > {
botApi . editMessageText . mockResolvedValue ( { message_id : 1 , chat : { id : "123" } } ) ;
await editMessageTelegram ( "123" , 1 , "hi" , {
token : "tok" ,
cfg : { } ,
buttons : [ ] ,
} ) ;
expect ( botApi . editMessageText ) . toHaveBeenCalledTimes ( 1 ) ;
const params = ( botApi . editMessageText . mock . calls [ 0 ] ? ? [ ] ) [ 3 ] as Record < string , unknown > ;
expect ( params ) . toEqual (
expect . objectContaining ( {
parse_mode : "HTML" ,
reply_markup : { inline_keyboard : [ ] } ,
} ) ,
) ;
} ) ;
it ( "falls back to plain text when Telegram HTML parse fails (and preserves reply_markup)" , async ( ) = > {
botApi . editMessageText
. mockRejectedValueOnce ( new Error ( "400: Bad Request: can't parse entities" ) )
. mockResolvedValueOnce ( { message_id : 1 , chat : { id : "123" } } ) ;
await editMessageTelegram ( "123" , 1 , "<bad> html" , {
token : "tok" ,
cfg : { } ,
buttons : [ ] ,
} ) ;
expect ( botApi . editMessageText ) . toHaveBeenCalledTimes ( 2 ) ;
const firstParams = ( botApi . editMessageText . mock . calls [ 0 ] ? ? [ ] ) [ 3 ] as Record < string , unknown > ;
expect ( firstParams ) . toEqual (
expect . objectContaining ( {
parse_mode : "HTML" ,
reply_markup : { inline_keyboard : [ ] } ,
} ) ,
) ;
const secondParams = ( botApi . editMessageText . mock . calls [ 1 ] ? ? [ ] ) [ 3 ] as Record < string , unknown > ;
expect ( secondParams ) . toEqual (
expect . objectContaining ( {
reply_markup : { inline_keyboard : [ ] } ,
} ) ,
) ;
} ) ;
2026-02-16 04:18:17 +01:00
it ( "treats 'message is not modified' as success" , async ( ) = > {
botApi . editMessageText . mockRejectedValueOnce (
new Error (
"400: Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message" ,
) ,
) ;
await expect (
editMessageTelegram ( "123" , 1 , "hi" , {
token : "tok" ,
cfg : { } ,
} ) ,
) . resolves . toEqual ( { ok : true , messageId : "1" , chatId : "123" } ) ;
expect ( botApi . editMessageText ) . toHaveBeenCalledTimes ( 1 ) ;
} ) ;
2026-02-15 20:43:53 +00:00
it ( "disables link previews when linkPreview is false" , async ( ) = > {
botApi . editMessageText . mockResolvedValue ( { message_id : 1 , chat : { id : "123" } } ) ;
await editMessageTelegram ( "123" , 1 , "https://example.com" , {
token : "tok" ,
cfg : { } ,
linkPreview : false ,
} ) ;
expect ( botApi . editMessageText ) . toHaveBeenCalledTimes ( 1 ) ;
const params = ( botApi . editMessageText . mock . calls [ 0 ] ? ? [ ] ) [ 3 ] as Record < string , unknown > ;
expect ( params ) . toEqual (
expect . objectContaining ( {
parse_mode : "HTML" ,
link_preview_options : { is_disabled : true } ,
} ) ,
) ;
} ) ;
} ) ;
describe ( "sendPollTelegram" , ( ) = > {
it ( "maps durationSeconds to open_period" , async ( ) = > {
const api = {
sendPoll : vi.fn ( async ( ) = > ( { message_id : 123 , chat : { id : 555 } , poll : { id : "p1" } } ) ) ,
} ;
const res = await sendPollTelegram (
"123" ,
{ question : " Q " , options : [ " A " , "B " ] , durationSeconds : 60 } ,
{ token : "t" , api : api as unknown as Bot [ "api" ] } ,
) ;
expect ( res ) . toEqual ( { messageId : "123" , chatId : "555" , pollId : "p1" } ) ;
expect ( api . sendPoll ) . toHaveBeenCalledTimes ( 1 ) ;
2026-02-17 14:30:36 +09:00
const sendPollMock = api . sendPoll as ReturnType < typeof vi.fn > ;
expect ( sendPollMock . mock . calls [ 0 ] ? . [ 0 ] ) . toBe ( "123" ) ;
expect ( sendPollMock . mock . calls [ 0 ] ? . [ 1 ] ) . toBe ( "Q" ) ;
expect ( sendPollMock . mock . calls [ 0 ] ? . [ 2 ] ) . toEqual ( [ "A" , "B" ] ) ;
expect ( sendPollMock . mock . calls [ 0 ] ? . [ 3 ] ) . toMatchObject ( { open_period : 60 } ) ;
2026-02-15 20:43:53 +00:00
} ) ;
it ( "retries without message_thread_id on thread-not-found" , async ( ) = > {
const api = {
sendPoll : vi.fn (
async ( _chatId : string , _question : string , _options : string [ ] , params : unknown ) = > {
const p = params as { message_thread_id? : unknown } | undefined ;
if ( p ? . message_thread_id ) {
throw new Error ( "400: Bad Request: message thread not found" ) ;
}
return { message_id : 1 , chat : { id : 2 } , poll : { id : "p2" } } ;
} ,
) ,
} ;
const res = await sendPollTelegram (
2026-02-16 12:01:35 +01:00
"-100123" ,
2026-02-15 20:43:53 +00:00
{ question : "Q" , options : [ "A" , "B" ] } ,
{ token : "t" , api : api as unknown as Bot [ "api" ] , messageThreadId : 99 } ,
) ;
expect ( res ) . toEqual ( { messageId : "1" , chatId : "2" , pollId : "p2" } ) ;
expect ( api . sendPoll ) . toHaveBeenCalledTimes ( 2 ) ;
expect ( api . sendPoll . mock . calls [ 0 ] ? . [ 3 ] ) . toMatchObject ( { message_thread_id : 99 } ) ;
expect (
( api . sendPoll . mock . calls [ 1 ] ? . [ 3 ] as { message_thread_id? : unknown } | undefined )
? . message_thread_id ,
) . toBeUndefined ( ) ;
} ) ;
it ( "rejects durationHours for Telegram polls" , async ( ) = > {
const api = { sendPoll : vi.fn ( ) } ;
await expect (
sendPollTelegram (
"123" ,
{ question : "Q" , options : [ "A" , "B" ] , durationHours : 1 } ,
{ token : "t" , api : api as unknown as Bot [ "api" ] } ,
) ,
) . rejects . toThrow ( /durationHours is not supported/i ) ;
expect ( api . sendPoll ) . not . toHaveBeenCalled ( ) ;
} ) ;
} ) ;
feat(telegram): add forum topic creation support (#17035)
* Revert "fix(gateway): set explicit chat timeouts for mesh gateway calls"
This reverts commit c529e6005a3e9e112883d86a973abc1e59e22e1d.
* Revert "fix: capture init script exit codes instead of swallowing via pipe"
This reverts commit 8b14052ebe9d0ced947de214e2ac20ed384013c3.
* Revert "feat(docker): add init script support via /openclaw-init.d/"
This reverts commit 53af9f743767e7ffca3c95b572a7d5451cfc9e4a.
* Revert "Agents: improve Windows scaffold helpers for venture studio"
This reverts commit b6d934c2c7da1276ce5a001ef7f1d16773918f0c.
* chore: Fix types in tests 1/N.
* chore: Fix types in tests 2/N.
* Revert "fix: remove stderr suppression so install failures are visible in build logs"
This reverts commit 717caa97fbe023a0d2f14b8e2cbad456954d6d2c.
* Revert "fix(docker): ensure memory-lancedb deps installed in Docker image"
This reverts commit 2ab6313d99d5c794f2ebcac30534533b6b26755f.
* Revert "fix: add windowsHide: true to spawn in runCommandWithTimeout"
This reverts commit 32c66aff49fcdb295484dd94f56c9c8a8f0d9482.
* Revert "Onboarding: fix webchat URL loopback and canonical session"
This reverts commit 59e0e7e4ffeb6853e4be24f3c25c5ceb1a212f2e.
* Revert "feat(linq): add interactive onboarding adapter"
This reverts commit b91e43714b4e937f33f1c63fe781eb98e20d8756.
* Revert "feat: add Linq channel — real iMessage via API, no Mac required"
This reverts commit d4a142fd8f1cc2e2d5f634c919a20cc9bf864475.
* docs: clarify discord proxy scope for startup REST calls
* Revert "fix: flatten remaining anyOf/oneOf in Gemini schema cleaning"
This reverts commit 06b961b0371f1dbea621c73d25951b3b44113941.
* Revert "fix: session-memory hook finds previous session file after /new/reset"
This reverts commit d6acd71576cda5a319abbcc244614eb5be91045d.
* Revert "fix: respect OPENCLAW_HOME for isolated gateway instances"
This reverts commit 34b18ea9db116730de38eab19c74492518b8567d.
* fix(process): harden graceful kill-tree cancellation semantics
* fix(slack): scope attachment extraction to forwarded shares
* docs(changelog): note process kill-tree hotfix
* docs(changelog): note slack forwarded attachment hotfix
* fix(session-memory): harden reset transcript recovery
* revert(telegram): undo accidental merge of PR #18601
* fix(ui): preserve locale bootstrap and trusted-proxy overview behavior
* fix(scripts): harden Windows UI spawn behavior
* fix(slack): validate interaction payloads and handle malformed actions
* fix(mattermost): harden react remove flag parsing
* docs(changelog): record PR 18608 fixups
* fix(heartbeat): bound responsePrefix strip for ack detection
* chore: Fix types in tests 3/N.
* chore: chore: Fix types in tests 4/N.
* chore: Fix types in tests 5/N.
* chore: Fix types in tests 6/N.
* chore: Format files.
* chore: Fix types that were broken due to reverts.
* chore: Cleanup unused vars that were leftover from the reverts.
* fix(actions): layer per-account gate fallback
* fix(subagents): pass group context in /subagents spawn
* fix(failover): align abort timeout detection and regressions
* fix(models): sync auth-profiles before availability checks
* fix(ui): correct usage range totals and muted styles
* Revert "feat: show transcript file size in session status"
This reverts commit 15dd2cda209ccabc9febc25e16eec620137ae744.
* revert(doctor): undo accidental merge of PR #18591
* fix(agents): align session lock hold budget with run timeouts
* Revert "fix: resolve #12770 - update Antigravity default model and trim leading whitespace in BlueBubbles replies"
This reverts commit e179d453c7d8aa3ffdc2dbd67fa5f5fbf93aa016.
* revert(tools): undo accidental merge of PR #18584
* revert(tools): finish rollback of PR #18584
* chore: Fix Slack test.
* revert: remove accidentally merged video-quote-finder skill (#18550)
* revert: accidental merge of OC-09 sandbox env sanitization change
* fix(doctor): move forced exit to top-level command
* chore: Fix types in tests 7/N.
* chore: Fix types in tests 8/N.
* chore: Fix types in tests 9/N.
* chore: Fix types in tests 10/N.
* chore: Fix types in tests 11/N.
* chore: chore: Fix types in tests 12/N.
* chore: Fix type errors from reverts.
* fix(gateway): remove watch-mode build/start race (#18782)
* fix(doctor): repair googlechat open dm wildcard auto-fix
* test(extensions): cast fetch mocks to satisfy tsgo
* fix(gateway): harden channel health monitor recovery
* fix(reply): track messaging media aliases for dedupe
* refactor(plugins): split before-agent hooks by model and prompt phases
* revert(telegram): undo accidental merge of PR #18564
* fix(agents): restore multi-image image tool schema contract
* chore: Format files.
* fix(ui): gate sessions refresh on successful delete
* revert(docs): undo accidental merge of #18516
* revert(exec): undo accidental merge of PR #18521
* docs(cron): clarify webhook posting summary condition
* fix(gateway): preserve chat.history context under hard caps
* chore: Fix types in tests 13/N.
* chore: Fix types in tests 14/N.
* chore: Fix types in tests 15/N.
* chore: Fix types in tests 16/N.
* chore: Fix types in tests 17/N.
* chore: Fix types in tests 18/N.
* chore: Format files.
* revert(sandbox): revert SHA-1 slug restoration
* test(session): cover stale threadId fallback
* test(status): cover token summary variants
* test(telegram): cover getFile file-too-big errors
* test(voice-call): cover stream disconnect auto-end
* chore(format): fix test import order
* test(agents): cover tool result media placeholders
* chore: chore: Fix types in tests 19/N.
* chore: Fix types in tests 20/N.
* chore: Fix types in tests 21/N.
* chore: Fix types in tests 22/N.
* chore: Fix types in tests 23/N.
* docs(voice-call): document stale call reaper config
* fix(doctor): audit env-only gateway tokens
* fix(sessions): purge deleted transcript archives
* test(docker): cover browser install build arg
* revert(gateway): restore loopback auth setup
* revert(voice-call): undo cached greeting note
* revert(voice-call): undo oxfmt formatting
* revert(voice-call): undo oxfmt formatting pass
* revert(voice-call): remove cached inbound greeting
* test: stabilize infra tests
* fix(subagents): harden announce retry guards
* Revert "fix(whatsapp): allow per-message link preview override\n\nWhatsApp messages default to enabling link previews for URLs. This adds\nsupport for overriding this behavior per-message via the \nparameter (e.g. from tool options), consistent with Telegram.\n\nFix: Updated internal WhatsApp Web API layers to pass option\ndown to Baileys ."
This reverts commit 1bef2fc68bc512c886c25ef1dfba67f58c90e811.
* fix(telegram): clear offsets on token change
* test(agents): cover exec non-zero exits
* CI: use self-hosted for labeler/automation
* Revert "channels: migrate extension account listing to factory"
This reverts commit d24340d75bf2b8e69a60f8c122ea59eaec9b0833.
* chore(format)
* chore: wtf.
* chore: Fix types.
* chore: Fix types in tests 24/N.
* chore: Fix types in tests 25/N.
* chore: Fix types in tests 26/N.
* chore: Fix types in tests 27/N.
* chore: Fix types in tests 28/N.
* chore: Fix types in tests 29/N.
* chore: Fix types in tests 30/N.
* chore: Fix types in tests 31/N.
* chore: Fix types in tests 32/N.
* fix(telegram): add initial message debounce for better push notifications (#18147)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 5e2285b6a03943a483993b540f86a0fa49d7de39
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* style(telegram): format dispatch files
* chore: Fix types in tests 33/N.
* chore: Fix types in tests 34/N.
* chore: Fix types in tests 35/N.
* chore: Fix types in tests 36/N.
* chore: Fix types in tests 37/N.
* chore: Fix types in tests 38/N.
* chore: Fix types in tests 39/N.
* chore: Fix types in tests 40/N.
* chore: Fix types in tests 41/N.
* chore: Fix types in tests 42/N.
* chore: Fix types in tests 43/N.
* chore: Fix types in tests 44/N.
* chore: Fix types in tests 45/N.
* chore: Typecheck tests.
* chore: Fix broken test.
* chore: Fix hanging test.
* fix(telegram): avoid duplicate preview bubbles in partial stream mode (#18956)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: cf4eca71d46e0c5ef1ec46af90f978b3d454c34a
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* fix: before_tool_call hook double-fires with abort signal (#16852)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 6269d617f3ac811e03cd29d915f94657da922ba1
Co-authored-by: sreuter <550246+sreuter@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* Revert "Default Telegram polls to public"
This reverts commit c43e95e011cdbdd3be9446b12d257899b4a90201.
* Revert "Fix Telegram poll action wiring"
This reverts commit 556b531a140330540a10299cd6c4907750a2c0b6.
* Revert "Add Telegram polls action to config typing"
This reverts commit 5cbfaf5cc70d8d69a39d640eea147d70589eeaaf.
* Revert "fix(telegram): wire sendPollTelegram into channel action handler (#16977)"
This reverts commit 7bb9a7dcfc3194a953f0ca20f86f9caba8a22923.
* CI: remove formal models conformance workflow (#19007)
* fix: preserve telegram dm topic thread ids
* style: drop aidev-note prefix in telegram comments
* test: pass extensionContext in abort dedupe e2e
* fix: align tool execute arg parsing for hooks
* test: type telegram action mock passthrough args
* Configure: make model picker allowlist searchable
* Configure: improve searchable model picker token matching
* Docs: add screenshot showing model picker usability issue
* fix: searchable model picker in configure (#19010) (thanks @bjesuiter)
* fix(extensions): revert openai codex auth plugin (PR #18009)
* feat(telegram): add channel_post support for bot-to-bot communication (#17857)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 27a343cd4d9c778a6017ff666d8285ae60256bf4
Co-authored-by: theSamPadilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* Revert "fix: handle forum/topics in Telegram DM thread routing (#17980)"
This reverts commit e20b87f1ba332e2b7838d05e38e3bd7d991f460d.
* Revert: undo #17974 README change
* voice-call: harden closed-loop turn loop and transcript routing (#19140)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 14a3edb00572b20348f839bbafa56ca826cee362
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* iOS onboarding: stop auth step-3 retry loop churn (#19153)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: a38ec42bdd4cf1bf5743ecd3c1d1f2bcceea91e0
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* Revert: fully roll back #17974 zh-cn UI README
* chore(subagents): add regression coverage and changelog
* fix(daemon): scope token drift warnings
* test(web): fix baileys mock typing
* test(cron): cover webhook session rollover overrides
* docs(changelog): note webhook session reuse fix
* fix(discord): normalize command allowFrom prefixes
* fix(cli): honor update restart overrides
* fix(cron): add spin-loop regression coverage
* test(gateway): cover trusted proxy trimming
* test(discord): cover audioAsVoice replies
* test(feishu): cover post mentions for other users
* fix(discord): preserve DM lastRoute user target
* Revert "fix(browser): track original port mapping for EADDRINUSE fallback"
This reverts commit 8e55503d7785e741ce9995f989d944e09a8f22db.
* Revert "fix(browser): handle EADDRINUSE with automatic port fallback"
This reverts commit 0e6daa2e6e30240ce5191ee5c7954f24b4b56b42.
* test(discord): fix mock call arg typing
* Revert: fully roll back #17986 templates
* test: add fetch mock helper and reaction coverage
* CLI: approve latest pending device request
* docs(readme): remove Android install link
* revert(agents): remove llms.txt discovery prompt (#19192)
* fix(ui): revert PR #18093 directive tags (#19188)
* test(discord): cover auto-thread skip types
* test(update): cover restart gating
* docs(zai): document tool_stream defaults
* revert: per-model thinkingDefault override (#19195)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: fe2c59e222a71086864f8abfae05d2da03ad4066
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* fix(gateway): make stale token cleanup non-fatal
* Agents: add before_message_write persistence regression tests
* fix(mattermost): surface reactions support
* Tests: fix fetch mock typings for type-aware checks
* revert: fix models set catalog validation (#19194)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 7e3b2ff7afe052097c4414fc64d7e66191e8fcc3
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* test: cover cron telemetry and typed fetch mocks
* revert(agents): revert base64 image validation (#19221)
* docs(cli): add components send example
* test(sessions): add delivery info regression coverage
* fix(daemon): guard preferred node selection
* test(auto-reply): cover sender_id metadata
* revert: PR 18288 accidental merge (#19224)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 3cda31578ccf71365d5815a8374922e835efff0e
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* test(telegram): cover autoSelectFamily env precedence
* test(cron): add model fallback regression coverage
* test(release): add appcast regression coverage
* docs(changelog): remove revert entries
* docs: add maintainer application section
* docs: refine maintainer application guidance
* docs: add vision doc and link from README
* docs: add community plugins guide
* Update auto-response message for third-party extensions
* update my contributing list
* iOS: use operator session for ChatSheet RPCs (#19320)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 0753b3a1a22a1d23e871d210e317279627f9fc0f
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* fix: sanitize native command names for Telegram API (#19257)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: b608be348888505b23bb4b2f1c8c42058a28e64e
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* docs(slack): add assistant:write requirement for typing status
* chore: document sessions_spawn response note and subagent context prefix
* feat(ios): auto-select local signing team (#18421)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: bbb9c3aa48a542539dc37136e6542d1f3958f9c2
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
* fix(bluebubbles): recover outbound message IDs and include sender metadata
* fix cron announce routing and timeout handling
* changelog: add @tyler6204 credit for today's entries
* feat: share to openclaw ios app (#19424)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 0a7ab8589ac23d0743d4377683d60601a8c19e61
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* Docs: expand multi-agent routing
* docs(changelog): add missing 2026.2.16 entries and reorder by user impact
* chore(release): bump version to 2026.2.17
* fix(signal): canonicalize message targets in tool and inbound flows
* docs: tighten contribution guidance and vision links
* docs: tighten PR scope and review-size policy in vision
* fix(gateway): block cross-session fallback in node event delivery
* fix(gateway): make health monitor checks single-flight
* fix(ios): harden share relay routing and delivery guards
* fix(telegram): normalize topic-create targets and add regression tests
* feat(cron): add default stagger controls for scheduled jobs
* fix(cron): retry next-second schedule compute on undefined
* docs(security): harden gateway security guidance
* feat(models): support anthropic sonnet 4.6
* fix: wire agents.defaults.imageModel into media understanding auto-discovery
resolveAutoEntries only checked a hardcoded list of providers
(openai, anthropic, google, minimax) when looking for an image model.
agents.defaults.imageModel was never consulted by the media understanding
pipeline — it was only wired into the explicit `image` tool.
Add resolveImageModelFromAgentDefaults that reads the imageModel config
(primary + fallbacks) and inserts it into the auto-discovery chain before
the hardcoded provider list. runProviderEntry already falls back to
describeImageWithModel (via pi-ai) for providers not in the media
understanding registry, so no additional provider registration is needed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
(cherry picked from commit b381029ede72a57ef6d12d9413c98fa29501b797)
* docs: update AGENTS instructions
* fix(subagent): harden read-tool overflow guards and sticky reply threading (#19508)
* fix(gateway): avoid premature agent.wait completion on transient errors
* fix(agent): preemptively guard tool results against context overflow
* fix: harden tool-result context guard and add message_id metadata
* fix: use importOriginal in session-key mock to include DEFAULT_ACCOUNT_ID
The run.skill-filter test was mocking ../../routing/session-key.js with only
buildAgentMainSessionKey and normalizeAgentId, but the module also exports
DEFAULT_ACCOUNT_ID which is required transitively by src/web/auth-store.ts.
Switch to importOriginal pattern so all real exports are preserved alongside
the mocked functions.
* pi-runner: guard accumulated tool-result overflow in transformContext
* PI runner: compact overflowing tool-result context
* Subagent: harden tool-result context recovery
* Enhance tool-result context handling by adding support for legacy tool outputs and improving character estimation for message truncation. This includes a new function to create legacy tool results and updates to existing functions to better manage context overflow scenarios.
* Enhance iMessage handling by adding reply tag support in send functions and tests. This includes modifications to prepend or rewrite reply tags based on provided replyToId, ensuring proper message formatting for replies.
* Enhance message delivery across multiple channels by implementing sticky reply context for chunked messages. This includes preserving reply references in Discord, Telegram, and iMessage, ensuring that follow-up messages maintain their intended reply targets. Additionally, improve handling of reply tags in system prompts and tests to support consistent reply behavior.
* Enhance read tool functionality by implementing auto-paging across chunks when no explicit limit is provided, scaling output budget based on model context window. Additionally, add tests for adaptive reading behavior and capped continuation guidance for large outputs. Update related functions to support these features.
* Refine tool-result context management by stripping oversized read-tool details payloads during compaction, ensuring repeated read calls do not bypass context limits. Introduce new utility functions for handling truncation content and enhance character estimation for tool results. Add tests to validate the removal of excessive details in context overflow scenarios.
* Refine message delivery logic in Matrix and Telegram by introducing a flag to track if a text chunk was sent. This ensures that replies are only marked as delivered when a text chunk has been successfully sent, improving the accuracy of reply handling in both channels.
* fix: tighten reply threading coverage and prep fixes (#19508) (thanks @tyler6204)
* fix(hooks): backport internal message hook bridge with safe delivery semantics
* fix(subagent): update SUBAGENT_SPAWN_ACCEPTED_NOTE for clarity on auto-announcement behavior
* fix: follow-up slack streaming routing/tests (#9972) (thanks @natedenh)
* fix: reduce default image dimension from 2000px to 1200px
Large images (2000px) consume excessive context tokens when sent to LLMs.
1200px provides sufficient detail for most use cases while significantly
reducing token usage.
The 5MB byte limit remains unchanged as JPEG compression at 1200px
naturally produces smaller files.
(cherry picked from commit 40182123dd2673b4b65e6846fa066380f10b781f)
* fix(agents): make image sanitization dimension configurable
* docs(tokens): document image dimension token tradeoffs
* Whatsapp/add resolve outbound target tests (#19345)
* test(whatsapp): add resolveWhatsAppOutboundTarget test suite
* style: auto-format files
* fix(test): correct mock order for invalid allowList entry test
* feat(skills): Add 'Use when / Don't use when' routing blocks (#14521)
* feat(skills): add 'Use when / Don't use when' blocks to skill descriptions
Based on OpenAI's Shell + Skills + Compaction best practices article.
Key changes:
- Added clear routing logic to skill descriptions
- Added negative examples to prevent misfires
- Added templates/examples to github skill
- Included Blake's specific setup notes for openhue
Skills updated:
- apple-reminders: Clarify vs Clawdbot cron
- github: Clarify vs local git operations
- imsg: Clarify vs other messaging channels
- openhue: Add device inventory, room layout
- tmux: Clarify vs exec tool
- weather: Add location defaults, format codes
Reference: https://developers.openai.com/blog/skills-shell-tips
* fix(skills): restore metadata and generic CLI examples
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(agents): add generic provider api key rotation (#19587)
* feat(skills): improve descriptions with routing logic (#14577)
* feat(skills): improve descriptions with routing logic
Apply OpenAI's recommended pattern for skill descriptions:
- Add 'Use when' conditions for clear triggering
- Add 'NOT for' negative examples to reduce misfires
- Make descriptions act as routing logic, not marketing copy
Based on: https://developers.openai.com/blog/skills-shell-tips/
Skills updated:
- coding-agent: clarify when to delegate vs direct edit
- github: add boundaries vs browser/scripting
- weather: add scope limitations
Glean reported 20% drop in skill triggering without negative
examples, recovering after adding them. This change brings
Clawdbot skills in line with that pattern.
* docs(skills): clarify routing boundaries (openclaw#14577) (thanks @DylanWoodAkers)
* docs(changelog): add PR 14577 release note (openclaw#14577) (thanks @DylanWoodAkers)
---------
Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* Add frontend-design skill
* feat(telegram): add forum topic creation support (#10427)
Add `topic-create` action to the Telegram message adapter, enabling
programmatic creation of forum topics in supergroups.
Changes:
- Add `createForumTopicTelegram()` to `src/telegram/send.ts`
- Add `createForumTopic` handler in `telegram-actions.ts`
- Wire `topic-create` action in Telegram adapter
- Register `topic-create` in message action names and spec
The bot requires `can_manage_topics` permission in the target group.
Supports optional `iconColor` and `iconCustomEmojiId` parameters.
Closes #10427
* chore: fix formatting in frontend-design SKILL.md
* fix: add action gate check and config type for createForumTopic
Address review feedback:
- Add isActionEnabled() gate in telegram-actions.ts
- Add gate() check in telegram adapter listActions
- Add createForumTopic to TelegramActionConfig type
* fix(telegram): normalize topic-create targets and add regression tests
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
Co-authored-by: cpojer <christoph.pojer@gmail.com>
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
Co-authored-by: Josh Avant <830519+joshavant@users.noreply.github.com>
Co-authored-by: Shadow <hi@shadowing.dev>
Co-authored-by: Hongwei Ma <Marvae@users.noreply.github.com>
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <zaidi@uplause.io>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Sascha Reuter <s.reuter@geek-it.de>
Co-authored-by: sreuter <550246+sreuter@users.noreply.github.com>
Co-authored-by: Nimrod Gutman <nimrod.g@singular.net>
Co-authored-by: Vignesh <mailvgnsh@gmail.com>
Co-authored-by: Benjamin Jesuiter <bjesuiter@gmail.com>
Co-authored-by: Sam Padilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: Muhammed Mukhthar CM <mukhtharcm@gmail.com>
Co-authored-by: Mariano <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
Co-authored-by: Mariano Belinky <mbelinky@gmail.com>
Co-authored-by: Shadow <shadow@openclaw.ai>
Co-authored-by: Sk Akram <skcodewizard786@gmail.com>
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: Onur <onur@textcortex.com>
Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: Pablo Nunez <pnunfe@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Tyler Yust <64381258+tyler6204@users.noreply.github.com>
Co-authored-by: Han Xiao <han.xiao@jina.ai>
Co-authored-by: Verite Igiraneza <69280208+VeriteIgiraneza@users.noreply.github.com>
Co-authored-by: Blakeshannon <blake@blakeshannon.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: DylanWoodAkers <dylan@lec.com>
Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Claw <claw@openclaw.ai>
2026-02-18 00:38:44 +00:00
describe ( "createForumTopicTelegram" , ( ) = > {
it ( "uses base chat id when target includes topic suffix" , async ( ) = > {
const createForumTopic = vi . fn ( ) . mockResolvedValue ( {
message_thread_id : 272 ,
name : "Build Updates" ,
} ) ;
2026-02-18 02:09:40 +01:00
const api = { createForumTopic } as unknown as Bot [ "api" ] ;
feat(telegram): add forum topic creation support (#17035)
* Revert "fix(gateway): set explicit chat timeouts for mesh gateway calls"
This reverts commit c529e6005a3e9e112883d86a973abc1e59e22e1d.
* Revert "fix: capture init script exit codes instead of swallowing via pipe"
This reverts commit 8b14052ebe9d0ced947de214e2ac20ed384013c3.
* Revert "feat(docker): add init script support via /openclaw-init.d/"
This reverts commit 53af9f743767e7ffca3c95b572a7d5451cfc9e4a.
* Revert "Agents: improve Windows scaffold helpers for venture studio"
This reverts commit b6d934c2c7da1276ce5a001ef7f1d16773918f0c.
* chore: Fix types in tests 1/N.
* chore: Fix types in tests 2/N.
* Revert "fix: remove stderr suppression so install failures are visible in build logs"
This reverts commit 717caa97fbe023a0d2f14b8e2cbad456954d6d2c.
* Revert "fix(docker): ensure memory-lancedb deps installed in Docker image"
This reverts commit 2ab6313d99d5c794f2ebcac30534533b6b26755f.
* Revert "fix: add windowsHide: true to spawn in runCommandWithTimeout"
This reverts commit 32c66aff49fcdb295484dd94f56c9c8a8f0d9482.
* Revert "Onboarding: fix webchat URL loopback and canonical session"
This reverts commit 59e0e7e4ffeb6853e4be24f3c25c5ceb1a212f2e.
* Revert "feat(linq): add interactive onboarding adapter"
This reverts commit b91e43714b4e937f33f1c63fe781eb98e20d8756.
* Revert "feat: add Linq channel — real iMessage via API, no Mac required"
This reverts commit d4a142fd8f1cc2e2d5f634c919a20cc9bf864475.
* docs: clarify discord proxy scope for startup REST calls
* Revert "fix: flatten remaining anyOf/oneOf in Gemini schema cleaning"
This reverts commit 06b961b0371f1dbea621c73d25951b3b44113941.
* Revert "fix: session-memory hook finds previous session file after /new/reset"
This reverts commit d6acd71576cda5a319abbcc244614eb5be91045d.
* Revert "fix: respect OPENCLAW_HOME for isolated gateway instances"
This reverts commit 34b18ea9db116730de38eab19c74492518b8567d.
* fix(process): harden graceful kill-tree cancellation semantics
* fix(slack): scope attachment extraction to forwarded shares
* docs(changelog): note process kill-tree hotfix
* docs(changelog): note slack forwarded attachment hotfix
* fix(session-memory): harden reset transcript recovery
* revert(telegram): undo accidental merge of PR #18601
* fix(ui): preserve locale bootstrap and trusted-proxy overview behavior
* fix(scripts): harden Windows UI spawn behavior
* fix(slack): validate interaction payloads and handle malformed actions
* fix(mattermost): harden react remove flag parsing
* docs(changelog): record PR 18608 fixups
* fix(heartbeat): bound responsePrefix strip for ack detection
* chore: Fix types in tests 3/N.
* chore: chore: Fix types in tests 4/N.
* chore: Fix types in tests 5/N.
* chore: Fix types in tests 6/N.
* chore: Format files.
* chore: Fix types that were broken due to reverts.
* chore: Cleanup unused vars that were leftover from the reverts.
* fix(actions): layer per-account gate fallback
* fix(subagents): pass group context in /subagents spawn
* fix(failover): align abort timeout detection and regressions
* fix(models): sync auth-profiles before availability checks
* fix(ui): correct usage range totals and muted styles
* Revert "feat: show transcript file size in session status"
This reverts commit 15dd2cda209ccabc9febc25e16eec620137ae744.
* revert(doctor): undo accidental merge of PR #18591
* fix(agents): align session lock hold budget with run timeouts
* Revert "fix: resolve #12770 - update Antigravity default model and trim leading whitespace in BlueBubbles replies"
This reverts commit e179d453c7d8aa3ffdc2dbd67fa5f5fbf93aa016.
* revert(tools): undo accidental merge of PR #18584
* revert(tools): finish rollback of PR #18584
* chore: Fix Slack test.
* revert: remove accidentally merged video-quote-finder skill (#18550)
* revert: accidental merge of OC-09 sandbox env sanitization change
* fix(doctor): move forced exit to top-level command
* chore: Fix types in tests 7/N.
* chore: Fix types in tests 8/N.
* chore: Fix types in tests 9/N.
* chore: Fix types in tests 10/N.
* chore: Fix types in tests 11/N.
* chore: chore: Fix types in tests 12/N.
* chore: Fix type errors from reverts.
* fix(gateway): remove watch-mode build/start race (#18782)
* fix(doctor): repair googlechat open dm wildcard auto-fix
* test(extensions): cast fetch mocks to satisfy tsgo
* fix(gateway): harden channel health monitor recovery
* fix(reply): track messaging media aliases for dedupe
* refactor(plugins): split before-agent hooks by model and prompt phases
* revert(telegram): undo accidental merge of PR #18564
* fix(agents): restore multi-image image tool schema contract
* chore: Format files.
* fix(ui): gate sessions refresh on successful delete
* revert(docs): undo accidental merge of #18516
* revert(exec): undo accidental merge of PR #18521
* docs(cron): clarify webhook posting summary condition
* fix(gateway): preserve chat.history context under hard caps
* chore: Fix types in tests 13/N.
* chore: Fix types in tests 14/N.
* chore: Fix types in tests 15/N.
* chore: Fix types in tests 16/N.
* chore: Fix types in tests 17/N.
* chore: Fix types in tests 18/N.
* chore: Format files.
* revert(sandbox): revert SHA-1 slug restoration
* test(session): cover stale threadId fallback
* test(status): cover token summary variants
* test(telegram): cover getFile file-too-big errors
* test(voice-call): cover stream disconnect auto-end
* chore(format): fix test import order
* test(agents): cover tool result media placeholders
* chore: chore: Fix types in tests 19/N.
* chore: Fix types in tests 20/N.
* chore: Fix types in tests 21/N.
* chore: Fix types in tests 22/N.
* chore: Fix types in tests 23/N.
* docs(voice-call): document stale call reaper config
* fix(doctor): audit env-only gateway tokens
* fix(sessions): purge deleted transcript archives
* test(docker): cover browser install build arg
* revert(gateway): restore loopback auth setup
* revert(voice-call): undo cached greeting note
* revert(voice-call): undo oxfmt formatting
* revert(voice-call): undo oxfmt formatting pass
* revert(voice-call): remove cached inbound greeting
* test: stabilize infra tests
* fix(subagents): harden announce retry guards
* Revert "fix(whatsapp): allow per-message link preview override\n\nWhatsApp messages default to enabling link previews for URLs. This adds\nsupport for overriding this behavior per-message via the \nparameter (e.g. from tool options), consistent with Telegram.\n\nFix: Updated internal WhatsApp Web API layers to pass option\ndown to Baileys ."
This reverts commit 1bef2fc68bc512c886c25ef1dfba67f58c90e811.
* fix(telegram): clear offsets on token change
* test(agents): cover exec non-zero exits
* CI: use self-hosted for labeler/automation
* Revert "channels: migrate extension account listing to factory"
This reverts commit d24340d75bf2b8e69a60f8c122ea59eaec9b0833.
* chore(format)
* chore: wtf.
* chore: Fix types.
* chore: Fix types in tests 24/N.
* chore: Fix types in tests 25/N.
* chore: Fix types in tests 26/N.
* chore: Fix types in tests 27/N.
* chore: Fix types in tests 28/N.
* chore: Fix types in tests 29/N.
* chore: Fix types in tests 30/N.
* chore: Fix types in tests 31/N.
* chore: Fix types in tests 32/N.
* fix(telegram): add initial message debounce for better push notifications (#18147)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 5e2285b6a03943a483993b540f86a0fa49d7de39
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* style(telegram): format dispatch files
* chore: Fix types in tests 33/N.
* chore: Fix types in tests 34/N.
* chore: Fix types in tests 35/N.
* chore: Fix types in tests 36/N.
* chore: Fix types in tests 37/N.
* chore: Fix types in tests 38/N.
* chore: Fix types in tests 39/N.
* chore: Fix types in tests 40/N.
* chore: Fix types in tests 41/N.
* chore: Fix types in tests 42/N.
* chore: Fix types in tests 43/N.
* chore: Fix types in tests 44/N.
* chore: Fix types in tests 45/N.
* chore: Typecheck tests.
* chore: Fix broken test.
* chore: Fix hanging test.
* fix(telegram): avoid duplicate preview bubbles in partial stream mode (#18956)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: cf4eca71d46e0c5ef1ec46af90f978b3d454c34a
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* fix: before_tool_call hook double-fires with abort signal (#16852)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 6269d617f3ac811e03cd29d915f94657da922ba1
Co-authored-by: sreuter <550246+sreuter@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* Revert "Default Telegram polls to public"
This reverts commit c43e95e011cdbdd3be9446b12d257899b4a90201.
* Revert "Fix Telegram poll action wiring"
This reverts commit 556b531a140330540a10299cd6c4907750a2c0b6.
* Revert "Add Telegram polls action to config typing"
This reverts commit 5cbfaf5cc70d8d69a39d640eea147d70589eeaaf.
* Revert "fix(telegram): wire sendPollTelegram into channel action handler (#16977)"
This reverts commit 7bb9a7dcfc3194a953f0ca20f86f9caba8a22923.
* CI: remove formal models conformance workflow (#19007)
* fix: preserve telegram dm topic thread ids
* style: drop aidev-note prefix in telegram comments
* test: pass extensionContext in abort dedupe e2e
* fix: align tool execute arg parsing for hooks
* test: type telegram action mock passthrough args
* Configure: make model picker allowlist searchable
* Configure: improve searchable model picker token matching
* Docs: add screenshot showing model picker usability issue
* fix: searchable model picker in configure (#19010) (thanks @bjesuiter)
* fix(extensions): revert openai codex auth plugin (PR #18009)
* feat(telegram): add channel_post support for bot-to-bot communication (#17857)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 27a343cd4d9c778a6017ff666d8285ae60256bf4
Co-authored-by: theSamPadilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* Revert "fix: handle forum/topics in Telegram DM thread routing (#17980)"
This reverts commit e20b87f1ba332e2b7838d05e38e3bd7d991f460d.
* Revert: undo #17974 README change
* voice-call: harden closed-loop turn loop and transcript routing (#19140)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 14a3edb00572b20348f839bbafa56ca826cee362
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* iOS onboarding: stop auth step-3 retry loop churn (#19153)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: a38ec42bdd4cf1bf5743ecd3c1d1f2bcceea91e0
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* Revert: fully roll back #17974 zh-cn UI README
* chore(subagents): add regression coverage and changelog
* fix(daemon): scope token drift warnings
* test(web): fix baileys mock typing
* test(cron): cover webhook session rollover overrides
* docs(changelog): note webhook session reuse fix
* fix(discord): normalize command allowFrom prefixes
* fix(cli): honor update restart overrides
* fix(cron): add spin-loop regression coverage
* test(gateway): cover trusted proxy trimming
* test(discord): cover audioAsVoice replies
* test(feishu): cover post mentions for other users
* fix(discord): preserve DM lastRoute user target
* Revert "fix(browser): track original port mapping for EADDRINUSE fallback"
This reverts commit 8e55503d7785e741ce9995f989d944e09a8f22db.
* Revert "fix(browser): handle EADDRINUSE with automatic port fallback"
This reverts commit 0e6daa2e6e30240ce5191ee5c7954f24b4b56b42.
* test(discord): fix mock call arg typing
* Revert: fully roll back #17986 templates
* test: add fetch mock helper and reaction coverage
* CLI: approve latest pending device request
* docs(readme): remove Android install link
* revert(agents): remove llms.txt discovery prompt (#19192)
* fix(ui): revert PR #18093 directive tags (#19188)
* test(discord): cover auto-thread skip types
* test(update): cover restart gating
* docs(zai): document tool_stream defaults
* revert: per-model thinkingDefault override (#19195)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: fe2c59e222a71086864f8abfae05d2da03ad4066
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* fix(gateway): make stale token cleanup non-fatal
* Agents: add before_message_write persistence regression tests
* fix(mattermost): surface reactions support
* Tests: fix fetch mock typings for type-aware checks
* revert: fix models set catalog validation (#19194)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 7e3b2ff7afe052097c4414fc64d7e66191e8fcc3
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* test: cover cron telemetry and typed fetch mocks
* revert(agents): revert base64 image validation (#19221)
* docs(cli): add components send example
* test(sessions): add delivery info regression coverage
* fix(daemon): guard preferred node selection
* test(auto-reply): cover sender_id metadata
* revert: PR 18288 accidental merge (#19224)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 3cda31578ccf71365d5815a8374922e835efff0e
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* test(telegram): cover autoSelectFamily env precedence
* test(cron): add model fallback regression coverage
* test(release): add appcast regression coverage
* docs(changelog): remove revert entries
* docs: add maintainer application section
* docs: refine maintainer application guidance
* docs: add vision doc and link from README
* docs: add community plugins guide
* Update auto-response message for third-party extensions
* update my contributing list
* iOS: use operator session for ChatSheet RPCs (#19320)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 0753b3a1a22a1d23e871d210e317279627f9fc0f
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* fix: sanitize native command names for Telegram API (#19257)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: b608be348888505b23bb4b2f1c8c42058a28e64e
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* docs(slack): add assistant:write requirement for typing status
* chore: document sessions_spawn response note and subagent context prefix
* feat(ios): auto-select local signing team (#18421)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: bbb9c3aa48a542539dc37136e6542d1f3958f9c2
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
* fix(bluebubbles): recover outbound message IDs and include sender metadata
* fix cron announce routing and timeout handling
* changelog: add @tyler6204 credit for today's entries
* feat: share to openclaw ios app (#19424)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 0a7ab8589ac23d0743d4377683d60601a8c19e61
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* Docs: expand multi-agent routing
* docs(changelog): add missing 2026.2.16 entries and reorder by user impact
* chore(release): bump version to 2026.2.17
* fix(signal): canonicalize message targets in tool and inbound flows
* docs: tighten contribution guidance and vision links
* docs: tighten PR scope and review-size policy in vision
* fix(gateway): block cross-session fallback in node event delivery
* fix(gateway): make health monitor checks single-flight
* fix(ios): harden share relay routing and delivery guards
* fix(telegram): normalize topic-create targets and add regression tests
* feat(cron): add default stagger controls for scheduled jobs
* fix(cron): retry next-second schedule compute on undefined
* docs(security): harden gateway security guidance
* feat(models): support anthropic sonnet 4.6
* fix: wire agents.defaults.imageModel into media understanding auto-discovery
resolveAutoEntries only checked a hardcoded list of providers
(openai, anthropic, google, minimax) when looking for an image model.
agents.defaults.imageModel was never consulted by the media understanding
pipeline — it was only wired into the explicit `image` tool.
Add resolveImageModelFromAgentDefaults that reads the imageModel config
(primary + fallbacks) and inserts it into the auto-discovery chain before
the hardcoded provider list. runProviderEntry already falls back to
describeImageWithModel (via pi-ai) for providers not in the media
understanding registry, so no additional provider registration is needed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
(cherry picked from commit b381029ede72a57ef6d12d9413c98fa29501b797)
* docs: update AGENTS instructions
* fix(subagent): harden read-tool overflow guards and sticky reply threading (#19508)
* fix(gateway): avoid premature agent.wait completion on transient errors
* fix(agent): preemptively guard tool results against context overflow
* fix: harden tool-result context guard and add message_id metadata
* fix: use importOriginal in session-key mock to include DEFAULT_ACCOUNT_ID
The run.skill-filter test was mocking ../../routing/session-key.js with only
buildAgentMainSessionKey and normalizeAgentId, but the module also exports
DEFAULT_ACCOUNT_ID which is required transitively by src/web/auth-store.ts.
Switch to importOriginal pattern so all real exports are preserved alongside
the mocked functions.
* pi-runner: guard accumulated tool-result overflow in transformContext
* PI runner: compact overflowing tool-result context
* Subagent: harden tool-result context recovery
* Enhance tool-result context handling by adding support for legacy tool outputs and improving character estimation for message truncation. This includes a new function to create legacy tool results and updates to existing functions to better manage context overflow scenarios.
* Enhance iMessage handling by adding reply tag support in send functions and tests. This includes modifications to prepend or rewrite reply tags based on provided replyToId, ensuring proper message formatting for replies.
* Enhance message delivery across multiple channels by implementing sticky reply context for chunked messages. This includes preserving reply references in Discord, Telegram, and iMessage, ensuring that follow-up messages maintain their intended reply targets. Additionally, improve handling of reply tags in system prompts and tests to support consistent reply behavior.
* Enhance read tool functionality by implementing auto-paging across chunks when no explicit limit is provided, scaling output budget based on model context window. Additionally, add tests for adaptive reading behavior and capped continuation guidance for large outputs. Update related functions to support these features.
* Refine tool-result context management by stripping oversized read-tool details payloads during compaction, ensuring repeated read calls do not bypass context limits. Introduce new utility functions for handling truncation content and enhance character estimation for tool results. Add tests to validate the removal of excessive details in context overflow scenarios.
* Refine message delivery logic in Matrix and Telegram by introducing a flag to track if a text chunk was sent. This ensures that replies are only marked as delivered when a text chunk has been successfully sent, improving the accuracy of reply handling in both channels.
* fix: tighten reply threading coverage and prep fixes (#19508) (thanks @tyler6204)
* fix(hooks): backport internal message hook bridge with safe delivery semantics
* fix(subagent): update SUBAGENT_SPAWN_ACCEPTED_NOTE for clarity on auto-announcement behavior
* fix: follow-up slack streaming routing/tests (#9972) (thanks @natedenh)
* fix: reduce default image dimension from 2000px to 1200px
Large images (2000px) consume excessive context tokens when sent to LLMs.
1200px provides sufficient detail for most use cases while significantly
reducing token usage.
The 5MB byte limit remains unchanged as JPEG compression at 1200px
naturally produces smaller files.
(cherry picked from commit 40182123dd2673b4b65e6846fa066380f10b781f)
* fix(agents): make image sanitization dimension configurable
* docs(tokens): document image dimension token tradeoffs
* Whatsapp/add resolve outbound target tests (#19345)
* test(whatsapp): add resolveWhatsAppOutboundTarget test suite
* style: auto-format files
* fix(test): correct mock order for invalid allowList entry test
* feat(skills): Add 'Use when / Don't use when' routing blocks (#14521)
* feat(skills): add 'Use when / Don't use when' blocks to skill descriptions
Based on OpenAI's Shell + Skills + Compaction best practices article.
Key changes:
- Added clear routing logic to skill descriptions
- Added negative examples to prevent misfires
- Added templates/examples to github skill
- Included Blake's specific setup notes for openhue
Skills updated:
- apple-reminders: Clarify vs Clawdbot cron
- github: Clarify vs local git operations
- imsg: Clarify vs other messaging channels
- openhue: Add device inventory, room layout
- tmux: Clarify vs exec tool
- weather: Add location defaults, format codes
Reference: https://developers.openai.com/blog/skills-shell-tips
* fix(skills): restore metadata and generic CLI examples
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(agents): add generic provider api key rotation (#19587)
* feat(skills): improve descriptions with routing logic (#14577)
* feat(skills): improve descriptions with routing logic
Apply OpenAI's recommended pattern for skill descriptions:
- Add 'Use when' conditions for clear triggering
- Add 'NOT for' negative examples to reduce misfires
- Make descriptions act as routing logic, not marketing copy
Based on: https://developers.openai.com/blog/skills-shell-tips/
Skills updated:
- coding-agent: clarify when to delegate vs direct edit
- github: add boundaries vs browser/scripting
- weather: add scope limitations
Glean reported 20% drop in skill triggering without negative
examples, recovering after adding them. This change brings
Clawdbot skills in line with that pattern.
* docs(skills): clarify routing boundaries (openclaw#14577) (thanks @DylanWoodAkers)
* docs(changelog): add PR 14577 release note (openclaw#14577) (thanks @DylanWoodAkers)
---------
Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* Add frontend-design skill
* feat(telegram): add forum topic creation support (#10427)
Add `topic-create` action to the Telegram message adapter, enabling
programmatic creation of forum topics in supergroups.
Changes:
- Add `createForumTopicTelegram()` to `src/telegram/send.ts`
- Add `createForumTopic` handler in `telegram-actions.ts`
- Wire `topic-create` action in Telegram adapter
- Register `topic-create` in message action names and spec
The bot requires `can_manage_topics` permission in the target group.
Supports optional `iconColor` and `iconCustomEmojiId` parameters.
Closes #10427
* chore: fix formatting in frontend-design SKILL.md
* fix: add action gate check and config type for createForumTopic
Address review feedback:
- Add isActionEnabled() gate in telegram-actions.ts
- Add gate() check in telegram adapter listActions
- Add createForumTopic to TelegramActionConfig type
* fix(telegram): normalize topic-create targets and add regression tests
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
Co-authored-by: cpojer <christoph.pojer@gmail.com>
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
Co-authored-by: Josh Avant <830519+joshavant@users.noreply.github.com>
Co-authored-by: Shadow <hi@shadowing.dev>
Co-authored-by: Hongwei Ma <Marvae@users.noreply.github.com>
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <zaidi@uplause.io>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Sascha Reuter <s.reuter@geek-it.de>
Co-authored-by: sreuter <550246+sreuter@users.noreply.github.com>
Co-authored-by: Nimrod Gutman <nimrod.g@singular.net>
Co-authored-by: Vignesh <mailvgnsh@gmail.com>
Co-authored-by: Benjamin Jesuiter <bjesuiter@gmail.com>
Co-authored-by: Sam Padilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: Muhammed Mukhthar CM <mukhtharcm@gmail.com>
Co-authored-by: Mariano <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
Co-authored-by: Mariano Belinky <mbelinky@gmail.com>
Co-authored-by: Shadow <shadow@openclaw.ai>
Co-authored-by: Sk Akram <skcodewizard786@gmail.com>
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: Onur <onur@textcortex.com>
Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: Pablo Nunez <pnunfe@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Tyler Yust <64381258+tyler6204@users.noreply.github.com>
Co-authored-by: Han Xiao <han.xiao@jina.ai>
Co-authored-by: Verite Igiraneza <69280208+VeriteIgiraneza@users.noreply.github.com>
Co-authored-by: Blakeshannon <blake@blakeshannon.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: DylanWoodAkers <dylan@lec.com>
Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Claw <claw@openclaw.ai>
2026-02-18 00:38:44 +00:00
const result = await createForumTopicTelegram ( "telegram:group:-1001234567890:topic:271" , "x" , {
token : "tok" ,
api ,
} ) ;
expect ( createForumTopic ) . toHaveBeenCalledWith ( "-1001234567890" , "x" , undefined ) ;
expect ( result ) . toEqual ( {
topicId : 272 ,
name : "Build Updates" ,
chatId : "-1001234567890" ,
} ) ;
} ) ;
it ( "forwards optional icon fields" , async ( ) = > {
const createForumTopic = vi . fn ( ) . mockResolvedValue ( {
message_thread_id : 300 ,
name : "Roadmap" ,
} ) ;
2026-02-18 02:09:40 +01:00
const api = { createForumTopic } as unknown as Bot [ "api" ] ;
feat(telegram): add forum topic creation support (#17035)
* Revert "fix(gateway): set explicit chat timeouts for mesh gateway calls"
This reverts commit c529e6005a3e9e112883d86a973abc1e59e22e1d.
* Revert "fix: capture init script exit codes instead of swallowing via pipe"
This reverts commit 8b14052ebe9d0ced947de214e2ac20ed384013c3.
* Revert "feat(docker): add init script support via /openclaw-init.d/"
This reverts commit 53af9f743767e7ffca3c95b572a7d5451cfc9e4a.
* Revert "Agents: improve Windows scaffold helpers for venture studio"
This reverts commit b6d934c2c7da1276ce5a001ef7f1d16773918f0c.
* chore: Fix types in tests 1/N.
* chore: Fix types in tests 2/N.
* Revert "fix: remove stderr suppression so install failures are visible in build logs"
This reverts commit 717caa97fbe023a0d2f14b8e2cbad456954d6d2c.
* Revert "fix(docker): ensure memory-lancedb deps installed in Docker image"
This reverts commit 2ab6313d99d5c794f2ebcac30534533b6b26755f.
* Revert "fix: add windowsHide: true to spawn in runCommandWithTimeout"
This reverts commit 32c66aff49fcdb295484dd94f56c9c8a8f0d9482.
* Revert "Onboarding: fix webchat URL loopback and canonical session"
This reverts commit 59e0e7e4ffeb6853e4be24f3c25c5ceb1a212f2e.
* Revert "feat(linq): add interactive onboarding adapter"
This reverts commit b91e43714b4e937f33f1c63fe781eb98e20d8756.
* Revert "feat: add Linq channel — real iMessage via API, no Mac required"
This reverts commit d4a142fd8f1cc2e2d5f634c919a20cc9bf864475.
* docs: clarify discord proxy scope for startup REST calls
* Revert "fix: flatten remaining anyOf/oneOf in Gemini schema cleaning"
This reverts commit 06b961b0371f1dbea621c73d25951b3b44113941.
* Revert "fix: session-memory hook finds previous session file after /new/reset"
This reverts commit d6acd71576cda5a319abbcc244614eb5be91045d.
* Revert "fix: respect OPENCLAW_HOME for isolated gateway instances"
This reverts commit 34b18ea9db116730de38eab19c74492518b8567d.
* fix(process): harden graceful kill-tree cancellation semantics
* fix(slack): scope attachment extraction to forwarded shares
* docs(changelog): note process kill-tree hotfix
* docs(changelog): note slack forwarded attachment hotfix
* fix(session-memory): harden reset transcript recovery
* revert(telegram): undo accidental merge of PR #18601
* fix(ui): preserve locale bootstrap and trusted-proxy overview behavior
* fix(scripts): harden Windows UI spawn behavior
* fix(slack): validate interaction payloads and handle malformed actions
* fix(mattermost): harden react remove flag parsing
* docs(changelog): record PR 18608 fixups
* fix(heartbeat): bound responsePrefix strip for ack detection
* chore: Fix types in tests 3/N.
* chore: chore: Fix types in tests 4/N.
* chore: Fix types in tests 5/N.
* chore: Fix types in tests 6/N.
* chore: Format files.
* chore: Fix types that were broken due to reverts.
* chore: Cleanup unused vars that were leftover from the reverts.
* fix(actions): layer per-account gate fallback
* fix(subagents): pass group context in /subagents spawn
* fix(failover): align abort timeout detection and regressions
* fix(models): sync auth-profiles before availability checks
* fix(ui): correct usage range totals and muted styles
* Revert "feat: show transcript file size in session status"
This reverts commit 15dd2cda209ccabc9febc25e16eec620137ae744.
* revert(doctor): undo accidental merge of PR #18591
* fix(agents): align session lock hold budget with run timeouts
* Revert "fix: resolve #12770 - update Antigravity default model and trim leading whitespace in BlueBubbles replies"
This reverts commit e179d453c7d8aa3ffdc2dbd67fa5f5fbf93aa016.
* revert(tools): undo accidental merge of PR #18584
* revert(tools): finish rollback of PR #18584
* chore: Fix Slack test.
* revert: remove accidentally merged video-quote-finder skill (#18550)
* revert: accidental merge of OC-09 sandbox env sanitization change
* fix(doctor): move forced exit to top-level command
* chore: Fix types in tests 7/N.
* chore: Fix types in tests 8/N.
* chore: Fix types in tests 9/N.
* chore: Fix types in tests 10/N.
* chore: Fix types in tests 11/N.
* chore: chore: Fix types in tests 12/N.
* chore: Fix type errors from reverts.
* fix(gateway): remove watch-mode build/start race (#18782)
* fix(doctor): repair googlechat open dm wildcard auto-fix
* test(extensions): cast fetch mocks to satisfy tsgo
* fix(gateway): harden channel health monitor recovery
* fix(reply): track messaging media aliases for dedupe
* refactor(plugins): split before-agent hooks by model and prompt phases
* revert(telegram): undo accidental merge of PR #18564
* fix(agents): restore multi-image image tool schema contract
* chore: Format files.
* fix(ui): gate sessions refresh on successful delete
* revert(docs): undo accidental merge of #18516
* revert(exec): undo accidental merge of PR #18521
* docs(cron): clarify webhook posting summary condition
* fix(gateway): preserve chat.history context under hard caps
* chore: Fix types in tests 13/N.
* chore: Fix types in tests 14/N.
* chore: Fix types in tests 15/N.
* chore: Fix types in tests 16/N.
* chore: Fix types in tests 17/N.
* chore: Fix types in tests 18/N.
* chore: Format files.
* revert(sandbox): revert SHA-1 slug restoration
* test(session): cover stale threadId fallback
* test(status): cover token summary variants
* test(telegram): cover getFile file-too-big errors
* test(voice-call): cover stream disconnect auto-end
* chore(format): fix test import order
* test(agents): cover tool result media placeholders
* chore: chore: Fix types in tests 19/N.
* chore: Fix types in tests 20/N.
* chore: Fix types in tests 21/N.
* chore: Fix types in tests 22/N.
* chore: Fix types in tests 23/N.
* docs(voice-call): document stale call reaper config
* fix(doctor): audit env-only gateway tokens
* fix(sessions): purge deleted transcript archives
* test(docker): cover browser install build arg
* revert(gateway): restore loopback auth setup
* revert(voice-call): undo cached greeting note
* revert(voice-call): undo oxfmt formatting
* revert(voice-call): undo oxfmt formatting pass
* revert(voice-call): remove cached inbound greeting
* test: stabilize infra tests
* fix(subagents): harden announce retry guards
* Revert "fix(whatsapp): allow per-message link preview override\n\nWhatsApp messages default to enabling link previews for URLs. This adds\nsupport for overriding this behavior per-message via the \nparameter (e.g. from tool options), consistent with Telegram.\n\nFix: Updated internal WhatsApp Web API layers to pass option\ndown to Baileys ."
This reverts commit 1bef2fc68bc512c886c25ef1dfba67f58c90e811.
* fix(telegram): clear offsets on token change
* test(agents): cover exec non-zero exits
* CI: use self-hosted for labeler/automation
* Revert "channels: migrate extension account listing to factory"
This reverts commit d24340d75bf2b8e69a60f8c122ea59eaec9b0833.
* chore(format)
* chore: wtf.
* chore: Fix types.
* chore: Fix types in tests 24/N.
* chore: Fix types in tests 25/N.
* chore: Fix types in tests 26/N.
* chore: Fix types in tests 27/N.
* chore: Fix types in tests 28/N.
* chore: Fix types in tests 29/N.
* chore: Fix types in tests 30/N.
* chore: Fix types in tests 31/N.
* chore: Fix types in tests 32/N.
* fix(telegram): add initial message debounce for better push notifications (#18147)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 5e2285b6a03943a483993b540f86a0fa49d7de39
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* style(telegram): format dispatch files
* chore: Fix types in tests 33/N.
* chore: Fix types in tests 34/N.
* chore: Fix types in tests 35/N.
* chore: Fix types in tests 36/N.
* chore: Fix types in tests 37/N.
* chore: Fix types in tests 38/N.
* chore: Fix types in tests 39/N.
* chore: Fix types in tests 40/N.
* chore: Fix types in tests 41/N.
* chore: Fix types in tests 42/N.
* chore: Fix types in tests 43/N.
* chore: Fix types in tests 44/N.
* chore: Fix types in tests 45/N.
* chore: Typecheck tests.
* chore: Fix broken test.
* chore: Fix hanging test.
* fix(telegram): avoid duplicate preview bubbles in partial stream mode (#18956)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: cf4eca71d46e0c5ef1ec46af90f978b3d454c34a
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* fix: before_tool_call hook double-fires with abort signal (#16852)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 6269d617f3ac811e03cd29d915f94657da922ba1
Co-authored-by: sreuter <550246+sreuter@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* Revert "Default Telegram polls to public"
This reverts commit c43e95e011cdbdd3be9446b12d257899b4a90201.
* Revert "Fix Telegram poll action wiring"
This reverts commit 556b531a140330540a10299cd6c4907750a2c0b6.
* Revert "Add Telegram polls action to config typing"
This reverts commit 5cbfaf5cc70d8d69a39d640eea147d70589eeaaf.
* Revert "fix(telegram): wire sendPollTelegram into channel action handler (#16977)"
This reverts commit 7bb9a7dcfc3194a953f0ca20f86f9caba8a22923.
* CI: remove formal models conformance workflow (#19007)
* fix: preserve telegram dm topic thread ids
* style: drop aidev-note prefix in telegram comments
* test: pass extensionContext in abort dedupe e2e
* fix: align tool execute arg parsing for hooks
* test: type telegram action mock passthrough args
* Configure: make model picker allowlist searchable
* Configure: improve searchable model picker token matching
* Docs: add screenshot showing model picker usability issue
* fix: searchable model picker in configure (#19010) (thanks @bjesuiter)
* fix(extensions): revert openai codex auth plugin (PR #18009)
* feat(telegram): add channel_post support for bot-to-bot communication (#17857)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 27a343cd4d9c778a6017ff666d8285ae60256bf4
Co-authored-by: theSamPadilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* Revert "fix: handle forum/topics in Telegram DM thread routing (#17980)"
This reverts commit e20b87f1ba332e2b7838d05e38e3bd7d991f460d.
* Revert: undo #17974 README change
* voice-call: harden closed-loop turn loop and transcript routing (#19140)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 14a3edb00572b20348f839bbafa56ca826cee362
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* iOS onboarding: stop auth step-3 retry loop churn (#19153)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: a38ec42bdd4cf1bf5743ecd3c1d1f2bcceea91e0
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* Revert: fully roll back #17974 zh-cn UI README
* chore(subagents): add regression coverage and changelog
* fix(daemon): scope token drift warnings
* test(web): fix baileys mock typing
* test(cron): cover webhook session rollover overrides
* docs(changelog): note webhook session reuse fix
* fix(discord): normalize command allowFrom prefixes
* fix(cli): honor update restart overrides
* fix(cron): add spin-loop regression coverage
* test(gateway): cover trusted proxy trimming
* test(discord): cover audioAsVoice replies
* test(feishu): cover post mentions for other users
* fix(discord): preserve DM lastRoute user target
* Revert "fix(browser): track original port mapping for EADDRINUSE fallback"
This reverts commit 8e55503d7785e741ce9995f989d944e09a8f22db.
* Revert "fix(browser): handle EADDRINUSE with automatic port fallback"
This reverts commit 0e6daa2e6e30240ce5191ee5c7954f24b4b56b42.
* test(discord): fix mock call arg typing
* Revert: fully roll back #17986 templates
* test: add fetch mock helper and reaction coverage
* CLI: approve latest pending device request
* docs(readme): remove Android install link
* revert(agents): remove llms.txt discovery prompt (#19192)
* fix(ui): revert PR #18093 directive tags (#19188)
* test(discord): cover auto-thread skip types
* test(update): cover restart gating
* docs(zai): document tool_stream defaults
* revert: per-model thinkingDefault override (#19195)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: fe2c59e222a71086864f8abfae05d2da03ad4066
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* fix(gateway): make stale token cleanup non-fatal
* Agents: add before_message_write persistence regression tests
* fix(mattermost): surface reactions support
* Tests: fix fetch mock typings for type-aware checks
* revert: fix models set catalog validation (#19194)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 7e3b2ff7afe052097c4414fc64d7e66191e8fcc3
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* test: cover cron telemetry and typed fetch mocks
* revert(agents): revert base64 image validation (#19221)
* docs(cli): add components send example
* test(sessions): add delivery info regression coverage
* fix(daemon): guard preferred node selection
* test(auto-reply): cover sender_id metadata
* revert: PR 18288 accidental merge (#19224)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 3cda31578ccf71365d5815a8374922e835efff0e
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
* test(telegram): cover autoSelectFamily env precedence
* test(cron): add model fallback regression coverage
* test(release): add appcast regression coverage
* docs(changelog): remove revert entries
* docs: add maintainer application section
* docs: refine maintainer application guidance
* docs: add vision doc and link from README
* docs: add community plugins guide
* Update auto-response message for third-party extensions
* update my contributing list
* iOS: use operator session for ChatSheet RPCs (#19320)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 0753b3a1a22a1d23e871d210e317279627f9fc0f
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* fix: sanitize native command names for Telegram API (#19257)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: b608be348888505b23bb4b2f1c8c42058a28e64e
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
* docs(slack): add assistant:write requirement for typing status
* chore: document sessions_spawn response note and subagent context prefix
* feat(ios): auto-select local signing team (#18421)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: bbb9c3aa48a542539dc37136e6542d1f3958f9c2
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
* fix(bluebubbles): recover outbound message IDs and include sender metadata
* fix cron announce routing and timeout handling
* changelog: add @tyler6204 credit for today's entries
* feat: share to openclaw ios app (#19424)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 0a7ab8589ac23d0743d4377683d60601a8c19e61
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
* Docs: expand multi-agent routing
* docs(changelog): add missing 2026.2.16 entries and reorder by user impact
* chore(release): bump version to 2026.2.17
* fix(signal): canonicalize message targets in tool and inbound flows
* docs: tighten contribution guidance and vision links
* docs: tighten PR scope and review-size policy in vision
* fix(gateway): block cross-session fallback in node event delivery
* fix(gateway): make health monitor checks single-flight
* fix(ios): harden share relay routing and delivery guards
* fix(telegram): normalize topic-create targets and add regression tests
* feat(cron): add default stagger controls for scheduled jobs
* fix(cron): retry next-second schedule compute on undefined
* docs(security): harden gateway security guidance
* feat(models): support anthropic sonnet 4.6
* fix: wire agents.defaults.imageModel into media understanding auto-discovery
resolveAutoEntries only checked a hardcoded list of providers
(openai, anthropic, google, minimax) when looking for an image model.
agents.defaults.imageModel was never consulted by the media understanding
pipeline — it was only wired into the explicit `image` tool.
Add resolveImageModelFromAgentDefaults that reads the imageModel config
(primary + fallbacks) and inserts it into the auto-discovery chain before
the hardcoded provider list. runProviderEntry already falls back to
describeImageWithModel (via pi-ai) for providers not in the media
understanding registry, so no additional provider registration is needed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
(cherry picked from commit b381029ede72a57ef6d12d9413c98fa29501b797)
* docs: update AGENTS instructions
* fix(subagent): harden read-tool overflow guards and sticky reply threading (#19508)
* fix(gateway): avoid premature agent.wait completion on transient errors
* fix(agent): preemptively guard tool results against context overflow
* fix: harden tool-result context guard and add message_id metadata
* fix: use importOriginal in session-key mock to include DEFAULT_ACCOUNT_ID
The run.skill-filter test was mocking ../../routing/session-key.js with only
buildAgentMainSessionKey and normalizeAgentId, but the module also exports
DEFAULT_ACCOUNT_ID which is required transitively by src/web/auth-store.ts.
Switch to importOriginal pattern so all real exports are preserved alongside
the mocked functions.
* pi-runner: guard accumulated tool-result overflow in transformContext
* PI runner: compact overflowing tool-result context
* Subagent: harden tool-result context recovery
* Enhance tool-result context handling by adding support for legacy tool outputs and improving character estimation for message truncation. This includes a new function to create legacy tool results and updates to existing functions to better manage context overflow scenarios.
* Enhance iMessage handling by adding reply tag support in send functions and tests. This includes modifications to prepend or rewrite reply tags based on provided replyToId, ensuring proper message formatting for replies.
* Enhance message delivery across multiple channels by implementing sticky reply context for chunked messages. This includes preserving reply references in Discord, Telegram, and iMessage, ensuring that follow-up messages maintain their intended reply targets. Additionally, improve handling of reply tags in system prompts and tests to support consistent reply behavior.
* Enhance read tool functionality by implementing auto-paging across chunks when no explicit limit is provided, scaling output budget based on model context window. Additionally, add tests for adaptive reading behavior and capped continuation guidance for large outputs. Update related functions to support these features.
* Refine tool-result context management by stripping oversized read-tool details payloads during compaction, ensuring repeated read calls do not bypass context limits. Introduce new utility functions for handling truncation content and enhance character estimation for tool results. Add tests to validate the removal of excessive details in context overflow scenarios.
* Refine message delivery logic in Matrix and Telegram by introducing a flag to track if a text chunk was sent. This ensures that replies are only marked as delivered when a text chunk has been successfully sent, improving the accuracy of reply handling in both channels.
* fix: tighten reply threading coverage and prep fixes (#19508) (thanks @tyler6204)
* fix(hooks): backport internal message hook bridge with safe delivery semantics
* fix(subagent): update SUBAGENT_SPAWN_ACCEPTED_NOTE for clarity on auto-announcement behavior
* fix: follow-up slack streaming routing/tests (#9972) (thanks @natedenh)
* fix: reduce default image dimension from 2000px to 1200px
Large images (2000px) consume excessive context tokens when sent to LLMs.
1200px provides sufficient detail for most use cases while significantly
reducing token usage.
The 5MB byte limit remains unchanged as JPEG compression at 1200px
naturally produces smaller files.
(cherry picked from commit 40182123dd2673b4b65e6846fa066380f10b781f)
* fix(agents): make image sanitization dimension configurable
* docs(tokens): document image dimension token tradeoffs
* Whatsapp/add resolve outbound target tests (#19345)
* test(whatsapp): add resolveWhatsAppOutboundTarget test suite
* style: auto-format files
* fix(test): correct mock order for invalid allowList entry test
* feat(skills): Add 'Use when / Don't use when' routing blocks (#14521)
* feat(skills): add 'Use when / Don't use when' blocks to skill descriptions
Based on OpenAI's Shell + Skills + Compaction best practices article.
Key changes:
- Added clear routing logic to skill descriptions
- Added negative examples to prevent misfires
- Added templates/examples to github skill
- Included Blake's specific setup notes for openhue
Skills updated:
- apple-reminders: Clarify vs Clawdbot cron
- github: Clarify vs local git operations
- imsg: Clarify vs other messaging channels
- openhue: Add device inventory, room layout
- tmux: Clarify vs exec tool
- weather: Add location defaults, format codes
Reference: https://developers.openai.com/blog/skills-shell-tips
* fix(skills): restore metadata and generic CLI examples
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(agents): add generic provider api key rotation (#19587)
* feat(skills): improve descriptions with routing logic (#14577)
* feat(skills): improve descriptions with routing logic
Apply OpenAI's recommended pattern for skill descriptions:
- Add 'Use when' conditions for clear triggering
- Add 'NOT for' negative examples to reduce misfires
- Make descriptions act as routing logic, not marketing copy
Based on: https://developers.openai.com/blog/skills-shell-tips/
Skills updated:
- coding-agent: clarify when to delegate vs direct edit
- github: add boundaries vs browser/scripting
- weather: add scope limitations
Glean reported 20% drop in skill triggering without negative
examples, recovering after adding them. This change brings
Clawdbot skills in line with that pattern.
* docs(skills): clarify routing boundaries (openclaw#14577) (thanks @DylanWoodAkers)
* docs(changelog): add PR 14577 release note (openclaw#14577) (thanks @DylanWoodAkers)
---------
Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* Add frontend-design skill
* feat(telegram): add forum topic creation support (#10427)
Add `topic-create` action to the Telegram message adapter, enabling
programmatic creation of forum topics in supergroups.
Changes:
- Add `createForumTopicTelegram()` to `src/telegram/send.ts`
- Add `createForumTopic` handler in `telegram-actions.ts`
- Wire `topic-create` action in Telegram adapter
- Register `topic-create` in message action names and spec
The bot requires `can_manage_topics` permission in the target group.
Supports optional `iconColor` and `iconCustomEmojiId` parameters.
Closes #10427
* chore: fix formatting in frontend-design SKILL.md
* fix: add action gate check and config type for createForumTopic
Address review feedback:
- Add isActionEnabled() gate in telegram-actions.ts
- Add gate() check in telegram adapter listActions
- Add createForumTopic to TelegramActionConfig type
* fix(telegram): normalize topic-create targets and add regression tests
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
Co-authored-by: cpojer <christoph.pojer@gmail.com>
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
Co-authored-by: Josh Avant <830519+joshavant@users.noreply.github.com>
Co-authored-by: Shadow <hi@shadowing.dev>
Co-authored-by: Hongwei Ma <Marvae@users.noreply.github.com>
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <zaidi@uplause.io>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Sascha Reuter <s.reuter@geek-it.de>
Co-authored-by: sreuter <550246+sreuter@users.noreply.github.com>
Co-authored-by: Nimrod Gutman <nimrod.g@singular.net>
Co-authored-by: Vignesh <mailvgnsh@gmail.com>
Co-authored-by: Benjamin Jesuiter <bjesuiter@gmail.com>
Co-authored-by: Sam Padilla <35386211+theSamPadilla@users.noreply.github.com>
Co-authored-by: Muhammed Mukhthar CM <mukhtharcm@gmail.com>
Co-authored-by: Mariano <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
Co-authored-by: Mariano Belinky <mbelinky@gmail.com>
Co-authored-by: Shadow <shadow@openclaw.ai>
Co-authored-by: Sk Akram <skcodewizard786@gmail.com>
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: Onur <onur@textcortex.com>
Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: Pablo Nunez <pnunfe@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Tyler Yust <64381258+tyler6204@users.noreply.github.com>
Co-authored-by: Han Xiao <han.xiao@jina.ai>
Co-authored-by: Verite Igiraneza <69280208+VeriteIgiraneza@users.noreply.github.com>
Co-authored-by: Blakeshannon <blake@blakeshannon.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: DylanWoodAkers <dylan@lec.com>
Co-authored-by: ClawdBotWolf <clawdbotwolf@proton.me>
Co-authored-by: Claw <claw@openclaw.ai>
2026-02-18 00:38:44 +00:00
await createForumTopicTelegram ( "-1001234567890" , "Roadmap" , {
token : "tok" ,
api ,
iconColor : 0x6fb9f0 ,
iconCustomEmojiId : " 1234567890 " ,
} ) ;
expect ( createForumTopic ) . toHaveBeenCalledWith ( "-1001234567890" , "Roadmap" , {
icon_color : 0x6fb9f0 ,
icon_custom_emoji_id : "1234567890" ,
} ) ;
} ) ;
} ) ;