@openclaw/whatsapp 2026.5.22 → 2026.5.24-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.
Files changed (35) hide show
  1. package/dist/{action-runtime-D4UqEXqf.js → action-runtime-DGx8vCsS.js} +1 -1
  2. package/dist/action-runtime-api.js +1 -1
  3. package/dist/action-runtime.runtime.js +1 -1
  4. package/dist/api.js +6 -6
  5. package/dist/approval-handler.runtime-j8U6lgcp.js +171 -0
  6. package/dist/approval-resolver-e1hAVRRy.js +15 -0
  7. package/dist/{auth-store-GTQvJznL.js → auth-store-BrKb0mwT.js} +2 -2
  8. package/dist/{channel-BQqQ-Gp5.js → channel-DOxZCBxE.js} +415 -34
  9. package/dist/channel-plugin-api.js +1 -1
  10. package/dist/{channel-react-action-CHdR3gbn.js → channel-react-action-O7asbQQ1.js} +2 -2
  11. package/dist/{channel.runtime-BZ_ya3mR.js → channel.runtime-B0cFknwD.js} +4 -4
  12. package/dist/{channel.setup-vWI0fm8Y.js → channel.setup-Bk7Hmp4a.js} +2 -2
  13. package/dist/{connection-controller-9TXWZRhx.js → connection-controller-ja6z4F_k.js} +2 -2
  14. package/dist/light-runtime-api.js +1 -1
  15. package/dist/{login-nJwaNuC-.js → login-BEkrpJbZ.js} +3 -3
  16. package/dist/{login-qr-l4UaR8Dj.js → login-qr-BVRx8j_M.js} +3 -3
  17. package/dist/login-qr-runtime.js +1 -1
  18. package/dist/{monitor-ClhD-fQ6.js → monitor-BU1ydkuF.js} +324 -87
  19. package/dist/{outbound-adapter-ltiZkW9L.js → outbound-adapter-BObZXXzU.js} +1 -1
  20. package/dist/outbound-payload-test-api.js +1 -1
  21. package/dist/{runtime-CboC4QhQ.js → runtime-BfAdAEYT.js} +2 -2
  22. package/dist/runtime-api.js +8 -8
  23. package/dist/runtime-setter-api.js +1 -1
  24. package/dist/send-C9jEw6hx.js +625 -0
  25. package/dist/{send-api-CXPUXbUL.js → send-api-CC-vHBWG.js} +2 -2
  26. package/dist/{session-CPsgRMMa.js → session-CDgpDc0l.js} +2 -2
  27. package/dist/{session.runtime-C6uBkEaI.js → session.runtime-LZFdq3yb.js} +1 -1
  28. package/dist/{setup-core-BTDKEc-x.js → setup-core-Ce-b_wzQ.js} +2 -2
  29. package/dist/{setup-finalize-DUYdQs-v.js → setup-finalize-BqOudqJw.js} +2 -2
  30. package/dist/setup-plugin-api.js +1 -1
  31. package/dist/{setup-surface-Uoh9W_29.js → setup-surface-CAchPwW5.js} +2 -2
  32. package/dist/test-api.js +1 -1
  33. package/npm-shrinkwrap.json +3 -3
  34. package/package.json +4 -4
  35. package/dist/send-E_kMiuZP.js +0 -269
@@ -1,17 +1,19 @@
1
1
  import { a as resolveWhatsAppAccount, s as resolveWhatsAppMediaMaxBytes } from "./accounts-BPYgj8Fv.js";
2
2
  import { t as isWhatsAppGroupJid } from "./normalize-target-bVWjgftN.js";
3
3
  import { t as resolveWhatsAppReactionLevel } from "./reaction-level-BqMOEBeq.js";
4
+ import { c as maybeResolveWhatsAppApprovalReaction, r as sendReactionWhatsApp } from "./send-C9jEw6hx.js";
5
+ import { n as getWhatsAppRuntime } from "./runtime-BfAdAEYT.js";
4
6
  import { i as prepareWhatsAppOutboundMedia, o as sendWhatsAppOutboundWithRetry, r as normalizeWhatsAppPayloadTextPreservingIndentation, t as normalizeWhatsAppOutboundPayload } from "./outbound-media-contract-ioTkt8tn.js";
5
7
  import { n as getStatusCode, t as formatError } from "./session-errors-BAj9D2La.js";
6
8
  import { d as markdownToWhatsApp, f as resolveJidToE164, l as isSelfChatMode, n as normalizeE164, t as convertMarkdownTables$1, u as jidToE164 } from "./text-runtime-Dk37KYHj.js";
7
- import { r as sendReactionWhatsApp } from "./send-E_kMiuZP.js";
8
9
  import { n as cacheInboundMessageMeta, r as lookupInboundMessageMeta, t as buildQuotedMessageOptions } from "./quoted-message-CveINB35.js";
9
- import { c as logoutWeb, g as readWebSelfId, o as getWebAuthAgeMs, r as WhatsAppAuthUnstableError, v as readWebSelfIdentityForDecision } from "./auth-store-GTQvJznL.js";
10
+ import { c as logoutWeb, g as readWebSelfId, o as getWebAuthAgeMs, r as WhatsAppAuthUnstableError, v as readWebSelfIdentityForDecision } from "./auth-store-BrKb0mwT.js";
10
11
  import { a as getSelfIdentity, c as resolveComparableIdentity, i as getReplyContext, n as getMentionIdentities, o as getSenderIdentity, r as getPrimaryIdentityId, s as identitiesOverlap, t as getComparableIdentityValues } from "./identity-xoLLdqEv.js";
11
12
  import { i as resolveWhatsAppInboundPolicy, r as resolveWhatsAppCommandAuthorized, t as checkInboundAccessControl } from "./access-control-p_vcYDW7.js";
