@openclaw/whatsapp 2026.7.2-beta.1 → 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 (43) 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-D_1cK7-w.js → channel-B2I4Opzb.js} +30 -28
  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-BPrKzcYd.js → channel.runtime-CjkhQzk8.js} +4 -4
  15. package/dist/{channel.setup-eX64WZDx.js → channel.setup-BMWE9LmN.js} +2 -2
  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/legacy-state-migrations-api.js +1 -1
  23. package/dist/light-runtime-api.js +1 -1
  24. package/dist/{login-DGIoToX8.js → login-ifOXCBjo.js} +3 -3
  25. package/dist/{login-qr-X-CZHOk9.js → login-qr-DUHzVH_Y.js} +4 -4
  26. package/dist/login-qr-runtime.js +1 -1
  27. package/dist/{monitor-BKtA0lzq.js → monitor-_jqDKBXj.js} +586 -382
  28. package/dist/question-reactions-C0qJ6wGA.js +142 -0
  29. package/dist/runtime-api.js +7 -7
  30. package/dist/{send-D9Ydnn4o.js → send-SB-4JrVZ.js} +2 -3
  31. package/dist/{send-api-W33lI2nR.js → send-api-BG0MgoFI.js} +3 -3
  32. package/dist/{setup-core-BXDFGqky.js → setup-core-BbIgEl2M.js} +3 -3
  33. package/dist/{setup-finalize-DzOHXnPj.js → setup-finalize-Cc-4iWGj.js} +3 -3
  34. package/dist/setup-plugin-api.js +1 -1
  35. package/dist/{setup-surface-5EUmMLHS.js → setup-surface-DhnXEZir.js} +2 -2
  36. package/dist/{session-mxWFn0eS.js → socket-close-jNGyZY4T.js} +221 -1
  37. package/dist/{state-migrations-D_BmQUR9.js → state-migrations-CAMTrs2U.js} +1 -8
  38. package/npm-shrinkwrap.json +3 -3
  39. package/package.json +4 -4
  40. package/dist/account-config-LiT9wWH9.js +0 -43
  41. package/dist/constants.js +0 -2
  42. package/dist/directory-config-CLBQrjtC.js +0 -37
  43. package/dist/targets.js +0 -2
