diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f777d594ab..76fc7a145f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ Docs: https://docs.openclaw.ai - Plugins/runtime: honor explicit capability allowlists during fallback speech, media-understanding, and image-generation provider loading so bundled capability plugins do not bypass restrictive `plugins.allow` config. (#52262) Thanks @PerfectPan. - Hooks/tool policy: block tool calls when a `before_tool_call` hook crashes so hook failures fail closed instead of silently allowing execution. (#59822) Thanks @pgondhi987. - Matrix/media: surface a dedicated `[matrix attachment too large]` marker for oversized inbound media instead of the generic unavailable marker, and classify size-limit failures with a typed Matrix error. (#60289) Thanks @efe-arv. +- WhatsApp/watchdog: reset watchdog timeout after reconnect so quiet channels no longer enter a tight reconnect loop from stale message timestamps carried across connection runs. (#60007) Thanks @MonkeyLeeT. ## 2026.4.2 diff --git a/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts b/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts index cbd9d07e70c..cdbcd3080f1 100644 --- a/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts +++ b/extensions/whatsapp/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts @@ -188,7 +188,7 @@ describe("web auto-reply connection", () => { } }); - it("keeps watchdog message age across reconnects", async () => { + it("gives a reconnected listener a fresh watchdog window", async () => { vi.useFakeTimers(); try { const { scripted, controller, run } = await startWatchdogScenario({ @@ -203,7 +203,11 @@ describe("web auto-reply connection", () => { { timeout: 250, interval: 2 }, ); - await vi.advanceTimersByTimeAsync(200); + await vi.advanceTimersByTimeAsync(20); + await Promise.resolve(); + expect(scripted.getListenerCount()).toBe(2); + + await vi.advanceTimersByTimeAsync(20); await Promise.resolve(); await vi.waitFor( () => { diff --git a/extensions/whatsapp/src/auto-reply/monitor.ts b/extensions/whatsapp/src/auto-reply/monitor.ts index 9f371ab23eb..befc6ea7681 100644 --- a/extensions/whatsapp/src/auto-reply/monitor.ts +++ b/extensions/whatsapp/src/auto-reply/monitor.ts @@ -50,13 +50,13 @@ type ActiveConnectionRun = { backgroundTasks: Set>; }; -function createActiveConnectionRun(lastInboundAt: number | null): ActiveConnectionRun { +function createActiveConnectionRun(): ActiveConnectionRun { return { connectionId: newConnectionId(), startedAt: Date.now(), heartbeat: null, watchdogTimer: null, - lastInboundAt, + lastInboundAt: null, handledMessages: 0, unregisterUnhandled: null, backgroundTasks: new Set>(), @@ -171,7 +171,7 @@ export async function monitorWebChannel( break; } - const active = createActiveConnectionRun(status.lastInboundAt ?? status.lastMessageAt ?? null); + const active = createActiveConnectionRun(); // Watchdog to detect stuck message processing (e.g., event emitter died). // Tuning overrides are test-oriented; production defaults remain unchanged. @@ -306,10 +306,9 @@ export async function monitorWebChannel( }, heartbeatSeconds * 1000); active.watchdogTimer = setInterval(() => { - if (!active.lastInboundAt) { - return; - } - const timeSinceLastMessage = Date.now() - active.lastInboundAt; + // A reconnect should get a fresh watchdog window even before the next inbound arrives. + const watchdogBaselineAt = active.lastInboundAt ?? active.startedAt; + const timeSinceLastMessage = Date.now() - watchdogBaselineAt; if (timeSinceLastMessage <= MESSAGE_TIMEOUT_MS) { return; } @@ -319,7 +318,7 @@ export async function monitorWebChannel( { connectionId: active.connectionId, minutesSinceLastMessage, - lastInboundAt: new Date(active.lastInboundAt), + lastInboundAt: active.lastInboundAt ? new Date(active.lastInboundAt) : null, messagesHandled: active.handledMessages, }, "Message timeout detected - forcing reconnect",