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

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 (147) hide show
  1. package/dist/{access-control-DazDX3RB.js → access-control-CRz_v6bR.js} +4 -5
  2. package/dist/{accounts-CKOFMPf-.js → accounts-CxPuEFi-.js} +44 -4
  3. package/dist/{action-runtime-Bf-12TIH.js → action-runtime-CHU7xb6o.js} +2 -2
  4. package/dist/action-runtime-api.js +1 -1
  5. package/dist/action-runtime.runtime.js +1 -1
  6. package/dist/{active-listener-5kHap439.js → active-listener-BEjWsCNM.js} +1 -1
  7. package/dist/api.js +12 -15
  8. package/dist/{approval-handler.runtime-CBiGQuZ5.js → approval-handler.runtime-C4ddB7Kc.js} +4 -4
  9. package/dist/{approval-reactions-BGQoz25s.js → approval-reactions-P2HXjmln.js} +3 -3
  10. package/dist/{auth-store-BQi9pZqQ.js → auth-store-DirYMTbo.js} +4 -3
  11. package/dist/call-tool-api.js +4 -4
  12. package/dist/{channel-DbadDUDD.js → channel-B9xjrOGG.js} +37 -19
  13. package/dist/channel-config-api.js +1 -1
  14. package/dist/channel-plugin-api.js +1 -1
  15. package/dist/{channel-react-action-PoRWfP9x.js → channel-react-action-CBW-PA8q.js} +10 -13
  16. package/dist/{channel.runtime-BDDy4Lhz.js → channel.runtime-DdKuhlNu.js} +5 -5
  17. package/dist/{channel.setup-BZ-po8fg.js → channel.setup-DXUCPHel.js} +3 -6
  18. package/dist/{config-schema-BZ8h1hWA.js → config-schema-C6IeKIC0.js} +6 -0
  19. package/dist/{connection-controller-C1rwfWb4.js → connection-controller-BVRExqrB.js} +186 -27
  20. package/dist/connection-controller-runtime-context-DG0tuYuw.js +23 -0
  21. package/dist/contract-api.js +1 -1
  22. package/dist/directory-config-BTbJkV1W.js +196 -0
  23. package/dist/directory-contract-api.js +1 -1
  24. package/dist/doctor-CwzVXKoE.js +10 -0
  25. package/dist/doctor-contract-api.js +35 -79
  26. package/dist/{group-session-key-CLTZeH_F.js → group-session-key-DJSdYfnb.js} +9 -4
  27. package/dist/{identity-D7L8LCuH.js → identity-BM4_wZeM.js} +2 -2
  28. package/dist/light-runtime-api.js +2 -2
  29. package/dist/{login-DGIoToX8.js → login-BKUm7KSk.js} +6 -7
  30. package/dist/{login-qr-X-CZHOk9.js → login-qr-DagJvMbg.js} +7 -8
  31. package/dist/login-qr-runtime.js +1 -1
  32. package/dist/{monitor-BZ8QvCeV.js → monitor-CBVNJ2lb.js} +623 -400
  33. package/dist/question-reactions-CaOrSYzU.js +142 -0
  34. package/dist/runtime-BLlToOi6.js +21 -0
  35. package/dist/runtime-api.js +10 -10
  36. package/dist/runtime-setter-api.js +1 -1
  37. package/dist/{send-D9Ydnn4o.js → send-DhJVN30g.js} +38 -85
  38. package/dist/{send-api-W33lI2nR.js → send-api-CpHwbor_.js} +38 -32
  39. package/dist/{setup-finalize-DBf1jjBj.js → setup-finalize-9XzKdFq6.js} +3 -3
  40. package/dist/setup-plugin-api.js +1 -1
  41. package/dist/{setup-surface-CiKGdNJv.js → setup-surface-Kv3mPki2.js} +3 -3
  42. package/dist/{setup-core-3aSOoK2T.js → shared-CuqHFnKd.js} +78 -56
  43. package/dist/{session-mxWFn0eS.js → socket-close-BoSHhQNJ.js} +226 -7
  44. package/dist/{targets-runtime-Bsh0HR9y.js → targets-runtime-CKFrhoX4.js} +142 -29
  45. package/dist/text-runtime-CU_IlbK5.js +4 -0
  46. package/node_modules/@protobufjs/utf8/CHANGELOG.md +8 -0
  47. package/node_modules/@protobufjs/utf8/index.js +29 -18
  48. package/node_modules/@protobufjs/utf8/package.json +2 -2
  49. package/node_modules/@protobufjs/utf8/tests/index.js +5 -0
  50. package/node_modules/@thi.ng/bitstream/package.json +4 -4
  51. package/node_modules/@thi.ng/errors/package.json +4 -4
  52. package/node_modules/@types/node/README.md +1 -1
  53. package/node_modules/@types/node/fs/promises.d.ts +16 -6
  54. package/node_modules/@types/node/package.json +2 -2
  55. package/node_modules/audio-type/package.json +3 -2
  56. package/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -1
  57. package/node_modules/lru-cache/dist/commonjs/browser/index.js +11 -4
  58. package/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -1
  59. package/node_modules/lru-cache/dist/commonjs/browser/index.min.js +1 -1
  60. package/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +3 -3
  61. package/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
  62. package/node_modules/lru-cache/dist/commonjs/index.js +11 -4
  63. package/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
  64. package/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
  65. package/node_modules/lru-cache/dist/commonjs/index.min.js.map +3 -3
  66. package/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -1
  67. package/node_modules/lru-cache/dist/commonjs/node/index.js +11 -4
  68. package/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -1
  69. package/node_modules/lru-cache/dist/commonjs/node/index.min.js +1 -1
  70. package/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +3 -3
  71. package/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -1
  72. package/node_modules/lru-cache/dist/esm/browser/index.js +11 -4
  73. package/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -1
  74. package/node_modules/lru-cache/dist/esm/browser/index.min.js +1 -1
  75. package/node_modules/lru-cache/dist/esm/browser/index.min.js.map +3 -3
  76. package/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
  77. package/node_modules/lru-cache/dist/esm/index.js +11 -4
  78. package/node_modules/lru-cache/dist/esm/index.js.map +1 -1
  79. package/node_modules/lru-cache/dist/esm/index.min.js +1 -1
  80. package/node_modules/lru-cache/dist/esm/index.min.js.map +3 -3
  81. package/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -1
  82. package/node_modules/lru-cache/dist/esm/node/index.js +11 -4
  83. package/node_modules/lru-cache/dist/esm/node/index.js.map +1 -1
  84. package/node_modules/lru-cache/dist/esm/node/index.min.js +1 -1
  85. package/node_modules/lru-cache/dist/esm/node/index.min.js.map +3 -3
  86. package/node_modules/lru-cache/package.json +1 -1
  87. package/node_modules/music-metadata/lib/common/Util.d.ts +1 -1
  88. package/node_modules/music-metadata/lib/common/Util.js +7 -1
  89. package/node_modules/music-metadata/lib/id3v2/FrameHeader.js +23 -3
  90. package/node_modules/music-metadata/lib/id3v2/FrameParser.js +2 -2
  91. package/node_modules/music-metadata/lib/id3v2/ID3v2Token.js +1 -1
  92. package/node_modules/music-metadata/lib/mp4/AtomToken.js +38 -13
  93. package/node_modules/music-metadata/lib/mpeg/MpegParser.js +1 -1
  94. package/node_modules/music-metadata/package.json +6 -6
  95. package/node_modules/p-queue/dist/index.js +17 -6
  96. package/node_modules/p-queue/package.json +1 -1
  97. package/node_modules/protobufjs/dist/light/protobuf.js +145 -188
  98. package/node_modules/protobufjs/dist/light/protobuf.js.map +1 -1
  99. package/node_modules/protobufjs/dist/light/protobuf.min.js +3 -3
  100. package/node_modules/protobufjs/dist/light/protobuf.min.js.map +1 -1
  101. package/node_modules/protobufjs/dist/minimal/protobuf.js +33 -76
  102. package/node_modules/protobufjs/dist/minimal/protobuf.js.map +1 -1
  103. package/node_modules/protobufjs/dist/minimal/protobuf.min.js +3 -3
  104. package/node_modules/protobufjs/dist/minimal/protobuf.min.js.map +1 -1
  105. package/node_modules/protobufjs/dist/protobuf.js +168 -208
  106. package/node_modules/protobufjs/dist/protobuf.js.map +1 -1
  107. package/node_modules/protobufjs/dist/protobuf.min.js +3 -3
  108. package/node_modules/protobufjs/dist/protobuf.min.js.map +1 -1
  109. package/node_modules/protobufjs/index.d.ts +0 -8
  110. package/node_modules/protobufjs/package.json +2 -3
  111. package/node_modules/protobufjs/src/parse.js +3 -0
  112. package/node_modules/protobufjs/src/util/minimal.js +0 -3
  113. package/node_modules/typebox/build/compile/validator.d.mts +1 -4
  114. package/node_modules/typebox/build/guard/guard.d.mts +1 -1
  115. package/node_modules/typebox/build/guard/guard.mjs +5 -2
  116. package/node_modules/typebox/build/guard/string.mjs +16 -4
  117. package/node_modules/typebox/build/schema/engine/_functions.mjs +34 -15
  118. package/node_modules/typebox/build/type/action/readonly_object.d.mts +5 -0
  119. package/node_modules/typebox/build/type/action/readonly_object.mjs +8 -0
  120. package/node_modules/typebox/build/typebox.d.mts +1 -1
  121. package/node_modules/typebox/build/typebox.mjs +1 -1
  122. package/node_modules/typebox/package.json +1 -1
  123. package/node_modules/typebox/readme.md +29 -32
  124. package/node_modules/ws/lib/receiver.js +15 -32
  125. package/node_modules/ws/lib/websocket-server.js +4 -4
  126. package/node_modules/ws/lib/websocket.js +4 -4
  127. package/node_modules/ws/package.json +5 -1
  128. package/npm-shrinkwrap.json +41 -45
  129. package/openclaw.plugin.json +139 -179
  130. package/package.json +21 -11
  131. package/dist/account-config-LiT9wWH9.js +0 -43
  132. package/dist/connection-controller-runtime-context-CbTsqmPT.js +0 -14
  133. package/dist/directory-config-CLBQrjtC.js +0 -37
  134. package/dist/doctor-By3SB4lm.js +0 -48
  135. package/dist/runtime-BfAdAEYT.js +0 -8
  136. package/dist/text-runtime-BTGkMN0o.js +0 -4
  137. package/node_modules/@protobufjs/inquire/CHANGELOG.md +0 -8
  138. package/node_modules/@protobufjs/inquire/LICENSE +0 -26
  139. package/node_modules/@protobufjs/inquire/README.md +0 -13
  140. package/node_modules/@protobufjs/inquire/index.d.ts +0 -10
  141. package/node_modules/@protobufjs/inquire/index.js +0 -38
  142. package/node_modules/@protobufjs/inquire/package.json +0 -21
  143. package/node_modules/@protobufjs/inquire/tests/data/array.js +0 -1
  144. package/node_modules/@protobufjs/inquire/tests/data/emptyArray.js +0 -1
  145. package/node_modules/@protobufjs/inquire/tests/data/emptyObject.js +0 -1
  146. package/node_modules/@protobufjs/inquire/tests/data/object.js +0 -1
  147. package/node_modules/@protobufjs/inquire/tests/index.js +0 -20
@@ -1,52 +1,48 @@
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";
3
- import { n as getWhatsAppRuntime } from "./runtime-BfAdAEYT.js";
4
- import { n as getWhatsAppConnectionController } from "./connection-controller-runtime-context-CbTsqmPT.js";
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
- 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";
8
- import { g as readWebSelfId, o as getWebAuthAgeMs, r as WhatsAppAuthUnstableError, v as readWebSelfIdentityForDecision } from "./auth-store-BQi9pZqQ.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-D7L8LCuH.js";
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";
3
+ import { i as getWhatsAppRuntime } from "./runtime-BLlToOi6.js";
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";
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-BoSHhQNJ.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-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";
18
19
  import { resolveAccountEntry } from "openclaw/plugin-sdk/account-core";
19
20
  import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