@@ -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
- import { normalizeLowercaseStringOrEmpty, normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
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,20 +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, 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, finalizeInboundContext, 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
- import { hasControlCommand, isControlCommandMessage, isControlCommandMessage as isControlCommandMessage$1, shouldComputeCommandAuthorized } from "openclaw/plugin-sdk/command-detection";
46
44
  import { drainPendingDeliveries } from "openclaw/plugin-sdk/delivery-queue-runtime";
47
45
  import { DEFAULT_GROUP_HISTORY_LIMIT, buildHistoryContextFromEntries, buildInboundHistoryFromEntries, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
48
46
  import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
49
47
  import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
50
- import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
51
48
  import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
52
49
  import { getRuntimeConfig as getRuntimeConfig$1, getRuntimeConfigSourceSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot";
53
50
  import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
@@ -56,60 +53,11 @@ import { createConnectedChannelStatusPatch, createTransportActivityStatusPatch }
56
53
  import { ensureConfiguredBindingRouteReady, resolveConfiguredBindingRoute } from "openclaw/plugin-sdk/conversation-binding-runtime";
57
54
  import { resolveAgentIdentity, resolveIdentityNamePrefix as resolveIdentityNamePrefix$1 } from "openclaw/plugin-sdk/agent-runtime";
58
55
  import { normalizeGroupActivation, parseActivationCommand } from "openclaw/plugin-sdk/group-activation";
56
+ import { hasControlCommand, isControlCommandMessage, shouldComputeCommandAuthorized } from "openclaw/plugin-sdk/command-detection";
59
57
  import { createInternalHookEvent, deriveInboundMessageHookContext, fireAndForgetBoundedHook, toInternalMessageReceivedContext, toPluginMessageContext, toPluginMessageReceivedEvent, triggerInternalHook } from "openclaw/plugin-sdk/hook-runtime";
60
58
  import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
61
59
  import { resolveBatchedReplyThreadingPolicy } from "openclaw/plugin-sdk/reply-reference";
62
- import { retryAsync } 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 claimableInboundMessages = createClaimableDedupe({
69
- ttlMs: WHATSAPP_INBOUND_DEDUPE_TTL_MS,
70
- memoryMaxSize: RECENT_WEB_MESSAGE_MAX
71
- });
72
- const recentOutboundMessages = createDedupeCache({
73
- ttlMs: RECENT_OUTBOUND_MESSAGE_TTL_MS,
74
- maxSize: RECENT_OUTBOUND_MESSAGE_MAX
75
- });
76
- var WhatsAppRetryableInboundError = class extends Error {
77
- constructor(message, options) {
78
- super(message, options);
79
- this.name = "WhatsAppRetryableInboundError";
80
- }
81
- };
82
- function buildMessageKey(params) {
83
- const accountId = params.accountId.trim();
84
- const remoteJid = params.remoteJid.trim();
85
- const messageId = params.messageId.trim();
86
- if (!accountId || !remoteJid || !messageId || messageId === "unknown") return null;
87
- return `${accountId}:${remoteJid}:${messageId}`;
88
- }
89
- function resetWebInboundDedupe() {
90
- claimableInboundMessages.clearMemory();
91
- recentOutboundMessages.clear();
92
- }
93
- async function claimRecentInboundMessageDelivery(key) {
94
- return (await claimableInboundMessages.claim(key)).kind;
95
- }
96
- async function commitRecentInboundMessage(key) {
97
- await claimableInboundMessages.commit(key);
98
- }
99
- function releaseRecentInboundMessage(key, error) {
100
- claimableInboundMessages.release(key, { error });
101
- }
102
- function rememberRecentOutboundMessage(params) {
103
- const key = buildMessageKey(params);
104
- if (!key) return;
105
- recentOutboundMessages.check(key);
106
- }
107
- function isRecentOutboundMessage(params) {
108
- const key = buildMessageKey(params);
109
- if (!key) return false;
110
- return recentOutboundMessages.peek(key);
111
- }
112
- //#endregion
60
+ import { createChannelApiRetryRunner } from "openclaw/plugin-sdk/retry-runtime";
113
61
  //#region extensions/whatsapp/src/inbound/message-aliases.ts
114
62
  function normalizeQuoteSender(sender) {
115
63
  if (!sender?.displayName && !sender?.jid && !sender?.e164) return;
@@ -434,9 +382,63 @@ function normalizeWebInboundMessage(msg) {
434
382
  return normalizeLegacyFlatWebInboundMessage(msg);
435
383
  }
436
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
437
439
  //#region extensions/whatsapp/src/inbound/durable-receive.ts
438
440
  const WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES = 450;
439
- const WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES = 450;
441
+ const WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES = 5e3;
440
442
  const WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS = 720 * 60 * 60 * 1e3;
441
443
  const WHATSAPP_DURABLE_INBOUND_COMPLETED_TTL_MS = 10080 * 60 * 1e3;
442
444
  function hashNamespacePart(value) {
@@ -445,31 +447,91 @@ function hashNamespacePart(value) {
445
447
  function createWhatsAppDurableInboundMessageId(params) {
446
448
  return createHash("sha256").update(`${params.remoteJid}\n${params.id}`).digest("hex");
447
449
  }
448
- function serializeWhatsAppDurableInboundMessage(message) {
449
- return JSON.parse(JSON.stringify(message, BufferJSON.replacer));
450
- }
451
- function deserializeWhatsAppDurableInboundMessage(message) {
452
- return JSON.parse(JSON.stringify(message), BufferJSON.reviver);
453
- }
454
- function createWhatsAppDurableInboundReceiveJournal(accountId) {
455
- const accountPart = hashNamespacePart(accountId);
456
- const runtime = getWhatsAppRuntime();
457
- return createDurableInboundReceiveJournalFromQueue({
458
- queue: runtime.state.openChannelIngressQueue({
459
- accountId: accountPart,
460
- 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
461
458
  }),
462
- retention: {
463
- pendingTtlMs: WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS,
464
- completedTtlMs: WHATSAPP_DURABLE_INBOUND_COMPLETED_TTL_MS,
465
- failedTtlMs: WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS,
466
- pendingMaxEntries: WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES,
467
- completedMaxEntries: WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES,
468
- 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);
469
521
  }
470
522
  });
471
523
  }
472
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
473
535
  //#region extensions/whatsapp/src/inbound/lifecycle.ts
474
536
  function attachEmitterListener(emitter, event, listener) {
475
537
  emitter.on(event, listener);
@@ -539,13 +601,8 @@ const RECONNECT_IN_PROGRESS_ERROR = "no active socket - reconnection in progress
539
601
  const GROUP_META_TTL_MS = 300 * 1e3;
540
602
  const BAILEYS_MESSAGE_TTL_MS = 600 * 1e3;
541
603
  const INBOUND_CLOSE_DRAIN_TIMEOUT_MS = 5e3;
542
- const REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE = /reply session initialization conflicted for \S+/u;
604
+ const WHATSAPP_INGRESS_DRAIN_INTERVAL_MS = 1e3;
543
605
  const WHATSAPP_GROUP_METADATA_CACHE_MAX_ENTRIES = 500;
544
- function resolveRetryableWhatsAppInboundError(error) {
545
- if (error instanceof WhatsAppRetryableInboundError) return error;
546
- if (!collectErrorGraphCandidates(error, (current) => [current.cause, current.error]).some((candidate) => REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE.test(formatErrorMessage(candidate)))) return null;
547
- return new WhatsAppRetryableInboundError(formatErrorMessage(error), { cause: error });
548
- }
549
606
  function resolveGroupMetadataExpiresAt(nowRaw = Date.now()) {
550
607
  const now = asDateTimestampMs(nowRaw);
551
608
  return now === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(GROUP_META_TTL_MS, { nowMs: now });
@@ -636,9 +693,6 @@ function isRetryableSendDisconnectError(err) {
636
693
  function shouldClearSocketRefAfterSendFailure(err) {
637
694
  return /closed|reset|disconnect|no active socket/i.test(formatError(err));
638
695
  }
639
- function isNonEmptyString(value) {
640
- return Boolean(value);
641
- }
642
696
  async function attachWebInboxToSocket(options) {
643
697
  const inboundLogger = getChildLogger({ module: "web-inbound" });
644
698
  const inboundConsoleLog = createSubsystemLogger("gateway/channels/whatsapp").child("inbound");
@@ -679,11 +733,30 @@ async function attachWebInboxToSocket(options) {
679
733
  if (!successorIdentity || !identitiesOverlap(self, successorIdentity)) return null;
680
734
  return successor.getCurrentSock();
681
735
  };
682
- const durableInboundJournal = createWhatsAppDurableInboundReceiveJournal(options.accountId);
736
+ const durableInboundQueue = options.durableInboundQueue ?? createWhatsAppDurableInboundQueue(options.accountId);
683
737
  const inboundDebounceMs = Math.max(0, Math.trunc(options.debounceMs ?? 0));
684
738
  const pendingDebounceKeys = /* @__PURE__ */ new Set();
685
739
  const activeInboundFlushes = /* @__PURE__ */ new Set();
686
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
+ };
687
760
  let nextReceiveOrder = 0;
688
761
  const publishPendingWorkState = (at = Date.now()) => {
689
762
  options.onPendingWorkChanged?.(pendingMessageHandlers.size + pendingDebounceKeys.size + activeInboundFlushes.size, at);
@@ -701,18 +774,47 @@ async function attachWebInboxToSocket(options) {
701
774
  if (timestampDiff !== 0) return timestampDiff;
702
775
  return (a.receiveOrder ?? 0) - (b.receiveOrder ?? 0);
703
776
  });
704
- const finalizeInboundDelivery = async (entries, error) => {
705
- const dedupeKeys = uniqueStrings(entries.map((entry) => entry.dedupeKey).filter(isNonEmptyString));
706
- const durableEntries = entries.filter((entry) => isNonEmptyString(entry.durableId));
707
- const readReceiptEntries = entries.filter((entry) => Boolean(entry.readReceipt));
708
- const retryableError = resolveRetryableWhatsAppInboundError(error);
709
- if (retryableError) {
710
- dedupeKeys.forEach((dedupeKey) => releaseRecentInboundMessage(dedupeKey, retryableError));
711
- await Promise.all(durableEntries.map((entry) => durableInboundJournal.release(entry.durableId, { lastError: formatError(retryableError) })));
712
- return;
713
- }
714
- await Promise.all([...dedupeKeys.map((dedupeKey) => commitRecentInboundMessage(dedupeKey)), ...durableEntries.map((entry) => durableInboundJournal.complete(entry.durableId, entry.readReceipt ? { metadata: { readReceipt: entry.readReceipt } } : void 0))]);
715
- 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
+ };
716
818
  };
717
819
  const debouncer = createInboundDebouncer({
718
820
  debounceMs: inboundDebounceMs,
@@ -725,14 +827,17 @@ async function attachWebInboxToSocket(options) {
725
827
  });
726
828
  activeInboundFlushes.add(flushTask);
727
829
  publishPendingWorkState();
830
+ notifyDebounceWork();
728
831
  try {
729
832
  const orderedEntries = orderDebouncedInboundEntries(entries);
730
833
  const last = orderedEntries.at(-1);
731
834
  if (!last) return;
835
+ const { lifecycle, settle, abandon } = buildFlushIngressLifecycle(orderedEntries);
732
836
  try {
733
837
  if (orderedEntries.length === 1) {
734
- await options.onMessage(last);
735
- await finalizeInboundDelivery(orderedEntries);
838
+ await options.onMessage(attachWhatsAppIngressLifecycle(last, lifecycle));
839
+ await settle();
840
+ await Promise.all(orderedEntries.map((entry) => maybeMarkInboundAsRead(entry.readReceipt)));
736
841
  return;
737
842
  }
738
843
  const mentioned = /* @__PURE__ */ new Set();
@@ -747,8 +852,9 @@ async function attachWebInboxToSocket(options) {
747
852
  ...last.group,
748
853
  mentions: combinedMentions
749
854
  } : void 0;
750
- const combinedMessage = withDeprecatedWebInboundMessageFlatAliases({
855
+ const combinedMessage = attachWhatsAppIngressLifecycle(withDeprecatedWebInboundMessageFlatAliases({
751
856
  ...last,
857
+ ...lifecycle ? { turnAdoptionLifecycle: lifecycle } : {},
752
858
  payload: {
753
859
  ...last.payload,
754
860
  body: combinedBody,
@@ -759,11 +865,12 @@ async function attachWebInboxToSocket(options) {
759
865
  ...last.event,
760
866
  isBatched: true
761
867
  }
762
- });
868
+ }), lifecycle);
763
869
  await options.onMessage(combinedMessage);
764
- await finalizeInboundDelivery(orderedEntries);
870
+ await settle();
871
+ await Promise.all(orderedEntries.map((entry) => maybeMarkInboundAsRead(entry.readReceipt)));
765
872
  } catch (error) {
766
- await finalizeInboundDelivery(orderedEntries, error);
873
+ await abandon();
767
874
  throw error;
768
875
  }
769
876
  } finally {
@@ -978,20 +1085,24 @@ async function attachWebInboxToSocket(options) {
978
1085
  }
979
1086
  return content;
980
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
+ };
981
1099
  const normalizeInboundMessage = async (msg) => {
982
1100
  const id = msg.key?.id ?? void 0;
983
1101
  const remoteJid = msg.key?.remoteJid;
984
1102
  if (!remoteJid) return null;
985
1103
  if (remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) return null;
986
1104
  const group = isJidGroup(remoteJid) === true;
987
- if (Boolean(msg.key?.fromMe) && id && isRecentOutboundMessage({
988
- accountId: options.accountId,
989
- remoteJid,
990
- messageId: id
991
- })) {
992
- logWhatsAppVerbose(options.verbose, `Skipping recent outbound WhatsApp echo ${id} for ${remoteJid}`);
993
- return null;
994
- }
1105
+ if (shouldSkipRecentOutboundEcho(msg)) return null;
995
1106
  if (!hasInboundUserContent(msg.message ?? void 0)) return null;
996
1107
  const participantJid = msg.key?.participant ?? void 0;
997
1108
  const from = group ? remoteJid : await resolveInboundJid(remoteJid);
@@ -1067,15 +1178,6 @@ async function attachWebInboxToSocket(options) {
1067
1178
  }
1068
1179
  await maybeMarkInboundAsRead(target);
1069
1180
  };
1070
- const completeUndeliverableDurableInbound = async (durableId, metadata) => {
1071
- if (!durableId) return;
1072
- await durableInboundJournal.complete(durableId, metadata?.readReceipt ? { metadata: { readReceipt: metadata.readReceipt } } : void 0);
1073
- };
1074
- const buildDurableInboundPayload = (msg, upsertType) => ({
1075
- message: serializeWhatsAppDurableInboundMessage(msg),
1076
- ...upsertType ? { upsertType } : {},
1077
- receivedAt: Date.now()
1078
- });
1079
1181
  const shouldSkipStaleAppend = (msg, upsertType) => {
1080
1182
  if (upsertType !== "append") return false;
1081
1183
  const APPEND_RECENT_GRACE_MS = 6e4;
@@ -1084,69 +1186,7 @@ async function attachWebInboxToSocket(options) {
1084
1186
  const nowMs = Date.now();
1085
1187
  return msgTsMs < (options.appendReplyWindow && nowMs <= options.appendReplyWindow.untilMs ? Math.max(options.appendReplyWindow.afterMs, nowMs - options.appendReplyWindow.maxAgeMs) : connectedAtMs - APPEND_RECENT_GRACE_MS);
1086
1188
  };
1087
- const processDurableInboundMessage = async (msg, upsertType, receiveOrder, stored) => {
1088
- const inbound = await normalizeInboundMessage(msg);
1089
- if (!inbound) {
1090
- if (stored) await completeUndeliverableDurableInbound(stored.id, stored.metadata);
1091
- return;
1092
- }
1093
- const readReceipt = stored?.metadata?.readReceipt ?? buildReadReceiptTarget(inbound);
1094
- const deliveryReadReceipt = inbound.access.isSelfChat ? void 0 : readReceipt;
1095
- if (!stored && shouldSkipStaleAppend(msg, upsertType)) {
1096
- await maybeMarkNonSelfChatReadReceipt(inbound, readReceipt);
1097
- return;
1098
- }
1099
- let durableId = stored?.id ?? (inbound.id ? createWhatsAppDurableInboundMessageId({
1100
- remoteJid: inbound.remoteJid,
1101
- id: inbound.id
1102
- }) : void 0);
1103
- const durableMetadata = deliveryReadReceipt ? { readReceipt: deliveryReadReceipt } : void 0;
1104
- if (durableId && !stored) try {
1105
- const accepted = await durableInboundJournal.accept(durableId, buildDurableInboundPayload(msg, upsertType), {
1106
- metadata: durableMetadata,
1107
- receivedAt: inbound.messageTimestampMs
1108
- });
1109
- if (accepted.kind === "completed") {
1110
- await maybeMarkNonSelfChatReadReceipt(inbound, accepted.record.metadata?.readReceipt ?? deliveryReadReceipt);
1111
- return;
1112
- }
1113
- if (accepted.kind === "pending" && accepted.record.attempts === 0) return;
1114
- } catch (err) {
1115
- durableId = void 0;
1116
- const error = formatError(err);
1117
- inboundLogger.warn({ error }, "failed persisting durable WhatsApp inbound; delivering live");
1118
- inboundConsoleLog.warn(`Failed persisting durable WhatsApp inbound; delivering live: ${error}`);
1119
- }
1120
- const enriched = await enrichInboundMessage(msg);
1121
- if (!enriched) {
1122
- await completeUndeliverableDurableInbound(durableId, durableMetadata);
1123
- await maybeMarkNonSelfChatReadReceipt(inbound, deliveryReadReceipt);
1124
- return;
1125
- }
1126
- const dedupeKey = inbound.id ? `${options.accountId}:${inbound.remoteJid}:${inbound.id}` : "";
1127
- const dedupeClaim = dedupeKey ? await claimRecentInboundMessageDelivery(dedupeKey) : "claimed";
1128
- if (dedupeClaim !== "claimed") {
1129
- if (dedupeClaim === "duplicate") {
1130
- await completeUndeliverableDurableInbound(durableId, durableMetadata);
1131
- await maybeMarkNonSelfChatReadReceipt(inbound, deliveryReadReceipt);
1132
- }
1133
- return;
1134
- }
1135
- recordAcceptedInboundActivity(options.accountId);
1136
- await enqueueInboundMessage(msg, inbound, enriched, {
1137
- durableId,
1138
- readReceipt: deliveryReadReceipt,
1139
- receiveOrder
1140
- });
1141
- };
1142
- const replayPendingDurableInboundMessages = async () => {
1143
- const pending = await durableInboundJournal.pending();
1144
- for (const record of pending) await processDurableInboundMessage(deserializeWhatsAppDurableInboundMessage(record.payload.message), record.payload.upsertType, record.payload.receivedAt, {
1145
- id: record.id,
1146
- payload: record.payload,
1147
- metadata: record.metadata
1148
- });
1149
- };
1189
+ const preparedInboundByDurableId = /* @__PURE__ */ new Map();
1150
1190
  const enrichInboundMessage = async (msg) => {
1151
1191
  const location = extractLocationData(msg.message ?? void 0);
1152
1192
  const locationText = location ? formatLocationText(location) : void 0;
@@ -1301,8 +1341,7 @@ async function attachWebInboxToSocket(options) {
1301
1341
  }
1302
1342
  } : void 0,
1303
1343
  group,
1304
- dedupeKey: inbound.id ? `${options.accountId}:${inbound.remoteJid}:${inbound.id}` : void 0,
1305
- durableId: durable.durableId,
1344
+ turnAdoptionLifecycle: durable.turnAdoptionLifecycle,
1306
1345
  readReceipt: durable.readReceipt,
1307
1346
  receiveOrder: durable.receiveOrder
1308
1347
  });
@@ -1312,6 +1351,7 @@ async function attachWebInboxToSocket(options) {
1312
1351
  if (inboundDebounceMs > 0 && shouldDebounceInboundMessage(inboundMessage)) {
1313
1352
  pendingDebounceKeys.add(debounceKey);
1314
1353
  publishPendingWorkState();
1354
+ notifyDebounceWork();
1315
1355
  }
1316
1356
  }
1317
1357
  if (inboundMessage.event.id) {
@@ -1323,16 +1363,119 @@ async function attachWebInboxToSocket(options) {
1323
1363
  fromMe: inboundMessage.platform.fromMe
1324
1364
  });
1325
1365
  }
1326
- try {
1327
- Promise.resolve(debouncer.enqueue(inboundMessage)).catch((err) => {
1328
- inboundLogger.error({ error: String(err) }, "failed handling inbound web message");
1329
- 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;
1330
1424
  });
1331
- } catch (err) {
1332
- inboundLogger.error({ error: String(err) }, "failed handling inbound web message");
1333
- 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);
1334
1436
  }
1437
+ return durableDrainPass;
1438
+ };
1439
+ const requestDrainAfterLaneSettlement = () => {
1440
+ requestDurableInboundDrain().catch((error) => {
1441
+ inboundLogger.error({ error: formatError(error) }, "whatsapp durable inbound drain failed");
1442
+ });
1335
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?.();
1336
1479
  const handleMessagesUpsert = async (upsert) => {
1337
1480
  if (upsert.type !== "notify" && upsert.type !== "append") return;
1338
1481
  for (const msg of upsert.messages ?? []) {
@@ -1348,7 +1491,78 @@ async function attachWebInboxToSocket(options) {
1348
1491
  resolveReactionTargetJids,
1349
1492
  logVerboseMessage: (message) => logWhatsAppVerbose(options.verbose, message)
1350
1493
  })) continue;
1351
- 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
+ }
1352
1566
  }
1353
1567
  };
1354
1568
  const handleMessagesUpsertEvent = (upsert) => {
@@ -1363,9 +1577,6 @@ async function attachWebInboxToSocket(options) {
1363
1577
  publishPendingWorkState();
1364
1578
  });
1365
1579
  };
1366
- const waitForPendingMessageHandlers = async () => {
1367
- while (pendingMessageHandlers.size > 0) await Promise.all(Array.from(pendingMessageHandlers));
1368
- };
1369
1580
  const drainDebouncedInboundMessages = async () => {
1370
1581
  while (pendingDebounceKeys.size > 0 || activeInboundFlushes.size > 0) {
1371
1582
  const debounceKeys = Array.from(pendingDebounceKeys);
@@ -1377,8 +1588,21 @@ async function attachWebInboxToSocket(options) {
1377
1588
  };
1378
1589
  const drainInboundBeforeSocketClose = async () => {
1379
1590
  groupMetadataCacheClosed = true;
1380
- 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
+ }
1381
1599
  await drainDebouncedInboundMessages();
1600
+ try {
1601
+ await durableInboundDrain.waitForIdle();
1602
+ } finally {
1603
+ durableDrainClosed = true;
1604
+ durableInboundDrain.dispose();
1605
+ }
1382
1606
  };
1383
1607
  const drainInboundBeforeSocketCloseWithTimeout = async () => {
1384
1608
  let timeout = null;
@@ -1391,6 +1615,8 @@ async function attachWebInboxToSocket(options) {
1391
1615
  })]);
1392
1616
  } finally {
1393
1617
  if (timeout) clearTimeout(timeout);
1618
+ durableDrainClosed = true;
1619
+ durableInboundDrain.dispose();
1394
1620
  }
