diff --git a/extensions/telegram/src/bot-message-dispatch-draft.ts b/extensions/telegram/src/bot-message-dispatch-draft.ts index 349c1c7b5fbe..689eecc304d6 100644 --- a/extensions/telegram/src/bot-message-dispatch-draft.ts +++ b/extensions/telegram/src/bot-message-dispatch-draft.ts @@ -8,7 +8,7 @@ import type { } from "openclaw/plugin-sdk/config-contracts"; import type { ReplyPayload } from "openclaw/plugin-sdk/reply-payload"; import type { BlockReplyContext } from "openclaw/plugin-sdk/reply-runtime"; -import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; +import { createSubsystemLogger, logVerbose } from "openclaw/plugin-sdk/runtime-env"; import type { TelegramBotDeps } from "./bot-deps.js"; import { resolveMarkdownTableMode } from "./bot-message-dispatch.runtime.js"; import type { @@ -26,6 +26,8 @@ import { recordOutboundMessageForPromptContext } from "./outbound-message-contex import { splitTelegramReasoningText } from "./reasoning-lane-coordinator.js"; import { buildTelegramRichMarkdown, TELEGRAM_RICH_TEXT_LIMIT } from "./rich-message.js"; +const draftLogger = createSubsystemLogger("telegram/draft-stream"); + const DRAFT_MIN_INITIAL_CHARS = 30; type DraftPartialTextUpdate = { @@ -161,7 +163,15 @@ export function createTelegramDraftController(params: { }); }, log: logVerbose, - warn: logVerbose, + // Draft delivery failures must stay operator-visible: verbose-only + // logging hid preview send/edit/cleanup errors, so a dead progress + // stream looked like the bot silently ignoring the user. + warn: (message) => + draftLogger.warn(message, { + lane: laneName, + chatId: params.chatId, + threadId: params.threadSpec.id, + }), }) : undefined; return { diff --git a/extensions/telegram/src/bot-message-dispatch.draft-failures-progress.test.ts b/extensions/telegram/src/bot-message-dispatch.draft-failures-progress.test.ts index 7b67af1d5dbc..cc5e25399720 100644 --- a/extensions/telegram/src/bot-message-dispatch.draft-failures-progress.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.draft-failures-progress.test.ts @@ -1,9 +1,10 @@ -import { expect, it } from "vitest"; +import { expect, it, vi } from "vitest"; import { describeTelegramDispatch, createContext, createDirectSessionPayload, createReasoningStreamContext, + createTelegramDraftStream, deliverReplies, dispatchReplyWithBufferedBlockDispatcher, dispatchWithContext, @@ -11,12 +12,48 @@ import { expectDeliveredReply, expectDeliverRepliesParams, expectWindowCollapsedTo, + mockCallArg, requireInvocationOrder, setupDraftStreams, telegramProgressPreview, } from "./bot-message-dispatch.test-harness.js"; +const draftWarn = vi.hoisted(() => vi.fn()); + +vi.mock("openclaw/plugin-sdk/runtime-env", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createSubsystemLogger: (subsystem: string) => { + const logger = actual.createSubsystemLogger(subsystem); + return subsystem === "telegram/draft-stream" ? { ...logger, warn: draftWarn } : logger; + }, + }; +}); + describeTelegramDispatch("dispatchTelegramMessage draft-failures-progress", () => { + it("routes draft stream failures to the warn-level telegram logger with lane context", async () => { + setupDraftStreams({ answerMessageId: 2001 }); + dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => { + await dispatcherOptions.deliver({ text: "Final answer" }, { kind: "final" }); + return { queuedFinal: true }; + }); + + await dispatchWithContext({ context: createContext() }); + + const draftParams = mockCallArg(createTelegramDraftStream) as { + warn?: (message: string) => void; + }; + expect(typeof draftParams.warn).toBe("function"); + draftWarn.mockClear(); + draftParams.warn?.("telegram stream preview failed: 400: Bad Request: chat not found"); + + expect(draftWarn).toHaveBeenCalledWith( + "telegram stream preview failed: 400: Bad Request: chat not found", + { lane: "answer", chatId: 123, threadId: 777 }, + ); + }); + it("sends an error fallback when dispatch fails after only partial output", async () => { dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => { await dispatcherOptions.deliver({ text: "partial answer" }, { kind: "block" });