12
- import { a as mayContainWhatsAppOutboundMention, c as extractContactContext, d as extractMediaPlaceholder, f as extractMentionedJids, i as addWhatsAppOutboundMentionsToContent, l as extractContextInfo, m as hasInboundUserContent, n as listWhatsAppSendResultMessageIds, o as resolveWhatsAppOutboundMentions, p as extractText, r as normalizeWhatsAppSendResult, s as describeReplyContext, t as createWebSendApi, u as extractLocationData } from "./send-api-CXPUXbUL.js";
13
- import { i as resolveWhatsAppSocketTiming, r as waitForWaConnection, t as createWaSocket } from "./session-CPsgRMMa.js";
14
- import { c as computeBackoff, d as resolveReconnectPolicy, f as sleepWithAbort, l as newConnectionId, n as WHATSAPP_WATCHDOG_TIMEOUT_ERROR, r as WhatsAppConnectionController, s as DEFAULT_RECONNECT_POLICY, u as resolveHeartbeatSeconds } from "./connection-controller-9TXWZRhx.js";
13
+ import { a as mayContainWhatsAppOutboundMention, c as extractContactContext, d as extractMediaPlaceholder, f as extractMentionedJids, i as addWhatsAppOutboundMentionsToContent, l as extractContextInfo, m as hasInboundUserContent, n as listWhatsAppSendResultMessageIds, o as resolveWhatsAppOutboundMentions, p as extractText, r as normalizeWhatsAppSendResult, s as describeReplyContext, t as createWebSendApi, u as extractLocationData } from "./send-api-CC-vHBWG.js";
14
+ import { i as resolveWhatsAppSocketTiming, r as waitForWaConnection, t as createWaSocket } from "./session-CDgpDc0l.js";
15
+ import { t as BufferJSON } from "./session.runtime-LZFdq3yb.js";
16
+ import { c as computeBackoff, d as resolveReconnectPolicy, f as sleepWithAbort, l as newConnectionId, n as WHATSAPP_WATCHDOG_TIMEOUT_ERROR, r as WhatsAppConnectionController, s as DEFAULT_RECONNECT_POLICY, u as resolveHeartbeatSeconds } from "./connection-controller-ja6z4F_k.js";
15
17
  import { resolveAccountEntry } from "openclaw/plugin-sdk/account-core";
