Skip to content

Commit 4b42898

Browse files
committed
fix(telegram): keep tool progress after non-final commentary
Root cause: non-final inter-tool commentary suppressed the progress draft via prepareAnswerLaneForText. In non-persist progress streaming mode (with streaming.progress.commentary enabled) commentary arrives as assistant partial text (onPartialReply -> ingestDraftLaneSegments), whose answer-lane rotation in rotateAnswerLaneAfterToolProgress called suppressProgressDraftState. The next tool's pushStreamToolProgress was then dropped on the compositor's progressSuppressed guard, so the tool-progress lines after commentary vanished. Fix: in progress mode, route non-final answer-lane partial text into the shared progress draft's commentary lane (progressDraft.pushCommentaryProgress, keyed per assistant message) so commentary and tool progress accumulate in one open draft instead of tearing the lane down. This reuses the same commentary lane the onItemEvent preamble path already uses. The pushStreamToolProgress finalized / final-delivery guard and the final-answer cleanup path are left unchanged, and no new config is added. Adds a dispatch regression covering the commentary -> tool -> commentary -> tool interleave that asserts every tool-progress line stays in the draft. This is the Telegram consumer-side fix, independent of #90883 (the commentary producer); it should land first so enabling commentary + non-persist progress does not immediately regress. Fixes #90962 Signed-off-by: Jason Yao <wsyjh8@gmail.com>
1 parent 96c5d33 commit 4b42898

2 files changed

Lines changed: 65 additions & 0 deletions

File tree

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,6 +2424,54 @@ describe("dispatchTelegramMessage draft streaming", () => {
24242424
expect(draftStream.update).toHaveBeenCalledWith("Shelling\n\n`🛠️ Exec`\n• _Checking files_");
24252425
});
24262426

2427+
it("keeps tool progress after inter-tool commentary in non-persist progress mode", async () => {
2428+
// Regression for #90962. In progress mode with commentary enabled, inter-tool
2429+
// commentary arrives as assistant partial text (onPartialReply -> ingestDraftLaneSegments).
2430+
// It must accumulate into the shared progress draft, not tear the lane down, otherwise the
2431+
// tool-progress lines that follow are dropped. Drives the dispatcher with the
2432+
// commentary -> tool -> commentary -> tool interleave the #90883 producer emits.
2433+
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
2434+
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => {
2435+
await replyOptions?.onReplyStart?.();
2436+
await replyOptions?.onAssistantMessageStart?.();
2437+
await replyOptions?.onPartialReply?.({ text: "Let me check the config file" });
2438+
// onToolStart primes the live progress draft (startImmediately), which is the
2439+
// precondition for the suppress that #90962 triggers on the next commentary.
2440+
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
2441+
await replyOptions?.onAssistantMessageStart?.();
2442+
await replyOptions?.onPartialReply?.({ text: "Now running the tests" });
2443+
// The tool line #90962 drops: it arrives after the commentary suppressed the draft.
2444+
await replyOptions?.onItemEvent?.({
2445+
kind: "command",
2446+
name: "exec",
2447+
progressText: "pnpm test",
2448+
});
2449+
return { queuedFinal: false };
2450+
});
2451+
2452+
await dispatchWithContext({
2453+
context: createContext(),
2454+
streamMode: "progress",
2455+
telegramCfg: {
2456+
streaming: {
2457+
mode: "progress",
2458+
progress: { label: "Cracking", commentary: true },
2459+
},
2460+
},
2461+
});
2462+
2463+
// Both tool-progress lines must survive the interleaved commentary; the second one is the
2464+
// line #90962 drops on main once commentary suppresses the draft.
2465+
expect(answerDraftStream.update).toHaveBeenCalledWith(expect.stringContaining("Exec"));
2466+
expect(answerDraftStream.update).toHaveBeenCalledWith(expect.stringContaining("pnpm test"));
2467+
// The final draft holds every tool line and the commentary together in one open draft.
2468+
const lastDraft = answerDraftStream.update.mock.calls.at(-1)?.[0];
2469+
expect(lastDraft).toContain("Exec");
2470+
expect(lastDraft).toContain("pnpm test");
2471+
expect(lastDraft).toContain("Let me check the config file");
2472+
expect(lastDraft).toContain("Now running the tests");
2473+
});
2474+
24272475
it("renders configured Telegram commentary progress from preamble item events", async () => {
24282476
const draftStream = createSequencedDraftStream(2001);
24292477
createTelegramDraftStream.mockReturnValue(draftStream);

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,9 @@ export const dispatchTelegramMessage = async ({
967967
: undefined;
968968
let lastAnswerPartialText = "";
969969
let activeAnswerDraftIsToolProgressOnly = false;
970+
// Per-assistant-message id for progress-mode inter-tool commentary lines so they
971+
// accumulate one-per-block in the shared draft (see ingestDraftLaneSegments).
972+
let progressCommentaryItemSeq = 0;
970973
function resetAnswerToolProgressDraft() {
971974
activeAnswerDraftIsToolProgressOnly = false;
972975
}
@@ -1165,6 +1168,16 @@ export const dispatchTelegramMessage = async ({
11651168
const split = splitTextIntoLaneSegments(update, isReasoning);
11661169
for (const segment of split.segments) {
11671170
if (segment.lane === "answer") {
1171+
if (streamMode === "progress" && progressDraft.commentaryProgressEnabled) {
1172+
// Progress mode delivers the answer separately, so inter-tool partial answer text is
1173+
// commentary. With commentary enabled, route it into the shared progress draft's
1174+
// commentary lane (keyed per assistant message) so it coexists with tool progress
1175+
// instead of suppressing the answer lane and dropping the tool lines after it. #90962.
1176+
await progressDraft.pushCommentaryProgress(segment.update.text, {
1177+
itemId: `partial-commentary:${progressCommentaryItemSeq}`,
1178+
});
1179+
continue;
1180+
}
11681181
await prepareAnswerLaneForText();
11691182
}
11701183
if (segment.lane === "reasoning") {
@@ -2021,6 +2034,10 @@ export const dispatchTelegramMessage = async ({
20212034
finalAnswerDelivered = false;
20222035
if (streamMode !== "progress") {
20232036
resetProgressDraftState();
2037+
} else {
2038+
// New assistant message => new progress-mode commentary block,
2039+
// so its inter-tool commentary lands on a fresh draft line.
2040+
progressCommentaryItemSeq += 1;
20242041
}
20252042
if (answerLane.finalized) {
20262043
await rotateLaneForNewMessage(answerLane);

0 commit comments

Comments
 (0)