@openclaw/feishu 2026.7.2-beta.3 → 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 (146) hide show
  1. package/dist/api.js +3 -3
  2. package/dist/{app-registration-BzkyrVZu.js → app-registration-cN1bVV6s.js} +5 -5
  3. package/dist/{channel-BdnXYU6g.js → channel-DB1x6UJX.js} +15 -8
  4. package/dist/channel-plugin-api.js +1 -1
  5. package/dist/{channel.runtime-CVIzo2dV.js → channel.runtime-B5yAAvbT.js} +5 -5
  6. package/dist/{client-87BmeMpj.js → client-Dee7cKsS.js} +62 -1
  7. package/dist/{doctor-contract-DXH5hux1.js → doctor-contract-Cw5sdjOm.js} +29 -41
  8. package/dist/doctor-contract-api.js +1 -1
  9. package/dist/{drive-BHv8WW9i.js → drive-_zrcOxzS.js} +1 -1
  10. package/dist/{send-DcrXbqqA.js → media-CRJunSRr.js} +1048 -1019
  11. package/dist/{monitor-BVQf7-Bl.js → monitor-8CLu6myv.js} +5 -4
  12. package/dist/{monitor.account-Blbf2T6J.js → monitor.account-CkYj45PC.js} +1563 -589
  13. package/dist/monitor.startup-CSUPT_U1.js +143 -0
  14. package/dist/{probe-BgboTHIn.js → probe-Za1kgUvx.js} +70 -10
  15. package/dist/setup-api.js +1 -1
  16. package/node_modules/@larksuiteoapi/node-sdk/README.md +13 -0
  17. package/node_modules/@larksuiteoapi/node-sdk/README.zh.md +12 -0
  18. package/node_modules/@larksuiteoapi/node-sdk/es/index.js +8950 -3084
  19. package/node_modules/@larksuiteoapi/node-sdk/lib/index.js +8951 -3083
  20. package/node_modules/@larksuiteoapi/node-sdk/package.json +1 -1
  21. package/node_modules/@larksuiteoapi/node-sdk/types/index.d.ts +12522 -698
  22. package/node_modules/@protobufjs/utf8/CHANGELOG.md +8 -0
  23. package/node_modules/@protobufjs/utf8/index.js +29 -18
  24. package/node_modules/@protobufjs/utf8/package.json +2 -2
  25. package/node_modules/@protobufjs/utf8/tests/index.js +5 -0
  26. package/node_modules/@types/node/README.md +1 -1
  27. package/node_modules/@types/node/fs/promises.d.ts +16 -6
  28. package/node_modules/@types/node/package.json +2 -2
  29. package/node_modules/agent-base/README.md +145 -0
  30. package/node_modules/agent-base/dist/src/index.d.ts +78 -0
  31. package/node_modules/agent-base/dist/src/index.js +203 -0
  32. package/node_modules/agent-base/dist/src/index.js.map +1 -0
  33. package/node_modules/agent-base/dist/src/promisify.d.ts +4 -0
  34. package/node_modules/agent-base/dist/src/promisify.js +18 -0
  35. package/node_modules/agent-base/dist/src/promisify.js.map +1 -0
  36. package/node_modules/agent-base/package.json +64 -0
  37. package/node_modules/agent-base/src/index.ts +345 -0
  38. package/node_modules/agent-base/src/promisify.ts +33 -0
  39. package/node_modules/axios/CHANGELOG.md +197 -1
  40. package/node_modules/axios/README.md +424 -256
  41. package/node_modules/axios/dist/axios.js +589 -204
  42. package/node_modules/axios/dist/axios.min.js +3 -3
  43. package/node_modules/axios/dist/axios.min.js.map +1 -1
  44. package/node_modules/axios/dist/browser/axios.cjs +630 -185
  45. package/node_modules/axios/dist/esm/axios.js +630 -185
  46. package/node_modules/axios/dist/esm/axios.min.js +2 -2
  47. package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  48. package/node_modules/axios/dist/node/axios.cjs +949 -301
  49. package/node_modules/axios/index.d.cts +28 -5
  50. package/node_modules/axios/index.d.ts +24 -3
  51. package/node_modules/axios/lib/adapters/adapters.js +1 -1
  52. package/node_modules/axios/lib/adapters/fetch.js +223 -49
  53. package/node_modules/axios/lib/adapters/http.js +408 -193
  54. package/node_modules/axios/lib/adapters/xhr.js +3 -1
  55. package/node_modules/axios/lib/core/Axios.js +4 -2
  56. package/node_modules/axios/lib/core/AxiosError.js +13 -1
  57. package/node_modules/axios/lib/core/AxiosHeaders.js +13 -42
  58. package/node_modules/axios/lib/core/buildFullPath.js +29 -1
  59. package/node_modules/axios/lib/core/mergeConfig.js +35 -0
  60. package/node_modules/axios/lib/defaults/transitional.js +2 -0
  61. package/node_modules/axios/lib/env/data.js +1 -1
  62. package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -3
  63. package/node_modules/axios/lib/helpers/Http2Sessions.js +119 -0
  64. package/node_modules/axios/lib/helpers/buildURL.js +7 -4
  65. package/node_modules/axios/lib/helpers/composeSignals.js +48 -47
  66. package/node_modules/axios/lib/helpers/cookies.js +5 -1
  67. package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +16 -11
  68. package/node_modules/axios/lib/helpers/formDataToJSON.js +26 -4
  69. package/node_modules/axios/lib/helpers/formDataToStream.js +2 -2
  70. package/node_modules/axios/lib/helpers/fromDataURI.js +20 -5
  71. package/node_modules/axios/lib/helpers/progressEventReducer.js +3 -0
  72. package/node_modules/axios/lib/helpers/resolveConfig.js +26 -13
  73. package/node_modules/axios/lib/helpers/sanitizeHeaderValue.js +60 -0
  74. package/node_modules/axios/lib/helpers/shouldBypassProxy.js +33 -1
  75. package/node_modules/axios/lib/helpers/toFormData.js +48 -12
  76. package/node_modules/axios/lib/helpers/validator.js +1 -1
  77. package/node_modules/axios/lib/utils.js +105 -19
  78. package/node_modules/axios/package.json +31 -13
  79. package/node_modules/https-proxy-agent/README.md +137 -0
  80. package/node_modules/https-proxy-agent/dist/agent.d.ts +30 -0
  81. package/node_modules/https-proxy-agent/dist/agent.js +177 -0
  82. package/node_modules/https-proxy-agent/dist/agent.js.map +1 -0
  83. package/node_modules/https-proxy-agent/dist/index.d.ts +23 -0
  84. package/node_modules/https-proxy-agent/dist/index.js +14 -0
  85. package/node_modules/https-proxy-agent/dist/index.js.map +1 -0
  86. package/node_modules/https-proxy-agent/dist/parse-proxy-response.d.ts +7 -0
  87. package/node_modules/https-proxy-agent/dist/parse-proxy-response.js +66 -0
  88. package/node_modules/https-proxy-agent/dist/parse-proxy-response.js.map +1 -0
  89. package/node_modules/https-proxy-agent/package.json +56 -0
  90. package/node_modules/protobufjs/dist/light/protobuf.js +145 -188
  91. package/node_modules/protobufjs/dist/light/protobuf.js.map +1 -1
  92. package/node_modules/protobufjs/dist/light/protobuf.min.js +3 -3
  93. package/node_modules/protobufjs/dist/light/protobuf.min.js.map +1 -1
  94. package/node_modules/protobufjs/dist/minimal/protobuf.js +33 -76
  95. package/node_modules/protobufjs/dist/minimal/protobuf.js.map +1 -1
  96. package/node_modules/protobufjs/dist/minimal/protobuf.min.js +3 -3
  97. package/node_modules/protobufjs/dist/minimal/protobuf.min.js.map +1 -1
  98. package/node_modules/protobufjs/dist/protobuf.js +168 -208
  99. package/node_modules/protobufjs/dist/protobuf.js.map +1 -1
  100. package/node_modules/protobufjs/dist/protobuf.min.js +3 -3
  101. package/node_modules/protobufjs/dist/protobuf.min.js.map +1 -1
  102. package/node_modules/protobufjs/index.d.ts +0 -8
  103. package/node_modules/protobufjs/package.json +2 -3
  104. package/node_modules/protobufjs/src/parse.js +3 -0
  105. package/node_modules/protobufjs/src/util/minimal.js +0 -3
  106. package/node_modules/qs/CHANGELOG.md +11 -0
  107. package/node_modules/qs/dist/qs.js +17 -17
  108. package/node_modules/qs/lib/parse.js +19 -8
  109. package/node_modules/qs/lib/utils.js +47 -8
  110. package/node_modules/qs/package.json +6 -5
  111. package/node_modules/qs/test/parse.js +230 -0
  112. package/node_modules/qs/test/utils.js +194 -0
  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 +58 -38
  129. package/openclaw.plugin.json +2 -10
  130. package/package.json +20 -7
  131. package/dist/monitor.startup-Cy8NPGAX.js +0 -43
  132. package/node_modules/@protobufjs/inquire/CHANGELOG.md +0 -8
  133. package/node_modules/@protobufjs/inquire/LICENSE +0 -26
  134. package/node_modules/@protobufjs/inquire/README.md +0 -13
  135. package/node_modules/@protobufjs/inquire/index.d.ts +0 -10
  136. package/node_modules/@protobufjs/inquire/index.js +0 -38
  137. package/node_modules/@protobufjs/inquire/package.json +0 -21
  138. package/node_modules/@protobufjs/inquire/tests/data/array.js +0 -1
  139. package/node_modules/@protobufjs/inquire/tests/data/emptyArray.js +0 -1
  140. package/node_modules/@protobufjs/inquire/tests/data/emptyObject.js +0 -1
  141. package/node_modules/@protobufjs/inquire/tests/data/object.js +0 -1
  142. package/node_modules/@protobufjs/inquire/tests/index.js +0 -20
  143. package/node_modules/axios/dist/axios.js.map +0 -1
  144. package/node_modules/axios/dist/browser/axios.cjs.map +0 -1
  145. package/node_modules/axios/dist/esm/axios.js.map +0 -1
  146. package/node_modules/axios/dist/node/axios.cjs.map +0 -1
@@ -1,17 +1,17 @@
1
- import { _ as normalizeCommentFileType, c as encodeQuery, d as isRecord$1, f as normalizeString, g as buildFeishuCommentTarget, h as requestFeishuApi, l as extractReplyText, m as readString, o as resolveFeishuAccount, p as parseCommentContentElements, s as resolveFeishuRuntimeAccount } from "./accounts-BDoGHJDk.js";
1
+ import { _ as normalizeCommentFileType, c as encodeQuery, d as isRecord$2, f as normalizeString, g as buildFeishuCommentTarget, h as requestFeishuApi, l as extractReplyText, m as readString$1, o as resolveFeishuAccount, p as parseCommentContentElements, s as resolveFeishuRuntimeAccount } from "./accounts-BDoGHJDk.js";
2
2
  import { i as resolveReceiveIdType } from "./targets-BUjQ1TcA.js";
3
3
  import { A as resolveFeishuChatType, C as resolveFeishuDmIngressAccess, E as resolveFeishuGroupSenderActivationIngressAccess, M as buildFeishuCardActionTextFallback, N as createFeishuCardInteractionEnvelope, O as resolveFeishuReplyPolicy, P as decodeFeishuCardAction, S as normalizeFeishuAllowEntry, T as resolveFeishuGroupConversationIngressAccess, d as chunkFeishuPostMarkdown, f as materializeFeishuPostMarkdownSoftBreaks, k as normalizeFeishuChatType$2, o as resolveFeishuCardTemplate, w as resolveFeishuGroupConfig, x as hasExplicitFeishuGroupConfig } from "./send-result-DfE5Fhz6.js";
4
4
  import { o as resolveConfiguredFeishuGroupSessionScope, t as buildFeishuConversationId } from "./conversation-id-BeJL-wq7.js";
5
- import { a as getFeishuUserAgent, i as createFeishuWSClient, n as createEventDispatcher, r as createFeishuClient, s as resolveConfiguredHttpTimeoutMs } from "./client-87BmeMpj.js";
5
+ import { a as getFeishuUserAgent, i as createFeishuWSClient, n as createEventDispatcher, r as createFeishuClient, s as resolveConfiguredHttpTimeoutMs } from "./client-Dee7cKsS.js";
6
6
  import { t as getFeishuRuntime } from "./runtime-C5JxBWZp.js";
7
- import { i as createCommentTypingReactionLifecycle, l as getChatInfo, t as deliverCommentThreadText } from "./drive-BHv8WW9i.js";
7
+ import { i as createCommentTypingReactionLifecycle, l as getChatInfo, t as deliverCommentThreadText } from "./drive-_zrcOxzS.js";
8
8
  import { t as createFeishuThreadBindingManager } from "./thread-bindings-V0bwk0A1.js";
9
9
  import { createReplyPrefixContext, evaluateSupplementalContextVisibility, getSessionEntry, normalizeAgentId as normalizeAgentId$2, resolveChannelContextVisibilityMode } from "./runtime-api.js";
10
10
  import { t as readFeishuJsonResponse } from "./json-response-CheljwGr.js";