20
21
  import { filterSupplementalContextItems, resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
21
- import { DEFAULT_TIMING, createAckReactionHandle, createStatusReactionController, logAckFailure, removeAckReactionHandleAfterReply, shouldAckReactionForWhatsApp } from "openclaw/plugin-sdk/channel-feedback";
22
+ import { 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 { bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, 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";
32
- import { LocalMediaAccessError, getDefaultLocalRoots, loadWebMedia as loadWebMedia$1, loadWebMediaRaw, optimizeImageToJpeg, optimizeImageToPng } from "openclaw/plugin-sdk/web-media";
33
32
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
34
- import { chunkMarkdownTextWithMode } from "openclaw/plugin-sdk/reply-chunking";
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";
35
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";
36
35
  import { asDateTimestampMs, parseStrictFiniteNumber, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
37
- import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
36
+ import { createHash } from "node:crypto";
38
37
  import { getSessionEntry, patchSessionEntry, resolveStorePath, resolveStorePath as resolveStorePath$1, updateLastRoute } from "openclaw/plugin-sdk/session-store-runtime";
39
- import { buildChannelInboundEventContext, filterChannelInboundQuoteContext, formatInboundEnvelope, formatInboundEnvelope as formatInboundEnvelope$1, formatInboundMediaUnavailableText, formatLocationText, hasVisibleInboundReplyDispatch, resolveInboundSessionEnvelopeContext, runChannelInboundEvent, shouldDebounceTextInbound, toInboundMediaFacts, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
40
38
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
41
- import { createHash } from "node:crypto";
42
- import { dispatchReplyWithBufferedBlockDispatcher, resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
39
+ import { resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
43
40
  import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-runtime";
44
41
  import { createInboundDebouncer, resolveInboundDebounceMs } from "openclaw/plugin-sdk/channel-inbound-debounce";
45
42
  import { drainPendingDeliveries } from "openclaw/plugin-sdk/delivery-queue-runtime";
46
43
  import { DEFAULT_GROUP_HISTORY_LIMIT, buildHistoryContextFromEntries, buildInboundHistoryFromEntries, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
47
44
  import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
48
45
  import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
49
- import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
50
46
  import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
51
47
  import { getRuntimeConfig as getRuntimeConfig$1, getRuntimeConfigSourceSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot";
52
48
  import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
@@ -59,51 +55,8 @@ import { hasControlCommand, isControlCommandMessage, shouldComputeCommandAuthori
59
55
  import { createInternalHookEvent, deriveInboundMessageHookContext, fireAndForgetBoundedHook, toInternalMessageReceivedContext, toPluginMessageContext, toPluginMessageReceivedEvent, triggerInternalHook } from "openclaw/plugin-sdk/hook-runtime";
60
56
  import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
61
57
  import { resolveBatchedReplyThreadingPolicy } from "openclaw/plugin-sdk/reply-reference";
58
+ import { LocalMediaAccessError, getDefaultLocalRoots, loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg, optimizeImageToPng } from "openclaw/plugin-sdk/web-media";
62
59
  import { createChannelApiRetryRunner } from "openclaw/plugin-sdk/retry-runtime";
63
- //#region extensions/whatsapp/src/inbound/dedupe.ts
64
- const WHATSAPP_INBOUND_DEDUPE_TTL_MS = 20 * 6e4;
65
- const RECENT_WEB_MESSAGE_MAX = 5e3;
66
- const RECENT_OUTBOUND_MESSAGE_TTL_MS = 20 * 6e4;
67
- const RECENT_OUTBOUND_MESSAGE_MAX = 5e3;
68
- const whatsAppInboundReplayGuard = createChannelReplayGuard({
69
- dedupe: {
70
- ttlMs: WHATSAPP_INBOUND_DEDUPE_TTL_MS,
71
- memoryMaxSize: RECENT_WEB_MESSAGE_MAX
72
- },
73
- buildReplayKey: (keys) => keys
74
- });
75
- const recentOutboundMessages = createDedupeCache({
76
- ttlMs: RECENT_OUTBOUND_MESSAGE_TTL_MS,
77
- maxSize: RECENT_OUTBOUND_MESSAGE_MAX
78
- });
79
- var WhatsAppRetryableInboundError = class extends Error {
80
- constructor(message, options) {
81
- super(message, options);
82
- this.name = "WhatsAppRetryableInboundError";
83
- }
84
- };
85
- function buildMessageKey(params) {
86
- const accountId = params.accountId.trim();
87
- const remoteJid = params.remoteJid.trim();
88
- const messageId = params.messageId.trim();
89
- if (!accountId || !remoteJid || !messageId || messageId === "unknown") return null;
90
- return `${accountId}:${remoteJid}:${messageId}`;
91
- }
92
- function resetWebInboundDedupe() {
93
- whatsAppInboundReplayGuard.clearMemory();
94
- recentOutboundMessages.clear();
95
- }
96
- function rememberRecentOutboundMessage(params) {
97
- const key = buildMessageKey(params);
98
- if (!key) return;
99
- recentOutboundMessages.check(key);
100
- }
101
- function isRecentOutboundMessage(params) {
102
- const key = buildMessageKey(params);
103
- if (!key) return false;
104
- return recentOutboundMessages.peek(key);
105
- }
106
- //#endregion
107
60
  //#region extensions/whatsapp/src/inbound/message-aliases.ts
108
61
  function normalizeQuoteSender(sender) {
109
62
  if (!sender?.displayName && !sender?.jid && !sender?.e164) return;
@@ -428,42 +381,173 @@ function normalizeWebInboundMessage(msg) {
428
381
  return normalizeLegacyFlatWebInboundMessage(msg);
429
382
  }
430
383
  //#endregion
384
+ //#region extensions/whatsapp/src/inbound/dedupe.ts
385
+ const recentOutboundMessages = createDedupeCache({
386
+ ttlMs: 20 * 6e4,
387
+ maxSize: 5e3
388
+ });
389
+ function buildMessageKey(params) {
390
+ const accountId = params.accountId.trim();
391
+ const remoteJid = params.remoteJid.trim();
392
+ const messageId = params.messageId.trim();
393
+ if (!accountId || !remoteJid || !messageId || messageId === "unknown") return null;
394
+ return `${accountId}:${remoteJid}:${messageId}`;
395
+ }
396
+ function resetWebInboundDedupe() {
397
+ recentOutboundMessages.clear();
398
+ }
399
+ function rememberRecentOutboundMessage(params) {
400
+ const key = buildMessageKey(params);
401
+ if (!key) return;
402
+ recentOutboundMessages.check(key);
403
+ }
404
+ function isRecentOutboundMessage(params) {
405
+ const key = buildMessageKey(params);
406
+ if (!key) return false;
407
+ return recentOutboundMessages.peek(key);
408
+ }
409
+ //#endregion
410
+ //#region extensions/whatsapp/src/inbound/durable-payload.ts
411
+ var WhatsAppIngressPermanentError = class extends Error {
412
+ constructor(reason, message, options) {
413
+ super(message, options);
414
+ this.reason = reason;
415
+ this.name = "WhatsAppIngressPermanentError";
416
+ }
417
+ };
418
+ function serializeWhatsAppDurableInboundMessage(message) {
419
+ const timestamp = message.messageTimestamp;
420
+ let serializedMessage = message;
421
+ if (timestamp != null && typeof timestamp === "object") try {
422
+ const numericTimestamp = Number(timestamp);
423
+ if (Number.isFinite(numericTimestamp)) serializedMessage = {
424
+ ...message,
425
+ messageTimestamp: numericTimestamp
426
+ };
427
+ } catch {}
428
+ return JSON.parse(JSON.stringify(serializedMessage, BufferJSON.replacer));
429
+ }
430
+ function deserializeWhatsAppDurableInboundMessage(message) {
431
+ try {
432
+ return JSON.parse(JSON.stringify(message), BufferJSON.reviver);
433
+ } catch (error) {
434
+ throw new WhatsAppIngressPermanentError("invalid-payload", "WhatsApp ingress row contains an invalid serialized message", { cause: error });
435
+ }
436
+ }
437
+ //#endregion
431
438
  //#region extensions/whatsapp/src/inbound/durable-receive.ts
432
439
  const WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES = 450;
433
- const WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES = 450;
440
+ const WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES = 5e3;
434
441
  const WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS = 720 * 60 * 60 * 1e3;
435
442
  const WHATSAPP_DURABLE_INBOUND_COMPLETED_TTL_MS = 10080 * 60 * 1e3;
443
+ const WHATSAPP_DURABLE_INBOUND_PRUNE_INTERVAL_MS = 3600 * 1e3;
444
+ const WHATSAPP_DURABLE_INBOUND_PAYLOAD_VERSION = 1;
436
445
  function hashNamespacePart(value) {
437
446
  return createHash("sha256").update(value).digest("hex").slice(0, 24);
438
447
  }
439
448
  function createWhatsAppDurableInboundMessageId(params) {
440
449
  return createHash("sha256").update(`${params.remoteJid}\n${params.id}`).digest("hex");
441
450
  }
442
- function serializeWhatsAppDurableInboundMessage(message) {
443
- return JSON.parse(JSON.stringify(message, BufferJSON.replacer));
444
- }
445
- function deserializeWhatsAppDurableInboundMessage(message) {
446
- return JSON.parse(JSON.stringify(message), BufferJSON.reviver);
447
- }
448
- function createWhatsAppDurableInboundReceiveJournal(accountId) {
449
- const accountPart = hashNamespacePart(accountId);
450
- const runtime = getWhatsAppRuntime();
451
- return createDurableInboundReceiveJournalFromQueue({
452
- queue: runtime.state.openChannelIngressQueue({
453
- accountId: accountPart,
454
- stateDir: runtime.state.resolveStateDir()
451
+ function inspectWhatsAppIngressMessage(message) {
452
+ const remoteJid = message.key?.remoteJid?.trim();
453
+ const id = message.key?.id?.trim();
454
+ if (!remoteJid || !id) throw new WhatsAppIngressPermanentError("missing-message-key", "WhatsApp ingress message is missing key.remoteJid or key.id");
455
+ return {
456
+ eventId: createWhatsAppDurableInboundMessageId({
457
+ remoteJid,
458
+ id
455
459
  }),
460
+ laneKey: remoteJid
461
+ };
462
+ }
463
+ /** Account-scoped queue shared with the pre-drain WhatsApp receive journal. */
464
+ function createWhatsAppDurableInboundQueue(accountId) {
465
+ return getWhatsAppRuntime().state.openChannelIngressQueue({
466
+ accountId: hashNamespacePart(accountId),
467
+ stateDir: getWhatsAppRuntime().state.resolveStateDir()
468
+ });
469
+ }
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
+ function resolveWhatsAppIngressNonRetryableFailure(error) {
487
+ return error instanceof WhatsAppIngressPermanentError ? {
488
+ reason: error.reason,
489
+ message: error.message
490
+ } : null;
491
+ }
492
+ /** Shared monitor with per-conversation lanes and completion at reply-lane adoption. */
493
+ function createWhatsAppIngressMonitor(params) {
494
+ return createChannelIngressMonitor({
495
+ queue: params.queue,
496
+ inspect: (message) => inspectWhatsAppIngressMessage(message),
497
+ payload: {
498
+ version: WHATSAPP_DURABLE_INBOUND_PAYLOAD_VERSION,
499
+ serialize: (message, { receivedAt }) => ({
500
+ message: serializeWhatsAppDurableInboundMessage(message),
501
+ receivedAt
502
+ }),
503
+ deserialize: (payload) => deserializeWhatsAppDurableInboundMessage(payload.message),
504
+ encode: ({ body }) => body,
505
+ decode: (payload) => ({
506
+ version: WHATSAPP_DURABLE_INBOUND_PAYLOAD_VERSION,
507
+ body: payload
508
+ }),
509
+ 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
+ },
511
+ deliver: (message, lifecycle, claim) => params.dispatch(message, claim.payload, lifecycle),
512
+ pollIntervalMs: params.pollIntervalMs,
456
513
  retention: {
514
+ pruneIntervalMs: WHATSAPP_DURABLE_INBOUND_PRUNE_INTERVAL_MS,
457
515
  pendingTtlMs: WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS,
458
- completedTtlMs: WHATSAPP_DURABLE_INBOUND_COMPLETED_TTL_MS,
459
- failedTtlMs: WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS,
460
516
  pendingMaxEntries: WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES,
517
+ completedTtlMs: WHATSAPP_DURABLE_INBOUND_COMPLETED_TTL_MS,
461
518
  completedMaxEntries: WHATSAPP_DURABLE_INBOUND_COMPLETED_MAX_ENTRIES,
519
+ failedTtlMs: WHATSAPP_DURABLE_INBOUND_PENDING_TTL_MS,
462
520
  failedMaxEntries: WHATSAPP_DURABLE_INBOUND_PENDING_MAX_ENTRIES
463
- }
521
+ },
522
+ drain: {
523
+ resolveNonRetryableFailure: resolveWhatsAppIngressNonRetryableFailure,
524
+ deriveLaneKey: (record) => {
525
+ try {
526
+ return inspectWhatsAppIngressMessage(deserializeWhatsAppDurableInboundMessage(record.payload.message)).laneKey;
527
+ } catch {
528
+ return record.id;
529
+ }
530
+ },
531
+ ...params.onLog ? { onLog: params.onLog } : {}
532
+ },
533
+ ...params.abortSignal ? { abortSignal: params.abortSignal } : {},
534
+ admissionMode: "while-running",
535
+ createStoppedError: () => /* @__PURE__ */ new Error("WhatsApp ingress monitor is stopped."),
536
+ ...params.onError ? { onError: params.onError } : {},
537
+ ...params.onActivityChange ? { onActivityChange: params.onActivityChange } : {}
464
538
  });
465
539
  }
466
540
  //#endregion
541
+ //#region extensions/whatsapp/src/inbound/ingress-lifecycle.ts
542
+ const ingressLifecycleKey = Symbol("whatsappIngressLifecycle");
543
+ function attachWhatsAppIngressLifecycle(message, lifecycle) {
544
+ if (lifecycle) message[ingressLifecycleKey] = lifecycle;
545
+ return message;
546
+ }
547
+ function resolveWhatsAppIngressLifecycle(message) {
548
+ return message[ingressLifecycleKey];
549
+ }
550
+ //#endregion
467
551
  //#region extensions/whatsapp/src/inbound/lifecycle.ts
468
552
  function attachEmitterListener(emitter, event, listener) {
469
553
  emitter.on(event, listener);
@@ -533,13 +617,8 @@ const RECONNECT_IN_PROGRESS_ERROR = "no active socket - reconnection in progress
533
617
  const GROUP_META_TTL_MS = 300 * 1e3;
534
618
  const BAILEYS_MESSAGE_TTL_MS = 600 * 1e3;
535
619
  const INBOUND_CLOSE_DRAIN_TIMEOUT_MS = 5e3;
536
- const REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE = /reply session initialization conflicted for \S+/u;
620
+ const WHATSAPP_INGRESS_DRAIN_INTERVAL_MS = 1e3;
537
621
  const WHATSAPP_GROUP_METADATA_CACHE_MAX_ENTRIES = 500;
538
- function resolveRetryableWhatsAppInboundError(error) {
539
- if (error instanceof WhatsAppRetryableInboundError) return error;
540
- if (!collectErrorGraphCandidates(error, (current) => [current.cause, current.error]).some((candidate) => REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE.test(formatErrorMessage(candidate)))) return null;
541
- return new WhatsAppRetryableInboundError(formatErrorMessage(error), { cause: error });
542
- }
543
622
  function resolveGroupMetadataExpiresAt(nowRaw = Date.now()) {
544
623
  const now = asDateTimestampMs(nowRaw);
545
624
  return now === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(GROUP_META_TTL_MS, { nowMs: now });
@@ -630,9 +709,6 @@ function isRetryableSendDisconnectError(err) {
630
709
  function shouldClearSocketRefAfterSendFailure(err) {
631
710
  return /closed|reset|disconnect|no active socket/i.test(formatError(err));
632
711
  }
633
- function isNonEmptyString(value) {
634
- return Boolean(value);
635
- }
636
712
  async function attachWebInboxToSocket(options) {
637
713
  const inboundLogger = getChildLogger({ module: "web-inbound" });
638
714
  const inboundConsoleLog = createSubsystemLogger("gateway/channels/whatsapp").child("inbound");
@@ -673,14 +749,34 @@ async function attachWebInboxToSocket(options) {
673
749
  if (!successorIdentity || !identitiesOverlap(self, successorIdentity)) return null;
674
750
  return successor.getCurrentSock();
675
751
  };
676
- const durableInboundJournal = createWhatsAppDurableInboundReceiveJournal(options.accountId);
752
+ const durableInboundQueue = options.durableInboundQueue ?? createWhatsAppDurableInboundQueue(options.accountId);
677
753
  const inboundDebounceMs = Math.max(0, Math.trunc(options.debounceMs ?? 0));
678
754
  const pendingDebounceKeys = /* @__PURE__ */ new Set();
679
755
  const activeInboundFlushes = /* @__PURE__ */ new Set();
680
756
  const pendingMessageHandlers = /* @__PURE__ */ new Set();
757
+ let durableIngressActive = false;
758
+ const debounceWorkWaiters = /* @__PURE__ */ new Set();
759
+ const notifyDebounceWork = () => {
760
+ if (debounceWorkWaiters.size === 0) return;
761
+ const waiters = [...debounceWorkWaiters];
762
+ debounceWorkWaiters.clear();
763
+ for (const resolve of waiters) resolve();
764
+ };
765
+ const waitForDebounceWorkOrIdle = (handlers) => {
766
+ if (pendingDebounceKeys.size > 0 || activeInboundFlushes.size > 0) return Promise.resolve();
767
+ if (pendingMessageHandlers.size === 0) return Promise.resolve();
768
+ return new Promise((resolve) => {
769
+ const finish = () => {
770
+ debounceWorkWaiters.delete(finish);
771
+ resolve();
772
+ };
773
+ debounceWorkWaiters.add(finish);
774
+ Promise.allSettled(handlers).then(finish);
775
+ });
776
+ };
681
777
  let nextReceiveOrder = 0;
682
778
  const publishPendingWorkState = (at = Date.now()) => {
683
- options.onPendingWorkChanged?.(pendingMessageHandlers.size + pendingDebounceKeys.size + activeInboundFlushes.size, at);
779
+ options.onPendingWorkChanged?.(pendingMessageHandlers.size + pendingDebounceKeys.size + activeInboundFlushes.size + (durableIngressActive ? 1 : 0), at);
684
780
  };
685
781
  const buildInboundDebounceKey = (msg) => {
686
782
  const admission = requireWhatsAppInboundAdmission(msg);
@@ -695,18 +791,47 @@ async function attachWebInboxToSocket(options) {
695
791
  if (timestampDiff !== 0) return timestampDiff;
696
792
  return (a.receiveOrder ?? 0) - (b.receiveOrder ?? 0);
697
793
  });
698
- const finalizeInboundDelivery = async (entries, error) => {
699
- const replayClaims = entries.map((entry) => entry.replayClaim).filter((claim) => claim !== void 0);
700
- const durableEntries = entries.filter((entry) => isNonEmptyString(entry.durableId));
701
- const readReceiptEntries = entries.filter((entry) => Boolean(entry.readReceipt));
702
- const retryableError = resolveRetryableWhatsAppInboundError(error);
703
- if (retryableError) {
704
- for (const claim of replayClaims) claim.release({ error: retryableError });
705
- await Promise.all(durableEntries.map((entry) => durableInboundJournal.release(entry.durableId, { lastError: formatError(retryableError) })));
706
- return;
707
- }
708
- await Promise.all([...replayClaims.map((claim) => claim.commit()), ...durableEntries.map((entry) => durableInboundJournal.complete(entry.durableId, entry.readReceipt ? { metadata: { readReceipt: entry.readReceipt } } : void 0))]);
709
- await Promise.all(readReceiptEntries.map((entry) => maybeMarkInboundAsRead(entry.readReceipt)));
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
+ };
710
835
  };
711
836
  const debouncer = createInboundDebouncer({
712
837
  debounceMs: inboundDebounceMs,
@@ -719,14 +844,17 @@ async function attachWebInboxToSocket(options) {
719
844
  });
720
845
  activeInboundFlushes.add(flushTask);
721
846
  publishPendingWorkState();
847
+ notifyDebounceWork();
722
848
  try {
723
849
  const orderedEntries = orderDebouncedInboundEntries(entries);
724
850
  const last = orderedEntries.at(-1);
725
851
  if (!last) return;
852
+ const { lifecycle, settle, abandon } = buildFlushIngressLifecycle(orderedEntries);
726
853
  try {
727
854
  if (orderedEntries.length === 1) {
728
- await options.onMessage(last);
729
- await finalizeInboundDelivery(orderedEntries);
855
+ await options.onMessage(attachWhatsAppIngressLifecycle(last, lifecycle));
856
+ await settle();
857
+ await Promise.all(orderedEntries.map((entry) => maybeMarkInboundAsRead(entry.readReceipt)));
730
858
  return;
731
859
  }
732
860
  const mentioned = /* @__PURE__ */ new Set();
@@ -741,8 +869,9 @@ async function attachWebInboxToSocket(options) {
741
869
  ...last.group,
742
870
  mentions: combinedMentions
743
871
  } : void 0;
744
- const combinedMessage = withDeprecatedWebInboundMessageFlatAliases({
872
+ const combinedMessage = attachWhatsAppIngressLifecycle(withDeprecatedWebInboundMessageFlatAliases({
745
873
  ...last,
874
+ ...lifecycle ? { turnAdoptionLifecycle: lifecycle } : {},
746
875
  payload: {
747
876
  ...last.payload,
748
877
  body: combinedBody,
@@ -753,11 +882,12 @@ async function attachWebInboxToSocket(options) {
753
882
  ...last.event,
754
883
  isBatched: true
755
884
  }
756
- });
885
+ }), lifecycle);
757
886
  await options.onMessage(combinedMessage);
758
- await finalizeInboundDelivery(orderedEntries);
887
+ await settle();
888
+ await Promise.all(orderedEntries.map((entry) => maybeMarkInboundAsRead(entry.readReceipt)));
759
889
  } catch (error) {
760
- await finalizeInboundDelivery(orderedEntries, error);
890
+ await abandon();
761
891
  throw error;
762
892
  }
763
893
  } finally {
@@ -972,20 +1102,24 @@ async function attachWebInboxToSocket(options) {
972
1102
  }
973
1103
  return content;
974
1104
  };
1105
+ const shouldSkipRecentOutboundEcho = (msg) => {
1106
+ const id = msg.key?.id ?? void 0;
1107
+ const remoteJid = msg.key?.remoteJid;
1108
+ if (!msg.key?.fromMe || !id || !remoteJid || !isRecentOutboundMessage({
1109
+ accountId: options.accountId,
1110
+ remoteJid,
1111
+ messageId: id
1112
+ })) return false;
1113
+ logWhatsAppVerbose(options.verbose, `Skipping recent outbound WhatsApp echo ${id} for ${remoteJid}`);
1114
+ return true;
1115
+ };
975
1116
  const normalizeInboundMessage = async (msg) => {
976
1117
  const id = msg.key?.id ?? void 0;
977
1118
  const remoteJid = msg.key?.remoteJid;
978
1119
  if (!remoteJid) return null;
979
1120
  if (remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) return null;
980
1121
  const group = isJidGroup(remoteJid) === true;
981
- if (Boolean(msg.key?.fromMe) && id && isRecentOutboundMessage({
982
- accountId: options.accountId,
983
- remoteJid,
984
- messageId: id
985
- })) {
986
- logWhatsAppVerbose(options.verbose, `Skipping recent outbound WhatsApp echo ${id} for ${remoteJid}`);
987
- return null;
988
- }
1122
+ if (shouldSkipRecentOutboundEcho(msg)) return null;
989
1123
  if (!hasInboundUserContent(msg.message ?? void 0)) return null;
990
1124
  const participantJid = msg.key?.participant ?? void 0;
991
1125
  const from = group ? remoteJid : await resolveInboundJid(remoteJid);
@@ -1061,15 +1195,6 @@ async function attachWebInboxToSocket(options) {
1061
1195
  }
1062
1196
  await maybeMarkInboundAsRead(target);
1063
1197
  };
1064
- const completeUndeliverableDurableInbound = async (durableId, metadata) => {
1065
- if (!durableId) return;
1066
- await durableInboundJournal.complete(durableId, metadata?.readReceipt ? { metadata: { readReceipt: metadata.readReceipt } } : void 0);
1067
- };
1068
- const buildDurableInboundPayload = (msg, upsertType) => ({
1069
- message: serializeWhatsAppDurableInboundMessage(msg),
1070
- ...upsertType ? { upsertType } : {},
1071
- receivedAt: Date.now()
1072
- });
1073
1198
  const shouldSkipStaleAppend = (msg, upsertType) => {
1074
1199
  if (upsertType !== "append") return false;
1075
1200
  const APPEND_RECENT_GRACE_MS = 6e4;
@@ -1078,86 +1203,25 @@ async function attachWebInboxToSocket(options) {
1078
1203
  const nowMs = Date.now();
1079
1204
  return msgTsMs < (options.appendReplyWindow && nowMs <= options.appendReplyWindow.untilMs ? Math.max(options.appendReplyWindow.afterMs, nowMs - options.appendReplyWindow.maxAgeMs) : connectedAtMs - APPEND_RECENT_GRACE_MS);
1080
1205
  };
1081
- const processDurableInboundMessage = async (msg, upsertType, receiveOrder, stored) => {
1082
- const inbound = await normalizeInboundMessage(msg);
1083
- if (!inbound) {
1084
- if (stored) await completeUndeliverableDurableInbound(stored.id, stored.metadata);
1085
- return;
1086
- }
1087
- const readReceipt = stored?.metadata?.readReceipt ?? buildReadReceiptTarget(inbound);
1088
- const deliveryReadReceipt = inbound.access.isSelfChat ? void 0 : readReceipt;
1089
- if (!stored && shouldSkipStaleAppend(msg, upsertType)) {
1090
- await maybeMarkNonSelfChatReadReceipt(inbound, readReceipt);
1091
- return;
1092
- }
1093
- let durableId = stored?.id ?? (inbound.id ? createWhatsAppDurableInboundMessageId({
1094
- remoteJid: inbound.remoteJid,
1095
- id: inbound.id
1096
- }) : void 0);
1097
- const durableMetadata = deliveryReadReceipt ? { readReceipt: deliveryReadReceipt } : void 0;
1098
- if (durableId && !stored) try {
1099
- const accepted = await durableInboundJournal.accept(durableId, buildDurableInboundPayload(msg, upsertType), {
1100
- metadata: durableMetadata,
1101
- receivedAt: inbound.messageTimestampMs
1102
- });
1103
- if (accepted.kind === "completed") {
1104
- await maybeMarkNonSelfChatReadReceipt(inbound, accepted.record.metadata?.readReceipt ?? deliveryReadReceipt);
1105
- return;
1106
- }
1107
- if (accepted.kind === "pending" && accepted.record.attempts === 0) return;
1108
- } catch (err) {
1109
- durableId = void 0;
1110
- const error = formatError(err);
1111
- inboundLogger.warn({ error }, "failed persisting durable WhatsApp inbound; delivering live");
1112
- inboundConsoleLog.warn(`Failed persisting durable WhatsApp inbound; delivering live: ${error}`);
1113
- }
1114
- const enriched = await enrichInboundMessage(msg);
1115
- if (!enriched) {
1116
- await completeUndeliverableDurableInbound(durableId, durableMetadata);
1117
- await maybeMarkNonSelfChatReadReceipt(inbound, deliveryReadReceipt);
1118
- return;
1119
- }
1120
- const dedupeKey = inbound.id ? `${options.accountId}:${inbound.remoteJid}:${inbound.id}` : "";
1121
- const dedupeClaim = dedupeKey ? await whatsAppInboundReplayGuard.claim(dedupeKey) : { kind: "invalid" };
1122
- if (dedupeClaim.kind === "duplicate" || dedupeClaim.kind === "inflight") {
1123
- if (dedupeClaim.kind === "duplicate") {
1124
- await completeUndeliverableDurableInbound(durableId, durableMetadata);
1125
- await maybeMarkNonSelfChatReadReceipt(inbound, deliveryReadReceipt);
1126
- }
1127
- return;
1128
- }
1129
- recordAcceptedInboundActivity(options.accountId);
1130
- await enqueueInboundMessage(msg, inbound, enriched, {
1131
- durableId,
1132
- readReceipt: deliveryReadReceipt,
1133
- receiveOrder,
1134
- ...dedupeClaim.kind === "claimed" ? { replayClaim: dedupeClaim.handle } : {}
1135
- });
1136
- };
1137
- const replayPendingDurableInboundMessages = async () => {
1138
- const pending = await durableInboundJournal.pending();
1139
- for (const record of pending) await processDurableInboundMessage(deserializeWhatsAppDurableInboundMessage(record.payload.message), record.payload.upsertType, record.payload.receivedAt, {
1140
- id: record.id,
1141
- payload: record.payload,
1142
- metadata: record.metadata
1143
- });
1144
- };
1206
+ const preparedInboundByDurableId = /* @__PURE__ */ new Map();
1145
1207
  const enrichInboundMessage = async (msg) => {
1146
1208
  const location = extractLocationData(msg.message ?? void 0);
1147
1209
  const locationText = location ? formatLocationText(location) : void 0;
1148
1210
  const contactContext = extractContactContext(msg.message ?? void 0);
1149
1211
  const externalAdReplyContext = extractExternalAdReplyContext(msg.message ?? void 0);
1150
- const mediaPlaceholder = extractMediaPlaceholder(msg.message ?? void 0);
1212
+ let mediaKind = extractMediaKind(msg.message ?? void 0);
1151
1213
  let body = extractText(msg.message ?? void 0);
1152
1214
  if (locationText) body = [body, locationText].filter(Boolean).join("\n").trim();
1153
- if (!body) {
1154
- body = mediaPlaceholder;
1155
- if (!body) return null;
1156
- }
1215
+ if (!body && !mediaKind) return null;
1216
+ body = body ?? "";
1157
1217
  const commandBody = body;
1158
1218
  const replyContext = describeReplyContext(msg.message);
1159
1219
  let mediaPath;
1160
- let mediaType;
1220
+ let mediaType = mediaKind ? resolveInboundMediaMimetype(msg.message) : void 0;
1221
+ const nativeMedia = mediaKind ? {
1222
+ contentType: mediaType,
1223
+ kind: mediaKind
1224
+ } : void 0;
1161
1225
  let mediaFileName;
1162
1226
  const maxBytes = (typeof options.mediaMaxMb === "number" && options.mediaMaxMb > 0 ? options.mediaMaxMb : 50) * 1024 * 1024;
1163
1227
  const saveInboundMedia = async (inboundMedia) => {
@@ -1172,12 +1236,13 @@ async function attachWebInboxToSocket(options) {
1172
1236
  logWhatsAppVerbose(options.verbose, `Inbound media download failed: ${String(err)}`);
1173
1237
  body = formatInboundMediaUnavailableText({
1174
1238
  body,
1175
- mediaPlaceholder,
1176
1239
  notice: "[whatsapp attachment unavailable]"
1177
1240
  });
1178
1241
  }
1179
- if (!mediaPath && replyContext) try {
1242
+ if (!mediaPath && !mediaKind && replyContext?.media) try {
1180
1243
  await saveInboundMedia(await downloadQuotedInboundMedia(msg, sock, maxBytes));
1244
+ mediaKind = replyContext.media.kind ?? void 0;
1245
+ mediaType = mediaType ?? replyContext.media.contentType ?? void 0;
1181
1246
  } catch (err) {
1182
1247
  logWhatsAppVerbose(options.verbose, `Quoted media download failed: ${String(err)}`);
1183
1248
  body = formatInboundMediaUnavailableText({
@@ -1194,7 +1259,9 @@ async function attachWebInboxToSocket(options) {
1194
1259
  replyContext,
1195
1260
  mediaPath,
1196
1261
  mediaType,
1197
- mediaFileName
1262
+ mediaFileName,
1263
+ mediaKind,
1264
+ nativeMedia
1198
1265
  };
1199
1266
  };
1200
1267
  const enqueueInboundMessage = async (msg, inbound, enriched, durable) => {
@@ -1229,10 +1296,11 @@ async function attachWebInboxToSocket(options) {
1229
1296
  mediaFileName: enriched.mediaFileName,
1230
1297
  timestamp
1231
1298
  }, "inbound message");
1232
- const media = enriched.mediaPath || enriched.mediaType || enriched.mediaFileName ? {
1299
+ const media = enriched.mediaPath || enriched.mediaType || enriched.mediaFileName || enriched.mediaKind ? {
1233
1300
  path: enriched.mediaPath,
1234
1301
  type: enriched.mediaType,
1235
- fileName: enriched.mediaFileName
1302
+ fileName: enriched.mediaFileName,
1303
+ kind: enriched.mediaKind
1236
1304
  } : void 0;
1237
1305
  const groupMentions = mentionedJids ? { jids: mentionedJids } : void 0;
1238
1306
  const group = inbound.group && (inbound.groupSubject || inbound.groupParticipants?.length || groupMentions) ? {
@@ -1240,17 +1308,26 @@ async function attachWebInboxToSocket(options) {
1240
1308
  participants: inbound.groupParticipants,
1241
1309
  mentions: groupMentions
1242
1310
  } : void 0;
1243
- const untrustedStructuredContext = [...enriched.contactContext ? [{
1244
- label: "WhatsApp contact",
1245
- source: "whatsapp",
1246
- type: enriched.contactContext.kind,
1247
- payload: enriched.contactContext
1248
- }] : [], ...enriched.externalAdReplyContext ? [{
1249
- label: "WhatsApp external ad reply",
1250
- source: "whatsapp",
1251
- type: "external_ad_reply",
1252
- payload: enriched.externalAdReplyContext
1253
- }] : []];
1311
+ const untrustedStructuredContext = [
1312
+ ...enriched.nativeMedia ? [{
1313
+ label: "WhatsApp media",
1314
+ source: "whatsapp",
1315
+ type: "media",
1316
+ payload: enriched.nativeMedia
1317
+ }] : [],
1318
+ ...enriched.contactContext ? [{
1319
+ label: "WhatsApp contact",
1320
+ source: "whatsapp",
1321
+ type: enriched.contactContext.kind,
1322
+ payload: enriched.contactContext
1323
+ }] : [],
1324
+ ...enriched.externalAdReplyContext ? [{
1325
+ label: "WhatsApp external ad reply",
1326
+ source: "whatsapp",
1327
+ type: "external_ad_reply",
1328
+ payload: enriched.externalAdReplyContext
1329
+ }] : []
1330
+ ];
1254
1331
  const inboundMessage = withDeprecatedWebInboundMessageFlatAliases({
1255
1332
  admission: inbound.access.admission,
1256
1333
  event: {
@@ -1289,6 +1366,7 @@ async function attachWebInboxToSocket(options) {
1289
1366
  context: enriched.replyContext,
1290
1367
  id: enriched.replyContext.id,
1291
1368
  body: enriched.replyContext.body,
1369
+ media: enriched.replyContext.media,
1292
1370
  sender: {
1293
1371
  displayName: enriched.replyContext.sender?.label ?? void 0,
1294
1372
  jid: enriched.replyContext.sender?.jid ?? void 0,
@@ -1296,8 +1374,7 @@ async function attachWebInboxToSocket(options) {
1296
1374
  }
1297
1375
  } : void 0,
1298
1376
  group,
1299
- replayClaim: durable.replayClaim,
1300
- durableId: durable.durableId,
1377
+ turnAdoptionLifecycle: durable.turnAdoptionLifecycle,
1301
1378
  readReceipt: durable.readReceipt,
1302
1379
  receiveOrder: durable.receiveOrder
1303
1380
  });
@@ -1307,6 +1384,7 @@ async function attachWebInboxToSocket(options) {
1307
1384
  if (inboundDebounceMs > 0 && shouldDebounceInboundMessage(inboundMessage)) {
1308
1385
  pendingDebounceKeys.add(debounceKey);
1309
1386
  publishPendingWorkState();
1387
+ notifyDebounceWork();
1310
1388
  }
1311
1389
  }
1312
1390
  if (inboundMessage.event.id) {
@@ -1315,19 +1393,70 @@ async function attachWebInboxToSocket(options) {
1315
1393
  participant: inboundMessage.platform.senderJid,
1316
1394
  participantE164: admission.conversation.kind === "direct" ? inboundMessage.platform.senderE164 : void 0,
1317
1395
  body: inboundMessage.payload.body,
1396
+ media: enriched.nativeMedia,
1318
1397
  fromMe: inboundMessage.platform.fromMe
1319
1398
  });
1320
1399
  }
1321
- try {
1322
- Promise.resolve(debouncer.enqueue(inboundMessage)).catch((err) => {
1323
- inboundLogger.error({ error: String(err) }, "failed handling inbound web message");
1324
- inboundConsoleLog.error(`Failed handling inbound web message: ${String(err)}`);
1325
- });
1326
- } catch (err) {
1327
- inboundLogger.error({ error: String(err) }, "failed handling inbound web message");
1328
- inboundConsoleLog.error(`Failed handling inbound web message: ${String(err)}`);
1400
+ await debouncer.enqueue(inboundMessage);
1401
+ };
1402
+ const processDurableInboundMessage = async (msg, context) => {
1403
+ rememberBaileysMessage(msg.key?.remoteJid, msg.key?.id, msg.message);
1404
+ 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
+ const prepared = await preparation;
1408
+ if (prepared === null) return "completed";
1409
+ const inbound = prepared ?? await normalizeInboundMessage(msg);
1410
+ if (!inbound) return "completed";
1411
+ if (await maybeResolveWhatsAppQuestionReaction({
1412
+ cfg: options.loadConfig?.() ?? options.cfg,
1413
+ accountId: options.accountId,
1414
+ msg,
1415
+ senderId: inbound.senderE164 ?? inbound.from,
1416
+ resolveReactionTargetJids,
1417
+ logDebug: (message) => logWhatsAppVerbose(options.verbose, message)
1418
+ })) return "completed";
1419
+ const readReceipt = buildReadReceiptTarget(inbound);
1420
+ const deliveryReadReceipt = inbound.access.isSelfChat ? void 0 : readReceipt;
1421
+ if (context.skipStaleAppend === true) {
1422
+ await maybeMarkNonSelfChatReadReceipt(inbound, readReceipt);
1423
+ return "completed";
1424
+ }
1425
+ const enriched = await enrichInboundMessage(msg);
1426
+ if (!enriched) {
1427
+ await maybeMarkNonSelfChatReadReceipt(inbound, deliveryReadReceipt);
1428
+ return "completed";
1329
1429
  }
1430
+ recordAcceptedInboundActivity(options.accountId);
1431
+ await enqueueInboundMessage(msg, inbound, enriched, {
1432
+ readReceipt: deliveryReadReceipt,
1433
+ receiveOrder: context.receiveOrder ?? context.receivedAt,
1434
+ turnAdoptionLifecycle: context.lifecycle
1435
+ });
1436
+ return "deferred";
1330
1437
  };
1438
+ const durableInboundMonitor = createWhatsAppIngressMonitor({
1439
+ 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
+ },
1452
+ pollIntervalMs: WHATSAPP_INGRESS_DRAIN_INTERVAL_MS,
1453
+ onLog: (message) => inboundLogger.warn({ message }, "whatsapp ingress drain"),
1454
+ onError: (error) => inboundLogger.error({ error: formatError(error) }, "whatsapp durable inbound drain failed"),
1455
+ onActivityChange: (active) => {
1456
+ durableIngressActive = active;
1457
+ publishPendingWorkState();
1458
+ }
1459
+ });
1331
1460
  const handleMessagesUpsert = async (upsert) => {
1332
1461
  if (upsert.type !== "notify" && upsert.type !== "append") return;
1333
1462
  for (const msg of upsert.messages ?? []) {
@@ -1343,7 +1472,76 @@ async function attachWebInboxToSocket(options) {
1343
1472
  resolveReactionTargetJids,
1344
1473
  logVerboseMessage: (message) => logWhatsAppVerbose(options.verbose, message)
1345
1474
  })) continue;
1346
- await processDurableInboundMessage(msg, upsert.type, receiveOrder);
1475
+ const receivedAt = Date.now();
1476
+ const skipStaleAppend = shouldSkipStaleAppend(msg, upsert.type);
1477
+ const skipRecentOutboundEcho = shouldSkipRecentOutboundEcho(msg);
1478
+ const remoteJid = msg.key?.remoteJid;
1479
+ const id = msg.key?.id;
1480
+ const durableId = remoteJid && id ? createWhatsAppDurableInboundMessageId({
1481
+ remoteJid,
1482
+ id
1483
+ }) : void 0;
1484
+ let resolvePrepared;
1485
+ if (durableId && !preparedInboundByDurableId.has(durableId)) {
1486
+ if (preparedInboundByDurableId.size >= 1e3) {
1487
+ const oldest = preparedInboundByDurableId.keys().next().value;
1488
+ if (oldest !== void 0) preparedInboundByDurableId.delete(oldest);
1489
+ }
1490
+ preparedInboundByDurableId.set(durableId, new Promise((resolve) => {
1491
+ resolvePrepared = resolve;
1492
+ }));
1493
+ }
1494
+ const finishPreparation = (inbound, keepForDrain = false) => {
1495
+ resolvePrepared?.(inbound);
1496
+ if (!keepForDrain && durableId && resolvePrepared) preparedInboundByDurableId.delete(durableId);
1497
+ };
1498
+ 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) {
1525
+ finishPreparation(void 0);
1526
+ const formattedError = formatError(appendError);
1527
+ inboundLogger.error({ error: formattedError }, "failed persisting durable WhatsApp inbound after retries; message dropped");
1528
+ inboundConsoleLog.error(`Failed persisting durable WhatsApp inbound after retries; message dropped: ${formattedError}`);
1529
+ continue;
1530
+ }
1531
+ if (result.kind === "completed") {
1532
+ finishPreparation(void 0);
1533
+ const inbound = await normalizeInboundMessage(msg);
1534
+ 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();
1544
+ }
1347
1545
  }
1348
1546
  };
1349
1547
  const handleMessagesUpsertEvent = (upsert) => {
@@ -1358,9 +1556,6 @@ async function attachWebInboxToSocket(options) {
1358
1556
  publishPendingWorkState();
1359
1557
  });
1360
1558
  };
1361
- const waitForPendingMessageHandlers = async () => {
1362
- while (pendingMessageHandlers.size > 0) await Promise.all(Array.from(pendingMessageHandlers));
1363
- };
1364
1559
  const drainDebouncedInboundMessages = async () => {
1365
1560
  while (pendingDebounceKeys.size > 0 || activeInboundFlushes.size > 0) {
1366
1561
  const debounceKeys = Array.from(pendingDebounceKeys);
@@ -1372,8 +1567,20 @@ async function attachWebInboxToSocket(options) {
1372
1567
  };
1373
1568
  const drainInboundBeforeSocketClose = async () => {
1374
1569
  groupMetadataCacheClosed = true;
1375
- await waitForPendingMessageHandlers();
1570
+ for (;;) {
1571
+ await drainDebouncedInboundMessages();
1572
+ if (pendingMessageHandlers.size === 0) break;
1573
+ const handlers = Array.from(pendingMessageHandlers);
1574
+ await Promise.race([Promise.allSettled(handlers), waitForDebounceWorkOrIdle(handlers)]);
1575
+ if (pendingMessageHandlers.size === 0 && pendingDebounceKeys.size === 0 && activeInboundFlushes.size === 0) break;
1576
+ }
1376
1577
  await drainDebouncedInboundMessages();
1578
+ for (;;) {
1579
+ await durableInboundMonitor.waitForIdle();
1580
+ if (pendingDebounceKeys.size === 0 && activeInboundFlushes.size === 0) break;
1581
+ await drainDebouncedInboundMessages();
1582
+ }
1583
+ await durableInboundMonitor.stop();
1377
1584
  };
1378
1585
  const drainInboundBeforeSocketCloseWithTimeout = async () => {
1379
1586
  let timeout = null;
@@ -1386,6 +1593,7 @@ async function attachWebInboxToSocket(options) {
1386
1593
  })]);
1387
1594
  } finally {
1388
1595
  if (timeout) clearTimeout(timeout);
1596
+ durableInboundMonitor.stop();
1389
1597
  }
1390
1598
  };
1391
1599
  const handleConnectionUpdate = (update) => {
@@ -1444,16 +1652,7 @@ async function attachWebInboxToSocket(options) {
1444
1652
  const detachGroupParticipantsUpdate = attachSockListener("group-participants.update", ((update) => {
1445
1653
  forgetFullGroupMetadata(update.id);
1446
1654
  }));
1447
- const replayTask = replayPendingDurableInboundMessages().catch((err) => {
1448
- inboundLogger.error({ error: String(err) }, "failed replaying durable WhatsApp inbound");
1449
- inboundConsoleLog.error(`Failed replaying durable WhatsApp inbound: ${String(err)}`);
1450
- });
1451
- pendingMessageHandlers.add(replayTask);
1452
- publishPendingWorkState();
1453
- replayTask.finally(() => {
1454
- pendingMessageHandlers.delete(replayTask);
1455
- publishPendingWorkState();
1456
- });
1655
+ durableInboundMonitor.start();
1457
1656
  (async () => {
1458
1657
  try {
1459
1658
  const groups = await sock.groupFetchAllParticipating();
@@ -1511,7 +1710,7 @@ async function attachWebInboxToSocket(options) {
1511
1710
  };
1512
1711
  }
1513
1712
  async function monitorWebInbox(options) {
1514
- const socketTiming = options.socketTiming ?? resolveWhatsAppSocketTiming(options.cfg);
1713
+ const socketTiming = options.socketTiming ?? resolveWhatsAppSocketTiming();
1515
1714
  const recentMessageKeys = options.recentMessageKeys ?? /* @__PURE__ */ new Map();
1516
1715
  const baileysGroupMetaCache = options.baileysGroupMetaCache ?? /* @__PURE__ */ new Map();
1517
1716
  const sock = await createWaSocket(false, options.verbose, {
@@ -1569,12 +1768,13 @@ function isBotMentionedFromTargets(msg, mentionCfg, targets) {
1569
1768
  const isGroupConversation = requireWhatsAppInboundAdmission(msg).conversation.kind === "group";
1570
1769
  const isSelfChat = explicitSelfChatOverride ? Boolean(mentionCfg.isSelfChat) : isSelfChatMode(targets.self.e164, mentionCfg.allowFrom) && !isGroupConversation;
1571
1770
  const hasMentions = targets.normalizedMentions.length > 0;
1572
- if (hasMentions && !isSelfChat) {
1771
+ const hasNativeMentionsOutsideSelfChat = hasMentions && !isSelfChat;
1772
+ if (hasNativeMentionsOutsideSelfChat) {
1573
1773
  for (const mention of targets.normalizedMentions) if (identitiesOverlap(targets.self, mention)) return true;
1574
- return false;
1575
1774
  } else if (hasMentions && isSelfChat) {}
1576
1775
  const bodyClean = clean(msg.payload.body);
1577
1776
  if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) return true;
1777
+ if (hasNativeMentionsOutsideSelfChat) return false;
1578
1778
  if (targets.self.e164) {
1579
1779
  const selfDigits = targets.self.e164.replace(/\D/g, "");
1580
1780
  if (selfDigits) {
@@ -1755,8 +1955,7 @@ function formatWhatsAppInboundListeningLog(account) {
1755
1955
  const DEFAULT_WHATSAPP_ACK_REACTION = "👀";
1756
1956
  function resolveWhatsAppAckEmoji(params) {
1757
1957
  if (!params.ackConfig) return "";
1758
- if (params.ackConfig.emoji !== void 0) return params.ackConfig.emoji.trim();
1759
- return resolveAgentIdentityEmoji(params.cfg, params.agentId) ?? DEFAULT_WHATSAPP_ACK_REACTION;
1958
+ return (typeof params.ackConfig === "string" ? params.ackConfig : params.ackConfig.emoji)?.trim() || resolveAgentIdentityEmoji(params.cfg, params.agentId) || DEFAULT_WHATSAPP_ACK_REACTION;
1760
1959
  }
1761
1960
  function resolveAgentIdentityEmoji(cfg, agentId) {
1762
1961
  return resolveAgentIdentity(cfg, agentId)?.emoji?.trim() || void 0;
@@ -1809,6 +2008,12 @@ async function resolveGroupActivationFor(params) {
1809
2008
  return normalizeGroupActivation(activation) ?? defaultActivation;
1810
2009
  }
1811
2010
  //#endregion
2011
+ //#region extensions/whatsapp/src/auto-reply/monitor/reaction-participant.ts
2012
+ function resolveReactionParticipant(msg) {
2013
+ const sender = getSenderIdentity(msg);
2014
+ return sender.jid ?? sender.lid ?? void 0;
2015
+ }
2016
+ //#endregion
1812
2017
  //#region extensions/whatsapp/src/auto-reply/monitor/ack-reaction.ts
1813
2018
  async function maybeSendAckReaction(params) {
1814
2019
  if (!params.msg.event.id) return null;
@@ -1818,14 +2023,16 @@ async function maybeSendAckReaction(params) {
1818
2023
  cfg: params.cfg,
1819
2024
  accountId
1820
2025
  }).level === "off") return null;
1821
- const ackConfig = params.cfg.channels?.whatsapp?.ackReaction;
2026
+ const ackConfig = params.cfg.messages?.ackReaction;
2027
+ const scope = params.cfg.messages?.ackReactionScope ?? "group-mentions";
2028
+ if (scope === "off" || scope === "none") return null;
1822
2029
  const emoji = resolveWhatsAppAckEmoji({
1823
2030
  cfg: params.cfg,
1824
2031
  agentId: params.agentId,
1825
2032
  ackConfig
1826
2033
  });
1827
- const directEnabled = ackConfig?.direct ?? true;
1828
- const groupMode = ackConfig?.group ?? "mentions";
2034
+ const directEnabled = scope === "all" || scope === "direct";
2035
+ const groupMode = scope === "all" || scope === "group-all" ? "always" : scope === "direct" ? "never" : "mentions";
1829
2036
  const isGroup = admission.conversation.kind === "group";
1830
2037
  const conversationIdForCheck = admission.conversation.id;
1831
2038
  const activation = isGroup ? await resolveGroupActivationFor({
@@ -1850,11 +2057,11 @@ async function maybeSendAckReaction(params) {
1850
2057
  messageId: params.msg.event.id,
1851
2058
  emoji
1852
2059
  }, "sending ack reaction");
1853
- const sender = getSenderIdentity(params.msg);
2060
+ const participant = resolveReactionParticipant(params.msg);
1854
2061
  const reactionOptions = {
1855
2062
  verbose: params.verbose,
1856
2063
  fromMe: false,
1857
- ...sender.jid ? { participant: sender.jid } : {},
2064
+ ...participant ? { participant } : {},
1858
2065
  accountId,
1859
2066
  cfg: params.cfg
1860
2067
  };
@@ -2024,7 +2231,13 @@ function recordPendingGroupHistoryEntry(params) {
2024
2231
  body: params.body ?? params.msg.payload.body,
2025
2232
  timestamp: params.msg.event.timestamp,
2026
2233
  id: params.msg.event.id,
2027
- senderJid: senderIdentity.jid ?? params.msg.platform.senderJid
2234
+ senderJid: senderIdentity.jid ?? params.msg.platform.senderJid,
2235
+ ...params.body === void 0 && params.msg.payload.media ? { media: [{
2236
+ path: params.msg.payload.media.path,
2237
+ url: params.msg.payload.media.url ?? params.msg.payload.media.path,
2238
+ contentType: params.msg.payload.media.type,
2239
+ kind: params.msg.payload.media.kind ?? void 0
2240
+ }] } : {}
2028
2241
  }
2029
2242
  });
2030
2243
  }
@@ -2360,7 +2573,7 @@ async function deliverWebReply(params) {
2360
2573
  const tableMode = params.tableMode ?? "code";
2361
2574
  const chunkMode = params.chunkMode ?? "length";
2362
2575
  const normalizedReply = params.normalizedReplyResult ?? normalizeWhatsAppOutboundPayload(replyResult, { normalizeText: normalizeWhatsAppPayloadTextPreservingIndentation });
2363
- const textChunks = chunkMarkdownTextWithMode(markdownToWhatsApp(convertMarkdownTables$1(normalizedReply.text ?? "", tableMode)), textLimit, chunkMode);
2576
+ const textChunks = markdownToWhatsAppChunks(normalizedReply.text ?? "", textLimit, tableMode, chunkMode);
2364
2577
  const mediaList = normalizedReply.mediaUrls ?? [];
2365
2578
  const getQuote = () => {
2366
2579
  if (!replyResult.replyToId) return;
@@ -2370,7 +2583,8 @@ async function deliverWebReply(params) {
2370
2583
  remoteJid: msg.platform.chatJid,
2371
2584
  fromMe: cached?.fromMe ?? false,
2372
2585
  participant: cached?.participant ?? (isGroupConversation ? msg.platform.senderJid : void 0),
2373
- messageText: cached?.body ?? ""
2586
+ messageText: cached?.body ?? "",
2587
+ media: cached?.media
2374
2588
  });
2375
2589
  };
2376
2590
  const sendWithRetry = async (fn, label) => {
@@ -2418,7 +2632,7 @@ async function deliverWebReply(params) {
2418
2632
  mediaUrls: mediaList,
2419
2633
  caption: remainingText.shift() || "",
2420
2634
  send: async ({ mediaUrl, caption }) => {
2421
- const media = await prepareWhatsAppOutboundMedia(await loadWebMedia$1(mediaUrl, {
2635
+ const media = await prepareWhatsAppOutboundMedia(await loadWebMedia(mediaUrl, {
2422
2636
  maxBytes: maxMediaBytes,
2423
2637
  localRoots: params.mediaLocalRoots
2424
2638
  }), mediaUrl);
@@ -2505,32 +2719,47 @@ function isWhatsAppSupplementalSenderAllowed(params) {
2505
2719
  return false;
2506
2720
  }
2507
2721
  function resolveVisibleWhatsAppGroupHistory(params) {
2508
- if (params.groupPolicy !== "allowlist") return params.history;
2509
2722
  return filterSupplementalContextItems({
2510
2723
  items: params.history,
2511
2724
  mode: params.mode,
2512
2725
  kind: "history",
2513
- isSenderAllowed: (entry) => isWhatsAppSupplementalSenderAllowed({
2726
+ isSenderAllowed: (entry) => resolveInboundSupplementalSenderAllowed({
2727
+ isGroup: true,
2728
+ groupPolicy: params.groupPolicy,
2514
2729
  allowFrom: params.groupAllowFrom,
2515
- authDir: params.authDir,
2516
- sender: entry.senderJid ? { jid: entry.senderJid } : null
2730
+ isSenderAllowed: (allowFrom) => isWhatsAppSupplementalSenderAllowed({
2731
+ allowFrom,
2732
+ authDir: params.authDir,
2733
+ sender: entry.senderJid ? { jid: entry.senderJid } : null
2734
+ })
2517
2735
  })
2518
2736
  }).items;
2519
2737
  }
2520
2738
  function resolveVisibleWhatsAppReplyContext(params) {
2521
2739
  const replyTo = getReplyContext(params.msg, params.authDir);
2522
2740
  if (!replyTo) return null;
2523
- const senderAllowed = requireWhatsAppInboundAdmission(params.msg).conversation.kind !== "group" || params.groupPolicy !== "allowlist" ? true : isWhatsAppSupplementalSenderAllowed({
2741
+ const admission = requireWhatsAppInboundAdmission(params.msg);
2742
+ const previewBody = [replyTo.body, formatMediaPlaceholderText(replyTo.media ? [replyTo.media] : [])].filter(Boolean).join("\n");
2743
+ const senderAllowed = resolveInboundSupplementalSenderAllowed({
2744
+ isGroup: admission.conversation.kind === "group",
2745
+ groupPolicy: params.groupPolicy,
2524
2746
  allowFrom: params.groupAllowFrom,
2525
- authDir: params.authDir,
2526
- sender: replyTo.sender
2747
+ isSenderAllowed: (allowFrom) => isWhatsAppSupplementalSenderAllowed({
2748
+ allowFrom,
2749
+ authDir: params.authDir,
2750
+ sender: replyTo.sender
2751
+ })
2527
2752
  });
2528
- return filterChannelInboundQuoteContext(params.mode, {
2753
+ const visible = filterChannelInboundQuoteContext(params.mode, {
2529
2754
  id: replyTo.id,
2530
- body: replyTo.body,
2755
+ body: previewBody,
2531
2756
  sender: replyTo.sender?.label ?? void 0,
2532
2757
  senderAllowed
2533
- }) ? replyTo : null;
2758
+ });
2759
+ return visible ? {
2760
+ ...replyTo,
2761
+ body: visible.body ?? ""
2762
+ } : null;
2534
2763
  }
2535
2764
  //#endregion
2536
2765
  //#region extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts
@@ -2639,7 +2868,7 @@ function logWhatsAppMediaOnlyFlushResult(result) {
2639
2868
  }
2640
2869
  function resolveWhatsAppResponsePrefix(params) {
2641
2870
  const configuredResponsePrefix = params.cfg.messages?.responsePrefix;
2642
- return params.pipelineResponsePrefix ?? (configuredResponsePrefix === void 0 && params.isSelfChat ? resolveIdentityNamePrefix$1(params.cfg, params.agentId) : void 0);
2871
+ return params.pipelineResponsePrefix ?? (configuredResponsePrefix === "auto" ? resolveIdentityNamePrefix$1(params.cfg, params.agentId) : configuredResponsePrefix) ?? (params.isSelfChat ? resolveIdentityNamePrefix$1(params.cfg, params.agentId) : void 0);
2643
2872
  }
2644
2873
  async function buildWhatsAppInboundContext(params) {
2645
2874
  const admission = requireWhatsAppInboundAdmission(params.msg);
@@ -2651,14 +2880,16 @@ async function buildWhatsAppInboundContext(params) {
2651
2880
  sender: entry.sender,
2652
2881
  body: entry.body,
2653
2882
  timestamp: entry.timestamp,
2654
- messageId: entry.id
2883
+ messageId: entry.id,
2884
+ media: entry.media
2655
2885
  })),
2656
2886
  limit: params.groupHistory?.length ?? 1
2657
2887
  }) : void 0;
2658
- const media = toInboundMediaFacts(params.msg.payload.media?.path || params.msg.payload.media?.url ? [{
2888
+ const media = toInboundMediaFacts(params.msg.payload.media ? [{
2659
2889
  path: params.msg.payload.media?.path,
2660
2890
  url: params.msg.payload.media?.url ?? params.msg.payload.media?.path,
2661
- contentType: params.msg.payload.media?.type
2891
+ contentType: params.msg.payload.media?.type,
2892
+ kind: params.msg.payload.media?.kind
2662
2893
  }] : void 0, { transcribed: (_entry, index) => params.mediaTranscribedIndexes?.includes(index) === true });
2663
2894
  return buildChannelInboundEventContext({
2664
2895
  channel: "whatsapp",
@@ -2686,6 +2917,7 @@ async function buildWhatsAppInboundContext(params) {
2686
2917
  },
2687
2918
  route: {
2688
2919
  agentId: params.route.agentId,
2920
+ dmScope: params.route.dmScope,
2689
2921
  accountId: params.route.accountId,
2690
2922
  routeSessionKey: params.route.sessionKey
2691
2923
  },
@@ -2700,6 +2932,7 @@ async function buildWhatsAppInboundContext(params) {
2700
2932
  rawBody: params.rawBody ?? params.msg.payload.body,
2701
2933
  commandBody: params.command?.body ?? params.msg.payload.body
2702
2934
  },
2935
+ sessionTranscript: { historyLimit: conversationKind === "group" ? params.groupHistoryLimit ?? params.groupHistory?.length ?? 0 : 0 },
2703
2936
  access: {
2704
2937
  ...wasMentioned !== void 0 ? { mentions: {
2705
2938
  canDetectMention: conversationKind === "group",
@@ -2754,16 +2987,12 @@ function updateWhatsAppMainLastRoute(params) {
2754
2987
  }
2755
2988
  if (params.dmRouteTarget && inboundLastRouteSessionKey === params.route.mainSessionKey && params.pinnedMainDmRecipient) logVerbose$1(`Skipping main-session last route update for ${params.dmRouteTarget} (pinned owner ${params.pinnedMainDmRecipient})`);
2756
2989
  }
2757
- async function dispatchWhatsAppBufferedReply(params) {
2990
+ function createWhatsAppReplyPlan(params) {
2758
2991
  const admission = requireWhatsAppInboundAdmission(params.msg);
2992
+ const ingressLifecycle = resolveWhatsAppIngressLifecycle(params.msg);
2759
2993
  const conversationId = admission.conversation.id;
2760
2994
  const conversationKind = admission.conversation.kind;
2761
2995
  const statusReactionController = params.statusReactionController ?? null;
2762
- const statusReactionTiming = {
2763
- ...DEFAULT_TIMING,
2764
- ...params.cfg.messages?.statusReactions?.timing
2765
- };
2766
- const removeAckAfterReply = params.cfg.messages?.removeAckAfterReply ?? false;
2767
2996
  const textLimit = params.maxMediaTextChunkLimit ?? resolveTextChunkLimit(params.cfg, "whatsapp");
2768
2997
  const chunkMode = resolveChunkMode(params.cfg, "whatsapp", params.route.accountId);
2769
2998
  const tableMode = resolveMarkdownTableMode$1({
@@ -2821,11 +3050,10 @@ async function dispatchWhatsAppBufferedReply(params) {
2821
3050
  const mediaOnlyCoalescer = createWhatsAppMediaOnlyReplyCoalescer({ deliver: async (pending) => {
2822
3051
  return await deliverNormalizedPayload(pending.payload, pending.info);
2823
3052
  } });
2824
- if (statusReactionController) statusReactionController.setThinking();
2825
- const dispatchResult = await dispatchReplyWithBufferedBlockDispatcher({
2826
- ctx: params.context,
2827
- cfg: params.cfg,
2828
- replyResolver: params.replyResolver,
3053
+ return {
3054
+ afterRecord: () => {
3055
+ if (statusReactionController) statusReactionController.setThinking();
3056
+ },
2829
3057
  dispatcherOptions: {
2830
3058
  ...params.replyPipeline,
2831
3059
  onHeartbeatStrip: () => {
@@ -2834,6 +3062,14 @@ async function dispatchWhatsAppBufferedReply(params) {
2834
3062
  logVerbose$1("Stripped stray HEARTBEAT_OK token from web reply");
2835
3063
  }
2836
3064
  },
3065
+ onSettled: async () => {
3066
+ const flushResult = await mediaOnlyCoalescer.flushAll();
3067
+ logWhatsAppMediaOnlyFlushResult(flushResult);
3068
+ return whatsAppReplyDeliveryVisibility(flushResult.delivered > 0);
3069
+ },
3070
+ onReplyStart: params.msg.platform.sendComposing
3071
+ },
3072
+ delivery: {
2837
3073
  deliver: async (payload, info) => {
2838
3074
  const deliveryPayload = resolveWhatsAppDeliverablePayload(payload, info);
2839
3075
  if (!deliveryPayload) return whatsAppReplyDeliveryVisibility(false);
@@ -2884,8 +3120,8 @@ async function dispatchWhatsAppBufferedReply(params) {
2884
3120
  return whatsAppReplyDeliveryVisibilityFromDurableResult(durable.delivery);
2885
3121
  }
2886
3122
  if (durable.status === "handled_no_send") return flushResult.delivered > 0 ? whatsAppReplyDeliveryVisibility(true) : whatsAppReplyDeliveryVisibilityFromDurableResult(durable.delivery);
2887
- const delivery = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
2888
- return flushResult.delivered > 0 && !delivery.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : delivery;
3123
+ const deliveryResult = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
3124
+ return flushResult.delivered > 0 && !deliveryResult.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : deliveryResult;
2889
3125
  } catch (error) {
2890
3126
  throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
2891
3127
  }
@@ -2906,27 +3142,12 @@ async function dispatchWhatsAppBufferedReply(params) {
2906
3142
  const flushResult = await mediaOnlyCoalescer.flushExceptDuplicateMedia(mediaUrls);
2907
3143
  logWhatsAppMediaOnlyFlushResult(flushResult);
2908
3144
  try {
2909
- const delivery = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
2910
- return flushResult.delivered > 0 && !delivery.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : delivery;
3145
+ const deliveryResult = await deliverNormalizedPayload(normalizedDeliveryPayload, info);
3146
+ return flushResult.delivered > 0 && !deliveryResult.visibleReplySent ? whatsAppReplyDeliveryVisibility(true) : deliveryResult;
2911
3147
  } catch (error) {
2912
3148
  throw markWhatsAppReplyDeliveryErrorVisibleAfterFlush(error, flushResult);
2913
3149
  }
2914
3150
  },
2915
- onSettled: async () => {
2916
- const flushResult = await mediaOnlyCoalescer.flushAll();
2917
- logWhatsAppMediaOnlyFlushResult(flushResult);
2918
- return whatsAppReplyDeliveryVisibility(flushResult.delivered > 0);
2919
- },
2920
- onReplyStart: params.msg.platform.sendComposing,
2921
- ...statusReactionController ? {
2922
- onCompactionStart: async () => {
2923
- await statusReactionController.setCompacting();
2924
- },
2925
- onCompactionEnd: async () => {
2926
- statusReactionController.cancelPending();
2927
- await statusReactionController.setThinking();
2928
- }
2929
- } : {},
2930
3151
  onError: (err, info) => {
2931
3152
  logWhatsAppReplyDeliveryError({
2932
3153
  err,
@@ -2938,64 +3159,50 @@ async function dispatchWhatsAppBufferedReply(params) {
2938
3159
  }
2939
3160
  },
2940
3161
  replyOptions: {
3162
+ ...ingressLifecycle ? bindIngressLifecycleToReplyOptions(ingressLifecycle) : {},
2941
3163
  suppressTyping: sourceRepliesAreToolOnly && conversationKind === "group" && !(params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned),
2942
3164
  disableBlockStreaming,
2943
3165
  ...sourceReplyDeliveryMode ? { sourceReplyDeliveryMode } : {},
2944
3166
  onModelSelected: params.onModelSelected,
2945
- ...statusReactionController ? { onToolStart: async (payload) => {
2946
- const toolName = payload.name?.trim();
2947
- if (toolName) await statusReactionController.setTool(toolName);
2948
- } } : {}
3167
+ ...statusReactionController ? {
3168
+ onToolStart: async (payload) => {
3169
+ const toolName = payload.name?.trim();
3170
+ if (toolName) await statusReactionController.setTool(toolName);
3171
+ },
3172
+ onCompactionStart: async () => {
3173
+ await statusReactionController.setCompacting();
3174
+ },
3175
+ onCompactionEnd: async () => {
3176
+ statusReactionController.cancelPending();
3177
+ await statusReactionController.setThinking();
3178
+ }
3179
+ } : {}
3180
+ },
3181
+ replyResolver: params.replyResolver,
3182
+ finalize: (dispatchResult) => {
3183
+ const didQueueVisibleReply = hasVisibleInboundReplyDispatch(dispatchResult);
3184
+ const didDeliverVisibleReply = didSendReply || dispatchResult.observedReplyDelivery === true;
3185
+ if (!didQueueVisibleReply) {
3186
+ if (statusReactionController) finalizeWhatsAppStatusReaction({
3187
+ controller: statusReactionController,
3188
+ outcome: "error"
3189
+ });
3190
+ if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
3191
+ logVerbose$1("Skipping auto-reply: silent token or no text/media returned from resolver");
3192
+ return false;
3193
+ }
3194
+ if (statusReactionController) finalizeWhatsAppStatusReaction({
3195
+ controller: statusReactionController,
3196
+ outcome: didDeliverVisibleReply ? "done" : "error"
3197
+ });
3198
+ if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
3199
+ return didDeliverVisibleReply;
2949
3200
  }
2950
- });
2951
- const didQueueVisibleReply = hasVisibleInboundReplyDispatch(dispatchResult);
2952
- const didDeliverVisibleReply = didSendReply || dispatchResult.observedReplyDelivery === true;
2953
- if (!didQueueVisibleReply) {
2954
- if (statusReactionController) finalizeWhatsAppStatusReaction({
2955
- controller: statusReactionController,
2956
- outcome: "error",
2957
- hasFinalResponse: false,
2958
- removeAckAfterReply,
2959
- timing: statusReactionTiming
2960
- });
2961
- if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
2962
- logVerbose$1("Skipping auto-reply: silent token or no text/media returned from resolver");
2963
- return false;
2964
- }
2965
- if (statusReactionController) finalizeWhatsAppStatusReaction({
2966
- controller: statusReactionController,
2967
- outcome: didDeliverVisibleReply ? "done" : "error",
2968
- hasFinalResponse: didDeliverVisibleReply,
2969
- removeAckAfterReply,
2970
- timing: statusReactionTiming
2971
- });
2972
- if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
2973
- return didDeliverVisibleReply;
3201
+ };
2974
3202
  }
2975
3203
  async function finalizeWhatsAppStatusReaction(params) {
2976
- if (params.outcome === "done") {
2977
- await params.controller.setDone();
2978
- if (params.removeAckAfterReply) {
2979
- await new Promise((resolve) => {
2980
- setTimeout(resolve, params.timing.doneHoldMs);
2981
- });
2982
- await params.controller.clear();
2983
- } else await params.controller.restoreInitial();
2984
- return;
2985
- }
2986
- await params.controller.setError();
2987
- if (params.hasFinalResponse) {
2988
- if (params.removeAckAfterReply) {
2989
- await new Promise((resolve) => {
2990
- setTimeout(resolve, params.timing.errorHoldMs);
2991
- });
2992
- await params.controller.clear();
2993
- } else await params.controller.restoreInitial();
2994
- return;
2995
- }
2996
- if (params.removeAckAfterReply) await new Promise((resolve) => {
2997
- setTimeout(resolve, params.timing.errorHoldMs);
2998
- });
3204
+ if (params.outcome === "done") await params.controller.setDone();
3205
+ else await params.controller.setError();
2999
3206
  await params.controller.restoreInitial();
3000
3207
  }
3001
3208
  //#endregion
@@ -3009,7 +3216,7 @@ function resolveIdentityNamePrefix(cfg, agentId) {
3009
3216
  return identityName ? `[${identityName}]` : void 0;
3010
3217
  }
3011
3218
  function resolveMessagePrefix(cfg, agentId, opts) {
3012
- const configured = opts?.configured ?? cfg.messages?.messagePrefix;
3219
+ const configured = opts?.configured;
3013
3220
  if (configured !== void 0) return configured;
3014
3221
  if (opts?.hasAllowFrom === true) return "";
3015
3222
  return resolveIdentityNamePrefix(cfg, agentId) ?? opts?.fallback ?? "[openclaw]";
@@ -3026,7 +3233,7 @@ function formatReplyContext(msg) {
3026
3233
  function buildInboundLine(params) {
3027
3234
  const { cfg, msg, agentId, previousTimestamp, envelope } = params;
3028
3235
  const messagePrefix = resolveMessagePrefix(cfg, agentId, {
3029
- configured: cfg.channels?.whatsapp?.messagePrefix,
3236
+ configured: cfg.channels?.whatsapp?.responsePrefix,
3030
3237
  hasAllowFrom: (cfg.channels?.whatsapp?.allowFrom?.length ?? 0) > 0
3031
3238
  });
3032
3239
  const admission = requireWhatsAppInboundAdmission(msg);
@@ -3056,23 +3263,24 @@ function buildInboundLine(params) {
3056
3263
  //#region extensions/whatsapp/src/auto-reply/monitor/status-reaction.ts
3057
3264
  async function createWhatsAppStatusReactionController(params) {
3058
3265
  if (!params.msg.event.id) return null;
3059
- const statusReactionsConfig = params.cfg.messages?.statusReactions;
3060
- if (statusReactionsConfig?.enabled !== true) return null;
3266
+ if ((params.cfg.messages?.statusReactions)?.enabled !== true) return null;
3061
3267
  const admission = requireWhatsAppInboundAdmission(params.msg);
3062
3268
  const accountId = admission.accountId;
3063
3269
  if (resolveWhatsAppReactionLevel({
3064
3270
  cfg: params.cfg,
3065
3271
  accountId
3066
3272
  }).level === "off") return null;
3067
- const ackConfig = params.cfg.channels?.whatsapp?.ackReaction;
3273
+ const ackConfig = params.cfg.messages?.ackReaction;
3274
+ const scope = params.cfg.messages?.ackReactionScope ?? "group-mentions";
3275
+ if (scope === "off" || scope === "none") return null;
3068
3276
  const ackEmoji = resolveWhatsAppAckEmoji({
3069
3277
  cfg: params.cfg,
3070
3278
  agentId: params.agentId,
3071
3279
  ackConfig
3072
3280
  });
3073
3281
  if (!ackEmoji) return null;
3074
- const directEnabled = ackConfig?.direct ?? true;
3075
- const groupMode = ackConfig?.group ?? "mentions";
3282
+ const directEnabled = scope === "all" || scope === "direct";
3283
+ const groupMode = scope === "all" || scope === "group-all" ? "always" : scope === "direct" ? "never" : "mentions";
3076
3284
  const isGroup = admission.conversation.kind === "group";
3077
3285
  const conversationIdForCheck = admission.conversation.id;
3078
3286
  const activation = isGroup ? await resolveGroupActivationFor({
@@ -3091,11 +3299,11 @@ async function createWhatsAppStatusReactionController(params) {
3091
3299
  wasMentioned: (params.msg.groupMention?.wasMentioned ?? params.msg.wasMentioned) === true,
3092
3300
  groupActivated: activation === "always"
3093
3301
  })) return null;
3094
- const sender = getSenderIdentity(params.msg);
3302
+ const participant = resolveReactionParticipant(params.msg);
3095
3303
  const reactionOptions = {
3096
3304
  verbose: params.verbose,
3097
3305
  fromMe: false,
3098
- ...sender.jid ? { participant: sender.jid } : {},
3306
+ ...participant ? { participant } : {},
3099
3307
  accountId,
3100
3308
  cfg: params.cfg
3101
3309
  };
@@ -3112,8 +3320,7 @@ async function createWhatsAppStatusReactionController(params) {
3112
3320
  }
3113
3321
  },
3114
3322
  initialEmoji: ackEmoji,
3115
- emojis: statusReactionsConfig.emojis,
3116
- timing: statusReactionsConfig.timing,
3323
+ emojis: void 0,
3117
3324
  onError: (err) => {
3118
3325
  logVerbose(`WhatsApp status-reaction error for chat ${chatId}/${msgId}: ${String(err)}`);
3119
3326
  }
@@ -3182,13 +3389,16 @@ async function processMessage(params) {
3182
3389
  sessionKey: params.route.sessionKey
3183
3390
  });
3184
3391
  let audioTranscript = params.preflightAudioTranscript ?? void 0;
3185
- const hasAudioBody = params.msg.payload.media?.type?.startsWith("audio/") === true && params.msg.payload.body === "<media:audio>";
3392
+ const hasAudioBody = (params.msg.payload.media?.kind === "audio" || params.msg.payload.media?.type?.startsWith("audio/") === true) && !params.msg.payload.body.trim();
3186
3393
  if (params.preflightAudioTranscript === void 0 && hasAudioBody && params.msg.payload.media?.path) try {
3187
3394
  const { transcribeFirstAudio } = await import("./audio-preflight.runtime-C_glQhZY.js");
3188
3395
  audioTranscript = await transcribeFirstAudio({
3189
3396
  ctx: {
3190
- MediaPaths: [params.msg.payload.media?.path],
3191
- MediaTypes: params.msg.payload.media?.type ? [params.msg.payload.media?.type] : void 0,
3397
+ media: [{
3398
+ path: params.msg.payload.media.path,
3399
+ contentType: params.msg.payload.media.type,
3400
+ kind: params.msg.payload.media.kind ?? void 0
3401
+ }],
3192
3402
  From: conversationId,
3193
3403
  To: params.msg.platform.recipientJid,
3194
3404
  Provider: "whatsapp",
@@ -3200,7 +3410,7 @@ async function processMessage(params) {
3200
3410
  cfg: params.cfg
3201
3411
  });
3202
3412
  } catch {
3203
- if (shouldLogVerbose$1()) logVerbose$1("whatsapp: audio preflight transcription failed, using placeholder");
3413
+ if (shouldLogVerbose$1()) logVerbose$1("whatsapp: audio preflight transcription failed, keeping structured audio");
3204
3414
  }
3205
3415
  const msgForAgent = audioTranscript !== void 0 ? {
3206
3416
  ...params.msg,
@@ -3238,7 +3448,8 @@ async function processMessage(params) {
3238
3448
  entries: history.map((m) => ({
3239
3449
  sender: m.sender,
3240
3450
  body: m.body,
3241
- timestamp: m.timestamp
3451
+ timestamp: m.timestamp,
3452
+ media: m.media
3242
3453
  })),
3243
3454
  currentMessage: combinedBody,
3244
3455
  excludeLast: false,
@@ -3247,7 +3458,7 @@ async function processMessage(params) {
3247
3458
  channel: "WhatsApp",
3248
3459
  from: conversationId,
3249
3460
  timestamp: entry.timestamp,
3250
- body: entry.body,
3461
+ body: [entry.body, formatMediaPlaceholderText(entry.media ?? [])].filter(Boolean).join("\n"),
3251
3462
  chatType: "group",
3252
3463
  senderLabel: entry.sender,
3253
3464
  envelope: envelopeOptions
@@ -3341,6 +3552,7 @@ async function processMessage(params) {
3341
3552
  authorized: commandAuthorized
3342
3553
  },
3343
3554
  groupHistory: visibleGroupHistory,
3555
+ groupHistoryLimit: params.groupHistoryLimit,
3344
3556
  groupMemberRoster: params.groupMemberNames.get(params.groupHistoryKey),
3345
3557
  groupSystemPrompt: conversationSystemPrompt,
3346
3558
  msg: params.msg,
@@ -3377,6 +3589,7 @@ async function processMessage(params) {
3377
3589
  updateLastRoute: updateLastRouteInBackground,
3378
3590
  warn: params.replyLogger.warn.bind(params.replyLogger)
3379
3591
  });
3592
+ let finalizeReply;
3380
3593
  const turnResult = await runChannelInboundEvent({
3381
3594
  channel: "whatsapp",
3382
3595
  accountId: params.route.accountId,
@@ -3410,26 +3623,8 @@ async function processMessage(params) {
3410
3623
  recordHistory: false
3411
3624
  } };
3412
3625
  },
3413
- resolveTurn: () => ({
3414
- channel: "whatsapp",
3415
- accountId: params.route.accountId,
3416
- routeSessionKey: params.route.sessionKey,
3417
- storePath,
3418
- ctxPayload,
3419
- recordInboundSession,
3420
- record: {
3421
- onRecordError: (err) => {
3422
- params.replyLogger.warn({
3423
- error: formatError(err),
3424
- storePath,
3425
- sessionKey: params.route.sessionKey
3426
- }, "failed updating session meta");
3427
- },
3428
- trackSessionMetaTask: (task) => {
3429
- trackBackgroundTask(params.backgroundTasks, task);
3430
- }
3431
- },
3432
- runDispatch: () => dispatchWhatsAppBufferedReply({
3626
+ resolveTurn: () => {
3627
+ const { finalize, ...replyPlan } = createWhatsAppReplyPlan({
3433
3628
  cfg: params.cfg,
3434
3629
  connectionId: params.connectionId,
3435
3630
  context: ctxPayload,
@@ -3450,13 +3645,37 @@ async function processMessage(params) {
3450
3645
  route: params.route,
3451
3646
  shouldClearGroupHistory,
3452
3647
  statusReactionController
3453
- })
3454
- })
3648
+ });
3649
+ finalizeReply = finalize;
3650
+ return {
3651
+ cfg: params.cfg,
3652
+ channel: "whatsapp",
3653
+ accountId: params.route.accountId,
3654
+ route: {
3655
+ agentId: params.route.agentId,
3656
+ sessionKey: params.route.sessionKey
3657
+ },
3658
+ ctxPayload,
3659
+ record: {
3660
+ onRecordError: (err) => {
3661
+ params.replyLogger.warn({
3662
+ error: formatError(err),
3663
+ storePath,
3664
+ sessionKey: params.route.sessionKey
3665
+ }, "failed updating session meta");
3666
+ },
3667
+ trackSessionMetaTask: (task) => {
3668
+ trackBackgroundTask(params.backgroundTasks, task);
3669
+ }
3670
+ },
3671
+ ...replyPlan
3672
+ };
3673
+ }
3455
3674
  }
3456
3675
  });
3457
- const didSendReply = turnResult.dispatched ? turnResult.dispatchResult : false;
3676
+ const didSendReply = turnResult.dispatched ? finalizeReply?.(turnResult.dispatchResult) ?? false : false;
3458
3677
  removeAckReactionHandleAfterReply({
3459
- removeAfterReply: Boolean(params.cfg.messages?.removeAckAfterReply && didSendReply),
3678
+ removeAfterReply: false,
3460
3679
  ackReaction,
3461
3680
  onError: (err) => {
3462
3681
  logAckFailure({
@@ -3496,6 +3715,7 @@ function createWebOnMessageHandler(params) {
3496
3715
  route,
3497
3716
  groupHistoryKey,
3498
3717
  groupHistories: params.groupHistories,
3718
+ groupHistoryLimit: params.groupHistoryLimit,
3499
3719
  groupMemberNames: params.groupMemberNames,
3500
3720
  connectionId: params.connectionId,
3501
3721
  verbose: params.verbose,
@@ -3563,7 +3783,7 @@ function createWebOnMessageHandler(params) {
3563
3783
  peerId
3564
3784
  }) : route.sessionKey;
3565
3785
  let preflightAudioTranscript;
3566
- const hasAudioBody = msg.payload.media?.type?.startsWith("audio/") === true && msg.payload.body === "<media:audio>";
3786
+ const hasAudioBody = (msg.payload.media?.kind === "audio" || msg.payload.media?.type?.startsWith("audio/") === true) && !msg.payload.body.trim();
3567
3787
  const canRunEarlyAudioPreflight = conversationKind === "group" || canRunDirectEarlyAudioPreflight;
3568
3788
  let ackAlreadySent = false;
3569
3789
  let ackReaction = null;
@@ -3589,8 +3809,11 @@ function createWebOnMessageHandler(params) {
3589
3809
  const { transcribeFirstAudio } = await import("./audio-preflight.runtime-C_glQhZY.js");
3590
3810
  preflightAudioTranscript = await transcribeFirstAudio({
3591
3811
  ctx: {
3592
- MediaPaths: [msg.payload.media?.path],
3593
- MediaTypes: msg.payload.media?.type ? [msg.payload.media?.type] : void 0,
3812
+ media: [{
3813
+ path: msg.payload.media.path,
3814
+ contentType: msg.payload.media.type,
3815
+ kind: msg.payload.media.kind ?? void 0
3816
+ }],
3594
3817
  From: conversationId,
3595
3818
  To: msg.platform.recipientJid,
3596
3819
  Provider: "whatsapp",
@@ -3761,8 +3984,7 @@ function resolveWebMonitorConfigSnapshot(params) {
3761
3984
  ...params.cfg.channels,
3762
3985
  whatsapp: {
3763
3986
  ...params.cfg.channels?.whatsapp,
3764
- ackReaction: account.ackReaction,
3765
- messagePrefix: account.messagePrefix,
3987
+ responsePrefix: account.messagePrefix,
3766
3988
  allowFrom: account.allowFrom,
3767
3989
  groupAllowFrom: account.groupAllowFrom,
3768
3990
  groupPolicy: account.groupPolicy,
@@ -3798,6 +4020,7 @@ function isRetryableAuthUnstableError(error) {
3798
4020
  return error instanceof WhatsAppAuthUnstableError || typeof error === "object" && error !== null && "code" in error && error.code === "whatsapp-auth-unstable";
3799
4021
  }
3800
4022
  const DEFAULT_TRANSPORT_TIMEOUT_MS = 300 * 1e3;
4023
+ const WHATSAPP_RECONNECT_CATCH_UP_MAX_MS = 20 * 6e4;
3801
4024
  async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSocket, keepAlive = true, replyResolver, runtime = defaultRuntime, abortSignal, tuning = {}) {
3802
4025
  const activeReplyResolver = replyResolver ?? (await loadReplyResolverRuntime()).getReplyFromConfig;
3803
4026
  const runId = newConnectionId();
@@ -3826,7 +4049,7 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
3826
4049
  const maxMediaBytes = resolveWhatsAppMediaMaxBytes(account);
3827
4050
  const heartbeatSeconds = resolveHeartbeatSeconds(cfg, tuning.heartbeatSeconds);
3828
4051
  const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
3829
- const socketTiming = resolveWhatsAppSocketTiming(cfg, tuning.socketTiming);
4052
+ const socketTiming = resolveWhatsAppSocketTiming(tuning.socketTiming);
3830
4053
  const baseMentionConfig = buildMentionConfig(cfg);
3831
4054
  const groupHistoryLimit = account.historyLimit ?? cfg.channels?.whatsapp?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT;
3832
4055
  const groupHistories = /* @__PURE__ */ new Map();
@@ -3849,7 +4072,7 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
3849
4072
  process.once("SIGINT", handleSigint);
3850
4073
  const transportTimeoutMs = tuning.transportTimeoutMs ?? DEFAULT_TRANSPORT_TIMEOUT_MS;
3851
4074
  const messageTimeoutMs = tuning.messageTimeoutMs ?? 1800 * 1e3;
3852
- const reconnectCatchUpWindowMs = Math.min(Math.max(messageTimeoutMs, 6e4), WHATSAPP_INBOUND_DEDUPE_TTL_MS);
4075
+ const reconnectCatchUpWindowMs = Math.min(Math.max(messageTimeoutMs, 6e4), WHATSAPP_RECONNECT_CATCH_UP_MAX_MS);
3853
4076
  const watchdogCheckMs = tuning.watchdogCheckMs ?? 60 * 1e3;
3854
4077
  const controller = new WhatsAppConnectionController({
3855
4078
  accountId: account.accountId,
@@ -4225,4 +4448,4 @@ async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSock
4225
4448
  }
4226
4449
  }
4227
4450
  //#endregion
4228
- export { loadWebMediaRaw as a, monitorWebInbox as c, loadWebMedia$1 as i, resetWebInboundDedupe as l, LocalMediaAccessError as n, optimizeImageToJpeg as o, getDefaultLocalRoots as r, optimizeImageToPng as s, monitorWebChannel as t };
4451
+ export { loadWebMediaRaw as a, monitorWebInbox as c, loadWebMedia as i, resetWebInboundDedupe as l, LocalMediaAccessError as n, optimizeImageToJpeg as o, getDefaultLocalRoots as r, optimizeImageToPng as s, monitorWebChannel as t };