16
18
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
17
19
  import { evaluateSupplementalContextVisibility, filterSupplementalContextItems, resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
@@ -22,17 +24,20 @@ import { resolveMarkdownTableMode as resolveMarkdownTableMode$1 } from "openclaw
22
24
  import { createSubsystemLogger, defaultRuntime, formatDurationPrecise, getChildLogger as getChildLogger$1, logVerbose, logVerbose as logVerbose$1, registerUnhandledRejectionHandler, shouldLogVerbose, shouldLogVerbose as shouldLogVerbose$1, warn } from "openclaw/plugin-sdk/runtime-env";
23
25
  import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
24
26
  import { LocalMediaAccessError, getDefaultLocalRoots, loadWebMedia as loadWebMedia$1, loadWebMediaRaw, optimizeImageToJpeg, optimizeImageToPng } from "openclaw/plugin-sdk/web-media";
25
- import { createChannelMessageReplyPipeline, createMessageReceiptFromOutboundResults, deliverInboundReplyWithMessageSendContext, resolveChannelMessageSourceReplyDeliveryMode } from "openclaw/plugin-sdk/channel-message";
27
+ import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, DEFAULT_MAIN_KEY, buildAgentMainSessionKey, buildAgentSessionKey, buildGroupHistoryKey, deriveLastRoutePolicy, normalizeAccountId as normalizeAccountId$1, normalizeAgentId, resolveAgentRoute, resolveInboundLastRouteSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
28
+ import { createChannelMessageReplyPipeline, createDurableInboundReceiveJournal, createMessageReceiptFromOutboundResults, deliverInboundReplyWithMessageSendContext, resolveChannelMessageSourceReplyDeliveryMode } from "openclaw/plugin-sdk/channel-message";
26
29
  import { chunkMarkdownTextWithMode } from "openclaw/plugin-sdk/reply-chunking";
27
30
  import { isReasoningReplyPayload, resolveSendableOutboundReplyParts, sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";
28
- import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, DEFAULT_MAIN_KEY, buildAgentMainSessionKey, buildAgentSessionKey, buildGroupHistoryKey, deriveLastRoutePolicy, normalizeAccountId as normalizeAccountId$1, normalizeAgentId, resolveAgentRoute, resolveInboundLastRouteSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
29
31
  import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
30
32
  import { loadSessionStore, resolveStorePath, updateLastRoute, updateSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
31
33
  import { DisconnectReason as DisconnectReason$1, downloadMediaMessage, isJidGroup, normalizeMessageContent as normalizeMessageContent$1 } from "baileys";
32
34
  import { formatLocationText, resolveInboundSessionEnvelopeContext } from "openclaw/plugin-sdk/channel-inbound";
33
35
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
36
+ import { createHash } from "node:crypto";
34
37
  import { dispatchReplyWithBufferedBlockDispatcher, finalizeInboundContext, resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
38
+ import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-runtime";
35
39
  import { createInboundDebouncer, resolveInboundDebounceMs } from "openclaw/plugin-sdk/channel-inbound-debounce";
40
+ import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
36
41
  import { hasControlCommand, isControlCommandMessage, isControlCommandMessage as isControlCommandMessage$1, shouldComputeCommandAuthorized } from "openclaw/plugin-sdk/command-detection";
37
42
  import { drainPendingDeliveries } from "openclaw/plugin-sdk/delivery-queue-runtime";
38
43
  import { DEFAULT_GROUP_HISTORY_LIMIT, buildHistoryContextFromEntries, buildInboundHistoryFromEntries, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
@@ -119,8 +124,8 @@ function resetWebInboundDedupe() {
119
124
  claimableInboundMessages.clearMemory();
120
125
  recentOutboundMessages.clear();
121
126
  }
122
- async function claimRecentInboundMessage(key) {
123
- return (await claimableInboundMessages.claim(key)).kind === "claimed";
127
+ async function claimRecentInboundMessageDelivery(key) {
128
+ return (await claimableInboundMessages.claim(key)).kind;
124
129
  }
125
130
  async function commitRecentInboundMessage(key) {
126
131
  await claimableInboundMessages.commit(key);
@@ -139,6 +144,42 @@ function isRecentOutboundMessage(params) {
139
144
  return recentOutboundMessages.peek(key);
140
145
  }
141
146
  //#endregion
147
+ //#region extensions/whatsapp/src/inbound/durable-receive.ts
148
+ const WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES = 450;
149
+ const WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES = 450;
150
+ const WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS = 720 * 60 * 60 * 1e3;
151
+ const WHATSAPP_DURABLE_INBOUND_COMPLETED_TTL_MS = 10080 * 60 * 1e3;
152
+ function hashNamespacePart(value) {
153
+ return createHash("sha256").update(value).digest("hex").slice(0, 24);
154
+ }
155
+ function createWhatsAppDurableInboundMessageId(params) {
156
+ return createHash("sha256").update(`${params.remoteJid}\n${params.id}`).digest("hex");
157
+ }
158
+ function serializeWhatsAppDurableInboundMessage(message) {
159
+ return JSON.parse(JSON.stringify(message, BufferJSON.replacer));
160
+ }
161
+ function deserializeWhatsAppDurableInboundMessage(message) {
162
+ return JSON.parse(JSON.stringify(message), BufferJSON.reviver);
163
+ }
164
+ function createWhatsAppDurableInboundReceiveJournal(accountId) {
165
+ const runtime = getWhatsAppRuntime();
166
+ const accountPart = hashNamespacePart(accountId);
167
+ return createDurableInboundReceiveJournal({
168
+ pendingStore: runtime.state.openKeyedStore({
169
+ namespace: `inbound.v1.pending.${accountPart}`,
170
+ maxEntries: WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES,
171
+ defaultTtlMs: WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS
172
+ }),
173
+ completedStore: runtime.state.openKeyedStore({
174
+ namespace: `inbound.v1.completed.${accountPart}`,
175
+ maxEntries: WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES,
176
+ defaultTtlMs: WHATSAPP_DURABLE_INBOUND_COMPLETED_TTL_MS
177
+ }),
178
+ pendingTtlMs: WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS,
179
+ completedTtlMs: WHATSAPP_DURABLE_INBOUND_COMPLETED_TTL_MS
180
+ });
181
+ }
182
+ //#endregion
142
183
  //#region extensions/whatsapp/src/inbound/lifecycle.ts
143
184
  function attachEmitterListener(emitter, event, listener) {
144
185
  emitter.on(event, listener);
@@ -298,6 +339,7 @@ async function attachWebInboxToSocket(options) {
298
339
  const selfIdentity = await readWebSelfIdentityForDecision(options.authDir, sock.user);
299
340
  if (selfIdentity.outcome === "unstable") throw new WhatsAppAuthUnstableError("WhatsApp auth state is still stabilizing; retrying inbox attach.");
300
341
  const self = selfIdentity.identity;
342
+ const durableInboundJournal = createWhatsAppDurableInboundReceiveJournal(options.accountId);
301
343
  const inboundDebounceMs = Math.max(0, Math.trunc(options.debounceMs ?? 0));
302
344
  const pendingDebounceKeys = /* @__PURE__ */ new Set();
303
345
  const activeInboundFlushes = /* @__PURE__ */ new Set();
@@ -309,14 +351,17 @@ async function attachWebInboxToSocket(options) {
309
351
  return `${msg.accountId}:${conversationKey}:${senderKey}`;
310
352
  };
311
353
  const shouldDebounceInboundMessage = (msg) => options.shouldDebounce?.(msg) ?? true;
312
- const finalizeInboundDedupe = async (entries, error) => {
354
+ const finalizeInboundDelivery = async (entries, error) => {
313
355
  const dedupeKeys = [...new Set(entries.map((entry) => entry.dedupeKey).filter(isNonEmptyString))];
314
- if (dedupeKeys.length === 0) return;
356
+ const durableEntries = entries.filter((entry) => isNonEmptyString(entry.durableId));
357
+ const readReceiptEntries = entries.filter((entry) => Boolean(entry.readReceipt));
315
358
  if (error instanceof WhatsAppRetryableInboundError) {
316
359
  dedupeKeys.forEach((dedupeKey) => releaseRecentInboundMessage(dedupeKey, error));
360
+ await Promise.all(durableEntries.map((entry) => durableInboundJournal.release(entry.durableId, { lastError: formatError(error) })));
317
361
  return;
318
362
  }
319
- await Promise.all(dedupeKeys.map((dedupeKey) => commitRecentInboundMessage(dedupeKey)));
363
+ await Promise.all([...dedupeKeys.map((dedupeKey) => commitRecentInboundMessage(dedupeKey)), ...durableEntries.map((entry) => durableInboundJournal.complete(entry.durableId, entry.readReceipt ? { metadata: { readReceipt: entry.readReceipt } } : void 0))]);
364
+ await Promise.all(readReceiptEntries.map((entry) => maybeMarkInboundAsRead(entry.readReceipt)));
320
365
  };
321
366
  const debouncer = createInboundDebouncer({
322
367
  debounceMs: inboundDebounceMs,
@@ -334,7 +379,7 @@ async function attachWebInboxToSocket(options) {
334
379
  try {
335
380
  if (entries.length === 1) {
336
381
  await options.onMessage(last);
337
- await finalizeInboundDedupe(entries);
382
+ await finalizeInboundDelivery(entries);
338
383
  return;
339
384
  }
340
385
  const mentioned = /* @__PURE__ */ new Set();
@@ -348,9 +393,9 @@ async function attachWebInboxToSocket(options) {
348
393
  isBatched: true
349
394
  };
350
395
  await options.onMessage(combinedMessage);
351
- await finalizeInboundDedupe(entries);
396
+ await finalizeInboundDelivery(entries);
352
397
  } catch (error) {
353
- await finalizeInboundDedupe(entries, error);
398
+ await finalizeInboundDelivery(entries, error);
354
399
  throw error;
355
400
  }
356
401
  } finally {
@@ -537,21 +582,121 @@ async function attachWebInboxToSocket(options) {
537
582
  access
538
583
  };
539
584
  };
540
- const maybeMarkInboundAsRead = async (inbound) => {
541
- const { id, remoteJid, participantJid, access } = inbound;
542
- if (id && !access.isSelfChat && options.sendReadReceipts !== false) try {
543
- await sock.readMessages([{
585
+ const buildReadReceiptTarget = (inbound) => inbound.id ? {
586
+ remoteJid: inbound.remoteJid,
587
+ id: inbound.id,
588
+ ...inbound.participantJid ? { participant: inbound.participantJid } : {}
589
+ } : void 0;
590
+ const maybeMarkInboundAsRead = async (target) => {
591
+ if (!target || options.sendReadReceipts === false) return;
592
+ const { id, remoteJid, participant } = target;
593
+ try {
594
+ await (getCurrentSock() ?? sock).readMessages([{
544
595
  remoteJid,
545
596
  id,
546
- participant: participantJid,
597
+ participant,
547
598
  fromMe: false
548
599
  }]);
549
- const suffix = participantJid ? ` (participant ${participantJid})` : "";
600
+ const suffix = participant ? ` (participant ${participant})` : "";
550
601
  logWhatsAppVerbose(options.verbose, `Marked message ${id} as read for ${remoteJid}${suffix}`);
551
602
  } catch (err) {
552
603
  logWhatsAppVerbose(options.verbose, `Failed to mark message ${id} read: ${String(err)}`);
553
604
  }
554
- else if (id && access.isSelfChat && options.verbose) logWhatsAppVerbose(options.verbose, `Self-chat mode: skipping read receipt for ${id}`);
605
+ };
606
+ const maybeLogSkippedSelfChatReadReceipt = (inbound, target) => {
607
+ if (target?.id && inbound.access.isSelfChat && options.verbose) logWhatsAppVerbose(options.verbose, `Self-chat mode: skipping read receipt for ${target.id}`);
608
+ };
609
+ const maybeMarkReadReceiptAfterCompletedDelivery = async (inbound, target) => {
610
+ if (inbound.access.isSelfChat) {
611
+ maybeLogSkippedSelfChatReadReceipt(inbound, target);
612
+ return;
613
+ }
614
+ await maybeMarkInboundAsRead(target);
615
+ };
616
+ const maybeMarkReadReceiptForSkippedAppend = async (inbound, target) => {
617
+ if (inbound.access.isSelfChat) {
618
+ maybeLogSkippedSelfChatReadReceipt(inbound, target);
619
+ return;
620
+ }
621
+ await maybeMarkInboundAsRead(target);
622
+ };
623
+ const completeUndeliverableDurableInbound = async (durableId, metadata) => {
624
+ if (!durableId) return;
625
+ await durableInboundJournal.complete(durableId, metadata?.readReceipt ? { metadata: { readReceipt: metadata.readReceipt } } : void 0);
626
+ };
627
+ const buildDurableInboundPayload = (msg, upsertType) => ({
628
+ message: serializeWhatsAppDurableInboundMessage(msg),
629
+ ...upsertType ? { upsertType } : {},
630
+ receivedAt: Date.now()
631
+ });
632
+ const shouldSkipStaleAppend = (msg, upsertType) => {
633
+ if (upsertType !== "append") return false;
634
+ const APPEND_RECENT_GRACE_MS = 6e4;
635
+ const msgTsRaw = msg.messageTimestamp;
636
+ const msgTsNum = msgTsRaw != null ? Number(msgTsRaw) : NaN;
637
+ return (Number.isFinite(msgTsNum) ? msgTsNum * 1e3 : 0) < connectedAtMs - APPEND_RECENT_GRACE_MS;
638
+ };
639
+ const processDurableInboundMessage = async (msg, upsertType, stored) => {
640
+ const inbound = await normalizeInboundMessage(msg);
641
+ if (!inbound) {
642
+ if (stored) await completeUndeliverableDurableInbound(stored.id, stored.metadata);
643
+ return;
644
+ }
645
+ const readReceipt = stored?.metadata?.readReceipt ?? buildReadReceiptTarget(inbound);
646
+ const deliveryReadReceipt = inbound.access.isSelfChat ? void 0 : readReceipt;
647
+ if (!stored && shouldSkipStaleAppend(msg, upsertType)) {
648
+ await maybeMarkReadReceiptForSkippedAppend(inbound, readReceipt);
649
+ return;
650
+ }
651
+ let durableId = stored?.id ?? (inbound.id ? createWhatsAppDurableInboundMessageId({
652
+ remoteJid: inbound.remoteJid,
653
+ id: inbound.id
654
+ }) : void 0);
655
+ const durableMetadata = deliveryReadReceipt ? { readReceipt: deliveryReadReceipt } : void 0;
656
+ if (durableId && !stored) try {
657
+ const accepted = await durableInboundJournal.accept(durableId, buildDurableInboundPayload(msg, upsertType), {
658
+ metadata: durableMetadata,
659
+ receivedAt: inbound.messageTimestampMs
660
+ });
661
+ if (accepted.kind === "completed") {
662
+ await maybeMarkReadReceiptAfterCompletedDelivery(inbound, accepted.record.metadata?.readReceipt ?? deliveryReadReceipt);
663
+ return;
664
+ }
665
+ if (accepted.kind === "pending" && accepted.record.attempts === 0) return;
666
+ } catch (err) {
667
+ durableId = void 0;
668
+ const error = formatError(err);
669
+ inboundLogger.warn({ error }, "failed persisting durable WhatsApp inbound; delivering live");
670
+ inboundConsoleLog.warn(`Failed persisting durable WhatsApp inbound; delivering live: ${error}`);
671
+ }
672
+ const enriched = await enrichInboundMessage(msg);
673
+ if (!enriched) {
674
+ await completeUndeliverableDurableInbound(durableId, durableMetadata);
675
+ await maybeMarkReadReceiptAfterCompletedDelivery(inbound, deliveryReadReceipt);
676
+ return;
677
+ }
678
+ const dedupeKey = inbound.id ? `${options.accountId}:${inbound.remoteJid}:${inbound.id}` : "";
679
+ const dedupeClaim = dedupeKey ? await claimRecentInboundMessageDelivery(dedupeKey) : "claimed";
680
+ if (dedupeClaim !== "claimed") {
681
+ if (dedupeClaim === "duplicate") {
682
+ await completeUndeliverableDurableInbound(durableId, durableMetadata);
683
+ await maybeMarkReadReceiptAfterCompletedDelivery(inbound, deliveryReadReceipt);
684
+ }
685
+ return;
686
+ }
687
+ recordAcceptedInboundActivity(options.accountId);
688
+ await enqueueInboundMessage(msg, inbound, enriched, {
689
+ durableId,
690
+ readReceipt: deliveryReadReceipt
691
+ });
692
+ };
693
+ const replayPendingDurableInboundMessages = async () => {
694
+ const pending = await durableInboundJournal.pending();
695
+ for (const record of pending) await processDurableInboundMessage(deserializeWhatsAppDurableInboundMessage(record.payload.message), record.payload.upsertType, {
696
+ id: record.id,
697
+ payload: record.payload,
698
+ metadata: record.metadata
699
+ });
555
700
  };
556
701
  const enrichInboundMessage = async (msg) => {
557
702
  const location = extractLocationData(msg.message ?? void 0);
@@ -590,7 +735,7 @@ async function attachWebInboxToSocket(options) {
590
735
  mediaFileName
591
736
  };
592
737
  };
593
- const enqueueInboundMessage = async (msg, inbound, enriched) => {
738
+ const enqueueInboundMessage = async (msg, inbound, enriched, durable) => {
594
739
  const chatJid = inbound.remoteJid;
595
740
  const sendComposing = async () => {
596
741
  const currentSock = getCurrentSock();
@@ -668,7 +813,9 @@ async function attachWebInboxToSocket(options) {
668
813
  mediaPath: enriched.mediaPath,
669
814
  mediaType: enriched.mediaType,
670
815
  mediaFileName: enriched.mediaFileName,
671
- dedupeKey: inbound.id ? `${options.accountId}:${inbound.remoteJid}:${inbound.id}` : void 0
816
+ dedupeKey: inbound.id ? `${options.accountId}:${inbound.remoteJid}:${inbound.id}` : void 0,
817
+ durableId: durable.durableId,
818
+ readReceipt: durable.readReceipt
672
819
  };
673
820
  const debounceKey = buildInboundDebounceKey(inboundMessage);
674
821
  if (debounceKey) {
@@ -695,21 +842,16 @@ async function attachWebInboxToSocket(options) {
695
842
  const handleMessagesUpsert = async (upsert) => {
696
843
  if (upsert.type !== "notify" && upsert.type !== "append") return;
697
844
  for (const msg of upsert.messages ?? []) {
698
- const inbound = await normalizeInboundMessage(msg);
699
- if (!inbound) continue;
700
- await maybeMarkInboundAsRead(inbound);
701
- if (upsert.type === "append") {
702
- const APPEND_RECENT_GRACE_MS = 6e4;
703
- const msgTsRaw = msg.messageTimestamp;
704
- const msgTsNum = msgTsRaw != null ? Number(msgTsRaw) : NaN;
705
- if ((Number.isFinite(msgTsNum) ? msgTsNum * 1e3 : 0) < connectedAtMs - APPEND_RECENT_GRACE_MS) continue;
706
- }
707
- const enriched = await enrichInboundMessage(msg);
708
- if (!enriched) continue;
709
- const dedupeKey = inbound.id ? `${options.accountId}:${inbound.remoteJid}:${inbound.id}` : "";
710
- if (dedupeKey && !await claimRecentInboundMessage(dedupeKey)) continue;
711
- recordAcceptedInboundActivity(options.accountId);
712
- await enqueueInboundMessage(msg, inbound, enriched);
845
+ if (await maybeResolveWhatsAppApprovalReaction({
846
+ cfg: options.loadConfig?.() ?? options.cfg,
847
+ accountId: options.accountId,
848
+ msg,
849
+ selfJid: self.jid,
850
+ selfLid: self.lid,
851
+ resolveInboundJid,
852
+ logVerboseMessage: (message) => logWhatsAppVerbose(options.verbose, message)
853
+ })) continue;
854
+ await processDurableInboundMessage(msg, upsert.type);
713
855
  }
714
856
  };
715
857
  const handleMessagesUpsertEvent = (upsert) => {
@@ -773,6 +915,14 @@ async function attachWebInboxToSocket(options) {
773
915
  };
774
916
  const detachMessagesUpsert = attachEmitterListener(sock.ev, "messages.upsert", handleMessagesUpsertEvent);
775
917
  const detachConnectionUpdate = attachEmitterListener(sock.ev, "connection.update", handleConnectionUpdate);
918
+ const replayTask = replayPendingDurableInboundMessages().catch((err) => {
919
+ inboundLogger.error({ error: String(err) }, "failed replaying durable WhatsApp inbound");
920
+ inboundConsoleLog.error(`Failed replaying durable WhatsApp inbound: ${String(err)}`);
921
+ });
922
+ pendingMessageHandlers.add(replayTask);
923
+ replayTask.finally(() => {
924
+ pendingMessageHandlers.delete(replayTask);
925
+ });
776
926
  (async () => {
777
927
  try {
778
928
  const groups = await sock.groupFetchAllParticipating();
@@ -1496,6 +1646,21 @@ function createWhatsAppReplyDeliveryReceipt(results) {
1496
1646
  kind: resolveWhatsAppReceiptKind(results)
1497
1647
  });
1498
1648
  }
1649
+ function markWhatsAppVisibleDeliveryError(error) {
1650
+ if (typeof error === "object" && error !== null && !Array.isArray(error)) try {
1651
+ Object.assign(error, {
1652
+ sentBeforeError: true,
1653
+ visibleReplySent: true
1654
+ });
1655
+ return error;
1656
+ } catch {}
1657
+ const visibleError = new Error("visible WhatsApp reply delivery failed", { cause: error });
1658
+ Object.assign(visibleError, {
1659
+ sentBeforeError: true,
1660
+ visibleReplySent: true
1661
+ });
1662
+ return visibleError;
1663
+ }
1499
1664
  async function deliverWebReply(params) {
1500
1665
  const { replyResult, msg, maxMediaBytes, textLimit, replyLogger, connectionId, skipLog } = params;
1501
1666
  const replyStarted = Date.now();
@@ -1531,13 +1696,18 @@ async function deliverWebReply(params) {
1531
1696
  });
1532
1697
  };
1533
1698
  const sendWithRetry = async (fn, label, maxAttempts = 3) => {
1534
- return await sendWhatsAppOutboundWithRetry({
1535
- send: fn,
1536
- maxAttempts,
1537
- onRetry: ({ attempt, maxAttempts: retryMaxAttempts, backoffMs, errorText }) => {
1538
- logVerbose(`Retrying ${label} to ${msg.from} after failure (${attempt}/${retryMaxAttempts - 1}) in ${backoffMs}ms: ${errorText}`);
1539
- }
1540
- });
1699
+ try {
1700
+ return await sendWhatsAppOutboundWithRetry({
1701
+ send: fn,
1702
+ maxAttempts,
1703
+ onRetry: ({ attempt, maxAttempts: retryMaxAttempts, backoffMs, errorText }) => {
1704
+ logVerbose(`Retrying ${label} to ${msg.from} after failure (${attempt}/${retryMaxAttempts - 1}) in ${backoffMs}ms: ${errorText}`);
1705
+ }
1706
+ });
1707
+ } catch (error) {
1708
+ if (sendResults.some((result) => result.providerAccepted)) throw markWhatsAppVisibleDeliveryError(error);
1709
+ throw error;
1710
+ }
1541
1711
  };
1542
1712
  if (mediaList.length === 0 && textChunks.length) {
1543
1713
  const totalChunks = textChunks.length;
@@ -1679,9 +1849,42 @@ function resolveVisibleWhatsAppReplyContext(params) {
1679
1849
  }
1680
1850
  //#endregion
1681
1851
  //#region extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts
1852
+ function normalizeErrForLog(err) {
1853
+ if (err instanceof Error) return {
1854
+ ...Object.fromEntries(Object.entries(err)),
1855
+ type: err.name,
1856
+ message: err.message,
1857
+ stack: err.stack
1858
+ };
1859
+ return err;
1860
+ }
1861
+ function whatsAppReplyDeliveryVisibility(visibleReplySent) {
1862
+ return { visibleReplySent };
1863
+ }
1864
+ function whatsAppReplyDeliveryVisibilityFromDurableResult(result) {
1865
+ return whatsAppReplyDeliveryVisibility(result.visibleReplySent === true);
1866
+ }
1867
+ function markWhatsAppReplyDeliveryErrorVisible(error) {
1868
+ if (typeof error === "object" && error !== null && !Array.isArray(error)) try {
1869
+ Object.assign(error, {
1870
+ sentBeforeError: true,
1871
+ visibleReplySent: true
1872
+ });
1873
+ return error;
1874
+ } catch {}
1875
+ const visibleError = new Error("visible WhatsApp reply delivery failed", { cause: error });
1876
+ Object.assign(visibleError, {
1877
+ sentBeforeError: true,
1878
+ visibleReplySent: true
1879
+ });
1880
+ return visibleError;
1881
+ }
1882
+ function markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult) {
1883
+ return flushResult.delivered > 0 ? markWhatsAppReplyDeliveryErrorVisible(error) : error;
1884
+ }
1682
1885
  function logWhatsAppReplyDeliveryError(params) {
1683
1886
  params.replyLogger.error({
1684
- err: params.err,
1887
+ err: normalizeErrForLog(params.err),
1685
1888
  replyKind: params.info.kind,
1686
1889
  correlationId: params.msg.id ?? null,
1687
1890
  connectionId: params.connectionId,
@@ -1730,8 +1933,11 @@ function createWhatsAppMediaOnlyReplyCoalescer(params) {
1730
1933
  flushResult.droppedDuplicateMedia += 1;
1731
1934
  continue;
1732
1935
  }
1733
- await params.deliver(candidate);
1734
- flushResult.delivered += 1;
1936
+ try {
1937
+ if ((await params.deliver(candidate)).visibleReplySent) flushResult.delivered += 1;
1938
+ } catch (error) {
1939
+ throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
1940
+ }
1735
1941
  }
1736
1942
  return flushResult;
1737
1943
  };
@@ -1895,7 +2101,7 @@ async function dispatchWhatsAppBufferedReply(params) {
1895
2101
  let didLogHeartbeatStrip = false;
1896
2102
  const deliverNormalizedPayload = async (normalizedDeliveryPayload, info) => {
1897
2103
  const reply = resolveSendableOutboundReplyParts(normalizedDeliveryPayload);
1898
- if (!reply.hasMedia && !reply.text.trim()) return;
2104
+ if (!reply.hasMedia && !reply.text.trim()) return whatsAppReplyDeliveryVisibility(false);
1899
2105
  if (!(await params.deliverReply({
1900
2106
  replyResult: normalizedDeliveryPayload,
1901
2107
  normalizedReplyResult: normalizedDeliveryPayload,
@@ -1918,7 +2124,7 @@ async function dispatchWhatsAppBufferedReply(params) {
1918
2124
  from: params.msg.to,
1919
2125
  replyKind: info.kind
1920
2126
  }, "auto-reply was not accepted by WhatsApp provider");
1921
- return;
2127
+ return whatsAppReplyDeliveryVisibility(false);
1922
2128
  }
1923
2129
  didSendReply = true;
1924
2130
  const shouldLog = normalizedDeliveryPayload.text ? true : void 0;
@@ -1929,9 +2135,10 @@ async function dispatchWhatsAppBufferedReply(params) {
1929
2135
  });
1930
2136
  const fromDisplay = params.msg.chatType === "group" ? params.conversationId : params.msg.from ?? "unknown";
1931
2137
  if (shouldLogVerbose$1()) logVerbose$1(`Reply body: ${normalizedDeliveryPayload.text != null ? reply.text : "<media>"}${reply.hasMedia ? " (media)" : ""} -> ${fromDisplay}`);
2138
+ return whatsAppReplyDeliveryVisibility(true);
1932
2139
  };
1933
2140
  const mediaOnlyCoalescer = createWhatsAppMediaOnlyReplyCoalescer({ deliver: async (pending) => {
1934
- await deliverNormalizedPayload(pending.payload, pending.info);
2141
+ return await deliverNormalizedPayload(pending.payload, pending.info);
1935
2142
  } });
1936
2143
  if (statusReactionController) statusReactionController.setThinking();
1937
2144
  const { queuedFinal, counts } = await dispatchReplyWithBufferedBlockDispatcher({
@@ -1948,45 +2155,53 @@ async function dispatchWhatsAppBufferedReply(params) {
1948
2155
  },
1949
2156
  deliver: async (payload, info) => {
1950
2157
  const deliveryPayload = resolveWhatsAppDeliverablePayload(payload, info);
1951
- if (!deliveryPayload) return;
2158
+ if (!deliveryPayload) return whatsAppReplyDeliveryVisibility(false);
1952
2159
  const normalizedOutboundPayload = normalizeWhatsAppOutboundPayload(deliveryPayload, { normalizeText: normalizeWhatsAppPayloadTextPreservingIndentation });
1953
2160
  const normalizedDeliveryPayload = deliveryPayload.text === void 0 ? {
1954
2161
  ...normalizedOutboundPayload,
1955
2162
  text: void 0
1956
2163
  } : normalizedOutboundPayload;
1957
2164
  const reply = resolveSendableOutboundReplyParts(normalizedDeliveryPayload);
1958
- if (!reply.hasMedia && !reply.text.trim()) return;
2165
+ if (!reply.hasMedia && !reply.text.trim()) return whatsAppReplyDeliveryVisibility(false);
1959
2166
  if (!reply.hasMedia) {
1960
- logWhatsAppMediaOnlyFlushResult(await mediaOnlyCoalescer.flushAll());
1961
- const durable = await deliverInboundReplyWithMessageSendContext({
1962
- cfg: params.cfg,
1963
- channel: "whatsapp",
1964
- accountId: params.route.accountId,
1965
- agentId: params.route.agentId,
1966
- ctxPayload: params.context,
1967
- payload: normalizedDeliveryPayload,
1968
- info,
1969
- to: params.msg.from,
1970
- formatting: {
1971
- textLimit,
1972
- tableMode,
1973
- chunkMode
1974
- }
1975
- });
1976
- if (durable.status === "failed") throw durable.error;
1977
- if (durable.status === "handled_visible") {
1978
- didSendReply = true;
1979
- const shouldLog = normalizedDeliveryPayload.text ? true : void 0;
1980
- params.rememberSentText(normalizedDeliveryPayload.text, {
1981
- combinedBody: params.context.Body,
1982
- combinedBodySessionKey: params.route.sessionKey,
1983
- logVerboseMessage: shouldLog
2167
+ const flushResult = await mediaOnlyCoalescer.flushAll();
2168
+ logWhatsAppMediaOnlyFlushResult(flushResult);
2169
+ try {
2170
+ const durable = await deliverInboundReplyWithMessageSendContext({
2171
+ cfg: params.cfg,
2172
+ channel: "whatsapp",
2173
+ accountId: params.route.accountId,
2174
+ agentId: params.route.agentId,
2175
+ ctxPayload: params.context,
2176
+ payload: normalizedDeliveryPayload,
2177
+ info,
2178
+ to: params.msg.from,
2179
+ formatting: {
2180
+ textLimit,
2181
+ tableMode,
2182
+ chunkMode
2183
+ }
1984
2184
  });
1985
- return;
2185
+ if (durable.status === "failed") {
2186
+ if (durable.sentBeforeError === true) throw markWhatsAppReplyDeliveryErrorVisible(durable.error);
2187
+ throw durable.error;
2188
+ }
2189
+ if (durable.status === "handled_visible") {
2190
+ didSendReply = true;
2191
+ const shouldLog = normalizedDeliveryPayload.text ? true : void 0;
2192
+ params.rememberSentText(normalizedDeliveryPayload.text, {
2193
+ combinedBody: params.context.Body,
2194
+ combinedBodySessionKey: params.route.sessionKey,
2195
+ logVerboseMessage: shouldLog
2196
+ });
2197
+ return whatsAppReplyDeliveryVisibilityFromDurableResult(durable.delivery);
2198
+ }
2199
+ if (durable.status === "handled_no_send") return flushResult.delivered > 0 ? whatsAppReplyDeliveryVisibility(true) : whatsAppReplyDeliveryVisibilityFromDurableResult(durable.delivery);
2200
+ const delivery = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
2201
+ return flushResult.delivered > 0 && !delivery.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : delivery;
2202
+ } catch (error) {
2203
+ throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
1986
2204
  }
1987
- if (durable.status === "handled_no_send") return;
1988
- await deliverNormalizedPayload(normalizedDeliveryPayload, info);
1989
- return;
1990
2205
  }
1991
2206
  const mediaUrls = getWhatsAppPayloadMediaUrls(normalizedDeliveryPayload);
1992
2207
  if (shouldDeferWhatsAppMediaOnlyPayload({
@@ -1999,10 +2214,21 @@ async function dispatchWhatsAppBufferedReply(params) {
1999
2214
  mediaUrls,
2000
2215
  payload: normalizedDeliveryPayload
2001
2216
  });
2002
- return;
2217
+ return whatsAppReplyDeliveryVisibility(false);
2003
2218
  }
2004
- logWhatsAppMediaOnlyFlushResult(await mediaOnlyCoalescer.flushExceptDuplicateMedia(mediaUrls));
2005
- await deliverNormalizedPayload(normalizedDeliveryPayload, info);
2219
+ const flushResult = await mediaOnlyCoalescer.flushExceptDuplicateMedia(mediaUrls);
2220
+ logWhatsAppMediaOnlyFlushResult(flushResult);
2221
+ try {
2222
+ const delivery = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
2223
+ return flushResult.delivered > 0 && !delivery.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : delivery;
2224
+ } catch (error) {
2225
+ throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
2226
+ }
2227
+ },
2228
+ onSettled: async () => {
2229
+ const flushResult = await mediaOnlyCoalescer.flushAll();
2230
+ logWhatsAppMediaOnlyFlushResult(flushResult);
2231
+ return whatsAppReplyDeliveryVisibility(flushResult.delivered > 0);
2006
2232
  },
2007
2233
  onReplyStart: params.msg.sendComposing,
2008
2234
  ...statusReactionController ? {
@@ -2035,7 +2261,6 @@ async function dispatchWhatsAppBufferedReply(params) {
2035
2261
  } } : {}
2036
2262
  }
2037
2263
  });
2038
- logWhatsAppMediaOnlyFlushResult(await mediaOnlyCoalescer.flushAll());
2039
2264
  if (!hasVisibleInboundReplyDispatch({
2040
2265
  queuedFinal,
2041
2266
  counts
@@ -3054,6 +3279,14 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
3054
3279
  continue;
3055
3280
  }
3056
3281
  statusController.noteConnected();
3282
+ const approvalContextLease = registerChannelRuntimeContext({
3283
+ channelRuntime: tuning.channelRuntime,
3284
+ channelId: "whatsapp",
3285
+ accountId: account.accountId,
3286
+ capability: CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY,
3287
+ context: { accountId: account.accountId },
3288
+ abortSignal
3289
+ });
3057
3290
  controller.setUnhandledRejectionCleanup(registerUnhandledRejectionHandler((reason) => {
3058
3291
  if (!isLikelyWhatsAppCryptoError(reason)) return false;
3059
3292
  const errorStr = formatError(reason);
@@ -3121,10 +3354,14 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
3121
3354
  if (process.stdout.isTTY || process.stderr.isTTY) whatsappLog.raw("Ctrl+C to stop.");
3122
3355
  if (!keepAlive) {
3123
3356
  clearInterval(periodicDrainInterval);
3357
+ approvalContextLease?.dispose();
3124
3358
  await controller.shutdown();
3125
3359
  return;
3126
3360
  }
3127
- const reason = await controller.waitForClose().finally(() => clearInterval(periodicDrainInterval));
3361
+ const reason = await controller.waitForClose().finally(() => {
3362
+ clearInterval(periodicDrainInterval);
3363
+ approvalContextLease?.dispose();
3364
+ });
3128
3365
  if (stopRequested() || sigintStop || reason === "aborted") {
3129
3366
  await controller.shutdown();
3130
3367
  break;
@@ -6,7 +6,7 @@ import { chunkText } from "openclaw/plugin-sdk/reply-chunking";
6
6
  //#region extensions/whatsapp/src/outbound-adapter.ts
7
7
  let whatsAppSendModulePromise;
8
8
  function loadWhatsAppSendModule() {
9
- whatsAppSendModulePromise ??= import("./send-E_kMiuZP.js").then((n) => n.a);
9
+ whatsAppSendModulePromise ??= import("./send-C9jEw6hx.js").then((n) => n.a);
10
10
  return whatsAppSendModulePromise;
11
11
  }
12
12
  function normalizeOutboundText(text) {
@@ -1,2 +1,2 @@
1
- import { t as whatsappOutbound } from "./outbound-adapter-ltiZkW9L.js";
1
+ import { t as whatsappOutbound } from "./outbound-adapter-BObZXXzU.js";
2
2
  export { whatsappOutbound };
@@ -1,8 +1,8 @@
1
1
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
2
2
  //#region extensions/whatsapp/src/runtime.ts
3
- const { setRuntime: setWhatsAppRuntime, getRuntime: getWhatsAppRuntime } = createPluginRuntimeStore({
3
+ const { setRuntime: setWhatsAppRuntime, getRuntime: getWhatsAppRuntime, tryGetRuntime: getOptionalWhatsAppRuntime } = createPluginRuntimeStore({
4
4
  pluginId: "whatsapp",
5
5
  errorMessage: "WhatsApp runtime not initialized"
6
6
  });
7
7
  //#endregion
8
- export { setWhatsAppRuntime as n, getWhatsAppRuntime as t };
8
+ export { getWhatsAppRuntime as n, setWhatsAppRuntime as r, getOptionalWhatsAppRuntime as t };