11
- import { _ as buildFeishuMediaFallbackText, c as parsePostContent, d as isMentionForwardRequest, f as isFeishuGroupChatType, g as normalizeFeishuExternalKey, h as shouldSuppressFeishuTextForVoiceMedia, i as sendCardFeishu, l as extractMentionTargets, m as sendMediaFeishu, n as getMessageFeishu, o as sendMessageFeishu, p as saveMessageResourceFeishu, r as listFeishuThreadMessages, s as sendStructuredCardFeishu, u as isFeishuBroadcastMention, v as resolveFeishuIdentityEmoji } from "./send-DcrXbqqA.js";
12
- import { i as waitForAbortableDelay, r as raceWithTimeoutAndAbort } from "./probe-BgboTHIn.js";
13
- import { t as fetchBotIdentityForMonitor } from "./monitor.startup-Cy8NPGAX.js";
14
- import { createChannelMessageReplyPipeline, formatChannelProgressDraftLineForEntry, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelPreviewStreamMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
11
+ import { _ as buildFeishuMediaFallbackText, a as getMessageFeishu, d as parsePostContent, f as extractMentionTargets, g as normalizeFeishuExternalKey, h as isFeishuGroupChatType, l as sendMessageFeishu, m as isMentionForwardRequest, n as sendMediaFeishu, o as listFeishuThreadMessages, p as isFeishuBroadcastMention, r as shouldSuppressFeishuTextForVoiceMedia, s as sendCardFeishu, t as saveMessageResourceFeishu, u as sendStructuredCardFeishu, v as resolveFeishuIdentityEmoji } from "./media-CRJunSRr.js";
12
+ import { a as waitForAbortableDelay, i as raceWithTimeoutAndAbort } from "./probe-Za1kgUvx.js";
13
+ import { t as fetchBotIdentityForMonitor } from "./monitor.startup-CSUPT_U1.js";
14
+ import { DEFAULT_INGRESS_ADOPTION_STALL_MS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, createChannelMessageReplyPipeline, createMessageReceiptFromOutboundResults, formatChannelProgressDraftLineForEntry, isChannelProgressDraftWorkToolName, resolveAgentOutboundIdentity, resolveChannelPreviewStreamMode, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
15
15
  import { createChannelPairingController, createChannelPairingController as createChannelPairingController$1 } from "openclaw/plugin-sdk/channel-pairing";
16
16
  import { ensureConfiguredBindingRouteReady, resolveConfiguredBindingRoute, resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
17
17
  import { asDateTimestampMs, isFutureDateTimestampMs, parseStrictNonNegativeInteger, resolveDateTimestampMs, resolveExpiresAtMsFromDurationMs, resolveExpiresAtMsFromDurationSeconds, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
@@ -23,7 +23,7 @@ import os from "node:os";
23
23
  import path from "node:path";
24
24
  import { normalizeAccountId, resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
25
25
  import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
26
- import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
26
+ import { collectErrorGraphCandidates, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
27
27
  import * as Lark from "@larksuiteoapi/node-sdk";
28
28
  import { WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, applyBasicWebhookRequestGuards, createFixedWindowRateLimiter, createWebhookAnomalyTracker, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
29
29
  import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
@@ -31,7 +31,7 @@ import { getReplyPayloadTtsSupplement, resolveSendableOutboundReplyParts, resolv
31
31
  import { resolvePinnedMainDmOwnerFromAllowlist, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
32
32
  import * as crypto$1 from "node:crypto";
33
33
  import crypto, { createHash } from "node:crypto";
34
- import { buildChannelInboundEventContext, formatAgentEnvelope, formatInboundMediaUnavailableText, recordChannelBotPairLoopAndCheckSuppression, resolveEnvelopeFormatOptions, toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
34
+ import { buildChannelInboundEventContext, createChannelPartialDeliveryError, formatAgentEnvelope, formatInboundMediaUnavailableText, isChannelPartialDeliveryError, recordChannelBotPairLoopAndCheckSuppression, resolveEnvelopeFormatOptions, toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
35
35
  import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
36
36
  import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
37
37
  import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
@@ -39,6 +39,7 @@ import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe"
39
39
  import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-writes";
40
40
  import { formatReasoningMessage, resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
41
41
  import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
42
+ import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
42
43
  import * as http from "node:http";
43
44
  import { installRequestBodyLimitGuard, readWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
44
45
  import { isAbortRequestText, isBtwRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
@@ -74,6 +75,167 @@ function buildFeishuAgentBody(params) {
74
75
  }
75
76
  //#endregion
76
77
  //#region extensions/feishu/src/bot-broadcast.ts
78
+ function createFeishuBroadcastIngressSettlement(params) {
79
+ const lanes = /* @__PURE__ */ new Set();
80
+ const failures = [];
81
+ const fallbackAbortSignal = new AbortController().signal;
82
+ let fanoutSettled = false;
83
+ let terminal;
84
+ let adoption;
85
+ let abandonment;
86
+ let finalizing = false;
87
+ let deferred = false;
88
+ let replayReleased = false;
89
+ const beginFinalizing = () => {
90
+ if (finalizing) return;
91
+ finalizing = true;
92
+ params.lifecycle?.onAdoptionFinalizing();
93
+ };
94
+ const defer = () => {
95
+ if (deferred) return;
96
+ deferred = true;
97
+ params.lifecycle?.onDeferred();
98
+ };
99
+ const reportReplayCommitError = (error) => {
100
+ try {
101
+ params.onReplayCommitError?.(error);
102
+ } catch {}
103
+ };
104
+ const releaseReplayClaim = (error) => {
105
+ if (replayReleased || terminal === "adopted") return;
106
+ replayReleased = true;
107
+ params.replayClaim?.release({ error });
108
+ };
109
+ const runAbandonment = async (error) => {
110
+ if (terminal) return;
111
+ releaseReplayClaim(error);
112
+ try {
113
+ await params.lifecycle?.onAbandoned();
114
+ } finally {
115
+ terminal = "abandoned";
116
+ }
117
+ };
118
+ const abandon = async (error) => {
119
+ if (terminal) return;
120
+ if (adoption) {
121
+ await adoption.catch(() => void 0);
122
+ if (terminal) return;
123
+ }
124
+ const activeAbandonment = abandonment ?? runAbandonment(error);
125
+ abandonment = activeAbandonment;
126
+ await activeAbandonment;
127
+ };
128
+ const runAdoption = async () => {
129
+ beginFinalizing();
130
+ try {
131
+ await params.lifecycle?.onAdopted();
132
+ terminal = "adopted";
133
+ try {
134
+ params.onAdopted?.();
135
+ } catch {}
136
+ try {
137
+ await params.replayClaim?.commit();
138
+ } catch (error) {
139
+ reportReplayCommitError(error);
140
+ }
141
+ } catch (error) {
142
+ await runAbandonment(error).catch(() => void 0);
143
+ throw error;
144
+ }
145
+ };
146
+ const adopt = async () => {
147
+ if (terminal) return;
148
+ if (abandonment) {
149
+ await abandonment.catch(() => void 0);
150
+ if (terminal) return;
151
+ }
152
+ const activeAdoption = adoption ?? runAdoption();
153
+ adoption = activeAdoption;
154
+ await activeAdoption;
155
+ };
156
+ const maybeSettle = async () => {
157
+ if (!fanoutSettled || terminal) return;
158
+ if (failures.length > 0 || [...lanes].some((lane) => lane.status === "failed" || lane.status === "abandoned")) {
159
+ await abandon(failures.length === 1 ? failures[0] : new AggregateError(failures, "Feishu broadcast dispatch failed"));
160
+ return;
161
+ }
162
+ if ([...lanes].some((lane) => lane.status === "pending" || lane.status === "deferred" || lane.status === "adopted")) return;
163
+ await adopt();
164
+ };
165
+ return {
166
+ createLane: (replayClaim) => {
167
+ const lane = {
168
+ replayClaim,
169
+ status: "pending"
170
+ };
171
+ lanes.add(lane);
172
+ const releaseLane = (error) => {
173
+ lane.replayClaim?.release({ error });
174
+ };
175
+ return {
176
+ lifecycle: {
177
+ abortSignal: params.lifecycle?.abortSignal ?? fallbackAbortSignal,
178
+ onAdopted: async () => {
179
+ if (lane.status === "adopted" || lane.status === "completed" || lane.status === "failed" || lane.status === "abandoned") return;
180
+ lane.status = "adopted";
181
+ beginFinalizing();
182
+ try {
183
+ await lane.replayClaim?.commit();
184
+ } catch (error) {
185
+ reportReplayCommitError(error);
186
+ }
187
+ lane.status = "completed";
188
+ await maybeSettle();
189
+ },
190
+ onDeferred: () => {
191
+ if (lane.status !== "pending") return;
192
+ lane.status = "deferred";
193
+ defer();
194
+ },
195
+ onAdoptionFinalizing: beginFinalizing,
196
+ onAbandoned: async () => {
197
+ if (lane.status === "completed" || lane.status === "failed" || lane.status === "abandoned") return;
198
+ lane.status = "abandoned";
199
+ releaseLane(/* @__PURE__ */ new Error("feishu-broadcast-turn-abandoned"));
200
+ await maybeSettle();
201
+ }
202
+ },
203
+ onDispatchComplete: async (dispatched) => {
204
+ if (!dispatched && lane.status === "pending") {
205
+ const error = /* @__PURE__ */ new Error("feishu broadcast lane was not dispatched");
206
+ lane.status = "failed";
207
+ failures.push(error);
208
+ releaseLane(error);
209
+ return;
210
+ }
211
+ if (lane.status !== "pending") return;
212
+ const error = /* @__PURE__ */ new Error("feishu broadcast dispatch returned before turn adoption");
213
+ lane.status = "failed";
214
+ failures.push(error);
215
+ releaseLane(error);
216
+ },
217
+ onDispatchFailed: async (error) => {
218
+ failures.push(error);
219
+ if (lane.status !== "completed") {
220
+ lane.status = "failed";
221
+ releaseLane(error);
222
+ }
223
+ await maybeSettle();
224
+ }
225
+ };
226
+ },
227
+ onLanePending: defer,
228
+ onDispatchComplete: async () => {
229
+ fanoutSettled = true;
230
+ await maybeSettle();
231
+ },
232
+ onDispatchFailed: async (error) => {
233
+ failures.push(error);
234
+ fanoutSettled = true;
235
+ await maybeSettle();
236
+ }
237
+ };
238
+ }
77
239
  function resolveBroadcastAgents(cfg, peerId) {
78
240
  const broadcast = cfg.broadcast;
79
241
  if (!broadcast || typeof broadcast !== "object") return null;
@@ -139,11 +301,14 @@ function resolveFeishuGroupSession(params) {
139
301
  };
140
302
  }
141
303
  function parseMessageContent(content, messageType) {
142
- if (messageType === "post") return parsePostContent(content).textContent;
304
+ if (messageType === "post") return parsePostContent(content, {
305
+ renderMediaPlaceholders: false,
306
+ emptyTextFallback: ""
307
+ }).textContent;
143
308
  try {
144
309
  const parsed = JSON.parse(content);
145
310
  if (messageType === "text") return parsed.text || "";
146
- if (FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return formatFeishuMediaContent(parsed, messageType).body;
311
+ if (FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return formatFeishuMediaContent(parsed, messageType);
147
312
  if (messageType === "share_chat") {
148
313
  if (parsed && typeof parsed === "object") {
149
314
  const share = parsed;
@@ -156,7 +321,7 @@ function parseMessageContent(content, messageType) {
156
321
  if (messageType === "merge_forward") return "[Merged and Forwarded Message - loading...]";
157
322
  return content;
158
323
  } catch {
159
- return content;
324
+ return FEISHU_MEDIA_MESSAGE_TYPES.has(messageType) ? "" : content;
160
325
  }
161
326
  }
162
327
  const FEISHU_MEDIA_MESSAGE_TYPES = /* @__PURE__ */ new Set([
@@ -169,32 +334,8 @@ const FEISHU_MEDIA_MESSAGE_TYPES = /* @__PURE__ */ new Set([
169
334
  ]);
170
335
  function formatFeishuMediaContent(parsed, messageType) {
171
336
  const speechToText = messageType === "audio" && typeof parsed.speech_to_text === "string" ? parsed.speech_to_text.trim() : "";
172
- if (speechToText) return { body: speechToText };
173
- const placeholder = inferPlaceholder(messageType);
174
- const fileName = typeof parsed.file_name === "string" ? parsed.file_name.trim() : "";
175
- return {
176
- body: fileName ? `${placeholder} (${fileName})` : placeholder,
177
- mediaPlaceholder: placeholder,
178
- unavailableBody: fileName || void 0
179
- };
180
- }
181
- function resolveFeishuMediaFailurePresentation(content, messageType) {
182
- if (messageType === "post") return { unavailableBody: parsePostContent(content, {
183
- renderMediaPlaceholders: false,
184
- emptyTextFallback: ""
185
- }).textContent };
186
- if (!FEISHU_MEDIA_MESSAGE_TYPES.has(messageType)) return {};
187
- try {
188
- const parsed = JSON.parse(content);
189
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
190
- const presentation = formatFeishuMediaContent(parsed, messageType);
191
- return {
192
- mediaPlaceholder: presentation.mediaPlaceholder,
193
- unavailableBody: presentation.unavailableBody
194
- };
195
- } catch {
196
- return {};
197
- }
337
+ if (speechToText) return speechToText;
338
+ return "";
198
339
  }
199
340
  function formatSubMessageContent(content, contentType) {
200
341
  try {
@@ -295,15 +436,15 @@ async function resolveSavedFeishuMedia(params) {
295
436
  const contentType = params.result.contentType ?? await core.media.detectMime({ buffer: params.result.buffer });
296
437
  return await core.channel.media.saveMediaBuffer(params.result.buffer, contentType, "inbound", params.maxBytes, params.result.fileName ?? params.originalFilename);
297
438
  }
298
- function inferPlaceholder(messageType) {
439
+ function resolveFeishuMediaKind(messageType) {
299
440
  switch (messageType) {
300
- case "image": return "<media:image>";
301
- case "file": return "<media:document>";
302
- case "audio": return "<media:audio>";
441
+ case "image": return "image";
442
+ case "file": return "document";
443
+ case "audio": return "audio";
303
444
  case "video":
304
- case "media": return "<media:video>";
305
- case "sticker": return "<media:sticker>";
306
- default: return "<media:document>";
445
+ case "media": return "video";
446
+ case "sticker": return "sticker";
447
+ default: return "document";
307
448
  }
308
449
  }
309
450
  async function resolveFeishuMediaList(params) {
@@ -316,18 +457,11 @@ async function resolveFeishuMediaList(params) {
316
457
  "media",
317
458
  "sticker",
318
459
  "post"
319
- ].includes(messageType)) return {
320
- media: [],
321
- unavailableCount: 0
322
- };
460
+ ].includes(messageType)) return [];
323
461
  const out = [];
324
- let unavailableCount = 0;
325
462
  if (messageType === "post") {
326
463
  const { imageKeys, mediaKeys } = parsePostContent(content);
327
- if (imageKeys.length === 0 && mediaKeys.length === 0) return {
328
- media: [],
329
- unavailableCount: 0
330
- };
464
+ if (imageKeys.length === 0 && mediaKeys.length === 0) return [];
331
465
  if (imageKeys.length > 0) log?.(`feishu: post message contains ${imageKeys.length} embedded image(s)`);
332
466
  if (mediaKeys.length > 0) log?.(`feishu: post message contains ${mediaKeys.length} embedded media file(s)`);
333
467
  for (const imageKey of imageKeys) try {
@@ -345,11 +479,11 @@ async function resolveFeishuMediaList(params) {
345
479
  out.push({
346
480
  path: saved.path,
347
481
  contentType: saved.contentType,
348
- placeholder: "<media:image>"
482
+ kind: "image"
349
483
  });
350
484
  log?.(`feishu: downloaded embedded image ${imageKey}, saved to ${saved.path}`);
351
485
  } catch (err) {
352
- unavailableCount += 1;
486
+ out.push({ kind: "image" });
353
487
  log?.(`feishu: failed to download embedded image ${imageKey}: ${String(err)}`);
354
488
  }
355
489
  for (const media of mediaKeys) try {
@@ -369,29 +503,20 @@ async function resolveFeishuMediaList(params) {
369
503
  out.push({
370
504
  path: saved.path,
371
505
  contentType: saved.contentType,
372
- placeholder: "<media:video>"
506
+ kind: "video"
373
507
  });
374
508
  log?.(`feishu: downloaded embedded media ${media.fileKey}, saved to ${saved.path}`);
375
509
  } catch (err) {
376
- unavailableCount += 1;
510
+ out.push({ kind: "video" });
377
511
  log?.(`feishu: failed to download embedded media ${media.fileKey}: ${String(err)}`);
378
512
  }
379
- return {
380
- media: out,
381
- unavailableCount
382
- };
513
+ return out;
383
514
  }
384
515
  const mediaKeys = parseMediaKeys(content, messageType);
385
- if (!mediaKeys.imageKey && !mediaKeys.fileKey) return {
386
- media: [],
387
- unavailableCount: 1
388
- };
516
+ if (!mediaKeys.imageKey && !mediaKeys.fileKey) return [{ kind: resolveFeishuMediaKind(messageType) }];
389
517
  try {
390
518
  const fileKey = mediaKeys.fileKey || mediaKeys.imageKey;
391
- if (!fileKey) return {
392
- media: [],
393
- unavailableCount: 1
394
- };
519
+ if (!fileKey) return [{ kind: resolveFeishuMediaKind(messageType) }];
395
520
  const saved = await resolveSavedFeishuMedia({
396
521
  result: await saveMessageResourceFeishu({
397
522
  cfg,
@@ -408,17 +533,14 @@ async function resolveFeishuMediaList(params) {
408
533
  out.push({
409
534
  path: saved.path,
410
535
  contentType: saved.contentType,
411
- placeholder: inferPlaceholder(messageType)
536
+ kind: resolveFeishuMediaKind(messageType)
412
537
  });
413
538
  log?.(`feishu: downloaded ${messageType} media, saved to ${saved.path}`);
414
539
  } catch (err) {
415
- unavailableCount += 1;
540
+ out.push({ kind: resolveFeishuMediaKind(messageType) });
416
541
  log?.(`feishu: failed to download ${messageType} media: ${String(err)}`);
417
542
  }
418
- return {
419
- media: out,
420
- unavailableCount
421
- };
543
+ return out;
422
544
  }
423
545
  //#endregion
424
546
  //#region extensions/feishu/src/bot-group-name-state.ts
@@ -742,9 +864,7 @@ function dedupeOptions(namespace, log) {
742
864
  * unidentifiable event is never suppressed.
743
865
  */
744
866
  async function claimUnprocessedFeishuMessage(params) {
745
- const claim = await feishuDedupeState.guard.claim(params.messageId, dedupeOptions(params.namespace, params.log));
746
- if (claim.kind === "inflight") return { kind: "inflight" };
747
- return claim;
867
+ return await feishuDedupeState.guard.claim(params.messageId, dedupeOptions(params.namespace, params.log));
748
868
  }
749
869
  /**
750
870
  * Claims (unless the caller already holds the claim) and commits a message.
@@ -759,11 +879,6 @@ async function finalizeFeishuMessageProcessing(params) {
759
879
  if ("kind" in claim && claim.kind !== "claimed") return false;
760
880
  return await ("kind" in claim ? claim.handle : claim).commit();
761
881
  }
762
- /** Records a handled message so restart/replay cannot dispatch it again; false when already recorded. */
763
- async function recordProcessedFeishuMessage(messageId, namespace = "global", log) {
764
- const claim = await feishuDedupeState.guard.claim(messageId, dedupeOptions(namespace, log));
765
- return claim.kind === "claimed" ? await claim.handle.commit() : false;
766
- }
767
882
  /** Forgets a recorded message so a retryable synthetic event can be handled on redelivery. */
768
883
  async function forgetProcessedFeishuMessage(messageId, namespace = "global", log) {
769
884
  return await feishuDedupeState.guard.forget(messageId, dedupeOptions(namespace, log));
@@ -1021,6 +1136,47 @@ function resolveFeishuReasoningPreviewEnabled(params) {
1021
1136
  return configDefault === "stream";
1022
1137
  }
1023
1138
  //#endregion
1139
+ //#region extensions/feishu/src/reply-delivery-result.ts
1140
+ const noVisibleFeishuReplyDelivery = { visibleReplySent: false };
1141
+ function hasProviderIdentity(result) {
1142
+ return Boolean(result && (result.messageId?.trim() || result.receipt?.primaryPlatformMessageId?.trim() || result.receipt?.platformMessageIds.length));
1143
+ }
1144
+ /** Normalizes every physical Lark send behind one logical reply payload. */
1145
+ function createFeishuReplyDeliveryResult(params) {
1146
+ const results = params.visibleReplySent ? (params.results ?? []).filter(hasProviderIdentity) : [];
1147
+ const receipt = results.length > 0 ? createMessageReceiptFromOutboundResults({
1148
+ results,
1149
+ ...params.kind ? { kind: params.kind } : {}
1150
+ }) : void 0;
1151
+ if (!receipt) return {
1152
+ visibleReplySent: params.visibleReplySent,
1153
+ ...params.content === void 0 ? {} : { content: params.content }
1154
+ };
1155
+ return {
1156
+ messageIds: [...receipt.platformMessageIds],
1157
+ receipt,
1158
+ visibleReplySent: params.visibleReplySent,
1159
+ ...params.content === void 0 ? {} : { content: params.content }
1160
+ };
1161
+ }
1162
+ /** Preserves the first result's provider identity while retaining supplemental ids. */
1163
+ function mergeFeishuReplyDeliveryResults(results, content) {
1164
+ const visible = results.filter((result) => result.visibleReplySent === true);
1165
+ return createFeishuReplyDeliveryResult({
1166
+ results: visible,
1167
+ visibleReplySent: visible.length > 0,
1168
+ content: content === void 0 ? results.find((result) => result.content !== void 0)?.content : content
1169
+ });
1170
+ }
1171
+ /** Keeps a failed outcome while exposing the provider-visible subset to core lifecycle hooks. */
1172
+ function createFeishuPartialReplyDeliveryError(cause, result) {
1173
+ if (result.visibleReplySent !== true) return cause instanceof Error ? cause : new Error(formatErrorMessage(cause), { cause });
1174
+ return createChannelPartialDeliveryError(cause, {
1175
+ ...result,
1176
+ visibleReplySent: true
1177
+ });
1178
+ }
1179
+ //#endregion
1024
1180
  //#region extensions/feishu/src/reply-dispatcher-state.ts
1025
1181
  const streamingStartBackoffUntilByAccount = /* @__PURE__ */ new Map();
1026
1182
  //#endregion
@@ -1032,6 +1188,14 @@ function resolveStreamingCardSendMode(options) {
1032
1188
  }
1033
1189
  //#endregion
1034
1190
  //#region extensions/feishu/src/streaming-card.ts
1191
+ /** Provider finalization failed after a streaming card may already be visible. */
1192
+ var FeishuStreamingFinalizationError = class extends Error {
1193
+ constructor(cause, result) {
1194
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
1195
+ this.name = "FeishuStreamingFinalizationError";
1196
+ this.result = result;
1197
+ }
1198
+ };
1035
1199
  const STREAMING_UPDATE_THROTTLE_MS = 160;
1036
1200
  const STREAMING_SIGNIFICANT_DELTA_CHARS = 18;
1037
1201
  const FEISHU_STREAMING_TOKEN_DEFAULT_LIFETIME_SECONDS = 7200;
@@ -1106,23 +1270,17 @@ function truncateSummary(text, max = 50) {
1106
1270
  const clean = text.replace(/\n/g, " ").trim();
1107
1271
  return clean.length <= max ? clean : sliceUtf16Safe(clean, 0, max - 3) + "...";
1108
1272
  }
1109
- function hasNaturalStreamingBoundary(text) {
1110
- return /[\n。!?!?;;::]$/.test(text);
1111
- }
1112
1273
  function shouldPushStreamingUpdate(previousText, nextText) {
1113
- if (!previousText) return true;
1114
- if (hasNaturalStreamingBoundary(nextText)) return true;
1115
- return nextText.length - previousText.length >= STREAMING_SIGNIFICANT_DELTA_CHARS;
1274
+ return !previousText || /[\n。!?!?;;::]$/.test(nextText) || nextText.length - previousText.length >= STREAMING_SIGNIFICANT_DELTA_CHARS;
1116
1275
  }
1276
+ /** Merges cumulative or overlapping streaming snapshots without duplicating content. */
1117
1277
  function mergeStreamingText(previousText, nextText) {
1118
1278
  const previous = typeof previousText === "string" ? previousText : "";
1119
1279
  const next = typeof nextText === "string" ? nextText : "";
1120
1280
  if (!next) return previous;
1121
1281
  if (!previous || next === previous) return next;
1122
- if (next.startsWith(previous)) return next;
1123
- if (previous.startsWith(next)) return previous;
1124
- if (next.includes(previous)) return next;
1125
- if (previous.includes(next)) return previous;
1282
+ if (next.startsWith(previous) || next.includes(previous)) return next;
1283
+ if (previous.startsWith(next) || previous.includes(next)) return previous;
1126
1284
  const maxOverlap = Math.min(previous.length, next.length);
1127
1285
  for (let overlap = maxOverlap; overlap > 0; overlap -= 1) if (previous.slice(-overlap) === next.slice(0, overlap)) return `${previous}${next.slice(overlap)}`;
1128
1286
  return `${previous}${next}`;
@@ -1412,16 +1570,23 @@ var FeishuStreamingSession = class {
1412
1570
  }
1413
1571
  }).catch((e) => this.log?.(`Note update failed: ${String(e)}`));
1414
1572
  }
1415
- async close(finalText, options) {
1416
- if (!this.state || this.closed) return false;
1573
+ async closeWithResult(finalText, options) {
1574
+ if (!this.state || this.closed) return { visibleReplySent: false };
1417
1575
  this.closed = true;
1418
1576
  this.clearFlushTimer();
1419
1577
  await this.queue;
1420
1578
  const text = finalText ?? this.pendingText ?? this.state.currentText;
1421
1579
  const apiBase = resolveApiBase(this.creds.domain);
1422
1580
  let visibleContentSent = Boolean(this.state.sentText.trim());
1581
+ let finalWriteError;
1423
1582
  if ((text || finalText !== void 0) && text !== this.state.sentText) {
1424
- const sent = text.startsWith(this.state.sentText) ? await this.updateCardContent(text, (e) => this.log?.(`Final update failed: ${String(e)}`)) : await this.replaceCardContent(text, (e) => this.log?.(`Final replace failed: ${String(e)}`));
1583
+ const sent = text.startsWith(this.state.sentText) ? await this.updateCardContent(text, (e) => {
1584
+ finalWriteError = e;
1585
+ this.log?.(`Final update failed: ${String(e)}`);
1586
+ }) : await this.replaceCardContent(text, (e) => {
1587
+ finalWriteError = e;
1588
+ this.log?.(`Final replace failed: ${String(e)}`);
1589
+ });
1425
1590
  this.state.currentText = text;
1426
1591
  if (sent) {
1427
1592
  this.state.sentText = text;
@@ -1431,44 +1596,63 @@ var FeishuStreamingSession = class {
1431
1596
  if (options?.note) await this.updateNoteContent(options.note);
1432
1597
  const acceptedText = this.state.sentText;
1433
1598
  this.state.sequence += 1;
1434
- await fetchWithSsrFGuard({
1435
- url: `${apiBase}/cardkit/v1/cards/${this.state.cardId}/settings`,
1436
- init: {
1437
- method: "PATCH",
1438
- headers: {
1439
- Authorization: `Bearer ${await getToken(this.creds, {
1440
- fetchImpl: this.fetchImpl,
1441
- lookupFn: this.lookupFn
1442
- })}`,
1443
- "Content-Type": "application/json; charset=utf-8",
1444
- "User-Agent": getFeishuUserAgent()
1599
+ let closeError;
1600
+ try {
1601
+ const { response, release } = await fetchWithSsrFGuard({
1602
+ url: `${apiBase}/cardkit/v1/cards/${this.state.cardId}/settings`,
1603
+ init: {
1604
+ method: "PATCH",
1605
+ headers: {
1606
+ Authorization: `Bearer ${await getToken(this.creds, {
1607
+ fetchImpl: this.fetchImpl,
1608
+ lookupFn: this.lookupFn
1609
+ })}`,
1610
+ "Content-Type": "application/json; charset=utf-8",
1611
+ "User-Agent": getFeishuUserAgent()
1612
+ },
1613
+ body: JSON.stringify({
1614
+ settings: JSON.stringify({ config: {
1615
+ streaming_mode: false,
1616
+ summary: { content: truncateSummary(acceptedText) }
1617
+ } }),
1618
+ sequence: this.state.sequence,
1619
+ uuid: `c_${this.state.cardId}_${this.state.sequence}`
1620
+ })
1445
1621
  },
1446
- body: JSON.stringify({
1447
- settings: JSON.stringify({ config: {
1448
- streaming_mode: false,
1449
- summary: { content: truncateSummary(acceptedText) }
1450
- } }),
1451
- sequence: this.state.sequence,
1452
- uuid: `c_${this.state.cardId}_${this.state.sequence}`
1453
- })
1454
- },
1455
- fetchImpl: this.fetchImpl,
1456
- lookupFn: this.lookupFn,
1457
- policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
1458
- auditContext: "feishu.streaming-card.close",
1459
- timeoutMs: this.creds.httpTimeoutMs ?? 3e4
1460
- }).then(async ({ response, release }) => {
1622
+ fetchImpl: this.fetchImpl,
1623
+ lookupFn: this.lookupFn,
1624
+ policy: { allowedHostnames: resolveAllowedHostnames(this.creds.domain) },
1625
+ auditContext: "feishu.streaming-card.close",
1626
+ timeoutMs: this.creds.httpTimeoutMs ?? 3e4
1627
+ });
1461
1628
  try {
1462
1629
  await assertSuccessfulCardKitResponse(response, "feishu.streaming-card.close", "Close streaming card");
1463
1630
  } finally {
1464
1631
  await release();
1465
1632
  }
1466
- }).catch((e) => this.log?.(`Close failed: ${String(e)}`));
1633
+ } catch (error) {
1634
+ closeError = error;
1635
+ this.log?.(`Close failed: ${String(error)}`);
1636
+ }
1467
1637
  const finalState = this.state;
1468
1638
  this.state = null;
1469
1639
  this.pendingText = null;
1470
1640
  this.log?.(`Closed streaming: cardId=${finalState.cardId}`);
1471
- return visibleContentSent;
1641
+ const result = {
1642
+ visibleReplySent: visibleContentSent,
1643
+ ...visibleContentSent ? { content: finalState.sentText } : {},
1644
+ messageId: finalState.messageId
1645
+ };
1646
+ if (finalWriteError !== void 0 || closeError !== void 0) throw new FeishuStreamingFinalizationError(finalWriteError !== void 0 && closeError !== void 0 ? new AggregateError([finalWriteError, closeError], "Feishu streaming card finalization failed") : finalWriteError ?? closeError, result);
1647
+ return result;
1648
+ }
1649
+ async close(finalText, options) {
1650
+ try {
1651
+ return (await this.closeWithResult(finalText, options)).visibleReplySent;
1652
+ } catch (error) {
1653
+ if (error instanceof FeishuStreamingFinalizationError) return error.result.visibleReplySent;
1654
+ throw error;
1655
+ }
1472
1656
  }
1473
1657
  async discard() {
1474
1658
  if (!this.state || this.closed) return;
@@ -1614,6 +1798,9 @@ function mergeStreamingFinalText(previousText, nextText, appendError) {
1614
1798
  if (previousText.endsWith(`\n\n${nextText}`)) return previousText;
1615
1799
  return `${previousText}\n\n${nextText}`;
1616
1800
  }
1801
+ function toError(error) {
1802
+ return error instanceof Error ? error : new Error(String(error));
1803
+ }
1617
1804
  /** Maximum age (ms) for a message to receive a typing indicator reaction.
1618
1805
  * Messages older than this are likely replays after context compaction (#30418). */
1619
1806
  const TYPING_INDICATOR_MAX_AGE_MS = 2 * 6e4;
@@ -1724,9 +1911,12 @@ function createFeishuReplyDispatcher(params) {
1724
1911
  });
1725
1912
  const renderMode = account.config?.renderMode ?? "auto";
1726
1913
  const streamingEnabled = !requiredMentionTargets?.length && resolveChannelPreviewStreamMode(account.config, "partial") !== "off" && renderMode !== "raw";
1914
+ const hookRunner = getGlobalHookRunner();
1915
+ const modifyingHooksRegistered = (hookRunner?.hasHooks("reply_payload_sending") ?? false) || (hookRunner?.hasHooks("message_sending") ?? false);
1916
+ const previewStreamingEnabled = streamingEnabled && !modifyingHooksRegistered;
1727
1917
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
1728
1918
  const coreBlockStreamingEnabled = blockStreamingEnabled === true;
1729
- const reasoningPreviewEnabled = streamingEnabled && params.allowReasoningPreview === true;
1919
+ const reasoningPreviewEnabled = previewStreamingEnabled && params.allowReasoningPreview === true;
1730
1920
  let streaming = null;
1731
1921
  let streamText = "";
1732
1922
  let lastPartial = "";
@@ -1736,15 +1926,20 @@ function createFeishuReplyDispatcher(params) {
1736
1926
  let lastSnapshotTextLength = 0;
1737
1927
  let hasStreamingFinalText = false;
1738
1928
  const deliveredFinalTexts = /* @__PURE__ */ new Set();
1929
+ const closedStreamingSettlements = /* @__PURE__ */ new Map();
1739
1930
  let sentIndependentBlockText = false;
1740
1931
  let partialUpdateQueue = Promise.resolve();
1741
1932
  let streamingStartPromise = null;
1742
- let streamingClosedForReply = false;
1743
- let streamingCloseErroredForReply = false;
1933
+ let streamingGeneration = 0;
1934
+ let activeStreamingGeneration;
1935
+ let inFlightStreamingClose;
1744
1936
  let visibleReplySent = false;
1745
1937
  let skippedFinalReason = null;
1746
1938
  let idleSideEffectsPromise = Promise.resolve();
1939
+ let activeIdleSideEffectsPromise = null;
1940
+ let idleRequestedForReply = false;
1747
1941
  let replyLifecycleStateInitialized = false;
1942
+ const pendingStreamingDeliveries = [];
1748
1943
  const markVisibleReplySent = () => {
1749
1944
  visibleReplySent = true;
1750
1945
  };
@@ -1761,9 +1956,12 @@ function createFeishuReplyDispatcher(params) {
1761
1956
  return parts.join("");
1762
1957
  };
1763
1958
  const flushStreamingCardUpdate = (combined) => {
1959
+ const session = streaming;
1960
+ const generation = activeStreamingGeneration;
1961
+ const startPromise = streamingStartPromise;
1764
1962
  partialUpdateQueue = partialUpdateQueue.then(async () => {
1765
- if (streamingStartPromise) await streamingStartPromise;
1766
- if (streaming?.isActive()) await streaming.update(combined);
1963
+ if (startPromise) await startPromise;
1964
+ if (generation !== void 0 && session?.isActive()) await session.update(combined);
1767
1965
  });
1768
1966
  };
1769
1967
  const queueStreamingUpdate = (nextText, options) => {
@@ -1796,12 +1994,15 @@ function createFeishuReplyDispatcher(params) {
1796
1994
  httpTimeoutMs: resolveConfiguredHttpTimeoutMs(account)
1797
1995
  } : null;
1798
1996
  if (!creds) return;
1799
- streaming = new FeishuStreamingSession(createFeishuClient(account), creds, (message) => params.runtime.log?.(`feishu[${account.accountId}] ${message}`));
1997
+ const session = new FeishuStreamingSession(createFeishuClient(account), creds, (message) => params.runtime.log?.(`feishu[${account.accountId}] ${message}`));
1998
+ const generation = ++streamingGeneration;
1999
+ streaming = session;
2000
+ activeStreamingGeneration = generation;
1800
2001
  try {
1801
2002
  const cardHeader = resolveCardHeader(agentId, identity);
1802
2003
  const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
1803
2004
  const streamingTarget = sendTarget.replace(/^(feishu|lark):/i, "").replace(/^(chat|user|group|dm|open_id):/i, "").trim();
1804
- await streaming.start(streamingTarget, resolveReceiveIdType(sendTarget), {
2005
+ await session.start(streamingTarget, resolveReceiveIdType(sendTarget), {
1805
2006
  replyToMessageId: sendReplyToMessageId,
1806
2007
  replyInThread: effectiveReplyInThread,
1807
2008
  rootId,
@@ -1812,14 +2013,18 @@ function createFeishuReplyDispatcher(params) {
1812
2013
  } catch (error) {
1813
2014
  rememberStreamingStartFailure(account.accountId);
1814
2015
  params.runtime.error?.(`feishu[${account.accountId}]: streaming start failed; using non-streaming card fallback for ${STREAMING_START_FAILURE_BACKOFF_MS / 1e3}s: ${String(error)}`);
1815
- streaming = null;
1816
- streamingStartPromise = null;
2016
+ if (streaming === session) {
2017
+ streaming = null;
2018
+ streamingStartPromise = null;
2019
+ activeStreamingGeneration = void 0;
2020
+ }
1817
2021
  }
1818
2022
  })();
1819
2023
  };
1820
2024
  const resetStreamingState = () => {
1821
2025
  streaming = null;
1822
2026
  streamingStartPromise = null;
2027
+ activeStreamingGeneration = void 0;
1823
2028
  partialUpdateQueue = Promise.resolve();
1824
2029
  streamText = "";
1825
2030
  lastPartial = "";
@@ -1829,24 +2034,126 @@ function createFeishuReplyDispatcher(params) {
1829
2034
  lastSnapshotTextLength = 0;
1830
2035
  hasStreamingFinalText = false;
1831
2036
  };
1832
- const closeStreaming = async (options) => {
2037
+ const rememberClosedStreamingSettlement = (generation, content, result, error) => {
2038
+ if (generation === void 0 || !content) return;
2039
+ closedStreamingSettlements.set(generation, {
2040
+ result,
2041
+ content,
2042
+ ...error === void 0 ? {} : { error }
2043
+ });
2044
+ };
2045
+ const performStreamingClose = async () => {
2046
+ const streamingToClose = streaming;
2047
+ const generationToClose = activeStreamingGeneration;
2048
+ const startPromiseToClose = streamingStartPromise;
2049
+ const updateQueueToClose = partialUpdateQueue;
2050
+ const finalizedAnswerText = streamText;
2051
+ const finalizedReasoningText = reasoningText;
2052
+ if (generationToClose !== void 0 && activeStreamingGeneration === generationToClose) activeStreamingGeneration = void 0;
1833
2053
  try {
1834
- if (streamingStartPromise) await streamingStartPromise;
1835
- await partialUpdateQueue;
1836
- if (streaming?.isActive()) {
2054
+ if (startPromiseToClose) await startPromiseToClose;
2055
+ await updateQueueToClose;
2056
+ if (streamingToClose?.isActive()) {
1837
2057
  statusLine = "";
1838
- const text = buildCombinedStreamText(reasoningText, streamText);
2058
+ const text = buildCombinedStreamText(finalizedReasoningText, finalizedAnswerText);
1839
2059
  const finalNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
1840
- const contentVisible = await streaming.close(text, { note: finalNote });
1841
- if (contentVisible) markVisibleReplySent();
1842
- if (contentVisible && streamText) {
1843
- deliveredFinalTexts.add(streamText);
1844
- if (options?.markClosedForReply !== false && !streamingCloseErroredForReply) streamingClosedForReply = true;
2060
+ let closed;
2061
+ try {
2062
+ closed = await streamingToClose.closeWithResult(text, { note: finalNote });
2063
+ } catch (error) {
2064
+ if (!(error instanceof FeishuStreamingFinalizationError)) throw error;
2065
+ const failedResult = createFeishuReplyDeliveryResult({
2066
+ results: [error.result],
2067
+ visibleReplySent: error.result.visibleReplySent,
2068
+ content: error.result.content,
2069
+ kind: "card"
2070
+ });
2071
+ if (failedResult.visibleReplySent) markVisibleReplySent();
2072
+ if (failedResult.visibleReplySent && finalizedAnswerText) {
2073
+ const partialError = createFeishuPartialReplyDeliveryError(error.cause ?? error, failedResult);
2074
+ if (failedResult.content === text) deliveredFinalTexts.add(finalizedAnswerText);
2075
+ rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, failedResult, partialError);
2076
+ return {
2077
+ result: failedResult,
2078
+ ...generationToClose === void 0 ? {} : { generation: generationToClose },
2079
+ error: partialError
2080
+ };
2081
+ }
2082
+ rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, failedResult, error);
2083
+ return {
2084
+ result: failedResult,
2085
+ ...generationToClose === void 0 ? {} : { generation: generationToClose },
2086
+ error
2087
+ };
2088
+ }
2089
+ const result = createFeishuReplyDeliveryResult({
2090
+ results: [closed],
2091
+ visibleReplySent: closed.visibleReplySent,
2092
+ content: closed.content,
2093
+ kind: "card"
2094
+ });
2095
+ if (result.visibleReplySent) markVisibleReplySent();
2096
+ if (result.visibleReplySent && finalizedAnswerText) {
2097
+ deliveredFinalTexts.add(finalizedAnswerText);
2098
+ rememberClosedStreamingSettlement(generationToClose, finalizedAnswerText, result);
1845
2099
  }
2100
+ return {
2101
+ result,
2102
+ ...generationToClose === void 0 ? {} : { generation: generationToClose }
2103
+ };
1846
2104
  }
2105
+ return {
2106
+ result: noVisibleFeishuReplyDelivery,
2107
+ ...generationToClose === void 0 ? {} : { generation: generationToClose }
2108
+ };
2109
+ } catch (error) {
2110
+ return {
2111
+ result: noVisibleFeishuReplyDelivery,
2112
+ ...generationToClose === void 0 ? {} : { generation: generationToClose },
2113
+ error
2114
+ };
1847
2115
  } finally {
1848
- resetStreamingState();
2116
+ if (streaming === streamingToClose) resetStreamingState();
2117
+ }
2118
+ };
2119
+ const closeStreaming = () => {
2120
+ const generation = activeStreamingGeneration;
2121
+ if (generation !== void 0 && inFlightStreamingClose?.generation === generation) return inFlightStreamingClose.promise;
2122
+ const content = streamText;
2123
+ const closePromise = performStreamingClose();
2124
+ if (generation !== void 0) {
2125
+ const closing = {
2126
+ generation,
2127
+ content,
2128
+ promise: closePromise
2129
+ };
2130
+ inFlightStreamingClose = closing;
2131
+ const clear = () => {
2132
+ if (inFlightStreamingClose === closing) inFlightStreamingClose = void 0;
2133
+ };
2134
+ closePromise.then(clear, clear);
1849
2135
  }
2136
+ return closePromise;
2137
+ };
2138
+ const deferStreamingDelivery = (result, infoKind, ownerGeneration) => {
2139
+ let resolveFinalization;
2140
+ let rejectFinalization;
2141
+ const finalization = new Promise((resolve, reject) => {
2142
+ resolveFinalization = resolve;
2143
+ rejectFinalization = reject;
2144
+ });
2145
+ pendingStreamingDeliveries.push({
2146
+ result,
2147
+ ...infoKind ? { infoKind } : {},
2148
+ ...ownerGeneration === void 0 ? {} : { streamingGeneration: ownerGeneration },
2149
+ resolve: resolveFinalization,
2150
+ reject: rejectFinalization
2151
+ });
2152
+ if (idleRequestedForReply) queueIdleSideEffects().catch((error) => params.runtime.error?.(`feishu[${account.accountId}] late reply finalization failed: ${String(error)}`));
2153
+ return {
2154
+ ...noVisibleFeishuReplyDelivery,
2155
+ finalization
2156
+ };
1850
2157
  };
1851
2158
  const discardStreamingPreview = async () => {
1852
2159
  try {
@@ -1874,82 +2181,98 @@ function createFeishuReplyDispatcher(params) {
1874
2181
  chunkMentions: paramsLocal.chunkMentions,
1875
2182
  initialChunks
1876
2183
  }));
2184
+ const results = [];
2185
+ const acceptedChunks = [];
1877
2186
  for (const [index, chunk] of chunks.entries()) {
1878
2187
  const mentions = [...paramsLocal.chunkMentions ?? [], ...index === 0 ? paramsLocal.firstChunkMentions ?? [] : []];
1879
- await paramsLocal.sendChunk({
1880
- chunk,
1881
- isFirst: index === 0,
1882
- mentions: mentions.length > 0 ? mentions : void 0
1883
- });
1884
- markVisibleReplySent();
2188
+ try {
2189
+ const result = await paramsLocal.sendChunk({
2190
+ chunk,
2191
+ isFirst: index === 0,
2192
+ mentions: mentions.length > 0 ? mentions : void 0
2193
+ });
2194
+ results.push(result);
2195
+ acceptedChunks.push(chunk);
2196
+ markVisibleReplySent();
2197
+ } catch (error) {
2198
+ throw createFeishuPartialReplyDeliveryError(error, createFeishuReplyDeliveryResult({
2199
+ results,
2200
+ visibleReplySent: results.length > 0,
2201
+ content: acceptedChunks.join(""),
2202
+ kind: paramsLocal.useCard ? "card" : "text"
2203
+ }));
2204
+ }
1885
2205
  }
1886
2206
  if (paramsLocal.infoKind === "final") deliveredFinalTexts.add(paramsLocal.text);
2207
+ return createFeishuReplyDeliveryResult({
2208
+ results,
2209
+ visibleReplySent: results.length > 0,
2210
+ content: paramsLocal.text,
2211
+ kind: paramsLocal.useCard ? "card" : "text"
2212
+ });
1887
2213
  };
1888
2214
  const sendMediaReplies = async (payload, options) => {
1889
2215
  const mediaUrls = resolveSendableOutboundReplyParts(payload).mediaUrls;
1890
2216
  let sentFallbackText = false;
1891
- await sendMediaWithLeadingCaption({
1892
- mediaUrls,
1893
- caption: "",
1894
- send: async ({ mediaUrl }) => {
1895
- const result = await sendMediaFeishu({
1896
- cfg,
1897
- to: sendTarget,
1898
- mediaUrl,
1899
- replyToMessageId: sendReplyToMessageId,
1900
- replyInThread: effectiveReplyInThread,
1901
- accountId,
1902
- ...payload.audioAsVoice === true ? { audioAsVoice: true } : {}
1903
- });
1904
- markVisibleReplySent();
1905
- if (result?.voiceIntentDegradedToFile && options?.fallbackText && !sentFallbackText) {
1906
- sentFallbackText = true;
1907
- await sendChunkedTextReply({
1908
- text: options.fallbackText,
1909
- useCard: false,
1910
- infoKind: "final",
1911
- chunkMentions: requiredMentionTargets,
1912
- sendChunk: async ({ chunk, mentions }) => {
1913
- await sendMessageFeishu({
1914
- cfg,
1915
- to: sendTarget,
1916
- text: chunk,
1917
- replyToMessageId: sendReplyToMessageId,
1918
- replyInThread: effectiveReplyInThread,
1919
- allowTopLevelReplyFallback,
1920
- accountId,
1921
- ...mentions ? { mentions } : {}
1922
- });
1923
- }
2217
+ let degradedVoiceFallbackText;
2218
+ const results = [];
2219
+ const sendFallbackText = async (text) => await sendChunkedTextReply({
2220
+ text,
2221
+ useCard: false,
2222
+ infoKind: "final",
2223
+ chunkMentions: requiredMentionTargets,
2224
+ sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
2225
+ cfg,
2226
+ to: sendTarget,
2227
+ text: chunk,
2228
+ replyToMessageId: sendReplyToMessageId,
2229
+ replyInThread: effectiveReplyInThread,
2230
+ allowTopLevelReplyFallback,
2231
+ accountId,
2232
+ ...mentions ? { mentions } : {}
2233
+ })
2234
+ });
2235
+ try {
2236
+ await sendMediaWithLeadingCaption({
2237
+ mediaUrls,
2238
+ caption: "",
2239
+ send: async ({ mediaUrl }) => {
2240
+ const result = await sendMediaFeishu({
2241
+ cfg,
2242
+ to: sendTarget,
2243
+ mediaUrl,
2244
+ replyToMessageId: sendReplyToMessageId,
2245
+ replyInThread: effectiveReplyInThread,
2246
+ allowTopLevelReplyFallback,
2247
+ accountId,
2248
+ ...payload.audioAsVoice === true ? { audioAsVoice: true } : {}
2249
+ });
2250
+ results.push(createFeishuReplyDeliveryResult({
2251
+ results: [result],
2252
+ visibleReplySent: true,
2253
+ kind: result?.voiceIntentDegradedToFile ? "media" : void 0
2254
+ }));
2255
+ markVisibleReplySent();
2256
+ if (result?.voiceIntentDegradedToFile && options?.fallbackText && !sentFallbackText) degradedVoiceFallbackText = options.fallbackText;
2257
+ },
2258
+ onError: options?.fallbackText === void 0 ? void 0 : async ({ mediaUrl }) => {
2259
+ const fallbackText = await buildFeishuMediaFallbackText({
2260
+ text: sentFallbackText ? void 0 : options.fallbackText,
2261
+ mediaUrl
1924
2262
  });
2263
+ sentFallbackText = true;
2264
+ results.push(await sendFallbackText(fallbackText));
1925
2265
  }
1926
- },
1927
- onError: options?.fallbackText === void 0 ? void 0 : async ({ mediaUrl }) => {
1928
- const fallbackText = await buildFeishuMediaFallbackText({
1929
- text: sentFallbackText ? void 0 : options.fallbackText,
1930
- mediaUrl
1931
- });
2266
+ });
2267
+ if (degradedVoiceFallbackText && !sentFallbackText) {
1932
2268
  sentFallbackText = true;
1933
- await sendChunkedTextReply({
1934
- text: fallbackText,
1935
- useCard: false,
1936
- infoKind: "final",
1937
- chunkMentions: requiredMentionTargets,
1938
- sendChunk: async ({ chunk, mentions }) => {
1939
- await sendMessageFeishu({
1940
- cfg,
1941
- to: sendTarget,
1942
- text: chunk,
1943
- replyToMessageId: sendReplyToMessageId,
1944
- replyInThread: effectiveReplyInThread,
1945
- allowTopLevelReplyFallback,
1946
- accountId,
1947
- ...mentions ? { mentions } : {}
1948
- });
1949
- }
1950
- });
2269
+ results.push(await sendFallbackText(degradedVoiceFallbackText));
1951
2270
  }
1952
- });
2271
+ } catch (error) {
2272
+ const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
2273
+ throw createFeishuPartialReplyDeliveryError(error, mergeFeishuReplyDeliveryResults([...results, ...partial ? [partial] : []]));
2274
+ }
2275
+ return mergeFeishuReplyDeliveryResults(results);
1953
2276
  };
1954
2277
  const ensureNoVisibleReplyFallback = async (reason) => {
1955
2278
  await idleSideEffectsPromise;
@@ -1972,13 +2295,130 @@ function createFeishuReplyDispatcher(params) {
1972
2295
  params.runtime.error?.(`feishu[${account.accountId}]: sent no-visible-reply fallback (${reason})`);
1973
2296
  return true;
1974
2297
  };
1975
- const queueIdleSideEffects = (options) => {
2298
+ const claimClosedStreamingResult = (generation, content) => {
2299
+ if (generation !== void 0) {
2300
+ const settlement = closedStreamingSettlements.get(generation);
2301
+ if (settlement) settlement.contentClaimed = true;
2302
+ return settlement;
2303
+ }
2304
+ let latestKey;
2305
+ for (const [key, settlement] of closedStreamingSettlements) if (settlement.contentClaimed !== true && (content === void 0 || settlement.content === content)) latestKey = key;
2306
+ if (latestKey === void 0) return;
2307
+ const result = closedStreamingSettlements.get(latestKey);
2308
+ if (result) result.contentClaimed = true;
2309
+ return result;
2310
+ };
2311
+ const markClosedStreamingContentClaimed = (generation) => {
2312
+ if (generation !== void 0) {
2313
+ const settlement = closedStreamingSettlements.get(generation);
2314
+ if (settlement) settlement.contentClaimed = true;
2315
+ }
2316
+ };
2317
+ const ensureVisibleStreamingDelivery = async (result, content, infoKind) => {
2318
+ if (result?.visibleReplySent === true || !content?.trim()) return result;
2319
+ const cardHeader = resolveCardHeader(agentId, identity);
2320
+ const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
2321
+ return await sendChunkedTextReply({
2322
+ text: content,
2323
+ useCard: true,
2324
+ infoKind,
2325
+ chunkMentions: requiredMentionTargets,
2326
+ sendChunk: async ({ chunk, mentions }) => await sendStructuredCardFeishu({
2327
+ cfg,
2328
+ to: sendTarget,
2329
+ text: chunk,
2330
+ replyToMessageId: sendReplyToMessageId,
2331
+ replyInThread: effectiveReplyInThread,
2332
+ allowTopLevelReplyFallback,
2333
+ accountId,
2334
+ header: cardHeader,
2335
+ note: cardNote,
2336
+ ...mentions ? { mentions } : {}
2337
+ })
2338
+ });
2339
+ };
2340
+ function queueIdleSideEffects() {
2341
+ idleRequestedForReply = true;
2342
+ if (activeIdleSideEffectsPromise) return activeIdleSideEffectsPromise;
1976
2343
  const nextIdleSideEffects = idleSideEffectsPromise.then(async () => {
1977
- await closeStreaming(options);
1978
- typingCallbacks?.onIdle?.();
2344
+ try {
2345
+ do {
2346
+ const completions = pendingStreamingDeliveries.splice(0);
2347
+ const closeOutcome = await closeStreaming();
2348
+ const finalized = closeOutcome.result;
2349
+ const ownsCurrentClose = (completion) => closeOutcome.generation !== void 0 && completion.streamingGeneration === closeOutcome.generation;
2350
+ if (completions.some((completion) => ownsCurrentClose(completion))) markClosedStreamingContentClaimed(closeOutcome.generation);
2351
+ for (const completion of completions) {
2352
+ const claimedSettlement = ownsCurrentClose(completion) ? {
2353
+ result: finalized,
2354
+ ...closeOutcome.error === void 0 ? {} : { error: closeOutcome.error }
2355
+ } : claimClosedStreamingResult(completion.streamingGeneration, completion.result.content);
2356
+ const deliveryError = claimedSettlement?.error;
2357
+ let providerFinalized = claimedSettlement?.result;
2358
+ try {
2359
+ providerFinalized = await ensureVisibleStreamingDelivery(providerFinalized, completion.result.content, completion.infoKind);
2360
+ } catch (fallbackError) {
2361
+ const fallbackPartial = isChannelPartialDeliveryError(fallbackError) ? fallbackError.deliveryResult : void 0;
2362
+ const fallbackCause = fallbackPartial && fallbackError instanceof Error ? fallbackError.cause ?? fallbackError : fallbackError;
2363
+ completion.reject(createFeishuPartialReplyDeliveryError(deliveryError === void 0 ? fallbackCause : new AggregateError([deliveryError, fallbackCause], "Feishu streaming finalization and static fallback failed"), mergeFeishuReplyDeliveryResults([
2364
+ ...providerFinalized ? [providerFinalized] : [],
2365
+ ...fallbackPartial ? [fallbackPartial] : [],
2366
+ completion.result
2367
+ ], fallbackPartial?.content ?? providerFinalized?.content ?? completion.result.content)));
2368
+ continue;
2369
+ }
2370
+ const settledResult = mergeFeishuReplyDeliveryResults([...providerFinalized ? [providerFinalized] : [], completion.result], deliveryError === void 0 ? completion.result.content ?? providerFinalized?.content : providerFinalized?.content ?? completion.result.content);
2371
+ if (deliveryError !== void 0) completion.reject(createFeishuPartialReplyDeliveryError(isChannelPartialDeliveryError(deliveryError) && deliveryError instanceof Error ? deliveryError.cause ?? deliveryError : deliveryError instanceof FeishuStreamingFinalizationError ? deliveryError.cause ?? deliveryError : deliveryError, settledResult));
2372
+ else completion.resolve(settledResult);
2373
+ }
2374
+ if (closeOutcome.error !== void 0) throw toError(closeOutcome.error);
2375
+ } while (pendingStreamingDeliveries.length > 0);
2376
+ } finally {
2377
+ typingCallbacks?.onIdle?.();
2378
+ }
1979
2379
  });
2380
+ activeIdleSideEffectsPromise = nextIdleSideEffects;
1980
2381
  idleSideEffectsPromise = nextIdleSideEffects.catch(() => {});
2382
+ const finishIdleSideEffects = () => {
2383
+ if (activeIdleSideEffectsPromise === nextIdleSideEffects) activeIdleSideEffectsPromise = null;
2384
+ if (pendingStreamingDeliveries.length > 0) queueIdleSideEffects().catch((error) => params.runtime.error?.(`feishu[${account.accountId}] queued reply finalization failed: ${String(error)}`));
2385
+ };
2386
+ nextIdleSideEffects.then(finishIdleSideEffects, finishIdleSideEffects);
1981
2387
  return nextIdleSideEffects;
2388
+ }
2389
+ const throwStreamingDeliveryFailure = async (paramsLocal) => {
2390
+ let finalized = noVisibleFeishuReplyDelivery;
2391
+ let finalizationError;
2392
+ let fallbackPartial;
2393
+ const claimedSettlement = claimClosedStreamingResult(paramsLocal.ownerGeneration, paramsLocal.content);
2394
+ if (claimedSettlement) {
2395
+ finalized = claimedSettlement.result;
2396
+ finalizationError = claimedSettlement.error;
2397
+ } else if (paramsLocal.ownerGeneration !== void 0 && inFlightStreamingClose?.generation === paramsLocal.ownerGeneration) {
2398
+ const closeOutcome = await inFlightStreamingClose.promise;
2399
+ const completedSettlement = claimClosedStreamingResult(paramsLocal.ownerGeneration, paramsLocal.content);
2400
+ finalized = completedSettlement?.result ?? closeOutcome?.result ?? finalized;
2401
+ finalizationError = completedSettlement?.error ?? closeOutcome?.error;
2402
+ } else if (paramsLocal.ownerGeneration !== void 0 && activeStreamingGeneration === paramsLocal.ownerGeneration) {
2403
+ const closeOutcome = await closeStreaming();
2404
+ finalized = closeOutcome.result;
2405
+ finalizationError = closeOutcome.error;
2406
+ }
2407
+ try {
2408
+ finalized = await ensureVisibleStreamingDelivery(finalized, paramsLocal.content, paramsLocal.infoKind) ?? finalized;
2409
+ } catch (fallbackError) {
2410
+ fallbackPartial = isChannelPartialDeliveryError(fallbackError) ? fallbackError.deliveryResult : void 0;
2411
+ const fallbackCause = fallbackPartial && fallbackError instanceof Error ? fallbackError.cause ?? fallbackError : fallbackError;
2412
+ finalizationError = finalizationError ? new AggregateError([finalizationError, fallbackCause], "Feishu streaming finalization and static fallback failed") : fallbackCause;
2413
+ }
2414
+ const mediaPartial = isChannelPartialDeliveryError(paramsLocal.error) ? paramsLocal.error.deliveryResult : void 0;
2415
+ const accepted = mergeFeishuReplyDeliveryResults([
2416
+ finalized,
2417
+ ...fallbackPartial ? [fallbackPartial] : [],
2418
+ ...mediaPartial ? [mediaPartial] : []
2419
+ ], fallbackPartial?.visibleReplySent === true ? fallbackPartial.content : finalized.visibleReplySent === true ? finalized.content : paramsLocal.content);
2420
+ const mediaCause = mediaPartial && paramsLocal.error instanceof Error ? paramsLocal.error.cause ?? paramsLocal.error : paramsLocal.error;
2421
+ throw createFeishuPartialReplyDeliveryError(finalizationError ? new AggregateError([mediaCause, finalizationError instanceof Error ? finalizationError.cause ?? finalizationError : finalizationError], "Feishu reply delivery and streaming finalization failed") : mediaCause, accepted);
1982
2422
  };
1983
2423
  const dispatcherOptions = {
1984
2424
  responsePrefix: prefixContext.responsePrefix,
@@ -1997,13 +2437,13 @@ function createFeishuReplyDispatcher(params) {
1997
2437
  if (!replyLifecycleStateInitialized) {
1998
2438
  replyLifecycleStateInitialized = true;
1999
2439
  deliveredFinalTexts.clear();
2440
+ closedStreamingSettlements.clear();
2000
2441
  sentIndependentBlockText = false;
2001
- streamingClosedForReply = false;
2002
- streamingCloseErroredForReply = false;
2442
+ idleRequestedForReply = false;
2003
2443
  visibleReplySent = false;
2004
2444
  skippedFinalReason = null;
2005
2445
  }
2006
- if (streamingEnabled && renderMode === "card") startStreaming();
2446
+ if (previewStreamingEnabled && renderMode === "card") startStreaming();
2007
2447
  await Promise.resolve(typingCallbacks?.onReplyStart?.());
2008
2448
  },
2009
2449
  onIdle: () => queueIdleSideEffects(),
@@ -2012,14 +2452,13 @@ function createFeishuReplyDispatcher(params) {
2012
2452
  }
2013
2453
  };
2014
2454
  const handleDeliveryError = async (error, info) => {
2015
- streamingCloseErroredForReply = true;
2016
- streamingClosedForReply = false;
2017
2455
  params.runtime.error?.(`feishu[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
2018
- await queueIdleSideEffects({ markClosedForReply: false }).catch((cleanupError) => params.runtime.error?.(`feishu[${account.accountId}] reply error cleanup failed: ${String(cleanupError)}`));
2456
+ await queueIdleSideEffects().catch((cleanupError) => params.runtime.error?.(`feishu[${account.accountId}] reply error cleanup failed: ${String(cleanupError)}`));
2019
2457
  };
2020
2458
  return {
2021
2459
  dispatcherOptions,
2022
2460
  delivery: {
2461
+ observeMessageSent: true,
2023
2462
  deliver: async (payload, info) => {
2024
2463
  if (info?.kind === "final") skippedFinalReason = null;
2025
2464
  const payloadText = payload.isReasoning && payload.text ? formatReasoningMessage(payload.text) : payload.text;
@@ -2040,42 +2479,52 @@ function createFeishuReplyDispatcher(params) {
2040
2479
  const finalTextExceedsStreamingLimit = info?.kind === "final" && hasText && text.length > textChunkLimit;
2041
2480
  const useStaticCard = hasText && (renderMode === "card" || info?.kind === "block" && coreBlockStreamingEnabled && renderMode !== "raw" || renderMode === "auto" && shouldUseCard(text));
2042
2481
  const useStreamingCard = hasText && streamingEnabled && !finalTextExceedsStreamingLimit && (info?.kind === "final" || useStaticCard);
2043
- const finalTextWouldUseStreamingCard = info?.kind === "final" && hasText && streamingEnabled;
2044
2482
  const useCard = useStaticCard || useStreamingCard;
2045
2483
  const skipTextForDuplicateFinal = info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
2046
- const skipTextForClosedStreamingFinal = info?.kind === "final" && hasText && streamingClosedForReply && !streamingCloseErroredForReply && finalTextWouldUseStreamingCard;
2047
- const shouldDeliverText = hasText && !hasVoiceMedia && !skipTextForDuplicateFinal && !skipTextForClosedStreamingFinal;
2484
+ const shouldDeliverText = hasText && !hasVoiceMedia && !skipTextForDuplicateFinal;
2048
2485
  const shouldDiscardStreamingPreview = info?.kind === "final" && (finalTextExceedsStreamingLimit || hasMedia && (hasVoiceMedia && !shouldDeliverText && !ttsTextAlreadyVisible || skipTextForDuplicateFinal));
2049
- if (!shouldDeliverText && !hasMedia) return;
2486
+ const priorClosedStreamingSettlement = info?.kind === "final" && hasText && skipTextForDuplicateFinal ? claimClosedStreamingResult(void 0, text) : void 0;
2487
+ if (!shouldDeliverText && !hasMedia) {
2488
+ if (priorClosedStreamingSettlement?.error !== void 0) throw toError(priorClosedStreamingSettlement.error);
2489
+ return priorClosedStreamingSettlement?.result ?? noVisibleFeishuReplyDelivery;
2490
+ }
2491
+ const deliveredResults = priorClosedStreamingSettlement ? [priorClosedStreamingSettlement.result] : [];
2492
+ const collectMediaDelivery = async (mediaPayload, mediaOptions) => {
2493
+ try {
2494
+ deliveredResults.push(await sendMediaReplies(mediaPayload, mediaOptions));
2495
+ } catch (error) {
2496
+ const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
2497
+ const accumulated = mergeFeishuReplyDeliveryResults([...deliveredResults, ...partial ? [partial] : []]);
2498
+ throw createFeishuPartialReplyDeliveryError(partial && error instanceof Error ? error.cause ?? error : error, accumulated);
2499
+ }
2500
+ };
2050
2501
  if (shouldDiscardStreamingPreview) await discardStreamingPreview();
2051
2502
  if (shouldDeliverText) {
2052
2503
  if (info?.kind === "block") {
2053
2504
  if (!useStreamingCard) {
2054
2505
  if (coreBlockStreamingEnabled) {
2055
2506
  const firstChunkMentions = !sentIndependentBlockText && mentionTargets?.length ? mentionTargets : void 0;
2056
- await sendChunkedTextReply({
2507
+ deliveredResults.push(await sendChunkedTextReply({
2057
2508
  text,
2058
2509
  useCard: false,
2059
2510
  infoKind: "block",
2060
2511
  firstChunkMentions,
2061
2512
  chunkMentions: requiredMentionTargets,
2062
- sendChunk: async ({ chunk, mentions }) => {
2063
- await sendMessageFeishu({
2064
- cfg,
2065
- to: sendTarget,
2066
- text: chunk,
2067
- replyToMessageId: sendReplyToMessageId,
2068
- replyInThread: effectiveReplyInThread,
2069
- allowTopLevelReplyFallback,
2070
- accountId,
2071
- ...mentions ? { mentions } : {}
2072
- });
2073
- }
2074
- });
2513
+ sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
2514
+ cfg,
2515
+ to: sendTarget,
2516
+ text: chunk,
2517
+ replyToMessageId: sendReplyToMessageId,
2518
+ replyInThread: effectiveReplyInThread,
2519
+ allowTopLevelReplyFallback,
2520
+ accountId,
2521
+ ...mentions ? { mentions } : {}
2522
+ })
2523
+ }));
2075
2524
  sentIndependentBlockText = true;
2076
- if (hasMedia) await sendMediaReplies(payload);
2525
+ if (hasMedia) await collectMediaDelivery(payload);
2077
2526
  }
2078
- return;
2527
+ return mergeFeishuReplyDeliveryResults(deliveredResults, text);
2079
2528
  }
2080
2529
  startStreaming();
2081
2530
  if (streamingStartPromise) await streamingStartPromise;
@@ -2085,75 +2534,87 @@ function createFeishuReplyDispatcher(params) {
2085
2534
  if (streamingStartPromise) await streamingStartPromise;
2086
2535
  }
2087
2536
  const shouldStreamText = info?.kind === "block" || info?.kind === "final";
2088
- if (streaming?.isActive() && shouldStreamText) {
2089
- if (info?.kind === "block") queueStreamingUpdate(text, {
2090
- mode: "delta",
2091
- dedupeWithLastPartial: true
2092
- });
2093
- if (info?.kind === "final") {
2094
- streamText = text;
2095
- hasStreamingFinalText = true;
2096
- snapshotBaseText = "";
2097
- lastSnapshotTextLength = text.length;
2098
- flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
2537
+ const matchingInFlightClose = info?.kind === "final" && inFlightStreamingClose?.content === text ? inFlightStreamingClose : void 0;
2538
+ const ownerGeneration = activeStreamingGeneration ?? matchingInFlightClose?.generation;
2539
+ if (shouldStreamText && ownerGeneration !== void 0 && (streaming?.isActive() || matchingInFlightClose !== void 0)) {
2540
+ if (activeStreamingGeneration !== void 0) {
2541
+ if (info?.kind === "block") queueStreamingUpdate(text, {
2542
+ mode: "delta",
2543
+ dedupeWithLastPartial: true
2544
+ });
2545
+ if (info?.kind === "final") {
2546
+ streamText = text;
2547
+ hasStreamingFinalText = true;
2548
+ snapshotBaseText = "";
2549
+ lastSnapshotTextLength = text.length;
2550
+ flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
2551
+ }
2099
2552
  }
2100
- if (hasMedia) await sendMediaReplies(payload);
2101
- return;
2553
+ if (hasMedia) try {
2554
+ await collectMediaDelivery(payload);
2555
+ } catch (error) {
2556
+ await throwStreamingDeliveryFailure({
2557
+ error,
2558
+ content: text,
2559
+ infoKind: info?.kind,
2560
+ ownerGeneration
2561
+ });
2562
+ }
2563
+ return deferStreamingDelivery(mergeFeishuReplyDeliveryResults(deliveredResults, text), info?.kind, ownerGeneration);
2102
2564
  }
2103
2565
  if (useCard) {
2104
2566
  const cardHeader = resolveCardHeader(agentId, identity);
2105
2567
  const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
2106
- await sendChunkedTextReply({
2568
+ deliveredResults.push(await sendChunkedTextReply({
2107
2569
  text,
2108
2570
  useCard: true,
2109
2571
  infoKind: info?.kind,
2110
2572
  chunkMentions: requiredMentionTargets,
2111
- sendChunk: async ({ chunk, mentions }) => {
2112
- await sendStructuredCardFeishu({
2113
- cfg,
2114
- to: sendTarget,
2115
- text: chunk,
2116
- replyToMessageId: sendReplyToMessageId,
2117
- replyInThread: effectiveReplyInThread,
2118
- allowTopLevelReplyFallback,
2119
- accountId,
2120
- header: cardHeader,
2121
- note: cardNote,
2122
- ...mentions ? { mentions } : {}
2123
- });
2124
- }
2125
- });
2573
+ sendChunk: async ({ chunk, mentions }) => await sendStructuredCardFeishu({
2574
+ cfg,
2575
+ to: sendTarget,
2576
+ text: chunk,
2577
+ replyToMessageId: sendReplyToMessageId,
2578
+ replyInThread: effectiveReplyInThread,
2579
+ allowTopLevelReplyFallback,
2580
+ accountId,
2581
+ header: cardHeader,
2582
+ note: cardNote,
2583
+ ...mentions ? { mentions } : {}
2584
+ })
2585
+ }));
2126
2586
  } else {
2127
2587
  const firstChunkMentions = info?.kind === "final" && mentionTargets?.length ? mentionTargets : void 0;
2128
- await sendChunkedTextReply({
2588
+ deliveredResults.push(await sendChunkedTextReply({
2129
2589
  text,
2130
2590
  useCard: false,
2131
2591
  infoKind: info?.kind,
2132
2592
  firstChunkMentions,
2133
2593
  chunkMentions: requiredMentionTargets,
2134
- sendChunk: async ({ chunk, mentions }) => {
2135
- await sendMessageFeishu({
2136
- cfg,
2137
- to: sendTarget,
2138
- text: chunk,
2139
- replyToMessageId: sendReplyToMessageId,
2140
- replyInThread: effectiveReplyInThread,
2141
- allowTopLevelReplyFallback,
2142
- accountId,
2143
- ...mentions ? { mentions } : {}
2144
- });
2145
- }
2146
- });
2594
+ sendChunk: async ({ chunk, mentions }) => await sendMessageFeishu({
2595
+ cfg,
2596
+ to: sendTarget,
2597
+ text: chunk,
2598
+ replyToMessageId: sendReplyToMessageId,
2599
+ replyInThread: effectiveReplyInThread,
2600
+ allowTopLevelReplyFallback,
2601
+ accountId,
2602
+ ...mentions ? { mentions } : {}
2603
+ })
2604
+ }));
2147
2605
  }
2148
2606
  }
2149
- if (hasMedia) await sendMediaReplies(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
2607
+ if (hasMedia) await collectMediaDelivery(payload, hasVoiceMedia && hasText ? { fallbackText: text } : void 0);
2608
+ const result = mergeFeishuReplyDeliveryResults(deliveredResults, text);
2609
+ if (priorClosedStreamingSettlement?.error !== void 0) throw createFeishuPartialReplyDeliveryError(isChannelPartialDeliveryError(priorClosedStreamingSettlement.error) && priorClosedStreamingSettlement.error instanceof Error ? priorClosedStreamingSettlement.error.cause ?? priorClosedStreamingSettlement.error : priorClosedStreamingSettlement.error, result);
2610
+ return result;
2150
2611
  },
2151
2612
  onError: handleDeliveryError
2152
2613
  },
2153
2614
  replyOptions: {
2154
2615
  onModelSelected: prefixContext.onModelSelected,
2155
2616
  disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : true,
2156
- onPartialReply: streamingEnabled ? (payload) => {
2617
+ onPartialReply: previewStreamingEnabled ? (payload) => {
2157
2618
  if (!payload.text) return;
2158
2619
  const cleaned = stripReasoningTagsFromText(payload.text, {
2159
2620
  mode: "strict",
@@ -2172,7 +2633,7 @@ function createFeishuReplyDispatcher(params) {
2172
2633
  queueReasoningUpdate(formatReasoningMessage(payload.text));
2173
2634
  } : void 0,
2174
2635
  onReasoningEnd: reasoningPreviewEnabled ? () => {} : void 0,
2175
- onToolStart: streamingEnabled ? (payload) => {
2636
+ onToolStart: previewStreamingEnabled ? (payload) => {
2176
2637
  if (!isChannelProgressDraftWorkToolName(payload.name)) return;
2177
2638
  const statusLineLocal = formatChannelProgressDraftLineForEntry(account.config, {
2178
2639
  event: "tool",
@@ -2182,13 +2643,13 @@ function createFeishuReplyDispatcher(params) {
2182
2643
  }, { detailMode: payload.detailMode });
2183
2644
  if (statusLineLocal) updateStreamingStatusLine(statusLineLocal);
2184
2645
  } : void 0,
2185
- onAssistantMessageStart: streamingEnabled ? () => {
2646
+ onAssistantMessageStart: previewStreamingEnabled ? () => {
2186
2647
  updateStreamingStatusLine("", { startIfNeeded: false });
2187
2648
  } : void 0,
2188
- onCompactionStart: streamingEnabled ? () => {
2649
+ onCompactionStart: previewStreamingEnabled ? () => {
2189
2650
  updateStreamingStatusLine("📦 **Compacting context...**");
2190
2651
  } : void 0,
2191
- onCompactionEnd: streamingEnabled ? () => {
2652
+ onCompactionEnd: previewStreamingEnabled ? () => {
2192
2653
  updateStreamingStatusLine("");
2193
2654
  } : void 0
2194
2655
  },
@@ -2225,15 +2686,14 @@ function isFeishuTopicSessionScope(scope) {
2225
2686
  return scope === "group_topic" || scope === "group_topic_sender";
2226
2687
  }
2227
2688
  async function resolveFeishuAudioPreflightTranscript(params) {
2228
- if (params.content.trim() !== "<media:audio>") return;
2229
- const audioMedia = params.mediaList.filter((media) => media.contentType?.startsWith("audio/"));
2689
+ if (params.messageType !== "audio" || params.content.trim()) return;
2690
+ const audioMedia = params.mediaList.filter((media) => Boolean(media.path) && (media.kind === "audio" || media.contentType?.startsWith("audio/")));
2230
2691
  if (audioMedia.length === 0) return;
2231
2692
  try {
2232
2693
  const { transcribeFirstAudio } = await import("./audio-preflight.runtime-BBfC9elv.js");
2233
2694
  return await transcribeFirstAudio({
2234
2695
  ctx: {
2235
- MediaPaths: audioMedia.map((media) => media.path),
2236
- MediaTypes: audioMedia.map((media) => media.contentType).filter(Boolean),
2696
+ media: audioMedia,
2237
2697
  ChatType: params.chatType
2238
2698
  },
2239
2699
  cfg: params.cfg
@@ -2244,8 +2704,7 @@ async function resolveFeishuAudioPreflightTranscript(params) {
2244
2704
  }
2245
2705
  }
2246
2706
  /**
2247
- * Build media payload for inbound context.
2248
- * Similar to Discord's buildDiscordMediaPayload().
2707
+ * Parse an inbound Feishu event into its caption and routing metadata.
2249
2708
  */
2250
2709
  function parseFeishuMessageEvent(event, botOpenId, _botName) {
2251
2710
  const rawContent = parseMessageContent(event.message.content, event.message.message_type);
@@ -2313,7 +2772,7 @@ async function filterFetchedGroupContextMessages(messages, params) {
2313
2772
  }) ? message : void 0))).filter((message) => message !== void 0);
2314
2773
  }
2315
2774
  async function handleFeishuMessage(params) {
2316
- const { cfg, event, botOpenId, botName, runtime, channelRuntime, chatHistories, accountId, processingClaim, messageDedupeKey: messageDedupeKeyOverride } = params;
2775
+ const { cfg, event, botOpenId, botName, runtime, channelRuntime, chatHistories, accountId, processingClaim, messageDedupeKey: messageDedupeKeyOverride, turnAdoptionLifecycle } = params;
2317
2776
  const account = resolveFeishuRuntimeAccount({
2318
2777
  cfg,
2319
2778
  accountId
@@ -2323,7 +2782,7 @@ async function handleFeishuMessage(params) {
2323
2782
  const error = runtime?.error ?? console.error;
2324
2783
  const messageId = event.message.message_id;
2325
2784
  const messageDedupeKey = messageDedupeKeyOverride ?? resolveFeishuMessageDedupeKey(event);
2326
- if (!await finalizeFeishuMessageProcessing({
2785
+ if (!turnAdoptionLifecycle && !await finalizeFeishuMessageProcessing({
2327
2786
  messageId: messageDedupeKey,
2328
2787
  namespace: account.accountId,
2329
2788
  log,
@@ -2787,7 +3246,7 @@ async function handleFeishuMessage(params) {
2787
3246
  });
2788
3247
  log(`feishu[${account.accountId}]: ${inboundLabel}: ${preview}`);
2789
3248
  const mediaMaxBytes = (feishuCfg?.mediaMaxMb ?? 30) * 1024 * 1024;
2790
- const mediaResolution = await resolveFeishuMediaList({
3249
+ const mediaList = await resolveFeishuMediaList({
2791
3250
  cfg,
2792
3251
  messageId: ctx.messageId,
2793
3252
  messageType: event.message.message_type,
@@ -2796,12 +3255,10 @@ async function handleFeishuMessage(params) {
2796
3255
  log,
2797
3256
  accountId: account.accountId
2798
3257
  });
2799
- const mediaList = mediaResolution.media;
2800
- const mediaFailurePresentation = resolveFeishuMediaFailurePresentation(event.message.content, event.message.message_type);
2801
- const mediaFailureContent = mediaResolution.unavailableCount > 0 ? formatInboundMediaUnavailableText({
2802
- body: mediaFailurePresentation.unavailableBody ?? ctx.content,
2803
- mediaPlaceholder: mediaFailurePresentation.mediaPlaceholder,
2804
- notice: `[feishu ${mediaResolution.unavailableCount > 1 ? `${mediaResolution.unavailableCount} attachments` : "attachment"} unavailable]`
3258
+ const unavailableMediaCount = mediaList.filter((media) => !media.path).length;
3259
+ const mediaFailureContent = unavailableMediaCount > 0 ? formatInboundMediaUnavailableText({
3260
+ body: ctx.content,
3261
+ notice: `[feishu ${unavailableMediaCount > 1 ? `${unavailableMediaCount} attachments` : "attachment"} unavailable]`
2805
3262
  }) : ctx.content;
2806
3263
  let quotedMessageInfo = null;
2807
3264
  let quotedContent;
@@ -2836,10 +3293,11 @@ async function handleFeishuMessage(params) {
2836
3293
  cfg: effectiveCfg,
2837
3294
  mediaList,
2838
3295
  content: ctx.content,
3296
+ messageType: event.message.message_type,
2839
3297
  chatType: isGroup ? "group" : "direct",
2840
3298
  log
2841
3299
  });
2842
- const preflightAudioIndex = audioTranscript === void 0 ? -1 : mediaList.findIndex((media) => media.contentType?.startsWith("audio/"));
3300
+ const preflightAudioIndex = audioTranscript === void 0 ? -1 : mediaList.findIndex((media) => media.kind === "audio" || media.contentType?.startsWith("audio/"));
2843
3301
  const inboundMedia = toInboundMediaFacts(mediaList, { transcribed: (_media, index) => index === preflightAudioIndex });
2844
3302
  const requiredMentionTargets = isGroup && ctx.senderType === "bot" && ctx.senderOpenId ? [{
2845
3303
  openId: ctx.senderOpenId,
@@ -3074,6 +3532,7 @@ async function handleFeishuMessage(params) {
3074
3532
  rawBody: commandFacingContent,
3075
3533
  commandBody: commandFacingContent
3076
3534
  },
3535
+ sessionTranscript: { historyLimit: isGroup ? historyLimit : 0 },
3077
3536
  access: {
3078
3537
  mentions: {
3079
3538
  canDetectMention: isGroup,
@@ -3123,144 +3582,213 @@ async function handleFeishuMessage(params) {
3123
3582
  };
3124
3583
  };
3125
3584
  if (broadcastAgents) {
3126
- if (!await recordProcessedFeishuMessage(messageDedupeKey ?? ctx.messageId, "broadcast", log)) {
3585
+ const broadcastDedupeKey = messageDedupeKey ?? ctx.messageId;
3586
+ const broadcastClaim = await claimUnprocessedFeishuMessage({
3587
+ messageId: broadcastDedupeKey,
3588
+ namespace: "broadcast",
3589
+ log
3590
+ });
3591
+ if (broadcastClaim.kind === "duplicate" || broadcastClaim.kind === "inflight") {
3127
3592
  log(`feishu[${account.accountId}]: broadcast already claimed by another account for message ${ctx.messageId}; skipping`);
3128
3593
  return;
3129
3594
  }
3595
+ const broadcastSettlement = createFeishuBroadcastIngressSettlement({
3596
+ lifecycle: turnAdoptionLifecycle,
3597
+ replayClaim: broadcastClaim.kind === "claimed" ? broadcastClaim.handle : void 0,
3598
+ onReplayCommitError: (err) => error(`feishu[${account.accountId}]: failed to commit broadcast replay guard: ${String(err)}`),
3599
+ onAdopted: () => {
3600
+ if (isGroup && historyKey && chatHistories) createChannelHistoryWindow({ historyMap: chatHistories }).clear({
3601
+ historyKey,
3602
+ limit: historyLimit
3603
+ });
3604
+ }
3605
+ });
3606
+ const abandonBroadcast = async (err) => {
3607
+ try {
3608
+ await broadcastSettlement.onDispatchFailed(err);
3609
+ } catch (abandonError) {
3610
+ error(`feishu[${account.accountId}]: failed to abandon broadcast ingress: ${String(abandonError)}`);
3611
+ }
3612
+ };
3130
3613
  const strategy = cfg.broadcast?.strategy === "sequential" ? "sequential" : "parallel";
3131
3614
  const activeAgentId = ctx.mentionedBot || !requireMention ? normalizeAgentId$2(route.agentId) : null;
3132
3615
  const agentIds = (cfg.agents?.list ?? []).map((a) => normalizeAgentId$2(a.id));
3133
3616
  const hasKnownAgents = agentIds.length > 0;
3134
3617
  log(`feishu[${account.accountId}]: broadcasting to ${broadcastAgents.length} agents (strategy=${strategy}, active=${activeAgentId ?? "none"})`);
3135
3618
  const dispatchForAgent = async (agentId) => {
3136
- if (hasKnownAgents && !agentIds.includes(normalizeAgentId$2(agentId))) {
3619
+ const normalizedAgentId = normalizeAgentId$2(agentId);
3620
+ if (hasKnownAgents && !agentIds.includes(normalizedAgentId)) {
3137
3621
  log(`feishu[${account.accountId}]: broadcast agent ${agentId} not found in agents.list; skipping`);
3138
3622
  return;
3139
3623
  }
3140
- const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
3141
- const agentStorePath = resolveStorePath(cfg.session?.store, { agentId });
3142
- const agentRecord = {
3143
- updateLastRoute: buildFeishuInboundLastRouteUpdate({
3144
- sessionKey: agentSessionKey,
3145
- accountId: route.accountId
3146
- }),
3147
- onRecordError: (err) => {
3148
- log(`feishu[${account.accountId}]: failed to record broadcast inbound session ${agentSessionKey}: ${String(err)}`);
3624
+ let agentClaim;
3625
+ for (let attempt = 0; attempt < 2; attempt += 1) {
3626
+ agentClaim = await claimUnprocessedFeishuMessage({
3627
+ messageId: broadcastDedupeKey,
3628
+ namespace: `broadcast:${normalizedAgentId}`,
3629
+ log
3630
+ });
3631
+ if (agentClaim.kind === "duplicate") return;
3632
+ if (agentClaim.kind !== "inflight") break;
3633
+ broadcastSettlement.onLanePending();
3634
+ try {
3635
+ await agentClaim.pending;
3636
+ return;
3637
+ } catch (err) {
3638
+ if (attempt === 1) throw err;
3149
3639
  }
3150
- };
3151
- const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
3152
- cfg,
3153
- agentId,
3154
- storePath: agentStorePath,
3155
- sessionKey: agentSessionKey
3156
- });
3157
- const agentCtx = await buildCtxPayloadForAgent(agentId, agentSessionKey, route.accountId, ctx.mentionedBot && agentId === activeAgentId);
3158
- if (agentId === activeAgentId) {
3159
- const identity = resolveAgentOutboundIdentity(cfg, agentId);
3160
- const { dispatcherOptions, delivery, replyOptions, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3640
+ }
3641
+ const lane = broadcastSettlement.createLane(agentClaim?.kind === "claimed" ? agentClaim.handle : void 0);
3642
+ try {
3643
+ const agentSessionKey = buildBroadcastSessionKey(route.sessionKey, route.agentId, agentId);
3644
+ const agentStorePath = resolveStorePath(cfg.session?.store, { agentId });
3645
+ const agentRecord = {
3646
+ updateLastRoute: buildFeishuInboundLastRouteUpdate({
3647
+ sessionKey: agentSessionKey,
3648
+ accountId: route.accountId
3649
+ }),
3650
+ onRecordError: (err) => {
3651
+ log(`feishu[${account.accountId}]: failed to record broadcast inbound session ${agentSessionKey}: ${String(err)}`);
3652
+ }
3653
+ };
3654
+ const allowReasoningPreview = resolveFeishuReasoningPreviewEnabled({
3161
3655
  cfg,
3162
3656
  agentId,
3163
- runtime,
3164
- chatId: ctx.chatId,
3165
- sendTarget: feishuTo,
3166
- allowReasoningPreview,
3167
- replyToMessageId: replyTargetMessageId,
3168
- typingTargetMessageId,
3169
- skipReplyToInMessages: !isGroup && !directThreadReply,
3170
- replyInThread,
3171
- rootId: ctx.rootId,
3172
- threadReply,
3173
- accountId: account.accountId,
3174
- identity,
3175
- mentionTargets: ctx.mentionTargets,
3176
- requiredMentionTargets,
3177
- messageCreateTimeMs,
3657
+ storePath: agentStorePath,
3178
3658
  sessionKey: agentSessionKey
3179
3659
  });
3180
- log(`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`);
3181
- const turnResult = await core.channel.inbound.run({
3182
- channel: "feishu",
3183
- accountId: route.accountId,
3184
- raw: ctx,
3185
- adapter: {
3186
- ingest: () => ({
3187
- id: ctx.messageId,
3188
- timestamp: messageCreateTimeMs,
3189
- rawText: ctx.content,
3190
- textForAgent: agentCtx.BodyForAgent,
3191
- textForCommands: agentCtx.CommandBody,
3192
- raw: ctx
3193
- }),
3194
- resolveTurn: () => ({
3195
- cfg,
3196
- channel: "feishu",
3197
- accountId: route.accountId,
3198
- route: {
3199
- agentId,
3200
- sessionKey: agentSessionKey
3201
- },
3202
- ctxPayload: agentCtx,
3203
- record: agentRecord,
3204
- dispatcherOptions,
3205
- delivery,
3206
- replyOptions
3207
- })
3208
- }
3209
- });
3210
- if (turnResult.dispatched && shouldSendNoVisibleReplyFallback(turnResult.dispatchResult)) await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
3211
- } else {
3212
- delete agentCtx.CommandAuthorized;
3213
- log(`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`);
3214
- await core.channel.inbound.run({
3215
- channel: "feishu",
3216
- accountId: route.accountId,
3217
- raw: ctx,
3218
- adapter: {
3219
- ingest: () => ({
3220
- id: ctx.messageId,
3221
- timestamp: messageCreateTimeMs,
3222
- rawText: ctx.content,
3223
- textForAgent: agentCtx.BodyForAgent,
3224
- textForCommands: agentCtx.CommandBody,
3225
- raw: ctx
3226
- }),
3227
- resolveTurn: () => ({
3228
- cfg,
3229
- channel: "feishu",
3230
- accountId: route.accountId,
3231
- route: {
3232
- agentId,
3233
- sessionKey: agentSessionKey
3234
- },
3235
- ctxPayload: agentCtx,
3236
- record: agentRecord,
3237
- admission: {
3238
- kind: "observeOnly",
3239
- reason: "broadcast-observer"
3240
- },
3241
- delivery: { deliver: async () => ({ visibleReplySent: false }) }
3242
- })
3243
- }
3244
- });
3660
+ const agentCtx = await buildCtxPayloadForAgent(agentId, agentSessionKey, route.accountId, ctx.mentionedBot && agentId === activeAgentId);
3661
+ if (agentId === activeAgentId) {
3662
+ const identity = resolveAgentOutboundIdentity(cfg, agentId);
3663
+ const { dispatcherOptions, delivery, replyOptions, ensureNoVisibleReplyFallback } = createFeishuReplyDispatcher({
3664
+ cfg,
3665
+ agentId,
3666
+ runtime,
3667
+ chatId: ctx.chatId,
3668
+ sendTarget: feishuTo,
3669
+ allowReasoningPreview,
3670
+ replyToMessageId: replyTargetMessageId,
3671
+ typingTargetMessageId,
3672
+ skipReplyToInMessages: !isGroup && !directThreadReply,
3673
+ replyInThread,
3674
+ rootId: ctx.rootId,
3675
+ threadReply,
3676
+ accountId: account.accountId,
3677
+ identity,
3678
+ mentionTargets: ctx.mentionTargets,
3679
+ requiredMentionTargets,
3680
+ messageCreateTimeMs,
3681
+ sessionKey: agentSessionKey
3682
+ });
3683
+ log(`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`);
3684
+ const turnResult = await core.channel.inbound.run({
3685
+ channel: "feishu",
3686
+ accountId: route.accountId,
3687
+ raw: ctx,
3688
+ adapter: {
3689
+ ingest: () => ({
3690
+ id: ctx.messageId,
3691
+ timestamp: messageCreateTimeMs,
3692
+ rawText: ctx.content,
3693
+ textForAgent: agentCtx.BodyForAgent,
3694
+ textForCommands: agentCtx.CommandBody,
3695
+ raw: ctx
3696
+ }),
3697
+ resolveTurn: () => ({
3698
+ cfg,
3699
+ channel: "feishu",
3700
+ accountId: route.accountId,
3701
+ route: {
3702
+ agentId,
3703
+ sessionKey: agentSessionKey
3704
+ },
3705
+ ctxPayload: agentCtx,
3706
+ record: agentRecord,
3707
+ dispatcherOptions,
3708
+ delivery,
3709
+ replyOptions: {
3710
+ ...replyOptions,
3711
+ ...bindIngressLifecycleToReplyOptions(lane.lifecycle)
3712
+ }
3713
+ })
3714
+ }
3715
+ });
3716
+ if (turnResult.dispatched && shouldSendNoVisibleReplyFallback(turnResult.dispatchResult)) await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
3717
+ await lane.onDispatchComplete(turnResult.dispatched);
3718
+ } else {
3719
+ delete agentCtx.CommandAuthorized;
3720
+ log(`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`);
3721
+ const turnResult = await core.channel.inbound.run({
3722
+ channel: "feishu",
3723
+ accountId: route.accountId,
3724
+ raw: ctx,
3725
+ adapter: {
3726
+ ingest: () => ({
3727
+ id: ctx.messageId,
3728
+ timestamp: messageCreateTimeMs,
3729
+ rawText: ctx.content,
3730
+ textForAgent: agentCtx.BodyForAgent,
3731
+ textForCommands: agentCtx.CommandBody,
3732
+ raw: ctx
3733
+ }),
3734
+ resolveTurn: () => ({
3735
+ cfg,
3736
+ channel: "feishu",
3737
+ accountId: route.accountId,
3738
+ route: {
3739
+ agentId,
3740
+ sessionKey: agentSessionKey
3741
+ },
3742
+ ctxPayload: agentCtx,
3743
+ record: agentRecord,
3744
+ admission: {
3745
+ kind: "observeOnly",
3746
+ reason: "broadcast-observer"
3747
+ },
3748
+ delivery: { deliver: async () => ({ visibleReplySent: false }) },
3749
+ replyOptions: bindIngressLifecycleToReplyOptions(lane.lifecycle)
3750
+ })
3751
+ }
3752
+ });
3753
+ await lane.onDispatchComplete(turnResult.dispatched);
3754
+ }
3755
+ } catch (err) {
3756
+ await lane.onDispatchFailed(err);
3757
+ throw err;
3245
3758
  }
3246
3759
  };
3760
+ const results = [];
3247
3761
  if (strategy === "sequential") for (const agentId of broadcastAgents) try {
3248
3762
  await dispatchForAgent(agentId);
3249
- } catch (err) {
3250
- log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(err)}`);
3763
+ results.push({
3764
+ status: "fulfilled",
3765
+ value: void 0
3766
+ });
3767
+ } catch (reason) {
3768
+ results.push({
3769
+ status: "rejected",
3770
+ reason
3771
+ });
3251
3772
  }
3252
- else {
3253
- const results = await Promise.allSettled(broadcastAgents.map(dispatchForAgent));
3254
- for (const [i, result] of results.entries()) if (result.status === "rejected") {
3255
- const agentId = broadcastAgents.at(i);
3256
- if (agentId === void 0) continue;
3257
- log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(result.reason)}`);
3258
- }
3773
+ else results.push(...await Promise.allSettled(broadcastAgents.map(dispatchForAgent)));
3774
+ const failures = [];
3775
+ for (const [i, result] of results.entries()) if (result.status === "rejected") {
3776
+ const agentId = broadcastAgents.at(i);
3777
+ if (agentId === void 0) continue;
3778
+ log(`feishu[${account.accountId}]: broadcast dispatch failed for agent=${agentId}: ${String(result.reason)}`);
3779
+ failures.push(result.reason);
3780
+ }
3781
+ if (failures.length > 0) {
3782
+ const failure = failures.length === 1 ? failures[0] : new AggregateError(failures, "Feishu broadcast dispatch failed");
3783
+ await abandonBroadcast(failure);
3784
+ throw failure;
3785
+ }
3786
+ try {
3787
+ await broadcastSettlement.onDispatchComplete();
3788
+ } catch (err) {
3789
+ await abandonBroadcast(err);
3790
+ throw err;
3259
3791
  }
3260
- if (isGroup && historyKey && chatHistories) createChannelHistoryWindow({ historyMap: chatHistories }).clear({
3261
- historyKey,
3262
- limit: historyLimit
3263
- });
3264
3792
  log(`feishu[${account.accountId}]: broadcast dispatch complete for ${broadcastAgents.length} agents`);
3265
3793
  } else {
3266
3794
  const ctxPayload = await buildCtxPayloadForAgent(route.agentId, route.sessionKey, route.accountId, ctx.mentionedBot);
@@ -3332,7 +3860,10 @@ async function handleFeishuMessage(params) {
3332
3860
  },
3333
3861
  dispatcherOptions,
3334
3862
  delivery,
3335
- replyOptions
3863
+ replyOptions: {
3864
+ ...replyOptions,
3865
+ ...turnAdoptionLifecycle ? bindIngressLifecycleToReplyOptions(turnAdoptionLifecycle) : {}
3866
+ }
3336
3867
  })
3337
3868
  }
3338
3869
  });
@@ -3344,6 +3875,7 @@ async function handleFeishuMessage(params) {
3344
3875
  }
3345
3876
  } catch (err) {
3346
3877
  error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
3878
+ if (turnAdoptionLifecycle) throw err;
3347
3879
  }
3348
3880
  }
3349
3881
  //#endregion
@@ -3721,6 +4253,350 @@ async function handleFeishuCardAction(params) {
3721
4253
  }
3722
4254
  }
3723
4255
  //#endregion
4256
+ //#region extensions/feishu/src/feishu-ingress.ts
4257
+ const FEISHU_INGRESS_PAYLOAD_VERSION = 1;
4258
+ const FEISHU_INGRESS_POLL_INTERVAL_MS = 500;
4259
+ const FEISHU_INGRESS_PRUNE_INTERVAL_MS = 3600 * 1e3;
4260
+ const FEISHU_INGRESS_COMPLETED_TTL_MS = 720 * 60 * 60 * 1e3;
4261
+ const FEISHU_INGRESS_COMPLETED_MAX_ENTRIES = 2e4;
4262
+ const FEISHU_INGRESS_FAILED_TTL_MS = 720 * 60 * 60 * 1e3;
4263
+ const FEISHU_INGRESS_FAILED_MAX_ENTRIES = 2e4;
4264
+ const FEISHU_DURABLE_EVENT_TYPES = /* @__PURE__ */ new Set(["drive.notice.comment_add_v1", "im.message.receive_v1"]);
4265
+ var FeishuIngressPermanentError = class extends Error {
4266
+ constructor(reason, message, options) {
4267
+ super(message, options);
4268
+ this.reason = reason;
4269
+ this.name = "FeishuIngressPermanentError";
4270
+ }
4271
+ };
4272
+ function isRecord$1(value) {
4273
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4274
+ }
4275
+ function readString(value) {
4276
+ return typeof value === "string" && value.trim() ? value.trim() : null;
4277
+ }
4278
+ function parseRawEnvelope(rawEnvelope) {
4279
+ let parsed;
4280
+ try {
4281
+ parsed = JSON.parse(rawEnvelope);
4282
+ } catch (error) {
4283
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope contains invalid JSON.", { cause: error });
4284
+ }
4285
+ if (!isRecord$1(parsed)) throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope must be a JSON object.");
4286
+ return parsed;
4287
+ }
4288
+ function decryptEnvelope(envelope, encryptKey) {
4289
+ const encrypted = readString(envelope.encrypt);
4290
+ if (!encrypted) return envelope;
4291
+ const key = encryptKey?.trim();
4292
+ if (!key) throw new FeishuIngressPermanentError("authentication-failed", "Feishu encrypted ingress envelope has no configured encrypt key.");
4293
+ try {
4294
+ return parseRawEnvelope(new Lark.AESCipher(key).decrypt(encrypted));
4295
+ } catch (error) {
4296
+ if (error instanceof FeishuIngressPermanentError) throw error;
4297
+ throw new FeishuIngressPermanentError("authentication-failed", "Feishu ingress envelope decryption failed.", { cause: error });
4298
+ }
4299
+ }
4300
+ function inspectFeishuIngressEnvelope(rawEnvelope, encryptKey, allowInvalidLane = false) {
4301
+ const envelope = decryptEnvelope(parseRawEnvelope(rawEnvelope), encryptKey);
4302
+ const nestedHeader = isRecord$1(envelope.header) ? envelope.header : null;
4303
+ const nestedEvent = isRecord$1(envelope.event) ? envelope.event : null;
4304
+ const eventType = readString(nestedHeader?.event_type) ?? readString(envelope.event_type);
4305
+ if (!eventType || !FEISHU_DURABLE_EVENT_TYPES.has(eventType)) return null;
4306
+ const eventId = readString(nestedHeader?.event_id) ?? readString(envelope.event_id);
4307
+ if (!eventId) throw new FeishuIngressPermanentError("invalid-event", `Feishu ${eventType} envelope is missing header.event_id.`);
4308
+ const event = nestedEvent ?? envelope;
4309
+ if (eventType === "im.message.receive_v1") {
4310
+ const chatId = readString((isRecord$1(event.message) ? event.message : null)?.chat_id);
4311
+ if (!chatId) {
4312
+ if (allowInvalidLane) return {
4313
+ eventId,
4314
+ eventType,
4315
+ laneKey: `invalid:${eventType}:${eventId}`
4316
+ };
4317
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu message ingress envelope is missing event.message.chat_id.");
4318
+ }
4319
+ return {
4320
+ eventId,
4321
+ eventType,
4322
+ laneKey: `chat:${chatId}`
4323
+ };
4324
+ }
4325
+ const noticeMeta = isRecord$1(event.notice_meta) ? event.notice_meta : null;
4326
+ const fileType = readString(noticeMeta?.file_type);
4327
+ const documentId = readString(noticeMeta?.file_token);
4328
+ if (!fileType || !documentId) {
4329
+ if (allowInvalidLane) return {
4330
+ eventId,
4331
+ eventType,
4332
+ laneKey: `invalid:${eventType}:${eventId}`
4333
+ };
4334
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu comment ingress envelope is missing its document identity.");
4335
+ }
4336
+ return {
4337
+ eventId,
4338
+ eventType,
4339
+ laneKey: `comment-doc:${fileType}:${documentId}`
4340
+ };
4341
+ }
4342
+ function resolveFeishuIngressNonRetryableFailure(error) {
4343
+ if (error instanceof FeishuIngressPermanentError) return {
4344
+ reason: error.reason,
4345
+ message: error.message
4346
+ };
4347
+ for (const candidate of collectErrorGraphCandidates(error, (current) => [current.cause, current.response])) {
4348
+ const status = isRecord$1(candidate) ? candidate.status : void 0;
4349
+ if (status === 401 || status === 403) return {
4350
+ reason: "authentication-failed",
4351
+ message: formatErrorMessage(error)
4352
+ };
4353
+ }
4354
+ return null;
4355
+ }
4356
+ /** Fan one merged Feishu turn's adoption across every transport and logical claim. */
4357
+ function buildFeishuFlushIngressLifecycle(sources, options) {
4358
+ const durableSources = sources.filter((source) => source.lifecycle !== void 0);
4359
+ const lifecycles = durableSources.map((source) => source.lifecycle);
4360
+ const replayClaims = durableSources.map((source) => source.replayClaim).filter((claim) => claim !== void 0);
4361
+ const [firstLifecycle] = lifecycles;
4362
+ if (!firstLifecycle) return {
4363
+ lifecycle: void 0,
4364
+ settle: async () => {}
4365
+ };
4366
+ let handedOff = false;
4367
+ let terminal;
4368
+ let adopting;
4369
+ let abandoning;
4370
+ const releaseReplayClaims = () => {
4371
+ for (const claim of replayClaims) claim.release({ error: /* @__PURE__ */ new Error("feishu-ingress-not-adopted") });
4372
+ };
4373
+ const runAbandon = async () => {
4374
+ if (terminal) return;
4375
+ releaseReplayClaims();
4376
+ await Promise.all(lifecycles.map(async (lifecycle) => await lifecycle.onAbandoned()));
4377
+ terminal = "abandoned";
4378
+ };
4379
+ const ensureAbandoned = async () => {
4380
+ if (terminal) return;
4381
+ const activeAbandonment = abandoning ?? runAbandon();
4382
+ abandoning = activeAbandonment;
4383
+ try {
4384
+ await activeAbandonment;
4385
+ } finally {
4386
+ if (abandoning === activeAbandonment && terminal !== "abandoned") abandoning = void 0;
4387
+ }
4388
+ };
4389
+ const abandonAll = async () => {
4390
+ if (terminal) return;
4391
+ if (adopting) {
4392
+ await adopting.catch(() => void 0);
4393
+ if (terminal) return;
4394
+ }
4395
+ await ensureAbandoned();
4396
+ };
4397
+ const adoptAll = async () => {
4398
+ if (terminal) return;
4399
+ if (abandoning) {
4400
+ await abandoning.catch(() => void 0);
4401
+ if (terminal) return;
4402
+ }
4403
+ const activeAdoption = adopting ?? (async () => {
4404
+ try {
4405
+ for (const lifecycle of lifecycles) await lifecycle.onAdopted();
4406
+ terminal = "adopted";
4407
+ const results = await Promise.allSettled(replayClaims.map(async (claim) => claim.commit()));
4408
+ for (const result of results) if (result.status === "rejected") try {
4409
+ options?.onReplayCommitError?.(result.reason);
4410
+ } catch {}
4411
+ } catch (error) {
4412
+ await ensureAbandoned().catch(() => void 0);
4413
+ throw error;
4414
+ }
4415
+ })();
4416
+ adopting = activeAdoption;
4417
+ try {
4418
+ await activeAdoption;
4419
+ } finally {
4420
+ if (adopting === activeAdoption && terminal !== "adopted") adopting = void 0;
4421
+ }
4422
+ };
4423
+ return {
4424
+ lifecycle: {
4425
+ abortSignal: lifecycles.length === 1 ? firstLifecycle.abortSignal : AbortSignal.any(lifecycles.map((lifecycle) => lifecycle.abortSignal)),
4426
+ onAdopted: async () => {
4427
+ handedOff = true;
4428
+ await adoptAll();
4429
+ },
4430
+ onDeferred: () => {
4431
+ handedOff = true;
4432
+ for (const lifecycle of lifecycles) lifecycle.onDeferred();
4433
+ },
4434
+ onAdoptionFinalizing: () => {
4435
+ for (const lifecycle of lifecycles) lifecycle.onAdoptionFinalizing();
4436
+ },
4437
+ onAbandoned: async () => {
4438
+ handedOff = true;
4439
+ await abandonAll();
4440
+ }
4441
+ },
4442
+ settle: async () => {
4443
+ if (handedOff) return;
4444
+ handedOff = true;
4445
+ releaseReplayClaims();
4446
+ try {
4447
+ for (const lifecycle of lifecycles) lifecycle.onAdoptionFinalizing();
4448
+ for (const lifecycle of lifecycles) await lifecycle.onAdopted();
4449
+ terminal = "adopted";
4450
+ } catch (error) {
4451
+ await ensureAbandoned().catch(() => void 0);
4452
+ throw error;
4453
+ }
4454
+ }
4455
+ };
4456
+ }
4457
+ function createFeishuDurableIngress(options) {
4458
+ let socketTerminator;
4459
+ const activeLifecycles = /* @__PURE__ */ new Map();
4460
+ const deferredClaims = /* @__PURE__ */ new Map();
4461
+ const monitor = createChannelIngressMonitor({
4462
+ queue: options.queue ?? (() => getFeishuRuntime().state.openChannelIngressQueue({ accountId: options.accountId })),
4463
+ inspect: (rawEnvelope, context) => {
4464
+ const facts = inspectFeishuIngressEnvelope(rawEnvelope, options.encryptKey, context.phase === "admission");
4465
+ return facts ? {
4466
+ eventId: facts.eventId,
4467
+ laneKey: facts.laneKey
4468
+ } : null;
4469
+ },
4470
+ payload: {
4471
+ version: FEISHU_INGRESS_PAYLOAD_VERSION,
4472
+ serialize: (rawEnvelope, { receivedAt }) => ({
4473
+ receivedAt,
4474
+ rawEnvelope
4475
+ }),
4476
+ deserialize: (body) => body.rawEnvelope,
4477
+ encode: ({ body }) => ({
4478
+ version: FEISHU_INGRESS_PAYLOAD_VERSION,
4479
+ ...body
4480
+ }),
4481
+ decode: (payload) => ({
4482
+ version: payload.version,
4483
+ body: {
4484
+ receivedAt: payload.receivedAt,
4485
+ rawEnvelope: payload.rawEnvelope
4486
+ }
4487
+ }),
4488
+ createClaimError: (kind, claim) => new FeishuIngressPermanentError("invalid-event", kind === "invalid-version" ? `Feishu ingress row ${claim.id} has an unsupported version.` : `Feishu ingress row ${claim.id} has invalid delivery identity.`)
4489
+ },
4490
+ deliver: async (rawEnvelope, lifecycle, claim) => {
4491
+ let resolveDeferred;
4492
+ const deferred = new Promise((resolve) => {
4493
+ resolveDeferred = resolve;
4494
+ });
4495
+ let deferredSettled = false;
4496
+ const settleDeferred = () => {
4497
+ if (deferredSettled) return;
4498
+ deferredSettled = true;
4499
+ if (deferredClaims.get(claim.id) === deferred) deferredClaims.delete(claim.id);
4500
+ resolveDeferred();
4501
+ };
4502
+ const abandonHandlers = /* @__PURE__ */ new Set();
4503
+ const wrappedLifecycle = {
4504
+ ...lifecycle,
4505
+ onAdopted: async () => {
4506
+ try {
4507
+ await lifecycle.onAdopted();
4508
+ } finally {
4509
+ settleDeferred();
4510
+ }
4511
+ },
4512
+ onAbandoned: async () => {
4513
+ try {
4514
+ await Promise.allSettled([...abandonHandlers].map(async (handler) => await handler()));
4515
+ await lifecycle.onAbandoned();
4516
+ } finally {
4517
+ settleDeferred();
4518
+ }
4519
+ },
4520
+ registerAbandonHandler: (handler) => {
4521
+ abandonHandlers.add(handler);
4522
+ return () => abandonHandlers.delete(handler);
4523
+ }
4524
+ };
4525
+ activeLifecycles.set(claim.id, wrappedLifecycle);
4526
+ try {
4527
+ const result = await options.dispatcher.invoke(parseRawEnvelope(rawEnvelope), { needCheck: false });
4528
+ if (!isRecord$1(result)) return;
4529
+ if (result.kind === "deferred") {
4530
+ if (!deferredSettled) deferredClaims.set(claim.id, deferred);
4531
+ return { kind: "deferred" };
4532
+ }
4533
+ if (result.kind === "completed") return { kind: "completed" };
4534
+ if (result.kind === "failed-retryable") return {
4535
+ kind: "failed-retryable",
4536
+ error: result.error
4537
+ };
4538
+ return;
4539
+ } finally {
4540
+ if (activeLifecycles.get(claim.id) === wrappedLifecycle) activeLifecycles.delete(claim.id);
4541
+ }
4542
+ },
4543
+ pollIntervalMs: options.pollIntervalMs ?? FEISHU_INGRESS_POLL_INTERVAL_MS,
4544
+ retention: {
4545
+ pruneIntervalMs: FEISHU_INGRESS_PRUNE_INTERVAL_MS,
4546
+ completedTtlMs: FEISHU_INGRESS_COMPLETED_TTL_MS,
4547
+ completedMaxEntries: FEISHU_INGRESS_COMPLETED_MAX_ENTRIES,
4548
+ failedTtlMs: FEISHU_INGRESS_FAILED_TTL_MS,
4549
+ failedMaxEntries: FEISHU_INGRESS_FAILED_MAX_ENTRIES
4550
+ },
4551
+ drain: {
4552
+ adoptionStallTimeoutMs: options.adoptionStallTimeoutMs ?? DEFAULT_INGRESS_ADOPTION_STALL_MS,
4553
+ resolveNonRetryableFailure: resolveFeishuIngressNonRetryableFailure,
4554
+ onLog: (message) => options.runtime.error?.(`feishu ingress: ${message}`)
4555
+ },
4556
+ createStoppedError: () => /* @__PURE__ */ new Error("Feishu ingress is stopped."),
4557
+ onError: (error) => options.runtime.error?.(`feishu ingress drain failed: ${formatErrorMessage(error)}`)
4558
+ });
4559
+ const invoke = async (data, params) => {
4560
+ let rawEnvelope;
4561
+ try {
4562
+ const serialized = JSON.stringify(data);
4563
+ if (serialized === void 0) throw new TypeError("Feishu ingress envelope has no JSON representation.");
4564
+ rawEnvelope = serialized;
4565
+ } catch (error) {
4566
+ throw new FeishuIngressPermanentError("invalid-event", "Feishu ingress envelope is not serializable.", { cause: error });
4567
+ }
4568
+ const facts = inspectFeishuIngressEnvelope(rawEnvelope, options.encryptKey, true);
4569
+ if (!facts) return await options.dispatcher.invoke(data, params);
4570
+ try {
4571
+ await monitor.admit(rawEnvelope, { facts: {
4572
+ eventId: facts.eventId,
4573
+ laneKey: facts.laneKey
4574
+ } });
4575
+ } catch (error) {
4576
+ socketTerminator?.();
4577
+ throw error;
4578
+ }
4579
+ };
4580
+ return {
4581
+ invoke,
4582
+ resolveLifecycle: (data) => {
4583
+ const eventId = isRecord$1(data) ? readString(data.event_id) : null;
4584
+ return eventId ? activeLifecycles.get(eventId) : void 0;
4585
+ },
4586
+ setSocketTerminator: (terminate) => {
4587
+ socketTerminator = terminate;
4588
+ },
4589
+ start: monitor.start,
4590
+ stop: async () => {
4591
+ await monitor.stop();
4592
+ await Promise.allSettled(deferredClaims.values());
4593
+ activeLifecycles.clear();
4594
+ socketTerminator = void 0;
4595
+ },
4596
+ waitForIdle: monitor.waitForIdle
4597
+ };
4598
+ }
4599
+ //#endregion
3724
4600
  //#region extensions/feishu/src/monitor-defaults.ts
3725
4601
  const FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS = {
3726
4602
  windowMs: 6e4,
@@ -3871,7 +4747,8 @@ function applyBotIdentityState(accountId, identity) {
3871
4747
  });
3872
4748
  return {
3873
4749
  botOpenId,
3874
- botName
4750
+ botName,
4751
+ source: botOpenId ? identity.source : void 0
3875
4752
  };
3876
4753
  }
3877
4754
  async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
@@ -3881,12 +4758,13 @@ async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
3881
4758
  for (const [i, delayMs] of BOT_IDENTITY_RETRY_DELAYS_MS.entries()) {
3882
4759
  if (abortSignal?.aborted) return;
3883
4760
  if (!await waitForAbortableDelay(delayMs, abortSignal)) return;
3884
- const resolved = applyBotIdentityState(accountId, await fetchBotIdentityForMonitor(account, {
4761
+ const identity = await fetchBotIdentityForMonitor(account, {
3885
4762
  runtime,
3886
- abortSignal
3887
- }));
3888
- if (resolved.botOpenId) {
3889
- log(`feishu[${accountId}]: bot open_id recovered via background retry: ${resolved.botOpenId}`);
4763
+ abortSignal,
4764
+ allowCachedFallback: false
4765
+ });
4766
+ if (normalizeOptionalString(identity.botOpenId) && identity.source === "provider") {
4767
+ log(`feishu[${accountId}]: bot open_id recovered via background retry: ${applyBotIdentityState(accountId, identity).botOpenId}`);
3890
4768
  return;
3891
4769
  }
3892
4770
  const nextDelayResult = nextDelays.next();
@@ -3896,10 +4774,10 @@ async function retryBotIdentityProbe(account, accountId, runtime, abortSignal) {
3896
4774
  error(`feishu[${accountId}]: bot identity background retry exhausted; requireMention group messages may be skipped until restart`);
3897
4775
  }
3898
4776
  function startBotIdentityRecovery(params) {
3899
- const { account, accountId, runtime, abortSignal } = params;
4777
+ const { account, accountId, runtime, abortSignal, currentSource } = params;
3900
4778
  const log = runtime?.log ?? console.log;
3901
- log(`feishu[${accountId}]: bot open_id unknown; starting background retry (delays: ${BOT_IDENTITY_RETRY_DELAYS_MS.map((delay) => `${delay / 1e3}s`).join(", ")})`);
3902
- log(`feishu[${accountId}]: requireMention group messages stay gated until bot identity recovery succeeds`);
4779
+ log(`feishu[${accountId}]: bot open_id ${currentSource === "cache" ? "loaded from cache" : "unknown"}; starting background provider refresh (delays: ${BOT_IDENTITY_RETRY_DELAYS_MS.map((delay) => `${delay / 1e3}s`).join(", ")})`);
4780
+ if (currentSource !== "cache") log(`feishu[${accountId}]: requireMention group messages stay gated until bot identity recovery succeeds`);
3903
4781
  retryBotIdentityProbe(account, accountId, runtime, abortSignal);
3904
4782
  }
3905
4783
  //#endregion
@@ -4153,20 +5031,40 @@ function createFeishuCommentReplyDispatcher(params) {
4153
5031
  }
4154
5032
  },
4155
5033
  delivery: {
5034
+ observeMessageSent: true,
4156
5035
  deliver: async (payload, info) => {
4157
- if (info.kind !== "final") return;
5036
+ if (info.kind !== "final") return noVisibleFeishuReplyDelivery;
4158
5037
  const reply = resolveSendableOutboundReplyParts(payload);
4159
5038
  if (!reply.hasText) {
4160
5039
  if (reply.hasMedia) params.runtime.log?.(`feishu[${params.accountId ?? "default"}]: comment reply ignored media-only payload for comment=${params.commentId}`);
4161
- return;
5040
+ return noVisibleFeishuReplyDelivery;
4162
5041
  }
4163
5042
  const chunks = core.channel.text.chunkTextWithMode(reply.text, textChunkLimit, chunkMode);
4164
- for (const chunk of chunks) await deliverCommentThreadText(client, {
4165
- file_token: params.fileToken,
4166
- file_type: params.fileType,
4167
- comment_id: params.commentId,
4168
- content: chunk,
4169
- is_whole_comment: params.isWholeComment
5043
+ const results = [];
5044
+ const acceptedChunks = [];
5045
+ for (const chunk of chunks) try {
5046
+ const result = await deliverCommentThreadText(client, {
5047
+ file_token: params.fileToken,
5048
+ file_type: params.fileType,
5049
+ comment_id: params.commentId,
5050
+ content: chunk,
5051
+ is_whole_comment: params.isWholeComment
5052
+ });
5053
+ results.push({ messageId: result.delivery_mode === "reply_comment" ? result.reply_id : result.comment_id });
5054
+ acceptedChunks.push(chunk);
5055
+ } catch (error) {
5056
+ throw createFeishuPartialReplyDeliveryError(error, createFeishuReplyDeliveryResult({
5057
+ results,
5058
+ visibleReplySent: results.length > 0,
5059
+ content: acceptedChunks.join(""),
5060
+ kind: "text"
5061
+ }));
5062
+ }
5063
+ return createFeishuReplyDeliveryResult({
5064
+ results,
5065
+ visibleReplySent: results.length > 0,
5066
+ content: reply.text,
5067
+ kind: "text"
4170
5068
  });
4171
5069
  },
4172
5070
  onError: (err, info) => {
@@ -4332,18 +5230,18 @@ async function fetchDriveComments(params) {
4332
5230
  }
4333
5231
  async function requestFeishuOpenApi(params) {
4334
5232
  const formatErrorDetails = (error) => {
4335
- if (!isRecord$1(error)) return typeof error === "string" ? error : JSON.stringify(error);
4336
- const response = isRecord$1(error.response) ? error.response : void 0;
4337
- const responseData = isRecord$1(response?.data) ? response?.data : void 0;
5233
+ if (!isRecord$2(error)) return typeof error === "string" ? error : JSON.stringify(error);
5234
+ const response = isRecord$2(error.response) ? error.response : void 0;
5235
+ const responseData = isRecord$2(response?.data) ? response?.data : void 0;
4338
5236
  return safeJsonStringify({
4339
5237
  message: typeof error.message === "string" ? error.message : typeof error === "string" ? error : JSON.stringify(error),
4340
- code: readString(error.code),
4341
- method: readString(isRecord$1(error.config) ? error.config.method : void 0),
4342
- url: readString(isRecord$1(error.config) ? error.config.url : void 0),
5238
+ code: readString$1(error.code),
5239
+ method: readString$1(isRecord$2(error.config) ? error.config.method : void 0),
5240
+ url: readString$1(isRecord$2(error.config) ? error.config.url : void 0),
4343
5241
  http_status: typeof response?.status === "number" ? response.status : void 0,
4344
- feishu_code: typeof responseData?.code === "number" ? responseData.code : readString(responseData?.code),
4345
- feishu_msg: readString(responseData?.msg),
4346
- feishu_log_id: readString(responseData?.log_id)
5242
+ feishu_code: typeof responseData?.code === "number" ? responseData.code : readString$1(responseData?.code),
5243
+ feishu_msg: readString$1(responseData?.msg),
5244
+ feishu_log_id: readString$1(responseData?.log_id)
4347
5245
  });
4348
5246
  };
4349
5247
  const result = await raceWithTimeoutAndAbort(params.client.request({
@@ -4399,7 +5297,7 @@ async function resolveCommentReplyContext(params) {
4399
5297
  createTime: typeof params.reply.create_time === "number" ? params.reply.create_time : void 0,
4400
5298
  isBotAuthored: typeof userId === "string" && normalizedBotOpenIds.has(userId),
4401
5299
  content: await resolveParsedCommentContent({
4402
- elements: isRecord$1(params.reply.content) ? params.reply.content.elements : void 0,
5300
+ elements: isRecord$2(params.reply.content) ? params.reply.content.elements : void 0,
4403
5301
  botOpenIds: params.botOpenIds,
4404
5302
  currentDocument: params.currentDocument,
4405
5303
  client: params.client,
@@ -4571,7 +5469,7 @@ async function fetchDriveCommentContext(params) {
4571
5469
  const rootWholeReply = comment.reply_list?.replies?.[0];
4572
5470
  const normalizedBotOpenIds = new Set(Array.from(params.botOpenIds ?? []).map((botId) => normalizeString(botId)).filter((botId) => Boolean(botId)));
4573
5471
  const content = await resolveParsedCommentContent({
4574
- elements: isRecord$1(rootWholeReply?.content) ? rootWholeReply.content.elements : void 0,
5472
+ elements: isRecord$2(rootWholeReply?.content) ? rootWholeReply.content.elements : void 0,
4575
5473
  botOpenIds: params.botOpenIds,
4576
5474
  currentDocument,
4577
5475
  client: params.client,
@@ -4678,11 +5576,14 @@ async function resolveDriveCommentEventCore(params) {
4678
5576
  logger?.(`feishu[${accountId}]: unsupported drive comment notice type ${noticeType}`);
4679
5577
  return null;
4680
5578
  }
4681
- if (!botOpenId) {
5579
+ const configuredBotOpenId = botOpenId?.trim();
5580
+ const eventRecipientBotOpenId = event.notice_meta?.to_user_id?.open_id?.trim();
5581
+ const effectiveBotOpenId = configuredBotOpenId || (event.is_mentioned === true ? eventRecipientBotOpenId : void 0);
5582
+ if (!effectiveBotOpenId) {
4682
5583
  logger?.(`feishu[${accountId}]: skipping drive comment notice because bot open_id is unavailable event=${eventId}`);
4683
5584
  return null;
4684
5585
  }
4685
- if (senderId === botOpenId) {
5586
+ if (senderId === effectiveBotOpenId) {
4686
5587
  logger?.(`feishu[${accountId}]: ignoring self-authored drive comment notice event=${eventId} sender=${senderId}`);
4687
5588
  return null;
4688
5589
  }
@@ -4695,7 +5596,7 @@ async function resolveDriveCommentEventCore(params) {
4695
5596
  fileType,
4696
5597
  commentId,
4697
5598
  replyId,
4698
- botOpenIds: [botOpenId, event.notice_meta?.to_user_id?.open_id],
5599
+ botOpenIds: [effectiveBotOpenId, event.notice_meta?.to_user_id?.open_id],
4699
5600
  timeoutMs: verificationTimeoutMs,
4700
5601
  logger,
4701
5602
  accountId,
@@ -4717,32 +5618,32 @@ async function resolveDriveCommentEventCore(params) {
4717
5618
  };
4718
5619
  }
4719
5620
  function parseFeishuDriveCommentNoticeEventPayload(value) {
4720
- if (!isRecord$1(value) || !isRecord$1(value.notice_meta)) return null;
5621
+ if (!isRecord$2(value) || !isRecord$2(value.notice_meta)) return null;
4721
5622
  const noticeMeta = value.notice_meta;
4722
- const fromUserId = isRecord$1(noticeMeta.from_user_id) ? noticeMeta.from_user_id : void 0;
4723
- const toUserId = isRecord$1(noticeMeta.to_user_id) ? noticeMeta.to_user_id : void 0;
5623
+ const fromUserId = isRecord$2(noticeMeta.from_user_id) ? noticeMeta.from_user_id : void 0;
5624
+ const toUserId = isRecord$2(noticeMeta.to_user_id) ? noticeMeta.to_user_id : void 0;
4724
5625
  return {
4725
- comment_id: readString(value.comment_id),
4726
- event_id: readString(value.event_id),
5626
+ comment_id: readString$1(value.comment_id),
5627
+ event_id: readString$1(value.event_id),
4727
5628
  is_mentioned: asBoolean(value.is_mentioned),
4728
5629
  notice_meta: {
4729
- file_token: readString(noticeMeta.file_token),
4730
- file_type: readString(noticeMeta.file_type),
5630
+ file_token: readString$1(noticeMeta.file_token),
5631
+ file_type: readString$1(noticeMeta.file_type),
4731
5632
  from_user_id: fromUserId ? {
4732
- open_id: readString(fromUserId.open_id),
4733
- user_id: readString(fromUserId.user_id),
4734
- union_id: readString(fromUserId.union_id)
5633
+ open_id: readString$1(fromUserId.open_id),
5634
+ user_id: readString$1(fromUserId.user_id),
5635
+ union_id: readString$1(fromUserId.union_id)
4735
5636
  } : void 0,
4736
- notice_type: readString(noticeMeta.notice_type),
5637
+ notice_type: readString$1(noticeMeta.notice_type),
4737
5638
  to_user_id: toUserId ? {
4738
- open_id: readString(toUserId.open_id),
4739
- user_id: readString(toUserId.user_id),
4740
- union_id: readString(toUserId.union_id)
5639
+ open_id: readString$1(toUserId.open_id),
5640
+ user_id: readString$1(toUserId.user_id),
5641
+ union_id: readString$1(toUserId.union_id)
4741
5642
  } : void 0
4742
5643
  },
4743
- reply_id: readString(value.reply_id),
4744
- timestamp: readString(value.timestamp),
4745
- type: readString(value.type)
5644
+ reply_id: readString$1(value.reply_id),
5645
+ timestamp: readString$1(value.timestamp),
5646
+ type: readString$1(value.type)
4746
5647
  };
4747
5648
  }
4748
5649
  async function resolveDriveCommentEventTurn(params) {
@@ -5034,7 +5935,8 @@ async function handleFeishuCommentEvent(params) {
5034
5935
  error(`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`);
5035
5936
  } },
5036
5937
  dispatcherOptions,
5037
- delivery
5938
+ delivery,
5939
+ ...params.turnAdoptionLifecycle ? { replyOptions: bindIngressLifecycleToReplyOptions(params.turnAdoptionLifecycle) } : {}
5038
5940
  })
5039
5941
  }
5040
5942
  });
@@ -5121,52 +6023,40 @@ function createFeishuDriveCommentNoticeHandler(params) {
5121
6023
  });
5122
6024
  if (!fireAndForget) await promise;
5123
6025
  };
5124
- return async (data) => {
5125
- await runFeishuHandler(async () => {
5126
- const event = parseFeishuDriveCommentNoticeEventPayload(data);
5127
- if (!event) {
5128
- error(`feishu[${accountId}]: ignoring malformed drive comment notice payload`);
6026
+ const handleNotice = async (data, turnAdoptionLifecycle) => {
6027
+ const event = parseFeishuDriveCommentNoticeEventPayload(data);
6028
+ if (!event) {
6029
+ if (turnAdoptionLifecycle) throw new FeishuIngressPermanentError("invalid-event", "Feishu durable comment event payload is malformed.");
6030
+ error(`feishu[${accountId}]: ignoring malformed drive comment notice payload`);
6031
+ return;
6032
+ }
6033
+ log(`feishu[${accountId}]: received drive comment notice event=${event.event_id ?? "unknown"} type=${event.notice_meta?.notice_type ?? "unknown"} file=${event.notice_meta?.file_type ?? "unknown"}:${event.notice_meta?.file_token ?? "unknown"} comment=${event.comment_id ?? "unknown"} reply=${event.reply_id ?? "none"} from=${event.notice_meta?.from_user_id?.open_id ?? "unknown"} mentioned=${event.is_mentioned === true ? "yes" : "no"}`);
6034
+ await enqueue(buildCommentNoticeQueueKey(event), async () => {
6035
+ if (turnAdoptionLifecycle?.abortSignal.aborted) {
6036
+ await turnAdoptionLifecycle.onAbandoned();
5129
6037
  return;
5130
6038
  }
5131
- const eventId = event.event_id?.trim();
5132
- const syntheticMessageId = eventId ? `drive-comment:${eventId}` : void 0;
5133
- let processingClaim;
5134
- if (syntheticMessageId) {
5135
- const claim = await claimUnprocessedFeishuMessage({
5136
- messageId: syntheticMessageId,
5137
- namespace: accountId,
5138
- log
5139
- });
5140
- if (claim.kind === "duplicate") {
5141
- log(`feishu[${accountId}]: dropping duplicate comment event ${syntheticMessageId}`);
5142
- return;
5143
- }
5144
- if (claim.kind === "inflight") {
5145
- log(`feishu[${accountId}]: dropping in-flight comment event ${syntheticMessageId}`);
5146
- return;
5147
- }
5148
- processingClaim = claim.kind === "claimed" ? claim.handle : void 0;
5149
- }
5150
- log(`feishu[${accountId}]: received drive comment notice event=${event.event_id ?? "unknown"} type=${event.notice_meta?.notice_type ?? "unknown"} file=${event.notice_meta?.file_type ?? "unknown"}:${event.notice_meta?.file_token ?? "unknown"} comment=${event.comment_id ?? "unknown"} reply=${event.reply_id ?? "none"} from=${event.notice_meta?.from_user_id?.open_id ?? "unknown"} mentioned=${event.is_mentioned === true ? "yes" : "no"}`);
5151
- try {
5152
- await enqueue(buildCommentNoticeQueueKey(event), async () => {
5153
- await handleFeishuCommentEvent({
5154
- cfg,
5155
- accountId,
5156
- event,
5157
- botOpenId: getBotOpenId(accountId),
5158
- runtime,
5159
- abortSignal
5160
- });
5161
- });
5162
- await processingClaim?.commit();
5163
- } catch (err) {
5164
- if (isFeishuRetryableSyntheticEventError(err)) processingClaim?.release({ error: err });
5165
- else await processingClaim?.commit();
5166
- throw err;
5167
- }
6039
+ await handleFeishuCommentEvent({
6040
+ cfg,
6041
+ accountId,
6042
+ event,
6043
+ botOpenId: getBotOpenId(accountId),
6044
+ runtime,
6045
+ abortSignal,
6046
+ turnAdoptionLifecycle
6047
+ });
5168
6048
  });
5169
6049
  };
6050
+ return async (data) => {
6051
+ const ingressLifecycle = params.resolveIngressLifecycle?.(data);
6052
+ if (!ingressLifecycle) {
6053
+ await runFeishuHandler(async () => await handleNotice(data));
6054
+ return;
6055
+ }
6056
+ const { lifecycle, settle } = buildFeishuFlushIngressLifecycle([{ lifecycle: ingressLifecycle }]);
6057
+ await handleNotice(data, lifecycle);
6058
+ await settle();
6059
+ };
5170
6060
  }
5171
6061
  //#endregion
5172
6062
  //#region extensions/feishu/src/monitor.message-handler.ts
@@ -5227,7 +6117,7 @@ function resolveFeishuDebounceMentions(params) {
5227
6117
  const botMentions = merged.filter((mention) => mention.id.open_id?.trim() === normalizedBotOpenId);
5228
6118
  return botMentions.length > 0 ? botMentions : void 0;
5229
6119
  }
5230
- function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, runtime, chatHistories, fireAndForget, handleMessage, resolveDebounceText: resolveText, hasProcessedMessage, getBotOpenId = () => void 0, getBotName = () => void 0, resolveSequentialKey = ({ accountId: accountIdLocal, event }) => `feishu:${accountIdLocal}:${event.message.chat_id?.trim() || "unknown"}`, statusSink }) {
6120
+ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, runtime, chatHistories, fireAndForget, handleMessage, resolveDebounceText: resolveText, hasProcessedMessage, getBotOpenId = () => void 0, getBotName = () => void 0, resolveSequentialKey = ({ accountId: accountIdLocal, event }) => `feishu:${accountIdLocal}:${event.message.chat_id?.trim() || "unknown"}`, statusSink, resolveIngressLifecycle }) {
5231
6121
  const inboundDebounceMs = channelRuntime.debounce.resolveInboundDebounceMs({
5232
6122
  cfg,
5233
6123
  channel: "feishu"
@@ -5237,25 +6127,32 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5237
6127
  const enqueue = createSequentialQueue({ onTaskTimeout: (key, timeoutMs) => {
5238
6128
  log(`feishu[${accountId}]: per-chat task exceeded ${timeoutMs}ms cap (key=${key}); evicting from queue so later same-key messages can proceed (#70133)`);
5239
6129
  } });
5240
- const dispatchFeishuMessage = async (event, messageDedupeKey, processingClaim) => {
6130
+ const dispatchFeishuMessage = async (event, messageDedupeKey, processingClaim, turnAdoptionLifecycle) => {
5241
6131
  const sequentialKey = resolveSequentialKey({
5242
6132
  accountId,
5243
6133
  event,
5244
6134
  botOpenId: getBotOpenId(accountId),
5245
6135
  botName: getBotName(accountId)
5246
6136
  });
5247
- const task = () => handleMessage({
5248
- cfg,
5249
- event,
5250
- botOpenId: getBotOpenId(accountId),
5251
- botName: getBotName(accountId),
5252
- runtime,
5253
- channelRuntime,
5254
- chatHistories,
5255
- accountId,
5256
- processingClaim,
5257
- messageDedupeKey
5258
- });
6137
+ const task = async () => {
6138
+ if (turnAdoptionLifecycle?.abortSignal.aborted) {
6139
+ await turnAdoptionLifecycle.onAbandoned();
6140
+ return;
6141
+ }
6142
+ await handleMessage({
6143
+ cfg,
6144
+ event,
6145
+ botOpenId: getBotOpenId(accountId),
6146
+ botName: getBotName(accountId),
6147
+ runtime,
6148
+ channelRuntime,
6149
+ chatHistories,
6150
+ accountId,
6151
+ processingClaim,
6152
+ messageDedupeKey,
6153
+ turnAdoptionLifecycle
6154
+ });
6155
+ };
5259
6156
  await enqueue(sequentialKey, task);
5260
6157
  };
5261
6158
  const resolveSenderDebounceId = (event) => {
@@ -5295,45 +6192,80 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5295
6192
  return Boolean(text) && !channelRuntime.commands.isControlCommandMessage(text, cfg);
5296
6193
  },
5297
6194
  onFlush: async (entries) => {
5298
- const last = entries.at(-1);
6195
+ const activeEntries = entries.filter((entry) => !entry.abandoned);
6196
+ const last = activeEntries.at(-1);
5299
6197
  if (!last) return;
5300
- if (entries.length === 1) {
5301
- await dispatchFeishuMessage(last.event, resolveFeishuMessageDedupeKey(last.event), last.processingClaim);
6198
+ const { lifecycle, settle } = buildFeishuFlushIngressLifecycle(activeEntries.map((entry) => ({
6199
+ lifecycle: entry.turnAdoptionLifecycle,
6200
+ replayClaim: entry.processingClaim
6201
+ })), { onReplayCommitError: (err) => error(`feishu[${accountId}]: failed to commit logical replay guard: ${String(err)}`) });
6202
+ if (lifecycle?.abortSignal.aborted) {
6203
+ await lifecycle.onAbandoned();
5302
6204
  return;
5303
6205
  }
5304
- const dedupedEntries = dedupeFeishuDebounceEntriesByDedupeKey(entries);
5305
- const freshEntries = [];
5306
- for (const entry of dedupedEntries) if (!await hasProcessedMessage(resolveFeishuMessageDedupeKey(entry.event), accountId, log)) freshEntries.push(entry);
5307
- const dispatchEntry = freshEntries.at(-1);
5308
- if (!dispatchEntry) return;
5309
- const dispatchDedupeKey = resolveFeishuMessageDedupeKey(dispatchEntry.event);
5310
- await recordSuppressedMessageIds(dedupedEntries, dispatchDedupeKey);
5311
- const combinedText = freshEntries.map((entry) => resolveDebounceText(entry.event)).filter(Boolean).join("\n");
5312
- const mergedMentions = resolveFeishuDebounceMentions({
5313
- entries: freshEntries.map((entry) => entry.event),
5314
- botOpenId: getBotOpenId(accountId)
5315
- });
5316
- await dispatchFeishuMessage({
5317
- ...dispatchEntry.event,
5318
- message: {
5319
- ...dispatchEntry.event.message,
5320
- ...combinedText.trim() ? {
5321
- message_type: "text",
5322
- content: JSON.stringify({ text: combinedText })
5323
- } : {},
5324
- mentions: mergedMentions ?? dispatchEntry.event.message.mentions
6206
+ try {
6207
+ if (activeEntries.length === 1) {
6208
+ await dispatchFeishuMessage(last.event, resolveFeishuMessageDedupeKey(last.event), last.processingClaim, lifecycle);
6209
+ await settle();
6210
+ return;
6211
+ }
6212
+ const dedupedEntries = dedupeFeishuDebounceEntriesByDedupeKey(activeEntries);
6213
+ const freshEntries = [];
6214
+ for (const entry of dedupedEntries) if (!await hasProcessedMessage(resolveFeishuMessageDedupeKey(entry.event), accountId, log)) freshEntries.push(entry);
6215
+ const dispatchEntry = freshEntries.at(-1);
6216
+ if (!dispatchEntry) {
6217
+ await settle();
6218
+ return;
5325
6219
  }
5326
- }, dispatchDedupeKey, dispatchEntry.processingClaim);
6220
+ const dispatchDedupeKey = resolveFeishuMessageDedupeKey(dispatchEntry.event);
6221
+ if (!lifecycle) await recordSuppressedMessageIds(dedupedEntries, dispatchDedupeKey);
6222
+ const combinedText = freshEntries.map((entry) => resolveDebounceText(entry.event)).filter(Boolean).join("\n");
6223
+ const mergedMentions = resolveFeishuDebounceMentions({
6224
+ entries: freshEntries.map((entry) => entry.event),
6225
+ botOpenId: getBotOpenId(accountId)
6226
+ });
6227
+ await dispatchFeishuMessage({
6228
+ ...dispatchEntry.event,
6229
+ message: {
6230
+ ...dispatchEntry.event.message,
6231
+ ...combinedText.trim() ? {
6232
+ message_type: "text",
6233
+ content: JSON.stringify({ text: combinedText })
6234
+ } : {},
6235
+ mentions: mergedMentions ?? dispatchEntry.event.message.mentions
6236
+ }
6237
+ }, dispatchDedupeKey, dispatchEntry.processingClaim, lifecycle);
6238
+ await settle();
6239
+ } catch (err) {
6240
+ await lifecycle?.onAbandoned();
6241
+ throw err;
6242
+ }
5327
6243
  },
5328
6244
  onError: (err, entries) => {
5329
- for (const entry of entries) entry.processingClaim?.release({ error: err });
6245
+ for (const entry of entries) {
6246
+ entry.processingClaim?.release({ error: err });
6247
+ try {
6248
+ Promise.resolve(entry.turnAdoptionLifecycle?.onAbandoned()).catch((abandonError) => {
6249
+ error(`feishu[${accountId}]: failed to abandon durable ingress after debounce error: ${String(abandonError)}`);
6250
+ });
6251
+ } catch (abandonError) {
6252
+ error(`feishu[${accountId}]: failed to abandon durable ingress after debounce error: ${String(abandonError)}`);
6253
+ }
6254
+ }
5330
6255
  error(`feishu[${accountId}]: inbound debounce flush failed: ${String(err)}`);
5331
6256
  }
5332
6257
  });
5333
6258
  return async (data) => {
6259
+ const turnAdoptionLifecycle = resolveIngressLifecycle?.(data);
6260
+ const completeSuppressedIngress = async () => {
6261
+ if (!turnAdoptionLifecycle) return;
6262
+ turnAdoptionLifecycle.onAdoptionFinalizing();
6263
+ await turnAdoptionLifecycle.onAdopted();
6264
+ };
5334
6265
  statusSink?.({ lastEventAt: Date.now() });
5335
6266
  const event = parseFeishuMessageEventPayload(data);
5336
6267
  if (!event) {
6268
+ if (turnAdoptionLifecycle) throw new FeishuIngressPermanentError("invalid-event", "Feishu durable message event payload is malformed.");
5337
6269
  error(`feishu[${accountId}]: ignoring malformed message event payload`);
5338
6270
  return;
5339
6271
  }
@@ -5342,6 +6274,7 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5342
6274
  const senderOpenId = event.sender.sender_id.open_id?.trim();
5343
6275
  if (botOpenId && senderOpenId === botOpenId) {
5344
6276
  log(`feishu[${accountId}]: dropping self-authored message ${messageId ?? "unknown"}`);
6277
+ await completeSuppressedIngress();
5345
6278
  return;
5346
6279
  }
5347
6280
  const claim = await claimUnprocessedFeishuMessage({
@@ -5351,14 +6284,31 @@ function createFeishuMessageReceiveHandler({ cfg, channelRuntime, accountId, run
5351
6284
  });
5352
6285
  if (claim.kind === "duplicate" || claim.kind === "inflight") {
5353
6286
  log(`feishu[${accountId}]: dropping ${claim.kind} event for message ${messageId}`);
6287
+ await completeSuppressedIngress();
5354
6288
  return;
5355
6289
  }
6290
+ const debounceEntry = {
6291
+ event,
6292
+ ...claim.kind === "claimed" ? { processingClaim: claim.handle } : {},
6293
+ ...turnAdoptionLifecycle ? { turnAdoptionLifecycle } : {}
6294
+ };
6295
+ if (claim.kind === "claimed" && turnAdoptionLifecycle) turnAdoptionLifecycle.registerAbandonHandler?.(() => {
6296
+ debounceEntry.abandoned = true;
6297
+ claim.handle.release({ error: /* @__PURE__ */ new Error("feishu-ingress-abandoned-before-flush") });
6298
+ });
5356
6299
  const processMessage = async () => {
5357
- await inboundDebouncer.enqueue({
5358
- event,
5359
- ...claim.kind === "claimed" ? { processingClaim: claim.handle } : {}
5360
- });
6300
+ await inboundDebouncer.enqueue(debounceEntry);
5361
6301
  };
6302
+ if (turnAdoptionLifecycle) try {
6303
+ await processMessage();
6304
+ return { kind: "deferred" };
6305
+ } catch (err) {
6306
+ if (claim.kind === "claimed") claim.handle.release({ error: err });
6307
+ return {
6308
+ kind: "failed-retryable",
6309
+ error: err
6310
+ };
6311
+ }
5362
6312
  if (fireAndForget) {
5363
6313
  processMessage().catch((err) => {
5364
6314
  if (claim.kind === "claimed") claim.handle.release({ error: err });
@@ -5468,7 +6418,7 @@ function waitForFeishuWsCycleEnd(params) {
5468
6418
  params.terminalError.then(finish);
5469
6419
  });
5470
6420
  }
5471
- async function monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher, statusSink }) {
6421
+ async function monitorWebSocket({ account, accountId, runtime, abortSignal, eventDispatcher, setSocketTerminator, statusSink }) {
5472
6422
  const log = runtime?.log ?? console.log;
5473
6423
  const error = runtime?.error ?? console.error;
5474
6424
  let attempt = 0;
@@ -5509,6 +6459,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5509
6459
  onReconnected: publishWsConnected,
5510
6460
  onReconnecting: publishWsReconnecting
5511
6461
  });
6462
+ setSocketTerminator?.(() => wsClient?.close({ force: true }));
5512
6463
  if (abortSignal?.aborted) {
5513
6464
  cleanupFeishuWsClient({
5514
6465
  accountId,
@@ -5534,6 +6485,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5534
6485
  error,
5535
6486
  clearIdentity: true
5536
6487
  });
6488
+ setSocketTerminator?.(void 0);
5537
6489
  return;
5538
6490
  }
5539
6491
  cleanupFeishuWsClient({
@@ -5542,6 +6494,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5542
6494
  error,
5543
6495
  clearIdentity: false
5544
6496
  });
6497
+ setSocketTerminator?.(void 0);
5545
6498
  if (abortSignal?.aborted) break;
5546
6499
  statusSink?.({
5547
6500
  connected: false,
@@ -5558,6 +6511,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5558
6511
  error,
5559
6512
  clearIdentity: false
5560
6513
  });
6514
+ setSocketTerminator?.(void 0);
5561
6515
  if (abortSignal?.aborted) break;
5562
6516
  statusSink?.({
5563
6517
  connected: false,
@@ -5575,6 +6529,7 @@ async function monitorWebSocket({ account, accountId, runtime, abortSignal, even
5575
6529
  error,
5576
6530
  clearIdentity: true
5577
6531
  });
6532
+ setSocketTerminator?.(void 0);
5578
6533
  }
5579
6534
  async function monitorWebhook({ account, accountId, runtime, abortSignal, eventDispatcher, statusSink }) {
5580
6535
  const log = runtime?.log ?? console.log;
@@ -5882,41 +6837,41 @@ function normalizeFeishuChatType(value) {
5882
6837
  return value === "group" || value === "topic_group" || value === "private" || value === "p2p" ? value : void 0;
5883
6838
  }
5884
6839
  function parseFeishuBotAddedEventPayload(value) {
5885
- if (!isRecord$1(value) || !readString(value.chat_id) || !isRecord$1(value.operator_id)) return null;
6840
+ if (!isRecord$2(value) || !readString$1(value.chat_id) || !isRecord$2(value.operator_id)) return null;
5886
6841
  return value;
5887
6842
  }
5888
6843
  function parseFeishuBotRemovedChatId(value) {
5889
- if (!isRecord$1(value)) return null;
5890
- return readString(value.chat_id) ?? null;
6844
+ if (!isRecord$2(value)) return null;
6845
+ return readString$1(value.chat_id) ?? null;
5891
6846
  }
5892
6847
  function firstString(...values) {
5893
6848
  for (const value of values) {
5894
- const trimmed = readString(value)?.trim();
6849
+ const trimmed = readString$1(value)?.trim();
5895
6850
  if (trimmed) return trimmed;
5896
6851
  }
5897
6852
  }
5898
6853
  function readFeishuIdentityField(value, field) {
5899
- if (!isRecord$1(value)) return;
6854
+ if (!isRecord$2(value)) return;
5900
6855
  return firstString(value[field]);
5901
6856
  }
5902
6857
  function parseFeishuCardActionEventPayload(value) {
5903
- if (!isRecord$1(value)) return null;
5904
- const operator = isRecord$1(value.operator) ? value.operator : {};
6858
+ if (!isRecord$2(value)) return null;
6859
+ const operator = isRecord$2(value.operator) ? value.operator : {};
5905
6860
  const action = value.action;
5906
- const context = isRecord$1(value.context) ? value.context : {};
5907
- if (!isRecord$1(action)) return null;
6861
+ const context = isRecord$2(value.context) ? value.context : {};
6862
+ if (!isRecord$2(action)) return null;
5908
6863
  const operatorUserId = operator.user_id;
5909
- const token = readString(value.token);
6864
+ const token = readString$1(value.token);
5910
6865
  const openId = firstString(operator.open_id, readFeishuIdentityField(operatorUserId, "open_id"), value.open_id, context.open_id);
5911
6866
  const userId = firstString(operator.user_id, readFeishuIdentityField(operatorUserId, "user_id"), value.user_id, context.user_id);
5912
6867
  const unionId = firstString(operator.union_id, readFeishuIdentityField(operatorUserId, "union_id"));
5913
- const tag = readString(action.tag);
6868
+ const tag = readString$1(action.tag);
5914
6869
  const actionValue = action.value;
5915
6870
  const openMessageId = firstString(context.open_message_id, value.open_message_id);
5916
6871
  const contextOpenId = firstString(context.open_id, openId);
5917
6872
  const contextUserId = firstString(context.user_id, userId);
5918
6873
  const chatId = firstString(context.chat_id, context.open_chat_id);
5919
- if (!token || !openId || !tag || !isRecord$1(actionValue)) return null;
6874
+ if (!token || !openId || !tag || !isRecord$2(actionValue)) return null;
5920
6875
  return {
5921
6876
  operator: {
5922
6877
  open_id: openId,
@@ -5968,6 +6923,7 @@ function registerEventHandlers(eventDispatcher, context) {
5968
6923
  getBotOpenId: (id) => botOpenIds.get(id),
5969
6924
  getBotName: (id) => botNames.get(id),
5970
6925
  resolveSequentialKey: getFeishuSequentialKey,
6926
+ resolveIngressLifecycle: context.resolveIngressLifecycle,
5971
6927
  ...context.statusSink ? { statusSink: context.statusSink } : {}
5972
6928
  }),
5973
6929
  "im.message.message_read_v1": async () => {},
@@ -5995,7 +6951,8 @@ function registerEventHandlers(eventDispatcher, context) {
5995
6951
  accountId,
5996
6952
  runtime,
5997
6953
  fireAndForget,
5998
- abortSignal
6954
+ abortSignal,
6955
+ resolveIngressLifecycle: context.resolveIngressLifecycle
5999
6956
  }),
6000
6957
  "vc.bot.meeting_invited_v1": createFeishuVcMeetingInvitedHandler({
6001
6958
  cfg,
@@ -6098,19 +7055,22 @@ async function monitorSingleAccount(params) {
6098
7055
  const { accountId } = account;
6099
7056
  const log = runtime?.log ?? console.log;
6100
7057
  const botOpenIdSource = params.botOpenIdSource ?? { kind: "fetch" };
6101
- const { botOpenId } = applyBotIdentityState(accountId, botOpenIdSource.kind === "prefetched" ? {
7058
+ const botIdentity = botOpenIdSource.kind === "prefetched" ? {
6102
7059
  botOpenId: botOpenIdSource.botOpenId,
6103
- botName: botOpenIdSource.botName
7060
+ botName: botOpenIdSource.botName,
7061
+ source: botOpenIdSource.source
6104
7062
  } : await fetchBotIdentityForMonitor(account, {
6105
7063
  runtime,
6106
7064
  abortSignal
6107
- }));
7065
+ });
7066
+ const { botOpenId } = applyBotIdentityState(accountId, botIdentity);
6108
7067
  log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
6109
- if (!botOpenId && !abortSignal?.aborted) startBotIdentityRecovery({
7068
+ if ((!botOpenId || botIdentity.source === "cache") && !abortSignal?.aborted) startBotIdentityRecovery({
6110
7069
  account,
6111
7070
  accountId,
6112
7071
  runtime,
6113
- abortSignal
7072
+ abortSignal,
7073
+ currentSource: botIdentity.source
6114
7074
  });
6115
7075
  const connectionMode = account.config.connectionMode ?? "websocket";
6116
7076
  if (connectionMode === "webhook" && !account.verificationToken?.trim()) throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`);
@@ -6120,6 +7080,13 @@ async function monitorSingleAccount(params) {
6120
7080
  let threadBindingManager;
6121
7081
  try {
6122
7082
  const eventDispatcher = createEventDispatcher(account);
7083
+ const durableIngress = typeof eventDispatcher.invoke === "function" ? createFeishuDurableIngress({
7084
+ accountId,
7085
+ dispatcher: eventDispatcher,
7086
+ ...account.encryptKey ? { encryptKey: account.encryptKey } : {},
7087
+ runtime: runtime ?? {}
7088
+ }) : void 0;
7089
+ const durableEventDispatcher = durableIngress ? Object.assign(Object.create(eventDispatcher), { invoke: durableIngress.invoke }) : eventDispatcher;
6123
7090
  const chatHistories = /* @__PURE__ */ new Map();
6124
7091
  threadBindingManager = createFeishuThreadBindingManager({
6125
7092
  accountId,
@@ -6134,24 +7101,31 @@ async function monitorSingleAccount(params) {
6134
7101
  fireAndForget: params.fireAndForget ?? true,
6135
7102
  vcAutoJoin: account.config.vcAutoJoin === true,
6136
7103
  abortSignal,
7104
+ ...durableIngress ? { resolveIngressLifecycle: durableIngress.resolveLifecycle } : {},
6137
7105
  ...params.statusSink ? { statusSink: params.statusSink } : {}
6138
7106
  });
6139
- if (connectionMode === "webhook") return await monitorWebhook({
6140
- account,
6141
- accountId,
6142
- runtime,
6143
- abortSignal,
6144
- eventDispatcher,
6145
- ...params.statusSink ? { statusSink: params.statusSink } : {}
6146
- });
6147
- return await monitorWebSocket({
6148
- account,
6149
- accountId,
6150
- runtime,
6151
- abortSignal,
6152
- eventDispatcher,
6153
- ...params.statusSink ? { statusSink: params.statusSink } : {}
6154
- });
7107
+ durableIngress?.start();
7108
+ try {
7109
+ if (connectionMode === "webhook") return await monitorWebhook({
7110
+ account,
7111
+ accountId,
7112
+ runtime,
7113
+ abortSignal,
7114
+ eventDispatcher: durableEventDispatcher,
7115
+ ...params.statusSink ? { statusSink: params.statusSink } : {}
7116
+ });
7117
+ return await monitorWebSocket({
7118
+ account,
7119
+ accountId,
7120
+ runtime,
7121
+ abortSignal,
7122
+ eventDispatcher: durableEventDispatcher,
7123
+ ...durableIngress ? { setSocketTerminator: durableIngress.setSocketTerminator } : {},
7124
+ ...params.statusSink ? { statusSink: params.statusSink } : {}
7125
+ });
7126
+ } finally {
7127
+ await durableIngress?.stop();
7128
+ }
6155
7129
  } finally {
6156
7130
  threadBindingManager?.stop();
6157
7131
  }