Skip to content

Commit e8b142f

Browse files
committed
refactor(telegram): remove native draft previews
1 parent 547cc0f commit e8b142f

15 files changed

Lines changed: 339 additions & 822 deletions

docs/channels/telegram.md

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,6 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
311311

312312
- direct chats: preview message + `editMessageText`
313313
- groups/topics: preview message + `editMessageText`
314-
- direct-chat tool progress: optional native `sendMessageDraft` status preview when enabled and supported
315314

316315
Requirement:
317316

@@ -324,25 +323,6 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
324323

325324
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.
326325

327-
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:
328-
329-
```json
330-
{
331-
"channels": {
332-
"telegram": {
333-
"streaming": {
334-
"mode": "partial",
335-
"preview": {
336-
"toolProgress": true,
337-
"nativeToolProgress": true,
338-
"nativeToolProgressAllowFrom": ["123456789"]
339-
}
340-
}
341-
}
342-
}
343-
}
344-
```
345-
346326
To keep the edited preview for answer text but hide tool-progress lines, set:
347327

348328
```json
@@ -420,14 +400,16 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
420400

421401
</Accordion>
422402

423-
<Accordion title="Formatting and HTML fallback">
424-
Outbound text uses Telegram `parse_mode: "HTML"`.
403+
<Accordion title="Rich message formatting">
404+
Outbound text uses Telegram rich messages.
405+
406+
- Markdown text is sent as rich Markdown without converting it to HTML.
407+
- Explicit HTML payloads are sent as rich HTML.
408+
- Media captions still use Telegram HTML captions because rich messages do not replace captions.
425409

426-
- Markdown-ish text is rendered to Telegram-safe HTML.
427-
- Supported Telegram HTML tags are preserved; unsupported HTML is escaped.
428-
- If Telegram rejects parsed HTML, OpenClaw retries as plain text.
410+
Long rich text is split automatically across Telegram's rich text and rich block limits. Tables over Telegram's column limit are sent as code blocks.
429411

430-
Link previews are enabled by default and can be disabled with `channels.telegram.linkPreview: false`.
412+
Link previews are enabled by default. `channels.telegram.linkPreview: false` skips automatic entity detection for rich text.
431413

432414
</Accordion>
433415

extensions/discord/src/config-schema.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@ describe("discord config schema", () => {
9191
expect(cfg.accounts?.noisy?.suppressEmbeds).toBe(false);
9292
});
9393

94-
it("rejects Telegram-only native tool-progress draft config", () => {
94+
it("rejects unknown preview config keys", () => {
9595
const issues = expectInvalidDiscordConfig({
9696
streaming: {
9797
preview: {
98-
nativeToolProgress: true,
98+
unknownPreviewFlag: true,
9999
},
100100
},
101101
});

extensions/telegram/src/bot-deps.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { syncTelegramMenuCommands } from "./bot-native-command-menu.js";
2424
import { deliverReplies, emitInternalMessageSentHook } from "./bot/delivery.js";
2525
import { createTelegramDraftStream } from "./draft-stream.js";
2626
import { resolveTelegramExecApproval } from "./exec-approval-resolver.js";
27-
import { createNativeTelegramToolProgressDraft } from "./native-tool-progress-draft.js";
2827
import { recordOutboundMessageForPromptContext } from "./outbound-message-context.js";
2928
import { editMessageTelegram } from "./send.js";
3029
import { wasSentByBot } from "./sent-message-cache.js";
@@ -50,7 +49,6 @@ export type TelegramBotDeps = {
5049
wasSentByBot: typeof wasSentByBot;
5150
resolveExecApproval?: typeof resolveTelegramExecApproval;
5251
createTelegramDraftStream?: typeof createTelegramDraftStream;
53-
createNativeTelegramToolProgressDraft?: typeof createNativeTelegramToolProgressDraft;
5452
deliverReplies?: typeof deliverReplies;
5553
deliverInboundReplyWithMessageSendContext?: typeof deliverInboundReplyWithMessageSendContext;
5654
emitInternalMessageSentHook?: typeof emitInternalMessageSentHook;
@@ -120,9 +118,6 @@ export const defaultTelegramBotDeps: TelegramBotDeps = {
120118
get createTelegramDraftStream() {
121119
return createTelegramDraftStream;
122120
},
123-
get createNativeTelegramToolProgressDraft() {
124-
return createNativeTelegramToolProgressDraft;
125-
},
126121
get deliverReplies() {
127122
return deliverReplies;
128123
},

extensions/telegram/src/bot-message-dispatch.test.ts

Lines changed: 1 addition & 239 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ type DispatchReplyWithBufferedBlockDispatcherArgs = Parameters<
2828
>[0];
2929

3030
const createTelegramDraftStream = vi.hoisted(() => vi.fn());
31-
const createNativeTelegramToolProgressDraft = vi.hoisted(() => vi.fn());
3231
const dispatchReplyWithBufferedBlockDispatcher = vi.hoisted(() =>
3332
vi.fn<(params: DispatchReplyWithBufferedBlockDispatcherArgs) => Promise<unknown>>(),
3433
);
@@ -221,8 +220,6 @@ const telegramDepsForTest: TelegramBotDeps = {
221220
wasSentByBot: wasSentByBot as TelegramBotDeps["wasSentByBot"],
222221
createTelegramDraftStream:
223222
createTelegramDraftStream as TelegramBotDeps["createTelegramDraftStream"],
224-
createNativeTelegramToolProgressDraft:
225-
createNativeTelegramToolProgressDraft as TelegramBotDeps["createNativeTelegramToolProgressDraft"],
226223
deliverReplies: deliverReplies as TelegramBotDeps["deliverReplies"],
227224
deliverInboundReplyWithMessageSendContext:
228225
deliverInboundReplyWithMessageSendContext as TelegramBotDeps["deliverInboundReplyWithMessageSendContext"],
@@ -247,7 +244,6 @@ describe("dispatchTelegramMessage draft streaming", () => {
247244
installTelegramStateRuntimeForTest();
248245
resetTelegramReplyFenceForTests();
249246
createTelegramDraftStream.mockReset();
250-
createNativeTelegramToolProgressDraft.mockReset();
251247
dispatchReplyWithBufferedBlockDispatcher.mockReset();
252248
deliverReplies.mockReset();
253249
deliverInboundReplyWithMessageSendContext.mockReset();
@@ -352,10 +348,6 @@ describe("dispatchTelegramMessage draft streaming", () => {
352348
const createDraftStream = (messageId?: number) => createTestDraftStream({ messageId });
353349
const createSequencedDraftStream = (startMessageId = 1001) =>
354350
createSequencedTestDraftStream(startMessageId);
355-
const createNativeToolProgressDraft = (updateResult = true) => ({
356-
update: vi.fn(async () => updateResult),
357-
stop: vi.fn(),
358-
});
359351

360352
function setupDraftStreams(params?: { answerMessageId?: number; reasoningMessageId?: number }) {
361353
const answerDraftStream = createDraftStream(params?.answerMessageId);
@@ -1827,234 +1819,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
18271819
expect(deliverReplies).not.toHaveBeenCalled();
18281820
});
18291821

1830-
it("uses native DM drafts for transient tool progress before answer text", async () => {
1831-
const nativeDraft = createNativeToolProgressDraft();
1832-
createNativeTelegramToolProgressDraft.mockReturnValue(nativeDraft);
1833-
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
1834-
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
1835-
async ({ dispatcherOptions, replyOptions }) => {
1836-
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
1837-
await replyOptions?.onPartialReply?.({ text: "Done ", delta: "Done " });
1838-
await dispatcherOptions.deliver({ text: "Done answer." }, { kind: "final" });
1839-
return { queuedFinal: true };
1840-
},
1841-
);
1842-
1843-
await dispatchWithContext({
1844-
context: createContext(),
1845-
streamMode: "partial",
1846-
telegramCfg: {
1847-
streaming: {
1848-
mode: "partial",
1849-
preview: { nativeToolProgress: true, nativeToolProgressAllowFrom: ["123"] },
1850-
},
1851-
},
1852-
});
1853-
1854-
expect(createNativeTelegramToolProgressDraft).toHaveBeenCalledWith(
1855-
expect.objectContaining({
1856-
chatId: 123,
1857-
thread: { id: 777, scope: "dm" },
1858-
}),
1859-
);
1860-
expect(nativeDraft.update).toHaveBeenCalledWith(expect.stringContaining("Exec"));
1861-
expect(nativeDraft.update).toHaveBeenCalledWith(expect.not.stringContaining("`"));
1862-
expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "Done ");
1863-
expect(answerDraftStream.update).toHaveBeenLastCalledWith("Done answer.");
1864-
expect(answerDraftStream.update).not.toHaveBeenCalledWith(expect.stringContaining("Exec"));
1865-
expect(nativeDraft.stop).toHaveBeenCalled();
1866-
});
1867-
1868-
it("keeps native DM drafts off by default", async () => {
1869-
const nativeDraft = createNativeToolProgressDraft();
1870-
createNativeTelegramToolProgressDraft.mockReturnValue(nativeDraft);
1871-
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
1872-
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
1873-
async ({ dispatcherOptions, replyOptions }) => {
1874-
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
1875-
await dispatcherOptions.deliver({ text: "Done answer." }, { kind: "final" });
1876-
return { queuedFinal: true };
1877-
},
1878-
);
1879-
1880-
await dispatchWithContext({
1881-
context: createContext(),
1882-
streamMode: "partial",
1883-
telegramCfg: { streaming: { mode: "partial" } },
1884-
});
1885-
1886-
expect(createNativeTelegramToolProgressDraft).not.toHaveBeenCalled();
1887-
expect(nativeDraft.update).not.toHaveBeenCalled();
1888-
expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, expect.stringContaining("Exec"));
1889-
expect(answerDraftStream.update).toHaveBeenLastCalledWith("Done answer.");
1890-
});
1891-
1892-
it("honors the native DM draft allowlist", async () => {
1893-
const nativeDraft = createNativeToolProgressDraft();
1894-
createNativeTelegramToolProgressDraft.mockReturnValue(nativeDraft);
1895-
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
1896-
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
1897-
async ({ dispatcherOptions, replyOptions }) => {
1898-
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
1899-
await dispatcherOptions.deliver({ text: "Done answer." }, { kind: "final" });
1900-
return { queuedFinal: true };
1901-
},
1902-
);
1903-
1904-
await dispatchWithContext({
1905-
context: createContext(),
1906-
streamMode: "partial",
1907-
telegramCfg: {
1908-
streaming: {
1909-
mode: "partial",
1910-
preview: {
1911-
nativeToolProgress: true,
1912-
nativeToolProgressAllowFrom: ["999"],
1913-
},
1914-
},
1915-
},
1916-
});
1917-
1918-
expect(createNativeTelegramToolProgressDraft).not.toHaveBeenCalled();
1919-
expect(nativeDraft.update).not.toHaveBeenCalled();
1920-
expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, expect.stringContaining("Exec"));
1921-
expect(answerDraftStream.update).toHaveBeenLastCalledWith("Done answer.");
1922-
});
1923-
1924-
it("falls back to edited preview tool progress when native DM draft update fails", async () => {
1925-
const nativeDraft = createNativeToolProgressDraft(false);
1926-
createNativeTelegramToolProgressDraft.mockReturnValue(nativeDraft);
1927-
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
1928-
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
1929-
async ({ dispatcherOptions, replyOptions }) => {
1930-
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
1931-
await dispatcherOptions.deliver({ text: "Done answer." }, { kind: "final" });
1932-
return { queuedFinal: true };
1933-
},
1934-
);
1935-
1936-
await dispatchWithContext({
1937-
context: createContext(),
1938-
streamMode: "partial",
1939-
telegramCfg: {
1940-
streaming: { mode: "partial", preview: { nativeToolProgress: true } },
1941-
},
1942-
});
1943-
1944-
expect(nativeDraft.update).toHaveBeenCalledWith(expect.stringContaining("Exec"));
1945-
expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, expect.stringContaining("Exec"));
1946-
expect(answerDraftStream.update).toHaveBeenLastCalledWith("Done answer.");
1947-
});
1948-
1949-
it("does not hide durable tool media in native DM drafts", async () => {
1950-
const nativeDraft = createNativeToolProgressDraft();
1951-
createNativeTelegramToolProgressDraft.mockReturnValue(nativeDraft);
1952-
setupDraftStreams({ answerMessageId: 2001 });
1953-
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
1954-
await dispatcherOptions.deliver(
1955-
{ text: "Rendered chart", mediaUrl: "/tmp/chart.png" },
1956-
{ kind: "tool" },
1957-
);
1958-
return { queuedFinal: true };
1959-
});
1960-
1961-
await dispatchWithContext({
1962-
context: createContext(),
1963-
streamMode: "partial",
1964-
telegramCfg: {
1965-
streaming: { mode: "partial", preview: { nativeToolProgress: true } },
1966-
},
1967-
});
1968-
1969-
expect(nativeDraft.update).not.toHaveBeenCalled();
1970-
expectDeliveredReply(0, { text: "Rendered chart", mediaUrl: "/tmp/chart.png" });
1971-
});
1972-
1973-
it("does not hide durable tool errors in native DM drafts", async () => {
1974-
const nativeDraft = createNativeToolProgressDraft();
1975-
createNativeTelegramToolProgressDraft.mockReturnValue(nativeDraft);
1976-
setupDraftStreams({ answerMessageId: 2001 });
1977-
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
1978-
await dispatcherOptions.deliver({ text: "Tool failed", isError: true }, { kind: "tool" });
1979-
return { queuedFinal: true };
1980-
});
1981-
1982-
await dispatchWithContext({
1983-
context: createContext(),
1984-
streamMode: "partial",
1985-
telegramCfg: {
1986-
streaming: { mode: "partial", preview: { nativeToolProgress: true } },
1987-
},
1988-
});
1989-
1990-
expect(nativeDraft.update).not.toHaveBeenCalled();
1991-
expectDeliveredReply(0, { text: "Tool failed", isError: true });
1992-
});
1993-
1994-
it("does not hide exec approval payloads in native DM drafts", async () => {
1995-
const nativeDraft = createNativeToolProgressDraft();
1996-
createNativeTelegramToolProgressDraft.mockReturnValue(nativeDraft);
1997-
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
1998-
const execApproval = { id: "approval-1", command: "pnpm test" };
1999-
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
2000-
await dispatcherOptions.deliver(
2001-
{
2002-
text: "Approve command?",
2003-
channelData: { execApproval },
2004-
},
2005-
{ kind: "tool" },
2006-
);
2007-
return { queuedFinal: true };
2008-
});
2009-
2010-
await dispatchWithContext({
2011-
context: createContext(),
2012-
streamMode: "partial",
2013-
telegramCfg: {
2014-
streaming: { mode: "partial", preview: { nativeToolProgress: true } },
2015-
},
2016-
});
2017-
2018-
expect(nativeDraft.update).not.toHaveBeenCalled();
2019-
expect(answerDraftStream.update).toHaveBeenCalledWith("Approve command?");
2020-
});
2021-
2022-
it("does not use native tool progress drafts in groups", async () => {
2023-
setupDraftStreams({ answerMessageId: 2001 });
2024-
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
2025-
async ({ dispatcherOptions, replyOptions }) => {
2026-
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
2027-
await dispatcherOptions.deliver({ text: "Done answer." }, { kind: "final" });
2028-
return { queuedFinal: true };
2029-
},
2030-
);
2031-
2032-
await dispatchWithContext({
2033-
context: createContext({
2034-
ctxPayload: {
2035-
SessionKey: "agent:main:telegram:group:-100123",
2036-
ChatType: "group",
2037-
} as unknown as TelegramMessageContext["ctxPayload"],
2038-
msg: {
2039-
chat: { id: -100123, type: "supergroup" },
2040-
message_id: 99,
2041-
} as unknown as TelegramMessageContext["msg"],
2042-
chatId: -100123,
2043-
isGroup: true,
2044-
threadSpec: { id: undefined, scope: "none" },
2045-
}),
2046-
streamMode: "partial",
2047-
telegramCfg: {
2048-
streaming: { mode: "partial", preview: { nativeToolProgress: true } },
2049-
},
2050-
});
2051-
2052-
expect(createNativeTelegramToolProgressDraft).not.toHaveBeenCalled();
2053-
});
2054-
20551822
it("does not hide text-only tool output after answer streaming starts", async () => {
2056-
const nativeDraft = createNativeToolProgressDraft();
2057-
createNativeTelegramToolProgressDraft.mockReturnValue(nativeDraft);
20581823
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
20591824
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
20601825
async ({ dispatcherOptions, replyOptions }) => {
@@ -2068,13 +1833,10 @@ describe("dispatchTelegramMessage draft streaming", () => {
20681833
context: createContext(),
20691834
streamMode: "partial",
20701835
telegramCfg: {
2071-
streaming: { mode: "partial", preview: { nativeToolProgress: true } },
1836+
streaming: { mode: "partial" },
20721837
},
20731838
});
20741839

2075-
expect(nativeDraft.update).not.toHaveBeenCalledWith(
2076-
expect.stringContaining("Tool result after partial"),
2077-
);
20781840
expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "Partial answer");
20791841
expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "Tool result after partial");
20801842
});

0 commit comments

Comments
 (0)