Skip to content

Commit 86b71f2

Browse files
fix(telegram): confine stream-off accumulator to streaming.mode=off
Moving the progress-set flush to the common final path made it fire on the bare absence of an answer-lane draft (!answerLane.stream), which is also true in partial/progress turns that lack a live draft (e.g. selected-quote replies) — so those turns could get a surprise durable progress message. Gate accumulation, suppressed-source callback forwarding, and emission on a single streamOffPersistEnabled = persistProgressEnabled && streamMode === 'off' so the accumulator only ever engages in genuine stream-off turns. Regression: a no-draft partial-mode turn with persistProgress emits no progress set.
1 parent 62fa8b4 commit 86b71f2

2 files changed

Lines changed: 55 additions & 4 deletions

File tree

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,6 +1551,50 @@ describe("dispatchTelegramMessage draft streaming", () => {
15511551
expect(progress.mediaUrls).toBeUndefined();
15521552
});
15531553

1554+
it("does not emit the stream-off progress set in a no-draft non-off turn", async () => {
1555+
// The accumulator is a streaming.mode === "off" mechanism. In partial/progress turns
1556+
// that merely lack a live draft (e.g. selected-quote replies), it must stay inert and
1557+
// not deliver a surprise durable progress message.
1558+
deliverInboundReplyWithMessageSendContext.mockResolvedValue({
1559+
delivery: { messageId: 9100 },
1560+
});
1561+
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
1562+
async ({ dispatcherOptions, replyOptions }) => {
1563+
await replyOptions?.onItemEvent?.({
1564+
kind: "preamble",
1565+
itemId: "c1",
1566+
progressText: "Looking into it.",
1567+
});
1568+
await replyOptions?.onToolStart?.({
1569+
name: "Read",
1570+
phase: "start",
1571+
args: { file_path: "/x.md" },
1572+
});
1573+
await dispatcherOptions.deliver({ text: "Done." }, { kind: "final" });
1574+
return { queuedFinal: true };
1575+
},
1576+
);
1577+
deliverReplies.mockResolvedValue({ delivered: true });
1578+
1579+
// partial mode with NO draft stream set up (createTelegramDraftStream returns undefined)
1580+
await dispatchWithContext({
1581+
context: createContext(),
1582+
streamMode: "partial",
1583+
telegramCfg: {
1584+
streaming: {
1585+
mode: "partial",
1586+
progress: { persistProgress: true, commentary: true, toolProgress: true },
1587+
},
1588+
},
1589+
});
1590+
1591+
const progressSets = deliverInboundReplyWithMessageSendContext.mock.calls.filter((call) => {
1592+
const text = (call[0] as { payload?: { text?: string } })?.payload?.text;
1593+
return typeof text === "string" && text.includes("💭");
1594+
});
1595+
expect(progressSets.length).toBe(0);
1596+
});
1597+
15541598
it("suppresses text-only tool payloads delivered after the final answer", async () => {
15551599
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
15561600
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,13 @@ export const dispatchTelegramMessage = async ({
909909
const progressSeed = `${route.accountId}:${chatId}:${threadSpec.id ?? ""}`;
910910
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId);
911911
const persistProgressEnabled = resolveChannelStreamingProgressPersist(telegramCfg);
912+
// The stream-off accumulator (#89890) is strictly a streaming.mode === "off" mechanism:
913+
// it buffers tool/commentary progress and emits it as a durable message before the
914+
// answer. It must NOT engage in partial/progress turns that merely lack a live draft
915+
// (e.g. selected-quote replies), or those turns would get a surprise durable progress
916+
// message. Gate accumulation, callback-forwarding, and emission on this — not on the
917+
// bare absence of a draft stream.
918+
const streamOffPersistEnabled = persistProgressEnabled && streamMode === "off";
912919
const createDraftLane = (laneName: LaneName, enabled: boolean): DraftLaneState => {
913920
const stream = enabled
914921
? (telegramDeps.createTelegramDraftStream ?? createTelegramDraftStream)({
@@ -1768,7 +1775,7 @@ export const dispatchTelegramMessage = async ({
17681775
// stream-ON path (which persists the live progress message in place) does
17691776
// not double-emit. Awaited here so the set lands before the answer (barrier).
17701777
if (
1771-
!persistProgressEnabled ||
1778+
!streamOffPersistEnabled ||
17721779
answerLane.stream ||
17731780
persistedProgressLog.length === 0
17741781
) {
@@ -2099,7 +2106,7 @@ export const dispatchTelegramMessage = async ({
20992106
suppressDefaultToolProgressMessages:
21002107
!streamDeliveryEnabled || Boolean(answerLane.stream),
21012108
allowProgressCallbacksWhenSourceDeliverySuppressed:
2102-
!isRoomEvent && (Boolean(answerLane.stream) || persistProgressEnabled),
2109+
!isRoomEvent && (Boolean(answerLane.stream) || streamOffPersistEnabled),
21032110
onToolStart: async (payload) => {
21042111
const toolName = payload.name?.trim();
21052112
const progressPromise = pushStreamToolProgress(
@@ -2116,7 +2123,7 @@ export const dispatchTelegramMessage = async ({
21162123
{ toolName, startImmediately: true },
21172124
);
21182125
if (
2119-
persistProgressEnabled &&
2126+
streamOffPersistEnabled &&
21202127
toolName &&
21212128
resolveChannelStreamingProgressToolProgressEnabled(telegramCfg)
21222129
) {
@@ -2136,7 +2143,7 @@ export const dispatchTelegramMessage = async ({
21362143
onItemEvent: async (payload) => {
21372144
if (payload.kind === "preamble") {
21382145
if (
2139-
persistProgressEnabled &&
2146+
streamOffPersistEnabled &&
21402147
resolveChannelStreamingProgressCommentaryEnabled(telegramCfg) &&
21412148
payload.progressText
21422149
) {

0 commit comments

Comments
 (0)