@openclaw/whatsapp 2026.7.2-beta.2 → 2026.7.2-beta.3

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 (39) hide show
  1. package/dist/{access-control-DazDX3RB.js → access-control-tjy_em01.js} +1 -1
  2. package/dist/{accounts-CKOFMPf-.js → accounts-ejK8sXPU.js} +43 -3
  3. package/dist/action-runtime-api.js +1 -1
  4. package/dist/{action-runtime-Bf-12TIH.js → action-runtime-p0V390gA.js} +2 -2
  5. package/dist/action-runtime.runtime.js +1 -1
  6. package/dist/{active-listener-5kHap439.js → active-listener-BbdlXooT.js} +1 -1
  7. package/dist/api.js +10 -10
  8. package/dist/{approval-handler.runtime-CBiGQuZ5.js → approval-handler.runtime-BO1oJ-b8.js} +3 -3
  9. package/dist/{approval-reactions-BGQoz25s.js → approval-reactions-CrZFAyf-.js} +2 -2
  10. package/dist/call-tool-api.js +2 -2
  11. package/dist/{channel-DbadDUDD.js → channel-B2I4Opzb.js} +18 -9
  12. package/dist/channel-plugin-api.js +1 -1
  13. package/dist/{channel-react-action-PoRWfP9x.js → channel-react-action-CPRQE-s3.js} +3 -3
  14. package/dist/{channel.runtime-BDDy4Lhz.js → channel.runtime-CjkhQzk8.js} +4 -4
  15. package/dist/{channel.setup-BZ-po8fg.js → channel.setup-BMWE9LmN.js} +1 -1
  16. package/dist/{connection-controller-C1rwfWb4.js → connection-controller-HGaffeFb.js} +183 -22
  17. package/dist/{connection-controller-runtime-context-CbTsqmPT.js → connection-controller-runtime-context-9mEeAvu1.js} +10 -1
  18. package/dist/contract-api.js +1 -1
  19. package/dist/directory-config-BZ775eGX.js +196 -0
  20. package/dist/directory-contract-api.js +1 -1
  21. package/dist/{group-session-key-CLTZeH_F.js → group-session-key-Dh8WZun_.js} +1 -1
  22. package/dist/light-runtime-api.js +1 -1
  23. package/dist/{login-DGIoToX8.js → login-ifOXCBjo.js} +3 -3
  24. package/dist/{login-qr-X-CZHOk9.js → login-qr-DUHzVH_Y.js} +4 -4
  25. package/dist/login-qr-runtime.js +1 -1
  26. package/dist/{monitor-BZ8QvCeV.js → monitor-_jqDKBXj.js} +544 -295
  27. package/dist/question-reactions-C0qJ6wGA.js +142 -0
  28. package/dist/runtime-api.js +7 -7
  29. package/dist/{send-D9Ydnn4o.js → send-SB-4JrVZ.js} +2 -3
  30. package/dist/{send-api-W33lI2nR.js → send-api-BG0MgoFI.js} +3 -3
  31. package/dist/{setup-core-3aSOoK2T.js → setup-core-BbIgEl2M.js} +3 -3
  32. package/dist/{setup-finalize-DBf1jjBj.js → setup-finalize-Cc-4iWGj.js} +3 -3
  33. package/dist/setup-plugin-api.js +1 -1
  34. package/dist/{setup-surface-CiKGdNJv.js → setup-surface-DhnXEZir.js} +2 -2
  35. package/dist/{session-mxWFn0eS.js → socket-close-jNGyZY4T.js} +221 -1
  36. package/npm-shrinkwrap.json +3 -3
  37. package/package.json +4 -4
  38. package/dist/account-config-LiT9wWH9.js +0 -43
  39. package/dist/directory-config-CLBQrjtC.js +0 -37
@@ -1,31 +1,31 @@
1
- import { a as resolveWhatsAppAccount, s as resolveWhatsAppMediaMaxBytes } from "./accounts-CKOFMPf-.js";
2
- import { a as normalizeWhatsAppOutboundPayload, c as prepareWhatsAppOutboundMedia, r as sendReactionWhatsApp, s as normalizeWhatsAppPayloadTextPreservingIndentation, u as resolveWhatsAppReactionLevel } from "./send-D9Ydnn4o.js";
1
+ import { a as resolveWhatsAppAccount, s as resolveWhatsAppMediaMaxBytes } from "./accounts-ejK8sXPU.js";
2
+ import { a as normalizeWhatsAppOutboundPayload, c as prepareWhatsAppOutboundMedia, r as sendReactionWhatsApp, s as normalizeWhatsAppPayloadTextPreservingIndentation, u as resolveWhatsAppReactionLevel } from "./send-SB-4JrVZ.js";
3
3
  import { n as getWhatsAppRuntime } from "./runtime-BfAdAEYT.js";
4
- import { n as getWhatsAppConnectionController } from "./connection-controller-runtime-context-CbTsqmPT.js";
4
+ import { r as getWhatsAppConnectionController } from "./connection-controller-runtime-context-9mEeAvu1.js";
5
5
  import { a as resolveEquivalentWhatsAppDirectChatJids, c as toWhatsappJidWithLid, i as markdownToWhatsApp, n as isSelfChatMode, o as resolveJidToE164, r as jidToE164, s as toWhatsappJid } from "./targets-runtime-Bsh0HR9y.js";
6
6
  import { n as normalizeE164, t as convertMarkdownTables$1 } from "./text-runtime-BTGkMN0o.js";
7
- import { a as cacheInboundMessageMeta, i as buildQuotedMessageOptions, n as resolveWhatsAppGroupSessionRoute, o as lookupInboundMessageMeta, r as resolveWhatsAppLegacyGroupSessionKey } from "./group-session-key-CLTZeH_F.js";
7
+ import { a as cacheInboundMessageMeta, i as buildQuotedMessageOptions, n as resolveWhatsAppGroupSessionRoute, o as lookupInboundMessageMeta, r as resolveWhatsAppLegacyGroupSessionKey } from "./group-session-key-Dh8WZun_.js";
8
8
  import { g as readWebSelfId, o as getWebAuthAgeMs, r as WhatsAppAuthUnstableError, v as readWebSelfIdentityForDecision } from "./auth-store-BQi9pZqQ.js";
9
9
  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-D7L8LCuH.js";
10
10
  import { n as resolveWhatsAppGroupsConfigPath } from "./group-config-path-BGyzT9Lg.js";
11
- import { c as requireWhatsAppInboundAdmission, i as resolveWhatsAppInboundPolicy, l as resolveWhatsAppGroupConversationId, o as buildDeprecatedFlatWhatsAppInboundAdmission, r as resolveWhatsAppCommandAuthorized, s as requireAdmittedWhatsAppInboundMessage, t as checkInboundAccessControl } from "./access-control-DazDX3RB.js";
12
- import { _ as extractLocationData, a as mayContainWhatsAppOutboundMention, b as extractText, c as DisconnectReason, d as normalizeMessageContent, f as resolveInboundMediaMimetype, g as extractExternalAdReplyContext, h as extractContextInfo, i as addWhatsAppOutboundMentionsToContent, l as downloadMediaMessage, m as extractContactContext, n as listWhatsAppSendResultMessageIds, o as resolveWhatsAppOutboundMentions, p as describeReplyContext, r as normalizeWhatsAppSendResult, s as addWhatsAppImagePreviewFields, t as createWebSendApi, u as isJidGroup, v as extractMediaPlaceholder, x as hasInboundUserContent, y as extractMentionedJids } from "./send-api-W33lI2nR.js";
13
- import { a as createWhatsAppSocketOperationTimeoutAdapter, c as resolveWhatsAppSocketTiming, l as withWhatsAppSocketOperationTimeout, o as isWhatsAppSocketOperationTimeoutError, r as waitForWaConnection, s as resolveWhatsAppSocketOperationTimeoutMs, t as createWaSocket } from "./session-mxWFn0eS.js";
11
+ import { a as waitForWaConnection, c as isWhatsAppSocketOperationTimeoutError, d as withWhatsAppSocketOperationTimeout, l as resolveWhatsAppSocketOperationTimeoutMs, r as createWaSocket, s as createWhatsAppSocketOperationTimeoutAdapter, u as resolveWhatsAppSocketTiming } from "./socket-close-jNGyZY4T.js";
14
12
  import { n as getStatusCode, t as formatError } from "./session-errors-JuazczrA.js";
15
13
  import { t as BufferJSON } from "./session.runtime-Nsqaehbd.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-C1rwfWb4.js";
