diff --git a/extensions/matrix/src/matrix/monitor/handler-reply-dispatcher.ts b/extensions/matrix/src/matrix/monitor/handler-reply-dispatcher.ts index c08922b51d3c..fdf4e54659e6 100644 --- a/extensions/matrix/src/matrix/monitor/handler-reply-dispatcher.ts +++ b/extensions/matrix/src/matrix/monitor/handler-reply-dispatcher.ts @@ -98,8 +98,7 @@ export function createMatrixReplyDispatcher(config: { // Re-assert typing so the user still sees the indicator while // the next block generates. - const { sendTypingMatrix } = await loadMatrixSendModule(); - await sendTypingMatrix(roomId, true, undefined, client).catch(() => {}); + await typingCallbacks.onReplyStart(); } return result; }; diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index 36bb098a9260..f826fb971b5a 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -38,6 +38,7 @@ const sendSingleTextMessageMatrixMock = vi.hoisted(() => vi.fn(async (..._args: unknown[]) => ({ messageId: "$draft1", roomId: "!room" })), ); const editMessageMatrixMock = vi.hoisted(() => vi.fn(async () => "$edited")); +const sendTypingMatrixMock = vi.hoisted(() => vi.fn(async () => {})); const prepareMatrixSingleTextMock = vi.hoisted(() => vi.fn((text: string) => { const trimmedText = text.trim(); @@ -76,7 +77,7 @@ vi.mock("../send.js", () => ({ sendMessageMatrix: sendMessageMatrixMock, sendSingleTextMessageMatrix: sendSingleTextMessageMatrixMock, sendReadReceiptMatrix: vi.fn(async () => {}), - sendTypingMatrix: vi.fn(async () => {}), + sendTypingMatrix: sendTypingMatrixMock, })); const deliverMatrixRepliesMock = vi.hoisted(() => vi.fn()); @@ -142,6 +143,7 @@ beforeEach(() => { }; }); resolveMatrixMentionsForBodyMock.mockClear(); + sendTypingMatrixMock.mockReset().mockResolvedValue(undefined); deliverMatrixRepliesMock.mockReset().mockResolvedValue(createMockMatrixDeliveryResult()); }); @@ -2978,6 +2980,7 @@ describe("matrix monitor handler draft streaming", () => { deliverMatrixRepliesMock.mockReset().mockResolvedValue(createMockMatrixDeliveryResult()); const redactEventMock = vi.fn(async () => "$redacted"); + const logVerboseMessage = vi.fn(); const { handler } = createMatrixHandlerTestHarness({ streaming: opts?.streaming ?? "quiet", @@ -2986,6 +2989,7 @@ describe("matrix monitor handler draft streaming", () => { blockStreamingEnabled: opts?.blockStreamingEnabled ?? false, replyToMode: opts?.replyToMode ?? "off", client: { redactEvent: redactEventMock }, + logVerboseMessage, createReplyDispatcherWithTyping: (params: Record | undefined) => { capturedDeliver = params?.deliver as DeliverFn | undefined; notifyCaptured(); @@ -3027,9 +3031,38 @@ describe("matrix monitor handler draft streaming", () => { }; }; - return { dispatch, redactEventMock }; + return { dispatch, redactEventMock, logVerboseMessage }; } + it("records a failed block typing restart without replaying the accepted delivery", async () => { + const acceptedDelivery = createMockMatrixDeliveryResult("$accepted", "Already delivered block"); + const { dispatch, logVerboseMessage } = createStreamingHarness({ streaming: "off" }); + deliverMatrixRepliesMock.mockResolvedValueOnce(acceptedDelivery); + sendTypingMatrixMock.mockRejectedValueOnce(new Error("typing unavailable")); + const { deliver, finish } = await dispatch(); + + await expect(deliver({ text: "Already delivered block" }, { kind: "block" })).resolves.toBe( + acceptedDelivery, + ); + + expect(deliverMatrixRepliesMock).toHaveBeenCalledOnce(); + expect(sendTypingMatrixMock).toHaveBeenCalledExactlyOnceWith( + "!room:example.org", + true, + undefined, + expect.anything(), + ); + const expectedDiagnostic = + "matrix typing action=start failed target=!room:example.org: Error: typing unavailable"; + await waitForMatrixState(() => + expect( + logVerboseMessage.mock.calls.filter(([message]) => message === expectedDiagnostic), + ).toHaveLength(1), + ); + + await finish(); + }); + it("shares first-reply state between tool and final Matrix deliveries", async () => { const { dispatch } = createStreamingHarness({ replyToMode: "first", streaming: "off" }); const { deliver, finish } = await dispatch();