fix: skip whatsapp heartbeat when provider inactive
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
### Fixes
|
||||
- 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.
|
||||
- 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]`.
|
||||
- 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.
|
||||
|
||||
@@ -160,7 +160,13 @@ describe("runHeartbeatOnce", () => {
|
||||
|
||||
await runHeartbeatOnce({
|
||||
cfg,
|
||||
deps: { sendWhatsApp, getQueueSize: () => 0, nowMs: () => 0 },
|
||||
deps: {
|
||||
sendWhatsApp,
|
||||
getQueueSize: () => 0,
|
||||
nowMs: () => 0,
|
||||
webAuthExists: async () => true,
|
||||
hasActiveWebListener: () => true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||
@@ -174,4 +180,59 @@ describe("runHeartbeatOnce", () => {
|
||||
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 { createSubsystemLogger } from "../logging.js";
|
||||
import { getQueueSize } from "../process/command-queue.js";
|
||||
import { webAuthExists } from "../providers/web/index.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { sendMessageTelegram } from "../telegram/send.js";
|
||||
import { normalizeE164 } from "../utils.js";
|
||||
import { getActiveWebListener } from "../web/active-listener.js";
|
||||
import { sendMessageWhatsApp } from "../web/outbound.js";
|
||||
import { emitHeartbeatEvent } from "./heartbeat-events.js";
|
||||
import {
|
||||
@@ -49,6 +51,8 @@ type HeartbeatDeps = {
|
||||
sendDiscord?: typeof sendMessageDiscord;
|
||||
getQueueSize?: (lane?: string) => number;
|
||||
nowMs?: () => number;
|
||||
webAuthExists?: () => Promise<boolean>;
|
||||
hasActiveWebListener?: () => boolean;
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("gateway/heartbeat");
|
||||
@@ -143,6 +147,26 @@ function resolveHeartbeatSender(params: {
|
||||
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: {
|
||||
cfg: ClawdisConfig;
|
||||
entry?: SessionEntry;
|
||||
@@ -392,6 +416,23 @@ export async function runHeartbeatOnce(opts: {
|
||||
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 = {
|
||||
sendWhatsApp: opts.deps?.sendWhatsApp ?? sendMessageWhatsApp,
|
||||
sendTelegram: opts.deps?.sendTelegram ?? sendMessageTelegram,
|
||||
|
||||
Reference in New Issue
Block a user