1395
1621
  };
1396
1622
  const handleConnectionUpdate = (update) => {
@@ -1449,16 +1675,10 @@ async function attachWebInboxToSocket(options) {
1449
1675
  const detachGroupParticipantsUpdate = attachSockListener("group-participants.update", ((update) => {
1450
1676
  forgetFullGroupMetadata(update.id);
1451
1677
  }));
1452
- const replayTask = replayPendingDurableInboundMessages().catch((err) => {
1678
+ requestDurableInboundDrain().catch((err) => {
1453
1679
  inboundLogger.error({ error: String(err) }, "failed replaying durable WhatsApp inbound");
1454
1680
  inboundConsoleLog.error(`Failed replaying durable WhatsApp inbound: ${String(err)}`);
1455
1681
  });
1456
- pendingMessageHandlers.add(replayTask);
1457
- publishPendingWorkState();
1458
- replayTask.finally(() => {
1459
- pendingMessageHandlers.delete(replayTask);
1460
- publishPendingWorkState();
1461
- });
1462
1682
  (async () => {
1463
1683
  try {
1464
1684
  const groups = await sock.groupFetchAllParticipating();
@@ -2224,29 +2444,34 @@ function isRetryableWhatsAppOutboundError(error) {
2224
2444
  return WHATSAPP_RETRYABLE_OUTBOUND_ERROR_PATTERN.test(formatError(error));
2225
2445
  }
2226
2446
  async function sendWhatsAppOutboundWithRetry(params) {
2447
+ const runWithRetry = createChannelApiRetryRunner({
2448
+ retry: {
2449
+ attempts: WHATSAPP_OUTBOUND_MAX_ATTEMPTS,
2450
+ minDelayMs: WHATSAPP_OUTBOUND_MIN_DELAY_MS,
2451
+ maxDelayMs: WHATSAPP_OUTBOUND_MAX_DELAY_MS,
2452
+ jitter: 0
2453
+ },
2454
+ strictShouldRetry: true,
2455
+ retryAfterMs: () => void 0,
2456
+ shouldRetry: (error, attempt) => {
2457
+ if (!(error instanceof WhatsAppOutboundRetryError) || !isRetryableWhatsAppOutboundError(error.original)) return false;
2458
+ params.onRetry?.({
2459
+ attempt,
2460
+ maxAttempts: WHATSAPP_OUTBOUND_MAX_ATTEMPTS,
2461
+ backoffMs: Math.min(WHATSAPP_OUTBOUND_MIN_DELAY_MS * 2 ** (attempt - 1), WHATSAPP_OUTBOUND_MAX_DELAY_MS),
2462
+ error: error.original,
2463
+ errorText: formatError(error.original)
2464
+ });
2465
+ return true;
2466
+ }
2467
+ });
2227
2468
  try {
2228
- return await retryAsync(async () => {
2469
+ return await runWithRetry(async () => {
2229
2470
  try {
2230
2471
  return await params.send();
2231
2472
  } catch (error) {
2232
2473
  throw new WhatsAppOutboundRetryError(error);
2233
2474
  }
2234
- }, {
2235
- attempts: WHATSAPP_OUTBOUND_MAX_ATTEMPTS,
2236
- minDelayMs: WHATSAPP_OUTBOUND_MIN_DELAY_MS,
2237
- maxDelayMs: WHATSAPP_OUTBOUND_MAX_DELAY_MS,
2238
- jitter: 0,
2239
- shouldRetry: (error) => error instanceof WhatsAppOutboundRetryError && isRetryableWhatsAppOutboundError(error.original),
2240
- onRetry: ({ attempt, maxAttempts, delayMs, err }) => {
2241
- if (!(err instanceof WhatsAppOutboundRetryError)) return;
2242
- params.onRetry?.({
2243
- attempt,
2244
- maxAttempts,
2245
- backoffMs: delayMs,
2246
- error: err.original,
2247
- errorText: formatError(err.original)
2248
- });
2249
- }
2250
2475
  });
2251
2476
  } catch (error) {
2252
2477
  if (error instanceof WhatsAppOutboundRetryError) throw error.original;
@@ -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,
@@ -2662,7 +2896,6 @@ async function buildWhatsAppInboundContext(params) {
2662
2896
  }] : void 0, { transcribed: (_entry, index) => params.mediaTranscribedIndexes?.includes(index) === true });
2663
2897
  return buildChannelInboundEventContext({
2664
2898
  channel: "whatsapp",
2665
- finalize: finalizeInboundContext,
2666
2899
  supplemental: {
2667
2900
  quote: params.visibleReplyTo ? {
2668
2901
  id: params.visibleReplyTo.id,
@@ -2687,6 +2920,7 @@ async function buildWhatsAppInboundContext(params) {
2687
2920
  },
2688
2921
  route: {
2689
2922
  agentId: params.route.agentId,
2923
+ dmScope: params.route.dmScope,
2690
2924
  accountId: params.route.accountId,
2691
2925
  routeSessionKey: params.route.sessionKey
2692
2926
  },
@@ -2699,7 +2933,7 @@ async function buildWhatsAppInboundContext(params) {
2699
2933
  bodyForAgent: params.bodyForAgent ?? params.msg.payload.body,
2700
2934
  inboundHistory,
2701
2935
  rawBody: params.rawBody ?? params.msg.payload.body,
2702
- commandBody: params.commandBody ?? params.msg.payload.body
2936
+ commandBody: params.command?.body ?? params.msg.payload.body
2703
2937
  },
2704
2938
  access: {
2705
2939
  ...wasMentioned !== void 0 ? { mentions: {
@@ -2707,9 +2941,9 @@ async function buildWhatsAppInboundContext(params) {
2707
2941
  wasMentioned,
2708
2942
  requireMention: params.msg.groupMention?.requireMention
2709
2943
  } } : {},
2710
- commands: { authorized: params.commandAuthorized }
2944
+ commands: { authorized: params.command?.authorized === true }
2711
2945
  },
2712
- commandTurn: params.commandTurn,
2946
+ command: params.command,
2713
2947
  extra: {
2714
2948
  Transcript: params.transcript,
2715
2949
  GroupSubject: params.msg.group?.subject,
@@ -2719,40 +2953,12 @@ async function buildWhatsAppInboundContext(params) {
2719
2953
  fallbackE164: params.sender.e164
2720
2954
  }),
2721
2955
  SenderE164: params.sender.e164,
2722
- CommandSource: params.commandSource ?? (params.commandTurn?.source === "native" || params.commandTurn?.source === "text" ? params.commandTurn.source : void 0),
2723
2956
  ReplyThreading: params.replyThreading,
2724
2957
  SuppressMessageReceivedHooks: params.suppressMessageReceivedHooks,
2725
2958
  ...params.msg.payload.location ? toLocationContext(params.msg.payload.location) : {}
2726
2959
  }
2727
2960
  });
2728
2961
  }
2729
- function normalizeCommandTurnFromContext(value) {
2730
- if (!value || typeof value !== "object") return;
2731
- const record = value;
2732
- const kind = record.kind;
2733
- const source = record.source;
2734
- if (kind === "native" && source === "native" && typeof record.authorized === "boolean") return {
2735
- kind: "native",
2736
- source: "native",
2737
- authorized: record.authorized,
2738
- commandName: typeof record.commandName === "string" ? record.commandName : void 0,
2739
- body: typeof record.body === "string" ? record.body : void 0
2740
- };
2741
- if (kind === "text-slash" && source === "text" && typeof record.authorized === "boolean") return {
2742
- kind: "text-slash",
2743
- source: "text",
2744
- authorized: record.authorized,
2745
- commandName: typeof record.commandName === "string" ? record.commandName : void 0,
2746
- body: typeof record.body === "string" ? record.body : void 0
2747
- };
2748
- if (kind === "normal" && source === "message") return {
2749
- kind: "normal",
2750
- source: "message",
2751
- authorized: false,
2752
- commandName: typeof record.commandName === "string" ? record.commandName : void 0,
2753
- body: typeof record.body === "string" ? record.body : void 0
2754
- };
2755
- }
2756
2962
  function resolveWhatsAppDmRouteTarget(params) {
2757
2963
  const admission = requireWhatsAppInboundAdmission(params.msg);
2758
2964
  const conversationId = admission.conversation.id;
@@ -2783,8 +2989,9 @@ function updateWhatsAppMainLastRoute(params) {
2783
2989
  }
2784
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})`);
2785
2991
  }
2786
- async function dispatchWhatsAppBufferedReply(params) {
2992
+ function createWhatsAppReplyPlan(params) {
2787
2993
  const admission = requireWhatsAppInboundAdmission(params.msg);
2994
+ const ingressLifecycle = resolveWhatsAppIngressLifecycle(params.msg);
2788
2995
  const conversationId = admission.conversation.id;
2789
2996
  const conversationKind = admission.conversation.kind;
2790
2997
  const statusReactionController = params.statusReactionController ?? null;
@@ -2801,18 +3008,9 @@ async function dispatchWhatsAppBufferedReply(params) {
2801
3008
  accountId: params.route.accountId
2802
3009
  });
2803
3010
  const mediaLocalRoots = getAgentScopedMediaLocalRoots(params.cfg, params.route.agentId);
2804
- const sourceReplyChatType = typeof params.context.ChatType === "string" ? params.context.ChatType : conversationKind;
2805
- const sourceReplyCommandSource = params.context.CommandSource === "native" || params.context.CommandSource === "text" ? params.context.CommandSource : void 0;
2806
- const sourceReplyCommandTurn = normalizeCommandTurnFromContext(params.context.CommandTurn);
2807
- const sourceReplyCommandAuthorized = typeof params.context.CommandAuthorized === "boolean" ? params.context.CommandAuthorized : void 0;
2808
- const sourceReplyDeliveryMode = sourceReplyChatType === "group" || sourceReplyChatType === "channel" ? resolveChannelMessageSourceReplyDeliveryMode({
3011
+ const sourceReplyDeliveryMode = params.context.ChatType === "group" || params.context.ChatType === "channel" ? resolveChannelMessageSourceReplyDeliveryMode({
2809
3012
  cfg: params.cfg,
2810
- ctx: {
2811
- ChatType: sourceReplyChatType,
2812
- CommandTurn: sourceReplyCommandTurn,
2813
- CommandSource: sourceReplyCommandSource,
2814
- CommandAuthorized: sourceReplyCommandAuthorized
2815
- }
3013
+ ctx: params.context
2816
3014
  }) : void 0;
2817
3015
  const sourceRepliesAreToolOnly = sourceReplyDeliveryMode === "message_tool_only";
2818
3016
  const disableBlockStreaming = sourceRepliesAreToolOnly ? true : resolveWhatsAppDisableBlockStreaming(params.cfg);
@@ -2859,11 +3057,10 @@ async function dispatchWhatsAppBufferedReply(params) {
2859
3057
  const mediaOnlyCoalescer = createWhatsAppMediaOnlyReplyCoalescer({ deliver: async (pending) => {
2860
3058
  return await deliverNormalizedPayload(pending.payload, pending.info);
2861
3059
  } });
2862
- if (statusReactionController) statusReactionController.setThinking();
2863
- const dispatchResult = await dispatchReplyWithBufferedBlockDispatcher({
2864
- ctx: params.context,
2865
- cfg: params.cfg,
2866
- replyResolver: params.replyResolver,
3060
+ return {
3061
+ afterRecord: () => {
3062
+ if (statusReactionController) statusReactionController.setThinking();
3063
+ },
2867
3064
  dispatcherOptions: {
2868
3065
  ...params.replyPipeline,
2869
3066
  onHeartbeatStrip: () => {
@@ -2872,6 +3069,14 @@ async function dispatchWhatsAppBufferedReply(params) {
2872
3069
  logVerbose$1("Stripped stray HEARTBEAT_OK token from web reply");
2873
3070
  }
2874
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: {
2875
3080
  deliver: async (payload, info) => {
2876
3081
  const deliveryPayload = resolveWhatsAppDeliverablePayload(payload, info);
2877
3082
  if (!deliveryPayload) return whatsAppReplyDeliveryVisibility(false);
@@ -2922,8 +3127,8 @@ async function dispatchWhatsAppBufferedReply(params) {
2922
3127
  return whatsAppReplyDeliveryVisibilityFromDurableResult(durable.delivery);
2923
3128
  }
2924
3129
  if (durable.status === "handled_no_send") return flushResult.delivered > 0 ? whatsAppReplyDeliveryVisibility(true) : whatsAppReplyDeliveryVisibilityFromDurableResult(durable.delivery);
2925
- const delivery = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
2926
- 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;
2927
3132
  } catch (error) {
2928
3133
  throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
2929
3134
  }
@@ -2944,27 +3149,12 @@ async function dispatchWhatsAppBufferedReply(params) {
2944
3149
  const flushResult = await mediaOnlyCoalescer.flushExceptDuplicateMedia(mediaUrls);
2945
3150
  logWhatsAppMediaOnlyFlushResult(flushResult);
2946
3151
  try {
2947
- const delivery = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
2948
- 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;
2949
3154
  } catch (error) {
2950
3155
  throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
2951
3156
  }
2952
3157
  },
2953
- onSettled: async () => {
2954
- const flushResult = await mediaOnlyCoalescer.flushAll();
2955
- logWhatsAppMediaOnlyFlushResult(flushResult);
2956
- return whatsAppReplyDeliveryVisibility(flushResult.delivered > 0);
2957
- },
2958
- onReplyStart: params.msg.platform.sendComposing,
2959
- ...statusReactionController ? {
2960
- onCompactionStart: async () => {
2961
- await statusReactionController.setCompacting();
2962
- },
2963
- onCompactionEnd: async () => {
2964
- statusReactionController.cancelPending();
2965
- await statusReactionController.setThinking();
2966
- }
2967
- } : {},
2968
3158
  onError: (err, info) => {
2969
3159
  logWhatsAppReplyDeliveryError({
2970
3160
  err,
@@ -2976,39 +3166,52 @@ async function dispatchWhatsAppBufferedReply(params) {
2976
3166
  }
2977
3167
  },
2978
3168
  replyOptions: {
3169
+ ...ingressLifecycle ? bindIngressLifecycleToReplyOptions(ingressLifecycle) : {},
2979
3170
  suppressTyping: sourceRepliesAreToolOnly && conversationKind === "group" && !(params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned),
2980
3171
  disableBlockStreaming,
2981
3172
  ...sourceReplyDeliveryMode ? { sourceReplyDeliveryMode } : {},
2982
3173
  onModelSelected: params.onModelSelected,
2983
- ...statusReactionController ? { onToolStart: async (payload) => {
2984
- const toolName = payload.name?.trim();
2985
- if (toolName) await statusReactionController.setTool(toolName);
2986
- } } : {}
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;
2987
3213
  }
2988
- });
2989
- const didQueueVisibleReply = hasVisibleInboundReplyDispatch(dispatchResult);
2990
- const didDeliverVisibleReply = didSendReply || dispatchResult.observedReplyDelivery === true;
2991
- if (!didQueueVisibleReply) {
2992
- if (statusReactionController) finalizeWhatsAppStatusReaction({
2993
- controller: statusReactionController,
2994
- outcome: "error",
2995
- hasFinalResponse: false,
2996
- removeAckAfterReply,
2997
- timing: statusReactionTiming
2998
- });
2999
- if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
3000
- logVerbose$1("Skipping auto-reply: silent token or no text/media returned from resolver");
3001
- return false;
3002
- }
3003
- if (statusReactionController) finalizeWhatsAppStatusReaction({
3004
- controller: statusReactionController,
3005
- outcome: didDeliverVisibleReply ? "done" : "error",
3006
- hasFinalResponse: didDeliverVisibleReply,
3007
- removeAckAfterReply,
3008
- timing: statusReactionTiming
3009
- });
3010
- if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
3011
- return didDeliverVisibleReply;
3214
+ };
3012
3215
  }
3013
3216
  async function finalizeWhatsAppStatusReaction(params) {
3014
3217
  if (params.outcome === "done") {
@@ -3343,24 +3546,13 @@ async function processMessage(params) {
3343
3546
  normalizeE164
3344
3547
  });
3345
3548
  const shouldCheckCommandAuth = shouldComputeCommandAuthorized(commandBody, params.cfg);
3346
- const isTextCommand = isControlCommandMessage$1(commandBody, params.cfg);
3549
+ const isTextCommand = isControlCommandMessage(commandBody, params.cfg);
3347
3550
  const commandAuthorized = shouldCheckCommandAuth ? await resolveWhatsAppCommandAuthorized({
3348
3551
  cfg: params.cfg,
3349
3552
  msg: params.msg,
3350
3553
  policy: inboundPolicy,
3351
3554
  authDir: account.authDir
3352
3555
  }) : void 0;
3353
- const commandTurn = isTextCommand ? {
3354
- kind: "text-slash",
3355
- source: "text",
3356
- authorized: Boolean(commandAuthorized),
3357
- body: commandBody
3358
- } : {
3359
- kind: "normal",
3360
- source: "message",
3361
- authorized: false,
3362
- body: commandBody
3363
- };
3364
3556
  const { onModelSelected, ...replyPipeline } = createChannelMessageReplyPipeline({
3365
3557
  cfg: params.cfg,
3366
3558
  agentId: params.route.agentId,
@@ -3384,9 +3576,11 @@ async function processMessage(params) {
3384
3576
  const ctxPayload = await buildWhatsAppInboundContext({
3385
3577
  bodyForAgent: msgForAgent.payload.body,
3386
3578
  combinedBody,
3387
- commandBody,
3388
- commandAuthorized,
3389
- commandTurn,
3579
+ command: {
3580
+ kind: isTextCommand ? "text-slash" : "normal",
3581
+ body: commandBody,
3582
+ authorized: commandAuthorized
3583
+ },
3390
3584
  groupHistory: visibleGroupHistory,
3391
3585
  groupMemberRoster: params.groupMemberNames.get(params.groupHistoryKey),
3392
3586
  groupSystemPrompt: conversationSystemPrompt,
@@ -3424,6 +3618,7 @@ async function processMessage(params) {
3424
3618
  updateLastRoute: updateLastRouteInBackground,
3425
3619
  warn: params.replyLogger.warn.bind(params.replyLogger)
3426
3620
  });
3621
+ let finalizeReply;
3427
3622
  const turnResult = await runChannelInboundEvent({
3428
3623
  channel: "whatsapp",
3429
3624
  accountId: params.route.accountId,
@@ -3457,26 +3652,8 @@ async function processMessage(params) {
3457
3652
  recordHistory: false
3458
3653
  } };
3459
3654
  },
3460
- resolveTurn: () => ({
3461
- channel: "whatsapp",
3462
- accountId: params.route.accountId,
3463
- routeSessionKey: params.route.sessionKey,
3464
- storePath,
3465
- ctxPayload,
3466
- recordInboundSession,
3467
- record: {
3468
- onRecordError: (err) => {
3469
- params.replyLogger.warn({
3470
- error: formatError(err),
3471
- storePath,
3472
- sessionKey: params.route.sessionKey
3473
- }, "failed updating session meta");
3474
- },
3475
- trackSessionMetaTask: (task) => {
3476
- trackBackgroundTask(params.backgroundTasks, task);
3477
- }
3478
- },
3479
- runDispatch: () => dispatchWhatsAppBufferedReply({
3655
+ resolveTurn: () => {
3656
+ const { finalize, ...replyPlan } = createWhatsAppReplyPlan({
3480
3657
  cfg: params.cfg,
3481
3658
  connectionId: params.connectionId,
3482
3659
  context: ctxPayload,
@@ -3497,11 +3674,35 @@ async function processMessage(params) {
3497
3674
  route: params.route,
3498
3675
  shouldClearGroupHistory,
3499
3676
  statusReactionController
3500
- })
3501
- })
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
+ }
3502
3703
  }
3503
3704
  });
3504
- const didSendReply = turnResult.dispatched ? turnResult.dispatchResult : false;
3705
+ const didSendReply = turnResult.dispatched ? finalizeReply?.(turnResult.dispatchResult) ?? false : false;
3505
3706
  removeAckReactionHandleAfterReply({
3506
3707
  removeAfterReply: Boolean(params.cfg.messages?.removeAckAfterReply && didSendReply),
3507
3708
  ackReaction,
@@ -3845,6 +4046,7 @@ function isRetryableAuthUnstableError(error) {
3845
4046
  return error instanceof WhatsAppAuthUnstableError || typeof error === "object" && error !== null && "code" in error && error.code === "whatsapp-auth-unstable";
3846
4047
  }
3847
4048
  const DEFAULT_TRANSPORT_TIMEOUT_MS = 300 * 1e3;
4049
+ const WHATSAPP_RECONNECT_CATCH_UP_MAX_MS = 20 * 6e4;
3848
4050
  async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSocket, keepAlive = true, replyResolver, runtime = defaultRuntime, abortSignal, tuning = {}) {
3849
4051
  const activeReplyResolver = replyResolver ?? (await loadReplyResolverRuntime()).getReplyFromConfig;
3850
4052
  const runId = newConnectionId();
@@ -3896,7 +4098,7 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
3896
4098
  process.once("SIGINT", handleSigint);
3897
4099
  const transportTimeoutMs = tuning.transportTimeoutMs ?? DEFAULT_TRANSPORT_TIMEOUT_MS;
3898
4100
  const messageTimeoutMs = tuning.messageTimeoutMs ?? 1800 * 1e3;
3899
- 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);
3900
4102
  const watchdogCheckMs = tuning.watchdogCheckMs ?? 60 * 1e3;
3901
4103
  const controller = new WhatsAppConnectionController({
3902
4104
  accountId: account.accountId,
@@ -3930,10 +4132,12 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
3930
4132
  });
3931
4133
  const shouldDebounce = (msg) => {
3932
4134
  const normalized = normalizeWebInboundMessage(msg);
3933
- if (normalized.payload.media?.path || normalized.payload.media?.type) return false;
3934
- if (normalized.payload.location) return false;
3935
- if (normalized.quote?.id || normalized.quote?.body) return false;
3936
- return !isControlCommandMessage(normalized.payload.commandBody ?? normalized.payload.body, cfg);
4135
+ return shouldDebounceTextInbound({
4136
+ text: normalized.payload.commandBody ?? normalized.payload.body,
4137
+ cfg,
4138
+ hasMedia: Boolean(normalized.payload.media?.path || normalized.payload.media?.type),
4139
+ allowDebounce: !(normalized.payload.location || normalized.quote?.id || normalized.quote?.body)
4140
+ });
3937
4141
  };
3938
4142
  let connection;
3939
4143
  try {