@openclaw/whatsapp 2026.5.1-beta.1 → 2026.5.2-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,33 +0,0 @@
1
- export { appendCronStyleCurrentTimeLine } from "openclaw/plugin-sdk/agent-runtime";
2
- export {
3
- canonicalizeMainSessionAlias,
4
- loadSessionStore,
5
- resolveSessionKey,
6
- resolveStorePath,
7
- updateSessionStore,
8
- } from "openclaw/plugin-sdk/session-store-runtime";
9
- export { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
10
- export {
11
- emitHeartbeatEvent,
12
- resolveHeartbeatVisibility,
13
- resolveIndicatorType,
14
- } from "openclaw/plugin-sdk/heartbeat-runtime";
15
- export {
16
- hasOutboundReplyContent,
17
- resolveSendableOutboundReplyParts,
18
- } from "openclaw/plugin-sdk/reply-payload";
19
- export {
20
- DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
21
- HEARTBEAT_TOKEN,
22
- getReplyFromConfig,
23
- resolveHeartbeatPrompt,
24
- resolveHeartbeatReplyPayload,
25
- stripHeartbeatToken,
26
- } from "openclaw/plugin-sdk/reply-runtime";
27
- export { normalizeMainKey } from "openclaw/plugin-sdk/routing";
28
- export { getChildLogger } from "openclaw/plugin-sdk/runtime-env";
29
- export { redactIdentifier } from "openclaw/plugin-sdk/text-runtime";
30
- export { resolveWhatsAppHeartbeatRecipients } from "../runtime-api.js";
31
- export { sendMessageWhatsApp } from "../send.js";
32
- export { formatError } from "../session.js";
33
- export { whatsappHeartbeatLog } from "./loggers.js";
@@ -1,214 +0,0 @@
1
- import { redactIdentifier } from "openclaw/plugin-sdk/logging-core";
2
- import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
- import type { sendMessageWhatsApp } from "../send.js";
4
- import type { getReplyFromConfig } from "./heartbeat-runner.runtime.js";
5
-
6
- const HEARTBEAT_TOKEN = "HEARTBEAT_OK";
7
-
8
- const state = vi.hoisted(() => ({
9
- visibility: { showAlerts: true, showOk: true, useIndicator: false },
10
- store: {} as Record<string, { updatedAt?: number; sessionId?: string }>,
11
- snapshot: {
12
- key: "k",
13
- entry: { sessionId: "s1", updatedAt: 123 },
14
- fresh: false,
15
- resetPolicy: { mode: "none", atHour: null, idleMinutes: null },
16
- dailyResetAt: null as number | null,
17
- idleExpiresAt: null as number | null,
18
- },
19
- events: [] as unknown[],
20
- loggerInfoCalls: [] as unknown[][],
21
- loggerWarnCalls: [] as unknown[][],
22
- heartbeatInfoLogs: [] as string[],
23
- heartbeatWarnLogs: [] as string[],
24
- }));
25
-
26
- vi.mock("./heartbeat-runner.runtime.js", () => {
27
- const logger = {
28
- child: () => logger,
29
- info: (...args: unknown[]) => state.loggerInfoCalls.push(args),
30
- warn: (...args: unknown[]) => state.loggerWarnCalls.push(args),
31
- error: vi.fn(),
32
- debug: vi.fn(),
33
- };
34
- return {
35
- DEFAULT_HEARTBEAT_ACK_MAX_CHARS: 32,
36
- HEARTBEAT_TOKEN,
37
- appendCronStyleCurrentTimeLine: (body: string) =>
38
- `${body}\nCurrent time: 2026-02-15T00:00:00Z (mock)`,
39
- canonicalizeMainSessionAlias: ({ sessionKey }: { sessionKey: string }) => sessionKey,
40
- emitHeartbeatEvent: (event: unknown) => state.events.push(event),
41
- formatError: (err: unknown) => `ERR:${String(err)}`,
42
- getChildLogger: () => logger,
43
- getReplyFromConfig: vi.fn(async () => undefined),
44
- hasOutboundReplyContent: (payload: { text?: string } | undefined) =>
45
- Boolean(payload?.text?.trim()),
46
- loadConfig: () => ({ agents: { defaults: {} }, session: {} }),
47
- loadSessionStore: () => state.store,
48
- normalizeMainKey: () => null,
49
- redactIdentifier,
50
- resolveHeartbeatPrompt: (prompt?: string) => prompt || "Heartbeat",
51
- resolveHeartbeatReplyPayload: (reply: unknown) => reply,
52
- resolveHeartbeatVisibility: () => state.visibility,
53
- resolveIndicatorType: (status: string) => `indicator:${status}`,
54
- resolveSendableOutboundReplyParts: (payload: { text?: string }) => ({
55
- text: payload.text ?? "",
56
- hasMedia: false,
57
- }),
58
- resolveSessionKey: () => "k",
59
- resolveStorePath: () => "/tmp/store.json",
60
- resolveWhatsAppHeartbeatRecipients: () => [],
61
- sendMessageWhatsApp: vi.fn(async () => ({ messageId: "m1" })),
62
- stripHeartbeatToken: (text: string) => {
63
- const trimmed = text.trim();
64
- if (trimmed === HEARTBEAT_TOKEN) {
65
- return { shouldSkip: true, text: "" };
66
- }
67
- return { shouldSkip: false, text: trimmed };
68
- },
69
- updateSessionStore: async (_path: string, updater: (store: typeof state.store) => void) => {
70
- updater(state.store);
71
- },
72
- whatsappHeartbeatLog: {
73
- info: (msg: string) => state.heartbeatInfoLogs.push(msg),
74
- warn: (msg: string) => state.heartbeatWarnLogs.push(msg),
75
- },
76
- };
77
- });
78
-
79
- vi.mock("./session-snapshot.js", () => ({
80
- getSessionSnapshot: () => state.snapshot,
81
- }));
82
-
83
- vi.mock("../reconnect.js", () => ({
84
- newConnectionId: () => "run-1",
85
- }));
86
-
87
- describe("runWebHeartbeatOnce", () => {
88
- let senderMock: ReturnType<typeof vi.fn>;
89
- let sender: typeof sendMessageWhatsApp;
90
- let replyResolverMock: ReturnType<typeof vi.fn>;
91
- let replyResolver: typeof getReplyFromConfig;
92
- let runWebHeartbeatOnce: typeof import("./heartbeat-runner.js").runWebHeartbeatOnce;
93
-
94
- const buildRunArgs = (overrides: Record<string, unknown> = {}) => ({
95
- cfg: { agents: { defaults: {} }, session: {} } as never,
96
- to: "+123",
97
- sender,
98
- replyResolver,
99
- ...overrides,
100
- });
101
-
102
- beforeAll(async () => {
103
- ({ runWebHeartbeatOnce } = await import("./heartbeat-runner.js"));
104
- });
105
-
106
- beforeEach(() => {
107
- state.visibility = { showAlerts: true, showOk: true, useIndicator: false };
108
- state.store = { k: { updatedAt: 999, sessionId: "s1" } };
109
- state.snapshot = {
110
- key: "k",
111
- entry: { sessionId: "s1", updatedAt: 123 },
112
- fresh: false,
113
- resetPolicy: { mode: "none", atHour: null, idleMinutes: null },
114
- dailyResetAt: null,
115
- idleExpiresAt: null,
116
- };
117
- state.events = [];
118
- state.loggerInfoCalls = [];
119
- state.loggerWarnCalls = [];
120
- state.heartbeatInfoLogs = [];
121
- state.heartbeatWarnLogs = [];
122
-
123
- senderMock = vi.fn(async () => ({ messageId: "m1" }));
124
- sender = senderMock as unknown as typeof sendMessageWhatsApp;
125
- replyResolverMock = vi.fn(async () => undefined);
126
- replyResolver = replyResolverMock as unknown as typeof getReplyFromConfig;
127
- });
128
-
129
- it("supports manual override body dry-run without sending", async () => {
130
- await runWebHeartbeatOnce(buildRunArgs({ overrideBody: "hello", dryRun: true }));
131
- expect(senderMock).not.toHaveBeenCalled();
132
- expect(state.events).toHaveLength(0);
133
- });
134
-
135
- it("sends HEARTBEAT_OK when reply is empty and showOk is enabled", async () => {
136
- await runWebHeartbeatOnce(buildRunArgs());
137
- expect(senderMock).toHaveBeenCalledWith(
138
- "+123",
139
- HEARTBEAT_TOKEN,
140
- expect.objectContaining({ verbose: false, cfg: expect.any(Object) }),
141
- );
142
- expect(state.events).toEqual(
143
- expect.arrayContaining([expect.objectContaining({ status: "ok-empty", silent: false })]),
144
- );
145
- });
146
-
147
- it("injects a cron-style Current time line into the heartbeat prompt", async () => {
148
- await runWebHeartbeatOnce(
149
- buildRunArgs({
150
- cfg: { agents: { defaults: { heartbeat: { prompt: "Ops check" } } }, session: {} } as never,
151
- dryRun: true,
152
- }),
153
- );
154
- expect(replyResolver).toHaveBeenCalledTimes(1);
155
- const ctx = replyResolverMock.mock.calls[0]?.[0];
156
- expect(ctx?.Body).toContain("Ops check");
157
- expect(ctx?.Body).toContain("Current time: 2026-02-15T00:00:00Z (mock)");
158
- });
159
-
160
- it("treats heartbeat token-only replies as ok-token and preserves session updatedAt", async () => {
161
- replyResolverMock.mockResolvedValue({ text: HEARTBEAT_TOKEN });
162
- await runWebHeartbeatOnce(buildRunArgs());
163
- expect(state.store.k?.updatedAt).toBe(123);
164
- expect(senderMock).toHaveBeenCalledWith(
165
- "+123",
166
- HEARTBEAT_TOKEN,
167
- expect.objectContaining({ verbose: false, cfg: expect.any(Object) }),
168
- );
169
- expect(state.events).toEqual(
170
- expect.arrayContaining([expect.objectContaining({ status: "ok-token", silent: false })]),
171
- );
172
- });
173
-
174
- it("skips sending alerts when showAlerts is disabled but still emits a skipped event", async () => {
175
- state.visibility = { showAlerts: false, showOk: true, useIndicator: true };
176
- replyResolverMock.mockResolvedValue({ text: "ALERT" });
177
- await runWebHeartbeatOnce(buildRunArgs());
178
- expect(senderMock).not.toHaveBeenCalled();
179
- expect(state.events).toEqual(
180
- expect.arrayContaining([
181
- expect.objectContaining({ status: "skipped", reason: "alerts-disabled", preview: "ALERT" }),
182
- ]),
183
- );
184
- });
185
-
186
- it("emits failed events when sending throws and rethrows the error", async () => {
187
- replyResolverMock.mockResolvedValue({ text: "ALERT" });
188
- senderMock.mockRejectedValueOnce(new Error("nope"));
189
- await expect(runWebHeartbeatOnce(buildRunArgs())).rejects.toThrow("nope");
190
- expect(state.events).toEqual(
191
- expect.arrayContaining([
192
- expect.objectContaining({ status: "failed", reason: "ERR:Error: nope" }),
193
- ]),
194
- );
195
- });
196
-
197
- it("redacts recipient and omits body preview in heartbeat logs", async () => {
198
- replyResolverMock.mockResolvedValue({ text: "sensitive heartbeat body" });
199
- await runWebHeartbeatOnce(buildRunArgs({ dryRun: true }));
200
-
201
- const expected = redactIdentifier("+123");
202
- const heartbeatLogs = state.heartbeatInfoLogs.join("\n");
203
- const childLoggerLogs = state.loggerInfoCalls.map((entry) => JSON.stringify(entry)).join("\n");
204
-
205
- expect(heartbeatLogs).toContain(expected);
206
- expect(heartbeatLogs).not.toContain("+123");
207
- expect(heartbeatLogs).not.toContain("sensitive heartbeat body");
208
-
209
- expect(childLoggerLogs).toContain(expected);
210
- expect(childLoggerLogs).not.toContain("+123");
211
- expect(childLoggerLogs).not.toContain("sensitive heartbeat body");
212
- expect(childLoggerLogs).not.toContain('"preview"');
213
- });
214
- });
@@ -1,330 +0,0 @@
1
- import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
2
- import { newConnectionId } from "../reconnect.js";
3
- import {
4
- DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
5
- HEARTBEAT_TOKEN,
6
- appendCronStyleCurrentTimeLine,
7
- canonicalizeMainSessionAlias,
8
- emitHeartbeatEvent,
9
- formatError,
10
- getRuntimeConfig,
11
- getChildLogger,
12
- getReplyFromConfig,
13
- hasOutboundReplyContent,
14
- loadSessionStore,
15
- normalizeMainKey,
16
- redactIdentifier,
17
- resolveHeartbeatPrompt,
18
- resolveHeartbeatReplyPayload,
19
- resolveHeartbeatVisibility,
20
- resolveIndicatorType,
21
- resolveSendableOutboundReplyParts,
22
- resolveSessionKey,
23
- resolveStorePath,
24
- resolveWhatsAppHeartbeatRecipients,
25
- sendMessageWhatsApp,
26
- stripHeartbeatToken,
27
- updateSessionStore,
28
- whatsappHeartbeatLog,
29
- } from "./heartbeat-runner.runtime.js";
30
- import { getSessionSnapshot } from "./session-snapshot.js";
31
-
32
- function resolveDefaultAgentIdFromConfig(cfg: ReturnType<typeof getRuntimeConfig>): string {
33
- const agents = cfg.agents?.list ?? [];
34
- const chosen = agents.find((agent) => agent?.default)?.id ?? agents[0]?.id ?? "main";
35
- return normalizeOptionalLowercaseString(chosen) ?? "main";
36
- }
37
-
38
- export async function runWebHeartbeatOnce(opts: {
39
- cfg?: ReturnType<typeof getRuntimeConfig>;
40
- to: string;
41
- verbose?: boolean;
42
- replyResolver?: typeof getReplyFromConfig;
43
- sender?: typeof sendMessageWhatsApp;
44
- sessionId?: string;
45
- overrideBody?: string;
46
- dryRun?: boolean;
47
- }) {
48
- const { cfg: cfgOverride, to, verbose = false, sessionId, overrideBody, dryRun = false } = opts;
49
- const replyResolver = opts.replyResolver ?? getReplyFromConfig;
50
- const sender = opts.sender ?? sendMessageWhatsApp;
51
- const runId = newConnectionId();
52
- const redactedTo = redactIdentifier(to);
53
- const heartbeatLogger = getChildLogger({
54
- module: "web-heartbeat",
55
- runId,
56
- to: redactedTo,
57
- });
58
-
59
- const cfg = cfgOverride ?? getRuntimeConfig();
60
-
61
- // Resolve heartbeat visibility settings for WhatsApp
62
- const visibility = resolveHeartbeatVisibility({ cfg, channel: "whatsapp" });
63
- const heartbeatOkText = HEARTBEAT_TOKEN;
64
-
65
- const maybeSendHeartbeatOk = async (): Promise<boolean> => {
66
- if (!visibility.showOk) {
67
- return false;
68
- }
69
- if (dryRun) {
70
- whatsappHeartbeatLog.info(`[dry-run] heartbeat ok -> ${redactedTo}`);
71
- return false;
72
- }
73
- const sendResult = await sender(to, heartbeatOkText, { verbose, cfg });
74
- heartbeatLogger.info(
75
- {
76
- to: redactedTo,
77
- messageId: sendResult.messageId,
78
- chars: heartbeatOkText.length,
79
- reason: "heartbeat-ok",
80
- },
81
- "heartbeat ok sent",
82
- );
83
- whatsappHeartbeatLog.info(`heartbeat ok sent to ${redactedTo} (id ${sendResult.messageId})`);
84
- return true;
85
- };
86
-
87
- const sessionCfg = cfg.session;
88
- const sessionScope = sessionCfg?.scope ?? "per-sender";
89
- const mainKey = normalizeMainKey(sessionCfg?.mainKey);
90
- // Canonicalize so the written key matches what read paths produce (#29683).
91
- const rawSessionKey = resolveSessionKey(sessionScope, { From: to }, mainKey);
92
- const sessionKey = canonicalizeMainSessionAlias({
93
- cfg,
94
- agentId: resolveDefaultAgentIdFromConfig(cfg),
95
- sessionKey: rawSessionKey,
96
- });
97
- if (sessionId) {
98
- const storePath = resolveStorePath(cfg.session?.store);
99
- const store = loadSessionStore(storePath);
100
- const current = store[sessionKey] ?? {};
101
- store[sessionKey] = {
102
- ...current,
103
- sessionId,
104
- updatedAt: Date.now(),
105
- };
106
- await updateSessionStore(storePath, (nextStore) => {
107
- const nextCurrent = nextStore[sessionKey] ?? current;
108
- nextStore[sessionKey] = {
109
- ...nextCurrent,
110
- sessionId,
111
- updatedAt: Date.now(),
112
- };
113
- });
114
- }
115
- const sessionSnapshot = getSessionSnapshot(cfg, to, true, { sessionKey });
116
- if (verbose) {
117
- heartbeatLogger.info(
118
- {
119
- to: redactedTo,
120
- sessionKey: sessionSnapshot.key,
121
- sessionId: sessionId ?? sessionSnapshot.entry?.sessionId ?? null,
122
- sessionFresh: sessionSnapshot.fresh,
123
- resetMode: sessionSnapshot.resetPolicy.mode,
124
- resetAtHour: sessionSnapshot.resetPolicy.atHour,
125
- idleMinutes: sessionSnapshot.resetPolicy.idleMinutes ?? null,
126
- dailyResetAt: sessionSnapshot.dailyResetAt ?? null,
127
- idleExpiresAt: sessionSnapshot.idleExpiresAt ?? null,
128
- },
129
- "heartbeat session snapshot",
130
- );
131
- }
132
-
133
- if (overrideBody && overrideBody.trim().length === 0) {
134
- throw new Error("Override body must be non-empty when provided.");
135
- }
136
-
137
- try {
138
- if (overrideBody) {
139
- if (dryRun) {
140
- whatsappHeartbeatLog.info(
141
- `[dry-run] web send -> ${redactedTo} (${overrideBody.trim().length} chars, manual message)`,
142
- );
143
- return;
144
- }
145
- const sendResult = await sender(to, overrideBody, { verbose, cfg });
146
- emitHeartbeatEvent({
147
- status: "sent",
148
- to,
149
- preview: overrideBody.slice(0, 160),
150
- hasMedia: false,
151
- channel: "whatsapp",
152
- indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : undefined,
153
- });
154
- heartbeatLogger.info(
155
- {
156
- to: redactedTo,
157
- messageId: sendResult.messageId,
158
- chars: overrideBody.length,
159
- reason: "manual-message",
160
- },
161
- "manual heartbeat message sent",
162
- );
163
- whatsappHeartbeatLog.info(
164
- `manual heartbeat sent to ${redactedTo} (id ${sendResult.messageId})`,
165
- );
166
- return;
167
- }
168
-
169
- if (!visibility.showAlerts && !visibility.showOk && !visibility.useIndicator) {
170
- heartbeatLogger.info({ to: redactedTo, reason: "alerts-disabled" }, "heartbeat skipped");
171
- emitHeartbeatEvent({
172
- status: "skipped",
173
- to,
174
- reason: "alerts-disabled",
175
- channel: "whatsapp",
176
- });
177
- return;
178
- }
179
-
180
- const replyResult = await replyResolver(
181
- {
182
- Body: appendCronStyleCurrentTimeLine(
183
- resolveHeartbeatPrompt(cfg.agents?.defaults?.heartbeat?.prompt),
184
- cfg,
185
- Date.now(),
186
- ),
187
- From: to,
188
- To: to,
189
- MessageSid: sessionId ?? sessionSnapshot.entry?.sessionId,
190
- },
191
- { isHeartbeat: true },
192
- cfg,
193
- );
194
- const replyPayload = resolveHeartbeatReplyPayload(replyResult);
195
-
196
- if (!replyPayload || !hasOutboundReplyContent(replyPayload)) {
197
- heartbeatLogger.info(
198
- {
199
- to: redactedTo,
200
- reason: "empty-reply",
201
- sessionId: sessionSnapshot.entry?.sessionId ?? null,
202
- },
203
- "heartbeat skipped",
204
- );
205
- const okSent = await maybeSendHeartbeatOk();
206
- emitHeartbeatEvent({
207
- status: "ok-empty",
208
- to,
209
- channel: "whatsapp",
210
- silent: !okSent,
211
- indicatorType: visibility.useIndicator ? resolveIndicatorType("ok-empty") : undefined,
212
- });
213
- return;
214
- }
215
-
216
- const reply = resolveSendableOutboundReplyParts(replyPayload);
217
- const hasMedia = reply.hasMedia;
218
- const ackMaxChars = Math.max(
219
- 0,
220
- cfg.agents?.defaults?.heartbeat?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
221
- );
222
- const stripped = stripHeartbeatToken(replyPayload.text, {
223
- mode: "heartbeat",
224
- maxAckChars: ackMaxChars,
225
- });
226
- if (stripped.shouldSkip && !hasMedia) {
227
- // Don't let heartbeats keep sessions alive: restore previous updatedAt so idle expiry still works.
228
- const storePath = resolveStorePath(cfg.session?.store);
229
- const store = loadSessionStore(storePath);
230
- if (sessionSnapshot.entry && store[sessionSnapshot.key]) {
231
- store[sessionSnapshot.key].updatedAt = sessionSnapshot.entry.updatedAt;
232
- await updateSessionStore(storePath, (nextStore) => {
233
- const nextEntry = nextStore[sessionSnapshot.key];
234
- if (!nextEntry) {
235
- return;
236
- }
237
- nextStore[sessionSnapshot.key] = {
238
- ...nextEntry,
239
- updatedAt: sessionSnapshot.entry.updatedAt,
240
- };
241
- });
242
- }
243
-
244
- heartbeatLogger.info(
245
- { to: redactedTo, reason: "heartbeat-token", rawLength: replyPayload.text?.length },
246
- "heartbeat skipped",
247
- );
248
- const okSent = await maybeSendHeartbeatOk();
249
- emitHeartbeatEvent({
250
- status: "ok-token",
251
- to,
252
- channel: "whatsapp",
253
- silent: !okSent,
254
- indicatorType: visibility.useIndicator ? resolveIndicatorType("ok-token") : undefined,
255
- });
256
- return;
257
- }
258
-
259
- if (hasMedia) {
260
- heartbeatLogger.warn(
261
- { to: redactedTo },
262
- "heartbeat reply contained media; sending text only",
263
- );
264
- }
265
-
266
- const finalText = stripped.text || reply.text;
267
-
268
- // Check if alerts are disabled for WhatsApp
269
- if (!visibility.showAlerts) {
270
- heartbeatLogger.info({ to: redactedTo, reason: "alerts-disabled" }, "heartbeat skipped");
271
- emitHeartbeatEvent({
272
- status: "skipped",
273
- to,
274
- reason: "alerts-disabled",
275
- preview: finalText.slice(0, 200),
276
- channel: "whatsapp",
277
- hasMedia,
278
- indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : undefined,
279
- });
280
- return;
281
- }
282
-
283
- if (dryRun) {
284
- heartbeatLogger.info(
285
- { to: redactedTo, reason: "dry-run", chars: finalText.length },
286
- "heartbeat dry-run",
287
- );
288
- whatsappHeartbeatLog.info(`[dry-run] heartbeat -> ${redactedTo} (${finalText.length} chars)`);
289
- return;
290
- }
291
-
292
- const sendResult = await sender(to, finalText, { verbose, cfg });
293
- emitHeartbeatEvent({
294
- status: "sent",
295
- to,
296
- preview: finalText.slice(0, 160),
297
- hasMedia,
298
- channel: "whatsapp",
299
- indicatorType: visibility.useIndicator ? resolveIndicatorType("sent") : undefined,
300
- });
301
- heartbeatLogger.info(
302
- {
303
- to: redactedTo,
304
- messageId: sendResult.messageId,
305
- chars: finalText.length,
306
- },
307
- "heartbeat sent",
308
- );
309
- whatsappHeartbeatLog.info(`heartbeat alert sent to ${redactedTo}`);
310
- } catch (err) {
311
- const reason = formatError(err);
312
- heartbeatLogger.warn({ to: redactedTo, error: reason }, "heartbeat failed");
313
- whatsappHeartbeatLog.warn(`heartbeat failed (${reason})`);
314
- emitHeartbeatEvent({
315
- status: "failed",
316
- to,
317
- reason,
318
- channel: "whatsapp",
319
- indicatorType: visibility.useIndicator ? resolveIndicatorType("failed") : undefined,
320
- });
321
- throw err;
322
- }
323
- }
324
-
325
- export function resolveHeartbeatRecipients(
326
- cfg: ReturnType<typeof getRuntimeConfig>,
327
- opts: { to?: string; all?: boolean; accountId?: string } = {},
328
- ) {
329
- return resolveWhatsAppHeartbeatRecipients(cfg, opts);
330
- }
@@ -1,69 +0,0 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2
- import { normalizeMainKey } from "openclaw/plugin-sdk/routing";
3
- import {
4
- evaluateSessionFreshness,
5
- loadSessionStore,
6
- resolveChannelResetConfig,
7
- resolveThreadFlag,
8
- resolveSessionResetPolicy,
9
- resolveSessionResetType,
10
- resolveSessionKey,
11
- resolveStorePath,
12
- } from "./config.runtime.js";
13
-
14
- export function getSessionSnapshot(
15
- cfg: OpenClawConfig,
16
- from: string,
17
- _isHeartbeat = false,
18
- ctx?: {
19
- sessionKey?: string | null;
20
- isGroup?: boolean;
21
- messageThreadId?: string | number | null;
22
- threadLabel?: string | null;
23
- threadStarterBody?: string | null;
24
- parentSessionKey?: string | null;
25
- },
26
- ) {
27
- const sessionCfg = cfg.session;
28
- const scope = sessionCfg?.scope ?? "per-sender";
29
- const key =
30
- ctx?.sessionKey?.trim() ??
31
- resolveSessionKey(
32
- scope,
33
- { From: from, To: "", Body: "" },
34
- normalizeMainKey(sessionCfg?.mainKey),
35
- );
36
- const store = loadSessionStore(resolveStorePath(sessionCfg?.store));
37
- const entry = store[key];
38
-
39
- const isThread = resolveThreadFlag({
40
- sessionKey: key,
41
- messageThreadId: ctx?.messageThreadId ?? null,
42
- threadLabel: ctx?.threadLabel ?? null,
43
- threadStarterBody: ctx?.threadStarterBody ?? null,
44
- parentSessionKey: ctx?.parentSessionKey ?? null,
45
- });
46
- const resetType = resolveSessionResetType({ sessionKey: key, isGroup: ctx?.isGroup, isThread });
47
- const channelReset = resolveChannelResetConfig({
48
- sessionCfg,
49
- channel: entry?.lastChannel ?? entry?.channel,
50
- });
51
- const resetPolicy = resolveSessionResetPolicy({
52
- sessionCfg,
53
- resetType,
54
- resetOverride: channelReset,
55
- });
56
- const now = Date.now();
57
- const freshness = entry
58
- ? evaluateSessionFreshness({ updatedAt: entry.updatedAt, now, policy: resetPolicy })
59
- : { fresh: false };
60
- return {
61
- key,
62
- entry,
63
- fresh: freshness.fresh,
64
- resetPolicy,
65
- resetType,
66
- dailyResetAt: freshness.dailyResetAt,
67
- idleExpiresAt: freshness.idleExpiresAt,
68
- };
69
- }
@@ -1,6 +0,0 @@
1
- export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
2
- export { normalizeE164 } from "openclaw/plugin-sdk/account-resolution";
3
- export { readChannelAllowFromStoreSync } from "openclaw/plugin-sdk/channel-pairing";
4
- export { normalizeChannelId } from "openclaw/plugin-sdk/channel-targets";
5
- export { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
6
- export type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";