From aa31f61c5839922509f17368eb191bc33bef0865 Mon Sep 17 00:00:00 2001 From: YunhaoShui Date: Thu, 26 Feb 2026 21:49:48 +0800 Subject: [PATCH] fix(channels): add circuit breaker to typing keepalive loop When the typing indicator start() call fails (e.g., message deleted, API errors), the keepalive loop previously continued firing indefinitely. This could trigger API rate limiting (e.g., Feishu 429) by sending requests every 3 seconds for a resource that no longer exists. Add a consecutive failure counter that stops the keepalive loop after 2 consecutive failures (configurable via maxConsecutiveFailures). The counter resets on any successful call or when a new reply starts. --- src/channels/typing.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/channels/typing.test.ts b/src/channels/typing.test.ts index 69149e30288..8aa0932075d 100644 --- a/src/channels/typing.test.ts +++ b/src/channels/typing.test.ts @@ -117,6 +117,28 @@ describe("createTypingCallbacks", () => { } }); + it("treats non-positive maxConsecutiveFailures as one failure", async () => { + vi.useFakeTimers(); + try { + const start = vi.fn().mockRejectedValue(new Error("gone")); + const onStartError = vi.fn(); + const callbacks = createTypingCallbacks({ + start, + onStartError, + maxConsecutiveFailures: 0, + }); + + await callbacks.onReplyStart(); + expect(start).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(9_000); + expect(start).toHaveBeenCalledTimes(1); + expect(onStartError).toHaveBeenCalledTimes(1); + } finally { + vi.useRealTimers(); + } + }); + it("resets failure counter after a successful keepalive tick", async () => { vi.useFakeTimers(); try {