17
- import { n as maybeResolveWhatsAppApprovalReaction } from "./approval-reactions-BGQoz25s.js";
14
+ import { c as requireWhatsAppInboundAdmission, i as resolveWhatsAppInboundPolicy, l as resolveWhatsAppGroupConversationId, o as buildDeprecatedFlatWhatsAppInboundAdmission, r as resolveWhatsAppCommandAuthorized, s as requireAdmittedWhatsAppInboundMessage, t as checkInboundAccessControl } from "./access-control-tjy_em01.js";
15
+ import { _ as extractLocationData, a as mayContainWhatsAppOutboundMention, b as extractText, c as DisconnectReason, d as normalizeMessageContent, f as resolveInboundMediaMimetype, g as extractExternalAdReplyContext, h as extractContextInfo, i as addWhatsAppOutboundMentionsToContent, l as downloadMediaMessage, m as extractContactContext, n as listWhatsAppSendResultMessageIds, o as resolveWhatsAppOutboundMentions, p as describeReplyContext, r as normalizeWhatsAppSendResult, s as addWhatsAppImagePreviewFields, t as createWebSendApi, u as isJidGroup, v as extractMediaPlaceholder, x as hasInboundUserContent, y as extractMentionedJids } from "./send-api-BG0MgoFI.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-HGaffeFb.js";
17
+ import { n as maybeResolveWhatsAppApprovalReaction } from "./approval-reactions-CrZFAyf-.js";
18
+ import { t as maybeResolveWhatsAppQuestionReaction } from "./question-reactions-C0qJ6wGA.js";
18
19
  import { resolveAccountEntry } from "openclaw/plugin-sdk/account-core";
19
20
  import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
