feat(telegram): use sendMessageDraft for private chat streaming#31824
feat(telegram): use sendMessageDraft for private chat streaming#31824
Conversation
Greptile SummaryThis PR introduces Telegram draft streaming for private chats (DM threads) using the Key changes:
Issues found:
Confidence Score: 3/5
Last reviewed commit: a9bbcac |
Additional Comments (1)
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/telegram/draft-stream.test.ts
Line: 144-165
Comment:
Consider adding test coverage for `forceNewMessage()` with DM drafts (scope: "dm"). Current tests only cover non-threaded streams or forum threads, leaving the DM draft path untested where a new draft ID is allocated.
How can I resolve this? If you propose a fix, please make it concise. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e09ae54c08
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🔒 Aisle Security AnalysisWe found 3 potential security issue(s) in this PR:
1. 🟠 DM draft preview fallback may send reasoning preview as a normal message (leaking sensitive content / causing duplicates)
DescriptionIn However,
When this happens, Impact:
Vulnerable code: const previewRevisionBeforeFlush = lane.stream?.previewRevision?.() ?? 0;
lane.stream?.update(text);
await params.flushDraftLane(lane);
const previewUpdated = (lane.stream?.previewRevision?.() ?? 0) > previewRevisionBeforeFlush;
if (!previewUpdated) {
const delivered = await params.sendPayload(params.applyTextToPayload(payload, text));
return delivered ? "sent" : "skipped";
}RecommendationAvoid using Recommended safeguards:
Example fix (conservative: never fall back for draft reasoning updates): if (isDraftPreviewLane(lane)) {
const before = lane.stream?.previewRevision?.() ?? 0;
lane.stream?.update(text);
await params.flushDraftLane(lane);
const after = lane.stream?.previewRevision?.() ?? 0;
// If nothing was emitted, treat as skipped rather than sending to chat history.
if (after <= before) return "skipped";
lane.lastPartialText = text;
params.markDelivered();
return "preview-updated";
}If you must keep fallback, add an explicit check such as 2. 🟡 Telegram topic/thread fallback retries preview send without message_thread_id, potentially posting drafts to main chat
DescriptionIn This is unsafe when the stream is running in a forum supergroup topic (where
Concrete leakage scenarios:
Tests added in this diff only assert the retry behavior for Vulnerable code: } catch (err) {
const hasThreadParam =
"message_thread_id" in (sendParams ?? {}) &&
typeof (sendParams as { message_thread_id?: unknown }).message_thread_id === "number";
if (!hasThreadParam || !THREAD_NOT_FOUND_RE.test(String(err))) {
throw err;
}
const threadlessParams = {
...(sendParams as Record<string, unknown>),
};
delete threadlessParams.message_thread_id;
params.warn?.(
"telegram stream preview send failed with message_thread_id, retrying without thread",
);
sent = await params.api.sendMessage(chatId, renderedText, /* threadless */ ...);
}RecommendationFail closed for forum topics; only allow a threadless retry when the thread scope is truly DM (matching the design already used in For example: } catch (err) {
const hasThreadParam = typeof (sendParams as any)?.message_thread_id === "number";
const allowThreadlessRetry = params.thread?.scope === "dm";
if (!allowThreadlessRetry || !hasThreadParam || !THREAD_NOT_FOUND_RE.test(String(err))) {
throw err;
}
const threadlessParams = { ...(sendParams as Record<string, unknown>) };
delete threadlessParams.message_thread_id;
params.warn?.("telegram stream preview thread not found in DM; retrying without thread");
sent = await params.api.sendMessage(
chatId,
renderedText,
Object.keys(threadlessParams).length ? threadlessParams : undefined,
);
}Additionally:
3. 🟡 Telegram DM reasoning lane forced to "message" preview transport can expose model chain-of-thought
DescriptionThe Telegram dispatcher now forces the reasoning lane to use message-based preview transport in DM scope.
Impact:
Vulnerable change: const useMessagePreviewTransportForDmReasoning =
laneName === "reasoning" && threadSpec?.scope === "dm" && canStreamAnswerDraft;
createTelegramDraftStream({
...,
previewTransport: useMessagePreviewTransportForDmReasoning ? "message" : "auto",
});RecommendationDo not expose raw reasoning by default in Telegram DMs. Recommended hardening options (pick one based on intended product behavior):
const useMessagePreviewTransportForDmReasoning = false;
// or explicitly
previewTransport: laneName === "reasoning" && threadSpec?.scope === "dm" ? "draft" : "auto";
const allowVisibleReasoningInDm =
resolvedReasoningLevel === "stream" && telegramCfg.debugReasoning === true;
previewTransport:
laneName === "reasoning" && threadSpec?.scope === "dm" && allowVisibleReasoningInDm
? "message"
: "auto";
Any approach should be paired with clear user/admin documentation that enabling reasoning may reveal sensitive internal data. Analyzed PR: #31824 at commit Last updated on: 2026-03-02T17:41:10Z |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ed43adee3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
draft-streaming.MP4Demo of sraft streaming in DMs. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ff3c6e8a83
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1bb322c3d9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!previewUpdated) { | ||
| params.log( | ||
| `telegram: ${laneName} draft preview update not emitted; falling back to standard send`, | ||
| ); | ||
| const delivered = await params.sendPayload(params.applyTextToPayload(payload, text)); |
There was a problem hiding this comment.
Treat unchanged draft preview text as delivered
When the reasoning lane uses draft transport, this fallback path treats any non-increment of previewRevision as a failed preview update and sends the payload as a normal message. However, createTelegramDraftStream intentionally skips sending when the rendered text is unchanged (renderedText === lastSentText), so a block payload that matches the already-streamed draft text will hit this branch and post a duplicate reasoning message in DM flows (notably reasoning-stream mode where only the reasoning lane is active). This branch should distinguish “no-op because already up to date” from “failed to emit preview” before falling back to sendPayload.
Useful? React with 👍 / 👎.
1bb322c to
932ef30
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 932ef30796
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (sent) { | ||
| previewRevision += 1; |
There was a problem hiding this comment.
Ignore superseded draft sends when bumping preview revision
sendOrEditStreamMessage increments previewRevision whenever sendStreamPreview returns true, but the draft-transport branch never checks whether the send belonged to the current generation after forceNewMessage(). In a race where an older in-flight draft send resolves during a later flush, this stale completion can bump previewRevision and make createLaneTextDeliverer treat the current non-final reasoning block as delivered, skipping sendPayload fallback even when the new text was not emitted (for example, because it is below minInitialChars).
Useful? React with 👍 / 👎.
…claw#31824) * feat(telegram): use sendMessageDraft for private stream previews * test(telegram): cover DM draft id rotation race * fix(telegram): keep DM reasoning updates in draft preview * fix(telegram): split DM reasoning preview transport * fix(telegram): harden DM draft preview fallback paths * style(telegram): normalize draft preview formatting
…claw#31824) * feat(telegram): use sendMessageDraft for private stream previews * test(telegram): cover DM draft id rotation race * fix(telegram): keep DM reasoning updates in draft preview * fix(telegram): split DM reasoning preview transport * fix(telegram): harden DM draft preview fallback paths * style(telegram): normalize draft preview formatting
Align Telegram streaming behavior with Discord's existing pattern: - In partial mode (streaming: 'partial'), do not rotate to a new message on assistant message boundaries. The stream keeps editing the same message across tool-call turns, matching Discord's shouldSplitPreviewMessages gating. - Force message transport (sendMessage+editMessageText) for all DM lanes, not just reasoning. Draft transport (sendMessageDraft) has no real messageId, so editMessageText finalization cannot work — every turn falls through to sendPayload creating a new message. This restores pre-openclaw#31824 DM behavior. - Reset streamState.stopped/final in forceNewMessage() so the stream can be reused when rotation IS needed (block mode). Previously the stream was permanently dead after the first stop(). - Add keepStreamAlive option to lane-delivery so intermediate finals edit the preview in place without stopping the stream. - Preserve visible preview messages in the finally cleanup block when in partial mode, even if finalizedPreviewByLane was reset between turns. Existing config controls the behavior: streaming: 'partial' → one coalesced message (default for Telegram DMs) streaming: 'block' → split on message boundaries (existing behavior) Co-authored-by: Claude <noreply@anthropic.com>
Align Telegram streaming behavior with Discord's existing pattern: - In partial mode (streaming: 'partial'), do not rotate to a new message on assistant message boundaries. The stream keeps editing the same message across tool-call turns, matching Discord's shouldSplitPreviewMessages gating. - Force message transport (sendMessage+editMessageText) for all DM lanes, not just reasoning. Draft transport (sendMessageDraft) has no real messageId, so editMessageText finalization cannot work — every turn falls through to sendPayload creating a new message. This restores pre-openclaw#31824 DM behavior. - Reset streamState.stopped/final in forceNewMessage() so the stream can be reused when rotation IS needed (block mode). Previously the stream was permanently dead after the first stop(). - Add keepStreamAlive option to lane-delivery so intermediate finals edit the preview in place without stopping the stream. - Preserve visible preview messages in the finally cleanup block when in partial mode, even if finalizedPreviewByLane was reset between turns. Existing config controls the behavior: streaming: 'partial' → one coalesced message (default for Telegram DMs) streaming: 'block' → split on message boundaries (existing behavior) Closes openclaw#32535 Co-authored-by: Claude <noreply@anthropic.com>
…claw#31824) * feat(telegram): use sendMessageDraft for private stream previews * test(telegram): cover DM draft id rotation race * fix(telegram): keep DM reasoning updates in draft preview * fix(telegram): split DM reasoning preview transport * fix(telegram): harden DM draft preview fallback paths * style(telegram): normalize draft preview formatting
…claw#31824) * feat(telegram): use sendMessageDraft for private stream previews * test(telegram): cover DM draft id rotation race * fix(telegram): keep DM reasoning updates in draft preview * fix(telegram): split DM reasoning preview transport * fix(telegram): harden DM draft preview fallback paths * style(telegram): normalize draft preview formatting
Summary
sendMessageDraftfor private-chat draft streamingValidation
pnpm test src/telegram/draft-stream.test.tspnpm tsgo