mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
refactor(telegram): simplify native draft progress path
This commit is contained in:
@@ -7,7 +7,6 @@ Verified against Telegram Bot API 10.0, May 8 2026.
|
||||
## Streaming
|
||||
|
||||
- Do not reintroduce `sendMessageDraft` for answer streaming. Telegram drafts are ephemeral 30-second previews in private chats; final delivery still requires a separate `sendMessage`. OpenClaw uses `sendMessage` plus `editMessageText`, then finalizes in place so the user sees one persistent answer.
|
||||
- `sendMessageDraft` is allowed only for explicitly enabled transient private-chat tool progress/Thinking previews. Keep it default-off/canary-gated. Never route answer text, reasoning text, or final delivery through native drafts.
|
||||
- Streaming owns one visible preview message. Edit it forward. Do not send an extra final bubble unless the final edit genuinely failed.
|
||||
- Keep the first-preview debounce. If a provider sends token-sized deltas, coalesce them into cumulative preview text instead of removing the debounce.
|
||||
- Respect Telegram limits in the Telegram layer. Text over 4096 chars chains into continuation messages. Polls keep the current Bot API 12-option cap.
|
||||
|
||||
@@ -307,7 +307,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
|
||||
- direct chats: preview message + `editMessageText`
|
||||
- groups/topics: preview message + `editMessageText`
|
||||
- direct-chat tool progress: optional native ephemeral `sendMessageDraft` Thinking/status preview when explicitly enabled and the Bot API supports it
|
||||
- direct-chat tool progress: optional native `sendMessageDraft` status preview when enabled and supported
|
||||
|
||||
Requirement:
|
||||
|
||||
@@ -319,7 +319,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
|
||||
Tool-progress preview updates are the short status lines shown while tools run, for example command execution, file reads, planning updates, patch summaries, or Codex preamble/commentary text in Codex app-server mode. Telegram keeps these enabled by default to match released OpenClaw behavior from `v2026.4.22` and later.
|
||||
|
||||
In direct chats, supported Telegram Bot API clients can use native ephemeral drafts for these tool-progress lines. This shows an immediate Telegram-native Thinking/status preview without persisting tool chatter into the chat history. As soon as assistant answer text starts, OpenClaw stops updating the native draft and continues with the normal persistent answer preview/final delivery path. If `sendMessageDraft` is unavailable or rejected, OpenClaw silently falls back to the edited preview behavior. This native draft lane is off by default; enable it only for trusted DM canaries first:
|
||||
Direct chats can use native Telegram drafts for these tool-progress lines without persisting tool chatter into chat history. Native drafts stop before answer text starts; final answers stay on the normal persistent delivery path. This lane is off by default and should be gated to trusted DM IDs first:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -1605,6 +1605,28 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
expect(editMessageTelegram).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not restart progress drafts after final answer delivery", async () => {
|
||||
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||
async ({ dispatcherOptions, replyOptions }) => {
|
||||
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
|
||||
await dispatcherOptions.deliver({ text: "Branch is up to date" }, { kind: "final" });
|
||||
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
|
||||
return { queuedFinal: true };
|
||||
},
|
||||
);
|
||||
|
||||
await dispatchWithContext({
|
||||
context: createContext(),
|
||||
streamMode: "progress",
|
||||
telegramCfg: { streaming: { mode: "progress", progress: { label: "Shelling" } } },
|
||||
});
|
||||
|
||||
expect(answerDraftStream.update).toHaveBeenCalledTimes(1);
|
||||
expect(answerDraftStream.update).toHaveBeenCalledWith("Shelling\n`🛠️ Exec`");
|
||||
expectDeliveredReply(0, { text: "Branch is up to date" });
|
||||
});
|
||||
|
||||
it("uses the transcript final when progress-mode final text is truncated", async () => {
|
||||
setupDraftStreams({ answerMessageId: 2001 });
|
||||
const fullAnswer =
|
||||
@@ -1727,6 +1749,31 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
expect(draftStream.flush).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the progress draft label when tool progress lines are hidden", async () => {
|
||||
const draftStream = createSequencedDraftStream(2001);
|
||||
createTelegramDraftStream.mockReturnValue(draftStream);
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
|
||||
await replyOptions?.onReplyStart?.();
|
||||
await replyOptions?.onAssistantMessageStart?.();
|
||||
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
|
||||
return { queuedFinal: false };
|
||||
});
|
||||
|
||||
await dispatchWithContext({
|
||||
context: createContext(),
|
||||
streamMode: "progress",
|
||||
telegramCfg: {
|
||||
streaming: {
|
||||
mode: "progress",
|
||||
progress: { label: "Shelling", toolProgress: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(draftStream.update).toHaveBeenCalledWith("Shelling");
|
||||
expect(draftStream.flush).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders Telegram progress drafts before slow status reactions resolve", async () => {
|
||||
const draftStream = createSequencedDraftStream(2001);
|
||||
createTelegramDraftStream.mockReturnValue(draftStream);
|
||||
|
||||
@@ -671,15 +671,28 @@ export const dispatchTelegramMessage = async ({
|
||||
}
|
||||
const rawText = typeof line === "string" ? line : line?.text;
|
||||
const normalized = sanitizeProgressMarkdownText(rawText?.replace(/\s+/g, " ").trim() ?? "");
|
||||
if (streamToolProgressSuppressed) {
|
||||
return false;
|
||||
}
|
||||
if (streamMode !== "progress" && !streamToolProgressEnabled) {
|
||||
return false;
|
||||
}
|
||||
const shouldUpdateProgressLines =
|
||||
streamToolProgressEnabled && !streamToolProgressSuppressed && Boolean(normalized);
|
||||
if (!shouldUpdateProgressLines && streamMode !== "progress") {
|
||||
return false;
|
||||
}
|
||||
const progressLine =
|
||||
typeof line === "object" && line !== undefined ? { ...line, text: normalized } : normalized;
|
||||
if (nativeToolProgressDraft && !streamToolProgressSuppressed && normalized) {
|
||||
const nextLines = mergeChannelProgressDraftLine(streamToolProgressLines, progressLine, {
|
||||
maxLines: resolveChannelProgressDraftMaxLines(telegramCfg),
|
||||
});
|
||||
if (nextLines === streamToolProgressLines) {
|
||||
return false;
|
||||
}
|
||||
const nextLines = shouldUpdateProgressLines
|
||||
? mergeChannelProgressDraftLine(streamToolProgressLines, progressLine, {
|
||||
maxLines: resolveChannelProgressDraftMaxLines(telegramCfg),
|
||||
})
|
||||
: streamToolProgressLines;
|
||||
if (shouldUpdateProgressLines && nextLines === streamToolProgressLines) {
|
||||
return false;
|
||||
}
|
||||
if (nativeToolProgressDraft && shouldUpdateProgressLines) {
|
||||
const streamText = formatChannelProgressDraftText({
|
||||
entry: telegramCfg,
|
||||
lines: nextLines,
|
||||
@@ -691,15 +704,6 @@ export const dispatchTelegramMessage = async ({
|
||||
}
|
||||
}
|
||||
if (streamMode !== "progress") {
|
||||
if (!streamToolProgressEnabled || streamToolProgressSuppressed || !normalized) {
|
||||
return false;
|
||||
}
|
||||
const nextLines = mergeChannelProgressDraftLine(streamToolProgressLines, progressLine, {
|
||||
maxLines: resolveChannelProgressDraftMaxLines(telegramCfg),
|
||||
});
|
||||
if (nextLines === streamToolProgressLines) {
|
||||
return false;
|
||||
}
|
||||
streamToolProgressLines = nextLines;
|
||||
const streamText = formatChannelProgressDraftText({
|
||||
entry: telegramCfg,
|
||||
@@ -714,21 +718,8 @@ export const dispatchTelegramMessage = async ({
|
||||
answerLane.stream.update(streamText);
|
||||
return true;
|
||||
}
|
||||
if (streamToolProgressEnabled && !streamToolProgressSuppressed && normalized) {
|
||||
streamToolProgressLines = mergeChannelProgressDraftLine(
|
||||
streamToolProgressLines,
|
||||
progressLine,
|
||||
{
|
||||
maxLines: resolveChannelProgressDraftMaxLines(telegramCfg),
|
||||
},
|
||||
);
|
||||
}
|
||||
if (
|
||||
options?.startImmediately &&
|
||||
streamToolProgressEnabled &&
|
||||
!streamToolProgressSuppressed &&
|
||||
normalized
|
||||
) {
|
||||
streamToolProgressLines = nextLines;
|
||||
if (options?.startImmediately) {
|
||||
const alreadyStarted = progressDraftGate.hasStarted;
|
||||
await progressDraftGate.startNow();
|
||||
if (alreadyStarted && progressDraftGate.hasStarted) {
|
||||
|
||||
@@ -30,14 +30,12 @@ describe("createNativeTelegramToolProgressDraft", () => {
|
||||
} as never);
|
||||
|
||||
expect(draft).toBeDefined();
|
||||
await draft?.update("");
|
||||
await draft?.update("Running command");
|
||||
|
||||
expect(sendMessageDraft).toHaveBeenCalledTimes(2);
|
||||
expect(sendMessageDraft).toHaveBeenCalledTimes(1);
|
||||
const firstDraftId = sendMessageDraft.mock.calls[0]?.[1];
|
||||
expect(firstDraftId).toEqual(expect.any(Number));
|
||||
expect(firstDraftId).not.toBe(0);
|
||||
expect(sendMessageDraft.mock.calls[1]?.[1]).toBe(firstDraftId);
|
||||
expect(sendMessageDraft).toHaveBeenLastCalledWith(123, firstDraftId, "Running command", {
|
||||
message_thread_id: 456,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ const TELEGRAM_DRAFT_ID_STATE_KEY = Symbol.for("openclaw.telegramNativeDraftIdSt
|
||||
type TelegramSendMessageDraft = (
|
||||
chatId: Parameters<Bot["api"]["sendMessage"]>[0],
|
||||
draftId: number,
|
||||
text?: string,
|
||||
text: string,
|
||||
params?: {
|
||||
message_thread_id?: number;
|
||||
parse_mode?: "HTML";
|
||||
@@ -69,6 +69,9 @@ export function createNativeTelegramToolProgressDraft(params: {
|
||||
return false;
|
||||
}
|
||||
const normalizedText = normalizeDraftText(text);
|
||||
if (!normalizedText) {
|
||||
return false;
|
||||
}
|
||||
if (normalizedText === lastSentText) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -64,12 +64,9 @@ export type TelegramStreamingMode = "off" | "partial" | "block" | "progress";
|
||||
export type TelegramExecApprovalTarget = "dm" | "channel" | "both";
|
||||
|
||||
export type TelegramStreamingPreviewConfig = ChannelStreamingPreviewConfig & {
|
||||
/**
|
||||
* Use Telegram-native ephemeral draft UI for DM preview tool progress.
|
||||
* Default: false.
|
||||
*/
|
||||
/** Use Telegram-native ephemeral draft UI for DM preview tool progress. */
|
||||
nativeToolProgress?: boolean;
|
||||
/** Optional Telegram sender/user ID allowlist for native DM preview tool progress. */
|
||||
/** Telegram sender/user IDs allowed to use native DM preview tool progress. */
|
||||
nativeToolProgressAllowFrom?: Array<string | number>;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user