fix: heartbeat falls back to last session contact

This commit is contained in:
Peter Steinberger
2025-11-26 17:08:43 +01:00
parent 3998933b30
commit 0d5e5f8dee
2 changed files with 75 additions and 9 deletions

View File

@@ -105,6 +105,37 @@ describe("runWebHeartbeatOnce", () => {
});
expect(sender).toHaveBeenCalledWith("+1555", "ALERT", { verbose: false });
});
it("falls back to most recent session when no to is provided", async () => {
const sender: typeof sendMessageWeb = vi
.fn()
.mockResolvedValue({ messageId: "m1", toJid: "jid" });
const resolver = vi.fn(async () => ({ text: "ALERT" }));
// Seed session store
const now = Date.now();
const store = {
"+1222": { sessionId: "s1", updatedAt: now - 1000 },
"+1333": { sessionId: "s2", updatedAt: now },
};
const storePath = resolveStorePath();
await fs.mkdir(resolveStorePath().replace("sessions.json", ""), {
recursive: true,
});
await fs.writeFile(storePath, JSON.stringify(store));
setLoadConfigMock({
inbound: {
allowFrom: ["+1999"],
reply: { mode: "command", session: {} },
},
});
await runWebHeartbeatOnce({
to: "+1999",
verbose: false,
sender,
replyResolver: resolver,
});
expect(sender).toHaveBeenCalledWith("+1333", "ALERT", { verbose: false });
});
});
describe("web auto-reply", () => {

View File

@@ -2,10 +2,12 @@ import { getReplyFromConfig } from "../auto-reply/reply.js";
import type { ReplyPayload } from "../auto-reply/types.js";
import { waitForever } from "../cli/wait.js";
import { loadConfig } from "../config/config.js";
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
import { danger, isVerbose, logVerbose, success } from "../globals.js";
import { logInfo } from "../logger.js";
import { getChildLogger } from "../logging.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { normalizeE164 } from "../utils.js";
import { monitorWebInbox } from "./inbound.js";
import { loadWebMedia } from "./media.js";
import { sendMessageWeb } from "./outbound.js";
@@ -141,6 +143,23 @@ export async function runWebHeartbeatOnce(opts: {
}
}
function getFallbackRecipient(cfg: ReturnType<typeof loadConfig>) {
const sessionCfg = cfg.inbound?.reply?.session;
const storePath = resolveStorePath(sessionCfg?.store);
const store = loadSessionStore(storePath);
const candidates = Object.entries(store).filter(([key]) => key !== "global");
if (candidates.length === 0) {
return (
(Array.isArray(cfg.inbound?.allowFrom) && cfg.inbound.allowFrom[0]) ||
null
);
}
const mostRecent = candidates.sort(
(a, b) => (b[1]?.updatedAt ?? 0) - (a[1]?.updatedAt ?? 0),
)[0];
return mostRecent ? normalizeE164(mostRecent[0]) : null;
}
async function deliverWebReply(params: {
replyResult: ReplyPayload;
msg: WebInboundMsg;
@@ -437,15 +456,31 @@ export async function monitorWebProvider(
if (!replyHeartbeatMinutes) return;
const tickStart = Date.now();
if (!lastInboundMsg) {
heartbeatLogger.info(
{
connectionId,
reason: "no-recent-inbound",
durationMs: Date.now() - tickStart,
},
"reply heartbeat skipped",
);
console.log(success("heartbeat: skipped (no recent inbound)"));
const fallbackTo = getFallbackRecipient(cfg);
if (!fallbackTo) {
heartbeatLogger.info(
{
connectionId,
reason: "no-recent-inbound",
durationMs: Date.now() - tickStart,
},
"reply heartbeat skipped",
);
console.log(success("heartbeat: skipped (no recent inbound)"));
return;
}
if (isVerbose()) {
heartbeatLogger.info(
{ connectionId, to: fallbackTo, reason: "fallback-session" },
"reply heartbeat start",
);
}
await runWebHeartbeatOnce({
to: fallbackTo,
verbose,
replyResolver,
runtime,
});
return;
}