fix: skip whatsapp heartbeat when provider inactive
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
### Fixes
|
### Fixes
|
||||||
- Heartbeat replies now strip repeated `HEARTBEAT_OK` tails to avoid accidental “OK OK” spam.
|
- Heartbeat replies now strip repeated `HEARTBEAT_OK` tails to avoid accidental “OK OK” spam.
|
||||||
- Heartbeat delivery now uses the last non-empty payload, preventing tool preambles from swallowing the final reply.
|
- Heartbeat delivery now uses the last non-empty payload, preventing tool preambles from swallowing the final reply.
|
||||||
|
- Heartbeats now skip WhatsApp delivery when the web provider is inactive or unlinked (instead of logging “no active gateway listener”).
|
||||||
- Heartbeat failure logs now include the error reason instead of `[object Object]`.
|
- Heartbeat failure logs now include the error reason instead of `[object Object]`.
|
||||||
- Duration strings now accept `h` (hours) where durations are parsed (e.g., heartbeat intervals).
|
- Duration strings now accept `h` (hours) where durations are parsed (e.g., heartbeat intervals).
|
||||||
- WhatsApp inbound now normalizes more wrapper types so quoted reply bodies are extracted reliably.
|
- WhatsApp inbound now normalizes more wrapper types so quoted reply bodies are extracted reliably.
|
||||||
|
|||||||
@@ -160,7 +160,13 @@ describe("runHeartbeatOnce", () => {
|
|||||||
|
|
||||||
await runHeartbeatOnce({
|
await runHeartbeatOnce({
|
||||||
cfg,
|
cfg,
|
||||||
deps: { sendWhatsApp, getQueueSize: () => 0, nowMs: () => 0 },
|
deps: {
|
||||||
|
sendWhatsApp,
|
||||||
|
getQueueSize: () => 0,
|
||||||
|
nowMs: () => 0,
|
||||||
|
webAuthExists: async () => true,
|
||||||
|
hasActiveWebListener: () => true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||||
@@ -174,4 +180,59 @@ describe("runHeartbeatOnce", () => {
|
|||||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips WhatsApp delivery when not linked or running", async () => {
|
||||||
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-hb-"));
|
||||||
|
const storePath = path.join(tmpDir, "sessions.json");
|
||||||
|
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||||
|
try {
|
||||||
|
await fs.writeFile(
|
||||||
|
storePath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
main: {
|
||||||
|
sessionId: "sid",
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
lastChannel: "whatsapp",
|
||||||
|
lastTo: "+1555",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const cfg: ClawdisConfig = {
|
||||||
|
agent: {
|
||||||
|
heartbeat: { every: "5m", target: "whatsapp", to: "+1555" },
|
||||||
|
},
|
||||||
|
routing: { allowFrom: ["*"] },
|
||||||
|
session: { store: storePath },
|
||||||
|
};
|
||||||
|
|
||||||
|
replySpy.mockResolvedValue({ text: "Heartbeat alert" });
|
||||||
|
const sendWhatsApp = vi.fn().mockResolvedValue({
|
||||||
|
messageId: "m1",
|
||||||
|
toJid: "jid",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await runHeartbeatOnce({
|
||||||
|
cfg,
|
||||||
|
deps: {
|
||||||
|
sendWhatsApp,
|
||||||
|
getQueueSize: () => 0,
|
||||||
|
nowMs: () => 0,
|
||||||
|
webAuthExists: async () => false,
|
||||||
|
hasActiveWebListener: () => false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe("skipped");
|
||||||
|
expect(res).toMatchObject({ reason: "whatsapp-not-linked" });
|
||||||
|
expect(sendWhatsApp).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
replySpy.mockRestore();
|
||||||
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ import { sendMessageDiscord } from "../discord/send.js";
|
|||||||
import { formatErrorMessage } from "../infra/errors.js";
|
import { formatErrorMessage } from "../infra/errors.js";
|
||||||
import { createSubsystemLogger } from "../logging.js";
|
import { createSubsystemLogger } from "../logging.js";
|
||||||
import { getQueueSize } from "../process/command-queue.js";
|
import { getQueueSize } from "../process/command-queue.js";
|
||||||
|
import { webAuthExists } from "../providers/web/index.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
import { sendMessageTelegram } from "../telegram/send.js";
|
import { sendMessageTelegram } from "../telegram/send.js";
|
||||||
import { normalizeE164 } from "../utils.js";
|
import { normalizeE164 } from "../utils.js";
|
||||||
|
import { getActiveWebListener } from "../web/active-listener.js";
|
||||||
import { sendMessageWhatsApp } from "../web/outbound.js";
|
import { sendMessageWhatsApp } from "../web/outbound.js";
|
||||||
import { emitHeartbeatEvent } from "./heartbeat-events.js";
|
import { emitHeartbeatEvent } from "./heartbeat-events.js";
|
||||||
import {
|
import {
|
||||||
@@ -49,6 +51,8 @@ type HeartbeatDeps = {
|
|||||||
sendDiscord?: typeof sendMessageDiscord;
|
sendDiscord?: typeof sendMessageDiscord;
|
||||||
getQueueSize?: (lane?: string) => number;
|
getQueueSize?: (lane?: string) => number;
|
||||||
nowMs?: () => number;
|
nowMs?: () => number;
|
||||||
|
webAuthExists?: () => Promise<boolean>;
|
||||||
|
hasActiveWebListener?: () => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const log = createSubsystemLogger("gateway/heartbeat");
|
const log = createSubsystemLogger("gateway/heartbeat");
|
||||||
@@ -143,6 +147,26 @@ function resolveHeartbeatSender(params: {
|
|||||||
return candidates[0] ?? "heartbeat";
|
return candidates[0] ?? "heartbeat";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveWhatsAppReadiness(
|
||||||
|
cfg: ClawdisConfig,
|
||||||
|
deps?: HeartbeatDeps,
|
||||||
|
): Promise<{ ok: boolean; reason: string }> {
|
||||||
|
if (cfg.web?.enabled === false) {
|
||||||
|
return { ok: false, reason: "whatsapp-disabled" };
|
||||||
|
}
|
||||||
|
const authExists = await (deps?.webAuthExists ?? webAuthExists)();
|
||||||
|
if (!authExists) {
|
||||||
|
return { ok: false, reason: "whatsapp-not-linked" };
|
||||||
|
}
|
||||||
|
const listenerActive = deps?.hasActiveWebListener
|
||||||
|
? deps.hasActiveWebListener()
|
||||||
|
: Boolean(getActiveWebListener());
|
||||||
|
if (!listenerActive) {
|
||||||
|
return { ok: false, reason: "whatsapp-not-running" };
|
||||||
|
}
|
||||||
|
return { ok: true, reason: "ok" };
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveHeartbeatDeliveryTarget(params: {
|
export function resolveHeartbeatDeliveryTarget(params: {
|
||||||
cfg: ClawdisConfig;
|
cfg: ClawdisConfig;
|
||||||
entry?: SessionEntry;
|
entry?: SessionEntry;
|
||||||
@@ -392,6 +416,23 @@ export async function runHeartbeatOnce(opts: {
|
|||||||
return { status: "ran", durationMs: Date.now() - startedAt };
|
return { status: "ran", durationMs: Date.now() - startedAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (delivery.channel === "whatsapp") {
|
||||||
|
const readiness = await resolveWhatsAppReadiness(cfg, opts.deps);
|
||||||
|
if (!readiness.ok) {
|
||||||
|
emitHeartbeatEvent({
|
||||||
|
status: "skipped",
|
||||||
|
reason: readiness.reason,
|
||||||
|
preview: normalized.text?.slice(0, 200),
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
hasMedia: mediaUrls.length > 0,
|
||||||
|
});
|
||||||
|
log.info("heartbeat: whatsapp not ready", {
|
||||||
|
reason: readiness.reason,
|
||||||
|
});
|
||||||
|
return { status: "skipped", reason: readiness.reason };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deps = {
|
const deps = {
|
||||||
sendWhatsApp: opts.deps?.sendWhatsApp ?? sendMessageWhatsApp,
|
sendWhatsApp: opts.deps?.sendWhatsApp ?? sendMessageWhatsApp,
|
||||||
sendTelegram: opts.deps?.sendTelegram ?? sendMessageTelegram,
|
sendTelegram: opts.deps?.sendTelegram ?? sendMessageTelegram,
|
||||||
|
|||||||
Reference in New Issue
Block a user