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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,28 @@
1
- const require_rolldown_runtime = require("./rolldown-runtime-D6vf50IK.cjs");
1
+ const require_delegated_state = require("./delegated-state-DOSCHaAB.cjs");
2
2
  const require_runtime = require("./runtime-gZTAuFux.cjs");
3
3
  require("./runtime-api.cjs");
4
- const require_resolve_allowlist = require("./resolve-allowlist-DXd097EP.cjs");
5
- const require_channel = require("./channel-BKc8zroK.cjs");
6
- const require_setup_surface = require("./setup-surface-D4f6x5cx.cjs");
7
- const require_polls = require("./polls-BhoI4VaN.cjs");
8
- const require_sso_token_store = require("./sso-token-store-CktCz88k.cjs");
9
- const require_messenger = require("./messenger-BVcKl6dv.cjs");
4
+ const require_resolve_allowlist = require("./resolve-allowlist-Dk-1CqBm.cjs");
5
+ const require_channel = require("./channel-oNBYNcJT.cjs");
6
+ const require_setup_surface = require("./setup-surface-BcRVoUY8.cjs");
7
+ const require_polls = require("./polls-CWExq2xZ.cjs");
8
+ const require_sso_token_store = require("./sso-token-store-OSg229re.cjs");
9
+ const require_messenger = require("./messenger-Do4oje4J.cjs");
10
10
  let openclaw_plugin_sdk_allow_from = require("openclaw/plugin-sdk/allow-from");
11
11
  let openclaw_plugin_sdk_channel_outbound = require("openclaw/plugin-sdk/channel-outbound");
12
12
  let openclaw_plugin_sdk_string_coerce_runtime = require("openclaw/plugin-sdk/string-coerce-runtime");
13
13
  let openclaw_plugin_sdk_media_runtime = require("openclaw/plugin-sdk/media-runtime");
14
- let openclaw_plugin_sdk_channel_inbound = require("openclaw/plugin-sdk/channel-inbound");
15
14
  let openclaw_plugin_sdk_ssrf_runtime = require("openclaw/plugin-sdk/ssrf-runtime");
16
15
  let openclaw_plugin_sdk_provider_http = require("openclaw/plugin-sdk/provider-http");
17
16
  let openclaw_plugin_sdk_number_runtime = require("openclaw/plugin-sdk/number-runtime");
18
17
  let openclaw_plugin_sdk_text_utility_runtime = require("openclaw/plugin-sdk/text-utility-runtime");
19
- let node_path = require("node:path");
20
- node_path = require_rolldown_runtime.__toESM(node_path, 1);
21
- let openclaw_plugin_sdk_security_runtime = require("openclaw/plugin-sdk/security-runtime");
22
18
  let openclaw_plugin_sdk_routing = require("openclaw/plugin-sdk/routing");
23
19
  let node_crypto = require("node:crypto");
24
- node_crypto = require_rolldown_runtime.__toESM(node_crypto, 1);
20
+ node_crypto = require_delegated_state.__toESM(node_crypto, 1);
21
+ let openclaw_plugin_sdk_channel_inbound = require("openclaw/plugin-sdk/channel-inbound");
25
22
  let openclaw_plugin_sdk_channel_ingress_runtime = require("openclaw/plugin-sdk/channel-ingress-runtime");
26
23
  let openclaw_plugin_sdk_context_visibility_runtime = require("openclaw/plugin-sdk/context-visibility-runtime");
27
24
  let openclaw_plugin_sdk_reply_history = require("openclaw/plugin-sdk/reply-history");
25
+ let openclaw_plugin_sdk_agent_runtime = require("openclaw/plugin-sdk/agent-runtime");
28
26
  let openclaw_plugin_sdk_dedupe_runtime = require("openclaw/plugin-sdk/dedupe-runtime");
29
27
  let openclaw_plugin_sdk_collection_runtime = require("openclaw/plugin-sdk/collection-runtime");
30
28
  let openclaw_plugin_sdk_channel_pairing = require("openclaw/plugin-sdk/channel-pairing");
@@ -33,100 +31,24 @@ let openclaw_plugin_sdk_runtime_group_policy = require("openclaw/plugin-sdk/runt
33
31
  let openclaw_plugin_sdk_dangerous_name_runtime = require("openclaw/plugin-sdk/dangerous-name-runtime");
34
32
  let openclaw_plugin_sdk_reply_payload = require("openclaw/plugin-sdk/reply-payload");
35
33
  let openclaw_plugin_sdk_webhook_ingress = require("openclaw/plugin-sdk/webhook-ingress");
36
- //#region extensions/msteams/src/feedback-reflection-prompt.ts
37
- /** Max chars of the thumbed-down response to include in the reflection prompt. */
38
- const MAX_RESPONSE_CHARS = 500;
39
- function buildReflectionPrompt(params) {
40
- const parts = ["A user indicated your previous response wasn't helpful."];
41
- if (params.thumbedDownResponse) {
42
- const truncated = params.thumbedDownResponse.length > MAX_RESPONSE_CHARS ? `${(0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(params.thumbedDownResponse, MAX_RESPONSE_CHARS)}...` : params.thumbedDownResponse;
43
- parts.push(`\nYour response was:\n> ${truncated}`);
44
- }
45
- if (params.userComment) parts.push(`\nUser's comment: "${params.userComment}"`);
46
- parts.push("\nBriefly reflect: what could you improve? Consider tone, length, accuracy, relevance, and specificity. Reply with a single JSON object only, no markdown or prose, using this exact shape:\n{\"learning\":\"...\",\"followUp\":false,\"userMessage\":\"\"}\n- learning: a short internal adjustment note (1-2 sentences) for your future behavior in this conversation.\n- followUp: true only if the user needs a direct follow-up message.\n- userMessage: only the exact user-facing message to send; empty string when followUp is false.");
47
- return parts.join("\n");
48
- }
49
- function parseBooleanLike(value) {
50
- if (typeof value === "boolean") return value;
51
- if (typeof value === "string") {
52
- const normalized = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(value);
53
- if (normalized === "true" || normalized === "yes") return true;
54
- if (normalized === "false" || normalized === "no") return false;
55
- }
56
- }
57
- function parseStructuredReflectionValue(value) {
58
- if (value == null || typeof value !== "object" || Array.isArray(value)) return null;
59
- const candidate = value;
60
- const learning = typeof candidate.learning === "string" ? candidate.learning.trim() : void 0;
61
- if (!learning) return null;
62
- return {
63
- learning,
64
- followUp: parseBooleanLike(candidate.followUp) ?? false,
65
- userMessage: typeof candidate.userMessage === "string" && candidate.userMessage.trim() ? candidate.userMessage.trim() : void 0
66
- };
67
- }
68
- function parseReflectionResponse(text) {
69
- const trimmed = text.trim();
70
- if (!trimmed) return null;
71
- const candidates = [trimmed, ...trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i)?.slice(1, 2) ?? []];
72
- for (const candidateText of candidates) {
73
- const candidate = candidateText.trim();
74
- if (!candidate) continue;
75
- try {
76
- const parsed = parseStructuredReflectionValue(JSON.parse(candidate));
77
- if (parsed) return parsed;
78
- } catch {}
79
- }
80
- return {
81
- learning: trimmed,
82
- followUp: false
83
- };
84
- }
85
- /** Tracks last reflection time per session to enforce cooldown. */
86
- const lastReflectionBySession = /* @__PURE__ */ new Map();
87
- /** Maximum cooldown entries before pruning expired ones. */
88
- const MAX_COOLDOWN_ENTRIES = 500;
34
+ //#region extensions/msteams/src/feedback-reflection-store.ts
89
35
  const LEARNINGS_NAMESPACE = "feedback-learnings";
