mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 12:18:37 +00:00
fix(telegram): use message previews in DMs
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Mattermost replies: keep `root_id` pinned to the existing thread root when an agent replies inside a thread, while still using reply-target threading for top-level posts. (#27744) thanks @hnykda.
|
- Mattermost replies: keep `root_id` pinned to the existing thread root when an agent replies inside a thread, while still using reply-target threading for top-level posts. (#27744) thanks @hnykda.
|
||||||
- Agents/failover: detect Amazon Bedrock `Too many tokens per day` quota errors as rate limits across fallback, cron retry, and memory embeddings while keeping context-window `too many tokens per request` errors out of the rate-limit lane. (#39377) Thanks @gambletan.
|
- Agents/failover: detect Amazon Bedrock `Too many tokens per day` quota errors as rate limits across fallback, cron retry, and memory embeddings while keeping context-window `too many tokens per request` errors out of the rate-limit lane. (#39377) Thanks @gambletan.
|
||||||
- Android/Play distribution: remove self-update, background location, `screen.record`, and background mic capture from the Android app, narrow the foreground service to `dataSync` only, and clean up the legacy `location.enabledMode=always` preference migration. (#39660) Thanks @obviyus.
|
- Android/Play distribution: remove self-update, background location, `screen.record`, and background mic capture from the Android app, narrow the foreground service to `dataSync` only, and clean up the legacy `location.enabledMode=always` preference migration. (#39660) Thanks @obviyus.
|
||||||
|
- Telegram/DM partial streaming: keep DM preview lanes on real message edits instead of native draft materialization so final replies no longer flash a second duplicate copy before collapsing back to one.
|
||||||
- macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared `inout` visibility mutation from `OverlayPanelFactory.present`, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH.
|
- macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared `inout` visibility mutation from `OverlayPanelFactory.present`, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH.
|
||||||
- macOS Talk Mode: set the speech recognition request `taskHint` to `.dictation` for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv.
|
- macOS Talk Mode: set the speech recognition request `taskHint` to `.dictation` for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv.
|
||||||
- macOS release packaging: default `scripts/package-mac-app.sh` to universal binaries for `BUILD_CONFIG=release`, and clarify that `scripts/package-mac-dist.sh` already produces the release zip + DMG. (#33891) Thanks @cgdusek.
|
- macOS release packaging: default `scripts/package-mac-app.sh` to universal binaries for `BUILD_CONFIG=release`, and clarify that `scripts/package-mac-dist.sh` already produces the release zip + DMG. (#33891) Thanks @cgdusek.
|
||||||
|
|||||||
@@ -232,10 +232,10 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
|||||||
## Feature reference
|
## Feature reference
|
||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
<Accordion title="Live stream preview (native drafts + message edits)">
|
<Accordion title="Live stream preview (message edits)">
|
||||||
OpenClaw can stream partial replies in real time:
|
OpenClaw can stream partial replies in real time:
|
||||||
|
|
||||||
- direct chats: Telegram native draft streaming via `sendMessageDraft`
|
- direct chats: preview message + `editMessageText`
|
||||||
- groups/topics: preview message + `editMessageText`
|
- groups/topics: preview message + `editMessageText`
|
||||||
|
|
||||||
Requirement:
|
Requirement:
|
||||||
@@ -244,11 +244,9 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
|||||||
- `progress` maps to `partial` on Telegram (compat with cross-channel naming)
|
- `progress` maps to `partial` on Telegram (compat with cross-channel naming)
|
||||||
- legacy `channels.telegram.streamMode` and boolean `streaming` values are auto-mapped
|
- legacy `channels.telegram.streamMode` and boolean `streaming` values are auto-mapped
|
||||||
|
|
||||||
Telegram enabled `sendMessageDraft` for all bots in Bot API 9.5 (March 1, 2026).
|
|
||||||
|
|
||||||
For text-only replies:
|
For text-only replies:
|
||||||
|
|
||||||
- DM: OpenClaw updates the draft in place (no extra preview message)
|
- DM: OpenClaw keeps the same preview message and performs a final edit in place (no second message)
|
||||||
- group/topic: OpenClaw keeps the same preview message and performs a final edit in place (no second message)
|
- group/topic: OpenClaw keeps the same preview message and performs a final edit in place (no second message)
|
||||||
|
|
||||||
For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message.
|
For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message.
|
||||||
@@ -872,7 +870,7 @@ Primary reference:
|
|||||||
- `channels.telegram.textChunkLimit`: outbound chunk size (chars).
|
- `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.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.linkPreview`: toggle link previews for outbound messages (default: true).
|
||||||
- `channels.telegram.streaming`: `off | partial | block | progress` (live stream preview; default: `partial`; `progress` maps to `partial`; `block` is legacy preview mode compatibility). In DMs, `partial` uses native `sendMessageDraft` when available.
|
- `channels.telegram.streaming`: `off | partial | block | progress` (live stream preview; default: `partial`; `progress` maps to `partial`; `block` is legacy preview mode compatibility). Telegram preview streaming uses a single preview message that is edited in place.
|
||||||
- `channels.telegram.mediaMaxMb`: inbound/outbound Telegram media cap (MB, default: 100).
|
- `channels.telegram.mediaMaxMb`: inbound/outbound Telegram media cap (MB, default: 100).
|
||||||
- `channels.telegram.retry`: retry policy for Telegram send helpers (CLI/tools/actions) on recoverable outbound API errors (attempts, minDelayMs, maxDelayMs, jitter).
|
- `channels.telegram.retry`: retry policy for Telegram send helpers (CLI/tools/actions) on recoverable outbound API errors (attempts, minDelayMs, maxDelayMs, jitter).
|
||||||
- `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to enabled on Node 22+, with WSL2 defaulting to disabled.
|
- `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to enabled on Node 22+, with WSL2 defaulting to disabled.
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ Legacy key migration:
|
|||||||
|
|
||||||
Telegram:
|
Telegram:
|
||||||
|
|
||||||
- Uses Bot API `sendMessageDraft` in DMs when available, and `sendMessage` + `editMessageText` for group/topic preview updates.
|
- Uses `sendMessage` + `editMessageText` preview updates across DMs and group/topics.
|
||||||
- Preview streaming is skipped when Telegram block streaming is explicitly enabled (to avoid double-streaming).
|
- Preview streaming is skipped when Telegram block streaming is explicitly enabled (to avoid double-streaming).
|
||||||
- `/reasoning stream` can write reasoning to preview.
|
- `/reasoning stream` can write reasoning to preview.
|
||||||
|
|
||||||
|
|||||||
@@ -1171,7 +1171,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("uses message preview transport for DM reasoning lane when answer preview lane is active", async () => {
|
it("uses message preview transport for all DM lanes when streaming is active", async () => {
|
||||||
setupDraftStreams({ answerMessageId: 999, reasoningMessageId: 111 });
|
setupDraftStreams({ answerMessageId: 999, reasoningMessageId: 111 });
|
||||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||||
async ({ dispatcherOptions, replyOptions }) => {
|
async ({ dispatcherOptions, replyOptions }) => {
|
||||||
@@ -1190,7 +1190,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual(
|
expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
thread: { id: 777, scope: "dm" },
|
thread: { id: 777, scope: "dm" },
|
||||||
previewTransport: "auto",
|
previewTransport: "message",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(createTelegramDraftStream.mock.calls[1]?.[0]).toEqual(
|
expect(createTelegramDraftStream.mock.calls[1]?.[0]).toEqual(
|
||||||
@@ -1201,9 +1201,8 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("materializes DM answer draft final without sending a duplicate final message", async () => {
|
it("finalizes DM answer preview in place without materializing or sending a duplicate", async () => {
|
||||||
const answerDraftStream = createTestDraftStream({ previewMode: "draft" });
|
const answerDraftStream = createDraftStream(321);
|
||||||
answerDraftStream.materialize.mockResolvedValue(321);
|
|
||||||
const reasoningDraftStream = createDraftStream(111);
|
const reasoningDraftStream = createDraftStream(111);
|
||||||
createTelegramDraftStream
|
createTelegramDraftStream
|
||||||
.mockImplementationOnce(() => answerDraftStream)
|
.mockImplementationOnce(() => answerDraftStream)
|
||||||
@@ -1222,12 +1221,17 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual(
|
expect(createTelegramDraftStream.mock.calls[0]?.[0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
thread: { id: 777, scope: "dm" },
|
thread: { id: 777, scope: "dm" },
|
||||||
previewTransport: "auto",
|
previewTransport: "message",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(answerDraftStream.materialize).toHaveBeenCalledTimes(1);
|
expect(answerDraftStream.materialize).not.toHaveBeenCalled();
|
||||||
expect(deliverReplies).not.toHaveBeenCalled();
|
expect(deliverReplies).not.toHaveBeenCalled();
|
||||||
expect(editMessageTelegram).not.toHaveBeenCalled();
|
expect(editMessageTelegram).toHaveBeenCalledWith(
|
||||||
|
123,
|
||||||
|
321,
|
||||||
|
"Checking the directory...",
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps reasoning and answer streaming in separate preview lanes", async () => {
|
it("keeps reasoning and answer streaming in separate preview lanes", async () => {
|
||||||
|
|||||||
@@ -190,19 +190,21 @@ export const dispatchTelegramMessage = async ({
|
|||||||
const draftReplyToMessageId =
|
const draftReplyToMessageId =
|
||||||
replyToMode !== "off" && typeof msg.message_id === "number" ? msg.message_id : undefined;
|
replyToMode !== "off" && typeof msg.message_id === "number" ? msg.message_id : undefined;
|
||||||
const draftMinInitialChars = DRAFT_MIN_INITIAL_CHARS;
|
const draftMinInitialChars = DRAFT_MIN_INITIAL_CHARS;
|
||||||
|
// Keep DM preview lanes on real message transport. Native draft previews still
|
||||||
|
// require a draft->message materialize hop, and that overlap keeps reintroducing
|
||||||
|
// a visible duplicate flash at finalize time.
|
||||||
|
const useMessagePreviewTransportForDm = threadSpec?.scope === "dm" && canStreamAnswerDraft;
|
||||||
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId);
|
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId);
|
||||||
const archivedAnswerPreviews: ArchivedPreview[] = [];
|
const archivedAnswerPreviews: ArchivedPreview[] = [];
|
||||||
const archivedReasoningPreviewIds: number[] = [];
|
const archivedReasoningPreviewIds: number[] = [];
|
||||||
const createDraftLane = (laneName: LaneName, enabled: boolean): DraftLaneState => {
|
const createDraftLane = (laneName: LaneName, enabled: boolean): DraftLaneState => {
|
||||||
const useMessagePreviewTransportForDmReasoning =
|
|
||||||
laneName === "reasoning" && threadSpec?.scope === "dm" && canStreamAnswerDraft;
|
|
||||||
const stream = enabled
|
const stream = enabled
|
||||||
? createTelegramDraftStream({
|
? createTelegramDraftStream({
|
||||||
api: bot.api,
|
api: bot.api,
|
||||||
chatId,
|
chatId,
|
||||||
maxChars: draftMaxChars,
|
maxChars: draftMaxChars,
|
||||||
thread: threadSpec,
|
thread: threadSpec,
|
||||||
previewTransport: useMessagePreviewTransportForDmReasoning ? "message" : "auto",
|
previewTransport: useMessagePreviewTransportForDm ? "message" : "auto",
|
||||||
replyToMessageId: draftReplyToMessageId,
|
replyToMessageId: draftReplyToMessageId,
|
||||||
minInitialChars: draftMinInitialChars,
|
minInitialChars: draftMinInitialChars,
|
||||||
renderText: renderDraftPreview,
|
renderText: renderDraftPreview,
|
||||||
|
|||||||
Reference in New Issue
Block a user