diff --git a/CHANGELOG.md b/CHANGELOG.md index 63c12b1410a..95f83905527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - Security/Unused Dependencies: remove unused plugin-local `openclaw` devDependencies from `extensions/open-prose`, `extensions/lobster`, and `extensions/llm-task` after removing this dependency from build-time requirements. (#22495) Thanks @vincentkoc. - Agents/Subagents: default subagent spawn depth now uses shared `maxSpawnDepth=2`, enabling depth-1 orchestrator spawning by default while keeping depth policy checks consistent across spawn and prompt paths. (#22223) Thanks @tyler6204. - Channels/CLI: add per-account/channel `defaultTo` outbound routing fallback so `openclaw agent --deliver` can send without explicit `--reply-to` when a default target is configured. (#16985) Thanks @KirillShchetinin. +- Telegram/Streaming: simplify preview streaming config to `channels.telegram.streaming` (boolean), auto-map legacy `streamMode` values, and remove block-vs-partial preview branching. (#22012) thanks @obviyus. - iOS/Chat: clean chat UI noise by stripping inbound untrusted metadata/timestamp prefixes, formatting tool outputs into concise summaries/errors, compacting the composer while typing, and supporting tap-to-dismiss keyboard in chat view. (#22122) thanks @mbelinky. - iOS/Watch: bridge mirrored watch prompt notification actions into iOS quick-reply handling, including queued action handoff until app model initialization. (#22123) thanks @mbelinky. - iOS/Tests: cover IPv4-mapped IPv6 loopback in manual TLS policy tests for connect validation paths. (#22045) Thanks @mbelinky. diff --git a/docs/channels/grammy.md b/docs/channels/grammy.md index ae92c5292b0..570acabfb1c 100644 --- a/docs/channels/grammy.md +++ b/docs/channels/grammy.md @@ -21,7 +21,7 @@ title: grammY - **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `channels.telegram.webhookUrl` + `channels.telegram.webhookSecret` are set (otherwise it long-polls). - **Sessions:** direct chats collapse into the agent main session (`agent::`); groups use `agent::telegram:group:`; replies route back to the same channel. - **Config knobs:** `channels.telegram.botToken`, `channels.telegram.dmPolicy`, `channels.telegram.groups` (allowlist + mention defaults), `channels.telegram.allowFrom`, `channels.telegram.groupAllowFrom`, `channels.telegram.groupPolicy`, `channels.telegram.mediaMaxMb`, `channels.telegram.linkPreview`, `channels.telegram.proxy`, `channels.telegram.webhookSecret`, `channels.telegram.webhookUrl`, `channels.telegram.webhookHost`. -- **Live stream preview:** optional `channels.telegram.streamMode` sends a temporary message and updates it with `editMessageText`. This is separate from channel block streaming. +- **Live stream preview:** optional `channels.telegram.streaming` sends a temporary message and updates it with `editMessageText`. This is separate from channel block streaming. - **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome. Open questions diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index b354a37ac48..01e13ea1aa8 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -226,21 +226,8 @@ curl "https://api.telegram.org/bot/getUpdates" Requirement: - - `channels.telegram.streamMode` is not `"off"` (default: `"partial"`) - - Modes: - - - `off`: no live preview - - `partial`: frequent preview updates from partial text - - `block`: chunked preview updates using `channels.telegram.draftChunk` - - `draftChunk` defaults for `streamMode: "block"`: - - - `minChars: 200` - - `maxChars: 800` - - `breakPreference: "paragraph"` - - `maxChars` is clamped by `channels.telegram.textChunkLimit`. + - `channels.telegram.streaming` is `true` (default) + - legacy `channels.telegram.streamMode` values are auto-mapped to `streaming` This works in direct chats and groups/topics. @@ -248,7 +235,7 @@ curl "https://api.telegram.org/bot/getUpdates" For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message. - `streamMode` is separate from block streaming. When block streaming is explicitly enabled for Telegram, OpenClaw skips the preview stream to avoid double-streaming. + Preview streaming is separate from block streaming. When block streaming is explicitly enabled for Telegram, OpenClaw skips the preview stream to avoid double-streaming. Telegram-only reasoning stream: @@ -721,7 +708,7 @@ Primary reference: - `channels.telegram.textChunkLimit`: outbound chunk size (chars). - `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. - `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true). -- `channels.telegram.streamMode`: `off | partial | block` (live stream preview). +- `channels.telegram.streaming`: `true | false` (live stream preview; default: true). - `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB). - `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter). - `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to disabled on Node 22 to avoid Happy Eyeballs timeouts. @@ -745,7 +732,7 @@ Telegram-specific high-signal fields: - access control: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`, `groups.*.topics.*` - command/menu: `commands.native`, `customCommands` - threading/replies: `replyToMode` -- streaming: `streamMode` (preview), `draftChunk`, `blockStreaming` +- streaming: `streaming` (preview), `blockStreaming` - formatting/delivery: `textChunkLimit`, `chunkMode`, `linkPreview`, `responsePrefix` - media/network: `mediaMaxMb`, `timeoutSeconds`, `retry`, `network.autoSelectFamily`, `proxy` - webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost` diff --git a/docs/concepts/streaming.md b/docs/concepts/streaming.md index b81f87606d7..1ac8da84ce7 100644 --- a/docs/concepts/streaming.md +++ b/docs/concepts/streaming.md @@ -100,7 +100,7 @@ This maps to: **Channel note:** For non-Telegram channels, block streaming is **off unless** `*.blockStreaming` is explicitly set to `true`. Telegram can stream a live preview -(`channels.telegram.streamMode`) without block replies. +(`channels.telegram.streaming`) without block replies. Config location reminder: the `blockStreaming*` defaults live under `agents.defaults`, not the root config. @@ -110,11 +110,7 @@ Config location reminder: the `blockStreaming*` defaults live under Telegram is the only channel with live preview streaming: - Uses Bot API `sendMessage` (first update) + `editMessageText` (subsequent updates). -- `channels.telegram.streamMode: "partial" | "block" | "off"`. - - `partial`: preview updates with latest stream text. - - `block`: preview updates in chunked blocks (same chunker rules). - - `off`: no preview streaming. -- Preview chunk config (only for `streamMode: "block"`): `channels.telegram.draftChunk` (defaults: `minChars: 200`, `maxChars: 800`). +- `channels.telegram.streaming: true | false` (default: `true`). - Preview streaming is separate from block streaming. - When Telegram block streaming is explicitly enabled, preview streaming is skipped to avoid double-streaming. - Text-only finals are applied by editing the preview message in place. @@ -124,8 +120,7 @@ Telegram is the only channel with live preview streaming: ``` Telegram └─ sendMessage (temporary preview message) - ├─ streamMode=partial → edit latest text - └─ streamMode=block → chunker + edit updates + └─ streaming=true → edit latest text └─ final text-only reply → final edit on same message └─ fallback: cleanup preview + normal final delivery (media/complex) ``` diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index cb46bf4ec16..54708388649 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -151,12 +151,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat historyLimit: 50, replyToMode: "first", // off | first | all linkPreview: true, - streamMode: "partial", // off | partial | block - draftChunk: { - minChars: 200, - maxChars: 800, - breakPreference: "paragraph", // paragraph | newline | sentence - }, + streaming: true, // live preview on/off (default true) actions: { reactions: true, sendMessage: true }, reactionNotifications: "own", // off | own | all mediaMaxMb: 5, diff --git a/src/config/config.legacy-config-detection.rejects-routing-allowfrom.e2e.test.ts b/src/config/config.legacy-config-detection.rejects-routing-allowfrom.e2e.test.ts index 0fe46d9d58b..1a88e3e785d 100644 --- a/src/config/config.legacy-config-detection.rejects-routing-allowfrom.e2e.test.ts +++ b/src/config/config.legacy-config-detection.rejects-routing-allowfrom.e2e.test.ts @@ -378,11 +378,46 @@ describe("legacy config detection", () => { expect(res.config.channels?.telegram?.groupPolicy).toBe("allowlist"); } }); - it("defaults telegram.streamMode to partial when telegram section exists", async () => { + it("defaults telegram.streaming to true when telegram section exists", async () => { const res = validateConfigObject({ channels: { telegram: {} } }); expect(res.ok).toBe(true); if (res.ok) { - expect(res.config.channels?.telegram?.streamMode).toBe("partial"); + expect(res.config.channels?.telegram?.streaming).toBe(true); + expect(res.config.channels?.telegram?.streamMode).toBeUndefined(); + } + }); + it("migrates legacy telegram.streamMode=off to streaming=false", async () => { + const res = validateConfigObject({ channels: { telegram: { streamMode: "off" } } }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.channels?.telegram?.streaming).toBe(false); + expect(res.config.channels?.telegram?.streamMode).toBeUndefined(); + } + }); + it("migrates legacy telegram.streamMode=block to streaming=true", async () => { + const res = validateConfigObject({ channels: { telegram: { streamMode: "block" } } }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.channels?.telegram?.streaming).toBe(true); + expect(res.config.channels?.telegram?.streamMode).toBeUndefined(); + } + }); + it("migrates legacy telegram.accounts.*.streamMode to streaming", async () => { + const res = validateConfigObject({ + channels: { + telegram: { + accounts: { + ops: { + streamMode: "off", + }, + }, + }, + }, + }); + expect(res.ok).toBe(true); + if (res.ok) { + expect(res.config.channels?.telegram?.accounts?.ops?.streaming).toBe(false); + expect(res.config.channels?.telegram?.accounts?.ops?.streamMode).toBeUndefined(); } }); it('rejects whatsapp.dmPolicy="open" without allowFrom "*"', async () => { diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index d2dc3e24ef1..95520da2080 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -391,14 +391,8 @@ export const FIELD_HELP: Record = { "Debounce window (ms) for batching rapid inbound messages from the same sender (0 to disable).", "channels.telegram.dmPolicy": 'Direct message access control ("pairing" recommended). "open" requires channels.telegram.allowFrom=["*"].', - "channels.telegram.streamMode": - "Live stream preview mode for Telegram replies (off | partial | block). Separate from block streaming; uses sendMessage + editMessageText.", - "channels.telegram.draftChunk.minChars": - 'Minimum chars before emitting a Telegram stream preview update when channels.telegram.streamMode="block" (default: 200).', - "channels.telegram.draftChunk.maxChars": - 'Target max size for a Telegram stream preview chunk when channels.telegram.streamMode="block" (default: 800; clamped to channels.telegram.textChunkLimit).', - "channels.telegram.draftChunk.breakPreference": - "Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence). Default: paragraph.", + "channels.telegram.streaming": + "Enable Telegram live stream preview via message edits (default: true; legacy streamMode auto-maps here).", "channels.discord.streamMode": "Live stream preview mode for Discord replies (off | partial | block). Separate from block streaming; uses sendMessage + editMessage.", "channels.discord.draftChunk.minChars": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 2f9a1a2e593..cd8020b4de8 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -263,10 +263,7 @@ export const FIELD_LABELS: Record = { ...IRC_FIELD_LABELS, "channels.telegram.botToken": "Telegram Bot Token", "channels.telegram.dmPolicy": "Telegram DM Policy", - "channels.telegram.streamMode": "Telegram Stream Mode", - "channels.telegram.draftChunk.minChars": "Telegram Draft Chunk Min Chars", - "channels.telegram.draftChunk.maxChars": "Telegram Draft Chunk Max Chars", - "channels.telegram.draftChunk.breakPreference": "Telegram Draft Chunk Break Preference", + "channels.telegram.streaming": "Telegram Streaming", "channels.telegram.retry.attempts": "Telegram Retry Attempts", "channels.telegram.retry.minDelayMs": "Telegram Retry Min Delay (ms)", "channels.telegram.retry.maxDelayMs": "Telegram Retry Max Delay (ms)", diff --git a/src/config/types.telegram.ts b/src/config/types.telegram.ts index 0de285a2412..68079ebf18c 100644 --- a/src/config/types.telegram.ts +++ b/src/config/types.telegram.ts @@ -95,13 +95,15 @@ export type TelegramAccountConfig = { textChunkLimit?: number; /** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */ chunkMode?: "length" | "newline"; + /** Enable live stream preview via message edits (default: true). */ + streaming?: boolean; /** Disable block streaming for this account. */ blockStreaming?: boolean; - /** Chunking config for Telegram stream previews in `streamMode: "block"`. */ + /** @deprecated Legacy chunking config from `streamMode: "block"`; ignored after migration. */ draftChunk?: BlockStreamingChunkConfig; /** Merge streamed block replies before sending. */ blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - /** Telegram stream preview mode (off|partial|block). Default: partial. */ + /** @deprecated Legacy key; migrated automatically to `streaming` boolean. */ streamMode?: "off" | "partial" | "block"; mediaMaxMb?: number; /** Telegram API client timeout in seconds (grammY ApiClientOptions). */ diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 5109ac269ad..22c55b4035d 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -99,6 +99,27 @@ const validateTelegramCustomCommands = ( } }; +function normalizeTelegramStreamingConfig(value: { + streaming?: boolean; + streamMode?: "off" | "partial" | "block"; +}) { + if (typeof value.streaming === "boolean") { + delete value.streamMode; + return; + } + if (value.streamMode === "off") { + value.streaming = false; + delete value.streamMode; + return; + } + if (value.streamMode === "partial" || value.streamMode === "block") { + value.streaming = true; + delete value.streamMode; + return; + } + value.streaming = true; +} + export const TelegramAccountSchemaBase = z .object({ name: z.string().optional(), @@ -122,10 +143,12 @@ export const TelegramAccountSchemaBase = z dms: z.record(z.string(), DmConfigSchema.optional()).optional(), textChunkLimit: z.number().int().positive().optional(), chunkMode: z.enum(["length", "newline"]).optional(), + streaming: z.boolean().optional(), blockStreaming: z.boolean().optional(), draftChunk: BlockStreamingChunkSchema.optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), - streamMode: z.enum(["off", "partial", "block"]).optional().default("partial"), + // Legacy key kept for automatic migration to `streaming`. + streamMode: z.enum(["off", "partial", "block"]).optional(), mediaMaxMb: z.number().positive().optional(), timeoutSeconds: z.number().int().positive().optional(), retry: RetryConfigSchema, @@ -159,6 +182,7 @@ export const TelegramAccountSchemaBase = z .strict(); export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((value, ctx) => { + normalizeTelegramStreamingConfig(value); requireOpenAllowFrom({ policy: value.dmPolicy, allowFrom: value.allowFrom, @@ -173,6 +197,7 @@ export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((valu export const TelegramConfigSchema = TelegramAccountSchemaBase.extend({ accounts: z.record(z.string(), TelegramAccountSchema.optional()).optional(), }).superRefine((value, ctx) => { + normalizeTelegramStreamingConfig(value); requireOpenAllowFrom({ policy: value.dmPolicy, allowFrom: value.allowFrom, diff --git a/src/telegram/bot-message-dispatch.test.ts b/src/telegram/bot-message-dispatch.test.ts index 95c2c0af73c..c2f84730938 100644 --- a/src/telegram/bot-message-dispatch.test.ts +++ b/src/telegram/bot-message-dispatch.test.ts @@ -193,7 +193,7 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(draftStream.clear).toHaveBeenCalledTimes(1); }); - it("keeps a higher initial debounce threshold in block stream mode", async () => { + it("uses immediate preview updates for legacy block stream mode", async () => { const draftStream = createDraftStream(); createTelegramDraftStream.mockReturnValue(draftStream); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( @@ -209,7 +209,7 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(createTelegramDraftStream).toHaveBeenCalledWith( expect.objectContaining({ - minInitialChars: 30, + minInitialChars: 1, }), ); }); @@ -445,7 +445,7 @@ describe("dispatchTelegramMessage draft streaming", () => { ); }); - it("forces new message when new assistant message starts after previous output", async () => { + it("does not force new message for legacy block stream mode", async () => { const draftStream = createDraftStream(999); createTelegramDraftStream.mockReturnValue(draftStream); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( @@ -464,8 +464,7 @@ describe("dispatchTelegramMessage draft streaming", () => { await dispatchWithContext({ context: createContext(), streamMode: "block" }); - // Should force new message when assistant message starts after previous output - expect(draftStream.forceNewMessage).toHaveBeenCalled(); + expect(draftStream.forceNewMessage).not.toHaveBeenCalled(); }); it("does not force new message in partial mode when assistant message restarts", async () => { diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index a2419da6586..7026b8cca6c 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -6,7 +6,6 @@ import { modelSupportsVision, } from "../agents/model-catalog.js"; import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; -import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js"; import { resolveChunkMode } from "../auto-reply/chunk.js"; import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js"; import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js"; @@ -26,7 +25,6 @@ import type { TelegramBotOptions } from "./bot.js"; import { deliverReplies } from "./bot/delivery.js"; import type { TelegramStreamMode } from "./bot/types.js"; import type { TelegramInlineButtons } from "./button-types.js"; -import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js"; import { createTelegramDraftStream } from "./draft-stream.js"; import { renderTelegramHtmlText } from "./format.js"; import { @@ -143,21 +141,20 @@ export const dispatchTelegramMessage = async ({ }); const forceBlockStreamingForReasoning = resolvedReasoningLevel === "on"; const streamReasoningDraft = resolvedReasoningLevel === "stream"; + const previewStreamingEnabled = streamMode !== "off"; const canStreamAnswerDraft = - streamMode !== "off" && !accountBlockStreamingEnabled && !forceBlockStreamingForReasoning; + previewStreamingEnabled && !accountBlockStreamingEnabled && !forceBlockStreamingForReasoning; const canStreamReasoningDraft = canStreamAnswerDraft || streamReasoningDraft; const draftReplyToMessageId = replyToMode !== "off" && typeof msg.message_id === "number" ? msg.message_id : undefined; const draftMinInitialChars = - streamMode === "partial" || streamReasoningDraft ? 1 : DRAFT_MIN_INITIAL_CHARS; + previewStreamingEnabled || streamReasoningDraft ? 1 : DRAFT_MIN_INITIAL_CHARS; const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId); type LaneName = "answer" | "reasoning"; type DraftLaneState = { stream: ReturnType | undefined; lastPartialText: string; - draftText: string; hasStreamedMessage: boolean; - chunker: EmbeddedBlockChunker | undefined; }; const createDraftLane = (enabled: boolean): DraftLaneState => { const stream = enabled @@ -173,16 +170,10 @@ export const dispatchTelegramMessage = async ({ warn: logVerbose, }) : undefined; - const chunker = - stream && streamMode === "block" - ? new EmbeddedBlockChunker(resolveTelegramDraftStreamingChunking(cfg, route.accountId)) - : undefined; return { stream, lastPartialText: "", - draftText: "", hasStreamedMessage: false, - chunker, }; }; const lanes: Record = { @@ -207,9 +198,7 @@ export const dispatchTelegramMessage = async ({ }; const resetDraftLaneState = (lane: DraftLaneState) => { lane.lastPartialText = ""; - lane.draftText = ""; lane.hasStreamedMessage = false; - lane.chunker?.reset(); }; const updateDraftFromPartial = (lane: DraftLaneState, text: string | undefined) => { const laneStream = lane.stream; @@ -221,46 +210,18 @@ export const dispatchTelegramMessage = async ({ } // Mark that we've received streaming content (for forceNewMessage decision). lane.hasStreamedMessage = true; - if (streamMode === "partial") { - // Some providers briefly emit a shorter prefix snapshot (for example - // "Sure." -> "Sure" -> "Sure."). Keep the longer preview to avoid - // visible punctuation flicker. - if ( - lane.lastPartialText && - lane.lastPartialText.startsWith(text) && - text.length < lane.lastPartialText.length - ) { - return; - } - lane.lastPartialText = text; - laneStream.update(text); + // Some providers briefly emit a shorter prefix snapshot (for example + // "Sure." -> "Sure" -> "Sure."). Keep the longer preview to avoid + // visible punctuation flicker. + if ( + lane.lastPartialText && + lane.lastPartialText.startsWith(text) && + text.length < lane.lastPartialText.length + ) { return; } - let delta = text; - if (text.startsWith(lane.lastPartialText)) { - delta = text.slice(lane.lastPartialText.length); - } else { - // Streaming buffer reset (or non-monotonic stream). Start fresh. - lane.chunker?.reset(); - lane.draftText = ""; - } lane.lastPartialText = text; - if (!delta) { - return; - } - if (!lane.chunker) { - lane.draftText = text; - laneStream.update(lane.draftText); - return; - } - lane.chunker.append(delta); - lane.chunker.drain({ - force: false, - emit: (chunk) => { - lane.draftText += chunk; - laneStream.update(lane.draftText); - }, - }); + laneStream.update(text); }; const ingestDraftLaneSegments = (text: string | undefined) => { for (const segment of splitTextIntoLaneSegments(text)) { @@ -275,31 +236,18 @@ export const dispatchTelegramMessage = async ({ if (!lane.stream) { return; } - if (lane.chunker?.hasBuffered()) { - lane.chunker.drain({ - force: true, - emit: (chunk) => { - lane.draftText += chunk; - }, - }); - lane.chunker.reset(); - if (lane.draftText) { - lane.stream.update(lane.draftText); - } - } await lane.stream.flush(); }; - const disableBlockStreaming = - streamMode === "off" - ? true - : forceBlockStreamingForReasoning - ? false - : typeof telegramCfg.blockStreaming === "boolean" - ? !telegramCfg.blockStreaming - : canStreamAnswerDraft - ? true - : undefined; + const disableBlockStreaming = !previewStreamingEnabled + ? true + : forceBlockStreamingForReasoning + ? false + : typeof telegramCfg.blockStreaming === "boolean" + ? !telegramCfg.blockStreaming + : canStreamAnswerDraft + ? true + : undefined; const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({ cfg, @@ -395,8 +343,7 @@ export const dispatchTelegramMessage = async ({ linkPreview: telegramCfg.linkPreview, replyQuoteText, }; - const getLanePreviewText = (lane: DraftLaneState) => - streamMode === "block" ? lane.draftText : lane.lastPartialText; + const getLanePreviewText = (lane: DraftLaneState) => lane.lastPartialText; const tryUpdatePreviewForLane = async (params: { lane: DraftLaneState; laneName: LaneName; @@ -449,7 +396,6 @@ export const dispatchTelegramMessage = async ({ }); if (updateLaneSnapshot) { lane.lastPartialText = text; - lane.draftText = text; } deliveryState.delivered = true; return true; @@ -684,10 +630,6 @@ export const dispatchTelegramMessage = async ({ onAssistantMessageStart: answerLane.stream ? () => { reasoningStepState.resetForNextStep(); - // Keep answer blocks separated in block mode; partial mode keeps one answer lane. - if (streamMode === "block" && answerLane.hasStreamedMessage) { - answerLane.stream?.forceNewMessage(); - } resetDraftLaneState(answerLane); } : undefined, diff --git a/src/telegram/bot/helpers.ts b/src/telegram/bot/helpers.ts index b6cf1783f08..4ee7036553d 100644 --- a/src/telegram/bot/helpers.ts +++ b/src/telegram/bot/helpers.ts @@ -154,11 +154,18 @@ export function buildTypingThreadParams(messageThreadId?: number) { } export function resolveTelegramStreamMode(telegramCfg?: { + streaming?: boolean; streamMode?: TelegramStreamMode; }): TelegramStreamMode { + if (typeof telegramCfg?.streaming === "boolean") { + return telegramCfg.streaming ? "partial" : "off"; + } const raw = telegramCfg?.streamMode?.trim().toLowerCase(); - if (raw === "off" || raw === "partial" || raw === "block") { - return raw; + if (raw === "off") { + return "off"; + } + if (raw === "partial" || raw === "block") { + return "partial"; } return "partial"; }