90
36
  const MAX_LEARNING_ENTRIES = 1e4;
91
37
  function learningStoreKey(storePath, sessionKey) {
92
38
  return node_crypto.default.createHash("sha256").update(`${storePath}\0${sessionKey}`, "utf8").digest("hex");
93
39
  }
94
- function openLearningStore() {
95
- return require_runtime.getMSTeamsRuntime().state.openKeyedStore({
40
+ async function storeSessionLearning(params) {
41
+ const store = require_runtime.getMSTeamsRuntime().state.openKeyedStore({
96
42
  namespace: LEARNINGS_NAMESPACE,
97
43
  maxEntries: MAX_LEARNING_ENTRIES
98
44
  });
99
- }
100
- /** Prune expired cooldown entries to prevent unbounded memory growth. */
101
- function pruneExpiredCooldowns(cooldownMs) {
102
- if (lastReflectionBySession.size <= MAX_COOLDOWN_ENTRIES) return;
103
- const now = Date.now();
104
- for (const [key, time] of lastReflectionBySession) if (now - time >= cooldownMs) lastReflectionBySession.delete(key);
105
- }
106
- /** Check if a reflection is allowed (cooldown not active). */
107
- function isReflectionAllowed(sessionKey, cooldownMs) {
108
- const cooldown = cooldownMs ?? 3e5;
109
- const lastTime = lastReflectionBySession.get(sessionKey);
110
- if (lastTime == null) return true;
111
- return Date.now() - lastTime >= cooldown;
112
- }
113
- /** Record that a reflection was run for a session. */
114
- function recordReflectionTime(sessionKey, cooldownMs) {
115
- lastReflectionBySession.set(sessionKey, Date.now());
116
- pruneExpiredCooldowns(cooldownMs ?? 3e5);
117
- }
118
- /** Store a learning derived from feedback reflection. */
119
- async function storeSessionLearning(params) {
120
- const store = openLearningStore();
121
45
  const key = learningStoreKey(params.storePath, params.sessionKey);
122
- let learnings = (await store.lookup(key))?.learnings ?? [];
123
- learnings.push(params.learning);
124
- if (learnings.length > 10) learnings = learnings.slice(-10);
125
- await store.register(key, {
46
+ if (!store.update) throw new Error("plugin state atomic update is unavailable");
47
+ await store.update(key, (existing) => ({
126
48
  sessionKey: params.sessionKey,
127
- learnings,
49
+ learnings: [...existing?.learnings ?? [], params.learning].slice(-10),
128
50
  updatedAt: Date.now()
129
- });
51
+ }));
130
52
  }
131
53
  //#endregion
132
54
  //#region extensions/msteams/src/feedback-reflection.ts
@@ -143,144 +65,68 @@ function buildFeedbackEvent(params) {
143
65
  conversationId: params.conversationId
144
66
  };
145
67
  }
146
- function buildReflectionContext(params) {
147
- const core = require_runtime.getMSTeamsRuntime();
148
- const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(params.cfg);
149
- const body = core.channel.reply.formatAgentEnvelope({
150
- channel: "Teams",
151
- from: "system",
152
- body: params.reflectionPrompt,
153
- envelope: envelopeOptions
154
- });
155
- return { ctxPayload: core.channel.reply.finalizeInboundContext({
156
- Body: body,
157
- BodyForAgent: params.reflectionPrompt,
158
- RawBody: params.reflectionPrompt,
159
- CommandBody: params.reflectionPrompt,
160
- From: `msteams:system:${params.conversationId}`,
161
- To: `conversation:${params.conversationId}`,
162
- SessionKey: params.sessionKey,
163
- ChatType: "direct",
164
- SenderName: "system",
165
- SenderId: "system",
166
- Provider: "msteams",
167
- Surface: "msteams",
168
- Timestamp: Date.now(),
169
- WasMentioned: true,
170
- CommandAuthorized: false,
171
- OriginatingChannel: "msteams",
172
- OriginatingTo: `conversation:${params.conversationId}`
173
- }) };
174
- }
175
- function createReflectionCaptureDispatcher(params) {
176
- const core = require_runtime.getMSTeamsRuntime();
177
- let response = "";
178
- const { dispatcher, replyOptions } = core.channel.reply.createReplyDispatcherWithTyping({
179
- deliver: async (payload) => {
180
- if (payload.text) response += (response ? "\n" : "") + payload.text;
181
- },
182
- typingCallbacks: {
183
- onReplyStart: async () => {},
184
- onIdle: () => {},
185
- onCleanup: () => {}
186
- },
187
- humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
188
- onError: (err) => {
189
- params.log.debug?.("reflection reply error", { error: require_setup_surface.formatUnknownError(err) });
190
- }
191
- });
192
- return {
193
- dispatcher,
194
- replyOptions,
195
- readResponse: () => response
196
- };
197
- }
198
- async function sendReflectionFollowUp(params) {
199
- const baseRef = require_messenger.buildConversationReference(params.conversationRef);
200
- await require_messenger.sendMSTeamsActivityWithReference(params.app, baseRef, {
201
- type: "message",
202
- text: params.userMessage
203
- }, { serviceUrlBoundary: require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(params.cfg.channels?.msteams) });
204
- }
205
68
  /**
206
69
  * Run a background reflection after negative feedback.
207
70
  * This is designed to be called fire-and-forget (don't await in the invoke handler).
208
71
  */
209
72
  async function runFeedbackReflection(params) {
210
73
  const { cfg, log, sessionKey } = params;
211
- const cooldownMs = cfg.channels?.msteams?.feedbackReflectionCooldownMs ?? 3e5;
212
- if (!isReflectionAllowed(sessionKey, cooldownMs)) {
213
- log.debug?.("skipping reflection (cooldown active)", { sessionKey });
214
- return;
215
- }
216
- const reflectionPrompt = buildReflectionPrompt({
217
- thumbedDownResponse: params.thumbedDownResponse,
218
- userComment: params.userComment
219
- });
220
- const storePath = require_runtime.getMSTeamsRuntime().channel.session.resolveStorePath(cfg.session?.store, { agentId: params.agentId });
221
- const { ctxPayload } = buildReflectionContext({
222
- cfg,
223
- conversationId: params.conversationId,
224
- sessionKey: params.sessionKey,
225
- reflectionPrompt
226
- });
227
- const capture = createReflectionCaptureDispatcher({
228
- cfg,
229
- agentId: params.agentId,
230
- log
231
- });
74
+ const cooldownMs = cfg.channels?.msteams?.feedbackReflectionCooldownMs ?? openclaw_plugin_sdk_channel_inbound.DEFAULT_CHANNEL_FEEDBACK_REFLECTION_COOLDOWN_MS;
75
+ let reflection;
232
76
  try {
233
- await (0, openclaw_plugin_sdk_channel_inbound.dispatchReplyFromConfigWithSettledDispatcher)({
234
- ctxPayload,
77
+ reflection = await (0, openclaw_plugin_sdk_channel_inbound.runChannelFeedbackReflection)({
235
78
  cfg,
236
- dispatcher: capture.dispatcher,
237
- onSettled: () => {},
238
- replyOptions: capture.replyOptions
79
+ channel: "msteams",
80
+ channelLabel: "Teams",
81
+ agentId: params.agentId,
82
+ sessionKey,
83
+ conversationId: params.conversationId,
84
+ conversationKind: params.conversationKind,
85
+ thumbedDownResponse: params.thumbedDownResponse,
86
+ userComment: params.userComment,
87
+ cooldownMs,
88
+ onRecordError: (err) => log.debug?.("reflection session record failed", { error: require_setup_surface.formatUnknownError(err) }),
89
+ onDispatchError: (err) => log.debug?.("reflection reply error", { error: require_setup_surface.formatUnknownError(err) })
239
90
  });
240
91
  } catch (err) {
241
92
  log.error("reflection dispatch failed", { error: require_setup_surface.formatUnknownError(err) });
242
93
  return;
243
94
  }
244
- const reflectionResponse = capture.readResponse().trim();
245
- if (!reflectionResponse) {
246
- log.debug?.("reflection produced no output");
95
+ if (reflection.status === "cooldown") {
96
+ log.debug?.("skipping reflection (cooldown active)", { sessionKey });
247
97
  return;
248
98
  }
249
- const parsedReflection = parseReflectionResponse(reflectionResponse);
250
- if (!parsedReflection) {
251
- log.debug?.("reflection produced no structured output");
99
+ if (reflection.status === "empty") {
100
+ log.debug?.("reflection produced no output");
252
101
  return;
253
102
  }
254
- recordReflectionTime(sessionKey, cooldownMs);
255
103
  log.info("reflection complete", {
256
104
  sessionKey,
257
- responseLength: reflectionResponse.length,
258
- followUp: parsedReflection.followUp
105
+ responseLength: reflection.responseLength,
106
+ followUp: reflection.followUp
259
107
  });
260
108
  try {
261
109
  await storeSessionLearning({
262
- storePath,
263
- sessionKey: params.sessionKey,
264
- learning: parsedReflection.learning
110
+ storePath: reflection.storePath,
111
+ sessionKey,
112
+ learning: reflection.learning
265
113
  });
266
114
  } catch (err) {
267
115
  log.debug?.("failed to store reflection learning", { error: require_setup_surface.formatUnknownError(err) });
268
116
  }
269
117
  const conversationType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationRef.conversation?.conversationType);
270
- if (!(conversationType === "personal" && parsedReflection.followUp && Boolean(parsedReflection.userMessage))) {
271
- if (parsedReflection.followUp && conversationType !== "personal") log.debug?.("skipping reflection follow-up outside direct message", {
118
+ if (!(conversationType === "personal" && reflection.followUp && Boolean(reflection.userMessage))) {
119
+ if (reflection.followUp && conversationType !== "personal") log.debug?.("skipping reflection follow-up outside direct message", {
272
120
  sessionKey,
273
121
  conversationType
274
122
  });
275
123
  return;
276
124
  }
277
125
  try {
278
- await sendReflectionFollowUp({
279
- cfg,
280
- app: params.app,
281
- conversationRef: params.conversationRef,
282
- userMessage: parsedReflection.userMessage
283
- });
126
+ await require_messenger.sendMSTeamsActivityWithReference(params.app, require_messenger.buildConversationReference(params.conversationRef), {
127
+ type: "message",
128
+ text: reflection.userMessage
129
+ }, { serviceUrlBoundary: require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(cfg.channels?.msteams) });
284
130
  log.info("sent reflection follow-up", { sessionKey });
285
131
  } catch (err) {
286
132
  log.debug?.("failed to send reflection follow-up", { error: require_setup_surface.formatUnknownError(err) });
@@ -1815,14 +1661,16 @@ function createMSTeamsReplyDispatcher(params) {
1815
1661
  }
1816
1662
  if (ids.length > 0) params.onSentMessageIds?.(ids);
1817
1663
  };
1818
- const { dispatcher, replyOptions, markDispatchIdle: baseMarkDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
1664
+ const dispatcherOptions = {
1819
1665
  ...replyPipeline,
1820
- humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
1666
+ humanDelay: (0, openclaw_plugin_sdk_agent_runtime.resolveHumanDelayConfig)(params.cfg, params.agentId),
1821
1667
  onReplyStart: async () => {
1822
1668
  await streamController.onReplyStart();
1823
1669
  if (typingIndicatorEnabled) await typingCallbacks?.onReplyStart?.();
1824
1670
  },
1825
- typingCallbacks,
1671
+ typingCallbacks
1672
+ };
1673
+ const delivery = {
1826
1674
  deliver: async (payload) => {
1827
1675
  const preparedPayload = streamController.preparePayload(payload);
1828
1676
  if (!preparedPayload) return;
@@ -1841,8 +1689,8 @@ function createMSTeamsReplyDispatcher(params) {
1841
1689
  hint
1842
1690
  });
1843
1691
  }
1844
- });
1845
- const markDispatchIdle = () => {
1692
+ };
1693
+ const settleDelivery = () => {
1846
1694
  return flushPendingMessages().catch((err) => {
1847
1695
  const errMsg = require_setup_surface.formatUnknownError(err);
1848
1696
  const classification = require_setup_surface.classifyMSTeamsSendError(err);
@@ -1861,8 +1709,6 @@ function createMSTeamsReplyDispatcher(params) {
1861
1709
  queueReplyPayload(fallbackPayload);
1862
1710
  await flushPendingMessages();
1863
1711
  }
1864
- }).finally(() => {
1865
- baseMarkDispatchIdle();
1866
1712
  });
1867
1713
  };
1868
1714
  const previewToolProgressEnabled = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingPreviewToolProgress)(msteamsCfg);
@@ -1913,7 +1759,7 @@ function createMSTeamsReplyDispatcher(params) {
1913
1759
  },
1914
1760
  onPlanUpdate: async (payload) => {
1915
1761
  if (payload?.phase !== "update") return;
1916
- await streamController.pushPlanProgress((0, openclaw_plugin_sdk_channel_outbound.normalizeAgentPlanSteps)(payload.planSteps), { explanation: typeof payload.explanation === "string" ? payload.explanation : void 0 });
1762
+ await streamController.pushPlanProgress((0, openclaw_plugin_sdk_channel_outbound.normalizeAgentPlanSteps)(payload.steps), { explanation: typeof payload.explanation === "string" ? payload.explanation : void 0 });
1917
1763
  },
1918
1764
  onApprovalEvent: async (payload) => {
1919
1765
  if (payload?.phase !== "requested") return;
@@ -1956,16 +1802,18 @@ function createMSTeamsReplyDispatcher(params) {
1956
1802
  }
1957
1803
  } : {};
1958
1804
  return {
1959
- dispatcher,
1805
+ dispatcherOptions: {
1806
+ ...dispatcherOptions,
1807
+ onSettled: settleDelivery
1808
+ },
1809
+ delivery,
1960
1810
  replyOptions: {
1961
- ...replyOptions,
1962
1811
  ...streamController.hasStream() ? { onPartialReply: (payload) => streamController.onPartialReply(payload) } : {},
1963
1812
  ...progressCallbacks,
1964
1813
  ...shouldSuppressDefaultToolProgressMessages ? { suppressDefaultToolProgressMessages: true } : {},
1965
1814
  disableBlockStreaming: blockStreamingResolved == null ? void 0 : !blockStreamingResolved,
1966
1815
  onModelSelected
1967
- },
1968
- markDispatchIdle
1816
+ }
1969
1817
  };
1970
1818
  }
1971
1819
  //#endregion
@@ -2581,12 +2429,17 @@ function createMSTeamsMessageHandler(deps) {
2581
2429
  const replies = repliesResult.status === "fulfilled" ? repliesResult.value : [];
2582
2430
  if (parentResult.status === "rejected") log.debug?.("failed to fetch parent message", { error: require_setup_surface.formatUnknownError(parentResult.reason) });
2583
2431
  if (repliesResult.status === "rejected") log.debug?.("failed to fetch thread replies", { error: require_setup_surface.formatUnknownError(repliesResult.reason) });
2584
- const isThreadSenderAllowed = (msg) => groupPolicy === "allowlist" ? require_channel.resolveMSTeamsAllowlistMatch({
2432
+ const isThreadSenderAllowed = (msg) => (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2433
+ isGroup: isChannel,
2434
+ groupPolicy,
2585
2435
  allowFrom: effectiveGroupAllowFrom,
2586
- senderId: msg.from?.user?.id ?? "",
2587
- senderName: msg.from?.user?.displayName,
2588
- allowNameMatching
2589
- }).allowed : true;
2436
+ isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2437
+ allowFrom,
2438
+ senderId: msg.from?.user?.id ?? "",
2439
+ senderName: msg.from?.user?.displayName,
2440
+ allowNameMatching
2441
+ }).allowed
2442
+ });
2590
2443
  const parentSummary = summarizeParentMessage(parentMsg);
2591
2444
  const visibleParentMessages = parentMsg ? (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
2592
2445
  items: [parentMsg],
@@ -2617,17 +2470,14 @@ function createMSTeamsMessageHandler(deps) {
2617
2470
  }
2618
2471
  quoteSenderName ??= quoteInfo?.sender;
2619
2472
  const envelopeFrom = isDirectMessage ? senderName : conversationType;
2620
- const { storePath, envelopeOptions, previousTimestamp } = (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSessionEnvelopeContext)({
2473
+ const buildEnvelope = (0, openclaw_plugin_sdk_channel_inbound.createChannelInboundEnvelopeBuilder)({
2621
2474
  cfg,
2622
- agentId: route.agentId,
2623
- sessionKey: route.sessionKey
2475
+ route
2624
2476
  });
2625
- let combinedBody = core.channel.reply.formatAgentEnvelope({
2477
+ let combinedBody = buildEnvelope({
2626
2478
  channel: "Teams",
2627
2479
  from: envelopeFrom,
2628
2480
  timestamp,
2629
- previousTimestamp,
2630
- envelope: envelopeOptions,
2631
2481
  body: agentBody
2632
2482
  });
2633
2483
  const isRoomish = !isDirectMessage;
@@ -2636,12 +2486,12 @@ function createMSTeamsMessageHandler(deps) {
2636
2486
  historyKey,
2637
2487
  limit: historyLimit,
2638
2488
  currentMessage: combinedBody,
2639
- formatEntry: (entry) => core.channel.reply.formatAgentEnvelope({
2489
+ formatEntry: (entry) => buildEnvelope({
2640
2490
  channel: "Teams",
2641
2491
  from: conversationType,
2642
2492
  timestamp: entry.timestamp,
2643
- body: `${entry.sender}: ${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`,
2644
- envelope: envelopeOptions
2493
+ previousTimestamp: null,
2494
+ body: `${entry.sender}: ${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`
2645
2495
  })
2646
2496
  });
2647
2497
  const inboundHistory = isRoomish && historyKey && historyLimit > 0 ? (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildInboundHistory({
@@ -2649,17 +2499,21 @@ function createMSTeamsMessageHandler(deps) {
2649
2499
  limit: historyLimit
2650
2500
  }) : void 0;
2651
2501
  const commandBody = text.trim();
2652
- const quoteSenderAllowed = quoteInfo && quoteInfo.sender ? !isChannel || groupPolicy !== "allowlist" ? true : require_channel.resolveMSTeamsAllowlistMatch({
2502
+ const quoteSenderAllowed = quoteInfo && quoteInfo.sender ? (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2503
+ isGroup: !isDirectMessage,
2504
+ groupPolicy,
2653
2505
  allowFrom: effectiveGroupAllowFrom,
2654
- senderId: quoteSenderId ?? "",
2655
- senderName: quoteSenderName,
2656
- allowNameMatching
2657
- }).allowed : true;
2506
+ isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2507
+ allowFrom,
2508
+ senderId: quoteSenderId ?? "",
2509
+ senderName: quoteSenderName,
2510
+ allowNameMatching
2511
+ }).allowed
2512
+ }) : true;
2658
2513
  const bodyForAgent = threadContext ? `[Thread history]\n${threadContext}\n[/Thread history]\n\n${agentBody}` : agentBody;
2659
2514
  const nativeChannelId = isChannel && teamAadGroupId ? `${teamAadGroupId}/${graphChannelId}` : void 0;
2660
2515
  const ctxPayload = (0, openclaw_plugin_sdk_channel_inbound.buildChannelInboundEventContext)({
2661
2516
  channel: "msteams",
2662
- finalize: core.channel.reply.finalizeInboundContext,
2663
2517
  contextVisibility: contextVisibilityMode,
2664
2518
  supplemental: { quote: quoteInfo ? {
2665
2519
  id: quoteInfo.id ?? activity.replyToId ?? void 0,
@@ -2684,6 +2538,7 @@ function createMSTeamsMessageHandler(deps) {
2684
2538
  },
2685
2539
  route: {
2686
2540
  agentId: route.agentId,
2541
+ dmScope: route.dmScope,
2687
2542
  accountId: route.accountId,
2688
2543
  routeSessionKey: route.sessionKey
2689
2544
  },
@@ -2714,7 +2569,7 @@ function createMSTeamsMessageHandler(deps) {
2714
2569
  });
2715
2570
  logVerboseMessage(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
2716
2571
  const sharePointSiteId = msteamsCfg?.sharePointSiteId;
2717
- const { dispatcher, replyOptions, markDispatchIdle } = createMSTeamsReplyDispatcher({
2572
+ const { dispatcherOptions, delivery, replyOptions } = createMSTeamsReplyDispatcher({
2718
2573
  cfg,
2719
2574
  agentId: route.agentId,
2720
2575
  sessionKey: route.sessionKey,
@@ -2734,10 +2589,16 @@ function createMSTeamsMessageHandler(deps) {
2734
2589
  sharePointSiteId
2735
2590
  });
2736
2591
  const senderTimezone = (activity.entities?.find((e) => e.type === "clientInfo"))?.timezone || conversationRef.timezone;
2737
- const configOverride = senderTimezone && !cfg.agents?.defaults?.userTimezone ? { agents: { defaults: {
2738
- ...cfg.agents?.defaults,
2739
- userTimezone: senderTimezone
2740
- } } } : void 0;
2592
+ const turnConfig = senderTimezone && !cfg.agents?.defaults?.userTimezone ? {
2593
+ ...cfg,
2594
+ agents: {
2595
+ ...cfg.agents,
2596
+ defaults: {
2597
+ ...cfg.agents?.defaults,
2598
+ userTimezone: senderTimezone
2599
+ }
2600
+ }
2601
+ } : cfg;
2741
2602
  log.info("dispatching to agent", { sessionKey: route.sessionKey });
2742
2603
  try {
2743
2604
  const turnResult = await core.channel.inbound.run({
@@ -2754,12 +2615,14 @@ function createMSTeamsMessageHandler(deps) {
2754
2615
  raw: activity
2755
2616
  }),
2756
2617
  resolveTurn: () => ({
2618
+ cfg: turnConfig,
2757
2619
  channel: "msteams",
2758
2620
  accountId: route.accountId,
2759
- routeSessionKey: route.sessionKey,
2760
- storePath,
2621
+ route: {
2622
+ agentId: route.agentId,
2623
+ sessionKey: route.sessionKey
2624
+ },
2761
2625
  ctxPayload,
2762
- recordInboundSession: core.channel.session.recordInboundSession,
2763
2626
  record: { onRecordError: (err) => {
2764
2627
  logVerboseMessage(`msteams: failed updating session meta: ${require_setup_surface.formatUnknownError(err)}`);
2765
2628
  } },
@@ -2769,18 +2632,12 @@ function createMSTeamsMessageHandler(deps) {
2769
2632
  historyMap: conversationHistories,
2770
2633
  limit: historyLimit
2771
2634
  },
2772
- onPreDispatchFailure: () => core.channel.reply.settleReplyDispatcher({
2773
- dispatcher,
2774
- onSettled: () => markDispatchIdle()
2775
- }),
2776
- runDispatch: () => (0, openclaw_plugin_sdk_channel_inbound.dispatchReplyFromConfigWithSettledDispatcher)({
2777
- cfg,
2778
- ctxPayload,
2779
- dispatcher,
2780
- onSettled: () => markDispatchIdle(),
2781
- replyOptions,
2782
- configOverride
2783
- })
2635
+ dispatcherOptions,
2636
+ delivery,
2637
+ replyOptions: {
2638
+ ...replyOptions,
2639
+ ...params.turnAdoptionLifecycle ? (0, openclaw_plugin_sdk_channel_outbound.bindIngressLifecycleToReplyOptions)(params.turnAdoptionLifecycle) : {}
2640
+ }
2784
2641
  })
2785
2642
  }
2786
2643
  });
@@ -2798,11 +2655,47 @@ function createMSTeamsMessageHandler(deps) {
2798
2655
  } catch (err) {
2799
2656
  log.error("dispatch failed", { error: require_setup_surface.formatUnknownError(err) });
2800
2657
  runtime.error(`msteams dispatch failed: ${require_setup_surface.formatUnknownError(err)}`);
2658
+ if (params.turnAdoptionLifecycle) throw err;
2801
2659
  try {
2802
2660
  await context.sendActivity("⚠️ Something went wrong. Please try again.");
2803
2661
  } catch {}
2804
2662
  }
2805
2663
  };
2664
+ function buildFlushIngressLifecycle(entries) {
2665
+ const lifecycles = entries.map((entry) => entry.turnAdoptionLifecycle).filter((lifecycle) => lifecycle !== void 0);
2666
+ const [firstLifecycle] = lifecycles;
2667
+ if (!firstLifecycle) return {
2668
+ lifecycle: void 0,
2669
+ settle: async () => {}
2670
+ };
2671
+ let handedOff = false;
2672
+ const adoptAll = async () => {
2673
+ for (const lifecycle of lifecycles) await lifecycle.onAdopted();
2674
+ };
2675
+ return {
2676
+ lifecycle: {
2677
+ abortSignal: lifecycles.length === 1 ? firstLifecycle.abortSignal : AbortSignal.any(lifecycles.map((lifecycle) => lifecycle.abortSignal)),
2678
+ onAdopted: async () => {
2679
+ handedOff = true;
2680
+ await adoptAll();
2681
+ },
2682
+ onDeferred: () => {
2683
+ handedOff = true;
2684
+ for (const lifecycle of lifecycles) lifecycle.onDeferred();
2685
+ },
2686
+ onAdoptionFinalizing: () => {
2687
+ for (const lifecycle of lifecycles) lifecycle.onAdoptionFinalizing();
2688
+ },
2689
+ onAbandoned: async () => {
2690
+ handedOff = true;
2691
+ for (const lifecycle of lifecycles) await lifecycle.onAbandoned();
2692
+ }
2693
+ },
2694
+ settle: async () => {
2695
+ if (!handedOff) await adoptAll();
2696
+ }
2697
+ };
2698
+ }
2806
2699
  const inboundDebouncer = core.channel.debounce.createInboundDebouncer({
2807
2700
  debounceMs: inboundDebounceMs,
2808
2701
  buildKey: (entry) => {
@@ -2819,29 +2712,40 @@ function createMSTeamsMessageHandler(deps) {
2819
2712
  onFlush: async (entries) => {
2820
2713
  const last = entries.at(-1);
2821
2714
  if (!last) return;
2822
- if (entries.length === 1) {
2823
- await handleTeamsMessageNow(last);
2824
- return;
2715
+ const { lifecycle, settle } = buildFlushIngressLifecycle(entries);
2716
+ try {
2717
+ if (entries.length === 1) await handleTeamsMessageNow(lifecycle ? {
2718
+ ...last,
2719
+ turnAdoptionLifecycle: lifecycle
2720
+ } : last);
2721
+ else {
2722
+ const combinedText = entries.map((entry) => entry.text).filter(Boolean).join("\n");
2723
+ if (combinedText.trim()) {
2724
+ const combinedRawText = entries.map((entry) => entry.rawText).filter(Boolean).join("\n");
2725
+ const wasMentioned = entries.some((entry) => entry.wasMentioned);
2726
+ const implicitMentionKinds = entries.flatMap((entry) => entry.implicitMentionKinds);
2727
+ await handleTeamsMessageNow({
2728
+ context: last.context,
2729
+ rawText: combinedRawText,
2730
+ text: combinedText,
2731
+ attachments: [],
2732
+ wasMentioned,
2733
+ implicitMentionKinds,
2734
+ ...lifecycle ? { turnAdoptionLifecycle: lifecycle } : {}
2735
+ });
2736
+ }
2737
+ }
2738
+ await settle();
2739
+ } catch (err) {
2740
+ await lifecycle?.onAbandoned();
2741
+ throw err;
2825
2742
  }
2826
- const combinedText = entries.map((entry) => entry.text).filter(Boolean).join("\n");
2827
- if (!combinedText.trim()) return;
2828
- const combinedRawText = entries.map((entry) => entry.rawText).filter(Boolean).join("\n");
2829
- const wasMentioned = entries.some((entry) => entry.wasMentioned);
2830
- const implicitMentionKinds = entries.flatMap((entry) => entry.implicitMentionKinds);
2831
- await handleTeamsMessageNow({
2832
- context: last.context,
2833
- rawText: combinedRawText,
2834
- text: combinedText,
2835
- attachments: [],
2836
- wasMentioned,
2837
- implicitMentionKinds
2838
- });
2839
2743
  },
2840
2744
  onError: (err) => {
2841
2745
  runtime.error(`msteams debounce flush failed: ${require_setup_surface.formatUnknownError(err)}`);
2842
2746
  }
2843
2747
  });
2844
- return async function handleTeamsMessage(context) {
2748
+ return async function handleTeamsMessage(context, turnAdoptionLifecycle) {
2845
2749
  const activity = context.activity;
2846
2750
  const attachments = Array.isArray(activity.attachments) ? activity.attachments : [];
2847
2751
  const rawText = activity.text?.trim() ?? "";
@@ -2861,8 +2765,10 @@ function createMSTeamsMessageHandler(deps) {
2861
2765
  text,
2862
2766
  attachments,
2863
2767
  wasMentioned,
2864
- implicitMentionKinds
2768
+ implicitMentionKinds,
2769
+ turnAdoptionLifecycle
2865
2770
  });
2771
+ if (turnAdoptionLifecycle) return { kind: "deferred" };
2866
2772
  };
2867
2773
  }
2868
2774
  //#endregion
@@ -3059,29 +2965,37 @@ function registerMSTeamsHandlers(handler, deps) {
3059
2965
  const handleTeamsMessage = createMSTeamsMessageHandler(deps);
3060
2966
  const handleReaction = createMSTeamsReactionHandler(deps);
3061
2967
  const originalRun = handler.run;
3062
- if (originalRun) handler.run = async (context) => {
2968
+ if (originalRun) handler.run = async (context, turnAdoptionLifecycle) => {
3063
2969
  const ctx = context;
3064
2970
  if (ctx.activity?.type === "invoke" && ctx.activity?.name === "adaptiveCard/action") {
3065
2971
  const text = serializeMSTeamsAdaptiveCardActionValue(ctx.activity?.value);
3066
- if (text) await handleTeamsMessage({
2972
+ if (text) return await handleTeamsMessage({
3067
2973
  ...ctx,
3068
2974
  activity: {
3069
2975
  ...ctx.activity,
3070
2976
  type: "message",
3071
2977
  text
3072
2978
  }
3073
- });
2979
+ }, turnAdoptionLifecycle);
3074
2980
  return;
3075
2981
  }
3076
- return originalRun.call(handler, context);
2982
+ return originalRun.call(handler, context, turnAdoptionLifecycle);
3077
2983
  };
3078
- handler.onMessage(async (context, next) => {
2984
+ handler.onMessage(async (context, next, turnAdoptionLifecycle) => {
2985
+ let nextRan = false;
2986
+ const runNext = async () => {
2987
+ nextRan = true;
2988
+ await next();
2989
+ };
3079
2990
  try {
3080
- await handleTeamsMessage(context);
2991
+ const result = await handleTeamsMessage(context, turnAdoptionLifecycle);
2992
+ await runNext();
2993
+ return result;
3081
2994
  } catch (err) {
2995
+ if (turnAdoptionLifecycle) throw err;
3082
2996
  deps.runtime.error(`msteams handler failed: ${require_setup_surface.formatUnknownError(err)}`);
3083
2997
  }
3084
- await next();
2998
+ if (!nextRan) await runNext();
3085
2999
  });
3086
3000
  handler.onMembersAdded(async (context, next) => {
3087
3001
  const ctx = context;
@@ -3182,8 +3096,7 @@ async function runMSTeamsFeedbackInvokeHandler(context, deps) {
3182
3096
  const convType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(activity.conversation?.conversationType);
3183
3097
  const isDirectMessage = convType === "personal" || !convType && !activity.conversation?.isGroup;
3184
3098
  const isChannel = convType === "channel";
3185
- const core = require_runtime.getMSTeamsRuntime();
3186
- const route = core.channel.routing.resolveAgentRoute({
3099
+ const route = require_runtime.getMSTeamsRuntime().channel.routing.resolveAgentRoute({
3187
3100
  cfg: deps.cfg,
3188
3101
  channel: "msteams",
3189
3102
  peer: {
@@ -3212,13 +3125,12 @@ async function runMSTeamsFeedbackInvokeHandler(context, deps) {
3212
3125
  hasComment: Boolean(userComment)
3213
3126
  });
3214
3127
  try {
3215
- const storePath = core.channel.session.resolveStorePath(deps.cfg.session?.store, { agentId: route.agentId });
3216
- const safeKey = route.sessionKey.replace(/[^a-zA-Z0-9_-]/g, "_");
3217
- await (0, openclaw_plugin_sdk_security_runtime.appendRegularFile)({
3218
- filePath: node_path.default.join(storePath, `${safeKey}.jsonl`),
3219
- content: `${JSON.stringify(feedbackEvent)}\n`,
3220
- rejectSymlinkParents: true
3221
- }).catch(() => {});
3128
+ await (0, openclaw_plugin_sdk_channel_inbound.recordChannelFeedbackEvent)({
3129
+ cfg: deps.cfg,
3130
+ agentId: route.agentId,
3131
+ sessionKey: route.sessionKey,
3132
+ event: feedbackEvent
3133
+ });
3222
3134
  } catch {}
3223
3135
  const conversationRef = {
3224
3136
  activityId: activity.id,
@@ -3243,12 +3155,11 @@ async function runMSTeamsFeedbackInvokeHandler(context, deps) {
3243
3155
  if (isNegative && msteamsCfg?.feedbackReflection !== false) runFeedbackReflection({
3244
3156
  cfg: deps.cfg,
3245
3157
  app: deps.app,
3246
- appId: deps.appId,
3247
3158
  conversationRef,
3248
3159
  sessionKey: route.sessionKey,
3249
3160
  agentId: route.agentId,
3250
3161
  conversationId,
3251
- feedbackMessageId: messageId,
3162
+ conversationKind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
3252
3163
  userComment,
3253
3164
  log: deps.log
3254
3165
  }).catch((err) => {
@@ -3366,6 +3277,175 @@ async function runMSTeamsFileConsentInvokeHandler(context, log) {
3366
3277
  }
3367
3278
  }
3368
3279
  //#endregion
3280
+ //#region extensions/msteams/src/msteams-ingress.ts
3281
+ const MSTEAMS_INGRESS_VERSION = 1;
3282
+ const MSTEAMS_INGRESS_DRAIN_INTERVAL_MS = 500;
3283
+ const MSTEAMS_INGRESS_MAX_CONCURRENT_DELIVERIES = 8;
3284
+ const MSTEAMS_INGRESS_SCAN_LIMIT = 100;
3285
+ const MSTEAMS_INGRESS_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
3286
+ const MSTEAMS_INGRESS_COMPLETED_MAX_ENTRIES = 2e4;
3287
+ const MSTEAMS_INGRESS_FAILED_MAX_ENTRIES = 4096;
3288
+ var MSTeamsIngressPayloadError = class extends Error {
3289
+ constructor(reason, message, options) {
3290
+ super(message, options);
3291
+ this.reason = reason;
3292
+ this.name = "MSTeamsIngressPayloadError";
3293
+ }
3294
+ };
3295
+ function nonEmptyString(value) {
3296
+ return typeof value === "string" && value.trim() ? value.trim() : null;
3297
+ }
3298
+ function isDispatchableActivity(activity) {
3299
+ return activity.type === "message" || activity.type === "invoke" && activity.name === "adaptiveCard/action";
3300
+ }
3301
+ function inspectMSTeamsIngressActivity(activity) {
3302
+ if (!isDispatchableActivity(activity)) return null;
3303
+ const eventId = nonEmptyString(activity.id);
3304
+ if (!eventId) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams dispatchable activity is missing activity.id.");
3305
+ const laneKey = nonEmptyString(activity.conversation?.id);
3306
+ if (!laneKey) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams dispatchable activity is missing conversation.id.");
3307
+ return {
3308
+ eventId,
3309
+ laneKey
3310
+ };
3311
+ }
3312
+ function parseClaimedActivity(payload, claimedId) {
3313
+ if (payload.version !== MSTEAMS_INGRESS_VERSION || typeof payload.rawActivity !== "string" || !Number.isFinite(payload.receivedAt)) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams ingress payload is invalid.");
3314
+ let activity;
3315
+ try {
3316
+ activity = JSON.parse(payload.rawActivity);
3317
+ } catch (error) {
3318
+ throw new MSTeamsIngressPayloadError("invalid-json", "Microsoft Teams ingress activity JSON is invalid.", { cause: error });
3319
+ }
3320
+ if (!activity || typeof activity !== "object" || Array.isArray(activity)) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams ingress activity must be an object.");
3321
+ const parsed = activity;
3322
+ const facts = inspectMSTeamsIngressActivity(parsed);
3323
+ if (!facts) throw new MSTeamsIngressPayloadError("unsupported-activity", "Microsoft Teams ingress row is not an agent-turn activity.");
3324
+ if (facts.eventId !== claimedId) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams activity id changed after durable admission.");
3325
+ return parsed;
3326
+ }
3327
+ function errorText(error) {
3328
+ return error instanceof Error ? error.message : String(error);
3329
+ }
3330
+ function createMSTeamsIngress(options) {
3331
+ const queue = options.queue ?? require_runtime.getMSTeamsRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
3332
+ const shutdown = new AbortController();
3333
+ const liveContexts = /* @__PURE__ */ new Map();
3334
+ const activeDeliveries = /* @__PURE__ */ new Set();
3335
+ const drain = (0, openclaw_plugin_sdk_channel_outbound.createChannelIngressDrain)({
3336
+ queue,
3337
+ abortSignal: shutdown.signal,
3338
+ orderBy: "received",
3339
+ scanLimit: MSTEAMS_INGRESS_SCAN_LIMIT,
3340
+ startLimit: MSTEAMS_INGRESS_MAX_CONCURRENT_DELIVERIES,
3341
+ retryPolicy: {
3342
+ maxAttempts: openclaw_plugin_sdk_channel_outbound.DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
3343
+ deadLetterMinAgeMs: openclaw_plugin_sdk_channel_outbound.DEFAULT_INGRESS_RETRY_DEAD_LETTER_MIN_AGE_MS
3344
+ },
3345
+ resolveNonRetryableFailure: (error) => {
3346
+ if (error instanceof MSTeamsIngressPayloadError) return {
3347
+ reason: error.reason,
3348
+ message: error.message
3349
+ };
3350
+ return require_setup_surface.classifyMSTeamsSendError(error).kind === "auth" ? {
3351
+ reason: "authentication-failed",
3352
+ message: errorText(error)
3353
+ } : null;
3354
+ },
3355
+ onLog: (message) => options.runtime.error?.(`msteams: ${message}`),
3356
+ dispatchClaimedEvent: async (claimed, lifecycle) => {
3357
+ const activity = parseClaimedActivity(claimed.payload, claimed.id);
3358
+ const liveContext = liveContexts.get(claimed.id);
3359
+ liveContexts.delete(claimed.id);
3360
+ const delivery = Promise.resolve(options.dispatch(activity, lifecycle, liveContext));
3361
+ activeDeliveries.add(delivery);
3362
+ try {
3363
+ return await delivery;
3364
+ } finally {
3365
+ activeDeliveries.delete(delivery);
3366
+ }
3367
+ }
3368
+ });
3369
+ let running = false;
3370
+ let stopped = false;
3371
+ let drainRequested = false;
3372
+ let drainTask;
3373
+ let drainTimer;
3374
+ const requestDrain = () => {
3375
+ if (!running || shutdown.signal.aborted) return;
3376
+ drainRequested = true;
3377
+ if (drainTask) return;
3378
+ drainTask = (async () => {
3379
+ while (drainRequested && !shutdown.signal.aborted) {
3380
+ if (!running) break;
3381
+ drainRequested = false;
3382
+ await drain.drainOnce({ shouldStop: () => !running || shutdown.signal.aborted || activeDeliveries.size >= MSTEAMS_INGRESS_MAX_CONCURRENT_DELIVERIES });
3383
+ }
3384
+ })().catch((error) => {
3385
+ options.runtime.error?.(`msteams ingress drain failed: ${errorText(error)}`);
3386
+ }).finally(() => {
3387
+ drainTask = void 0;
3388
+ if (running && drainRequested && !shutdown.signal.aborted) requestDrain();
3389
+ });
3390
+ };
3391
+ return {
3392
+ accept: async (activity, liveContext) => {
3393
+ const facts = inspectMSTeamsIngressActivity(activity);
3394
+ if (!facts) return;
3395
+ await queue.prune({
3396
+ completedTtlMs: MSTEAMS_INGRESS_TOMBSTONE_TTL_MS,
3397
+ completedMaxEntries: MSTEAMS_INGRESS_COMPLETED_MAX_ENTRIES,
3398
+ failedTtlMs: MSTEAMS_INGRESS_TOMBSTONE_TTL_MS,
3399
+ failedMaxEntries: MSTEAMS_INGRESS_FAILED_MAX_ENTRIES
3400
+ });
3401
+ const receivedAt = Date.now();
3402
+ const installedLiveContext = Boolean(liveContext) && !liveContexts.has(facts.eventId);
3403
+ if (liveContext && installedLiveContext) liveContexts.set(facts.eventId, liveContext);
3404
+ const uninstallLiveContext = () => {
3405
+ if (installedLiveContext && liveContexts.get(facts.eventId) === liveContext) liveContexts.delete(facts.eventId);
3406
+ };
3407
+ let result;
3408
+ try {
3409
+ result = await queue.enqueue(facts.eventId, {
3410
+ version: MSTEAMS_INGRESS_VERSION,
3411
+ receivedAt,
3412
+ rawActivity: JSON.stringify(activity)
3413
+ }, {
3414
+ receivedAt,
3415
+ laneKey: facts.laneKey
3416
+ });
3417
+ } catch (error) {
3418
+ uninstallLiveContext();
3419
+ throw error;
3420
+ }
3421
+ if (!(result.kind === "accepted" || result.kind === "pending")) uninstallLiveContext();
3422
+ requestDrain();
3423
+ },
3424
+ start: () => {
3425
+ if (running || stopped) return;
3426
+ running = true;
3427
+ requestDrain();
3428
+ drainTimer = setInterval(requestDrain, MSTEAMS_INGRESS_DRAIN_INTERVAL_MS);
3429
+ drainTimer.unref?.();
3430
+ },
3431
+ stop: async () => {
3432
+ if (stopped) return;
3433
+ stopped = true;
3434
+ running = false;
3435
+ if (drainTimer) {
3436
+ clearInterval(drainTimer);
3437
+ drainTimer = void 0;
3438
+ }
3439
+ await drainTask;
3440
+ await Promise.allSettled(activeDeliveries);
3441
+ await drain.waitForIdle();
3442
+ shutdown.abort();
3443
+ drain.dispose();
3444
+ liveContexts.clear();
3445
+ }
3446
+ };
3447
+ }
3448
+ //#endregion
3369
3449
  //#region extensions/msteams/src/webhook-timeouts.ts
3370
3450
  const MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS = 3e4;
3371
3451
  const MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS = 3e4;
@@ -3548,6 +3628,15 @@ async function monitorMSTeamsProvider(opts) {
3548
3628
  log
3549
3629
  };
3550
3630
  registerMSTeamsHandlers(handler, handlerDeps);
3631
+ const ingress = createMSTeamsIngress({
3632
+ accountId: appId,
3633
+ runtime,
3634
+ dispatch: async (activity, lifecycle, liveContext) => {
3635
+ if (liveContext) liveContext.activity = activity;
3636
+ const context = liveContext ?? createMSTeamsReplayContext(activity, app, require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg));
3637
+ return await handler.run(context, lifecycle);
3638
+ }
3639
+ });
3551
3640
  app.on("card.action", async (ctx) => {
3552
3641
  const adaptedCtx = adaptSdkContext(ctx, app);
3553
3642
  try {
@@ -3624,9 +3713,7 @@ async function monitorMSTeamsProvider(opts) {
3624
3713
  };
3625
3714
  }
3626
3715
  }
3627
- handler.run(adaptedCtx).catch((err) => {
3628
- log.error("msteams card.action dispatch failed", { error: require_setup_surface.formatUnknownError(err) });
3629
- });
3716
+ await ingress.accept(activity, adaptedCtx);
3630
3717
  return {
3631
3718
  statusCode: 200,
3632
3719
  type: "application/vnd.microsoft.activity.message",
@@ -3708,24 +3795,30 @@ async function monitorMSTeamsProvider(opts) {
3708
3795
  if (!await runMSTeamsFeedbackInvokeHandler(adaptSdkContext(ctx, app), handlerDeps)) await ctx.next?.call(ctx);
3709
3796
  });
3710
3797
  app.on("activity", async (ctx) => {
3798
+ const adaptedCtx = adaptSdkContext(ctx, app);
3799
+ const activity = adaptedCtx.activity;
3800
+ if (activity?.type === "invoke") {
3801
+ if (activity?.name === "adaptiveCard/action") return;
3802
+ if (activity?.name === "fileConsent/invoke") return;
3803
+ if (activity?.name === "signin/tokenExchange" || activity?.name === "signin/verifyState") return;
3804
+ }
3805
+ if (activity?.type === "message") {
3806
+ await ingress.accept(activity, adaptedCtx);
3807
+ return;
3808
+ }
3711
3809
  try {
3712
- const adaptedCtx = adaptSdkContext(ctx, app);
3713
- const activity = adaptedCtx.activity;
3714
- if (activity?.type === "invoke") {
3715
- if (activity?.name === "adaptiveCard/action") return;
3716
- if (activity?.name === "fileConsent/invoke") return;
3717
- if (activity?.name === "signin/tokenExchange" || activity?.name === "signin/verifyState") return;
3718
- }
3719
3810
  await handler.run(adaptedCtx);
3720
3811
  } catch (err) {
3721
- log.error("msteams webhook failed", { error: require_setup_surface.formatUnknownError(err) });
3812
+ log.error("msteams non-turn activity failed", { error: require_setup_surface.formatUnknownError(err) });
3722
3813
  }
3723
3814
  });
3724
3815
  await app.initialize();
3816
+ ingress.start();
3725
3817
  const httpServer = await new Promise((resolve, reject) => {
3726
3818
  const server = expressApp.listen(port, (err) => err ? reject(err) : resolve(server));
3727
- }).catch((err) => {
3819
+ }).catch(async (err) => {
3728
3820
  log.error("msteams server error", { error: require_setup_surface.formatUnknownError(err) });
3821
+ await ingress.stop();
3729
3822
  throw err;
3730
3823
  });
3731
3824
  log.info(`msteams provider started on port ${port}`);
@@ -3735,12 +3828,13 @@ async function monitorMSTeamsProvider(opts) {
3735
3828
  });
3736
3829
  const shutdown = async () => {
3737
3830
  log.info("shutting down msteams provider");
3738
- return new Promise((resolve) => {
3831
+ await new Promise((resolve) => {
3739
3832
  httpServer.close((err) => {
3740
3833
  if (err) log.debug?.("msteams server close error", { error: require_setup_surface.formatUnknownError(err) });
3741
3834
  resolve();
3742
3835
  });
3743
3836
  });
3837
+ await ingress.stop();
3744
3838
  };
3745
3839
  await (0, openclaw_plugin_sdk_channel_outbound.keepHttpServerTaskAlive)({
3746
3840
  server: httpServer,
@@ -3778,11 +3872,14 @@ function buildActivityHandler() {
3778
3872
  reactionsRemovedHandlers.push(cb);
3779
3873
  return handler;
3780
3874
  },
3781
- async run(context) {
3875
+ async run(context, turnAdoptionLifecycle) {
3782
3876
  const ctx = context;
3783
3877
  const activityType = ctx?.activity?.type;
3784
3878
  const noop = async () => {};
3785
- if (activityType === "message") for (const h of messageHandlers) await h(context, noop);
3879
+ if (activityType === "message") for (const h of messageHandlers) {
3880
+ const result = await h(context, noop, turnAdoptionLifecycle);
3881
+ if (result) return result;
3882
+ }
3786
3883
  else if (activityType === "conversationUpdate") for (const h of membersAddedHandlers) await h(context, noop);
3787
3884
  else if (activityType === "messageReaction") {
3788
3885
  const activity = ctx?.activity;
@@ -3793,6 +3890,47 @@ function buildActivityHandler() {
3793
3890
  };
3794
3891
  return handler;
3795
3892
  }
3893
+ function createMSTeamsReplayContext(activity, app, serviceUrlBoundary) {
3894
+ const rawConversationId = activity.conversation?.id ?? "";
3895
+ const conversationId = require_channel.normalizeMSTeamsConversationId(rawConversationId);
3896
+ const conversationType = activity.conversation?.conversationType ?? "personal";
3897
+ const threadActivityId = conversationType.toLowerCase() === "channel" ? require_channel.extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId : void 0;
3898
+ const tenantId = activity.channelData?.tenant?.id ?? activity.conversation?.tenantId;
3899
+ const reference = {
3900
+ activityId: activity.id,
3901
+ user: activity.from,
3902
+ agent: activity.recipient,
3903
+ conversation: {
3904
+ id: conversationId,
3905
+ conversationType,
3906
+ ...tenantId ? { tenantId } : {}
3907
+ },
3908
+ channelId: activity.channelId,
3909
+ serviceUrl: activity.serviceUrl,
3910
+ locale: activity.locale,
3911
+ ...tenantId ? { tenantId } : {},
3912
+ ...activity.from?.aadObjectId ? { aadObjectId: activity.from.aadObjectId } : {}
3913
+ };
3914
+ const proactiveOptions = {
3915
+ ...threadActivityId ? { threadActivityId } : {},
3916
+ serviceUrlBoundary
3917
+ };
3918
+ const sendActivity = (outbound) => require_messenger.sendMSTeamsActivityWithReference(app, reference, outbound, proactiveOptions);
3919
+ return {
3920
+ activity,
3921
+ sendActivity,
3922
+ sendActivities: async (activities) => {
3923
+ const results = [];
3924
+ for (const outbound of activities) results.push(await sendActivity(outbound));
3925
+ return results;
3926
+ },
3927
+ updateActivity: async (outbound) => await require_messenger.updateMSTeamsActivityWithReference(app, reference, typeof outbound.id === "string" ? outbound.id : "", outbound, proactiveOptions),
3928
+ deleteActivity: async (activityId) => {
3929
+ await require_messenger.deleteMSTeamsActivityWithReference(app, reference, activityId, proactiveOptions);
3930
+ },
3931
+ getTeamDetails: (teamId) => app.api.teams.getById(teamId)
3932
+ };
3933
+ }
3796
3934
  /**
3797
3935
  * Adapt a new @microsoft/teams.apps SDK context to the MSTeamsTurnContext interface
3798
3936
  * our handlers expect. The new SDK uses reply()/send() instead of sendActivity().