From bb14a340787983e0a2151c1eb1215ee6f4c1beac Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sat, 21 Feb 2026 17:56:39 +0530 Subject: [PATCH] fix(telegram): restore 30-char preview debounce --- .../reply/agent-runner-execution.ts | 8 +++---- .../reply/agent-runner.runreplyagent.test.ts | 24 +++++++++++++++++++ src/auto-reply/tokens.ts | 6 +++++ src/telegram/bot-message-dispatch.test.ts | 6 ++--- src/telegram/bot-message-dispatch.ts | 3 +-- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 777d3adb864..eaabfe2f2d3 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -138,10 +138,7 @@ export async function runAgentTurnWithFallback(params: { } text = stripped.text; } - if ( - isSilentReplyText(text, SILENT_REPLY_TOKEN) || - isSilentReplyPrefixText(text, SILENT_REPLY_TOKEN) - ) { + if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) { return { skip: true }; } if (!text) { @@ -160,6 +157,9 @@ export async function runAgentTurnWithFallback(params: { return { text: sanitized, skip: false }; }; const handlePartialForTyping = async (payload: ReplyPayload): Promise => { + if (isSilentReplyPrefixText(payload.text, SILENT_REPLY_TOKEN)) { + return undefined; + } const { text, skip } = normalizeStreamingText(payload); if (skip || !text) { return undefined; diff --git a/src/auto-reply/reply/agent-runner.runreplyagent.test.ts b/src/auto-reply/reply/agent-runner.runreplyagent.test.ts index bbf3409a2b2..f7fd979c1fe 100644 --- a/src/auto-reply/reply/agent-runner.runreplyagent.test.ts +++ b/src/auto-reply/reply/agent-runner.runreplyagent.test.ts @@ -403,6 +403,30 @@ describe("runReplyAgent typing (heartbeat)", () => { expect(typing.startTypingLoop).not.toHaveBeenCalled(); }); + it("does not suppress partial streaming for normal 'No' prefixes", async () => { + const onPartialReply = vi.fn(); + state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + await params.onPartialReply?.({ text: "No" }); + await params.onPartialReply?.({ text: "No, that is valid" }); + return { payloads: [{ text: "No, that is valid" }], meta: {} }; + }); + + const { run, typing } = createMinimalRun({ + opts: { isHeartbeat: false, onPartialReply }, + typingMode: "message", + }); + await run(); + + expect(onPartialReply).toHaveBeenCalledTimes(2); + expect(onPartialReply).toHaveBeenNthCalledWith(1, { text: "No", mediaUrls: undefined }); + expect(onPartialReply).toHaveBeenNthCalledWith(2, { + text: "No, that is valid", + mediaUrls: undefined, + }); + expect(typing.startTypingOnText).toHaveBeenCalled(); + expect(typing.startTypingLoop).not.toHaveBeenCalled(); + }); + it("does not start typing on assistant message start without prior text in message mode", async () => { state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onAssistantMessageStart?.(); diff --git a/src/auto-reply/tokens.ts b/src/auto-reply/tokens.ts index 880dd202b44..c0bce2a2da3 100644 --- a/src/auto-reply/tokens.ts +++ b/src/auto-reply/tokens.ts @@ -30,5 +30,11 @@ export function isSilentReplyPrefixText( if (!normalized) { return false; } + if (!normalized.includes("_")) { + return false; + } + if (/[^A-Z_]/.test(normalized)) { + return false; + } return token.toUpperCase().startsWith(normalized); } diff --git a/src/telegram/bot-message-dispatch.test.ts b/src/telegram/bot-message-dispatch.test.ts index 3a88b46026b..ede7a128856 100644 --- a/src/telegram/bot-message-dispatch.test.ts +++ b/src/telegram/bot-message-dispatch.test.ts @@ -191,7 +191,7 @@ describe("dispatchTelegramMessage draft streaming", () => { expect.objectContaining({ chatId: 123, thread: { id: 777, scope: "dm" }, - minInitialChars: 1, + minInitialChars: 30, }), ); expect(draftStream.update).toHaveBeenCalledWith("Hello"); @@ -212,7 +212,7 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(draftStream.clear).toHaveBeenCalledTimes(1); }); - it("uses immediate preview updates for legacy block stream mode", async () => { + it("uses 30-char preview debounce for legacy block stream mode", async () => { const draftStream = createDraftStream(); createTelegramDraftStream.mockReturnValue(draftStream); dispatchReplyWithBufferedBlockDispatcher.mockImplementation( @@ -228,7 +228,7 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(createTelegramDraftStream).toHaveBeenCalledWith( expect.objectContaining({ - minInitialChars: 1, + minInitialChars: 30, }), ); }); diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index 9929d3a8ab6..71e53528051 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -147,8 +147,7 @@ export const dispatchTelegramMessage = async ({ const canStreamReasoningDraft = canStreamAnswerDraft || streamReasoningDraft; const draftReplyToMessageId = replyToMode !== "off" && typeof msg.message_id === "number" ? msg.message_id : undefined; - const draftMinInitialChars = - previewStreamingEnabled || streamReasoningDraft ? 1 : DRAFT_MIN_INITIAL_CHARS; + const draftMinInitialChars = DRAFT_MIN_INITIAL_CHARS; const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId); type LaneName = "answer" | "reasoning"; type DraftLaneState = {