From f28bb5e79dd14595b08154c3648d58c69057fcb2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 21 Aug 2026 01:19:49 -0700 Subject: [PATCH] refactor(telegram): remove obsolete progress summary collapse (#127089) --- .../src/bot-message-dispatch-progress.ts | 5 - .../src/bot-message-dispatch-reply.ts | 3 +- .../telegram/src/bot-message-dispatch-turn.ts | 4 - ...e-dispatch.draft-failures-progress.test.ts | 71 ++------------ ...ssage-dispatch.progress-lifecycle.test.ts} | 74 +++------------ ...essage-dispatch.progress-rendering.test.ts | 4 +- ...-message-dispatch.progress-updates.test.ts | 9 +- ...e-dispatch.progress-window.test-helpers.ts | 13 --- .../telegram/src/bot-message-dispatch.ts | 4 +- .../src/bot-message-dispatch.types.ts | 2 - .../telegram/src/draft-stream.test-helpers.ts | 18 ---- extensions/telegram/src/draft-stream.test.ts | 93 ------------------- extensions/telegram/src/draft-stream.ts | 63 ------------- 13 files changed, 24 insertions(+), 339 deletions(-) rename extensions/telegram/src/{bot-message-dispatch.progress-summary.test.ts => bot-message-dispatch.progress-lifecycle.test.ts} (77%) diff --git a/extensions/telegram/src/bot-message-dispatch-progress.ts b/extensions/telegram/src/bot-message-dispatch-progress.ts index 965fc27d426f..d1d6bdf511a3 100644 --- a/extensions/telegram/src/bot-message-dispatch-progress.ts +++ b/extensions/telegram/src/bot-message-dispatch-progress.ts @@ -51,14 +51,11 @@ type TelegramProgressDraftState = { export function createProgressState( config: TurnConfig, draftState: TelegramProgressDraftState, - getTurn: () => Turn, prepareAnswerLaneForToolProgress: () => Promise, ): TelegramProgressStateSlice { const progressState = { - draftEverRendered: false, finalAnswerDeliveryStarted: false, finalAnswerDelivered: false, - sawProgressFinal: false, verboseProgressActive: () => false, }; const progressCompositor = createChannelProgressDraftCompositor({ @@ -80,7 +77,6 @@ export function createProgressState( // headline/checklist mode, so they must not also arrive inside the text. rendersRollingLinesNatively: true, update: async (streamText, options) => { - getTurn().draftEverRendered = true; await prepareAnswerLaneForToolProgress(); draftState.answerLane.lastPartialText = streamText; draftState.answerLane.hasStreamedMessage = true; @@ -159,7 +155,6 @@ export function markFinalStarted(turn: Turn): void { export function markFinalDelivered(turn: Turn): void { turn.finalAnswerDelivered = true; - turn.sawProgressFinal = true; turn.progressCompositor.markFinalReplyDelivered(); } diff --git a/extensions/telegram/src/bot-message-dispatch-reply.ts b/extensions/telegram/src/bot-message-dispatch-reply.ts index 3a447e94cdaf..4acea989651e 100644 --- a/extensions/telegram/src/bot-message-dispatch-reply.ts +++ b/extensions/telegram/src/bot-message-dispatch-reply.ts @@ -372,8 +372,7 @@ export async function deliverReply( turn.streamMode === "progress" && info.kind === "block" && effectivePayload.isCommentary === true; - // CLI finals exclude separately classified commentary. Send that block outside - // the disposable progress stream or its collapse summary erases the text. + // CLI finals exclude separately classified commentary, so it must outlive the progress draft. const suppressProgressAnswerBlock = turn.streamMode === "progress" && info.kind === "block" && diff --git a/extensions/telegram/src/bot-message-dispatch-turn.ts b/extensions/telegram/src/bot-message-dispatch-turn.ts index cdfe03758694..199283d54264 100644 --- a/extensions/telegram/src/bot-message-dispatch-turn.ts +++ b/extensions/telegram/src/bot-message-dispatch-turn.ts @@ -241,7 +241,6 @@ export async function runTelegramDispatchTurn(turn: Turn) { beginDraftQueuedFollowup(turn); turn.finalAnswerDeliveryStarted = false; turn.finalAnswerDelivered = false; - turn.sawProgressFinal = false; turn.progressCompositor.beginNewTurn({ force: true }); }, onQueuedFollowupSettled: async () => { @@ -314,9 +313,6 @@ export async function runTelegramDispatchTurn(turn: Turn) { turn.agentRunFailed = readAgentRunTerminalOutcome(turnResult.dispatchResult) === "failed"; turn.noVisibleReplyFallbackEligible = turnResult.dispatchResult.noVisibleReplyFallbackEligible === true; - if (hasFinalInboundReplyDispatch(turnResult.dispatchResult)) { - turn.sawProgressFinal = true; - } turn.suppressSilentReplyFallback = turnResult.dispatchResult.sourceReplyDeliveryMode === "message_tool_only"; return true; 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 93b34e916d36..a6ae999ede71 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,14 +1,11 @@ import { dispatchReplyWithBufferedBlockDispatcher as dispatchReplyWithBufferedBlockDispatcherRuntime } from "openclaw/plugin-sdk/reply-dispatch-runtime"; import { expect, it, vi } from "vitest"; +import { expectWindowRetiredAfterFinal } from "./bot-message-dispatch.progress-window.test-helpers.js"; import { - expectWindowRetiredAfterFinal, - expectWindowRetiredWithoutSummary, -} from "./bot-message-dispatch.progress-window.test-helpers.js"; -import { + allDeliveredReplyTexts, describeTelegramDispatch, createContext, createDirectSessionPayload, - createReasoningStreamContext, createStatusReactionController, createTelegramDraftStream, deliverReplies, @@ -592,7 +589,6 @@ describeTelegramDispatch("dispatchTelegramMessage draft-failures-progress", () = // A tool-only window retires by repositioning in place (not delete + repost // — Discord parity), so clear() is never called on it. expect(answerDraftStream.clear).not.toHaveBeenCalled(); - expectWindowRetiredWithoutSummary(answerDraftStream); expectDeliveredReply(0, { text: "Branch is up to date" }); expectDeliverRepliesParams({ replyToMode: "off" }); // The final answer is SENT before the window retires: sending first keeps @@ -619,7 +615,6 @@ describeTelegramDispatch("dispatchTelegramMessage draft-failures-progress", () = }); expect(answerDraftStream.update).not.toHaveBeenCalledWith("Terminal block answer"); - expect(answerDraftStream.finalizeToPreview).not.toHaveBeenCalled(); expectDeliveredReply(0, { text: "Terminal block answer" }); }); @@ -646,22 +641,11 @@ describeTelegramDispatch("dispatchTelegramMessage draft-failures-progress", () = expect.objectContaining({ text: expect.stringContaining("Exec") }), ); expectDeliveredReply(0, { text: "Terminal block after tool" }); - expectWindowRetiredWithoutSummary(answerDraftStream); expectWindowRetiredAfterFinal(answerDraftStream, deliverReplies); }); - function allDeliveredReplyTexts(): string[] { - return deliverReplies.mock.calls.flatMap((call: unknown[]) => - ((call[0] as { replies?: Array<{ text?: string }> }).replies ?? []).map( - (reply) => reply.text ?? "", - ), - ); - } - it("sends the final answer before retiring the progress window", async () => { - // Edit-shrink anchor loss: shrinking the tall window to a one-line bar BEFORE - // the final is sent breaks the client's at-bottom follow and drops the final - // off screen. The final must be sent FIRST, then the window edited down. + // Deliver first so removing the progress window cannot move the final off screen. const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( async ({ dispatcherOptions, replyOptions }) => { @@ -677,15 +661,11 @@ describeTelegramDispatch("dispatchTelegramMessage draft-failures-progress", () = telegramCfg: { streaming: { mode: "progress" } }, }); - // Final delivered first, then the window retires behind it. expectDeliveredReply(0, { text: "All done" }); - expectWindowRetiredWithoutSummary(answerDraftStream); expectWindowRetiredAfterFinal(answerDraftStream, deliverReplies); }); - it("still collapses the window when the final answer send is skipped", async () => { - // Failure path: if the final send skips/fails, the window must not be left - // stale — it still collapses to the bar (once-guard already consumed). + it("retires the progress window when the final answer send is skipped", async () => { const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); deliverReplies.mockResolvedValue({ delivered: false }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( @@ -702,42 +682,12 @@ describeTelegramDispatch("dispatchTelegramMessage draft-failures-progress", () = telegramCfg: { streaming: { mode: "progress" } }, }); - // The bar still edits the window in place even though the final send failed. - expectWindowRetiredWithoutSummary(answerDraftStream); + expect(answerDraftStream.rotateToNewMessageDeferringDelete).toHaveBeenCalledTimes(1); }); - it("tallies reasoning bursts and tool calls into the collapse summary", async () => { - const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); - dispatchReplyWithBufferedBlockDispatcher.mockImplementation( - async ({ dispatcherOptions, replyOptions }) => { - // burst 1 → tool → burst 2 → tool, then a trailing burst flushed at the - // summary: 3 thoughts, 2 tool calls. - await replyOptions?.onReasoningStream?.({ text: "thinking a" }); - await replyOptions?.onToolStart?.({ name: "exec", phase: "start" }); - await replyOptions?.onReasoningStream?.({ text: "thinking b" }); - await replyOptions?.onToolStart?.({ name: "exec", phase: "start" }); - await replyOptions?.onReasoningStream?.({ text: "thinking c" }); - await dispatcherOptions.deliver({ text: "Done" }, { kind: "final" }); - return { queuedFinal: true }; - }, - ); - - await dispatchWithContext({ - // Reasoning must resolve to "stream" so thoughts route into the progress - // window — only window-streamed reasoning feeds the collapse summary. - context: createReasoningStreamContext(), - streamMode: "progress", - telegramCfg: { streaming: { mode: "progress" } }, - }); - - expectWindowRetiredWithoutSummary(answerDraftStream); - expectDeliveredReply(0, { text: "Done" }); - }); - - it("does not post a collapse summary when no progress draft started", async () => { + it("delivers only the final answer when no progress draft started", async () => { setupDraftStreams({ answerMessageId: 2001 }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => { - // No tools, thoughts, or notes — nothing collapses; just a final answer. await dispatcherOptions.deliver({ text: "Just an answer" }, { kind: "final" }); return { queuedFinal: true }; }); @@ -748,12 +698,10 @@ describeTelegramDispatch("dispatchTelegramMessage draft-failures-progress", () = telegramCfg: { streaming: { mode: "progress" } }, }); - const texts = allDeliveredReplyTexts(); - expect(texts.some((text) => text.includes("⏱️"))).toBe(false); - expect(texts).toContain("Just an answer"); + expect(allDeliveredReplyTexts()).toEqual(["Just an answer"]); }); - it("does not post a collapse summary before an error final", async () => { + it("delivers only the error final after tool progress", async () => { setupDraftStreams({ answerMessageId: 2001 }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( async ({ dispatcherOptions, replyOptions }) => { @@ -772,7 +720,6 @@ describeTelegramDispatch("dispatchTelegramMessage draft-failures-progress", () = telegramCfg: { streaming: { mode: "progress" } }, }); - const texts = allDeliveredReplyTexts(); - expect(texts.some((text) => text.includes("tool call · ⏱️"))).toBe(false); + expect(allDeliveredReplyTexts()).toEqual(["Something went wrong"]); }); }); diff --git a/extensions/telegram/src/bot-message-dispatch.progress-summary.test.ts b/extensions/telegram/src/bot-message-dispatch.progress-lifecycle.test.ts similarity index 77% rename from extensions/telegram/src/bot-message-dispatch.progress-summary.test.ts rename to extensions/telegram/src/bot-message-dispatch.progress-lifecycle.test.ts index 2d7c7627420d..cc024d9719ac 100644 --- a/extensions/telegram/src/bot-message-dispatch.progress-summary.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.progress-lifecycle.test.ts @@ -1,5 +1,5 @@ import { expect, it, vi } from "vitest"; -import { expectWindowRetiredWithoutSummary } from "./bot-message-dispatch.progress-window.test-helpers.js"; +import { expectWindowRetiredAfterFinal } from "./bot-message-dispatch.progress-window.test-helpers.js"; import { describeTelegramDispatch, allDeliveredReplyTexts, @@ -16,11 +16,9 @@ import { } from "./bot-message-dispatch.test-harness.js"; import type { TelegramMessageContext } from "./bot-message-dispatch.test-harness.js"; -describeTelegramDispatch("dispatchTelegramMessage progress-summary", () => { +describeTelegramDispatch("dispatchTelegramMessage progress-lifecycle", () => { it("keeps the progress window alive under /reasoning on so commentary and tools still stream", async () => { - // /reasoning on removes only the 🧠 lane from the window; commentary, tool - // lines, and the collapse bar must still stream (Discord parity). A prior - // regression forced block streaming in progress mode, killing the window. + // Durable reasoning removes only the reasoning lane, not commentary or tool progress. loadSessionStore.mockReturnValue({ s1: { reasoningLevel: "on" } }); const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( @@ -40,26 +38,16 @@ describeTelegramDispatch("dispatchTelegramMessage progress-summary", () => { telegramCfg: { streaming: { mode: "progress", progress: { commentary: true } } }, }); - // The window streamed (a preview was rendered) and collapsed into a bar - // counting the note + tool — proof the window was not killed. expect(answerDraftStream.updatePreview).toHaveBeenCalled(); - expectWindowRetiredWithoutSummary(answerDraftStream); expectDeliveredReply(0, { text: "Done" }); }); - it("collapses a tool-progress-only window without deleting when reasoning is durable and the lane rotated mid-turn (on-off)", async () => { - // on-off cell: /reasoning on (durable), /verbose off. The window streams - // tool progress only; a mid-turn assistant boundary/rotation must not leave - // the collapse to a delete + repost. Every non-error collapse edits in place - // (or posts the bar durably) — NEVER a bare clear()/deleteMessage — so there - // is exactly one bar and no Telegram focus-jump. + it("retires a tool-progress-only window after durable reasoning and a mid-turn boundary", async () => { loadSessionStore.mockReturnValue({ s1: { reasoningLevel: "on" } }); const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( async ({ dispatcherOptions, replyOptions }) => { await replyOptions?.onToolStart?.({ name: "exec", phase: "start" }); - // Durable reasoning + an assistant boundary land between tool progress - // and the final — the mid-turn churn that dropped the live window id. await dispatcherOptions.deliver( { text: "hidden", isReasoning: true }, { kind: "block" }, @@ -79,21 +67,12 @@ describeTelegramDispatch("dispatchTelegramMessage progress-summary", () => { telegramCfg: { streaming: { mode: "progress" } }, }); - // Collapse edited the window in place into the bar; the window was NOT - // deleted (no focus-jump), and exactly one bar exists. - expectWindowRetiredWithoutSummary(answerDraftStream); expect(answerDraftStream.clear).not.toHaveBeenCalled(); - const texts = allDeliveredReplyTexts(); - expect(texts.filter((text) => text.includes("⏱️"))).toHaveLength(0); // bar is the in-place edit - expect(texts).toContain("Done"); + expect(allDeliveredReplyTexts()).toContain("Done"); }); it("keeps a single stationary window when text follows durable reasoning (no mid-turn rotation)", async () => { - // Single-message model (Discord parity): in progress mode the window is ONE - // message edited through every lane handover — durable 🧠, interim answer - // text — and edited into the bar only at collapse. It must NOT reposition or - // rotate mid-turn (no new bubble, no delete), which is what caused the churn - // and the on-off jump. Interim answer text does not render into the window. + // Interim answer text must not rotate or render into the progress window. loadSessionStore.mockReturnValue({ s1: { reasoningLevel: "on" } }); const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( @@ -103,7 +82,6 @@ describeTelegramDispatch("dispatchTelegramMessage progress-summary", () => { { text: "hidden", isReasoning: true }, { kind: "block" }, ); - // Interim answer text mid-turn: must not spawn a new window bubble. await dispatcherOptions.deliver({ text: "Here is the answer" }, { kind: "block" }); await dispatcherOptions.deliver({ text: "Here is the answer." }, { kind: "final" }); return { queuedFinal: true }; @@ -118,20 +96,11 @@ describeTelegramDispatch("dispatchTelegramMessage progress-summary", () => { telegramCfg: { streaming: { mode: "progress" } }, }); - // The one window message stays put through the whole turn: no mid-turn - // reposition. It is retired once at end of turn, leaving the final answer as - // the only surviving message. expect(answerDraftStream.rotateToNewMessageDeferringDelete).not.toHaveBeenCalled(); expect(answerDraftStream.clear).toHaveBeenCalledTimes(1); - expect(answerDraftStream.finalizeToPreview).not.toHaveBeenCalled(); - expectWindowRetiredWithoutSummary(answerDraftStream); }); it("uses one stationary window message across a multi-boundary turn (commentary→tool→commentary→tool→final)", async () => { - // Single-message model (Discord parity): ONE window message id is created - // once and edited through every lane handover; it collapses into the bar in - // place at the end. Zero deletes in the happy path; the final is posted - // before the bar edit (task-9 order). const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( async ({ dispatcherOptions, replyOptions }) => { @@ -150,34 +119,22 @@ describeTelegramDispatch("dispatchTelegramMessage progress-summary", () => { telegramCfg: { streaming: { mode: "progress", progress: { commentary: true } } }, }); - // The SAME window message id is used the whole turn — no new bubble. const windowMessageIds = new Set( answerDraftStream.updatePreview.mock.calls .map(() => answerDraftStream.messageId()) .filter((id) => id != null), ); expect(windowMessageIds).toEqual(new Set([2001])); - // The window was EDITED many times (once per lane change) ... expect(answerDraftStream.updatePreview.mock.calls.length).toBeGreaterThan(1); - // A tool-only window is never deleted. It retires in place exactly once, - // after the final send, so the tool log survives with no mid-turn churn. expect(answerDraftStream.clear).not.toHaveBeenCalled(); - expect(answerDraftStream.finalizeToPreview).not.toHaveBeenCalled(); expect(answerDraftStream.rotateToNewMessageDeferringDelete).toHaveBeenCalledTimes(1); - expectWindowRetiredWithoutSummary(answerDraftStream); expectDeliveredReply(0, { text: "Final answer" }); - expect(requireInvocationOrder(deliverReplies, 0, "first reply delivery")).toBeLessThan( - requireInvocationOrder( - answerDraftStream.rotateToNewMessageDeferringDelete, - 0, - "progress window retirement", - ), - ); + expectWindowRetiredAfterFinal(answerDraftStream, deliverReplies); }); - it("keeps Claude CLI pre-tool commentary after the progress window collapses", async () => { + it("keeps CLI pre-tool commentary after the progress window retires", async () => { const markers = "Test markers: caribou-lampion-473, fromage-quantique, satellite-en-tricot"; - const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); + setupDraftStreams({ answerMessageId: 2001 }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( async ({ dispatcherOptions, replyOptions }) => { expect(replyOptions?.commentaryPayloadsEnabled).toBe(true); @@ -202,7 +159,6 @@ describeTelegramDispatch("dispatchTelegramMessage progress-summary", () => { telegramCfg: { streaming: { mode: "progress" } }, }); - expectWindowRetiredWithoutSummary(answerDraftStream); expect(allDeliveredReplyTexts()).toEqual([markers, "TEST DONE"]); }); @@ -235,15 +191,13 @@ describeTelegramDispatch("dispatchTelegramMessage progress-summary", () => { ), ]; expect(windowTexts.some((text) => text.includes("Interim answer prose"))).toBe(false); - // The final answer is delivered below the collapsed window. const delivered = allDeliveredReplyTexts(); expect(delivered).toContain("The real final answer."); expect(delivered.some((text) => text.includes("Interim answer prose"))).toBe(false); }); it("does not duplicate tool lines into the window under verbose", async () => { - // Invariant D2 (persistent XOR window): when the durable verbose lane owns - // tool messages, the window must render no tool line and must not count it. + // The durable verbose lane owns tool messages, so the progress window must not duplicate them. const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( async ({ dispatcherOptions, replyOptions }) => { @@ -260,12 +214,8 @@ describeTelegramDispatch("dispatchTelegramMessage progress-summary", () => { telegramCfg: { streaming: { mode: "progress" } }, }); - // No tool line ever rendered to the window (verbose owns it durably), so the - // window never streamed and there is no collapse bar to count it. expect(answerDraftStream.updatePreview).not.toHaveBeenCalled(); - expect(answerDraftStream.finalizeToPreview).not.toHaveBeenCalled(); - const texts = allDeliveredReplyTexts(); - expect(texts.some((text) => text.includes("tool call"))).toBe(false); + expect(allDeliveredReplyTexts()).toEqual(["Done"]); }); it("replaces Telegram command progress items with matching command output", async () => { @@ -340,8 +290,6 @@ describeTelegramDispatch("dispatchTelegramMessage progress-summary", () => { ).toBeLessThan( requireInvocationOrder(answerDraftStream.update, 0, "first answer draft update"), ); - // The window retires at end of turn; the final answer posts fresh below it. - expectWindowRetiredWithoutSummary(answerDraftStream); expectDeliveredReply(0, { text: "Branch is up to date" }); }); diff --git a/extensions/telegram/src/bot-message-dispatch.progress-rendering.test.ts b/extensions/telegram/src/bot-message-dispatch.progress-rendering.test.ts index fbc476e326ab..2b67e1f80c23 100644 --- a/extensions/telegram/src/bot-message-dispatch.progress-rendering.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.progress-rendering.test.ts @@ -346,9 +346,7 @@ describeTelegramDispatch("dispatchTelegramMessage progress-rendering", () => { "Shelling\n🔎 Web Search docs lookup\nUpdate tests passed", ), ); - // A tool-progress-only window with nothing to summarize is torn down via the - // deferred-delete reposition (new content first, delete later), not a bare - // immediate clear/delete or forceNewMessage. + // Retire a tool-progress-only window by repositioning, with its delete deferred. expect(draftStream.rotateToNewMessageDeferringDelete).toHaveBeenCalledTimes(1); expect(draftStream.forceNewMessage).not.toHaveBeenCalled(); expect(draftStream.clear).not.toHaveBeenCalled(); diff --git a/extensions/telegram/src/bot-message-dispatch.progress-updates.test.ts b/extensions/telegram/src/bot-message-dispatch.progress-updates.test.ts index 9d4c04758ab7..d6cc1799aa68 100644 --- a/extensions/telegram/src/bot-message-dispatch.progress-updates.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.progress-updates.test.ts @@ -3,7 +3,6 @@ import { isChannelPartialDeliveryError, } from "openclaw/plugin-sdk/channel-inbound"; import { expect, it } from "vitest"; -import { expectWindowRetiredWithoutSummary } from "./bot-message-dispatch.progress-window.test-helpers.js"; import { appendAssistantMirrorMessageByIdentity, type DispatchReplyWithBufferedBlockDispatcherArgs, @@ -52,7 +51,6 @@ describeTelegramDispatch("dispatchTelegramMessage progress-updates", () => { expect(answerDraftStream.updatePreview).toHaveBeenCalledWith( telegramProgressPreview("Shelling\n\n🛠️ Exec", "Shelling\n🛠️ Exec"), ); - expectWindowRetiredWithoutSummary(answerDraftStream); expectDeliveredReply(0, { text: "Branch is up to date" }); }); @@ -83,7 +81,6 @@ describeTelegramDispatch("dispatchTelegramMessage progress-updates", () => { expect(answerDraftStream.updatePreview).toHaveBeenCalledWith( telegramProgressPreview("Shelling\n\n🛠️ Exec", "Shelling\n🛠️ Exec"), ); - expectWindowRetiredWithoutSummary(answerDraftStream); expectDeliveredReply(0, { text: "Branch is up to date" }); }); @@ -118,12 +115,11 @@ describeTelegramDispatch("dispatchTelegramMessage progress-updates", () => { expect(answerDraftStream.updatePreview).toHaveBeenCalledWith( telegramProgressPreview("Shelling\n\n🛠️ Exec", "Shelling\n🛠️ Exec"), ); - expectWindowRetiredWithoutSummary(answerDraftStream); expectDeliveredReply(0, { text: "Branch is up to date" }); }); it("uses the transcript final when progress-mode final text is truncated", async () => { - const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 }); + setupDraftStreams({ answerMessageId: 2001 }); const fullAnswer = "Ja. Hier nochmal sauber Schritt fuer Schritt. Einen API Key kopiert man aus der Google Cloud Console. Danach pruefst du die Projekt- und API-Einstellungen."; const truncatedFinal = @@ -149,7 +145,6 @@ describeTelegramDispatch("dispatchTelegramMessage progress-updates", () => { telegramCfg: { streaming: { mode: "progress" } }, }); - expectWindowRetiredWithoutSummary(answerDraftStream); expectDeliveredReply(0, { text: fullAnswer }); }); @@ -447,7 +442,6 @@ describeTelegramDispatch("dispatchTelegramMessage progress-updates", () => { expect(rollingPreview?.text).toContain(`command-${index}`); } expectDeliveredReply(0, { text: "Done" }); - expectWindowRetiredWithoutSummary(draftStream); }); it("renders command status without command output in Telegram progress draft previews", async () => { @@ -587,7 +581,6 @@ describeTelegramDispatch("dispatchTelegramMessage progress-updates", () => { "Shelling\n🧠 Thinking… (~200 tokens)", ), ); - expectWindowRetiredWithoutSummary(draftStream); expectDeliveredReply(0, { text: "Done" }); }); diff --git a/extensions/telegram/src/bot-message-dispatch.progress-window.test-helpers.ts b/extensions/telegram/src/bot-message-dispatch.progress-window.test-helpers.ts index bbc5f0b1e81b..1ae4693bce70 100644 --- a/extensions/telegram/src/bot-message-dispatch.progress-window.test-helpers.ts +++ b/extensions/telegram/src/bot-message-dispatch.progress-window.test-helpers.ts @@ -4,19 +4,6 @@ import { requireInvocationOrder } from "./bot-message-dispatch.test-harness.js"; type OrderedMock = { mock: { invocationCallOrder: number[] } }; -/** - * Turn end retires the progress window: no synthesized activity digest is ever - * written back into it. - */ -export function expectWindowRetiredWithoutSummary(stream: { - finalizeToPreview: { mock: { calls: unknown[][] } }; -}) { - const digests = stream.finalizeToPreview.mock.calls - .map((call) => (call[0] as { text?: string } | undefined)?.text ?? "") - .filter((text) => text.includes("⏱️")); - expect(digests).toEqual([]); -} - /** * Retirement lands after the final, so shrinking the window above it never * pushes the final off the anchored viewport. Text windows clear; tool-only diff --git a/extensions/telegram/src/bot-message-dispatch.ts b/extensions/telegram/src/bot-message-dispatch.ts index b8edf117e2cf..4095af2a5d46 100644 --- a/extensions/telegram/src/bot-message-dispatch.ts +++ b/extensions/telegram/src/bot-message-dispatch.ts @@ -346,7 +346,6 @@ export const dispatchTelegramMessage = async ( const progressState = createProgressState( turnConfig, draftState, - () => turn, async () => await prepareAnswerLaneForToolProgress(turn), ); const deliveryState = createDeliveryState({ ...turnConfig, lanes: draftState.lanes }, () => turn); @@ -394,8 +393,7 @@ export const dispatchTelegramMessage = async ( turn.dispatchError = err; runtime.error?.(danger(`telegram dispatch failed: ${String(err)}`)); } finally { - // Terminal order: stop producers, drain queued drafts, materialize accepted text, - // clean previews, then collapse the progress window. + // Stop producers before draining drafts, finalizing accepted text, and cleaning previews. turn.progressCompositor.cancel(); await waitForDraftEvents(turn); try { diff --git a/extensions/telegram/src/bot-message-dispatch.types.ts b/extensions/telegram/src/bot-message-dispatch.types.ts index 6cb6ab0396c6..b035127f8dad 100644 --- a/extensions/telegram/src/bot-message-dispatch.types.ts +++ b/extensions/telegram/src/bot-message-dispatch.types.ts @@ -188,10 +188,8 @@ export type TelegramDraftStateSlice = { }; export type TelegramProgressStateSlice = { - draftEverRendered: boolean; finalAnswerDeliveryStarted: boolean; finalAnswerDelivered: boolean; - sawProgressFinal: boolean; verboseProgressActive: () => boolean; progressCompositor: TelegramProgressCompositor; commentaryProgressEnabled: boolean; diff --git a/extensions/telegram/src/draft-stream.test-helpers.ts b/extensions/telegram/src/draft-stream.test-helpers.ts index c889d8ea17df..5421fa3b874d 100644 --- a/extensions/telegram/src/draft-stream.test-helpers.ts +++ b/extensions/telegram/src/draft-stream.test-helpers.ts @@ -18,9 +18,6 @@ type TestDraftStream = { clear: ReturnType Promise>>; stop: ReturnType Promise>>; discard: ReturnType Promise>>; - finalizeToPreview: ReturnType< - typeof vi.fn<(preview: TelegramDraftPreview) => Promise> - >; forceNewMessage: ReturnType void>>; rotateToNewMessageDeferringDelete: ReturnType number | undefined>>; sendMayHaveLanded: ReturnType boolean>>; @@ -88,14 +85,6 @@ export function createTestDraftStream(params?: { } await params?.onDiscard?.(); }), - finalizeToPreview: vi.fn().mockImplementation(async (preview: TelegramDraftPreview) => { - if (messageId == null) { - return undefined; - } - lastDeliveredText = preview.text.trimEnd(); - stopped = true; - return messageId; - }), forceNewMessage: vi.fn().mockImplementation(() => { stopped = false; if (params?.clearMessageIdOnForceNew) { @@ -160,13 +149,6 @@ export function createSequencedTestDraftStream(startMessageId = 1001): TestDraft clear: vi.fn().mockResolvedValue(undefined), stop: vi.fn().mockResolvedValue(undefined), discard: vi.fn().mockResolvedValue(undefined), - finalizeToPreview: vi.fn().mockImplementation(async (preview: TelegramDraftPreview) => { - if (activeMessageId == null) { - return undefined; - } - lastDeliveredText = preview.text.trimEnd(); - return activeMessageId; - }), forceNewMessage: vi.fn().mockImplementation(() => { activeMessageId = undefined; }), diff --git a/extensions/telegram/src/draft-stream.test.ts b/extensions/telegram/src/draft-stream.test.ts index 441642365be5..1480d53896c7 100644 --- a/extensions/telegram/src/draft-stream.test.ts +++ b/extensions/telegram/src/draft-stream.test.ts @@ -460,99 +460,6 @@ describe("createTelegramDraftStream", () => { expect(api.editMessageText).toHaveBeenCalledWith(123, 17, "see https://example.com now"); }); - it("finalizeToPreview edits the live window message in place without deleting", async () => { - const api = createMockDraftApi(); - const stream = createDraftStream(api, { thread: { id: 42, scope: "dm" } }); - - stream.update("🛠️ Exec: pnpm test"); - await stream.flush(); - const messageId = await stream.finalizeToPreview({ text: "🛠️ 1 tool call · ⏱️ 1s" }); - - expect(messageId).toBe(17); - // The window message is EDITED into the bar, never deleted (no focus-jump). - expect(api.editMessageText).toHaveBeenCalledWith(123, 17, "🛠️ 1 tool call · ⏱️ 1s"); - expect(api.deleteMessage).not.toHaveBeenCalled(); - }); - - it("finalizeToPreview materializes a still-pending window before editing", async () => { - // A throttled preview may not have been sent yet when the collapse runs; - // finalizeToPreview must send it first so there is a message to edit into - // the bar, rather than returning undefined and forcing a delete + repost. - const api = createMockDraftApi(); - const stream = createDraftStream(api, { - thread: { id: 42, scope: "dm" }, - throttleMs: 10_000, - }); - - stream.update("🛠️ Exec: pnpm test"); - const messageId = await stream.finalizeToPreview({ text: "🛠️ 1 tool call · ⏱️ 1s" }); - - expect(messageId).toBe(17); - expect(api.sendMessage).toHaveBeenCalledTimes(1); - expect(api.deleteMessage).not.toHaveBeenCalled(); - }); - - it("finalizeToPreview returns undefined when no window ever rendered", async () => { - const api = createMockDraftApi(); - const stream = createDraftStream(api, { thread: { id: 42, scope: "dm" } }); - - const messageId = await stream.finalizeToPreview({ text: "🛠️ 1 tool call · ⏱️ 1s" }); - - expect(messageId).toBeUndefined(); - expect(api.sendMessage).not.toHaveBeenCalled(); - expect(api.editMessageText).not.toHaveBeenCalled(); - expect(api.deleteMessage).not.toHaveBeenCalled(); - }); - - it("finalizeToPreview returns undefined when the in-place collapse edit does not apply", async () => { - // Red-team F2: a flood-wait (429) on the collapse edit makes the underlying - // send return false without applying. finalizeToPreview must report that as - // "not collapsed in place" (undefined) so the dispatch falls back to posting - // a durable bar — otherwise it assumes success, clears state, posts no bar, - // and the tall window is left on screen. - const api = createMockDraftApi(); - api.editMessageText.mockRejectedValueOnce( - Object.assign( - new Error("Call to 'editMessageText' failed! (429: Too Many Requests: retry after 5)"), - { error_code: 429, parameters: { retry_after: 5 } }, - ), - ); - const stream = createDraftStream(api, { thread: { id: 42, scope: "dm" } }); - - stream.update("🛠️ Exec: pnpm test"); - await stream.flush(); - const messageId = await stream.finalizeToPreview({ text: "🛠️ 1 tool call · ⏱️ 1s" }); - - expect(messageId).toBeUndefined(); - expect(api.editMessageText).toHaveBeenCalledTimes(1); - // The live window is NOT deleted (the caller posts the bar below it instead). - expect(api.deleteMessage).not.toHaveBeenCalled(); - }); - - it("does not replay a rejected pending edit after collapse fallback", async () => { - const api = createMockDraftApi(); - const retryableEditError = () => - Object.assign(new Error("429: retry after 1"), { - error_code: 429, - parameters: { retry_after: 1 }, - }); - const stream = createDraftStream(api, { thread: { id: 42, scope: "dm" } }); - - stream.update("working"); - await stream.flush(); - api.editMessageText - .mockRejectedValueOnce(retryableEditError()) - .mockRejectedValueOnce(retryableEditError()); - stream.update("pending update"); - const messageId = await stream.finalizeToPreview({ text: "🛠️ 1 tool call · ⏱️ 1s" }); - - expect(messageId).toBeUndefined(); - expect(api.editMessageText).toHaveBeenCalledTimes(2); - await stream.stop(); - await stream.flush(); - expect(api.editMessageText).toHaveBeenCalledTimes(2); - }); - it("deletes message preview on clear after finalization", async () => { vi.useFakeTimers(); try { diff --git a/extensions/telegram/src/draft-stream.ts b/extensions/telegram/src/draft-stream.ts index 07bf50dd9d43..ca8becaa538e 100644 --- a/extensions/telegram/src/draft-stream.ts +++ b/extensions/telegram/src/draft-stream.ts @@ -66,13 +66,6 @@ export type TelegramDraftStream = { remainingFinalContent?: () => TelegramDraftMessageSnapshot | undefined; /** True while a pending or visible draft owns a first/batched reply target. */ hasConsumedReplyTarget?: () => boolean; - /** - * Collapse the preview in place: edit the existing window message so its - * content becomes `preview`, then stop without deleting. Used at end-of-turn - * so the streaming window becomes the summary bar (no delete + repost, which - * scroll-jumps the client). Returns the message id if the edit landed. - */ - finalizeToPreview: (preview: TelegramDraftPreview) => Promise; /** Reset internal state so the next update creates a new message instead of editing. */ forceNewMessage: () => void; /** @@ -918,61 +911,6 @@ export function createTelegramDraftStream(params: { return undefined; }; - const finalizeToPreview = async (preview: TelegramDraftPreview): Promise => { - const finalizeGeneration = generation; - const text = preview.text.trimEnd(); - if (!text) { - return undefined; - } - // Settle pending updates so we edit the real, current window message. - streamState.final = true; - await flush(); - if (generation !== finalizeGeneration) { - return undefined; - } - // A throttled preview can still be pending (the last tool-progress line was - // coalesced and never sent), leaving no message id even though the window - // "rendered". Materialize it as a final flush would, so the window message - // exists and can be edited in place — otherwise on-off collapses missed it - // and fell back to a delete + repost. - if (typeof streamMessageId !== "number" && !streamState.stopped) { - const pending = lastRequestedText.trimEnd(); - if (pending && pending !== lastDeliveredText.trimEnd()) { - const materialized = await sendOrEditStreamMessage(pending); - if (generation !== finalizeGeneration) { - return undefined; - } - if (materialized) { - loop.resetPending(); - } - } - } - // Genuinely no live window message (rv mode never rendered): caller posts a - // fresh durable bar instead — but it must NOT delete anything. - if (typeof streamMessageId !== "number") { - return undefined; - } - // Collapse takes ownership of the live window. A stale throttled edit must - // not replay after either this edit or the caller's durable fallback. - loop.resetPending(); - // Replace the whole message with the bar line. - finalPagePlan = undefined; - lastSentPreviewKey = ""; - lastRequestedText = text; - lastRequestedPreview = { ...preview, text }; - // The edit can fail to apply (flood-wait 429 or a terminal error both return - // false). Report that as "not collapsed in place" so the caller falls back to - // posting a durable bar instead of assuming the tall window became the bar. - const edited = await sendOrEditStreamMessage(text); - if (generation !== finalizeGeneration) { - return undefined; - } - streamState.stopped = true; - observeCurrentProviderMessage(); - await drainProviderMessageObservations(); - return edited ? streamMessageId : undefined; - }; - params.log?.(`telegram stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`); return { @@ -993,7 +931,6 @@ export function createTelegramDraftStream(params: { }, remainingFinalContent, hasConsumedReplyTarget: () => replyTargetState.kind !== "available", - finalizeToPreview, forceNewMessage: () => resetStreamToNewMessage(false, true), rotateToNewMessageDeferringDelete, sendMayHaveLanded: () => messageSendAttempted && typeof streamMessageId !== "number",