#19050: fix(telegram): skip message_thread_id for private chats to prevent silent message drops (v2)
channel: telegram
size: S
Cluster:
Messaging Platform Improvements
## Improved v2 of #17252 — Fixes #17242
Closes #17242
### Problem
When sending messages to Telegram **private chats** (positive chatId), the opts-level `messageThreadId` — typically derived from reply-context — is passed as `message_thread_id` to the Telegram API. Private chats don't support forum topics, so the API rejects with:
```
400: Bad Request: message thread not found
```
Messages are **silently dropped**, affecting:
- Sub-agent announce notifications (`sessions_spawn`)
- Cron job deliveries to private chats
- Proactive `message` tool sends
### Root cause
`buildTelegramThreadReplyParams` unconditionally merges `opts.messageThreadId` into the thread spec for all chat types, including private chats where `message_thread_id` is invalid.
### Fix
In `buildTelegramThreadReplyParams`, private chats (`chatType === "direct"`) now **only honour the thread ID embedded in the target address** (e.g., `123456789:topic:42`) and **ignore the opts-level `messageThreadId`**.
This is a single-point fix that covers **all send paths**: text messages, media, stickers, and polls — addressing the reviewer feedback on #17252 about missing sticker/poll checks.
### Why not blanket-strip?
[Previous #17252 was rejected](https://github.com/openclaw/openclaw/pull/17252) because blanket-suppressing `message_thread_id` for all private chats would break **DM topic routing** (Bot API 9.3, #18974). This v2 distinguishes between:
| Source | Private chat behaviour | Rationale |
|--------|----------------------|-----------|
| Target address (`123456789:topic:42`) | ✅ **Kept** | Intentional DM topic routing |
| `opts.messageThreadId` (reply context) | ❌ **Skipped** | Reply-context thread IDs don't apply to plain DMs |
### Changes
- **`src/telegram/send.ts`** — `buildTelegramThreadReplyParams`: for private chats, use only `targetMessageThreadId` (from target string), ignore `opts.messageThreadId`
- **`src/telegram/send.test.ts`** — Updated/added tests:
- Verifies opts-level `messageThreadId` is skipped for private chat text sends
- Verifies target-embedded thread IDs are preserved for DM topics
- Verifies opts-level `messageThreadId` is skipped for private chat sticker sends
- Updated chat-not-found test to use group chatId (was using private chatId with now-skipped thread param)
### Test results
All 38 telegram test files pass (469 tests).
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR fixes silent message drops when sending to Telegram private chats with a reply-context `messageThreadId` (#17242). The root cause was `buildTelegramThreadReplyParams` unconditionally forwarding `opts.messageThreadId` (from reply context) to the API as `message_thread_id`, which private chats reject with `400: Bad Request: message thread not found`.
The fix is a single-point change in `buildTelegramThreadReplyParams`:
- **Private chats** (`chatType === "direct"`): only use the thread ID embedded in the target address (e.g., `123456789:topic:42`), ignoring the opts-level `messageThreadId`
- **Group/unknown chats**: preserve the existing precedence (`opts.messageThreadId` takes priority, falls back to `targetMessageThreadId`)
This covers all send paths (text, media, stickers, polls) since they all flow through `buildTelegramThreadReplyParams`.
- Tests updated to verify opts-level thread ID is skipped for private text and sticker sends
- New test verifies target-embedded thread IDs are preserved for DM topic routing (Bot API 9.3)
- Existing chat-not-found test updated to use group chat ID (since private chats now skip opts thread ID, the old assertion was invalid)
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge — it's a well-scoped fix with correct logic and thorough test coverage.
- The change is minimal (6 lines of logic in one function), the fix is correct based on Telegram API semantics, it preserves existing behavior for non-private chats, and the tests comprehensively cover the key scenarios: opts-level thread ID skipped for private chats, target-embedded thread ID preserved for DM topics, and group chat behavior unchanged. The `??` operator is semantically equivalent to the previous `!= null` ternary for the non-private path.
- No files require special attention.
<sub>Last reviewed commit: 3cd7e0d</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#17432: fix(telegram): skip message_thread_id for private chats in sticker/...
by clawinho · 2026-02-15
90.2%
#14443: fix(telegram): skip General topic thread ID for all chat types (#14...
by lailoo · 2026-02-12
88.5%
#12936: fix(telegram): omit message_thread_id for private DM chats
by omair445 · 2026-02-09
86.1%
#7261: fix(telegram): preserve DM topic thread id for outbound media
by ViffyGwaanl · 2026-02-02
84.7%
#19213: Telegram: preserve DM topic thread in direct replies
by Kemalau · 2026-02-17
84.5%
#17968: fix(telegram): restore DM topic thread ids with send-path fallback
by Leonccaa · 2026-02-16
83.1%
#16548: fix(telegram): enhance chat_id validation and diagnostics
by tanujbhaud · 2026-02-14
82.2%
#17952: fix(telegram): support message_thread_id for DM topics
by timbrinded · 2026-02-16
82.1%
#6192: Telegram: fix DM Topics thread routing
by ViffyGwaanl · 2026-02-01
80.9%
#3368: fix: sessions navigation, DM thread display, and DM thread delivery...
by Lukavyi · 2026-01-28
80.8%