diff --git a/extensions/codex/src/app-server/event-projector.test.ts b/extensions/codex/src/app-server/event-projector.test.ts index de01d314803..1462c5fbc7b 100644 --- a/extensions/codex/src/app-server/event-projector.test.ts +++ b/extensions/codex/src/app-server/event-projector.test.ts @@ -802,7 +802,12 @@ describe("CodexAppServerEventProjector", () => { expect(onAgentEvent).toHaveBeenCalledWith( expect.objectContaining({ stream: "item", - data: expect.objectContaining({ phase: "start", kind: "tool", name: "message" }), + data: expect.objectContaining({ + phase: "start", + kind: "tool", + name: "message", + suppressChannelProgress: true, + }), }), ); expect(onAgentEvent).not.toHaveBeenCalledWith( diff --git a/extensions/codex/src/app-server/event-projector.ts b/extensions/codex/src/app-server/event-projector.ts index 36bc732d808..e21efe51604 100644 --- a/extensions/codex/src/app-server/event-projector.ts +++ b/extensions/codex/src/app-server/event-projector.ts @@ -663,7 +663,7 @@ export class CodexAppServerEventProjector { return; } const meta = itemMeta(item, this.toolProgressDetailMode()); - const suppressChannelProgress = shouldSynthesizeToolProgressForItem(item); + const suppressChannelProgress = shouldSuppressChannelProgressForItem(item); this.emitAgentEvent({ stream: "item", data: { @@ -1163,16 +1163,21 @@ function shouldSynthesizeToolProgressForItem(item: CodexThreadItem): boolean { case "webSearch": case "mcpToolCall": return true; - // Dynamic OpenClaw tool requests are emitted at the item/tool/call request - // boundary in run-attempt.ts. Re-emitting them from item notifications can - // duplicate start/result events when the app-server sends both signals. - case "dynamicToolCall": - return false; default: return false; } } +function shouldSuppressChannelProgressForItem(item: CodexThreadItem): boolean { + if (shouldSynthesizeToolProgressForItem(item)) { + return true; + } + // Dynamic OpenClaw tool requests are emitted at the item/tool/call request + // boundary in run-attempt.ts. Re-emitting item notifications to channels can + // duplicate start/result progress when the app-server sends both signals. + return item.type === "dynamicToolCall"; +} + function itemToolArgs(item: CodexThreadItem): Record | undefined { if (item.type === "commandExecution") { return sanitizeCodexAgentEventRecord({ diff --git a/extensions/telegram/src/bot-message-dispatch.test.ts b/extensions/telegram/src/bot-message-dispatch.test.ts index dcc317cbaa9..b634ec9cb8f 100644 --- a/extensions/telegram/src/bot-message-dispatch.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.test.ts @@ -2,7 +2,10 @@ import type { Bot } from "grammy"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { resolveAutoTopicLabelConfig as resolveAutoTopicLabelConfigRuntime } from "./auto-topic-label-config.js"; import type { TelegramBotDeps } from "./bot-deps.js"; -import { createTestDraftStream } from "./draft-stream.test-helpers.js"; +import { + createSequencedTestDraftStream, + createTestDraftStream, +} from "./draft-stream.test-helpers.js"; type DispatchReplyWithBufferedBlockDispatcherArgs = Parameters< TelegramBotDeps["dispatchReplyWithBufferedBlockDispatcher"] @@ -259,6 +262,8 @@ describe("dispatchTelegramMessage draft streaming", () => { }); const createDraftStream = (messageId?: number) => createTestDraftStream({ messageId }); + const createSequencedDraftStream = (startMessageId = 1001) => + createSequencedTestDraftStream(startMessageId); function setupDraftStreams(params?: { answerMessageId?: number; reasoningMessageId?: number }) { const answerDraftStream = createDraftStream(params?.answerMessageId); @@ -1077,13 +1082,13 @@ describe("dispatchTelegramMessage draft streaming", () => { ); expect(draftStream.forceNewMessage).not.toHaveBeenCalled(); expect(draftStream.materialize).not.toHaveBeenCalled(); - expect(editMessageTelegram).toHaveBeenCalledWith( - 123, - 2001, - "Final after tool", - expect.any(Object), + expect(draftStream.clear).toHaveBeenCalledTimes(1); + expect(deliverReplies).toHaveBeenCalledWith( + expect.objectContaining({ + replies: [expect.objectContaining({ text: "Final after tool" })], + }), ); - expect(draftStream.clear).not.toHaveBeenCalled(); + expect(editMessageTelegram).not.toHaveBeenCalled(); }); it("falls back to normal send for error payloads and clears the pending stream", async () => {