Skip to content

feat(telegram): support sendMessageDraft streaming in DMs#159

Draft
timolins wants to merge 21 commits intomainfrom
codex/telegram-sendmessagedraft-streaming
Draft

feat(telegram): support sendMessageDraft streaming in DMs#159
timolins wants to merge 21 commits intomainfrom
codex/telegram-sendmessagedraft-streaming

Conversation

@timolins
Copy link
Member

@timolins timolins commented Mar 2, 2026

Summary

  • add native stream() support in Telegram adapter using sendMessageDraft for private chats
  • keep post+edit fallback for group/topic chats and for environments where sendMessageDraft is unavailable
  • clean up placeholder messages for empty fallback streams and tighten unsupported-method detection
  • add/expand Telegram streaming tests (DM draft path, non-DM fallback, unsupported method fallback)
  • update docs/feature matrix to reflect Draft API (DMs) + Post+Edit behavior

Verification

  • pnpm --filter @chat-adapter/telegram typecheck
  • pnpm --filter @chat-adapter/telegram test

Notes

  • Telegram sendMessageDraft was introduced in Bot API 9.5 and is currently DM-scoped in this adapter path.

timolins and others added 7 commits February 28, 2026 14:09
…ring `"undefined"` (truthy) instead of removing it, leaking a truthy VERCEL env var into subsequent tests.

This commit fixes the issue reported at packages/adapter-telegram/src/index.test.ts:661

**Bug explanation:**

In the test "auto mode stays in webhook mode on serverless runtime" (line 622), the cleanup code in the `finally` block at line 662 uses `process.env.VERCEL = undefined` when the env var wasn't previously set. In Node.js, `process.env` coerces all values to strings, so `process.env.VERCEL = undefined` actually sets `process.env.VERCEL` to the string `"undefined"`. This is truthy (`Boolean("undefined")` === `true`).

This was verified empirically:
```
process.env.TEST_VAR = undefined;
typeof process.env.TEST_VAR  // "string"
process.env.TEST_VAR         // "undefined"  
Boolean(process.env.TEST_VAR) // true
```

The consequence is that after this test runs, `process.env.VERCEL` remains set to the truthy string `"undefined"`, causing `isLikelyServerlessRuntime()` to return `true` for all subsequent tests that run in the same process. This could cause any auto-mode tests that follow to incorrectly detect a serverless runtime and behave differently than expected.

**Fix explanation:**

Changed `process.env.VERCEL = undefined` to `delete process.env.VERCEL`, which properly removes the environment variable from `process.env`. After `delete`, `process.env.VERCEL` is `undefined` (the actual undefined value, not the string), and `Boolean(process.env.VERCEL)` correctly returns `false`. This matches the standard pattern for cleaning up environment variables in Node.js tests.

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: timolins <me@timo.sh>
@vercel
Copy link
Contributor

vercel bot commented Mar 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview, Comment, Open in v0 Mar 6, 2026 11:36pm
chat-sdk-nextjs-chat Ready Ready Preview, Comment, Open in v0 Mar 6, 2026 11:36pm

…ssagedraft-streaming

# Conflicts:
#	packages/adapter-telegram/src/index.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
haydenbleasel and others added 6 commits March 6, 2026 15:31
… convention

Telegram always assigns negative IDs to groups/supergroups/channels and
positive IDs to private chats, making this a reliable heuristic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, if sendMessageDraft failed after the first successful draft
update, the adapter silently disabled draft streaming and the user saw
no updates until the final message posted. Now any draft failure
immediately falls back to post+edit streaming with the remaining chunks,
ensuring a consistent streaming UX.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…elapses

Only call renderStreamMarkdown when the throttle interval has passed,
avoiding redundant parse+render cycles on every incoming chunk.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Apply the same time-check-first optimization to streamViaPostEdit: skip
renderStreamMarkdown entirely when the throttle interval hasn't elapsed,
avoiding redundant parse+render cycles on every chunk.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace verbose typeof guards with globalThis.crypto?.randomUUID?.()
and nullish coalescing fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@haydenbleasel
Copy link
Member

Hey Timo! Great PR — the draft streaming approach is really clean. I pushed a few follow-up fixes after reviewing:

What changed

  1. Added a comment explaining the isDM heuristic — Telegram always assigns negative IDs to groups/supergroups/channels and positive IDs to private chats, so !chatId.startsWith("-") is reliable. Added a comment to make that explicit for future readers.

  2. Mid-stream draft failures now fall back to post+edit — Previously, if sendMessageDraft failed after the first successful draft update, the adapter silently disabled draft streaming and the user saw nothing until the final postMessage. Now any draft failure (not just the first unsupported-method error) immediately falls back to streamViaPostEdit with the remaining chunks, so the streaming UX stays consistent.

  3. Deferred markdown rendering in the draft stream pathrenderStreamMarkdown was being called on every incoming chunk, even when the throttle interval hadn't elapsed. Restructured to check the time interval first and only render when we're actually going to send a draft update.

  4. Same rendering optimization in streamViaPostEdit — Applied the same time-check-first pattern to the post+edit fallback path, skipping renderStreamMarkdown when the interval hasn't elapsed yet.

  5. Simplified createDraftId — Replaced the verbose typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" guard with globalThis.crypto?.randomUUID?.() and a nullish coalescing fallback.

All changes pass pnpm validate cleanly (typecheck, lint, tests, build). 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants