@openclaw/whatsapp 2026.7.2-beta.4 → 2026.7.2-beta.5

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 (47) hide show
  1. package/README.md +1 -1
  2. package/dist/{access-control-CRz_v6bR.js → access-control-pZcxTmd5.js} +4 -4
  3. package/dist/{accounts-CxPuEFi-.js → accounts-CSqLvCY5.js} +0 -1
  4. package/dist/{action-runtime-CHU7xb6o.js → action-runtime-B2bNDUwE.js} +3 -3
  5. package/dist/action-runtime-api.js +1 -1
  6. package/dist/action-runtime.runtime.js +1 -1
  7. package/dist/api.js +13 -19
  8. package/dist/{approval-handler.runtime-C4ddB7Kc.js → approval-handler.runtime-CmtqgLSQ.js} +5 -5
  9. package/dist/{approval-reactions-P2HXjmln.js → approval-reactions-Cu66aYdq.js} +3 -3
  10. package/dist/{auth-store-DirYMTbo.js → auth-store-DcEpIOmE.js} +2 -2
  11. package/dist/call-tool-api.js +2 -2
  12. package/dist/{channel-B9xjrOGG.js → channel-DcWBc3Hb.js} +23 -60
  13. package/dist/channel-config-api.js +1 -1
  14. package/dist/channel-plugin-api.js +1 -1
  15. package/dist/{channel-react-action-CBW-PA8q.js → channel-react-action-DUIkjl6w.js} +5 -5
  16. package/dist/{channel.runtime-DdKuhlNu.js → channel.runtime-Q5fJ2I2b.js} +4 -4
  17. package/dist/{channel.setup-DXUCPHel.js → channel.setup-C_1u6HN6.js} +1 -1
  18. package/dist/{config-schema-C6IeKIC0.js → config-schema-DARep0Nn.js} +1 -5
  19. package/dist/{connection-controller-BVRExqrB.js → connection-controller-BOB2MVKY.js} +3 -3
  20. package/dist/contract-api.js +2 -2
  21. package/dist/{directory-config-BTbJkV1W.js → directory-config-DalGrfGN.js} +5 -5
  22. package/dist/directory-contract-api.js +1 -1
  23. package/dist/{group-session-key-DJSdYfnb.js → group-session-key-DVMZlO3x.js} +5 -5
  24. package/dist/{identity-BM4_wZeM.js → identity-CPrT5aEU.js} +2 -2
  25. package/dist/light-runtime-api.js +1 -1
  26. package/dist/{login-BKUm7KSk.js → login-CvY2Dhch.js} +4 -4
  27. package/dist/{login-qr-DagJvMbg.js → login-qr-Bncpb6js.js} +4 -4
  28. package/dist/login-qr-runtime.js +1 -1
  29. package/dist/{monitor-CBVNJ2lb.js → monitor-B7phZF9w.js} +258 -330
  30. package/dist/normalize-C7Hkc_vE.js +2 -0
  31. package/dist/{normalize-target-DSZnHkKF.js → normalize-target-CugRyONv.js} +6 -2
  32. package/dist/{question-reactions-CaOrSYzU.js → question-reactions-CRsnyoMK.js} +1 -1
  33. package/dist/{resolve-outbound-target-BR97fAbB.js → resolve-outbound-target-1GqbT1uX.js} +1 -1
  34. package/dist/runtime-api.js +7 -7
  35. package/dist/{send-DhJVN30g.js → send-BTXJAyK4.js} +5 -5
  36. package/dist/{send-api-CpHwbor_.js → send-api-BY_AkSiB.js} +7 -7
  37. package/dist/{setup-finalize-9XzKdFq6.js → setup-finalize-C-m-vCl9.js} +4 -4
  38. package/dist/setup-plugin-api.js +1 -1
  39. package/dist/{setup-surface-Kv3mPki2.js → setup-surface-BjvUbR6M.js} +3 -3
  40. package/dist/{shared-CuqHFnKd.js → shared-UcAwtJaq.js} +6 -6
  41. package/dist/{socket-close-BoSHhQNJ.js → socket-close-DridEuzz.js} +1 -1
  42. package/dist/{targets-runtime-CKFrhoX4.js → targets-runtime-TMk0lGuy.js} +4 -13
  43. package/dist/{text-runtime-CU_IlbK5.js → text-runtime-CaKLhowd.js} +1 -1
  44. package/openclaw.plugin.json +2 -18
  45. package/package.json +4 -5
  46. package/dist/normalize-C-Z16y7Q.js +0 -2
  47. package/npm-shrinkwrap.json +0 -954
@@ -1,22 +1,21 @@
1
- import { a as resolveWhatsAppAccount, s as resolveWhatsAppMediaMaxBytes } from "./accounts-CxPuEFi-.js";
2
- import { a as normalizeWhatsAppOutboundPayload, c as prepareWhatsAppOutboundMedia, r as sendReactionWhatsApp, s as normalizeWhatsAppPayloadTextPreservingIndentation, u as resolveWhatsAppReactionLevel } from "./send-DhJVN30g.js";
1
+ import { a as resolveWhatsAppAccount, s as resolveWhatsAppMediaMaxBytes } from "./accounts-CSqLvCY5.js";
2
+ import { a as normalizeWhatsAppOutboundPayload, c as prepareWhatsAppOutboundMedia, r as sendReactionWhatsApp, s as normalizeWhatsAppPayloadTextPreservingIndentation, u as resolveWhatsAppReactionLevel } from "./send-BTXJAyK4.js";
3
3
  import { i as getWhatsAppRuntime } from "./runtime-BLlToOi6.js";
4
4
  import { r as getWhatsAppConnectionController } from "./connection-controller-runtime-context-DG0tuYuw.js";
5
- import { a as markdownToWhatsAppChunks, c as toWhatsappJid, l as toWhatsappJidWithLid, n as isSelfChatMode, o as resolveEquivalentWhatsAppDirectChatJids, r as jidToE164, s as resolveJidToE164 } from "./targets-runtime-CKFrhoX4.js";
6
- import { t as normalizeE164 } from "./text-runtime-CU_IlbK5.js";
7
- import { a as cacheInboundMessageMeta, i as buildQuotedMessageOptions, n as resolveWhatsAppGroupSessionRoute, o as lookupInboundMessageMeta, r as resolveWhatsAppLegacyGroupSessionKey } from "./group-session-key-DJSdYfnb.js";
8
- import { g as readWebSelfId, o as getWebAuthAgeMs, r as WhatsAppAuthUnstableError, v as readWebSelfIdentityForDecision } from "./auth-store-DirYMTbo.js";
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-BM4_wZeM.js";
5
+ import { a as markdownToWhatsAppChunks, c as toWhatsappJid, l as toWhatsappJidWithLid, n as isSelfChatMode, o as resolveEquivalentWhatsAppDirectChatJids, r as jidToE164, s as resolveJidToE164 } from "./targets-runtime-TMk0lGuy.js";
6
+ import { t as normalizeE164 } from "./text-runtime-CaKLhowd.js";
7
+ import { a as cacheInboundMessageMeta, i as buildQuotedMessageOptions, n as resolveWhatsAppGroupSessionRoute, o as lookupInboundMessageMeta, r as resolveWhatsAppLegacyGroupSessionKey } from "./group-session-key-DVMZlO3x.js";
8
+ import { g as readWebSelfId, o as getWebAuthAgeMs, r as WhatsAppAuthUnstableError, v as readWebSelfIdentityForDecision } from "./auth-store-DcEpIOmE.js";
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-CPrT5aEU.js";
10
10
  import { n as resolveWhatsAppGroupsConfigPath } from "./group-config-path-BGyzT9Lg.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-BoSHhQNJ.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-DridEuzz.js";
12
12
  import { n as getStatusCode, t as formatError } from "./session-errors-JuazczrA.js";
13
13
  import { t as BufferJSON } from "./session.runtime-Nsqaehbd.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-CRz_v6bR.js";
15
- import { _ as extractMediaKind, a as mayContainWhatsAppOutboundMention, b as hasInboundUserContent, c as DisconnectReason, d as normalizeMessageContent, f as describeReplyContext, g as extractLocationData, h as extractExternalAdReplyContext, i as addWhatsAppOutboundMentionsToContent, l as downloadMediaMessage, m as extractContextInfo, n as listWhatsAppSendResultMessageIds, o as resolveWhatsAppOutboundMentions, p as extractContactContext, r as normalizeWhatsAppSendResult, s as addWhatsAppImagePreviewFields, t as createWebSendApi, u as isJidGroup, v as extractMentionedJids, x as resolveInboundMediaMimetype, y as extractText } from "./send-api-CpHwbor_.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-BVRExqrB.js";
17
- import { n as maybeResolveWhatsAppApprovalReaction } from "./approval-reactions-P2HXjmln.js";
18
- import { t as maybeResolveWhatsAppQuestionReaction } from "./question-reactions-CaOrSYzU.js";
19
- import { resolveAccountEntry } from "openclaw/plugin-sdk/account-core";
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-pZcxTmd5.js";
15
+ import { _ as extractMediaKind, a as mayContainWhatsAppOutboundMention, b as hasInboundUserContent, c as DisconnectReason, d as normalizeMessageContent, f as describeReplyContext, g as extractLocationData, h as extractExternalAdReplyContext, i as addWhatsAppOutboundMentionsToContent, l as downloadMediaMessage, m as extractContextInfo, n as listWhatsAppSendResultMessageIds, o as resolveWhatsAppOutboundMentions, p as extractContactContext, r as normalizeWhatsAppSendResult, s as addWhatsAppImagePreviewFields, t as createWebSendApi, u as isJidGroup, v as extractMentionedJids, x as resolveInboundMediaMimetype, y as extractText } from "./send-api-BY_AkSiB.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-BOB2MVKY.js";
17
+ import { n as maybeResolveWhatsAppApprovalReaction } from "./approval-reactions-Cu66aYdq.js";
18
+ import { t as maybeResolveWhatsAppQuestionReaction } from "./question-reactions-CRsnyoMK.js";
20
19
  import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
21
20
  import { filterSupplementalContextItems, resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
22
21
  import { createAckReactionHandle, createStatusReactionController, logAckFailure, removeAckReactionHandleAfterReply, shouldAckReactionForWhatsApp } from "openclaw/plugin-sdk/channel-feedback";
@@ -25,15 +24,16 @@ import { getChildLogger } from "openclaw/plugin-sdk/logging-core";
25
24
  import { resolveMarkdownTableMode as resolveMarkdownTableMode$1 } from "openclaw/plugin-sdk/markdown-table-runtime";
26
25
  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
26
  import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
28
- import { bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, createChannelMessageReplyPipeline, createMessageReceiptFromOutboundResults, deliverInboundReplyWithMessageSendContext, resolveChannelMessageSourceReplyDeliveryMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
27
+ import { bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, createChannelMessageReplyPipeline, createMessageReceiptFromOutboundResults, resolveChannelMessageSourceReplyDeliveryMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
29
28
  import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
30
29
  import { isReasoningReplyPayload, resolveSendableOutboundReplyParts, sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";
31
30
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
32
31
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
33
- import { buildChannelInboundEventContext, filterChannelInboundQuoteContext, formatInboundEnvelope, formatInboundEnvelope as formatInboundEnvelope$1, formatInboundMediaUnavailableText, formatLocationText, formatMediaPlaceholderText, hasVisibleInboundReplyDispatch, resolveInboundSessionEnvelopeContext, resolveInboundSupplementalSenderAllowed, runChannelInboundEvent, shouldDebounceTextInbound, toInboundMediaFacts, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
34
- 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";
32
+ import { buildChannelInboundEventContext, filterChannelInboundQuoteContext, formatInboundEnvelope, formatInboundEnvelope as formatInboundEnvelope$1, formatInboundMediaUnavailableText, formatLocationText, formatMediaPlaceholderText, hasVisibleInboundReplyDispatch, isChannelPartialDeliveryError, resolveInboundSessionEnvelopeContext, resolveInboundSupplementalSenderAllowed, runChannelInboundEvent, shouldDebounceTextInbound, toInboundMediaFacts, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
33
+ import { DEFAULT_ACCOUNT_ID, DEFAULT_MAIN_KEY, buildAgentMainSessionKey, buildAgentSessionKey, buildGroupHistoryKey, deriveLastRoutePolicy, normalizeAccountId, normalizeAgentId, resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
35
34
  import { asDateTimestampMs, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
36
35
  import { createHash } from "node:crypto";
36
+ import { fanInChannelIngressLifecycles } from "openclaw/plugin-sdk/channel-ingress-runtime";
37
37
  import { getSessionEntry, patchSessionEntry, resolveStorePath, resolveStorePath as resolveStorePath$1, updateLastRoute } from "openclaw/plugin-sdk/session-store-runtime";
38
38
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
39
39
  import { resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
@@ -44,7 +44,7 @@ import { DEFAULT_GROUP_HISTORY_LIMIT, buildHistoryContextFromEntries, buildInbou
44
44
  import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
45
45
  import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
46
46
  import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
47
- import { getRuntimeConfig as getRuntimeConfig$1, getRuntimeConfigSourceSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot";
47
+ import { getRuntimeConfig as getRuntimeConfig$1 } from "openclaw/plugin-sdk/runtime-config-snapshot";
48
48
  import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
49
49
  import { buildMentionRegexes, implicitMentionKindWhen, normalizeMentionText, resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-mention-gating";
50
50
  import { createConnectedChannelStatusPatch, createTransportActivityStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
@@ -127,6 +127,18 @@ function defineDeprecatedAliasAccessors(msg, descriptors) {
127
127
  defineDeprecatedAccessors(msg, descriptors);
128
128
  return msg;
129
129
  }
130
+ function defineDeprecatedStructuredContextPayloadAlias(msg) {
131
+ const channelStructuredContext = msg.payload.channelStructuredContext ?? msg.payload.untrustedStructuredContext;
132
+ if (channelStructuredContext !== void 0) msg.payload.channelStructuredContext = channelStructuredContext;
133
+ Object.defineProperty(msg.payload, "untrustedStructuredContext", {
134
+ configurable: true,
135
+ enumerable: true,
136
+ get: () => msg.payload.channelStructuredContext,
137
+ set: (value) => {
138
+ msg.payload.channelStructuredContext = value;
139
+ }
140
+ });
141
+ }
130
142
  function defineDeprecatedAdmissionTopLevelAccessors(msg) {
131
143
  let fallbackConversationId = msg.conversationId || msg.from;
132
144
  let fallbackAccountId = msg.accountId;
@@ -178,6 +190,7 @@ function defineDeprecatedAdmissionTopLevelAccessors(msg) {
178
190
  });
179
191
  }
180
192
  function withDeprecatedWebInboundMessageFlatAliases(msg) {
193
+ defineDeprecatedStructuredContextPayloadAlias(msg);
181
194
  return defineDeprecatedAliasAccessors(defineDeprecatedAdmissionTopLevelAccessors(msg), {
182
195
  id: {
183
196
  get: () => msg.event.id,
@@ -323,9 +336,13 @@ function withDeprecatedWebInboundMessageFlatAliases(msg) {
323
336
  get: () => msg.payload.media?.url,
324
337
  set: (value) => setMediaField(msg, "url", value)
325
338
  },
339
+ channelStructuredContext: {
340
+ get: () => msg.payload.channelStructuredContext,
341
+ set: (value) => msg.payload.channelStructuredContext = value
342
+ },
326
343
  untrustedStructuredContext: {
327
- get: () => msg.payload.untrustedStructuredContext,
328
- set: (value) => msg.payload.untrustedStructuredContext = value
344
+ get: () => msg.payload.channelStructuredContext,
345
+ set: (value) => msg.payload.channelStructuredContext = value
329
346
  },
330
347
  isBatched: {
331
348
  get: () => msg.event.isBatched,
@@ -352,7 +369,7 @@ function normalizeLegacyFlatWebInboundMessage(msg) {
352
369
  body: msg.body,
353
370
  media,
354
371
  location: msg.location,
355
- untrustedStructuredContext: msg.untrustedStructuredContext
372
+ channelStructuredContext: msg.channelStructuredContext ?? msg.untrustedStructuredContext
356
373
  },
357
374
  platform: {
358
375
  chatJid: msg.chatId,
@@ -467,22 +484,6 @@ function createWhatsAppDurableInboundQueue(accountId) {
467
484
  stateDir: getWhatsAppRuntime().state.resolveStateDir()
468
485
  });
469
486
  }
470
- /** Raw receive chokepoint: append first, then let the drain normalize and dispatch. */
471
- async function enqueueWhatsAppDurableInbound(params) {
472
- const facts = inspectWhatsAppIngressMessage(params.message);
473
- const receivedAt = params.receivedAt ?? Date.now();
474
- return await params.queue.enqueue(facts.eventId, {
475
- message: serializeWhatsAppDurableInboundMessage(params.message),
476
- upsertType: params.upsertType,
477
- ...params.skipStaleAppend === void 0 ? {} : { skipStaleAppend: params.skipStaleAppend },
478
- ...params.skipRecentOutboundEcho === void 0 ? {} : { skipRecentOutboundEcho: params.skipRecentOutboundEcho },
479
- receivedAt,
480
- ...params.receiveOrder === void 0 ? {} : { receiveOrder: params.receiveOrder }
481
- }, {
482
- receivedAt,
483
- laneKey: facts.laneKey
484
- });
485
- }
486
487
  function resolveWhatsAppIngressNonRetryableFailure(error) {
487
488
  return error instanceof WhatsAppIngressPermanentError ? {
488
489
  reason: error.reason,
@@ -493,14 +494,18 @@ function resolveWhatsAppIngressNonRetryableFailure(error) {
493
494
  function createWhatsAppIngressMonitor(params) {
494
495
  return createChannelIngressMonitor({
495
496
  queue: params.queue,
496
- inspect: (message) => inspectWhatsAppIngressMessage(message),
497
+ inspect: (admission) => inspectWhatsAppIngressMessage(admission.message),
497
498
  payload: {
498
499
  version: WHATSAPP_DURABLE_INBOUND_PAYLOAD_VERSION,
499
- serialize: (message, { receivedAt }) => ({
500
- message: serializeWhatsAppDurableInboundMessage(message),
500
+ serialize: (admission, { receivedAt }) => ({
501
+ ...admission,
502
+ message: serializeWhatsAppDurableInboundMessage(admission.message),
501
503
  receivedAt
502
504
  }),
503
- deserialize: (payload) => deserializeWhatsAppDurableInboundMessage(payload.message),
505
+ deserialize: (payload) => ({
506
+ ...payload,
507
+ message: deserializeWhatsAppDurableInboundMessage(payload.message)
508
+ }),
504
509
  encode: ({ body }) => body,
505
510
  decode: (payload) => ({
506
511
  version: WHATSAPP_DURABLE_INBOUND_PAYLOAD_VERSION,
@@ -508,7 +513,7 @@ function createWhatsAppIngressMonitor(params) {
508
513
  }),
509
514
  createClaimError: (kind) => new WhatsAppIngressPermanentError(kind === "invalid-version" ? "invalid-payload" : "event-id-mismatch", kind === "invalid-version" ? "WhatsApp ingress row has an invalid payload version" : "WhatsApp ingress row identity does not match its transport message key")
510
515
  },
511
- deliver: (message, lifecycle, claim) => params.dispatch(message, claim.payload, lifecycle),
516
+ deliver: (admission, lifecycle) => params.dispatch(admission, lifecycle),
512
517
  pollIntervalMs: params.pollIntervalMs,
513
518
  retention: {
514
519
  pruneIntervalMs: WHATSAPP_DURABLE_INBOUND_PRUNE_INTERVAL_MS,
@@ -791,48 +796,6 @@ async function attachWebInboxToSocket(options) {
791
796
  if (timestampDiff !== 0) return timestampDiff;
792
797
  return (a.receiveOrder ?? 0) - (b.receiveOrder ?? 0);
793
798
  });
794
- const buildFlushIngressLifecycle = (entries) => {
795
- const lifecycles = entries.map((entry) => entry.turnAdoptionLifecycle).filter((lifecycle) => lifecycle !== void 0);
796
- const [firstLifecycle] = lifecycles;
797
- if (!firstLifecycle) return {
798
- lifecycle: void 0,
799
- settle: async () => {},
800
- abandon: async () => {}
801
- };
802
- let handedOff = false;
803
- const adoptAll = async () => {
804
- for (const lifecycle of lifecycles) await lifecycle.onAdopted();
805
- };
806
- return {
807
- lifecycle: {
808
- abortSignal: lifecycles.length === 1 ? firstLifecycle.abortSignal : AbortSignal.any(lifecycles.map((lifecycle) => lifecycle.abortSignal)),
809
- onAdopted: async () => {
810
- handedOff = true;
811
- await adoptAll();
812
- },
813
- onDeferred: () => {
814
- handedOff = true;
815
- for (const lifecycle of lifecycles) lifecycle.onDeferred();
816
- },
817
- onAdoptionFinalizing: () => {
818
- for (const lifecycle of lifecycles) lifecycle.onAdoptionFinalizing();
819
- },
820
- onAbandoned: async () => {
821
- handedOff = true;
822
- await Promise.all(lifecycles.map((lifecycle) => Promise.resolve(lifecycle.onAbandoned())));
823
- }
824
- },
825
- settle: async () => {
826
- if (!handedOff) await adoptAll();
827
- },
828
- abandon: async () => {
829
- if (!handedOff) {
830
- handedOff = true;
831
- await Promise.all(lifecycles.map((lifecycle) => Promise.resolve(lifecycle.onAbandoned())));
832
- }
833
- }
834
- };
835
- };
836
799
  const debouncer = createInboundDebouncer({
837
800
  debounceMs: inboundDebounceMs,
838
801
  buildKey: (msg) => msg.debounceKey ?? buildInboundDebounceKey(msg),
@@ -849,7 +812,7 @@ async function attachWebInboxToSocket(options) {
849
812
  const orderedEntries = orderDebouncedInboundEntries(entries);
850
813
  const last = orderedEntries.at(-1);
851
814
  if (!last) return;
852
- const { lifecycle, settle, abandon } = buildFlushIngressLifecycle(orderedEntries);
815
+ const { lifecycle, settle, abandon } = fanInChannelIngressLifecycles(orderedEntries.map((entry) => entry.turnAdoptionLifecycle));
853
816
  try {
854
817
  if (orderedEntries.length === 1) {
855
818
  await options.onMessage(attachWhatsAppIngressLifecycle(last, lifecycle));
@@ -1308,7 +1271,7 @@ async function attachWebInboxToSocket(options) {
1308
1271
  participants: inbound.groupParticipants,
1309
1272
  mentions: groupMentions
1310
1273
  } : void 0;
1311
- const untrustedStructuredContext = [
1274
+ const channelStructuredContext = [
1312
1275
  ...enriched.nativeMedia ? [{
1313
1276
  label: "WhatsApp media",
1314
1277
  source: "whatsapp",
@@ -1338,7 +1301,7 @@ async function attachWebInboxToSocket(options) {
1338
1301
  body: enriched.body,
1339
1302
  commandBody: enriched.commandBody,
1340
1303
  location: enriched.location ?? void 0,
1341
- untrustedStructuredContext: untrustedStructuredContext.length > 0 ? untrustedStructuredContext : void 0,
1304
+ channelStructuredContext: channelStructuredContext.length > 0 ? channelStructuredContext : void 0,
1342
1305
  media
1343
1306
  },
1344
1307
  platform: {
@@ -1399,11 +1362,15 @@ async function attachWebInboxToSocket(options) {
1399
1362
  }
1400
1363
  await debouncer.enqueue(inboundMessage);
1401
1364
  };
1402
- const processDurableInboundMessage = async (msg, context) => {
1365
+ const processDurableInboundMessage = async (admission, lifecycle) => {
1366
+ const { message: msg, ...context } = admission;
1403
1367
  rememberBaileysMessage(msg.key?.remoteJid, msg.key?.id, msg.message);
1368
+ const remoteJid = msg.key?.remoteJid;
1369
+ const id = msg.key?.id;
1370
+ const durableId = remoteJid && id ? createHash("sha256").update(`${remoteJid}\n${id}`).digest("hex") : void 0;
1371
+ const preparation = durableId ? preparedInboundByDurableId.get(durableId) : void 0;
1372
+ if (durableId) preparedInboundByDurableId.delete(durableId);
1404
1373
  if (context.skipRecentOutboundEcho === true) return "completed";
1405
- const preparation = context.eventId ? preparedInboundByDurableId.get(context.eventId) : void 0;
1406
- if (context.eventId) preparedInboundByDurableId.delete(context.eventId);
1407
1374
  const prepared = await preparation;
1408
1375
  if (prepared === null) return "completed";
1409
1376
  const inbound = prepared ?? await normalizeInboundMessage(msg);
@@ -1431,24 +1398,13 @@ async function attachWebInboxToSocket(options) {
1431
1398
  await enqueueInboundMessage(msg, inbound, enriched, {
1432
1399
  readReceipt: deliveryReadReceipt,
1433
1400
  receiveOrder: context.receiveOrder ?? context.receivedAt,
1434
- turnAdoptionLifecycle: context.lifecycle
1401
+ turnAdoptionLifecycle: lifecycle
1435
1402
  });
1436
1403
  return "deferred";
1437
1404
  };
1438
1405
  const durableInboundMonitor = createWhatsAppIngressMonitor({
1439
1406
  queue: durableInboundQueue,
1440
- dispatch: async (msg, payload, lifecycle) => {
1441
- const remoteJid = msg.key?.remoteJid;
1442
- const id = msg.key?.id;
1443
- return { kind: await processDurableInboundMessage(msg, {
1444
- ...payload,
1445
- ...remoteJid && id ? { eventId: createWhatsAppDurableInboundMessageId({
1446
- remoteJid,
1447
- id
1448
- }) } : {},
1449
- lifecycle
1450
- }) };
1451
- },
1407
+ dispatch: async (admission, lifecycle) => ({ kind: await processDurableInboundMessage(admission, lifecycle) }),
1452
1408
  pollIntervalMs: WHATSAPP_INGRESS_DRAIN_INTERVAL_MS,
1453
1409
  onLog: (message) => inboundLogger.warn({ message }, "whatsapp ingress drain"),
1454
1410
  onError: (error) => inboundLogger.error({ error: formatError(error) }, "whatsapp durable inbound drain failed"),
@@ -1477,10 +1433,7 @@ async function attachWebInboxToSocket(options) {
1477
1433
  const skipRecentOutboundEcho = shouldSkipRecentOutboundEcho(msg);
1478
1434
  const remoteJid = msg.key?.remoteJid;
1479
1435
  const id = msg.key?.id;
1480
- const durableId = remoteJid && id ? createWhatsAppDurableInboundMessageId({
1481
- remoteJid,
1482
- id
1483
- }) : void 0;
1436
+ const durableId = remoteJid && id ? createHash("sha256").update(`${remoteJid}\n${id}`).digest("hex") : void 0;
1484
1437
  let resolvePrepared;
1485
1438
  if (durableId && !preparedInboundByDurableId.has(durableId)) {
1486
1439
  if (preparedInboundByDurableId.size >= 1e3) {
@@ -1496,52 +1449,34 @@ async function attachWebInboxToSocket(options) {
1496
1449
  if (!keepForDrain && durableId && resolvePrepared) preparedInboundByDurableId.delete(durableId);
1497
1450
  };
1498
1451
  let result;
1499
- let appendError;
1500
- for (const delayMs of [
1501
- 0,
1502
- 100,
1503
- 300
1504
- ]) {
1505
- if (delayMs > 0) await new Promise((resolve) => {
1506
- setTimeout(resolve, delayMs);
1507
- });
1508
- try {
1509
- result = await enqueueWhatsAppDurableInbound({
1510
- queue: durableInboundQueue,
1511
- message: msg,
1512
- upsertType: upsert.type,
1513
- skipStaleAppend,
1514
- skipRecentOutboundEcho,
1515
- receivedAt,
1516
- receiveOrder
1517
- });
1518
- appendError = void 0;
1519
- break;
1520
- } catch (error) {
1521
- appendError = error;
1522
- }
1523
- }
1524
- if (result === void 0) {
1452
+ try {
1453
+ result = await durableInboundMonitor.admit({
1454
+ message: msg,
1455
+ upsertType: upsert.type,
1456
+ skipStaleAppend,
1457
+ skipRecentOutboundEcho,
1458
+ receivedAt,
1459
+ receiveOrder
1460
+ }, { receivedAt });
1461
+ } catch (error) {
1525
1462
  finishPreparation(void 0);
1526
- const formattedError = formatError(appendError);
1463
+ const formattedError = formatError(error);
1527
1464
  inboundLogger.error({ error: formattedError }, "failed persisting durable WhatsApp inbound after retries; message dropped");
1528
1465
  inboundConsoleLog.error(`Failed persisting durable WhatsApp inbound after retries; message dropped: ${formattedError}`);
1529
1466
  continue;
1530
1467
  }
1531
- if (result.kind === "completed") {
1468
+ if (result.kind === "durable" && result.queueResult.kind === "completed") {
1532
1469
  finishPreparation(void 0);
1533
1470
  const inbound = await normalizeInboundMessage(msg);
1534
1471
  if (inbound) await maybeMarkNonSelfChatReadReceipt(inbound, buildReadReceiptTarget(inbound));
1535
- } else {
1536
- if (result.kind === "accepted") try {
1537
- finishPreparation(skipRecentOutboundEcho ? null : await normalizeInboundMessage(msg), true);
1538
- } catch (error) {
1539
- finishPreparation(void 0);
1540
- throw error;
1541
- }
1542
- else finishPreparation(void 0);
1543
- durableInboundMonitor.requestDrain();
1472
+ } else if (result.kind === "durable" && result.queueResult.kind === "accepted") if (skipRecentOutboundEcho) finishPreparation(null);
1473
+ else try {
1474
+ finishPreparation(await normalizeInboundMessage(msg), true);
1475
+ } catch (error) {
1476
+ finishPreparation(void 0);
1477
+ inboundLogger.warn({ error: formatError(error) }, "failed preparing WhatsApp inbound identity; durable drain will normalize again");
1544
1478
  }
1479
+ else finishPreparation(void 0);
1545
1480
  }
1546
1481
  };
1547
1482
  const handleMessagesUpsertEvent = (upsert) => {
@@ -1963,7 +1898,7 @@ function resolveAgentIdentityEmoji(cfg, agentId) {
1963
1898
  //#endregion
1964
1899
  //#region extensions/whatsapp/src/auto-reply/monitor/group-activation.ts
1965
1900
  function hasNamedWhatsAppAccounts(cfg) {
1966
- return Object.keys(cfg.channels?.whatsapp?.accounts ?? {}).some((accountId) => normalizeAccountId$1(accountId) !== DEFAULT_ACCOUNT_ID$1);
1901
+ return Object.keys(cfg.channels?.whatsapp?.accounts ?? {}).some((accountId) => normalizeAccountId(accountId) !== DEFAULT_ACCOUNT_ID);
1967
1902
  }
1968
1903
  function isActivationOnlyEntry(entry) {
1969
1904
  return entry?.groupActivation !== void 0 && typeof entry?.sessionId !== "string" && typeof entry?.updatedAt !== "number";
@@ -1986,7 +1921,7 @@ async function resolveGroupActivationFor(params) {
1986
1921
  ...sessionScope,
1987
1922
  sessionKey: params.sessionKey
1988
1923
  });
1989
- const activation = (normalizeAccountId$1(params.accountId) === DEFAULT_ACCOUNT_ID$1 && hasNamedWhatsAppAccounts(params.cfg) && isActivationOnlyEntry(scopedEntry) ? void 0 : scopedEntry?.groupActivation) ?? legacyEntry?.groupActivation;
1924
+ const activation = (normalizeAccountId(params.accountId) === DEFAULT_ACCOUNT_ID && hasNamedWhatsAppAccounts(params.cfg) && isActivationOnlyEntry(scopedEntry) ? void 0 : scopedEntry?.groupActivation) ?? legacyEntry?.groupActivation;
1990
1925
  if (activation !== void 0 && scopedEntry?.groupActivation === void 0) {
1991
1926
  if (scopedEntry) await patchSessionEntry({
1992
1927
  ...sessionScope,
@@ -2014,35 +1949,35 @@ function resolveReactionParticipant(msg) {
2014
1949
  return sender.jid ?? sender.lid ?? void 0;
2015
1950
  }
2016
1951
  //#endregion
2017
- //#region extensions/whatsapp/src/auto-reply/monitor/ack-reaction.ts
2018
- async function maybeSendAckReaction(params) {
2019
- if (!params.msg.event.id) return null;
1952
+ //#region extensions/whatsapp/src/auto-reply/monitor/reaction-eligibility.ts
1953
+ const DISABLED_REACTION = { status: "disabled" };
1954
+ async function resolveWhatsAppReactionEligibility(params) {
1955
+ const messageId = params.msg.event.id;
1956
+ if (!messageId) return DISABLED_REACTION;
2020
1957
  const admission = requireWhatsAppInboundAdmission(params.msg);
2021
1958
  const accountId = admission.accountId;
2022
1959
  if (resolveWhatsAppReactionLevel({
2023
1960
  cfg: params.cfg,
2024
1961
  accountId
2025
- }).level === "off") return null;
2026
- const ackConfig = params.cfg.messages?.ackReaction;
1962
+ }).level === "off") return DISABLED_REACTION;
2027
1963
  const scope = params.cfg.messages?.ackReactionScope ?? "group-mentions";
2028
- if (scope === "off" || scope === "none") return null;
1964
+ if (scope === "off" || scope === "none") return DISABLED_REACTION;
2029
1965
  const emoji = resolveWhatsAppAckEmoji({
2030
1966
  cfg: params.cfg,
2031
1967
  agentId: params.agentId,
2032
- ackConfig
1968
+ ackConfig: params.cfg.messages?.ackReaction
2033
1969
  });
2034
1970
  const directEnabled = scope === "all" || scope === "direct";
2035
1971
  const groupMode = scope === "all" || scope === "group-all" ? "always" : scope === "direct" ? "never" : "mentions";
2036
1972
  const isGroup = admission.conversation.kind === "group";
2037
- const conversationIdForCheck = admission.conversation.id;
2038
1973
  const activation = isGroup ? await resolveGroupActivationFor({
2039
1974
  cfg: params.cfg,
2040
1975
  accountId,
2041
1976
  agentId: params.agentId,
2042
1977
  sessionKey: params.sessionKey,
2043
- conversationId: conversationIdForCheck
1978
+ conversationId: admission.conversation.id
2044
1979
  }) : null;
2045
- const shouldSendReaction = () => shouldAckReactionForWhatsApp({
1980
+ if (!shouldAckReactionForWhatsApp({
2046
1981
  emoji,
2047
1982
  isDirect: admission.conversation.kind === "direct",
2048
1983
  isGroup,
@@ -2050,32 +1985,50 @@ async function maybeSendAckReaction(params) {
2050
1985
  groupMode,
2051
1986
  wasMentioned: (params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned) === true,
2052
1987
  groupActivated: activation === "always"
1988
+ })) return DISABLED_REACTION;
1989
+ const participant = resolveReactionParticipant(params.msg);
1990
+ return {
1991
+ status: "prepared",
1992
+ chatId: params.msg.platform.chatJid,
1993
+ messageId,
1994
+ emoji,
1995
+ reactionOptions: {
1996
+ verbose: params.verbose,
1997
+ fromMe: false,
1998
+ ...participant ? { participant } : {},
1999
+ accountId,
2000
+ cfg: params.cfg
2001
+ }
2002
+ };
2003
+ }
2004
+ //#endregion
2005
+ //#region extensions/whatsapp/src/auto-reply/monitor/ack-reaction.ts
2006
+ async function maybeSendAckReaction(params) {
2007
+ const eligibility = await resolveWhatsAppReactionEligibility({
2008
+ cfg: params.cfg,
2009
+ msg: params.msg,
2010
+ agentId: params.agentId,
2011
+ sessionKey: params.sessionKey,
2012
+ verbose: params.verbose
2053
2013
  });
2054
- if (!shouldSendReaction()) return null;
2014
+ if (eligibility.status === "disabled") return null;
2015
+ const { chatId, messageId, emoji, reactionOptions } = eligibility;
2055
2016
  params.info({
2056
- chatId: params.msg.platform.chatJid,
2057
- messageId: params.msg.event.id,
2017
+ chatId,
2018
+ messageId,
2058
2019
  emoji
2059
2020
  }, "sending ack reaction");
2060
- const participant = resolveReactionParticipant(params.msg);
2061
- const reactionOptions = {
2062
- verbose: params.verbose,
2063
- fromMe: false,
2064
- ...participant ? { participant } : {},
2065
- accountId,
2066
- cfg: params.cfg
2067
- };
2068
2021
  return createAckReactionHandle({
2069
2022
  ackReactionValue: emoji,
2070
- send: () => sendReactionWhatsApp(params.msg.platform.chatJid, params.msg.event.id, emoji, reactionOptions),
2071
- remove: () => sendReactionWhatsApp(params.msg.platform.chatJid, params.msg.event.id, "", reactionOptions),
2023
+ send: () => sendReactionWhatsApp(chatId, messageId, emoji, reactionOptions),
2024
+ remove: () => sendReactionWhatsApp(chatId, messageId, "", reactionOptions),
2072
2025
  onSendError: (err) => {
2073
2026
  params.warn({
2074
2027
  error: formatError(err),
2075
- chatId: params.msg.platform.chatJid,
2076
- messageId: params.msg.event.id
2028
+ chatId,
2029
+ messageId
2077
2030
  }, "failed to send ack reaction");
2078
- logVerbose(`WhatsApp ack reaction failed for chat ${params.msg.platform.chatJid}: ${formatError(err)}`);
2031
+ logVerbose(`WhatsApp ack reaction failed for chat ${chatId}: ${formatError(err)}`);
2079
2032
  }
2080
2033
  });
2081
2034
  }
@@ -2775,14 +2728,16 @@ function normalizeErrForLog(err) {
2775
2728
  function whatsAppReplyDeliveryVisibility(visibleReplySent) {
2776
2729
  return { visibleReplySent };
2777
2730
  }
2778
- function whatsAppReplyDeliveryVisibilityFromDurableResult(result) {
2779
- return whatsAppReplyDeliveryVisibility(result.visibleReplySent === true);
2731
+ function isWhatsAppVisibleDeliveryError(error) {
2732
+ return typeof error === "object" && error !== null && !Array.isArray(error) && error.visibleReplySent === true || isChannelPartialDeliveryError(error) && error.deliveryResult.visibleReplySent;
2780
2733
  }
2781
2734
  function readTrimmedString(value) {
2782
2735
  return typeof value === "string" ? value.trim() : "";
2783
2736
  }
2784
2737
  function markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult) {
2785
- return flushResult.delivered > 0 ? markWhatsAppVisibleDeliveryError(error) : error;
2738
+ if (flushResult.delivered === 0) return error;
2739
+ if (isWhatsAppVisibleDeliveryError(error)) return error;
2740
+ return markWhatsAppVisibleDeliveryError(new Error("deferred WhatsApp media delivery failed after an earlier visible send", { cause: error }));
2786
2741
  }
2787
2742
  function logWhatsAppReplyDeliveryError(params) {
2788
2743
  const admission = requireWhatsAppInboundAdmission(params.msg);
@@ -2834,31 +2789,58 @@ function shouldDeferWhatsAppMediaOnlyPayload(params) {
2834
2789
  }
2835
2790
  function createWhatsAppMediaOnlyReplyCoalescer(params) {
2836
2791
  const pendingMediaOnlyPayloads = [];
2837
- const flushExceptDuplicateMedia = async (mediaUrls) => {
2792
+ const flushWhere = async (shouldFlush) => {
2838
2793
  const flushResult = {
2839
2794
  delivered: 0,
2840
2795
  droppedDuplicateMedia: 0
2841
2796
  };
2842
- const pending = pendingMediaOnlyPayloads.splice(0);
2843
- for (const candidate of pending) {
2844
- if (mediaUrls && hasWhatsAppMediaUrlOverlap(candidate.mediaUrls, mediaUrls)) {
2845
- flushResult.droppedDuplicateMedia += 1;
2846
- continue;
2847
- }
2848
- try {
2849
- if ((await params.deliver(candidate)).visibleReplySent) flushResult.delivered += 1;
2850
- } catch (error) {
2851
- throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
2852
- }
2797
+ const candidates = [];
2798
+ const retained = [];
2799
+ for (const pending of pendingMediaOnlyPayloads.splice(0)) if (shouldFlush(pending)) candidates.push(pending);
2800
+ else retained.push(pending);
2801
+ pendingMediaOnlyPayloads.push(...retained);
2802
+ for (const [index, candidate] of candidates.entries()) try {
2803
+ const delivery = await params.deliver(candidate);
2804
+ candidate.resolveFinalization(delivery);
2805
+ if (delivery.visibleReplySent) flushResult.delivered += 1;
2806
+ } catch (error) {
2807
+ const visibleError = markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
2808
+ candidate.rejectFinalization(error);
2809
+ for (const remaining of candidates.slice(index + 1)) remaining.rejectFinalization(new Error("deferred WhatsApp media delivery was not attempted", { cause: error }));
2810
+ throw visibleError;
2853
2811
  }
2854
2812
  return flushResult;
2855
2813
  };
2856
2814
  return {
2857
2815
  defer(pending) {
2858
- pendingMediaOnlyPayloads.push(pending);
2816
+ let resolveFinalization;
2817
+ let rejectFinalization;
2818
+ const finalization = new Promise((resolve, reject) => {
2819
+ resolveFinalization = resolve;
2820
+ rejectFinalization = reject;
2821
+ });
2822
+ pendingMediaOnlyPayloads.push({
2823
+ ...pending,
2824
+ resolveFinalization,
2825
+ rejectFinalization
2826
+ });
2827
+ return finalization;
2859
2828
  },
2860
- flushExceptDuplicateMedia,
2861
- flushAll: () => flushExceptDuplicateMedia()
2829
+ flushNonDuplicateMedia: (mediaUrls) => flushWhere((pending) => !hasWhatsAppMediaUrlOverlap(pending.mediaUrls, mediaUrls)),
2830
+ dropDuplicateMedia(mediaUrls) {
2831
+ const flushResult = {
2832
+ delivered: 0,
2833
+ droppedDuplicateMedia: 0
2834
+ };
2835
+ const retained = [];
2836
+ for (const pending of pendingMediaOnlyPayloads.splice(0)) if (hasWhatsAppMediaUrlOverlap(pending.mediaUrls, mediaUrls)) {
2837
+ pending.resolveFinalization(whatsAppReplyDeliveryVisibility(false));
2838
+ flushResult.droppedDuplicateMedia += 1;
2839
+ } else retained.push(pending);
2840
+ pendingMediaOnlyPayloads.push(...retained);
2841
+ return flushResult;
2842
+ },
2843
+ flushAll: () => flushWhere(() => true)
2862
2844
  };
2863
2845
  }
2864
2846
  function logWhatsAppMediaOnlyFlushResult(result) {
@@ -2900,7 +2882,7 @@ async function buildWhatsAppInboundContext(params) {
2900
2882
  sender: params.visibleReplyTo.sender?.label ?? void 0
2901
2883
  } : void 0,
2902
2884
  groupSystemPrompt: params.groupSystemPrompt,
2903
- untrustedContext: params.msg.payload.untrustedStructuredContext
2885
+ channelStructuredContext: params.msg.payload.channelStructuredContext
2904
2886
  },
2905
2887
  media,
2906
2888
  messageId: params.msg.event.id,
@@ -3009,7 +2991,20 @@ function createWhatsAppReplyPlan(params) {
3009
2991
  const disableBlockStreaming = sourceRepliesAreToolOnly ? true : resolveWhatsAppDisableBlockStreaming(params.cfg);
3010
2992
  let didSendReply = false;
3011
2993
  let didLogHeartbeatStrip = false;
3012
- const deliverNormalizedPayload = async (normalizedDeliveryPayload, info) => {
2994
+ const recordDeliveredPayload = (payload) => {
2995
+ didSendReply = true;
2996
+ const shouldLog = payload.text ? true : void 0;
2997
+ params.rememberSentText(payload.text, {
2998
+ combinedBody: params.context.Body,
2999
+ combinedBodySessionKey: params.route.sessionKey,
3000
+ logVerboseMessage: shouldLog
3001
+ });
3002
+ if (shouldLogVerbose$1()) {
3003
+ const reply = resolveSendableOutboundReplyParts(payload);
3004
+ logVerbose$1(`Reply body: ${payload.text != null ? reply.text : "<media>"}${reply.hasMedia ? " (media)" : ""} -> ${conversationId}`);
3005
+ }
3006
+ };
3007
+ const deliverNormalizedPayload = async (normalizedDeliveryPayload, info, options) => {
3013
3008
  const reply = resolveSendableOutboundReplyParts(normalizedDeliveryPayload);
3014
3009
  if (!reply.hasMedia && !reply.text.trim()) return whatsAppReplyDeliveryVisibility(false);
3015
3010
  if (!(await params.deliverReply({
@@ -3036,15 +3031,11 @@ function createWhatsAppReplyPlan(params) {
3036
3031
  }, "auto-reply was not accepted by WhatsApp provider");
3037
3032
  return whatsAppReplyDeliveryVisibility(false);
3038
3033
  }
3039
- didSendReply = true;
3040
- const shouldLog = normalizedDeliveryPayload.text ? true : void 0;
3041
- params.rememberSentText(normalizedDeliveryPayload.text, {
3042
- combinedBody: params.context.Body,
3043
- combinedBodySessionKey: params.route.sessionKey,
3044
- logVerboseMessage: shouldLog
3045
- });
3046
- const fromDisplay = conversationId;
3047
- if (shouldLogVerbose$1()) logVerbose$1(`Reply body: ${normalizedDeliveryPayload.text != null ? reply.text : "<media>"}${reply.hasMedia ? " (media)" : ""} -> ${fromDisplay}`);
3034
+ if (options?.recordDelivery !== false) try {
3035
+ recordDeliveredPayload(normalizedDeliveryPayload);
3036
+ } catch (error) {
3037
+ throw markWhatsAppVisibleDeliveryError(error);
3038
+ }
3048
3039
  return whatsAppReplyDeliveryVisibility(true);
3049
3040
  };
3050
3041
  const mediaOnlyCoalescer = createWhatsAppMediaOnlyReplyCoalescer({ deliver: async (pending) => {
@@ -3065,90 +3056,82 @@ function createWhatsAppReplyPlan(params) {
3065
3056
  onSettled: async () => {
3066
3057
  const flushResult = await mediaOnlyCoalescer.flushAll();
3067
3058
  logWhatsAppMediaOnlyFlushResult(flushResult);
3068
- return whatsAppReplyDeliveryVisibility(flushResult.delivered > 0);
3059
+ return whatsAppReplyDeliveryVisibility(didSendReply || flushResult.delivered > 0);
3069
3060
  },
3070
3061
  onReplyStart: params.msg.platform.sendComposing
3071
3062
  },
3072
3063
  delivery: {
3073
- deliver: async (payload, info) => {
3064
+ preparePayload: async (payload, info) => {
3074
3065
  const deliveryPayload = resolveWhatsAppDeliverablePayload(payload, info);
3075
- if (!deliveryPayload) return whatsAppReplyDeliveryVisibility(false);
3066
+ if (!deliveryPayload) return null;
3076
3067
  const normalizedOutboundPayload = normalizeWhatsAppOutboundPayload(deliveryPayload, { normalizeText: normalizeWhatsAppPayloadTextPreservingIndentation });
3077
3068
  const normalizedDeliveryPayload = deliveryPayload.text === void 0 ? {
3078
3069
  ...normalizedOutboundPayload,
3079
3070
  text: void 0
3080
3071
  } : normalizedOutboundPayload;
3081
3072
  const reply = resolveSendableOutboundReplyParts(normalizedDeliveryPayload);
3082
- if (!reply.hasMedia && !reply.text.trim()) return whatsAppReplyDeliveryVisibility(false);
3083
- if (!reply.hasMedia) {
3084
- const flushResult = await mediaOnlyCoalescer.flushAll();
3085
- logWhatsAppMediaOnlyFlushResult(flushResult);
3086
- try {
3087
- const durable = await deliverInboundReplyWithMessageSendContext({
3088
- cfg: params.cfg,
3089
- channel: "whatsapp",
3090
- accountId: params.route.accountId,
3091
- agentId: params.route.agentId,
3092
- ctxPayload: params.context,
3093
- payload: normalizedDeliveryPayload,
3094
- info,
3095
- to: conversationId,
3096
- replyToId: resolveWhatsAppDurableReplyToId({
3097
- context: params.context,
3098
- info,
3099
- msg: params.msg,
3100
- payload: normalizedDeliveryPayload
3101
- }),
3102
- formatting: {
3103
- textLimit,
3104
- tableMode,
3105
- chunkMode
3106
- }
3107
- });
3108
- if (durable.status === "failed") {
3109
- if (durable.sentBeforeError === true) throw markWhatsAppVisibleDeliveryError(durable.error);
3110
- throw durable.error;
3111
- }
3112
- if (durable.status === "handled_visible") {
3113
- didSendReply = true;
3114
- const shouldLog = normalizedDeliveryPayload.text ? true : void 0;
3115
- params.rememberSentText(normalizedDeliveryPayload.text, {
3116
- combinedBody: params.context.Body,
3117
- combinedBodySessionKey: params.route.sessionKey,
3118
- logVerboseMessage: shouldLog
3119
- });
3120
- return whatsAppReplyDeliveryVisibilityFromDurableResult(durable.delivery);
3121
- }
3122
- if (durable.status === "handled_no_send") return flushResult.delivered > 0 ? whatsAppReplyDeliveryVisibility(true) : whatsAppReplyDeliveryVisibilityFromDurableResult(durable.delivery);
3123
- const deliveryResult = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
3124
- return flushResult.delivered > 0 && !deliveryResult.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : deliveryResult;
3125
- } catch (error) {
3126
- throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
3073
+ if (!reply.hasMedia && !reply.text.trim()) return normalizedDeliveryPayload;
3074
+ const mediaUrls = getWhatsAppPayloadMediaUrls(normalizedDeliveryPayload);
3075
+ logWhatsAppMediaOnlyFlushResult(reply.hasMedia ? shouldDeferWhatsAppMediaOnlyPayload({
3076
+ info,
3077
+ mediaUrls,
3078
+ reply
3079
+ }) ? {
3080
+ delivered: 0,
3081
+ droppedDuplicateMedia: 0
3082
+ } : await mediaOnlyCoalescer.flushNonDuplicateMedia(mediaUrls) : await mediaOnlyCoalescer.flushAll());
3083
+ return normalizedDeliveryPayload;
3084
+ },
3085
+ durable: (payload, info) => {
3086
+ const reply = resolveSendableOutboundReplyParts(payload);
3087
+ if (reply.hasMedia || !reply.text.trim()) return false;
3088
+ return {
3089
+ to: conversationId,
3090
+ replyToId: resolveWhatsAppDurableReplyToId({
3091
+ context: params.context,
3092
+ info,
3093
+ msg: params.msg,
3094
+ payload
3095
+ }),
3096
+ formatting: {
3097
+ textLimit,
3098
+ tableMode,
3099
+ chunkMode
3127
3100
  }
3128
- }
3101
+ };
3102
+ },
3103
+ deliver: async (payload, info) => {
3104
+ const normalizedDeliveryPayload = payload;
3105
+ const reply = resolveSendableOutboundReplyParts(normalizedDeliveryPayload);
3106
+ if (!reply.hasMedia && !reply.text.trim()) return whatsAppReplyDeliveryVisibility(false);
3107
+ if (!reply.hasMedia) return await deliverNormalizedPayload(normalizedDeliveryPayload, info, { recordDelivery: false });
3129
3108
  const mediaUrls = getWhatsAppPayloadMediaUrls(normalizedDeliveryPayload);
3130
3109
  if (shouldDeferWhatsAppMediaOnlyPayload({
3131
3110
  info,
3132
3111
  mediaUrls,
3133
3112
  reply
3134
- })) {
3135
- mediaOnlyCoalescer.defer({
3113
+ })) return {
3114
+ visibleReplySent: false,
3115
+ finalization: mediaOnlyCoalescer.defer({
3136
3116
  info,
3137
3117
  mediaUrls,
3138
3118
  payload: normalizedDeliveryPayload
3139
- });
3140
- return whatsAppReplyDeliveryVisibility(false);
3141
- }
3142
- const flushResult = await mediaOnlyCoalescer.flushExceptDuplicateMedia(mediaUrls);
3143
- logWhatsAppMediaOnlyFlushResult(flushResult);
3119
+ })
3120
+ };
3144
3121
  try {
3145
- const deliveryResult = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
3146
- return flushResult.delivered > 0 && !deliveryResult.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : deliveryResult;
3122
+ const result = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
3123
+ if (result.visibleReplySent) logWhatsAppMediaOnlyFlushResult(mediaOnlyCoalescer.dropDuplicateMedia(mediaUrls));
3124
+ return result;
3147
3125
  } catch (error) {
3148
- throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
3126
+ if (isWhatsAppVisibleDeliveryError(error)) logWhatsAppMediaOnlyFlushResult(mediaOnlyCoalescer.dropDuplicateMedia(mediaUrls));
3127
+ throw error;
3149
3128
  }
3150
3129
  },
3130
+ onDelivered: (payload, _info, result) => {
3131
+ if (!resolveSendableOutboundReplyParts(payload).hasMedia && result?.visibleReplySent === true) recordDeliveredPayload(payload);
3132
+ },
3151
3133
  onError: (err, info) => {
3134
+ if (didSendReply) markWhatsAppVisibleDeliveryError(err);
3152
3135
  logWhatsAppReplyDeliveryError({
3153
3136
  err,
3154
3137
  info,
@@ -3182,7 +3165,7 @@ function createWhatsAppReplyPlan(params) {
3182
3165
  finalize: (dispatchResult) => {
3183
3166
  const didQueueVisibleReply = hasVisibleInboundReplyDispatch(dispatchResult);
3184
3167
  const didDeliverVisibleReply = didSendReply || dispatchResult.observedReplyDelivery === true;
3185
- if (!didQueueVisibleReply) {
3168
+ if (!didQueueVisibleReply && !didDeliverVisibleReply) {
3186
3169
  if (statusReactionController) finalizeWhatsAppStatusReaction({
3187
3170
  controller: statusReactionController,
3188
3171
  outcome: "error"
@@ -3264,65 +3247,29 @@ function buildInboundLine(params) {
3264
3247
  async function createWhatsAppStatusReactionController(params) {
3265
3248
  if (!params.msg.event.id) return null;
3266
3249
  if ((params.cfg.messages?.statusReactions)?.enabled !== true) return null;
3267
- const admission = requireWhatsAppInboundAdmission(params.msg);
3268
- const accountId = admission.accountId;
3269
- if (resolveWhatsAppReactionLevel({
3270
- cfg: params.cfg,
3271
- accountId
3272
- }).level === "off") return null;
3273
- const ackConfig = params.cfg.messages?.ackReaction;
3274
- const scope = params.cfg.messages?.ackReactionScope ?? "group-mentions";
3275
- if (scope === "off" || scope === "none") return null;
3276
- const ackEmoji = resolveWhatsAppAckEmoji({
3277
- cfg: params.cfg,
3278
- agentId: params.agentId,
3279
- ackConfig
3280
- });
3281
- if (!ackEmoji) return null;
3282
- const directEnabled = scope === "all" || scope === "direct";
3283
- const groupMode = scope === "all" || scope === "group-all" ? "always" : scope === "direct" ? "never" : "mentions";
3284
- const isGroup = admission.conversation.kind === "group";
3285
- const conversationIdForCheck = admission.conversation.id;
3286
- const activation = isGroup ? await resolveGroupActivationFor({
3250
+ const eligibility = await resolveWhatsAppReactionEligibility({
3287
3251
  cfg: params.cfg,
3288
- accountId,
3252
+ msg: params.msg,
3289
3253
  agentId: params.agentId,
3290
3254
  sessionKey: params.sessionKey,
3291
- conversationId: conversationIdForCheck
3292
- }) : null;
3293
- if (!shouldAckReactionForWhatsApp({
3294
- emoji: ackEmoji,
3295
- isDirect: admission.conversation.kind === "direct",
3296
- isGroup,
3297
- directEnabled,
3298
- groupMode,
3299
- wasMentioned: (params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned) === true,
3300
- groupActivated: activation === "always"
3301
- })) return null;
3302
- const participant = resolveReactionParticipant(params.msg);
3303
- const reactionOptions = {
3304
- verbose: params.verbose,
3305
- fromMe: false,
3306
- ...participant ? { participant } : {},
3307
- accountId,
3308
- cfg: params.cfg
3309
- };
3310
- const chatId = params.msg.platform.chatJid;
3311
- const msgId = params.msg.event.id;
3255
+ verbose: params.verbose
3256
+ });
3257
+ if (eligibility.status === "disabled") return null;
3258
+ const { chatId, messageId, emoji: initialEmoji, reactionOptions } = eligibility;
3312
3259
  return createStatusReactionController({
3313
3260
  enabled: true,
3314
3261
  adapter: {
3315
3262
  setReaction: async (emoji) => {
3316
- await sendReactionWhatsApp(chatId, msgId, emoji, reactionOptions);
3263
+ await sendReactionWhatsApp(chatId, messageId, emoji, reactionOptions);
3317
3264
  },
3318
3265
  clearReaction: async () => {
3319
- await sendReactionWhatsApp(chatId, msgId, "", reactionOptions);
3266
+ await sendReactionWhatsApp(chatId, messageId, "", reactionOptions);
3320
3267
  }
3321
3268
  },
3322
- initialEmoji: ackEmoji,
3269
+ initialEmoji,
3323
3270
  emojis: void 0,
3324
3271
  onError: (err) => {
3325
- logVerbose(`WhatsApp status-reaction error for chat ${chatId}/${msgId}: ${String(err)}`);
3272
+ logVerbose(`WhatsApp status-reaction error for chat ${chatId}/${messageId}: ${String(err)}`);
3326
3273
  }
3327
3274
  });
3328
3275
  }
@@ -3998,23 +3945,11 @@ function resolveWebMonitorConfigSnapshot(params) {
3998
3945
  account
3999
3946
  };
4000
3947
  }
4001
- function normalizeReconnectAccountId(accountId) {
4002
- return (accountId ?? "").trim() || "default";
4003
- }
4004
3948
  function isNoListenerReconnectError(lastError) {
4005
3949
  return typeof lastError === "string" && /No active WhatsApp Web listener/i.test(lastError);
4006
3950
  }
4007
- function resolveExplicitWhatsAppDebounceOverride(params) {
4008
- const channel = params.sourceCfg?.channels?.whatsapp;
4009
- if (!channel) return;
4010
- const accountId = normalizeReconnectAccountId(params.accountId);
4011
- const accountDebounce = resolveAccountEntry(channel.accounts, accountId)?.debounceMs;
4012
- if (accountDebounce !== void 0) return accountDebounce;
4013
- if (accountId !== "default") {
4014
- const defaultAccountDebounce = resolveAccountEntry(channel.accounts, "default")?.debounceMs;
4015
- if (defaultAccountDebounce !== void 0) return defaultAccountDebounce;
4016
- }
4017
- return channel.debounceMs;
3951
+ function normalizeReconnectAccountId(accountId) {
3952
+ return (accountId ?? "").trim() || "default";
4018
3953
  }
4019
3954
  function isRetryableAuthUnstableError(error) {
4020
3955
  return error instanceof WhatsAppAuthUnstableError || typeof error === "object" && error !== null && "code" in error && error.code === "whatsapp-auth-unstable";
@@ -4036,10 +3971,8 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
4036
3971
  module: "web-reconnect",
4037
3972
  runId
4038
3973
  });
4039
- const baseCfg = getRuntimeConfig$1();
4040
- const sourceCfg = getRuntimeConfigSourceSnapshot();
4041
3974
  const { cfg, account } = resolveWebMonitorConfigSnapshot({
4042
- cfg: baseCfg,
3975
+ cfg: getRuntimeConfig$1(),
4043
3976
  accountId: tuning.accountId
4044
3977
  });
4045
3978
  const loadCurrentMonitorConfig = () => resolveWebMonitorConfigSnapshot({
@@ -4097,12 +4030,7 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
4097
4030
  const connectionId = newConnectionId();
4098
4031
  const inboundDebounceMs = resolveInboundDebounceMs({
4099
4032
  cfg,
4100
- channel: "whatsapp",
4101
- overrideMs: resolveExplicitWhatsAppDebounceOverride({
4102
- cfg,
4103
- sourceCfg,
4104
- accountId: account.accountId
4105
- })
4033
+ channel: "whatsapp"
4106
4034
  });
4107
4035
  const shouldDebounce = (msg) => {
4108
4036
  const normalized = normalizeWebInboundMessage(msg);