From 15826de945ef476c856dfbc867ae0250ee19f4ca Mon Sep 17 00:00:00 2001 From: Han Yang Date: Tue, 25 Aug 2026 18:25:35 +0800 Subject: [PATCH] fix(telegram): honor human delay for streamed replies (#69022) Punchcard-Session: calm-brook-harbor-jm --- .../telegram/src/bot-message-dispatch-turn.ts | 2 ++ .../src/bot-message-dispatch.agent.runtime.ts | 1 + ...t-message-dispatch.delivery-basics.test.ts | 24 +++++++++++++++++++ .../src/bot-message-dispatch.test-harness.ts | 5 ++++ 4 files changed, 32 insertions(+) diff --git a/extensions/telegram/src/bot-message-dispatch-turn.ts b/extensions/telegram/src/bot-message-dispatch-turn.ts index 2cf125234f84..9df76b34792e 100644 --- a/extensions/telegram/src/bot-message-dispatch-turn.ts +++ b/extensions/telegram/src/bot-message-dispatch-turn.ts @@ -41,6 +41,7 @@ import { handleReplySkip, resetReasoningStepState, } from "./bot-message-dispatch-reply.js"; +import { resolveHumanDelayConfig } from "./bot-message-dispatch.agent.runtime.js"; import type { TelegramDispatchTurn as Turn } from "./bot-message-dispatch.types.js"; import { TELEGRAM_CHAT_ACTION_INTERVAL_MS } from "./chat-action-timing.js"; import { telegramInboundEventDelivery } from "./inbound-event-delivery.js"; @@ -134,6 +135,7 @@ export async function runTelegramDispatchTurn(turn: Turn) { }, dispatcherOptions: { ...replyPipeline, + humanDelay: resolveHumanDelayConfig(turn.cfg, context.route.agentId), beforeDeliver: async (payload) => payload, onBeforeDeliverCancelled: (payload, info) => handleBeforeDeliverCancelled(turn, payload, info), diff --git a/extensions/telegram/src/bot-message-dispatch.agent.runtime.ts b/extensions/telegram/src/bot-message-dispatch.agent.runtime.ts index e4e5d42cbc3e..bf7d40c905e8 100644 --- a/extensions/telegram/src/bot-message-dispatch.agent.runtime.ts +++ b/extensions/telegram/src/bot-message-dispatch.agent.runtime.ts @@ -5,4 +5,5 @@ export { modelSupportsVision, resolveAgentDir, resolveDefaultModelForAgent, + resolveHumanDelayConfig, } from "openclaw/plugin-sdk/agent-runtime"; diff --git a/extensions/telegram/src/bot-message-dispatch.delivery-basics.test.ts b/extensions/telegram/src/bot-message-dispatch.delivery-basics.test.ts index 09fab5c3879e..a19dfa786893 100644 --- a/extensions/telegram/src/bot-message-dispatch.delivery-basics.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.delivery-basics.test.ts @@ -16,6 +16,7 @@ import { mockDefaultSessionEntry, readLatestAssistantTextByIdentity, recordOutboundMessageForPromptContext, + resolveHumanDelayConfig, setupDraftStreams, telegramDepsForTest, } from "./bot-message-dispatch.test-harness.js"; @@ -25,6 +26,29 @@ import type { } from "./bot-message-dispatch.test-harness.js"; describeTelegramDispatch("dispatchTelegramMessage delivery-basics", () => { + it("forwards route-scoped humanDelay to the block dispatcher", async () => { + const humanDelay = { mode: "custom" as const, minMs: 800, maxMs: 2_500 }; + const cfg = { agents: { defaults: { humanDelay } } } as Parameters< + typeof dispatchWithContext + >[0]["cfg"]; + resolveHumanDelayConfig.mockReturnValue(humanDelay); + + await dispatchWithContext({ + context: createContext({ + route: { + agentId: "ops", + accountId: "default", + } as unknown as TelegramMessageContext["route"], + }), + cfg, + streamMode: "off", + }); + + expect(resolveHumanDelayConfig).toHaveBeenCalledWith(cfg, "ops"); + const dispatch = expectRecordFields(mockCallArg(dispatchReplyWithBufferedBlockDispatcher), {}); + expectRecordFields(dispatch.dispatcherOptions, { humanDelay }); + }); + it("forwards cfg to direct reply delivery", async () => { dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => { await dispatcherOptions.deliver({ text: "Hello" }, { kind: "final" }); diff --git a/extensions/telegram/src/bot-message-dispatch.test-harness.ts b/extensions/telegram/src/bot-message-dispatch.test-harness.ts index 6dadc81db7e1..3715275bd445 100644 --- a/extensions/telegram/src/bot-message-dispatch.test-harness.ts +++ b/extensions/telegram/src/bot-message-dispatch.test-harness.ts @@ -110,6 +110,7 @@ const resolveAgentWorkspaceDirHoisted = vi.hoisted(() => vi.fn(() => "/tmp/works const resolveDefaultModelForAgentHoisted = vi.hoisted(() => vi.fn(() => ({ provider: "openai", model: "gpt-test" })), ); +const resolveHumanDelayConfigHoisted = vi.hoisted(() => vi.fn()); const getAgentScopedMediaLocalRootsHoisted = vi.hoisted(() => vi.fn((_cfg: unknown, agentId: string) => [`/tmp/.openclaw/workspace-${agentId}`]), ); @@ -154,6 +155,7 @@ const findModelInCatalog = findModelInCatalogHoisted; const modelSupportsVision = modelSupportsVisionHoisted; const resolveAgentDir = resolveAgentDirHoisted; const resolveDefaultModelForAgent = resolveDefaultModelForAgentHoisted; +export const resolveHumanDelayConfig = resolveHumanDelayConfigHoisted; const getAgentScopedMediaLocalRoots = getAgentScopedMediaLocalRootsHoisted; const resolveChunkMode = resolveChunkModeHoisted; export const resolveMarkdownTableMode = resolveMarkdownTableModeHoisted; @@ -301,6 +303,7 @@ vi.mock("./bot-message-dispatch.agent.runtime.js", () => ({ resolveAgentDir: resolveAgentDirHoisted, resolveAgentWorkspaceDir: resolveAgentWorkspaceDirHoisted, resolveDefaultModelForAgent: resolveDefaultModelForAgentHoisted, + resolveHumanDelayConfig: resolveHumanDelayConfigHoisted, })); vi.mock("./sticker-cache.js", () => ({ @@ -411,6 +414,7 @@ function resetTelegramDispatchTestState() { modelSupportsVision.mockReset(); resolveAgentDir.mockReset(); resolveDefaultModelForAgent.mockReset(); + resolveHumanDelayConfig.mockReset(); loadConfig.mockReturnValue({}); dispatchReplyWithBufferedBlockDispatcher.mockResolvedValue({ queuedFinal: false, @@ -473,6 +477,7 @@ function resetTelegramDispatchTestState() { provider: "openai", model: "gpt-test", }); + resolveHumanDelayConfig.mockReturnValue(undefined); getGlobalHookRunner.mockReturnValue(null); }