feat(telegram): MarkdownV2 + summarized streaming + approval re-keying#191
Merged
Conversation
Three deliverables for the hub Telegram bridge (one cohesive feature): 1. MarkdownV2 formatting. All user-facing bridge messages now send with parse_mode MarkdownV2. New sendMessageMd / editMessageTextMd wrap the send with a 400->plain-text single retry so an unbalanced reserved char never silently drops a session reply. Reuses the existing escapeMarkdownV2 (full reserved set). Inline approve/deny prompt is MarkdownV2 with the same fallback; keyboard unaffected. 2. Summarized streaming. New session-activity event bus (tool_use one-liners) emitted from ws/agent.ts. The bridge maintains one editable "Working..." message per (chat, session): each tool_use appends a collapsed one-liner (throttled edit), a typing chat-action refreshes ~every 4s, and assistant_message:final edits the same message to the full answer. Gated on config.telegram.summarizedStreaming (TELEGRAM_SUMMARIZED_STREAMING=false reverts to a single final-blob send). No parallel dispatch path; cost-cap gate untouched. 3. MED fast-follow (#189 review). approvals.ts pending-prompt registry was keyed by requestId alone -> a shared default session clobbered the first binding and locked out a valid approver. Re-keyed by (sessionId, requestId) holding a set of authorized userIds; takePendingPrompt(requestId, userId) folds in the authorization check and resolves exactly once for whichever authorized user taps first. Tests: escaper (every reserved char, existing) + 400->plaintext fallback (new file, cache-busted import to dodge cross-file Bun mock pollution) + approval re-keying incl. two-users-same-default-session. check-baseline GREEN (956 pass / 0 fail). docs/telegram-bridge.md updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #189. Three deliverables for the hub Telegram bridge in one PR (cohesive, interdependent — streaming uses the new MarkdownV2 helpers).
1. MarkdownV2 formatting
sendMessageMd/editMessageTextMdinclient.ts: send withparse_mode: MarkdownV2, and on a 400 retry once as plain text so an unbalanced reserved char never silently drops a session reply (editMessageTextMdalso treatsmessage is not modifiedas a benign success).escapeMarkdownV2(full reserved set_ * [ ] ( ) ~ \> # + - = | { } . !+`). Callers escape dynamic content, keep their own*bold*/code.2. Summarized streaming
events/session-activity-events.ts(tool_useone-liners), emitted from the existingtool_usespot inws/agent.ts. Not a dispatch path —hub/src/dispatch/and the non-bypassable cost-cap gate are untouched.⏳ Working…message per (chat, session): eachtool_useappends a collapsed one-liner (🔧 Edit hub/src/foo.ts, throttled ~900ms, last-12 cap);thinking/text_deltadropped. Atypingchat-action fires immediately then ~every 4s. Onassistant_message:finalthe bridge stops typing and edits the same message to the full answer (>4096 chars → follow-up send).config.telegram.summarizedStreaming(TELEGRAM_SUMMARIZED_STREAMING=false→ single final-blob send). Finalization + MarkdownV2 work regardless.3. MED fast-follow from the #189 security review
approvals.tsregistry was keyed byrequestIdalone → when two users share one default session the bridge fanned the prompt to both and the secondrememberPendingPromptclobbered the first, locking out a valid approver with "Not allowed" (fail-closed but wrong).(sessionId, requestId)holding a map of every authorizeduserId → {chatId, messageId}.takePendingPrompt(requestId, userId)folds the authorization check into the take and resolves the whole entry exactly once for whichever authorized user taps first.QC
bun run check-baselineGREEN:pass=956 skip=129 fail=0(within tolerance, fail_max=0).telegram-client-fallback.test.ts, cache-busted import to avoid cross-file Bunmock.modulepollution), approval re-keying incl. the two-users-same-default-session case and cross-session isolation. All 60 telegram tests pass isolated and combined.docs/telegram-bridge.mdupdated in the same commit. No routes changed → no OpenAPI/docs:syncdelta.Constraints honored
telegram-webhook.tsraw-body-before-parse + mount order untouched (mount-order.test.tspasses).schema.sqlnot touched. Smallest-diff, surgical.Prod-code diff ~390 LOC; slightly over the 250 split threshold but kept as one PR since the three pieces are interdependent (streaming consumes the MarkdownV2 helpers).
🤖 Generated with Claude Code