20
21
  import { filterSupplementalContextItems, resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
21
22
  import { DEFAULT_TIMING, createAckReactionHandle, createStatusReactionController, logAckFailure, removeAckReactionHandleAfterReply, shouldAckReactionForWhatsApp } from "openclaw/plugin-sdk/channel-feedback";
22
23
  import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
23
- import { collectErrorGraphCandidates, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
24
24
  import { getChildLogger } from "openclaw/plugin-sdk/logging-core";
25
25
  import { resolveMarkdownTableMode as resolveMarkdownTableMode$1 } from "openclaw/plugin-sdk/markdown-table-runtime";
26
26
  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";
27
27
  import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
28
- import { createChannelMessageReplyPipeline, createDurableInboundReceiveJournalFromQueue, createMessageReceiptFromOutboundResults, deliverInboundReplyWithMessageSendContext, resolveChannelMessageSourceReplyDeliveryMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
28
+ import { DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressDrain, createChannelMessageReplyPipeline, createMessageReceiptFromOutboundResults, deliverInboundReplyWithMessageSendContext, resolveChannelMessageSourceReplyDeliveryMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
29
29
  import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
30
30
  import { isReasoningReplyPayload, resolveSendableOutboundReplyParts, sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";
31
31
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -34,19 +34,17 @@ import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
34
34
  import { chunkMarkdownTextWithMode } from "openclaw/plugin-sdk/reply-chunking";
35
35
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, DEFAULT_MAIN_KEY, buildAgentMainSessionKey, buildAgentSessionKey, buildGroupHistoryKey, deriveLastRoutePolicy, normalizeAccountId as normalizeAccountId$1, normalizeAgentId, resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
36
36
  import { asDateTimestampMs, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
37
- import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
37
+ import { createHash } from "node:crypto";
38
38
  import { getSessionEntry, patchSessionEntry, resolveStorePath, resolveStorePath as resolveStorePath$1, updateLastRoute } from "openclaw/plugin-sdk/session-store-runtime";
39
- import { buildChannelInboundEventContext, filterChannelInboundQuoteContext, formatInboundEnvelope, formatInboundEnvelope as formatInboundEnvelope$1, formatInboundMediaUnavailableText, formatLocationText, hasVisibleInboundReplyDispatch, resolveInboundSessionEnvelopeContext, runChannelInboundEvent, shouldDebounceTextInbound, toInboundMediaFacts, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
39
+ import { buildChannelInboundEventContext, filterChannelInboundQuoteContext, formatInboundEnvelope, formatInboundEnvelope as formatInboundEnvelope$1, formatInboundMediaUnavailableText, formatLocationText, hasVisibleInboundReplyDispatch, resolveInboundSessionEnvelopeContext, resolveInboundSupplementalSenderAllowed, runChannelInboundEvent, shouldDebounceTextInbound, toInboundMediaFacts, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
40
40
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
41
- import { createHash } from "node:crypto";
42
- import { dispatchReplyWithBufferedBlockDispatcher, resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
41
+ import { resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
43
42
  import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-runtime";
44
43
  import { createInboundDebouncer, resolveInboundDebounceMs } from "openclaw/plugin-sdk/channel-inbound-debounce";
45
44
  import { drainPendingDeliveries } from "openclaw/plugin-sdk/delivery-queue-runtime";
46
45
  import { DEFAULT_GROUP_HISTORY_LIMIT, buildHistoryContextFromEntries, buildInboundHistoryFromEntries, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
47
46
  import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
48
47
  import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
49
- import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
50
48
  import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
51
49
  import { getRuntimeConfig as getRuntimeConfig$1, getRuntimeConfigSourceSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot";
52
50
  import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
@@ -60,50 +58,6 @@ import { createInternalHookEvent, deriveInboundMessageHookContext, fireAndForget
60
58
  import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
61
59
  import { resolveBatchedReplyThreadingPolicy } from "openclaw/plugin-sdk/reply-reference";
62
60
  import { createChannelApiRetryRunner } from "openclaw/plugin-sdk/retry-runtime";
63
- //#region extensions/whatsapp/src/inbound/dedupe.ts
64
- const WHATSAPP_INBOUND_DEDUPE_TTL_MS = 20 * 6e4;
65
- const RECENT_WEB_MESSAGE_MAX = 5e3;
66
- const RECENT_OUTBOUND_MESSAGE_TTL_MS = 20 * 6e4;
67
- const RECENT_OUTBOUND_MESSAGE_MAX = 5e3;
68
- const whatsAppInboundReplayGuard = createChannelReplayGuard({
69
- dedupe: {
70
- ttlMs: WHATSAPP_INBOUND_DEDUPE_TTL_MS,
71
- memoryMaxSize: RECENT_WEB_MESSAGE_MAX
72
- },
73
- buildReplayKey: (keys) => keys
74
- });
75
- const recentOutboundMessages = createDedupeCache({
76
- ttlMs: RECENT_OUTBOUND_MESSAGE_TTL_MS,
77
- maxSize: RECENT_OUTBOUND_MESSAGE_MAX
78
- });
79
- var WhatsAppRetryableInboundError = class extends Error {
80
- constructor(message, options) {
81
- super(message, options);
82
- this.name = "WhatsAppRetryableInboundError";
83
- }
84
- };
85
- function buildMessageKey(params) {
86
- const accountId = params.accountId.trim();
87
- const remoteJid = params.remoteJid.trim();
88
- const messageId = params.messageId.trim();
89
- if (!accountId || !remoteJid || !messageId || messageId === "unknown") return null;
90
- return `${accountId}:${remoteJid}:${messageId}`;
91
- }
92
- function resetWebInboundDedupe() {
93
- whatsAppInboundReplayGuard.clearMemory();
94
- recentOutboundMessages.clear();
95
- }
96
- function rememberRecentOutboundMessage(params) {
97
- const key = buildMessageKey(params);
98
- if (!key) return;
99
- recentOutboundMessages.check(key);
100
- }
101
- function isRecentOutboundMessage(params) {
102
- const key = buildMessageKey(params);
103
- if (!key) return false;
104
- return recentOutboundMessages.peek(key);
105
- }
106
- //#endregion
107
61
  //#region extensions/whatsapp/src/inbound/message-aliases.ts
108
62
  function normalizeQuoteSender(sender) {
109
63
  if (!sender?.displayName && !sender?.jid && !sender?.e164) return;
@@ -428,9 +382,63 @@ function normalizeWebInboundMessage(msg) {
428
382
  return normalizeLegacyFlatWebInboundMessage(msg);
429
383
  }
430
384
  //#endregion
385
+ //#region extensions/whatsapp/src/inbound/dedupe.ts
386
+ const recentOutboundMessages = createDedupeCache({
387
+ ttlMs: 20 * 6e4,
388
+ maxSize: 5e3
389
+ });
390
+ function buildMessageKey(params) {
391
+ const accountId = params.accountId.trim();
392
+ const remoteJid = params.remoteJid.trim();
393
+ const messageId = params.messageId.trim();
394
+ if (!accountId || !remoteJid || !messageId || messageId === "unknown") return null;
395
+ return `${accountId}:${remoteJid}:${messageId}`;
396
+ }
397
+ function resetWebInboundDedupe() {
398
+ recentOutboundMessages.clear();
399
+ }
400
+ function rememberRecentOutboundMessage(params) {
401
+ const key = buildMessageKey(params);
402
+ if (!key) return;
403
+ recentOutboundMessages.check(key);
404
+ }
405
+ function isRecentOutboundMessage(params) {
406
+ const key = buildMessageKey(params);
407
+ if (!key) return false;
408
+ return recentOutboundMessages.peek(key);
409
+ }
410
+ //#endregion
411
+ //#region extensions/whatsapp/src/inbound/durable-payload.ts
412
+ var WhatsAppIngressPermanentError = class extends Error {
413
+ constructor(reason, message, options) {
414
+ super(message, options);
415
+ this.reason = reason;
416
+ this.name = "WhatsAppIngressPermanentError";
417
+ }
418
+ };
419
+ function serializeWhatsAppDurableInboundMessage(message) {
420
+ const timestamp = message.messageTimestamp;
421
+ let serializedMessage = message;
422
+ if (timestamp != null && typeof timestamp === "object") try {
423
+ const numericTimestamp = Number(timestamp);
424
+ if (Number.isFinite(numericTimestamp)) serializedMessage = {
425
+ ...message,
426
+ messageTimestamp: numericTimestamp
427
+ };
428
+ } catch {}
429
+ return JSON.parse(JSON.stringify(serializedMessage, BufferJSON.replacer));
430
+ }
431
+ function deserializeWhatsAppDurableInboundMessage(message) {
432
+ try {
433
+ return JSON.parse(JSON.stringify(message), BufferJSON.reviver);
434
+ } catch (error) {
435
+ throw new WhatsAppIngressPermanentError("invalid-payload", "WhatsApp ingress row contains an invalid serialized message", { cause: error });
436
+ }
437
+ }
438
+ //#endregion
431
439
  //#region extensions/whatsapp/src/inbound/durable-receive.ts
432
440
  const WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES = 450;
433
- const WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES = 450;
441
+ const WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES = 5e3;
434
442
  const WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS = 720 * 60 * 60 * 1e3;
435
443
  const WHATSAPP_DURABLE_INBOUND_COMPLETED_TTL_MS = 10080 * 60 * 1e3;
436
444
  function hashNamespacePart(value) {
@@ -439,31 +447,91 @@ function hashNamespacePart(value) {
439
447
  function createWhatsAppDurableInboundMessageId(params) {
440
448
  return createHash("sha256").update(`${params.remoteJid}\n${params.id}`).digest("hex");
441
449
  }
442
- function serializeWhatsAppDurableInboundMessage(message) {
443
- return JSON.parse(JSON.stringify(message, BufferJSON.replacer));
444
- }
445
- function deserializeWhatsAppDurableInboundMessage(message) {
446
- return JSON.parse(JSON.stringify(message), BufferJSON.reviver);
447
- }
448
- function createWhatsAppDurableInboundReceiveJournal(accountId) {
449
- const accountPart = hashNamespacePart(accountId);
450
- const runtime = getWhatsAppRuntime();
451
- return createDurableInboundReceiveJournalFromQueue({
452
- queue: runtime.state.openChannelIngressQueue({
453
- accountId: accountPart,
454
- stateDir: runtime.state.resolveStateDir()
450
+ function inspectWhatsAppIngressMessage(message) {
451
+ const remoteJid = message.key?.remoteJid?.trim();
452
+ const id = message.key?.id?.trim();
453
+ if (!remoteJid || !id) throw new WhatsAppIngressPermanentError("missing-message-key", "WhatsApp ingress message is missing key.remoteJid or key.id");
454
+ return {
455
+ eventId: createWhatsAppDurableInboundMessageId({
456
+ remoteJid,
457
+ id
455
458
  }),
456
- retention: {
457
- pendingTtlMs: WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS,
458
- completedTtlMs: WHATSAPP_DURABLE_INBOUND_COMPLETED_TTL_MS,
459
- failedTtlMs: WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS,
460
- pendingMaxEntries: WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES,
461
- completedMaxEntries: WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES,
462
- failedMaxEntries: WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES
459
+ laneKey: remoteJid
460
+ };
461
+ }
462
+ /** Account-scoped queue shared with the pre-drain WhatsApp receive journal. */
463
+ function createWhatsAppDurableInboundQueue(accountId) {
464
+ return getWhatsAppRuntime().state.openChannelIngressQueue({
465
+ accountId: hashNamespacePart(accountId),
466
+ stateDir: getWhatsAppRuntime().state.resolveStateDir()
467
+ });
468
+ }
469
+ /** Raw receive chokepoint: append first, then let the drain normalize and dispatch. */
470
+ async function enqueueWhatsAppDurableInbound(params) {
471
+ const facts = inspectWhatsAppIngressMessage(params.message);
472
+ const receivedAt = params.receivedAt ?? Date.now();
473
+ await params.queue.prune({
474
+ pendingTtlMs: WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS,
475
+ completedTtlMs: WHATSAPP_DURABLE_INBOUND_COMPLETED_TTL_MS,
476
+ failedTtlMs: WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS,
477
+ pendingMaxEntries: WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES,
478
+ completedMaxEntries: WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES,
479
+ failedMaxEntries: WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES
480
+ });
481
+ return await params.queue.enqueue(facts.eventId, {
482
+ message: serializeWhatsAppDurableInboundMessage(params.message),
483
+ upsertType: params.upsertType,
484
+ ...params.skipStaleAppend === void 0 ? {} : { skipStaleAppend: params.skipStaleAppend },
485
+ ...params.skipRecentOutboundEcho === void 0 ? {} : { skipRecentOutboundEcho: params.skipRecentOutboundEcho },
486
+ receivedAt,
487
+ ...params.receiveOrder === void 0 ? {} : { receiveOrder: params.receiveOrder }
488
+ }, {
489
+ receivedAt,
490
+ laneKey: facts.laneKey
491
+ });
492
+ }
493
+ function resolveWhatsAppIngressNonRetryableFailure(error) {
494
+ return error instanceof WhatsAppIngressPermanentError ? {
495
+ reason: error.reason,
496
+ message: error.message
497
+ } : null;
498
+ }
499
+ /** Core drain with per-conversation lanes and completion at reply-lane adoption. */
500
+ function createWhatsAppIngressDrain(params) {
501
+ return createChannelIngressDrain({
502
+ queue: params.queue,
503
+ retryPolicy: {
504
+ maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
505
+ deadLetterMinAgeMs: DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS
506
+ },
507
+ resolveNonRetryableFailure: resolveWhatsAppIngressNonRetryableFailure,
508
+ deriveLaneKey: (record) => {
509
+ try {
510
+ return inspectWhatsAppIngressMessage(deserializeWhatsAppDurableInboundMessage(record.payload.message)).laneKey;
511
+ } catch {
512
+ return record.id;
513
+ }
514
+ },
515
+ ...params.onLog ? { onLog: params.onLog } : {},
516
+ ...params.abortSignal ? { abortSignal: params.abortSignal } : {},
517
+ dispatchClaimedEvent: async (record, lifecycle) => {
518
+ const message = deserializeWhatsAppDurableInboundMessage(record.payload.message);
519
+ if (inspectWhatsAppIngressMessage(message).eventId !== record.id) throw new WhatsAppIngressPermanentError("event-id-mismatch", "WhatsApp ingress row id does not match its transport message key");
520
+ return await params.dispatch(message, record.payload, lifecycle);
463
521
  }
464
522
  });
465
523
  }
466
524
  //#endregion
525
+ //#region extensions/whatsapp/src/inbound/ingress-lifecycle.ts
526
+ const ingressLifecycleKey = Symbol("whatsappIngressLifecycle");
527
+ function attachWhatsAppIngressLifecycle(message, lifecycle) {
528
+ if (lifecycle) message[ingressLifecycleKey] = lifecycle;
529
+ return message;
530
+ }
531
+ function resolveWhatsAppIngressLifecycle(message) {
532
+ return message[ingressLifecycleKey];
533
+ }
534
+ //#endregion
467
535
  //#region extensions/whatsapp/src/inbound/lifecycle.ts
468
536
  function attachEmitterListener(emitter, event, listener) {
469
537
  emitter.on(event, listener);
@@ -533,13 +601,8 @@ const RECONNECT_IN_PROGRESS_ERROR = "no active socket - reconnection in progress
533
601
  const GROUP_META_TTL_MS = 300 * 1e3;
534
602
  const BAILEYS_MESSAGE_TTL_MS = 600 * 1e3;
535
603
  const INBOUND_CLOSE_DRAIN_TIMEOUT_MS = 5e3;
536
- const REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE = /reply session initialization conflicted for \S+/u;
604
+ const WHATSAPP_INGRESS_DRAIN_INTERVAL_MS = 1e3;
537
605
  const WHATSAPP_GROUP_METADATA_CACHE_MAX_ENTRIES = 500;
538
- function resolveRetryableWhatsAppInboundError(error) {
539
- if (error instanceof WhatsAppRetryableInboundError) return error;
540
- if (!collectErrorGraphCandidates(error, (current) => [current.cause, current.error]).some((candidate) => REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE.test(formatErrorMessage(candidate)))) return null;
541
- return new WhatsAppRetryableInboundError(formatErrorMessage(error), { cause: error });
542
- }
543
606
  function resolveGroupMetadataExpiresAt(nowRaw = Date.now()) {
544
607
  const now = asDateTimestampMs(nowRaw);
545
608
  return now === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(GROUP_META_TTL_MS, { nowMs: now });
@@ -630,9 +693,6 @@ function isRetryableSendDisconnectError(err) {
630
693
  function shouldClearSocketRefAfterSendFailure(err) {
631
694
  return /closed|reset|disconnect|no active socket/i.test(formatError(err));
632
695
  }
633
- function isNonEmptyString(value) {
634
- return Boolean(value);
635
- }
636
696
  async function attachWebInboxToSocket(options) {
637
697
  const inboundLogger = getChildLogger({ module: "web-inbound" });
638
698
  const inboundConsoleLog = createSubsystemLogger("gateway/channels/whatsapp").child("inbound");
@@ -673,11 +733,30 @@ async function attachWebInboxToSocket(options) {
673
733
  if (!successorIdentity || !identitiesOverlap(self, successorIdentity)) return null;
674
734
  return successor.getCurrentSock();
675
735
  };
676
- const durableInboundJournal = createWhatsAppDurableInboundReceiveJournal(options.accountId);
736
+ const durableInboundQueue = options.durableInboundQueue ?? createWhatsAppDurableInboundQueue(options.accountId);
677
737
  const inboundDebounceMs = Math.max(0, Math.trunc(options.debounceMs ?? 0));
678
738
  const pendingDebounceKeys = /* @__PURE__ */ new Set();
679
739
  const activeInboundFlushes = /* @__PURE__ */ new Set();
680
740
  const pendingMessageHandlers = /* @__PURE__ */ new Set();
741
+ const debounceWorkWaiters = /* @__PURE__ */ new Set();
742
+ const notifyDebounceWork = () => {
743
+ if (debounceWorkWaiters.size === 0) return;
744
+ const waiters = [...debounceWorkWaiters];
745
+ debounceWorkWaiters.clear();
746
+ for (const resolve of waiters) resolve();
747
+ };
748
+ const waitForDebounceWorkOrIdle = (handlers) => {
749
+ if (pendingDebounceKeys.size > 0 || activeInboundFlushes.size > 0) return Promise.resolve();
750
+ if (pendingMessageHandlers.size === 0) return Promise.resolve();
751
+ return new Promise((resolve) => {
752
+ const finish = () => {
753
+ debounceWorkWaiters.delete(finish);
754
+ resolve();
755
+ };
756
+ debounceWorkWaiters.add(finish);
757
+ Promise.allSettled(handlers).then(finish);
758
+ });
759
+ };
681
760
  let nextReceiveOrder = 0;
682
761
  const publishPendingWorkState = (at = Date.now()) => {
683
762
  options.onPendingWorkChanged?.(pendingMessageHandlers.size + pendingDebounceKeys.size + activeInboundFlushes.size, at);
@@ -695,18 +774,47 @@ async function attachWebInboxToSocket(options) {
695
774
  if (timestampDiff !== 0) return timestampDiff;
696
775
  return (a.receiveOrder ?? 0) - (b.receiveOrder ?? 0);
697
776
  });
698
- const finalizeInboundDelivery = async (entries, error) => {
699
- const replayClaims = entries.map((entry) => entry.replayClaim).filter((claim) => claim !== void 0);
700
- const durableEntries = entries.filter((entry) => isNonEmptyString(entry.durableId));
701
- const readReceiptEntries = entries.filter((entry) => Boolean(entry.readReceipt));
702
- const retryableError = resolveRetryableWhatsAppInboundError(error);
703
- if (retryableError) {
704
- for (const claim of replayClaims) claim.release({ error: retryableError });
705
- await Promise.all(durableEntries.map((entry) => durableInboundJournal.release(entry.durableId, { lastError: formatError(retryableError) })));
706
- return;
707
- }
708
- await Promise.all([...replayClaims.map((claim) => claim.commit()), ...durableEntries.map((entry) => durableInboundJournal.complete(entry.durableId, entry.readReceipt ? { metadata: { readReceipt: entry.readReceipt } } : void 0))]);
709
- await Promise.all(readReceiptEntries.map((entry) => maybeMarkInboundAsRead(entry.readReceipt)));
777
+ const buildFlushIngressLifecycle = (entries) => {
778
+ const lifecycles = entries.map((entry) => entry.turnAdoptionLifecycle).filter((lifecycle) => lifecycle !== void 0);
779
+ const [firstLifecycle] = lifecycles;
780
+ if (!firstLifecycle) return {
781
+ lifecycle: void 0,
782
+ settle: async () => {},
783
+ abandon: async () => {}
784
+ };
785
+ let handedOff = false;
786
+ const adoptAll = async () => {
787
+ for (const lifecycle of lifecycles) await lifecycle.onAdopted();
788
+ };
789
+ return {
790
+ lifecycle: {
791
+ abortSignal: lifecycles.length === 1 ? firstLifecycle.abortSignal : AbortSignal.any(lifecycles.map((lifecycle) => lifecycle.abortSignal)),
792
+ onAdopted: async () => {
793
+ handedOff = true;
794
+ await adoptAll();
795
+ },
796
+ onDeferred: () => {
797
+ handedOff = true;
798
+ for (const lifecycle of lifecycles) lifecycle.onDeferred();
799
+ },
800
+ onAdoptionFinalizing: () => {
801
+ for (const lifecycle of lifecycles) lifecycle.onAdoptionFinalizing();
802
+ },
803
+ onAbandoned: async () => {
804
+ handedOff = true;
805
+ await Promise.all(lifecycles.map((lifecycle) => Promise.resolve(lifecycle.onAbandoned())));
806
+ }
807
+ },
808
+ settle: async () => {
809
+ if (!handedOff) await adoptAll();
810
+ },
811
+ abandon: async () => {
812
+ if (!handedOff) {
813
+ handedOff = true;
814
+ await Promise.all(lifecycles.map((lifecycle) => Promise.resolve(lifecycle.onAbandoned())));
815
+ }
816
+ }
817
+ };
710
818
  };
711
819
  const debouncer = createInboundDebouncer({
712
820
  debounceMs: inboundDebounceMs,
@@ -719,14 +827,17 @@ async function attachWebInboxToSocket(options) {
719
827
  });
720
828
  activeInboundFlushes.add(flushTask);
721
829
  publishPendingWorkState();
830
+ notifyDebounceWork();
722
831
  try {
723
832
  const orderedEntries = orderDebouncedInboundEntries(entries);
724
833
  const last = orderedEntries.at(-1);
725
834
  if (!last) return;
835
+ const { lifecycle, settle, abandon } = buildFlushIngressLifecycle(orderedEntries);
726
836
  try {
727
837
  if (orderedEntries.length === 1) {
728
- await options.onMessage(last);
729
- await finalizeInboundDelivery(orderedEntries);
838
+ await options.onMessage(attachWhatsAppIngressLifecycle(last, lifecycle));
839
+ await settle();
840
+ await Promise.all(orderedEntries.map((entry) => maybeMarkInboundAsRead(entry.readReceipt)));
730
841
  return;
731
842
  }
732
843
  const mentioned = /* @__PURE__ */ new Set();
@@ -741,8 +852,9 @@ async function attachWebInboxToSocket(options) {
741
852
  ...last.group,
742
853
  mentions: combinedMentions
743
854
  } : void 0;
744
- const combinedMessage = withDeprecatedWebInboundMessageFlatAliases({
855
+ const combinedMessage = attachWhatsAppIngressLifecycle(withDeprecatedWebInboundMessageFlatAliases({
745
856
  ...last,
857
+ ...lifecycle ? { turnAdoptionLifecycle: lifecycle } : {},
746
858
  payload: {
747
859
  ...last.payload,
748
860
  body: combinedBody,
@@ -753,11 +865,12 @@ async function attachWebInboxToSocket(options) {
753
865
  ...last.event,
754
866
  isBatched: true
755
867
  }
756
- });
868
+ }), lifecycle);
757
869
  await options.onMessage(combinedMessage);
758
- await finalizeInboundDelivery(orderedEntries);
870
+ await settle();
871
+ await Promise.all(orderedEntries.map((entry) => maybeMarkInboundAsRead(entry.readReceipt)));
759
872
  } catch (error) {
760
- await finalizeInboundDelivery(orderedEntries, error);
873
+ await abandon();
761
874
  throw error;
762
875
  }
763
876
  } finally {
@@ -972,20 +1085,24 @@ async function attachWebInboxToSocket(options) {
972
1085
  }
973
1086
  return content;
974
1087
  };
1088
+ const shouldSkipRecentOutboundEcho = (msg) => {
1089
+ const id = msg.key?.id ?? void 0;
1090
+ const remoteJid = msg.key?.remoteJid;
1091
+ if (!msg.key?.fromMe || !id || !remoteJid || !isRecentOutboundMessage({
1092
+ accountId: options.accountId,
1093
+ remoteJid,
1094
+ messageId: id
1095
+ })) return false;
1096
+ logWhatsAppVerbose(options.verbose, `Skipping recent outbound WhatsApp echo ${id} for ${remoteJid}`);
1097
+ return true;
1098
+ };
975
1099
  const normalizeInboundMessage = async (msg) => {
976
1100
  const id = msg.key?.id ?? void 0;
977
1101
  const remoteJid = msg.key?.remoteJid;
978
1102
  if (!remoteJid) return null;
979
1103
  if (remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) return null;
980
1104
  const group = isJidGroup(remoteJid) === true;
981
- if (Boolean(msg.key?.fromMe) && id && isRecentOutboundMessage({
982
- accountId: options.accountId,
983
- remoteJid,
984
- messageId: id
985
- })) {
986
- logWhatsAppVerbose(options.verbose, `Skipping recent outbound WhatsApp echo ${id} for ${remoteJid}`);
987
- return null;
988
- }
1105
+ if (shouldSkipRecentOutboundEcho(msg)) return null;
989
1106
  if (!hasInboundUserContent(msg.message ?? void 0)) return null;
990
1107
  const participantJid = msg.key?.participant ?? void 0;
991
1108
  const from = group ? remoteJid : await resolveInboundJid(remoteJid);
@@ -1061,15 +1178,6 @@ async function attachWebInboxToSocket(options) {
1061
1178
  }
1062
1179
  await maybeMarkInboundAsRead(target);
1063
1180
  };
1064
- const completeUndeliverableDurableInbound = async (durableId, metadata) => {
1065
- if (!durableId) return;
1066
- await durableInboundJournal.complete(durableId, metadata?.readReceipt ? { metadata: { readReceipt: metadata.readReceipt } } : void 0);
1067
- };
1068
- const buildDurableInboundPayload = (msg, upsertType) => ({
1069
- message: serializeWhatsAppDurableInboundMessage(msg),
1070
- ...upsertType ? { upsertType } : {},
1071
- receivedAt: Date.now()
1072
- });
1073
1181
  const shouldSkipStaleAppend = (msg, upsertType) => {
1074
1182
  if (upsertType !== "append") return false;
1075
1183
  const APPEND_RECENT_GRACE_MS = 6e4;
@@ -1078,70 +1186,7 @@ async function attachWebInboxToSocket(options) {
1078
1186
  const nowMs = Date.now();
1079
1187
  return msgTsMs < (options.appendReplyWindow && nowMs <= options.appendReplyWindow.untilMs ? Math.max(options.appendReplyWindow.afterMs, nowMs - options.appendReplyWindow.maxAgeMs) : connectedAtMs - APPEND_RECENT_GRACE_MS);
1080
1188
  };
1081
- const processDurableInboundMessage = async (msg, upsertType, receiveOrder, stored) => {
1082
- const inbound = await normalizeInboundMessage(msg);
1083
- if (!inbound) {
1084
- if (stored) await completeUndeliverableDurableInbound(stored.id, stored.metadata);
1085
- return;
1086
- }
1087
- const readReceipt = stored?.metadata?.readReceipt ?? buildReadReceiptTarget(inbound);
1088
- const deliveryReadReceipt = inbound.access.isSelfChat ? void 0 : readReceipt;
1089
- if (!stored && shouldSkipStaleAppend(msg, upsertType)) {
1090
- await maybeMarkNonSelfChatReadReceipt(inbound, readReceipt);
1091
- return;
1092
- }
1093
- let durableId = stored?.id ?? (inbound.id ? createWhatsAppDurableInboundMessageId({
1094
- remoteJid: inbound.remoteJid,
1095
- id: inbound.id
1096
- }) : void 0);
1097
- const durableMetadata = deliveryReadReceipt ? { readReceipt: deliveryReadReceipt } : void 0;
1098
- if (durableId && !stored) try {
1099
- const accepted = await durableInboundJournal.accept(durableId, buildDurableInboundPayload(msg, upsertType), {
1100
- metadata: durableMetadata,
1101
- receivedAt: inbound.messageTimestampMs
1102
- });
1103
- if (accepted.kind === "completed") {
1104
- await maybeMarkNonSelfChatReadReceipt(inbound, accepted.record.metadata?.readReceipt ?? deliveryReadReceipt);
1105
- return;
1106
- }
1107
- if (accepted.kind === "pending" && accepted.record.attempts === 0) return;
1108
- } catch (err) {
1109
- durableId = void 0;
1110
- const error = formatError(err);
1111
- inboundLogger.warn({ error }, "failed persisting durable WhatsApp inbound; delivering live");
1112
- inboundConsoleLog.warn(`Failed persisting durable WhatsApp inbound; delivering live: ${error}`);
1113
- }
1114
- const enriched = await enrichInboundMessage(msg);
1115
- if (!enriched) {
1116
- await completeUndeliverableDurableInbound(durableId, durableMetadata);
1117
- await maybeMarkNonSelfChatReadReceipt(inbound, deliveryReadReceipt);
1118
- return;
1119
- }
1120
- const dedupeKey = inbound.id ? `${options.accountId}:${inbound.remoteJid}:${inbound.id}` : "";
1121
- const dedupeClaim = dedupeKey ? await whatsAppInboundReplayGuard.claim(dedupeKey) : { kind: "invalid" };
1122
- if (dedupeClaim.kind === "duplicate" || dedupeClaim.kind === "inflight") {
1123
- if (dedupeClaim.kind === "duplicate") {
1124
- await completeUndeliverableDurableInbound(durableId, durableMetadata);
1125
- await maybeMarkNonSelfChatReadReceipt(inbound, deliveryReadReceipt);
1126
- }
1127
- return;
1128
- }
1129
- recordAcceptedInboundActivity(options.accountId);
1130
- await enqueueInboundMessage(msg, inbound, enriched, {
1131
- durableId,
1132
- readReceipt: deliveryReadReceipt,
1133
- receiveOrder,
1134
- ...dedupeClaim.kind === "claimed" ? { replayClaim: dedupeClaim.handle } : {}
1135
- });
1136
- };
1137
- const replayPendingDurableInboundMessages = async () => {
1138
- const pending = await durableInboundJournal.pending();
1139
- for (const record of pending) await processDurableInboundMessage(deserializeWhatsAppDurableInboundMessage(record.payload.message), record.payload.upsertType, record.payload.receivedAt, {
1140
- id: record.id,
1141
- payload: record.payload,
1142
- metadata: record.metadata
1143
- });
1144
- };
1189
+ const preparedInboundByDurableId = /* @__PURE__ */ new Map();
1145
1190
  const enrichInboundMessage = async (msg) => {
1146
1191
  const location = extractLocationData(msg.message ?? void 0);
1147
1192
  const locationText = location ? formatLocationText(location) : void 0;
@@ -1296,8 +1341,7 @@ async function attachWebInboxToSocket(options) {
1296
1341
  }
1297
1342
  } : void 0,
1298
1343
  group,
1299
- replayClaim: durable.replayClaim,
1300
- durableId: durable.durableId,
1344
+ turnAdoptionLifecycle: durable.turnAdoptionLifecycle,
1301
1345
  readReceipt: durable.readReceipt,
1302
1346
  receiveOrder: durable.receiveOrder
1303
1347
  });
@@ -1307,6 +1351,7 @@ async function attachWebInboxToSocket(options) {
1307
1351
  if (inboundDebounceMs > 0 && shouldDebounceInboundMessage(inboundMessage)) {
1308
1352
  pendingDebounceKeys.add(debounceKey);
1309
1353
  publishPendingWorkState();
1354
+ notifyDebounceWork();
1310
1355
  }
1311
1356
  }
1312
1357
  if (inboundMessage.event.id) {
@@ -1318,16 +1363,119 @@ async function attachWebInboxToSocket(options) {
1318
1363
  fromMe: inboundMessage.platform.fromMe
1319
1364
  });
1320
1365
  }
1321
- try {
1322
- Promise.resolve(debouncer.enqueue(inboundMessage)).catch((err) => {
1323
- inboundLogger.error({ error: String(err) }, "failed handling inbound web message");
1324
- inboundConsoleLog.error(`Failed handling inbound web message: ${String(err)}`);
1366
+ await debouncer.enqueue(inboundMessage);
1367
+ };
1368
+ const processWhatsAppInboundMessage = async (msg, context) => {
1369
+ rememberBaileysMessage(msg.key?.remoteJid, msg.key?.id, msg.message);
1370
+ if (context.skipRecentOutboundEcho === true) return "completed";
1371
+ const preparation = context.eventId ? preparedInboundByDurableId.get(context.eventId) : void 0;
1372
+ if (context.eventId) preparedInboundByDurableId.delete(context.eventId);
1373
+ const prepared = await preparation;
1374
+ if (prepared === null) return "completed";
1375
+ const inbound = prepared ?? await normalizeInboundMessage(msg);
1376
+ if (!inbound) return "completed";
1377
+ if (await maybeResolveWhatsAppQuestionReaction({
1378
+ cfg: options.loadConfig?.() ?? options.cfg,
1379
+ accountId: options.accountId,
1380
+ msg,
1381
+ senderId: inbound.senderE164 ?? inbound.from,
1382
+ resolveReactionTargetJids,
1383
+ logDebug: (message) => logWhatsAppVerbose(options.verbose, message)
1384
+ })) return "completed";
1385
+ const readReceipt = buildReadReceiptTarget(inbound);
1386
+ const deliveryReadReceipt = inbound.access.isSelfChat ? void 0 : readReceipt;
1387
+ if (context.skipStaleAppend === true) {
1388
+ await maybeMarkNonSelfChatReadReceipt(inbound, readReceipt);
1389
+ return "completed";
1390
+ }
1391
+ const enriched = await enrichInboundMessage(msg);
1392
+ if (!enriched) {
1393
+ await maybeMarkNonSelfChatReadReceipt(inbound, deliveryReadReceipt);
1394
+ return "completed";
1395
+ }
1396
+ recordAcceptedInboundActivity(options.accountId);
1397
+ await enqueueInboundMessage(msg, inbound, enriched, {
1398
+ readReceipt: deliveryReadReceipt,
1399
+ receiveOrder: context.receiveOrder ?? context.receivedAt,
1400
+ turnAdoptionLifecycle: context.lifecycle
1401
+ });
1402
+ return "deferred";
1403
+ };
1404
+ let durableDrainPass;
1405
+ let durableDrainRequested = false;
1406
+ let durableDrainClosed = false;
1407
+ const requestDurableInboundDrain = () => {
1408
+ if (durableDrainClosed) return Promise.resolve();
1409
+ durableDrainRequested = true;
1410
+ if (!durableDrainPass) {
1411
+ let passFailed = false;
1412
+ const task = (async () => {
1413
+ await durableInboundDrain.recoverStaleClaims();
1414
+ while (durableDrainRequested) {
1415
+ durableDrainRequested = false;
1416
+ if ((await durableInboundDrain.drainOnce()).started > 0) {
1417
+ await durableInboundDrain.waitForIdle();
1418
+ durableDrainRequested = true;
1419
+ }
1420
+ }
1421
+ })().catch((error) => {
1422
+ passFailed = true;
1423
+ throw error;
1325
1424
  });
1326
- } catch (err) {
1327
- inboundLogger.error({ error: String(err) }, "failed handling inbound web message");
1328
- inboundConsoleLog.error(`Failed handling inbound web message: ${String(err)}`);
1425
+ durableDrainPass = task;
1426
+ pendingMessageHandlers.add(task);
1427
+ publishPendingWorkState();
1428
+ task.finally(() => {
1429
+ if (durableDrainPass === task) durableDrainPass = void 0;
1430
+ pendingMessageHandlers.delete(task);
1431
+ publishPendingWorkState();
1432
+ if (durableDrainRequested && !passFailed) requestDurableInboundDrain().catch((error) => {
1433
+ inboundLogger.error({ error: formatError(error) }, "whatsapp durable inbound drain failed");
1434
+ });
1435
+ }).catch(() => void 0);
1329
1436
  }
1437
+ return durableDrainPass;
1438
+ };
1439
+ const requestDrainAfterLaneSettlement = () => {
1440
+ requestDurableInboundDrain().catch((error) => {
1441
+ inboundLogger.error({ error: formatError(error) }, "whatsapp durable inbound drain failed");
1442
+ });
1330
1443
  };
1444
+ const continueDrainAfterLaneSettlement = (lifecycle) => ({
1445
+ abortSignal: lifecycle.abortSignal,
1446
+ onAdopted: async () => {
1447
+ await lifecycle.onAdopted();
1448
+ requestDrainAfterLaneSettlement();
1449
+ },
1450
+ onDeferred: lifecycle.onDeferred,
1451
+ onAdoptionFinalizing: lifecycle.onAdoptionFinalizing,
1452
+ onAbandoned: async () => {
1453
+ await lifecycle.onAbandoned();
1454
+ requestDrainAfterLaneSettlement();
1455
+ }
1456
+ });
1457
+ const durableInboundDrain = createWhatsAppIngressDrain({
1458
+ queue: durableInboundQueue,
1459
+ dispatch: async (msg, payload, lifecycle) => {
1460
+ const remoteJid = msg.key?.remoteJid;
1461
+ const id = msg.key?.id;
1462
+ return { kind: await processWhatsAppInboundMessage(msg, {
1463
+ ...payload,
1464
+ ...remoteJid && id ? { eventId: createWhatsAppDurableInboundMessageId({
1465
+ remoteJid,
1466
+ id
1467
+ }) } : {},
1468
+ lifecycle: continueDrainAfterLaneSettlement(lifecycle)
1469
+ }) };
1470
+ },
1471
+ onLog: (message) => inboundLogger.warn({ message }, "whatsapp ingress drain")
1472
+ });
1473
+ const durableDrainTimer = setInterval(() => {
1474
+ requestDurableInboundDrain().catch((error) => {
1475
+ inboundLogger.error({ error: formatError(error) }, "whatsapp durable inbound drain failed");
1476
+ });
1477
+ }, WHATSAPP_INGRESS_DRAIN_INTERVAL_MS);
1478
+ durableDrainTimer.unref?.();
1331
1479
  const handleMessagesUpsert = async (upsert) => {
1332
1480
  if (upsert.type !== "notify" && upsert.type !== "append") return;
1333
1481
  for (const msg of upsert.messages ?? []) {
@@ -1343,7 +1491,78 @@ async function attachWebInboxToSocket(options) {
1343
1491
  resolveReactionTargetJids,
1344
1492
  logVerboseMessage: (message) => logWhatsAppVerbose(options.verbose, message)
1345
1493
  })) continue;
1346
- await processDurableInboundMessage(msg, upsert.type, receiveOrder);
1494
+ const receivedAt = Date.now();
1495
+ const skipStaleAppend = shouldSkipStaleAppend(msg, upsert.type);
1496
+ const skipRecentOutboundEcho = shouldSkipRecentOutboundEcho(msg);
1497
+ const remoteJid = msg.key?.remoteJid;
1498
+ const id = msg.key?.id;
1499
+ const durableId = remoteJid && id ? createWhatsAppDurableInboundMessageId({
1500
+ remoteJid,
1501
+ id
1502
+ }) : void 0;
1503
+ let resolvePrepared;
1504
+ if (durableId && !preparedInboundByDurableId.has(durableId)) {
1505
+ if (preparedInboundByDurableId.size >= 1e3) {
1506
+ const oldest = preparedInboundByDurableId.keys().next().value;
1507
+ if (oldest !== void 0) preparedInboundByDurableId.delete(oldest);
1508
+ }
1509
+ preparedInboundByDurableId.set(durableId, new Promise((resolve) => {
1510
+ resolvePrepared = resolve;
1511
+ }));
1512
+ }
1513
+ const finishPreparation = (inbound, keepForDrain = false) => {
1514
+ resolvePrepared?.(inbound);
1515
+ if (!keepForDrain && durableId && resolvePrepared) preparedInboundByDurableId.delete(durableId);
1516
+ };
1517
+ let result;
1518
+ let appendError;
1519
+ for (const delayMs of [
1520
+ 0,
1521
+ 100,
1522
+ 300
1523
+ ]) {
1524
+ if (delayMs > 0) await new Promise((resolve) => {
1525
+ setTimeout(resolve, delayMs);
1526
+ });
1527
+ try {
1528
+ result = await enqueueWhatsAppDurableInbound({
1529
+ queue: durableInboundQueue,
1530
+ message: msg,
1531
+ upsertType: upsert.type,
1532
+ skipStaleAppend,
1533
+ skipRecentOutboundEcho,
1534
+ receivedAt,
1535
+ receiveOrder
1536
+ });
1537
+ appendError = void 0;
1538
+ break;
1539
+ } catch (error) {
1540
+ appendError = error;
1541
+ }
1542
+ }
1543
+ if (result === void 0) {
1544
+ finishPreparation(void 0);
1545
+ const formattedError = formatError(appendError);
1546
+ inboundLogger.error({ error: formattedError }, "failed persisting durable WhatsApp inbound after retries; message dropped");
1547
+ inboundConsoleLog.error(`Failed persisting durable WhatsApp inbound after retries; message dropped: ${formattedError}`);
1548
+ continue;
1549
+ }
1550
+ if (result.kind === "completed") {
1551
+ finishPreparation(void 0);
1552
+ const inbound = await normalizeInboundMessage(msg);
1553
+ if (inbound) await maybeMarkNonSelfChatReadReceipt(inbound, buildReadReceiptTarget(inbound));
1554
+ } else {
1555
+ if (result.kind === "accepted") try {
1556
+ finishPreparation(skipRecentOutboundEcho ? null : await normalizeInboundMessage(msg), true);
1557
+ } catch (error) {
1558
+ finishPreparation(void 0);
1559
+ throw error;
1560
+ }
1561
+ else finishPreparation(void 0);
1562
+ requestDurableInboundDrain().catch((error) => {
1563
+ inboundLogger.error({ error: formatError(error) }, "whatsapp durable inbound drain failed");
1564
+ });
1565
+ }
1347
1566
  }
1348
1567
  };
1349
1568
  const handleMessagesUpsertEvent = (upsert) => {
@@ -1358,9 +1577,6 @@ async function attachWebInboxToSocket(options) {
1358
1577
  publishPendingWorkState();
1359
1578
  });
1360
1579
  };
1361
- const waitForPendingMessageHandlers = async () => {
1362
- while (pendingMessageHandlers.size > 0) await Promise.all(Array.from(pendingMessageHandlers));
1363
- };
1364
1580
  const drainDebouncedInboundMessages = async () => {
1365
1581
  while (pendingDebounceKeys.size > 0 || activeInboundFlushes.size > 0) {
1366
1582
  const debounceKeys = Array.from(pendingDebounceKeys);
@@ -1372,8 +1588,21 @@ async function attachWebInboxToSocket(options) {
1372
1588
  };
1373
1589
  const drainInboundBeforeSocketClose = async () => {
1374
1590
  groupMetadataCacheClosed = true;
1375
- await waitForPendingMessageHandlers();
1591
+ clearInterval(durableDrainTimer);
1592
+ for (;;) {
1593
+ await drainDebouncedInboundMessages();
1594
+ if (pendingMessageHandlers.size === 0) break;
1595
+ const handlers = Array.from(pendingMessageHandlers);
1596
+ await Promise.race([Promise.allSettled(handlers), waitForDebounceWorkOrIdle(handlers)]);
1597
+ if (pendingMessageHandlers.size === 0 && pendingDebounceKeys.size === 0 && activeInboundFlushes.size === 0) break;
1598
+ }
1376
1599
  await drainDebouncedInboundMessages();
1600
+ try {
1601
+ await durableInboundDrain.waitForIdle();
1602
+ } finally {
1603
+ durableDrainClosed = true;
1604
+ durableInboundDrain.dispose();
1605
+ }
1377
1606
  };
1378
1607
  const drainInboundBeforeSocketCloseWithTimeout = async () => {
1379
1608
  let timeout = null;
@@ -1386,6 +1615,8 @@ async function attachWebInboxToSocket(options) {
1386
1615
  })]);
1387
1616
  } finally {
1388
1617
  if (timeout) clearTimeout(timeout);
1618
+ durableDrainClosed = true;
1619
+ durableInboundDrain.dispose();
1389
1620
  }
1390
1621
  };
1391
1622
  const handleConnectionUpdate = (update) => {
@@ -1444,16 +1675,10 @@ async function attachWebInboxToSocket(options) {
1444
1675
  const detachGroupParticipantsUpdate = attachSockListener("group-participants.update", ((update) => {
1445
1676
  forgetFullGroupMetadata(update.id);
1446
1677
  }));
1447
- const replayTask = replayPendingDurableInboundMessages().catch((err) => {
1678
+ requestDurableInboundDrain().catch((err) => {
1448
1679
  inboundLogger.error({ error: String(err) }, "failed replaying durable WhatsApp inbound");
1449
1680
  inboundConsoleLog.error(`Failed replaying durable WhatsApp inbound: ${String(err)}`);
1450
1681
  });
1451
- pendingMessageHandlers.add(replayTask);
1452
- publishPendingWorkState();
1453
- replayTask.finally(() => {
1454
- pendingMessageHandlers.delete(replayTask);
1455
- publishPendingWorkState();
1456
- });
1457
1682
  (async () => {
1458
1683
  try {
1459
1684
  const groups = await sock.groupFetchAllParticipating();
@@ -2505,25 +2730,34 @@ function isWhatsAppSupplementalSenderAllowed(params) {
2505
2730
  return false;
2506
2731
  }
2507
2732
  function resolveVisibleWhatsAppGroupHistory(params) {
2508
- if (params.groupPolicy !== "allowlist") return params.history;
2509
2733
  return filterSupplementalContextItems({
2510
2734
  items: params.history,
2511
2735
  mode: params.mode,
2512
2736
  kind: "history",
2513
- isSenderAllowed: (entry) => isWhatsAppSupplementalSenderAllowed({
2737
+ isSenderAllowed: (entry) => resolveInboundSupplementalSenderAllowed({
2738
+ isGroup: true,
2739
+ groupPolicy: params.groupPolicy,
2514
2740
  allowFrom: params.groupAllowFrom,
2515
- authDir: params.authDir,
2516
- sender: entry.senderJid ? { jid: entry.senderJid } : null
2741
+ isSenderAllowed: (allowFrom) => isWhatsAppSupplementalSenderAllowed({
2742
+ allowFrom,
2743
+ authDir: params.authDir,
2744
+ sender: entry.senderJid ? { jid: entry.senderJid } : null
2745
+ })
2517
2746
  })
2518
2747
  }).items;
2519
2748
  }
2520
2749
  function resolveVisibleWhatsAppReplyContext(params) {
2521
2750
  const replyTo = getReplyContext(params.msg, params.authDir);
2522
2751
  if (!replyTo) return null;
2523
- const senderAllowed = requireWhatsAppInboundAdmission(params.msg).conversation.kind !== "group" || params.groupPolicy !== "allowlist" ? true : isWhatsAppSupplementalSenderAllowed({
2752
+ const senderAllowed = resolveInboundSupplementalSenderAllowed({
2753
+ isGroup: requireWhatsAppInboundAdmission(params.msg).conversation.kind === "group",
2754
+ groupPolicy: params.groupPolicy,
2524
2755
  allowFrom: params.groupAllowFrom,
2525
- authDir: params.authDir,
2526
- sender: replyTo.sender
2756
+ isSenderAllowed: (allowFrom) => isWhatsAppSupplementalSenderAllowed({
2757
+ allowFrom,
2758
+ authDir: params.authDir,
2759
+ sender: replyTo.sender
2760
+ })
2527
2761
  });
2528
2762
  return filterChannelInboundQuoteContext(params.mode, {
2529
2763
  id: replyTo.id,
@@ -2686,6 +2920,7 @@ async function buildWhatsAppInboundContext(params) {
2686
2920
  },
2687
2921
  route: {
2688
2922
  agentId: params.route.agentId,
2923
+ dmScope: params.route.dmScope,
2689
2924
  accountId: params.route.accountId,
2690
2925
  routeSessionKey: params.route.sessionKey
2691
2926
  },
@@ -2754,8 +2989,9 @@ function updateWhatsAppMainLastRoute(params) {
2754
2989
  }
2755
2990
  if (params.dmRouteTarget && inboundLastRouteSessionKey === params.route.mainSessionKey && params.pinnedMainDmRecipient) logVerbose$1(`Skipping main-session last route update for ${params.dmRouteTarget} (pinned owner ${params.pinnedMainDmRecipient})`);
2756
2991
  }
2757
- async function dispatchWhatsAppBufferedReply(params) {
2992
+ function createWhatsAppReplyPlan(params) {
2758
2993
  const admission = requireWhatsAppInboundAdmission(params.msg);
2994
+ const ingressLifecycle = resolveWhatsAppIngressLifecycle(params.msg);
2759
2995
  const conversationId = admission.conversation.id;
2760
2996
  const conversationKind = admission.conversation.kind;
2761
2997
  const statusReactionController = params.statusReactionController ?? null;
@@ -2821,11 +3057,10 @@ async function dispatchWhatsAppBufferedReply(params) {
2821
3057
  const mediaOnlyCoalescer = createWhatsAppMediaOnlyReplyCoalescer({ deliver: async (pending) => {
2822
3058
  return await deliverNormalizedPayload(pending.payload, pending.info);
2823
3059
  } });
2824
- if (statusReactionController) statusReactionController.setThinking();
2825
- const dispatchResult = await dispatchReplyWithBufferedBlockDispatcher({
2826
- ctx: params.context,
2827
- cfg: params.cfg,
2828
- replyResolver: params.replyResolver,
3060
+ return {
3061
+ afterRecord: () => {
3062
+ if (statusReactionController) statusReactionController.setThinking();
3063
+ },
2829
3064
  dispatcherOptions: {
2830
3065
  ...params.replyPipeline,
2831
3066
  onHeartbeatStrip: () => {
@@ -2834,6 +3069,14 @@ async function dispatchWhatsAppBufferedReply(params) {
2834
3069
  logVerbose$1("Stripped stray HEARTBEAT_OK token from web reply");
2835
3070
  }
2836
3071
  },
3072
+ onSettled: async () => {
3073
+ const flushResult = await mediaOnlyCoalescer.flushAll();
3074
+ logWhatsAppMediaOnlyFlushResult(flushResult);
3075
+ return whatsAppReplyDeliveryVisibility(flushResult.delivered > 0);
3076
+ },
3077
+ onReplyStart: params.msg.platform.sendComposing
3078
+ },
3079
+ delivery: {
2837
3080
  deliver: async (payload, info) => {
2838
3081
  const deliveryPayload = resolveWhatsAppDeliverablePayload(payload, info);
2839
3082
  if (!deliveryPayload) return whatsAppReplyDeliveryVisibility(false);
@@ -2884,8 +3127,8 @@ async function dispatchWhatsAppBufferedReply(params) {
2884
3127
  return whatsAppReplyDeliveryVisibilityFromDurableResult(durable.delivery);
2885
3128
  }
2886
3129
  if (durable.status === "handled_no_send") return flushResult.delivered > 0 ? whatsAppReplyDeliveryVisibility(true) : whatsAppReplyDeliveryVisibilityFromDurableResult(durable.delivery);
2887
- const delivery = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
2888
- return flushResult.delivered > 0 && !delivery.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : delivery;
3130
+ const deliveryResult = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
3131
+ return flushResult.delivered > 0 && !deliveryResult.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : deliveryResult;
2889
3132
  } catch (error) {
2890
3133
  throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
2891
3134
  }
@@ -2906,27 +3149,12 @@ async function dispatchWhatsAppBufferedReply(params) {
2906
3149
  const flushResult = await mediaOnlyCoalescer.flushExceptDuplicateMedia(mediaUrls);
2907
3150
  logWhatsAppMediaOnlyFlushResult(flushResult);
2908
3151
  try {
2909
- const delivery = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
2910
- return flushResult.delivered > 0 && !delivery.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : delivery;
3152
+ const deliveryResult = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
3153
+ return flushResult.delivered > 0 && !deliveryResult.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : deliveryResult;
2911
3154
  } catch (error) {
2912
3155
  throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
2913
3156
  }
2914
3157
  },
2915
- onSettled: async () => {
2916
- const flushResult = await mediaOnlyCoalescer.flushAll();
2917
- logWhatsAppMediaOnlyFlushResult(flushResult);
2918
- return whatsAppReplyDeliveryVisibility(flushResult.delivered > 0);
2919
- },
2920
- onReplyStart: params.msg.platform.sendComposing,
2921
- ...statusReactionController ? {
2922
- onCompactionStart: async () => {
2923
- await statusReactionController.setCompacting();
2924
- },
2925
- onCompactionEnd: async () => {
2926
- statusReactionController.cancelPending();
2927
- await statusReactionController.setThinking();
2928
- }
2929
- } : {},
2930
3158
  onError: (err, info) => {
2931
3159
  logWhatsAppReplyDeliveryError({
2932
3160
  err,
@@ -2938,39 +3166,52 @@ async function dispatchWhatsAppBufferedReply(params) {
2938
3166
  }
2939
3167
  },
2940
3168
  replyOptions: {
3169
+ ...ingressLifecycle ? bindIngressLifecycleToReplyOptions(ingressLifecycle) : {},
2941
3170
  suppressTyping: sourceRepliesAreToolOnly && conversationKind === "group" && !(params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned),
2942
3171
  disableBlockStreaming,
2943
3172
  ...sourceReplyDeliveryMode ? { sourceReplyDeliveryMode } : {},
2944
3173
  onModelSelected: params.onModelSelected,
2945
- ...statusReactionController ? { onToolStart: async (payload) => {
2946
- const toolName = payload.name?.trim();
2947
- if (toolName) await statusReactionController.setTool(toolName);
2948
- } } : {}
3174
+ ...statusReactionController ? {
3175
+ onToolStart: async (payload) => {
3176
+ const toolName = payload.name?.trim();
3177
+ if (toolName) await statusReactionController.setTool(toolName);
3178
+ },
3179
+ onCompactionStart: async () => {
3180
+ await statusReactionController.setCompacting();
3181
+ },
3182
+ onCompactionEnd: async () => {
3183
+ statusReactionController.cancelPending();
3184
+ await statusReactionController.setThinking();
3185
+ }
3186
+ } : {}
3187
+ },
3188
+ replyResolver: params.replyResolver,
3189
+ finalize: (dispatchResult) => {
3190
+ const didQueueVisibleReply = hasVisibleInboundReplyDispatch(dispatchResult);
3191
+ const didDeliverVisibleReply = didSendReply || dispatchResult.observedReplyDelivery === true;
3192
+ if (!didQueueVisibleReply) {
3193
+ if (statusReactionController) finalizeWhatsAppStatusReaction({
3194
+ controller: statusReactionController,
3195
+ outcome: "error",
3196
+ hasFinalResponse: false,
3197
+ removeAckAfterReply,
3198
+ timing: statusReactionTiming
3199
+ });
3200
+ if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
3201
+ logVerbose$1("Skipping auto-reply: silent token or no text/media returned from resolver");
3202
+ return false;
3203
+ }
3204
+ if (statusReactionController) finalizeWhatsAppStatusReaction({
3205
+ controller: statusReactionController,
3206
+ outcome: didDeliverVisibleReply ? "done" : "error",
3207
+ hasFinalResponse: didDeliverVisibleReply,
3208
+ removeAckAfterReply,
3209
+ timing: statusReactionTiming
3210
+ });
3211
+ if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
3212
+ return didDeliverVisibleReply;
2949
3213
  }
2950
- });
2951
- const didQueueVisibleReply = hasVisibleInboundReplyDispatch(dispatchResult);
2952
- const didDeliverVisibleReply = didSendReply || dispatchResult.observedReplyDelivery === true;
2953
- if (!didQueueVisibleReply) {
2954
- if (statusReactionController) finalizeWhatsAppStatusReaction({
2955
- controller: statusReactionController,
2956
- outcome: "error",
2957
- hasFinalResponse: false,
2958
- removeAckAfterReply,
2959
- timing: statusReactionTiming
2960
- });
2961
- if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
2962
- logVerbose$1("Skipping auto-reply: silent token or no text/media returned from resolver");
2963
- return false;
2964
- }
2965
- if (statusReactionController) finalizeWhatsAppStatusReaction({
2966
- controller: statusReactionController,
2967
- outcome: didDeliverVisibleReply ? "done" : "error",
2968
- hasFinalResponse: didDeliverVisibleReply,
2969
- removeAckAfterReply,
2970
- timing: statusReactionTiming
2971
- });
2972
- if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
2973
- return didDeliverVisibleReply;
3214
+ };
2974
3215
  }
2975
3216
  async function finalizeWhatsAppStatusReaction(params) {
2976
3217
  if (params.outcome === "done") {
@@ -3377,6 +3618,7 @@ async function processMessage(params) {
3377
3618
  updateLastRoute: updateLastRouteInBackground,
3378
3619
  warn: params.replyLogger.warn.bind(params.replyLogger)
3379
3620
  });
3621
+ let finalizeReply;
3380
3622
  const turnResult = await runChannelInboundEvent({
3381
3623
  channel: "whatsapp",
3382
3624
  accountId: params.route.accountId,
@@ -3410,26 +3652,8 @@ async function processMessage(params) {
3410
3652
  recordHistory: false
3411
3653
  } };
3412
3654
  },
3413
- resolveTurn: () => ({
3414
- channel: "whatsapp",
3415
- accountId: params.route.accountId,
3416
- routeSessionKey: params.route.sessionKey,
3417
- storePath,
3418
- ctxPayload,
3419
- recordInboundSession,
3420
- record: {
3421
- onRecordError: (err) => {
3422
- params.replyLogger.warn({
3423
- error: formatError(err),
3424
- storePath,
3425
- sessionKey: params.route.sessionKey
3426
- }, "failed updating session meta");
3427
- },
3428
- trackSessionMetaTask: (task) => {
3429
- trackBackgroundTask(params.backgroundTasks, task);
3430
- }
3431
- },
3432
- runDispatch: () => dispatchWhatsAppBufferedReply({
3655
+ resolveTurn: () => {
3656
+ const { finalize, ...replyPlan } = createWhatsAppReplyPlan({
3433
3657
  cfg: params.cfg,
3434
3658
  connectionId: params.connectionId,
3435
3659
  context: ctxPayload,
@@ -3450,11 +3674,35 @@ async function processMessage(params) {
3450
3674
  route: params.route,
3451
3675
  shouldClearGroupHistory,
3452
3676
  statusReactionController
3453
- })
3454
- })
3677
+ });
3678
+ finalizeReply = finalize;
3679
+ return {
3680
+ cfg: params.cfg,
3681
+ channel: "whatsapp",
3682
+ accountId: params.route.accountId,
3683
+ route: {
3684
+ agentId: params.route.agentId,
3685
+ sessionKey: params.route.sessionKey
3686
+ },
3687
+ ctxPayload,
3688
+ record: {
3689
+ onRecordError: (err) => {
3690
+ params.replyLogger.warn({
3691
+ error: formatError(err),
3692
+ storePath,
3693
+ sessionKey: params.route.sessionKey
3694
+ }, "failed updating session meta");
3695
+ },
3696
+ trackSessionMetaTask: (task) => {
3697
+ trackBackgroundTask(params.backgroundTasks, task);
3698
+ }
3699
+ },
3700
+ ...replyPlan
3701
+ };
3702
+ }
3455
3703
  }
3456
3704
  });
3457
- const didSendReply = turnResult.dispatched ? turnResult.dispatchResult : false;
3705
+ const didSendReply = turnResult.dispatched ? finalizeReply?.(turnResult.dispatchResult) ?? false : false;
3458
3706
  removeAckReactionHandleAfterReply({
3459
3707
  removeAfterReply: Boolean(params.cfg.messages?.removeAckAfterReply && didSendReply),
3460
3708
  ackReaction,
@@ -3798,6 +4046,7 @@ function isRetryableAuthUnstableError(error) {
3798
4046
  return error instanceof WhatsAppAuthUnstableError || typeof error === "object" && error !== null && "code" in error && error.code === "whatsapp-auth-unstable";
3799
4047
  }
3800
4048
  const DEFAULT_TRANSPORT_TIMEOUT_MS = 300 * 1e3;
4049
+ const WHATSAPP_RECONNECT_CATCH_UP_MAX_MS = 20 * 6e4;
3801
4050
  async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSocket, keepAlive = true, replyResolver, runtime = defaultRuntime, abortSignal, tuning = {}) {
3802
4051
  const activeReplyResolver = replyResolver ?? (await loadReplyResolverRuntime()).getReplyFromConfig;
3803
4052
  const runId = newConnectionId();
@@ -3849,7 +4098,7 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
3849
4098
  process.once("SIGINT", handleSigint);
3850
4099
  const transportTimeoutMs = tuning.transportTimeoutMs ?? DEFAULT_TRANSPORT_TIMEOUT_MS;
3851
4100
  const messageTimeoutMs = tuning.messageTimeoutMs ?? 1800 * 1e3;
3852
- const reconnectCatchUpWindowMs = Math.min(Math.max(messageTimeoutMs, 6e4), WHATSAPP_INBOUND_DEDUPE_TTL_MS);
4101
+ const reconnectCatchUpWindowMs = Math.min(Math.max(messageTimeoutMs, 6e4), WHATSAPP_RECONNECT_CATCH_UP_MAX_MS);
3853
4102
  const watchdogCheckMs = tuning.watchdogCheckMs ?? 60 * 1e3;
3854
4103
  const controller = new WhatsAppConnectionController({
3855
4104
  accountId: account.accountId,