From fab8d29d211eec2485209d00b57bffe860bc6b45 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 19:19:26 -0400 Subject: [PATCH] fix(feishu): clamp sequential queue timeouts --- .../feishu/src/sequential-queue.test.ts | 20 +++++++++++++++++++ extensions/feishu/src/sequential-queue.ts | 7 +++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/extensions/feishu/src/sequential-queue.test.ts b/extensions/feishu/src/sequential-queue.test.ts index 1df0d897c16..02f3593f6f4 100644 --- a/extensions/feishu/src/sequential-queue.test.ts +++ b/extensions/feishu/src/sequential-queue.test.ts @@ -1,3 +1,4 @@ +import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createSequentialQueue } from "./sequential-queue.js"; @@ -132,6 +133,25 @@ describe("createSequentialQueue", () => { await stuck; }); + it("clamps oversized task timeouts before scheduling", async () => { + vi.useFakeTimers(); + const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); + const enqueue = createSequentialQueue({ + taskTimeoutMs: Number.MAX_SAFE_INTEGER, + }); + const gate = createDeferred(); + + const first = enqueue("feishu:default:chat-large-timeout", async () => { + await gate.promise; + }); + + await Promise.resolve(); + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS); + + gate.resolve(); + await first; + }); + it("disables the timeout cap when taskTimeoutMs is 0 (legacy behavior)", async () => { vi.useFakeTimers(); const timeouts: Array<{ key: string; timeoutMs: number }> = []; diff --git a/extensions/feishu/src/sequential-queue.ts b/extensions/feishu/src/sequential-queue.ts index 04a7a7c19aa..0affec66f58 100644 --- a/extensions/feishu/src/sequential-queue.ts +++ b/extensions/feishu/src/sequential-queue.ts @@ -1,3 +1,5 @@ +import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime"; + /** * Per-key serial task queue for Feishu inbound message handling. * @@ -65,16 +67,17 @@ async function boundedRun( if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { return task(); } + const resolvedTimeoutMs = resolveTimerTimeoutMs(timeoutMs, DEFAULT_TASK_TIMEOUT_MS); let timeoutHandle: ReturnType | undefined; const timeoutPromise = new Promise((resolve) => { timeoutHandle = setTimeout(() => { try { - onTaskTimeout?.(key, timeoutMs); + onTaskTimeout?.(key, resolvedTimeoutMs); } catch { // Swallow logging errors so they cannot poison the queue chain. } resolve(); - }, timeoutMs); + }, resolvedTimeoutMs); }); try { await Promise.race([task(), timeoutPromise]);