@openclaw/msteams 2026.7.2-beta.5 → 2026.8.1-beta.2

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,12 +1,12 @@
1
- const require_delegated_state = require("./delegated-state-DOSCHaAB.cjs");
1
+ const require_rolldown_runtime = require("./rolldown-runtime-D6vf50IK.cjs");
2
2
  const require_runtime = require("./runtime-gZTAuFux.cjs");
3
3
  require("./runtime-api.cjs");
4
- const require_resolve_allowlist = require("./resolve-allowlist-Dr_1sOJh.cjs");
5
- const require_channel = require("./channel-DtZFlwhL.cjs");
6
- const require_setup_surface = require("./setup-surface-xVejKCJV.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-BJ0KM2sD.cjs");
4
+ const require_resolve_allowlist = require("./resolve-allowlist-C6YcNC9a.cjs");
5
+ const require_channel = require("./channel-DfCbi0d_.cjs");
6
+ const require_setup_surface = require("./setup-surface-DAhGL2h8.cjs");
7
+ const require_polls = require("./polls-DvuJvaDG.cjs");
8
+ const require_sso_token_store = require("./sso-token-store-ChhO1f4i.cjs");
9
+ const require_messenger = require("./messenger-BqA8G3pY.cjs");
10
10
  let openclaw_plugin_sdk_channel_outbound = require("openclaw/plugin-sdk/channel-outbound");
11
11
  let openclaw_plugin_sdk_string_coerce_runtime = require("openclaw/plugin-sdk/string-coerce-runtime");
12
12
  let openclaw_plugin_sdk_allow_from = require("openclaw/plugin-sdk/allow-from");
@@ -15,16 +15,21 @@ let openclaw_plugin_sdk_ssrf_runtime = require("openclaw/plugin-sdk/ssrf-runtime
15
15
  let openclaw_plugin_sdk_provider_http = require("openclaw/plugin-sdk/provider-http");
16
16
  let openclaw_plugin_sdk_number_runtime = require("openclaw/plugin-sdk/number-runtime");
17
17
  let openclaw_plugin_sdk_text_utility_runtime = require("openclaw/plugin-sdk/text-utility-runtime");
18
+ let openclaw_plugin_sdk_error_runtime = require("openclaw/plugin-sdk/error-runtime");
18
19
  let openclaw_plugin_sdk_routing = require("openclaw/plugin-sdk/routing");
19
20
  let node_crypto = require("node:crypto");
20
- node_crypto = require_delegated_state.__toESM(node_crypto, 1);
21
+ node_crypto = require_rolldown_runtime.__toESM(node_crypto, 1);
21
22
  let openclaw_plugin_sdk_channel_inbound = require("openclaw/plugin-sdk/channel-inbound");
22
23
  let openclaw_plugin_sdk_channel_ingress_runtime = require("openclaw/plugin-sdk/channel-ingress-runtime");
23
24
  let openclaw_plugin_sdk_context_visibility_runtime = require("openclaw/plugin-sdk/context-visibility-runtime");
24
25
  let openclaw_plugin_sdk_reply_history = require("openclaw/plugin-sdk/reply-history");
25
26
  let openclaw_plugin_sdk_agent_runtime = require("openclaw/plugin-sdk/agent-runtime");
27
+ let openclaw_plugin_sdk_extension_shared = require("openclaw/plugin-sdk/extension-shared");
28
+ let openclaw_plugin_sdk_plugin_runtime = require("openclaw/plugin-sdk/plugin-runtime");
26
29
  let openclaw_plugin_sdk_dedupe_runtime = require("openclaw/plugin-sdk/dedupe-runtime");
30
+ let openclaw_plugin_sdk_plugin_state_runtime = require("openclaw/plugin-sdk/plugin-state-runtime");
27
31
  let openclaw_plugin_sdk_collection_runtime = require("openclaw/plugin-sdk/collection-runtime");
32
+ let openclaw_plugin_sdk_gateway_runtime = require("openclaw/plugin-sdk/gateway-runtime");
28
33
  let openclaw_plugin_sdk_channel_pairing = require("openclaw/plugin-sdk/channel-pairing");
29
34
  let openclaw_plugin_sdk_account_id = require("openclaw/plugin-sdk/account-id");
30
35
  let openclaw_plugin_sdk_runtime_group_policy = require("openclaw/plugin-sdk/runtime-group-policy");
@@ -168,20 +173,50 @@ const msteamsIngressIdentity = {
168
173
  aliases: [{
169
174
  key: "sender-name",
170
175
  kind: "plugin:msteams-sender-name",
171
- normalizeEntry: normalizeIngressValue,
172
- normalizeSubject: normalizeIngressValue,
176
+ normalizeEntry: normalizeSenderNameIngressValue,
177
+ normalizeSubject: normalizeSenderNameIngressValue,
173
178
  dangerous: true
179
+ }, {
180
+ key: "conversation-id",
181
+ kind: "plugin:msteams-conversation-id",
182
+ normalizeEntry: normalizeAllowlistConversationId,
183
+ normalizeSubject: normalizeAllowlistConversationId
174
184
  }],
175
185
  isWildcardEntry: (entry) => normalizeIngressValue(entry) === "*",
176
- resolveEntryId: ({ entryIndex, fieldKey }) => `msteams-entry-${entryIndex + 1}:${fieldKey === "sender-name" ? "name" : "id"}`
186
+ resolveEntryId: ({ entryIndex, fieldKey }) => `msteams-entry-${entryIndex + 1}:${fieldKey === "sender-name" ? "name" : fieldKey === "conversation-id" ? "conversation-id" : "id"}`
177
187
  };
178
188
  function normalizeIngressValue(value) {
179
189
  return (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(value) ?? null;
180
190
  }
191
+ function normalizeSenderNameIngressValue(value) {
192
+ const normalized = normalizeIngressValue(value);
193
+ if (!normalized) return null;
194
+ return require_resolve_allowlist.looksLikeMSTeamsConversationId(require_resolve_allowlist.normalizeMSTeamsConversationId(normalized)) ? null : normalized;
195
+ }
196
+ function normalizeAllowlistConversationId(value) {
197
+ const trimmed = value?.trim();
198
+ return trimmed ? require_resolve_allowlist.normalizeMSTeamsConversationId(trimmed) : null;
199
+ }
200
+ function formatMSTeamsSenderReason(params) {
201
+ switch (params.reasonCode) {
202
+ case "dm_policy_open": return "dmPolicy=open";
203
+ case "dm_policy_disabled": return "dmPolicy=disabled";
204
+ case "dm_policy_pairing_required": return "dmPolicy=pairing (not allowlisted)";
205
+ case "dm_policy_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (allowlisted)`;
206
+ case "dm_policy_not_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (not allowlisted)`;
207
+ case "group_policy_disabled": return "groupPolicy=disabled";
208
+ case "group_policy_empty_allowlist":
209
+ case "route_sender_empty": return "groupPolicy=allowlist (empty allowlist)";
210
+ case "group_policy_not_allowlisted": return "groupPolicy=allowlist (not allowlisted)";
211
+ case "group_policy_open": return "groupPolicy=open";
212
+ case "group_policy_allowed": return `groupPolicy=${params.groupPolicy ?? "allowlist"}`;
213
+ default: return params.reasonCode;
214
+ }
215
+ }
181
216
  async function resolveMSTeamsSenderAccess(params) {
182
217
  const activity = params.activity;
183
218
  const msteamsCfg = params.cfg.channels?.msteams;
184
- const conversationId = require_channel.normalizeMSTeamsConversationId(activity.conversation?.id ?? "unknown");
219
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "unknown");
185
220
  const convType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(activity.conversation?.conversationType);
186
221
  const isDirectMessage = convType === "personal" || !convType && !activity.conversation?.isGroup;
187
222
  const senderId = activity.from?.aadObjectId ?? activity.from?.id ?? "unknown";
@@ -205,47 +240,53 @@ async function resolveMSTeamsSenderAccess(params) {
205
240
  channelName: activity.channelData?.channel?.name,
206
241
  allowNameMatching
207
242
  });
208
- return {
209
- ...await (0, openclaw_plugin_sdk_channel_ingress_runtime.resolveStableChannelMessageIngress)({
210
- channelId: "msteams",
211
- accountId: pairing.accountId,
212
- identity: msteamsIngressIdentity,
213
- cfg: params.cfg,
214
- readStoreAllowFrom: pairing.readAllowFromStore,
215
- subject: {
216
- stableId: senderId,
217
- aliases: { "sender-name": senderName }
218
- },
219
- conversation: {
220
- kind: isDirectMessage ? "direct" : convType === "channel" ? "channel" : "group",
221
- id: conversationId,
222
- parentId: activity.channelData?.team?.id
223
- },
224
- route: (0, openclaw_plugin_sdk_channel_ingress_runtime.channelIngressRoutes)(!isDirectMessage && channelGate.allowlistConfigured && {
225
- id: "msteams:team-channel",
226
- kind: "nestedAllowlist",
227
- allowed: channelGate.allowed,
228
- precedence: 0,
229
- matchId: "msteams-route",
230
- ...channelGate.allowed && groupPolicy === "allowlist" ? {
231
- senderPolicy: "deny-when-empty",
232
- senderAllowFromSource: "effective-group"
233
- } : {}
234
- }),
235
- dmPolicy,
236
- groupPolicy,
237
- policy: {
238
- groupAllowFromFallbackToAllowFrom: true,
239
- mutableIdentifierMatching: allowNameMatching ? "enabled" : "disabled"
240
- },
241
- allowFrom: configuredDmAllowFrom,
242
- groupAllowFrom,
243
- command: {
244
- allowTextCommands: true,
245
- hasControlCommand: params.hasControlCommand === true,
246
- directGroupAllowFrom: isDirectMessage ? "effective" : "none"
243
+ const resolved = await (0, openclaw_plugin_sdk_channel_ingress_runtime.resolveStableChannelMessageIngress)({
244
+ channelId: "msteams",
245
+ accountId: pairing.accountId,
246
+ identity: msteamsIngressIdentity,
247
+ cfg: params.cfg,
248
+ readStoreAllowFrom: pairing.readAllowFromStore,
249
+ subject: {
250
+ stableId: senderId,
251
+ aliases: {
252
+ "sender-name": senderName,
253
+ ...!isDirectMessage ? { "conversation-id": conversationId } : {}
247
254
  }
255
+ },
256
+ conversation: {
257
+ kind: isDirectMessage ? "direct" : convType === "channel" ? "channel" : "group",
258
+ id: conversationId,
259
+ threadId: params.conversationThreadId
260
+ },
261
+ ...params.contextBinding ? { contextBinding: params.contextBinding } : {},
262
+ route: (0, openclaw_plugin_sdk_channel_ingress_runtime.channelIngressRoutes)(!isDirectMessage && channelGate.allowlistConfigured && {
263
+ id: "msteams:team-channel",
264
+ kind: "nestedAllowlist",
265
+ allowed: channelGate.allowed,
266
+ precedence: 0,
267
+ matchId: "msteams-route",
268
+ ...channelGate.allowed && groupPolicy === "allowlist" ? {
269
+ senderPolicy: "deny-when-empty",
270
+ senderAllowFromSource: "effective-group"
271
+ } : {}
248
272
  }),
273
+ dmPolicy,
274
+ groupPolicy,
275
+ policy: {
276
+ groupAllowFromFallbackToAllowFrom: true,
277
+ mutableIdentifierMatching: allowNameMatching ? "enabled" : "disabled"
278
+ },
279
+ allowFrom: configuredDmAllowFrom,
280
+ groupAllowFrom,
281
+ command: {
282
+ allowTextCommands: true,
283
+ hasControlCommand: params.hasControlCommand === true,
284
+ directGroupAllowFrom: isDirectMessage ? "effective" : "none"
285
+ }
286
+ });
287
+ return {
288
+ ...resolved,
289
+ channelIngress: resolved,
249
290
  pairing,
250
291
  isDirectMessage,
251
292
  conversationId,
@@ -258,6 +299,136 @@ async function resolveMSTeamsSenderAccess(params) {
258
299
  groupPolicy
259
300
  };
260
301
  }
302
+ async function admitMSTeamsMessage(params) {
303
+ const core = require_runtime.getMSTeamsRuntime();
304
+ const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
305
+ cfg: params.cfg,
306
+ surface: "msteams"
307
+ });
308
+ const isControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(params.text, params.cfg);
309
+ const access = await resolveMSTeamsSenderAccess({
310
+ cfg: params.cfg,
311
+ activity: params.activity,
312
+ hasControlCommand: isControlCommand
313
+ });
314
+ const { dmPolicy, senderId, senderName, pairing, isDirectMessage, channelGate, senderAccess, commandAccess, allowNameMatching, groupPolicy, msteamsCfg } = access;
315
+ const effectiveDmAllowFrom = senderAccess.effectiveAllowFrom;
316
+ const effectiveGroupAllowFrom = senderAccess.effectiveGroupAllowFrom;
317
+ if (isDirectMessage && msteamsCfg && senderAccess.decision !== "allow") {
318
+ if (senderAccess.reasonCode === "dm_policy_disabled") {
319
+ params.log.info("dropping dm (dms disabled)", {
320
+ sender: senderId,
321
+ label: senderName
322
+ });
323
+ params.log.debug?.("dropping dm (dms disabled)");
324
+ return null;
325
+ }
326
+ const allowMatch = require_channel.resolveMSTeamsAllowlistMatch({
327
+ allowFrom: effectiveDmAllowFrom,
328
+ senderId,
329
+ senderName,
330
+ allowNameMatching
331
+ });
332
+ if (senderAccess.decision === "pairing") {
333
+ params.conversationStore.upsert(params.conversationId, params.conversationRef).catch((err) => {
334
+ params.log.debug?.("failed to save conversation reference", { error: require_setup_surface.formatUnknownError(err) });
335
+ });
336
+ if (await pairing.upsertPairingRequest({
337
+ id: senderId,
338
+ meta: { name: senderName }
339
+ })) params.log.info("msteams pairing request created", {
340
+ sender: senderId,
341
+ label: senderName
342
+ });
343
+ }
344
+ params.log.debug?.("dropping dm (not allowlisted)", {
345
+ sender: senderId,
346
+ label: senderName,
347
+ allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
348
+ });
349
+ params.log.info("dropping dm (not allowlisted)", {
350
+ sender: senderId,
351
+ label: senderName,
352
+ dmPolicy,
353
+ reason: formatMSTeamsSenderReason({
354
+ reasonCode: senderAccess.reasonCode,
355
+ dmPolicy,
356
+ groupPolicy
357
+ }),
358
+ allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
359
+ });
360
+ return null;
361
+ }
362
+ if (!isDirectMessage && msteamsCfg) {
363
+ if (channelGate.allowlistConfigured && !channelGate.allowed) {
364
+ params.log.info("dropping group message (not in team/channel allowlist)", {
365
+ conversationId: params.conversationId,
366
+ teamKey: channelGate.teamKey ?? "none",
367
+ channelKey: channelGate.channelKey ?? "none",
368
+ channelMatchKey: channelGate.channelMatchKey ?? "none",
369
+ channelMatchSource: channelGate.channelMatchSource ?? "none"
370
+ });
371
+ params.log.debug?.("dropping group message (not in team/channel allowlist)", {
372
+ conversationId: params.conversationId,
373
+ teamKey: channelGate.teamKey ?? "none",
374
+ channelKey: channelGate.channelKey ?? "none",
375
+ channelMatchKey: channelGate.channelMatchKey ?? "none",
376
+ channelMatchSource: channelGate.channelMatchSource ?? "none"
377
+ });
378
+ return null;
379
+ }
380
+ if (!senderAccess.allowed && senderAccess.reasonCode === "group_policy_disabled") {
381
+ params.log.info("dropping group message (groupPolicy: disabled)", { conversationId: params.conversationId });
382
+ params.log.debug?.("dropping group message (groupPolicy: disabled)", { conversationId: params.conversationId });
383
+ return null;
384
+ }
385
+ if (!senderAccess.allowed && (senderAccess.reasonCode === "group_policy_empty_allowlist" || senderAccess.reasonCode === "route_sender_empty")) {
386
+ params.log.info("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId: params.conversationId });
387
+ params.log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId: params.conversationId });
388
+ return null;
389
+ }
390
+ if (!senderAccess.allowed) {
391
+ const allowMatch = require_channel.resolveMSTeamsAllowlistMatch({
392
+ allowFrom: effectiveGroupAllowFrom,
393
+ senderId,
394
+ senderName,
395
+ allowNameMatching
396
+ });
397
+ params.log.debug?.("dropping group message (not in groupAllowFrom)", {
398
+ sender: senderId,
399
+ label: senderName,
400
+ allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
401
+ });
402
+ params.log.info("dropping group message (not in groupAllowFrom)", {
403
+ sender: senderId,
404
+ label: senderName,
405
+ allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
406
+ });
407
+ return null;
408
+ }
409
+ }
410
+ if (commandAccess.shouldBlockControlCommand) {
411
+ (0, openclaw_plugin_sdk_channel_inbound.logInboundDrop)({
412
+ log: params.logVerboseMessage,
413
+ channel: "msteams",
414
+ reason: "control command (unauthorized)",
415
+ target: senderId
416
+ });
417
+ return null;
418
+ }
419
+ params.conversationStore.upsert(params.conversationId, params.conversationRef).catch((err) => {
420
+ params.log.debug?.("failed to save conversation reference", { error: require_setup_surface.formatUnknownError(err) });
421
+ });
422
+ return {
423
+ ...access,
424
+ allowTextCommands,
425
+ isControlCommand,
426
+ commandAuthorized: commandAccess.requested ? commandAccess.authorized : void 0,
427
+ effectiveDmAllowFrom,
428
+ effectiveGroupAllowFrom,
429
+ isChannel: params.isChannel
430
+ };
431
+ }
261
432
  //#endregion
262
433
  //#region extensions/msteams/src/attachments/bot-framework.ts
263
434
  /**
@@ -308,7 +479,7 @@ async function fetchBotFrameworkAttachmentInfo(params) {
308
479
  timeoutMs: require_resolve_allowlist.resolveMSTeamsRequestTimeoutMs(params.deadline)
309
480
  });
310
481
  } catch (err) {
311
- params.logger?.warn?.("msteams botFramework attachmentInfo fetch failed", { error: err instanceof Error ? err.message : String(err) });
482
+ params.logger?.warn?.("msteams botFramework attachmentInfo fetch failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
312
483
  return;
313
484
  }
314
485
  if (!response.ok) {
@@ -319,7 +490,7 @@ async function fetchBotFrameworkAttachmentInfo(params) {
319
490
  try {
320
491
  return await (0, openclaw_plugin_sdk_provider_http.readProviderJsonResponse)(response, "msteams botFramework attachmentInfo");
321
492
  } catch (err) {
322
- params.logger?.warn?.("msteams botFramework attachmentInfo parse failed", { error: err instanceof Error ? err.message : String(err) });
493
+ params.logger?.warn?.("msteams botFramework attachmentInfo parse failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
323
494
  return;
324
495
  }
325
496
  }
@@ -341,7 +512,7 @@ async function saveBotFrameworkAttachmentView(params) {
341
512
  timeoutMs: require_resolve_allowlist.resolveMSTeamsRequestTimeoutMs(params.deadline)
342
513
  });
343
514
  } catch (err) {
344
- params.logger?.warn?.("msteams botFramework attachmentView fetch failed", { error: err instanceof Error ? err.message : String(err) });
515
+ params.logger?.warn?.("msteams botFramework attachmentView fetch failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
345
516
  return;
346
517
  }
347
518
  if (!response.ok) {
@@ -354,7 +525,7 @@ async function saveBotFrameworkAttachmentView(params) {
354
525
  contentLength = (0, openclaw_plugin_sdk_media_runtime.parseMediaContentLength)(response.headers.get("content-length"));
355
526
  } catch (err) {
356
527
  await response.body?.cancel().catch(() => void 0);
357
- params.logger?.warn?.("msteams botFramework attachmentView invalid content-length", { error: err instanceof Error ? err.message : String(err) });
528
+ params.logger?.warn?.("msteams botFramework attachmentView invalid content-length", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
358
529
  return;
359
530
  }
360
531
  if (contentLength !== null && contentLength > params.maxBytes) {
@@ -371,7 +542,7 @@ async function saveBotFrameworkAttachmentView(params) {
371
542
  originalFilename: params.preserveFilenames ? params.fileNameHint : void 0
372
543
  });
373
544
  } catch (err) {
374
- params.logger?.warn?.("msteams botFramework attachmentView save failed", { error: err instanceof Error ? err.message : String(err) });
545
+ params.logger?.warn?.("msteams botFramework attachmentView save failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
375
546
  return;
376
547
  } finally {
377
548
  await response.body?.cancel().catch(() => void 0);
@@ -399,7 +570,7 @@ async function downloadMSTeamsBotFrameworkAttachment(params) {
399
570
  work: () => tokenProvider.getAccessToken(BOT_FRAMEWORK_SCOPE)
400
571
  });
401
572
  } catch (err) {
402
- params.logger?.warn?.("msteams botFramework token acquisition failed", { error: err instanceof Error ? err.message : String(err) });
573
+ params.logger?.warn?.("msteams botFramework token acquisition failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
403
574
  return;
404
575
  }
405
576
  if (!accessToken) return;
@@ -500,7 +671,7 @@ async function downloadMSTeamsBotFrameworkAttachments(params) {
500
671
  sourceId: attachmentId
501
672
  });
502
673
  params.logger?.warn?.("msteams botFramework attachment download failed", {
503
- error: err instanceof Error ? err.message : String(err),
674
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
504
675
  attachmentId
505
676
  });
506
677
  }
@@ -713,9 +884,6 @@ function scopeCandidatesForUrl(url) {
713
884
  return ["https://api.botframework.com", "https://graph.microsoft.com"];
714
885
  }
715
886
  }
716
- function isRedirectStatus(status) {
717
- return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
718
- }
719
887
  async function resolveInlineDataImageMime(inline) {
720
888
  const mime = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(await require_runtime.getMSTeamsRuntime().media.detectMime({
721
889
  buffer: inline.data,
@@ -738,7 +906,7 @@ async function fetchWithAuthFallback(params) {
738
906
  const tokenProvider = params.tokenProvider;
739
907
  if (firstAttempt.status !== 401 && firstAttempt.status !== 403) return firstAttempt;
740
908
  if (!require_resolve_allowlist.isUrlAllowed(params.url, params.policy.authAllowHosts)) return firstAttempt;
741
- await firstAttempt.body?.cancel();
909
+ let fallbackAttempt = firstAttempt;
742
910
  const scopes = scopeCandidatesForUrl(params.url);
743
911
  const fetchFn = params.fetchFn ?? fetch;
744
912
  for (const scope of scopes) try {
@@ -761,15 +929,11 @@ async function fetchWithAuthFallback(params) {
761
929
  resolveFn: params.resolveFn,
762
930
  timeoutMs: require_resolve_allowlist.resolveMSTeamsRequestTimeoutMs(params.deadline)
763
931
  });
764
- if (authAttempt.ok) return authAttempt;
765
- if (isRedirectStatus(authAttempt.status)) return authAttempt;
766
- if (authAttempt.status !== 401 && authAttempt.status !== 403) {
767
- await authAttempt.body?.cancel();
768
- continue;
769
- }
770
- await authAttempt.body?.cancel();
932
+ await fallbackAttempt.body?.cancel().catch(() => void 0);
933
+ if (authAttempt.ok || require_resolve_allowlist.isRedirectStatus(authAttempt.status)) return authAttempt;
934
+ fallbackAttempt = authAttempt;
771
935
  } catch {}
772
- return firstAttempt;
936
+ return fallbackAttempt;
773
937
  }
774
938
  /**
775
939
  * Download all file attachments from a Teams message (images, documents, etc.).
@@ -850,7 +1014,7 @@ async function downloadMSTeamsAttachments(params) {
850
1014
  }, candidate.sourceId));
851
1015
  } catch (err) {
852
1016
  out.push(withSourceId({ kind: candidate.mediaKind }, candidate.sourceId));
853
- params.logger?.warn?.("msteams inline attachment decode failed", { error: err instanceof Error ? err.message : String(err) });
1017
+ params.logger?.warn?.("msteams inline attachment decode failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
854
1018
  }
855
1019
  continue;
856
1020
  }
@@ -882,7 +1046,7 @@ async function downloadMSTeamsAttachments(params) {
882
1046
  out.push(withSourceId(media, candidate.sourceId));
883
1047
  } catch (err) {
884
1048
  out.push(withSourceId({ kind: candidate.mediaKind }, candidate.sourceId));
885
- const msg = err instanceof Error ? err.message : String(err);
1049
+ const msg = (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err);
886
1050
  params.logger?.warn?.(`msteams attachment download failed host=${safeHostForLog(candidate.url)} error=${msg}`);
887
1051
  }
888
1052
  }
@@ -921,6 +1085,10 @@ function buildMSTeamsGraphMessageUrl(params) {
921
1085
  if (!chatId) return;
922
1086
  return `${require_resolve_allowlist.GRAPH_ROOT}/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}`;
923
1087
  }
1088
+ async function releaseGraphResponse(response, release) {
1089
+ if (!response.bodyUsed) response.body?.cancel().catch(() => void 0);
1090
+ await release();
1091
+ }
924
1092
  async function fetchGraphCollection(params) {
925
1093
  const fetchFn = params.fetchFn ?? fetch;
926
1094
  const { response, release } = await (0, openclaw_plugin_sdk_ssrf_runtime.fetchWithSsrFGuard)({
@@ -949,7 +1117,7 @@ async function fetchGraphCollection(params) {
949
1117
  };
950
1118
  }
951
1119
  } finally {
952
- await release();
1120
+ await releaseGraphResponse(response, release);
953
1121
  }
954
1122
  }
955
1123
  function normalizeGraphAttachment(att) {
@@ -981,7 +1149,7 @@ async function downloadGraphHostedContent(params) {
981
1149
  deadline: params.deadline
982
1150
  });
983
1151
  } catch (err) {
984
- params.logger?.warn?.("msteams graph hostedContents fetch failed", { error: err instanceof Error ? err.message : String(err) });
1152
+ params.logger?.warn?.("msteams graph hostedContents fetch failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
985
1153
  return {
986
1154
  media: [],
987
1155
  count: 0
@@ -1026,11 +1194,11 @@ async function downloadGraphHostedContent(params) {
1026
1194
  sourceId: item.id
1027
1195
  });
1028
1196
  } finally {
1029
- await release();
1197
+ await releaseGraphResponse(valRes, release);
1030
1198
  }
1031
1199
  } catch (err) {
1032
1200
  out.push(createGraphHostedContentFact(item));
1033
- params.logger?.warn?.("msteams graph hostedContent value fetch failed", { error: err instanceof Error ? err.message : String(err) });
1201
+ params.logger?.warn?.("msteams graph hostedContent value fetch failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1034
1202
  continue;
1035
1203
  }
1036
1204
  }
@@ -1059,9 +1227,9 @@ async function downloadMSTeamsGraphMedia(params) {
1059
1227
  } catch (err) {
1060
1228
  params.logger?.debug?.("graph media token acquisition failed", {
1061
1229
  messageUrl,
1062
- error: err instanceof Error ? err.message : String(err)
1230
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)
1063
1231
  });
1064
- params.logger?.warn?.("msteams graph token acquisition failed", { error: err instanceof Error ? err.message : String(err) });
1232
+ params.logger?.warn?.("msteams graph token acquisition failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1065
1233
  return {
1066
1234
  media: [],
1067
1235
  messageUrl,
@@ -1092,10 +1260,10 @@ async function downloadMSTeamsGraphMedia(params) {
1092
1260
  } catch (err) {
1093
1261
  params.logger?.debug?.("graph media message parse failed", {
1094
1262
  messageUrl,
1095
- error: err instanceof Error ? err.message : String(err)
1263
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)
1096
1264
  });
1097
1265
  params.logger?.warn?.("msteams graph message parse failed", {
1098
- error: err instanceof Error ? err.message : String(err),
1266
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
1099
1267
  messageUrl
1100
1268
  });
1101
1269
  msgData = {};
@@ -1107,14 +1275,14 @@ async function downloadMSTeamsGraphMedia(params) {
1107
1275
  status: messageStatus
1108
1276
  });
1109
1277
  } finally {
1110
- await release();
1278
+ await releaseGraphResponse(msgRes, release);
1111
1279
  }
1112
1280
  } catch (err) {
1113
1281
  params.logger?.debug?.("graph media message fetch failed", {
1114
1282
  messageUrl,
1115
- error: err instanceof Error ? err.message : String(err)
1283
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)
1116
1284
  });
1117
- params.logger?.warn?.("msteams graph message fetch failed", { error: err instanceof Error ? err.message : String(err) });
1285
+ params.logger?.warn?.("msteams graph message fetch failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1118
1286
  }
1119
1287
  for (const att of referenceAttachments) {
1120
1288
  const name = att.name ?? "file";
@@ -1180,7 +1348,7 @@ async function downloadMSTeamsGraphMedia(params) {
1180
1348
  } catch (err) {
1181
1349
  sharePointMedia.push(unavailableMedia);
1182
1350
  params.logger?.warn?.("msteams SharePoint reference download failed", {
1183
- error: err instanceof Error ? err.message : String(err),
1351
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
1184
1352
  name
1185
1353
  });
1186
1354
  }
@@ -1219,7 +1387,7 @@ async function downloadMSTeamsGraphMedia(params) {
1219
1387
  });
1220
1388
  } catch (err) {
1221
1389
  params.logger?.warn?.("msteams graph attachment download failed", {
1222
- error: err instanceof Error ? err.message : String(err),
1390
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
1223
1391
  messageUrl
1224
1392
  });
1225
1393
  }
@@ -1237,132 +1405,296 @@ async function downloadMSTeamsGraphMedia(params) {
1237
1405
  };
1238
1406
  }
1239
1407
  //#endregion
1240
- //#region extensions/msteams/src/thread-parent-context.ts
1241
- const PARENT_CACHE_TTL_MS = 300 * 1e3;
1242
- const PARENT_CACHE_MAX = 100;
1243
- const parentCache = /* @__PURE__ */ new Map();
1244
- const INJECTED_MAX = 200;
1245
- const injectedParents = /* @__PURE__ */ new Map();
1246
- function touchLru(map, key, value, max) {
1247
- if (map.has(key)) map.delete(key);
1248
- else if (map.size >= max) {
1249
- const firstKey = map.keys().next().value;
1250
- if (firstKey !== void 0) map.delete(firstKey);
1251
- }
1252
- map.set(key, value);
1408
+ //#region extensions/msteams/src/monitor-handler/inbound-media.ts
1409
+ function shouldAttemptMSTeamsGraphMediaFallback(params) {
1410
+ const conversationType = params.conversationType.trim().toLowerCase();
1411
+ return params.graphMediaFallback === true && (conversationType === "channel" || conversationType === "groupchat") && (params.htmlSummary?.htmlAttachments ?? 0) > 0;
1253
1412
  }
1254
- function buildParentCacheKey(groupId, channelId, parentId) {
1255
- return `${groupId}\u0000${channelId}\u0000${parentId}`;
1413
+ function resolveMSTeamsInboundMediaBody(params) {
1414
+ const unavailableCount = params.materializedMedia.filter((media) => !media.path).length + Math.max(0, params.nativeMedia.length - params.materializedMedia.length);
1415
+ if (unavailableCount === 0) return params.body;
1416
+ return (0, openclaw_plugin_sdk_channel_inbound.formatInboundMediaUnavailableText)({
1417
+ body: params.body,
1418
+ notice: `[msteams ${unavailableCount > 1 ? `${unavailableCount} attachments` : "attachment"} unavailable]`
1419
+ });
1256
1420
  }
1257
- function resolveParentCacheExpiresAt(nowRaw) {
1258
- const nowMs = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(nowRaw);
1259
- return nowMs === void 0 ? void 0 : (0, openclaw_plugin_sdk_number_runtime.resolveExpiresAtMsFromDurationMs)(PARENT_CACHE_TTL_MS, { nowMs });
1421
+ function hasDefinitiveContentType(media) {
1422
+ const contentType = media.contentType?.split(";", 1)[0]?.trim().toLowerCase();
1423
+ return Boolean(contentType && contentType !== "application/octet-stream" && contentType !== "binary/octet-stream");
1260
1424
  }
1261
- /**
1262
- * Fetch a channel parent message with an LRU+TTL cache.
1263
- *
1264
- * Uses the injected `fetchParent` (defaults to `fetchChannelMessage`) so
1265
- * tests can swap in a stub without mocking the Graph transport.
1266
- */
1267
- async function fetchParentMessageCached(token, groupId, channelId, parentId, fetchParent = require_messenger.fetchChannelMessage) {
1268
- const key = buildParentCacheKey(groupId, channelId, parentId);
1269
- const now = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(Date.now());
1270
- const cached = parentCache.get(key);
1271
- const cachedExpiresAt = cached ? (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(cached.expiresAt) : void 0;
1272
- if (cached && now !== void 0 && cachedExpiresAt !== void 0 && cachedExpiresAt > now) {
1273
- parentCache.delete(key);
1274
- parentCache.set(key, cached);
1275
- return cached.message;
1425
+ function mergeMSTeamsMediaFacts(nativeMedia, materializedMedia, options = {}) {
1426
+ const merged = [...nativeMedia];
1427
+ const nativeSlotCount = nativeMedia.length;
1428
+ const nativeIndexBySourceId = /* @__PURE__ */ new Map();
1429
+ nativeMedia.forEach((media, index) => {
1430
+ if (media.sourceId && !nativeIndexBySourceId.has(media.sourceId)) nativeIndexBySourceId.set(media.sourceId, index);
1431
+ });
1432
+ for (const [index, materialized] of materializedMedia.entries()) {
1433
+ const sourceIndex = materialized.sourceId ? nativeIndexBySourceId.get(materialized.sourceId) : void 0;
1434
+ const positionalIndex = options.positionallyAligned === false || index >= nativeMedia.length ? void 0 : index;
1435
+ const mayUseFallbackOrder = options.positionallyAligned === false;
1436
+ const isEligibleUnresolved = (media) => !media.path && !media.sourceId;
1437
+ const sameKindUnresolvedIndexes = mayUseFallbackOrder ? merged.slice(0, nativeSlotCount).flatMap((media, mediaIndex) => isEligibleUnresolved(media) && media.kind === materialized.kind ? [mediaIndex] : []) : [];
1438
+ const unresolvedIndexes = sameKindUnresolvedIndexes.length === 0 && mayUseFallbackOrder ? merged.slice(0, nativeSlotCount).flatMap((media, mediaIndex) => isEligibleUnresolved(media) ? [mediaIndex] : []) : [];
1439
+ const fallbackIndex = sameKindUnresolvedIndexes.length > 0 ? sameKindUnresolvedIndexes[0] : unresolvedIndexes.length === 1 ? unresolvedIndexes[0] : void 0;
1440
+ const targetIndex = sourceIndex ?? positionalIndex ?? fallbackIndex;
1441
+ if (targetIndex === void 0) {
1442
+ if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, merged.length);
1443
+ merged.push(materialized);
1444
+ } else {
1445
+ if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, targetIndex);
1446
+ const current = merged[targetIndex];
1447
+ if (materialized.path) merged[targetIndex] = materialized;
1448
+ else if (!current?.path) merged[targetIndex] = {
1449
+ ...current,
1450
+ ...materialized,
1451
+ kind: hasDefinitiveContentType(materialized) || !current?.kind ? materialized.kind : current.kind
1452
+ };
1453
+ }
1276
1454
  }
1277
- if (cached) parentCache.delete(key);
1278
- const message = await fetchParent(token, groupId, channelId, parentId);
1279
- const expiresAt = resolveParentCacheExpiresAt(Date.now());
1280
- if (expiresAt !== void 0) touchLru(parentCache, key, {
1281
- message,
1282
- expiresAt
1283
- }, PARENT_CACHE_MAX);
1284
- return message;
1285
- }
1286
- const PARENT_TEXT_MAX_CHARS = 400;
1287
- /**
1288
- * Extract a compact summary (sender + plain-text body) from a Graph parent
1289
- * message. Returns undefined when the parent cannot be summarized (missing
1290
- * or blank body).
1291
- */
1292
- function summarizeParentMessage(message) {
1293
- if (!message) return;
1294
- const sender = message.from?.user?.displayName ?? message.from?.application?.displayName ?? "unknown";
1295
- const contentType = message.body?.contentType ?? "text";
1296
- const raw = message.body?.content ?? "";
1297
- const text = contentType === "html" ? require_messenger.stripHtmlFromTeamsMessage(raw) : raw.replace(/\s+/g, " ").trim();
1298
- if (!text) return;
1299
- return {
1300
- sender,
1301
- text: text.length > PARENT_TEXT_MAX_CHARS ? `${(0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(text, PARENT_TEXT_MAX_CHARS - 1)}…` : text
1302
- };
1303
- }
1304
- /**
1305
- * Build the single-line `Replying to @sender: body` system event text.
1306
- * Callers should pass this text to `enqueueSystemEvent` together with a
1307
- * stable contextKey derived from the parent id.
1308
- */
1309
- function formatParentContextEvent(summary) {
1310
- return `Replying to @${summary.sender}: ${summary.text}`;
1311
- }
1312
- /**
1313
- * Decide whether a parent context event should be enqueued for the current
1314
- * session. Returns `false` when we already injected the same parent for this
1315
- * session recently (prevents re-prepending identical context on every reply
1316
- * in the thread).
1317
- */
1318
- function shouldInjectParentContext(sessionKey, parentId) {
1319
- const key = sessionKey;
1320
- return injectedParents.get(key) !== parentId;
1321
- }
1322
- /**
1323
- * Record that `parentId` was just injected for `sessionKey` so subsequent
1324
- * replies with the same parent can short-circuit via `shouldInjectParentContext`.
1325
- */
1326
- function markParentContextInjected(sessionKey, parentId) {
1327
- touchLru(injectedParents, sessionKey, parentId, INJECTED_MAX);
1455
+ return merged;
1328
1456
  }
1329
- //#endregion
1330
- //#region extensions/msteams/src/reply-stream-controller.ts
1331
- function isStreamCancelledError(err) {
1332
- return err instanceof Error && err.name === "StreamCancelledError";
1457
+ function hasMaterializedMedia(media) {
1458
+ return media.some((entry) => Boolean(entry.path));
1333
1459
  }
1334
- /**
1335
- * Bridges openclaw's reply pipeline callbacks to the SDK's `ctx.stream`.
1336
- * Streaming is enabled for personal (DM) conversations only; group/channel
1337
- * messages fall through to block delivery.
1338
- *
1339
- * Streaming modes (resolved from `cfg.channels.msteams.streaming.preview`):
1340
- * - "partial" (default): per-token streaming via `stream.emit(text)`. Each
1341
- * chunk goes onto the live preview card in Teams.
1342
- * - "progress": no per-token streaming; the preview card carries an
1343
- * informative status that updates as tools run (e.g. "Looking up the
1344
- * schema..." → "Generating SQL..."). When tool-progress streaming is also
1345
- * enabled, raw tool names appear as bullets above the label.
1346
- * - "block": disable native streaming entirely; the reply lands as a regular
1347
- * block message. We bypass the controller in that case.
1348
- */
1349
- function createTeamsReplyStreamController(params) {
1350
- const isPersonal = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationType) === "personal";
1351
- const streamMode = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelPreviewStreamMode)(params.msteamsConfig, "partial");
1352
- const shouldUseNativeStream = isPersonal && (streamMode === "partial" || streamMode === "progress");
1353
- const shouldStreamPreviewToolProgress = streamMode === "progress" && (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingPreviewToolProgress)(params.msteamsConfig);
1354
- const stream = shouldUseNativeStream ? params.context.stream : void 0;
1355
- let tokensEmitted = false;
1356
- let streamFinalizationPending = false;
1357
- let canceledLocally = false;
1358
- let streamFailed = false;
1359
- let lastInformativeText = "";
1360
- let progressLines = [];
1361
- let latestPlan;
1362
- let latestPlanExplanation;
1363
- let pendingFinalPayload;
1364
- let emittedText = "";
1460
+ async function resolveMSTeamsInboundMedia(params) {
1461
+ const { attachments, htmlSummary, maxBytes, tokenProvider, allowHosts, conversationType, conversationId, conversationMessageId, teamAadGroupId, serviceUrl, activity, log, preserveFilenames } = params;
1462
+ let mediaList = await downloadMSTeamsAttachments({
1463
+ attachments,
1464
+ maxBytes,
1465
+ tokenProvider,
1466
+ allowHosts,
1467
+ authAllowHosts: params.authAllowHosts,
1468
+ preserveFilenames,
1469
+ deadline: params.deadline,
1470
+ logger: log
1471
+ });
1472
+ if (!hasMaterializedMedia(mediaList)) {
1473
+ const attachmentIds = extractMSTeamsHtmlAttachmentIds(attachments);
1474
+ const hasHtmlFileAttachment = attachmentIds.length > 0;
1475
+ const hasChannelOrGroupHtml = shouldAttemptMSTeamsGraphMediaFallback({
1476
+ conversationType,
1477
+ htmlSummary,
1478
+ graphMediaFallback: params.graphMediaFallback
1479
+ });
1480
+ const shouldFetchGraphMessage = hasHtmlFileAttachment || hasChannelOrGroupHtml;
1481
+ const isBotFrameworkPersonalChat = isBotFrameworkPersonalChatId(conversationId);
1482
+ if (hasHtmlFileAttachment && isBotFrameworkPersonalChat) if (!serviceUrl) log.debug?.("bot framework attachment skipped (missing serviceUrl)", {
1483
+ conversationType,
1484
+ conversationId
1485
+ });
1486
+ else {
1487
+ const bfMedia = await downloadMSTeamsBotFrameworkAttachments({
1488
+ serviceUrl,
1489
+ attachmentIds,
1490
+ tokenProvider,
1491
+ maxBytes,
1492
+ allowHosts,
1493
+ authAllowHosts: params.authAllowHosts,
1494
+ preserveFilenames,
1495
+ deadline: params.deadline
1496
+ });
1497
+ if (bfMedia.media.length > 0) mediaList = mergeMSTeamsMediaFacts(mediaList, bfMedia.media, { positionallyAligned: false });
1498
+ if (!hasMaterializedMedia(bfMedia.media)) log.debug?.("bot framework attachments fetch empty", {
1499
+ conversationType,
1500
+ attachmentCount: bfMedia.attachmentCount ?? attachmentIds.length
1501
+ });
1502
+ }
1503
+ if (shouldFetchGraphMessage && !hasMaterializedMedia(mediaList) && !isBotFrameworkPersonalChat) {
1504
+ const graphTeamAadGroupId = conversationType.trim().toLowerCase() === "channel" && !teamAadGroupId ? await params.resolveTeamAadGroupId?.() : teamAadGroupId;
1505
+ const messageUrl = buildMSTeamsGraphMessageUrl({
1506
+ conversationType,
1507
+ conversationId,
1508
+ messageId: activity.id ?? void 0,
1509
+ threadRootMessageId: conversationMessageId ?? activity.replyToId,
1510
+ teamAadGroupId: graphTeamAadGroupId,
1511
+ channelId: activity.channelData?.channel?.id
1512
+ });
1513
+ if (!messageUrl) log.debug?.("graph message url unavailable", {
1514
+ conversationType,
1515
+ hasChannelData: Boolean(activity.channelData),
1516
+ messageId: activity.id ?? void 0,
1517
+ replyToId: activity.replyToId ?? void 0
1518
+ });
1519
+ else {
1520
+ const graphMedia = await downloadMSTeamsGraphMedia({
1521
+ messageUrl,
1522
+ tokenProvider,
1523
+ maxBytes,
1524
+ allowHosts,
1525
+ authAllowHosts: params.authAllowHosts,
1526
+ preserveFilenames,
1527
+ deadline: params.deadline,
1528
+ logger: log
1529
+ });
1530
+ if (graphMedia.media.length > 0) mediaList = mergeMSTeamsMediaFacts(mediaList, graphMedia.media, { positionallyAligned: false });
1531
+ if (!hasMaterializedMedia(mediaList)) log.debug?.("graph media fetch empty", {
1532
+ messageUrl,
1533
+ hostedStatus: graphMedia.hostedStatus,
1534
+ attachmentStatus: graphMedia.attachmentStatus,
1535
+ hostedCount: graphMedia.hostedCount,
1536
+ attachmentCount: graphMedia.attachmentCount,
1537
+ tokenError: graphMedia.tokenError,
1538
+ attachmentIdCount: attachmentIds.length
1539
+ });
1540
+ }
1541
+ }
1542
+ }
1543
+ const materializedCount = mediaList.filter((media) => Boolean(media.path)).length;
1544
+ if (materializedCount > 0) log.debug?.("downloaded attachments", { count: materializedCount });
1545
+ else if (htmlSummary?.imgTags) log.debug?.("inline images detected but none downloaded", {
1546
+ imgTags: htmlSummary.imgTags,
1547
+ srcHosts: htmlSummary.srcHosts,
1548
+ dataImages: htmlSummary.dataImages,
1549
+ cidImages: htmlSummary.cidImages
1550
+ });
1551
+ return mediaList;
1552
+ }
1553
+ //#endregion
1554
+ //#region extensions/msteams/src/monitor-handler/inbound-content.ts
1555
+ async function prepareMSTeamsInboundContent(params) {
1556
+ const activity = params.entry.context.activity;
1557
+ const mayRecoverGraphMedia = Boolean(params.htmlSummary?.attachmentIds.length) || shouldAttemptMSTeamsGraphMediaFallback({
1558
+ conversationType: params.conversationType,
1559
+ htmlSummary: params.htmlSummary,
1560
+ graphMediaFallback: params.graphMediaFallback
1561
+ });
1562
+ if (!params.rawBody && params.advertisedMedia.length === 0 && !mayRecoverGraphMedia) {
1563
+ params.log.debug?.("skipping empty message after stripping mentions");
1564
+ return null;
1565
+ }
1566
+ let mediaList = [];
1567
+ try {
1568
+ mediaList = await require_resolve_allowlist.withMSTeamsRequestDeadline({
1569
+ deadline: params.deadline,
1570
+ label: "MS Teams inbound media",
1571
+ work: () => resolveMSTeamsInboundMedia({
1572
+ attachments: params.entry.attachments,
1573
+ htmlSummary: params.htmlSummary,
1574
+ maxBytes: params.mediaMaxBytes,
1575
+ tokenProvider: params.tokenProvider,
1576
+ allowHosts: params.mediaAllowHosts,
1577
+ authAllowHosts: params.mediaAuthAllowHosts,
1578
+ graphMediaFallback: params.graphMediaFallback,
1579
+ conversationType: params.conversationType,
1580
+ conversationId: params.conversationId,
1581
+ conversationMessageId: params.conversationMessageId,
1582
+ teamAadGroupId: params.teamAadGroupId,
1583
+ resolveTeamAadGroupId: params.resolveTeamAadGroupId,
1584
+ serviceUrl: activity.serviceUrl,
1585
+ activity: {
1586
+ id: activity.id,
1587
+ replyToId: activity.replyToId,
1588
+ channelData: activity.channelData
1589
+ },
1590
+ log: params.log,
1591
+ deadline: params.deadline,
1592
+ preserveFilenames: false
1593
+ })
1594
+ });
1595
+ } catch (err) {
1596
+ params.log.debug?.("failed to resolve inbound Teams media", { error: require_setup_surface.formatUnknownError(err) });
1597
+ }
1598
+ const inboundMedia = mergeMSTeamsMediaFacts(params.advertisedMedia, mediaList);
1599
+ const nativeMediaForComparison = [...params.advertisedMedia, ...mediaList.slice(params.advertisedMedia.length).map((media) => ({
1600
+ contentType: media.contentType,
1601
+ kind: media.kind
1602
+ }))];
1603
+ const agentBody = resolveMSTeamsInboundMediaBody({
1604
+ body: params.rawBody,
1605
+ nativeMedia: nativeMediaForComparison,
1606
+ materializedMedia: inboundMedia
1607
+ });
1608
+ if (!agentBody && inboundMedia.length === 0) {
1609
+ params.log.debug?.("skipping empty message after Graph media recovery");
1610
+ return null;
1611
+ }
1612
+ return {
1613
+ agentBody,
1614
+ inboundMedia
1615
+ };
1616
+ }
1617
+ //#endregion
1618
+ //#region extensions/msteams/src/reply-stream-controller.ts
1619
+ function isStreamCancelledError(err) {
1620
+ return err instanceof Error && err.name === "StreamCancelledError";
1621
+ }
1622
+ /**
1623
+ * Bridges openclaw's reply pipeline callbacks to the SDK's `ctx.stream`.
1624
+ * Streaming is enabled for personal (DM) conversations only; group/channel
1625
+ * messages fall through to block delivery.
1626
+ *
1627
+ * Streaming modes (resolved from `cfg.channels.msteams.streaming.preview`):
1628
+ * - "partial" (default): per-token streaming via `stream.emit(text)`. Each
1629
+ * chunk goes onto the live preview card in Teams.
1630
+ * - "progress": no per-token streaming; the preview card carries an
1631
+ * informative status that updates as tools run (e.g. "Looking up the
1632
+ * schema..." → "Generating SQL..."). When tool-progress streaming is also
1633
+ * enabled, raw tool names appear as bullets above the label.
1634
+ * - "block": disable native streaming entirely; the reply lands as a regular
1635
+ * block message. We bypass the controller in that case.
1636
+ */
1637
+ function createTeamsReplyStreamController(params) {
1638
+ const isPersonal = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationType) === "personal";
1639
+ const streamMode = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelPreviewStreamMode)(params.msteamsConfig, "partial");
1640
+ const shouldUseNativeStream = params.allowProviderPreview && isPersonal && (streamMode === "partial" || streamMode === "progress");
1641
+ const shouldStreamPreviewToolProgress = streamMode === "progress" && (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingPreviewToolProgress)(params.msteamsConfig, true, streamMode);
1642
+ const stream = shouldUseNativeStream ? params.context.stream : void 0;
1643
+ let tokensEmitted = false;
1644
+ let nativeDispatchStarted = false;
1645
+ let nativeDeliveryClaimed = false;
1646
+ let streamFinalizationPending = false;
1647
+ let canceledLocally = false;
1648
+ let streamFailed = false;
1649
+ let lastInformativeText = "";
1650
+ let progressLines = [];
1651
+ let latestPlan;
1652
+ let latestPlanExplanation;
1653
+ let pendingFinalPayload;
1654
+ let emittedText = "";
1655
+ let acknowledgedText = "";
1656
+ let acknowledgedStreamId;
1657
+ let replacementFinalPending = false;
1658
+ let replacementEmitFailed = false;
1659
+ let replacementSettlementPending = false;
1660
+ let replacementTextAwaitingAcknowledgement;
1661
+ let deferredReplacementEntries = [];
1662
+ let finalMetadataQueued = false;
1663
+ let failedSegmentFallbackPrepared = false;
1664
+ const streamEvents = stream?.events;
1665
+ let streamChunkSubscription;
1666
+ if (typeof streamEvents?.on === "function" && typeof streamEvents.off === "function") streamChunkSubscription = streamEvents.on("chunk", (activity) => {
1667
+ const replacementAcknowledgementPending = replacementTextAwaitingAcknowledgement !== void 0;
1668
+ const replacementAcknowledgement = typeof activity.text === "string" && replacementAcknowledgementPending && activity.text === replacementTextAwaitingAcknowledgement;
1669
+ if (activity.type !== "typing" || activity.channelData?.streamType !== "streaming" || !activity.id || !activity.text || acknowledgedStreamId !== void 0 && activity.id !== acknowledgedStreamId || (replacementAcknowledgementPending ? !replacementAcknowledgement : !activity.text.startsWith(acknowledgedText)) || !emittedText.startsWith(activity.text)) return;
1670
+ acknowledgedStreamId = activity.id;
1671
+ acknowledgedText = activity.text;
1672
+ if (activity.text === replacementTextAwaitingAcknowledgement) replacementTextAwaitingAcknowledgement = void 0;
1673
+ });
1365
1674
  const wasCanceled = () => canceledLocally || Boolean(stream?.canceled);
1675
+ const releaseStreamChunkSubscription = () => {
1676
+ if (streamChunkSubscription === void 0) return;
1677
+ streamEvents?.off(streamChunkSubscription);
1678
+ streamChunkSubscription = void 0;
1679
+ };
1680
+ const acknowledgedNativeDelivery = () => {
1681
+ if (!acknowledgedStreamId || !acknowledgedText) return { visibleReplySent: false };
1682
+ return {
1683
+ visibleReplySent: true,
1684
+ content: acknowledgedText,
1685
+ messageId: acknowledgedStreamId
1686
+ };
1687
+ };
1688
+ const fallbackPayloadAfterAcknowledgedText = (payload) => {
1689
+ if (!acknowledgedText || typeof payload.text !== "string" || !payload.text.startsWith(acknowledgedText)) return payload;
1690
+ const remainingText = payload.text.slice(acknowledgedText.length);
1691
+ const hasMedia = Boolean(payload.mediaUrl || payload.mediaUrls?.length);
1692
+ if (!remainingText && !hasMedia) return;
1693
+ return {
1694
+ ...payload,
1695
+ text: remainingText || void 0
1696
+ };
1697
+ };
1366
1698
  const fallbackPayloadForSuppressedFinal = (payload) => {
1367
1699
  return Boolean(payload.mediaUrl || payload.mediaUrls?.length) ? {
1368
1700
  ...payload,
@@ -1370,6 +1702,35 @@ function createTeamsReplyStreamController(params) {
1370
1702
  mediaUrls: void 0
1371
1703
  } : payload;
1372
1704
  };
1705
+ const finalStreamActivity = (text) => ({
1706
+ type: "message",
1707
+ ...text ? { text } : {},
1708
+ entities: [{
1709
+ type: "https://schema.org/Message",
1710
+ "@type": "Message",
1711
+ "@context": "https://schema.org",
1712
+ "@id": "",
1713
+ additionalType: ["AIGeneratedContent"]
1714
+ }],
1715
+ channelData: params.feedbackLoopEnabled ? { feedbackLoopEnabled: true } : {}
1716
+ });
1717
+ const deferredReplacementLogicalContent = () => {
1718
+ return deferredReplacementEntries.map((entry) => entry.payload.text).filter((text) => Boolean(text)).join("\n") || void 0;
1719
+ };
1720
+ const takeDeferredReplacementPayloads = (replacementFallback) => {
1721
+ const payloads = deferredReplacementEntries.flatMap((entry) => {
1722
+ if (entry.kind === "payload") return [entry.payload];
1723
+ const hasMedia = Boolean(entry.payload.mediaUrl || entry.payload.mediaUrls?.length);
1724
+ const text = replacementFallback?.text;
1725
+ if (!text && !hasMedia) return [];
1726
+ return [{
1727
+ ...entry.payload,
1728
+ text: text || void 0
1729
+ }];
1730
+ });
1731
+ deferredReplacementEntries = [];
1732
+ return payloads;
1733
+ };
1373
1734
  /**
1374
1735
  * Render the current informative status line into the streaming card. Pulls
1375
1736
  * the rotating "Thinking..." label from msteams config (or the plugin-sdk
@@ -1394,14 +1755,19 @@ function createTeamsReplyStreamController(params) {
1394
1755
  canceledLocally = true;
1395
1756
  return;
1396
1757
  }
1397
- params.log?.debug?.(`stream informative update failed: ${err instanceof Error ? err.message : String(err)}`);
1758
+ params.log?.debug?.(`stream informative update failed: ${(0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)}`);
1398
1759
  }
1399
1760
  };
1400
1761
  const progressDraftGate = (0, openclaw_plugin_sdk_channel_outbound.createChannelProgressDraftGate)({ onStart: renderInformativeUpdate });
1401
1762
  return {
1402
1763
  async onReplyStart() {},
1403
1764
  onPartialReply(payload) {
1404
- if (!stream || !payload.text || wasCanceled() || streamMode !== "partial" || streamFinalizationPending) return;
1765
+ if (!stream || !payload.text || wasCanceled() || streamMode !== "partial") return;
1766
+ if (replacementSettlementPending && replacementFinalPending) {
1767
+ pendingFinalPayload = { text: payload.text };
1768
+ return;
1769
+ }
1770
+ if (streamFinalizationPending) return;
1405
1771
  const fullText = payload.text;
1406
1772
  let prefixLength = 0;
1407
1773
  while (prefixLength < emittedText.length && prefixLength < fullText.length && emittedText[prefixLength] === fullText[prefixLength]) prefixLength += 1;
@@ -1410,7 +1776,13 @@ function createTeamsReplyStreamController(params) {
1410
1776
  if (!delta) return;
1411
1777
  if (previousRemainder.trim()) {
1412
1778
  stream.clearText();
1413
- streamFailed = true;
1779
+ if (streamFailed) {
1780
+ streamFinalizationPending = true;
1781
+ return;
1782
+ }
1783
+ replacementFinalPending = true;
1784
+ replacementSettlementPending = true;
1785
+ pendingFinalPayload = { text: fullText };
1414
1786
  streamFinalizationPending = true;
1415
1787
  return;
1416
1788
  }
@@ -1418,13 +1790,14 @@ function createTeamsReplyStreamController(params) {
1418
1790
  stream.emit(delta);
1419
1791
  emittedText = fullText;
1420
1792
  tokensEmitted = true;
1793
+ nativeDispatchStarted = true;
1421
1794
  } catch (err) {
1422
1795
  if (isStreamCancelledError(err)) {
1423
1796
  canceledLocally = true;
1424
1797
  return;
1425
1798
  }
1426
1799
  streamFailed = true;
1427
- params.log?.warn?.(`msteams stream emit failed, falling back to block delivery: ${err instanceof Error ? err.message : String(err)}`);
1800
+ params.log?.warn?.(`msteams stream emit failed, falling back to block delivery: ${(0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)}`);
1428
1801
  }
1429
1802
  },
1430
1803
  /**
@@ -1466,6 +1839,49 @@ function createTeamsReplyStreamController(params) {
1466
1839
  preparePayload(payload) {
1467
1840
  if (!stream) return payload;
1468
1841
  if (wasCanceled()) return;
1842
+ if (replacementSettlementPending) {
1843
+ if (!replacementFinalPending) {
1844
+ deferredReplacementEntries.push({
1845
+ kind: "payload",
1846
+ payload
1847
+ });
1848
+ return;
1849
+ }
1850
+ if (!payload.text) {
1851
+ deferredReplacementEntries.push({
1852
+ kind: "payload",
1853
+ payload
1854
+ });
1855
+ return;
1856
+ }
1857
+ replacementFinalPending = false;
1858
+ deferredReplacementEntries.push({
1859
+ kind: "replacement",
1860
+ payload
1861
+ });
1862
+ pendingFinalPayload = fallbackPayloadForSuppressedFinal(payload);
1863
+ try {
1864
+ replacementTextAwaitingAcknowledgement = payload.text;
1865
+ stream.emit(finalStreamActivity(payload.text));
1866
+ finalMetadataQueued = true;
1867
+ emittedText = payload.text;
1868
+ tokensEmitted = false;
1869
+ return;
1870
+ } catch (err) {
1871
+ replacementTextAwaitingAcknowledgement = void 0;
1872
+ tokensEmitted = false;
1873
+ if (isStreamCancelledError(err)) {
1874
+ canceledLocally = true;
1875
+ pendingFinalPayload = void 0;
1876
+ deferredReplacementEntries = [];
1877
+ return;
1878
+ }
1879
+ streamFailed = true;
1880
+ replacementEmitFailed = true;
1881
+ params.log?.warn?.(`msteams stream replacement failed, falling back to block delivery: ${(0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)}`);
1882
+ return;
1883
+ }
1884
+ }
1469
1885
  if (tokensEmitted && !streamFailed) {
1470
1886
  const hasMedia = Boolean(payload.mediaUrl || payload.mediaUrls?.length);
1471
1887
  pendingFinalPayload = fallbackPayloadForSuppressedFinal(payload);
@@ -1476,8 +1892,15 @@ function createTeamsReplyStreamController(params) {
1476
1892
  text: void 0
1477
1893
  } : void 0;
1478
1894
  }
1895
+ if (streamFailed) {
1896
+ const fallback = failedSegmentFallbackPrepared ? payload : fallbackPayloadAfterAcknowledgedText(payload);
1897
+ failedSegmentFallbackPrepared = true;
1898
+ pendingFinalPayload = void 0;
1899
+ return fallback;
1900
+ }
1479
1901
  if (streamMode === "progress" && payload.text) try {
1480
1902
  stream.emit(payload.text);
1903
+ nativeDispatchStarted = true;
1481
1904
  pendingFinalPayload = fallbackPayloadForSuppressedFinal(payload);
1482
1905
  streamFinalizationPending = true;
1483
1906
  return Boolean(payload.mediaUrl || payload.mediaUrls?.length) ? {
@@ -1489,49 +1912,104 @@ function createTeamsReplyStreamController(params) {
1489
1912
  canceledLocally = true;
1490
1913
  return;
1491
1914
  }
1492
- params.log?.debug?.(`progress-mode finalize failed: ${err instanceof Error ? err.message : String(err)}`);
1915
+ params.log?.debug?.(`progress-mode finalize failed: ${(0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)}`);
1493
1916
  }
1494
1917
  return payload;
1495
1918
  },
1919
+ claimNativeDelivery() {
1920
+ if (!nativeDispatchStarted || nativeDeliveryClaimed) return false;
1921
+ nativeDeliveryClaimed = true;
1922
+ return true;
1923
+ },
1496
1924
  async finalize() {
1497
1925
  progressDraftGate.cancel();
1498
- if (!stream || !streamFinalizationPending || wasCanceled()) return;
1499
- const finalEntities = [{
1500
- type: "https://schema.org/Message",
1501
- "@type": "Message",
1502
- "@context": "https://schema.org",
1503
- "@id": "",
1504
- additionalType: ["AIGeneratedContent"]
1505
- }];
1506
- const finalChannelData = params.feedbackLoopEnabled ? { feedbackLoopEnabled: true } : {};
1926
+ if (!stream || !nativeDispatchStarted) {
1927
+ releaseStreamChunkSubscription();
1928
+ return { visibleReplySent: false };
1929
+ }
1930
+ let logicalContent;
1507
1931
  try {
1508
- stream.emit({
1509
- type: "message",
1510
- entities: finalEntities,
1511
- channelData: finalChannelData
1512
- });
1932
+ if (wasCanceled()) {
1933
+ pendingFinalPayload = void 0;
1934
+ deferredReplacementEntries = [];
1935
+ streamFinalizationPending = false;
1936
+ return acknowledgedNativeDelivery();
1937
+ }
1938
+ if (!streamFinalizationPending) return acknowledgedNativeDelivery();
1939
+ if (replacementSettlementPending && replacementFinalPending && pendingFinalPayload?.text) {
1940
+ replacementFinalPending = false;
1941
+ deferredReplacementEntries.push({
1942
+ kind: "replacement",
1943
+ payload: pendingFinalPayload
1944
+ });
1945
+ replacementTextAwaitingAcknowledgement = pendingFinalPayload.text;
1946
+ logicalContent = deferredReplacementLogicalContent();
1947
+ stream.emit(finalStreamActivity(pendingFinalPayload.text));
1948
+ finalMetadataQueued = true;
1949
+ emittedText = pendingFinalPayload.text;
1950
+ }
1951
+ logicalContent ??= replacementSettlementPending ? deferredReplacementLogicalContent() : void 0;
1952
+ const content = pendingFinalPayload?.text ?? (emittedText || void 0);
1953
+ if (!finalMetadataQueued) stream.emit(finalStreamActivity());
1513
1954
  const result = await stream.close();
1514
1955
  streamFinalizationPending = false;
1515
1956
  if (!result) {
1516
1957
  const fallback = pendingFinalPayload;
1517
1958
  pendingFinalPayload = void 0;
1518
- return fallback;
1959
+ const canceled = wasCanceled();
1960
+ const replacementFallback = replacementSettlementPending && fallback && !canceled ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
1961
+ const fallbackPayload = !replacementSettlementPending && fallback && !canceled ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
1962
+ const postNativePayloads = replacementSettlementPending && !canceled ? takeDeferredReplacementPayloads(replacementFallback) : [];
1963
+ if (canceled) deferredReplacementEntries = [];
1964
+ return {
1965
+ ...acknowledgedNativeDelivery(),
1966
+ ...!canceled && logicalContent ? { logicalContent } : {},
1967
+ ...fallbackPayload ? { fallbackPayload } : {},
1968
+ ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
1969
+ };
1519
1970
  }
1971
+ const replacementFallback = replacementSettlementPending && replacementEmitFailed && pendingFinalPayload ? fallbackPayloadAfterAcknowledgedText(pendingFinalPayload) : void 0;
1520
1972
  pendingFinalPayload = void 0;
1521
- return;
1973
+ const messageId = require_messenger.extractMessageId(result) ?? acknowledgedStreamId;
1974
+ const postNativePayloads = replacementSettlementPending ? takeDeferredReplacementPayloads(replacementFallback) : [];
1975
+ const nativeContent = replacementEmitFailed ? acknowledgedText || void 0 : content;
1976
+ return {
1977
+ visibleReplySent: replacementEmitFailed ? Boolean(nativeContent) : true,
1978
+ ...nativeContent === void 0 ? {} : { content: nativeContent },
1979
+ ...logicalContent ? { logicalContent } : {},
1980
+ ...messageId && (!replacementEmitFailed || nativeContent) ? { messageId } : {},
1981
+ ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
1982
+ };
1522
1983
  } catch (err) {
1523
1984
  if (isStreamCancelledError(err)) {
1524
1985
  canceledLocally = true;
1525
1986
  pendingFinalPayload = void 0;
1987
+ deferredReplacementEntries = [];
1526
1988
  streamFinalizationPending = false;
1527
- return;
1989
+ return acknowledgedNativeDelivery();
1528
1990
  }
1529
1991
  streamFailed = true;
1530
1992
  streamFinalizationPending = false;
1531
- params.log?.warn?.(`msteams stream finalize failed: ${err instanceof Error ? err.message : String(err)}`);
1993
+ params.log?.warn?.(`msteams stream finalize failed: ${(0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)}`);
1532
1994
  const fallback = pendingFinalPayload;
1533
1995
  pendingFinalPayload = void 0;
1534
- return fallback;
1996
+ const replacementFallback = replacementSettlementPending && fallback ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
1997
+ const fallbackPayload = !replacementSettlementPending && fallback ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
1998
+ const postNativePayloads = replacementSettlementPending ? takeDeferredReplacementPayloads(replacementFallback) : [];
1999
+ return {
2000
+ ...acknowledgedNativeDelivery(),
2001
+ ...logicalContent ? { logicalContent } : {},
2002
+ ...fallbackPayload ? { fallbackPayload } : {},
2003
+ ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
2004
+ };
2005
+ } finally {
2006
+ finalMetadataQueued = false;
2007
+ replacementEmitFailed = false;
2008
+ replacementFinalPending = false;
2009
+ replacementSettlementPending = false;
2010
+ replacementTextAwaitingAcknowledgement = void 0;
2011
+ deferredReplacementEntries = [];
2012
+ releaseStreamChunkSubscription();
1535
2013
  }
1536
2014
  },
1537
2015
  hasStream() {
@@ -1616,7 +2094,9 @@ function createMSTeamsReplyDispatcher(params) {
1616
2094
  resolveChannelLimitMb: ({ cfg }) => cfg.channels?.msteams?.mediaMaxMb
1617
2095
  });
1618
2096
  const feedbackLoopEnabled = params.cfg.channels?.msteams?.feedbackEnabled !== false;
2097
+ const hookRunner = (0, openclaw_plugin_sdk_plugin_runtime.getGlobalHookRunner)();
1619
2098
  const streamController = createTeamsReplyStreamController({
2099
+ allowProviderPreview: !((hookRunner?.hasHooks("reply_payload_sending") ?? false) || (hookRunner?.hasHooks("message_sending") ?? false)),
1620
2100
  conversationType,
1621
2101
  context: params.context,
1622
2102
  feedbackLoopEnabled,
@@ -1630,7 +2110,8 @@ function createMSTeamsReplyDispatcher(params) {
1630
2110
  const blockStreamingResolved = teamsStreamMode === "block" ? true : (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingBlockEnabled)(msteamsCfg);
1631
2111
  const blockStreamingEnabled = blockStreamingResolved ?? false;
1632
2112
  const typingIndicatorEnabled = typeof msteamsCfg?.typingIndicator === "boolean" ? msteamsCfg.typingIndicator : true;
1633
- const pendingMessages = [];
2113
+ const pendingDeliveries = [];
2114
+ const joinAcceptedContents = (contents) => contents.filter((content) => Boolean(content)).join("\n");
1634
2115
  const sendMessages = async (messages) => {
1635
2116
  return require_messenger.sendMSTeamsMessages({
1636
2117
  replyStyle: params.replyStyle,
@@ -1669,33 +2150,74 @@ function createMSTeamsReplyDispatcher(params) {
1669
2150
  contextKey: `msteams:delivery-failure:${params.conversationRef.conversation?.id ?? "unknown"}`
1670
2151
  });
1671
2152
  };
1672
- const queueReplyPayload = (payload) => {
1673
- const messages = require_messenger.renderReplyPayloadsToMessages([payload], {
2153
+ const renderReplyPayload = (payload) => {
2154
+ return require_messenger.renderReplyPayloadsToMessages([payload], {
1674
2155
  textChunkLimit: params.textLimit,
1675
2156
  chunkText: true,
1676
2157
  mediaMode: "split",
1677
2158
  tableMode,
1678
2159
  chunkMode
1679
2160
  });
1680
- pendingMessages.push(...messages);
2161
+ };
2162
+ const deliveryOutcome = (delivery) => {
2163
+ const acceptedParts = [...delivery.nativeResult ? [delivery.nativeResult] : [], ...delivery.blockResults];
2164
+ const messageIds = acceptedParts.flatMap((part) => part.messageIds);
2165
+ const content = delivery.errors.length > 0 ? joinAcceptedContents(acceptedParts.map((part) => part.content)) : delivery.content;
2166
+ return {
2167
+ visibleReplySent: acceptedParts.length > 0,
2168
+ ...messageIds.length > 0 ? { messageIds } : {},
2169
+ ...acceptedParts.length > 0 && content !== void 0 ? { content } : {}
2170
+ };
2171
+ };
2172
+ const settlePendingDelivery = (delivery) => {
2173
+ if (delivery.settled || !delivery.blockSettled || delivery.native && !delivery.nativeSettled) return;
2174
+ delivery.settled = true;
2175
+ const outcome = deliveryOutcome(delivery);
2176
+ if (delivery.errors.length === 0) {
2177
+ delivery.finalization.resolve(outcome);
2178
+ return;
2179
+ }
2180
+ const error = delivery.errors.find((candidate) => !(candidate instanceof openclaw_plugin_sdk_error_runtime.PlatformMessageNotDispatchedError)) ?? delivery.errors[0];
2181
+ delivery.finalization.reject(outcome.visibleReplySent ? (0, openclaw_plugin_sdk_channel_inbound.createChannelPartialDeliveryError)(error, {
2182
+ ...outcome,
2183
+ visibleReplySent: true
2184
+ }) : error);
2185
+ };
2186
+ const queueReplyPayload = (payload, messages, native) => {
2187
+ const delivery = {
2188
+ messages,
2189
+ finalization: (0, openclaw_plugin_sdk_extension_shared.createDeferred)(),
2190
+ content: payload.text,
2191
+ blockResults: [],
2192
+ native,
2193
+ nativeSettled: !native,
2194
+ blockSettled: messages.length === 0,
2195
+ settled: false,
2196
+ errors: []
2197
+ };
2198
+ pendingDeliveries.push(delivery);
2199
+ return delivery;
1681
2200
  };
1682
2201
  const flushPendingMessages = async () => {
1683
- if (pendingMessages.length === 0) return;
1684
- const toSend = pendingMessages.splice(0);
1685
- const total = toSend.length;
1686
- let ids;
1687
- try {
1688
- ids = await sendMessages(toSend);
1689
- } catch (batchError) {
1690
- ids = [];
2202
+ for (const delivery of pendingDeliveries) {
2203
+ if (delivery.blockSettled) continue;
2204
+ const toSend = delivery.messages.splice(0);
2205
+ const total = toSend.length;
1691
2206
  let failed = 0;
1692
- let lastFailedError = batchError;
2207
+ let lastFailedError;
2208
+ const sentIds = [];
1693
2209
  for (const msg of toSend) try {
1694
2210
  const msgIds = await sendMessages([msg]);
1695
- ids.push(...msgIds);
2211
+ const validIds = msgIds.filter((id) => id.trim() && id !== "unknown");
2212
+ if (msgIds.length > 0) delivery.blockResults.push({
2213
+ messageIds: validIds,
2214
+ ...msg.text ? { content: msg.text } : {}
2215
+ });
2216
+ sentIds.push(...validIds);
1696
2217
  } catch (msgError) {
1697
2218
  failed += 1;
1698
2219
  lastFailedError = msgError;
2220
+ delivery.errors.push(msgError);
1699
2221
  params.log.debug?.("individual message send failed, continuing with remaining blocks");
1700
2222
  }
1701
2223
  if (failed > 0) {
@@ -1709,8 +2231,14 @@ function createMSTeamsReplyDispatcher(params) {
1709
2231
  error: lastFailedError
1710
2232
  });
1711
2233
  }
2234
+ delivery.blockSettled = true;
2235
+ settlePendingDelivery(delivery);
2236
+ if (sentIds.length > 0) try {
2237
+ params.onSentMessageIds?.(sentIds);
2238
+ } catch (error) {
2239
+ params.log.warn?.("failed to record sent Teams message ids", { error: require_setup_surface.formatUnknownError(error) });
2240
+ }
1712
2241
  }
1713
- if (ids.length > 0) params.onSentMessageIds?.(ids);
1714
2242
  };
1715
2243
  const dispatcherOptions = {
1716
2244
  ...replyPipeline,
@@ -1722,11 +2250,22 @@ function createMSTeamsReplyDispatcher(params) {
1722
2250
  typingCallbacks
1723
2251
  };
1724
2252
  const delivery = {
2253
+ observeMessageSent: true,
1725
2254
  deliver: async (payload) => {
1726
2255
  const preparedPayload = streamController.preparePayload(payload);
1727
- if (!preparedPayload) return;
1728
- queueReplyPayload(preparedPayload);
2256
+ const native = streamController.claimNativeDelivery();
2257
+ const messages = preparedPayload ? renderReplyPayload(preparedPayload) : [];
2258
+ if (!native && messages.length === 0) return {
2259
+ visibleReplySent: false,
2260
+ suppression: { reason: "no_visible_result" }
2261
+ };
2262
+ const pending = queueReplyPayload(payload, messages, native);
1729
2263
  if (blockStreamingEnabled) await flushPendingMessages();
2264
+ settlePendingDelivery(pending);
2265
+ return {
2266
+ visibleReplySent: false,
2267
+ finalization: pending.finalization.promise
2268
+ };
1730
2269
  },
1731
2270
  onError: (err, info) => {
1732
2271
  const errMsg = require_setup_surface.formatUnknownError(err);
@@ -1741,37 +2280,53 @@ function createMSTeamsReplyDispatcher(params) {
1741
2280
  });
1742
2281
  }
1743
2282
  };
1744
- const settleDelivery = () => {
1745
- return flushPendingMessages().catch((err) => {
1746
- const errMsg = require_setup_surface.formatUnknownError(err);
1747
- const classification = require_setup_surface.classifyMSTeamsSendError(err);
1748
- const hint = require_setup_surface.formatMSTeamsSendErrorHint(classification);
1749
- params.runtime.error?.(`msteams flush reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`);
1750
- params.log.error("flush reply failed", {
1751
- error: errMsg,
1752
- classification,
1753
- hint
1754
- });
1755
- }).then(async () => {
1756
- const fallbackPayload = await streamController.finalize().catch((err) => {
1757
- params.log.debug?.("stream finalize failed", { error: require_setup_surface.formatUnknownError(err) });
1758
- });
1759
- if (fallbackPayload) {
1760
- queueReplyPayload(fallbackPayload);
1761
- await flushPendingMessages();
1762
- }
1763
- });
2283
+ const settleDelivery = async () => {
2284
+ await flushPendingMessages();
2285
+ const nativeDelivery = pendingDeliveries.find((candidate) => candidate.native && !candidate.nativeSettled);
2286
+ if (!nativeDelivery) {
2287
+ await streamController.finalize();
2288
+ return;
2289
+ }
2290
+ let nativeResult;
2291
+ try {
2292
+ nativeResult = await streamController.finalize();
2293
+ } catch (error) {
2294
+ nativeDelivery.errors.push(error);
2295
+ nativeDelivery.nativeSettled = true;
2296
+ settlePendingDelivery(nativeDelivery);
2297
+ return;
2298
+ }
2299
+ if (nativeResult.visibleReplySent) nativeDelivery.nativeResult = {
2300
+ messageIds: nativeResult.messageId ? [nativeResult.messageId] : [],
2301
+ ...nativeResult.content !== void 0 ? { content: nativeResult.content } : {}
2302
+ };
2303
+ const hasPostNativePayloads = Boolean(nativeResult.postNativePayloads?.length);
2304
+ if (nativeResult.logicalContent !== void 0) nativeDelivery.content = nativeResult.logicalContent;
2305
+ else if (nativeResult.content !== void 0 && (!nativeResult.fallbackPayload && !hasPostNativePayloads || nativeDelivery.content === void 0)) nativeDelivery.content = nativeResult.content;
2306
+ const afterNativePayloads = [...nativeResult.fallbackPayload ? [nativeResult.fallbackPayload] : [], ...nativeResult.postNativePayloads ?? []];
2307
+ if (afterNativePayloads.length > 0) {
2308
+ nativeDelivery.messages.push(...afterNativePayloads.flatMap((payload) => renderReplyPayload(payload)));
2309
+ nativeDelivery.blockSettled = nativeDelivery.messages.length === 0;
2310
+ }
2311
+ nativeDelivery.nativeSettled = true;
2312
+ if (!nativeDelivery.blockSettled) await flushPendingMessages();
2313
+ settlePendingDelivery(nativeDelivery);
2314
+ if (nativeResult.messageId) try {
2315
+ params.onSentMessageIds?.([nativeResult.messageId]);
2316
+ } catch (error) {
2317
+ params.log.warn?.("failed to record sent Teams message id", { error: require_setup_surface.formatUnknownError(error) });
2318
+ }
1764
2319
  };
1765
- const previewToolProgressEnabled = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingPreviewToolProgress)(msteamsCfg);
2320
+ const previewToolProgressEnabled = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingPreviewToolProgress)(msteamsCfg, true, teamsStreamMode);
1766
2321
  const suppressDefaultToolProgressMessages = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingSuppressDefaultToolProgressMessages)(msteamsCfg);
1767
- const shouldSuppressDefaultToolProgressMessages = teamsStreamMode === "progress" && suppressDefaultToolProgressMessages && previewToolProgressEnabled;
2322
+ const shouldSuppressDefaultToolProgressMessages = streamController.hasStream() && teamsStreamMode === "progress" && suppressDefaultToolProgressMessages && previewToolProgressEnabled;
1768
2323
  const progressCallbacks = streamController.hasStream() ? {
1769
2324
  onReasoningStream: async (payload) => {
1770
2325
  const text = typeof payload?.text === "string" ? payload.text : void 0;
1771
- if (!text) return;
2326
+ if (!text) return false;
1772
2327
  if (payload?.isReasoningSnapshot !== true) {
1773
2328
  await streamController.pushProgressLine(text);
1774
- return;
2329
+ return false;
1775
2330
  }
1776
2331
  await streamController.pushProgressLine((0, openclaw_plugin_sdk_channel_outbound.buildChannelProgressDraftLine)({
1777
2332
  event: "item",
@@ -1780,6 +2335,7 @@ function createMSTeamsReplyDispatcher(params) {
1780
2335
  title: "Reasoning",
1781
2336
  progressText: text
1782
2337
  }));
2338
+ return false;
1783
2339
  },
1784
2340
  onToolStart: async (payload) => {
1785
2341
  const name = typeof payload?.name === "string" ? payload.name : void 0;
@@ -1792,6 +2348,7 @@ function createMSTeamsReplyDispatcher(params) {
1792
2348
  ...typeof payload?.phase === "string" ? { phase: payload.phase } : {},
1793
2349
  ...payload?.args && typeof payload.args === "object" ? { args: payload.args } : {}
1794
2350
  }, detailMode === "explain" || detailMode === "raw" ? { detailMode } : void 0), name ? { toolName: name } : void 0);
2351
+ return false;
1795
2352
  },
1796
2353
  onItemEvent: async (payload) => {
1797
2354
  await streamController.pushProgressLine((0, openclaw_plugin_sdk_channel_outbound.buildChannelProgressDraftLineForEntry)(msteamsCfg, {
@@ -1807,13 +2364,15 @@ function createMSTeamsReplyDispatcher(params) {
1807
2364
  ...typeof payload?.progressText === "string" ? { progressText: payload.progressText } : {},
1808
2365
  ...typeof payload?.meta === "string" ? { meta: payload.meta } : {}
1809
2366
  }));
2367
+ return false;
1810
2368
  },
1811
2369
  onPlanUpdate: async (payload) => {
1812
- if (payload?.phase !== "update") return;
2370
+ if (payload?.phase !== "update") return false;
1813
2371
  await streamController.pushPlanProgress((0, openclaw_plugin_sdk_channel_outbound.normalizeAgentPlanSteps)(payload.steps), { explanation: typeof payload.explanation === "string" ? payload.explanation : void 0 });
2372
+ return false;
1814
2373
  },
1815
2374
  onApprovalEvent: async (payload) => {
1816
- if (payload?.phase !== "requested") return;
2375
+ if (payload?.phase !== "requested") return false;
1817
2376
  await streamController.pushProgressLine((0, openclaw_plugin_sdk_channel_outbound.buildChannelProgressDraftLine)({
1818
2377
  event: "approval",
1819
2378
  phase: payload.phase,
@@ -1822,10 +2381,11 @@ function createMSTeamsReplyDispatcher(params) {
1822
2381
  ...typeof payload?.reason === "string" ? { reason: payload.reason } : {},
1823
2382
  ...typeof payload?.message === "string" ? { message: payload.message } : {}
1824
2383
  }));
2384
+ return false;
1825
2385
  },
1826
2386
  onCommandOutput: async (payload) => {
1827
- if (payload?.phase !== "end") return;
1828
- await streamController.pushProgressLine((0, openclaw_plugin_sdk_channel_outbound.buildChannelProgressDraftLine)({
2387
+ if (payload?.phase !== "end") return false;
2388
+ await streamController.pushProgressLine((0, openclaw_plugin_sdk_channel_outbound.buildChannelProgressDraftLineForEntry)(msteamsCfg, {
1829
2389
  event: "command-output",
1830
2390
  ...typeof payload?.itemId === "string" ? { itemId: payload.itemId } : {},
1831
2391
  ...typeof payload?.toolCallId === "string" ? { toolCallId: payload.toolCallId } : {},
@@ -1835,9 +2395,10 @@ function createMSTeamsReplyDispatcher(params) {
1835
2395
  ...typeof payload?.status === "string" ? { status: payload.status } : {},
1836
2396
  ...typeof payload?.exitCode === "number" ? { exitCode: payload.exitCode } : {}
1837
2397
  }));
2398
+ return false;
1838
2399
  },
1839
2400
  onPatchSummary: async (payload) => {
1840
- if (payload?.phase !== "end") return;
2401
+ if (payload?.phase !== "end") return false;
1841
2402
  await streamController.pushProgressLine((0, openclaw_plugin_sdk_channel_outbound.buildChannelProgressDraftLine)({
1842
2403
  event: "patch",
1843
2404
  ...typeof payload?.itemId === "string" ? { itemId: payload.itemId } : {},
@@ -1850,6 +2411,7 @@ function createMSTeamsReplyDispatcher(params) {
1850
2411
  ...Array.isArray(payload?.deleted) && payload.deleted.every((s) => typeof s === "string") ? { deleted: payload.deleted } : {},
1851
2412
  ...typeof payload?.summary === "string" ? { summary: payload.summary } : {}
1852
2413
  }));
2414
+ return false;
1853
2415
  }
1854
2416
  } : {};
1855
2417
  return {
@@ -1859,7 +2421,10 @@ function createMSTeamsReplyDispatcher(params) {
1859
2421
  },
1860
2422
  delivery,
1861
2423
  replyOptions: {
1862
- ...streamController.hasStream() ? { onPartialReply: (payload) => streamController.onPartialReply(payload) } : {},
2424
+ ...streamController.hasStream() ? { onPartialReply: (payload) => {
2425
+ streamController.onPartialReply(payload);
2426
+ return false;
2427
+ } } : {},
1863
2428
  ...progressCallbacks,
1864
2429
  ...shouldSuppressDefaultToolProgressMessages ? { suppressDefaultToolProgressMessages: true } : {},
1865
2430
  disableBlockStreaming: blockStreamingResolved == null ? void 0 : !blockStreamingResolved,
@@ -1877,14 +2442,7 @@ const sentMessages = (0, openclaw_plugin_sdk_dedupe_runtime.createPersistentDedu
1877
2442
  namespace: "msteams.sent-messages",
1878
2443
  maxEntries: 1e3,
1879
2444
  openStore: (options) => require_runtime.getOptionalMSTeamsRuntime()?.state.openKeyedStore(options),
1880
- logError: (error) => {
1881
- try {
1882
- require_runtime.getOptionalMSTeamsRuntime()?.logging.getChildLogger({
1883
- plugin: "msteams",
1884
- feature: "sent-message-state"
1885
- }).warn("Microsoft Teams persistent sent-message state failed", { error: String(error) });
1886
- } catch {}
1887
- },
2445
+ logError: (0, openclaw_plugin_sdk_plugin_state_runtime.createPluginStateErrorReporter)(require_runtime.getOptionalMSTeamsRuntime, "msteams", "sent-message-state", "Microsoft Teams persistent sent-message state failed"),
1888
2446
  readTimestamp: (record) => record.sentAt
1889
2447
  }
1890
2448
  });
@@ -1901,21 +2459,351 @@ async function wasMSTeamsMessageSentWithPersistence(params) {
1901
2459
  return await sentMessages.lookup(makeKey(params.conversationId, params.messageId));
1902
2460
  }
1903
2461
  //#endregion
1904
- //#region extensions/msteams/src/team-identity.ts
1905
- const teamGroupIdCache = /* @__PURE__ */ new Map();
1906
- const TEAM_GROUP_ID_CACHE_MAX_ENTRIES = 500;
1907
- function cacheTeamGroupId(conversationTeamId, groupId) {
1908
- teamGroupIdCache.set(conversationTeamId, groupId);
1909
- (0, openclaw_plugin_sdk_collection_runtime.pruneMapToMaxSize)(teamGroupIdCache, TEAM_GROUP_ID_CACHE_MAX_ENTRIES);
1910
- }
1911
- /** Resolve the Graph team GUID without ever treating a Bot Framework team ID as equivalent. */
1912
- async function resolveTeamGroupId(params) {
1913
- const activityGroupId = params.aadGroupId?.trim();
1914
- if (activityGroupId) {
1915
- cacheTeamGroupId(params.conversationTeamId, activityGroupId);
1916
- return activityGroupId;
1917
- }
1918
- const cached = teamGroupIdCache.get(params.conversationTeamId);
2462
+ //#region extensions/msteams/src/monitor-handler/inbound-dispatch.ts
2463
+ async function dispatchMSTeamsInboundTurn(params) {
2464
+ const core = require_runtime.getMSTeamsRuntime();
2465
+ const { cfg, runtime, appId, app, tokenProvider, textLimit, log, logVerboseMessage, facts, admission, content, routing, thread, replyStyle, timestamp, contextVisibilityMode, conversationHistories, historyLimit } = params;
2466
+ const { context, activity, rawBody, text, quoteInfo, conversationRef } = facts;
2467
+ const { senderId, senderName, isDirectMessage, allowNameMatching, groupPolicy, commandAuthorized, effectiveGroupAllowFrom } = admission;
2468
+ const { route } = routing;
2469
+ const { agentBody, inboundMedia } = content;
2470
+ const { teamAadGroupId, quoteBodyFull, quoteSenderId, quoteSenderName, threadContext } = thread;
2471
+ const { conversationId, conversationType, isChannel, teamId, graphChannelId } = facts;
2472
+ const teamsFrom = isDirectMessage ? `msteams:${senderId}` : isChannel ? `msteams:channel:${conversationId}` : `msteams:group:${conversationId}`;
2473
+ const teamsTo = isDirectMessage ? `user:${senderId}` : `conversation:${conversationId}`;
2474
+ const envelopeFrom = isDirectMessage ? senderName : conversationType;
2475
+ const buildEnvelope = (0, openclaw_plugin_sdk_channel_inbound.createChannelInboundEnvelopeBuilder)({
2476
+ cfg,
2477
+ route
2478
+ });
2479
+ let combinedBody = buildEnvelope({
2480
+ channel: "Teams",
2481
+ from: envelopeFrom,
2482
+ timestamp,
2483
+ body: agentBody
2484
+ });
2485
+ const isRoomish = !isDirectMessage;
2486
+ const historyKey = isRoomish ? conversationId : void 0;
2487
+ if (isRoomish && historyKey) combinedBody = (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildPendingContext({
2488
+ historyKey,
2489
+ limit: historyLimit,
2490
+ currentMessage: combinedBody,
2491
+ formatEntry: (entry) => buildEnvelope({
2492
+ channel: "Teams",
2493
+ from: conversationType,
2494
+ timestamp: entry.timestamp,
2495
+ previousTimestamp: null,
2496
+ body: `${entry.sender}: ${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`
2497
+ })
2498
+ });
2499
+ const inboundHistory = isRoomish && historyKey && historyLimit > 0 ? (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildInboundHistory({
2500
+ historyKey,
2501
+ limit: historyLimit
2502
+ }) : void 0;
2503
+ const commandBody = text.trim();
2504
+ const quoteSenderAllowed = quoteInfo && quoteInfo.sender ? (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2505
+ isGroup: !isDirectMessage,
2506
+ groupPolicy,
2507
+ allowFrom: effectiveGroupAllowFrom,
2508
+ isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2509
+ allowFrom,
2510
+ senderId: quoteSenderId ?? "",
2511
+ senderName: quoteSenderName,
2512
+ allowNameMatching
2513
+ }).allowed
2514
+ }) : true;
2515
+ const bodyForAgent = threadContext ? `[Thread history]\n${threadContext}\n[/Thread history]\n\n${agentBody}` : agentBody;
2516
+ const nativeChannelId = isChannel && teamAadGroupId ? `${teamAadGroupId}/${graphChannelId}` : void 0;
2517
+ const boundIngress = await resolveMSTeamsSenderAccess({
2518
+ cfg,
2519
+ activity,
2520
+ hasControlCommand: admission.isControlCommand,
2521
+ conversationThreadId: facts.threadId,
2522
+ contextBinding: {
2523
+ agentId: route.agentId,
2524
+ sessionKey: route.sessionKey,
2525
+ ...activity.id ? { messageId: activity.id } : {},
2526
+ ...nativeChannelId ? { nativeChannelId } : {},
2527
+ inboundEventKind: "user_request"
2528
+ }
2529
+ });
2530
+ const ctxPayload = core.channel.inbound.buildContext({
2531
+ channelIngress: boundIngress.channelIngress,
2532
+ channel: "msteams",
2533
+ contextVisibility: contextVisibilityMode,
2534
+ supplemental: { quote: quoteInfo ? {
2535
+ id: quoteInfo.id ?? activity.replyToId ?? void 0,
2536
+ body: quoteBodyFull ?? quoteInfo.body,
2537
+ sender: quoteInfo.sender,
2538
+ senderAllowed: quoteSenderAllowed,
2539
+ isQuote: true
2540
+ } : void 0 },
2541
+ media: await (0, openclaw_plugin_sdk_channel_inbound.toInboundMediaFactsWithMetadata)(inboundMedia),
2542
+ messageId: activity.id,
2543
+ timestamp: timestamp?.getTime() ?? Date.now(),
2544
+ from: teamsFrom,
2545
+ sender: {
2546
+ id: senderId,
2547
+ name: senderName
2548
+ },
2549
+ conversation: {
2550
+ kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
2551
+ id: conversationId,
2552
+ label: envelopeFrom,
2553
+ spaceId: teamId,
2554
+ nativeChannelId
2555
+ },
2556
+ route: {
2557
+ agentId: route.agentId,
2558
+ dmScope: route.dmScope,
2559
+ accountId: route.accountId,
2560
+ routeSessionKey: route.sessionKey
2561
+ },
2562
+ reply: {
2563
+ to: teamsTo,
2564
+ originatingTo: `conversation:${conversationId}`,
2565
+ messageThreadId: facts.threadId ?? void 0,
2566
+ replyToId: activity.replyToId ?? void 0,
2567
+ nativeChannelId
2568
+ },
2569
+ message: {
2570
+ body: combinedBody,
2571
+ bodyForAgent,
2572
+ inboundHistory,
2573
+ rawBody,
2574
+ commandBody
2575
+ },
2576
+ sessionTranscript: { historyLimit: isRoomish ? historyLimit : 0 },
2577
+ access: {
2578
+ mentions: {
2579
+ canDetectMention: !isDirectMessage,
2580
+ wasMentioned: isDirectMessage || params.mentionWasEffective
2581
+ },
2582
+ commands: { authorized: commandAuthorized === true }
2583
+ },
2584
+ extra: {
2585
+ GroupSubject: !isDirectMessage ? conversationType : void 0,
2586
+ ReplyToIsQuote: quoteInfo ? true : void 0
2587
+ }
2588
+ });
2589
+ const preview = (0, openclaw_plugin_sdk_text_utility_runtime.sliceUtf16Safe)(rawBody.replace(/\s+/g, " "), 0, 160);
2590
+ logVerboseMessage(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
2591
+ const { dispatcherOptions, delivery, replyOptions } = createMSTeamsReplyDispatcher({
2592
+ cfg,
2593
+ agentId: route.agentId,
2594
+ sessionKey: route.sessionKey,
2595
+ accountId: route.accountId,
2596
+ runtime,
2597
+ log,
2598
+ app,
2599
+ appId,
2600
+ conversationRef,
2601
+ context,
2602
+ replyStyle,
2603
+ textLimit,
2604
+ onSentMessageIds: (ids) => {
2605
+ for (const id of ids) recordMSTeamsSentMessage(conversationId, id);
2606
+ },
2607
+ tokenProvider,
2608
+ sharePointSiteId: cfg.channels?.msteams?.sharePointSiteId
2609
+ });
2610
+ const senderTimezone = (activity.entities?.find((entity) => entity.type === "clientInfo"))?.timezone || conversationRef.timezone;
2611
+ const turnConfig = senderTimezone && !cfg.agents?.defaults?.userTimezone ? {
2612
+ ...cfg,
2613
+ agents: {
2614
+ ...cfg.agents,
2615
+ defaults: {
2616
+ ...cfg.agents?.defaults,
2617
+ userTimezone: senderTimezone
2618
+ }
2619
+ }
2620
+ } : cfg;
2621
+ log.info("dispatching to agent", { sessionKey: route.sessionKey });
2622
+ try {
2623
+ const turnResult = await core.channel.inbound.run({
2624
+ channel: "msteams",
2625
+ accountId: route.accountId,
2626
+ raw: context,
2627
+ adapter: {
2628
+ ingest: () => ({
2629
+ id: activity.id ?? `${teamsFrom}:${Date.now()}`,
2630
+ timestamp: timestamp?.getTime(),
2631
+ rawText: rawBody,
2632
+ textForAgent: bodyForAgent,
2633
+ textForCommands: commandBody,
2634
+ raw: activity
2635
+ }),
2636
+ resolveTurn: () => ({
2637
+ cfg: turnConfig,
2638
+ channel: "msteams",
2639
+ accountId: route.accountId,
2640
+ route: {
2641
+ agentId: route.agentId,
2642
+ sessionKey: route.sessionKey
2643
+ },
2644
+ ctxPayload,
2645
+ record: { onRecordError: (err) => {
2646
+ logVerboseMessage(`msteams: failed updating session meta: ${require_setup_surface.formatUnknownError(err)}`);
2647
+ } },
2648
+ history: {
2649
+ isGroup: isRoomish,
2650
+ historyKey,
2651
+ historyMap: conversationHistories,
2652
+ limit: historyLimit
2653
+ },
2654
+ dispatcherOptions,
2655
+ delivery,
2656
+ replyOptions: {
2657
+ ...replyOptions,
2658
+ ...facts.turnAdoptionLifecycle ? (0, openclaw_plugin_sdk_channel_outbound.bindIngressLifecycleToReplyOptions)(facts.turnAdoptionLifecycle) : {}
2659
+ }
2660
+ })
2661
+ }
2662
+ });
2663
+ const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
2664
+ const counts = (0, openclaw_plugin_sdk_channel_inbound.resolveInboundReplyDispatchCounts)(dispatchResult);
2665
+ log.info("dispatch complete", {
2666
+ queuedFinal: dispatchResult?.queuedFinal ?? false,
2667
+ counts
2668
+ });
2669
+ if ((0, openclaw_plugin_sdk_channel_inbound.hasFinalInboundReplyDispatch)(dispatchResult)) logVerboseMessage(`msteams: delivered ${counts.final} repl${counts.final === 1 ? "y" : "ies"} to ${teamsTo}`);
2670
+ return {
2671
+ kind: "completed",
2672
+ finalResponses: counts.final
2673
+ };
2674
+ } catch (err) {
2675
+ log.error("dispatch failed", { error: require_setup_surface.formatUnknownError(err) });
2676
+ runtime.error(`msteams dispatch failed: ${require_setup_surface.formatUnknownError(err)}`);
2677
+ if (facts.turnAdoptionLifecycle) throw err;
2678
+ try {
2679
+ await context.sendActivity("⚠️ Something went wrong. Please try again.");
2680
+ } catch {}
2681
+ return { kind: "failed" };
2682
+ }
2683
+ }
2684
+ //#endregion
2685
+ //#region extensions/msteams/src/monitor-handler/inbound-facts.ts
2686
+ function extractTextFromHtmlAttachments(attachments) {
2687
+ for (const attachment of attachments) {
2688
+ const raw = require_resolve_allowlist.extractHtmlFromAttachment(attachment);
2689
+ if (!raw) continue;
2690
+ const text = raw.replace(/<at[^>]*>.*?<\/at>/gis, " ").replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gis, "$2 $1").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/\s+/g, " ").trim();
2691
+ if (text) return text;
2692
+ }
2693
+ return "";
2694
+ }
2695
+ async function prepareMSTeamsDebounceEntry(params) {
2696
+ const activity = params.context.activity;
2697
+ const attachments = Array.isArray(activity.attachments) ? activity.attachments : [];
2698
+ const rawText = activity.text?.trim() ?? "";
2699
+ const htmlText = extractTextFromHtmlAttachments(attachments);
2700
+ const valueText = rawText || htmlText ? "" : serializeMSTeamsAdaptiveCardActionValue(activity.value);
2701
+ const text = require_resolve_allowlist.stripMSTeamsMentionTags(rawText || htmlText || valueText || "");
2702
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
2703
+ const replyToId = activity.replyToId ?? void 0;
2704
+ const implicitMentionKinds = conversationId && replyToId && await wasMSTeamsMessageSentWithPersistence({
2705
+ conversationId,
2706
+ messageId: replyToId
2707
+ }) ? ["reply_to_bot"] : [];
2708
+ return {
2709
+ context: params.context,
2710
+ rawText,
2711
+ text,
2712
+ attachments,
2713
+ wasMentioned: require_resolve_allowlist.wasMSTeamsBotMentioned(activity),
2714
+ implicitMentionKinds,
2715
+ turnAdoptionLifecycle: params.turnAdoptionLifecycle
2716
+ };
2717
+ }
2718
+ function buildStoredConversationReference(params) {
2719
+ const { activity, conversationId, conversationType, teamId, threadId } = params;
2720
+ const from = activity.from;
2721
+ const conversation = activity.conversation;
2722
+ const clientInfo = activity.entities?.find((entity) => entity.type === "clientInfo");
2723
+ const tenantId = activity.channelData?.tenant?.id ?? conversation?.tenantId;
2724
+ const aadObjectId = from?.aadObjectId;
2725
+ const serviceUrl = require_resolve_allowlist.tryNormalizeBotFrameworkServiceUrl(activity.serviceUrl);
2726
+ return {
2727
+ activityId: activity.id,
2728
+ user: from ? {
2729
+ id: from.id,
2730
+ name: from.name,
2731
+ aadObjectId: from.aadObjectId
2732
+ } : void 0,
2733
+ agent: activity.recipient,
2734
+ conversation: {
2735
+ id: conversationId,
2736
+ conversationType,
2737
+ tenantId
2738
+ },
2739
+ ...tenantId ? { tenantId } : {},
2740
+ ...aadObjectId ? { aadObjectId } : {},
2741
+ teamId,
2742
+ channelId: activity.channelId,
2743
+ ...serviceUrl ? { serviceUrl } : {},
2744
+ locale: activity.locale,
2745
+ ...clientInfo?.timezone ? { timezone: clientInfo.timezone } : {},
2746
+ ...threadId ? { threadId } : {}
2747
+ };
2748
+ }
2749
+ function assembleMSTeamsInboundFacts(params) {
2750
+ const { entry, mediaMaxBytes } = params;
2751
+ const activity = entry.context.activity;
2752
+ const conversation = activity.conversation;
2753
+ const rawConversationId = conversation?.id ?? "";
2754
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(rawConversationId);
2755
+ const conversationMessageId = require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId);
2756
+ const conversationType = conversation?.conversationType ?? "personal";
2757
+ const isChannel = conversationType === "channel";
2758
+ const teamId = activity.channelData?.team?.id;
2759
+ const threadId = isChannel ? conversationMessageId ?? activity.replyToId ?? void 0 : void 0;
2760
+ const advertisedMedia = resolveMSTeamsAdvertisedMedia(entry.attachments, {
2761
+ maxInlineBytes: mediaMaxBytes,
2762
+ maxInlineTotalBytes: mediaMaxBytes
2763
+ });
2764
+ return {
2765
+ ...entry,
2766
+ activity,
2767
+ from: activity.from,
2768
+ conversation,
2769
+ rawBody: entry.text,
2770
+ advertisedMedia,
2771
+ quoteInfo: require_resolve_allowlist.extractMSTeamsQuoteInfo(entry.attachments),
2772
+ attachmentTypes: entry.attachments.map((attachment) => typeof attachment.contentType === "string" ? attachment.contentType : void 0).filter(Boolean).slice(0, 3),
2773
+ htmlSummary: summarizeMSTeamsHtmlAttachments(entry.attachments),
2774
+ rawConversationId,
2775
+ conversationId,
2776
+ conversationMessageId,
2777
+ conversationType,
2778
+ isChannel,
2779
+ teamId,
2780
+ graphChannelId: activity.channelData?.channel?.id?.trim() || conversationId,
2781
+ threadId,
2782
+ conversationRef: buildStoredConversationReference({
2783
+ activity,
2784
+ conversationId,
2785
+ conversationType,
2786
+ teamId,
2787
+ threadId
2788
+ })
2789
+ };
2790
+ }
2791
+ //#endregion
2792
+ //#region extensions/msteams/src/team-identity.ts
2793
+ const teamGroupIdCache = /* @__PURE__ */ new Map();
2794
+ const TEAM_GROUP_ID_CACHE_MAX_ENTRIES = 500;
2795
+ function cacheTeamGroupId(conversationTeamId, groupId) {
2796
+ teamGroupIdCache.set(conversationTeamId, groupId);
2797
+ (0, openclaw_plugin_sdk_collection_runtime.pruneMapToMaxSize)(teamGroupIdCache, TEAM_GROUP_ID_CACHE_MAX_ENTRIES);
2798
+ }
2799
+ /** Resolve the Graph team GUID without ever treating a Bot Framework team ID as equivalent. */
2800
+ async function resolveTeamGroupId(params) {
2801
+ const activityGroupId = params.aadGroupId?.trim();
2802
+ if (activityGroupId) {
2803
+ cacheTeamGroupId(params.conversationTeamId, activityGroupId);
2804
+ return activityGroupId;
2805
+ }
2806
+ const cached = teamGroupIdCache.get(params.conversationTeamId);
1919
2807
  if (cached) return cached;
1920
2808
  const getTeamDetails = params.getTeamDetails;
1921
2809
  if (!getTeamDetails) return;
@@ -1929,210 +2817,229 @@ async function resolveTeamGroupId(params) {
1929
2817
  return groupId;
1930
2818
  }
1931
2819
  //#endregion
1932
- //#region extensions/msteams/src/monitor-handler/inbound-media.ts
1933
- function shouldAttemptMSTeamsGraphMediaFallback(params) {
1934
- const conversationType = params.conversationType.trim().toLowerCase();
1935
- return params.graphMediaFallback === true && (conversationType === "channel" || conversationType === "groupchat") && (params.htmlSummary?.htmlAttachments ?? 0) > 0;
2820
+ //#region extensions/msteams/src/thread-parent-context.ts
2821
+ const PARENT_CACHE_TTL_MS = 300 * 1e3;
2822
+ const PARENT_CACHE_MAX = 100;
2823
+ const parentCache = /* @__PURE__ */ new Map();
2824
+ const INJECTED_MAX = 200;
2825
+ const injectedParents = /* @__PURE__ */ new Map();
2826
+ function touchLru(map, key, value, max) {
2827
+ if (map.has(key)) map.delete(key);
2828
+ else if (map.size >= max) {
2829
+ const firstKey = map.keys().next().value;
2830
+ if (firstKey !== void 0) map.delete(firstKey);
2831
+ }
2832
+ map.set(key, value);
1936
2833
  }
1937
- function resolveMSTeamsInboundMediaBody(params) {
1938
- const unavailableCount = params.materializedMedia.filter((media) => !media.path).length + Math.max(0, params.nativeMedia.length - params.materializedMedia.length);
1939
- if (unavailableCount === 0) return params.body;
1940
- return (0, openclaw_plugin_sdk_channel_inbound.formatInboundMediaUnavailableText)({
1941
- body: params.body,
1942
- notice: `[msteams ${unavailableCount > 1 ? `${unavailableCount} attachments` : "attachment"} unavailable]`
1943
- });
2834
+ function buildParentCacheKey(groupId, channelId, parentId) {
2835
+ return `${groupId}\u0000${channelId}\u0000${parentId}`;
1944
2836
  }
1945
- function hasDefinitiveContentType(media) {
1946
- const contentType = media.contentType?.split(";", 1)[0]?.trim().toLowerCase();
1947
- return Boolean(contentType && contentType !== "application/octet-stream" && contentType !== "binary/octet-stream");
2837
+ function resolveParentCacheExpiresAt(nowRaw) {
2838
+ const nowMs = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(nowRaw);
2839
+ return nowMs === void 0 ? void 0 : (0, openclaw_plugin_sdk_number_runtime.resolveExpiresAtMsFromDurationMs)(PARENT_CACHE_TTL_MS, { nowMs });
1948
2840
  }
1949
- function mergeMSTeamsMediaFacts(nativeMedia, materializedMedia, options = {}) {
1950
- const merged = [...nativeMedia];
1951
- const nativeSlotCount = nativeMedia.length;
1952
- const nativeIndexBySourceId = /* @__PURE__ */ new Map();
1953
- nativeMedia.forEach((media, index) => {
1954
- if (media.sourceId && !nativeIndexBySourceId.has(media.sourceId)) nativeIndexBySourceId.set(media.sourceId, index);
1955
- });
1956
- for (const [index, materialized] of materializedMedia.entries()) {
1957
- const sourceIndex = materialized.sourceId ? nativeIndexBySourceId.get(materialized.sourceId) : void 0;
1958
- const positionalIndex = options.positionallyAligned === false || index >= nativeMedia.length ? void 0 : index;
1959
- const mayUseFallbackOrder = options.positionallyAligned === false;
1960
- const isEligibleUnresolved = (media) => !media.path && !media.sourceId;
1961
- const sameKindUnresolvedIndexes = mayUseFallbackOrder ? merged.slice(0, nativeSlotCount).flatMap((media, mediaIndex) => isEligibleUnresolved(media) && media.kind === materialized.kind ? [mediaIndex] : []) : [];
1962
- const unresolvedIndexes = sameKindUnresolvedIndexes.length === 0 && mayUseFallbackOrder ? merged.slice(0, nativeSlotCount).flatMap((media, mediaIndex) => isEligibleUnresolved(media) ? [mediaIndex] : []) : [];
1963
- const fallbackIndex = sameKindUnresolvedIndexes.length > 0 ? sameKindUnresolvedIndexes[0] : unresolvedIndexes.length === 1 ? unresolvedIndexes[0] : void 0;
1964
- const targetIndex = sourceIndex ?? positionalIndex ?? fallbackIndex;
1965
- if (targetIndex === void 0) {
1966
- if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, merged.length);
1967
- merged.push(materialized);
1968
- } else {
1969
- if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, targetIndex);
1970
- const current = merged[targetIndex];
1971
- if (materialized.path) merged[targetIndex] = materialized;
1972
- else if (!current?.path) merged[targetIndex] = {
1973
- ...current,
1974
- ...materialized,
1975
- kind: hasDefinitiveContentType(materialized) || !current?.kind ? materialized.kind : current.kind
1976
- };
1977
- }
2841
+ /**
2842
+ * Fetch a channel parent message with an LRU+TTL cache.
2843
+ *
2844
+ * Uses the injected `fetchParent` (defaults to `fetchChannelMessage`) so
2845
+ * tests can swap in a stub without mocking the Graph transport.
2846
+ */
2847
+ async function fetchParentMessageCached(token, groupId, channelId, parentId, fetchParent = require_messenger.fetchChannelMessage) {
2848
+ const key = buildParentCacheKey(groupId, channelId, parentId);
2849
+ const now = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(Date.now());
2850
+ const cached = parentCache.get(key);
2851
+ const cachedExpiresAt = cached ? (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(cached.expiresAt) : void 0;
2852
+ if (cached && now !== void 0 && cachedExpiresAt !== void 0 && cachedExpiresAt > now) {
2853
+ parentCache.delete(key);
2854
+ parentCache.set(key, cached);
2855
+ return cached.message;
1978
2856
  }
1979
- return merged;
2857
+ if (cached) parentCache.delete(key);
2858
+ const message = await fetchParent(token, groupId, channelId, parentId);
2859
+ const expiresAt = resolveParentCacheExpiresAt(Date.now());
2860
+ if (expiresAt !== void 0) touchLru(parentCache, key, {
2861
+ message,
2862
+ expiresAt
2863
+ }, PARENT_CACHE_MAX);
2864
+ return message;
1980
2865
  }
1981
- function hasMaterializedMedia(media) {
1982
- return media.some((entry) => Boolean(entry.path));
2866
+ const PARENT_TEXT_MAX_CHARS = 400;
2867
+ /**
2868
+ * Extract a compact summary (sender + plain-text body) from a Graph parent
2869
+ * message. Returns undefined when the parent cannot be summarized (missing
2870
+ * or blank body).
2871
+ */
2872
+ function summarizeParentMessage(message) {
2873
+ if (!message) return;
2874
+ const sender = message.from?.user?.displayName ?? message.from?.application?.displayName ?? "unknown";
2875
+ const contentType = message.body?.contentType ?? "text";
2876
+ const raw = message.body?.content ?? "";
2877
+ const text = contentType === "html" ? require_messenger.stripHtmlFromTeamsMessage(raw) : raw.replace(/\s+/g, " ").trim();
2878
+ if (!text) return;
2879
+ return {
2880
+ sender,
2881
+ text: text.length > PARENT_TEXT_MAX_CHARS ? `${(0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(text, PARENT_TEXT_MAX_CHARS - 1)}…` : text
2882
+ };
1983
2883
  }
1984
- async function resolveMSTeamsInboundMedia(params) {
1985
- const { attachments, htmlSummary, maxBytes, tokenProvider, allowHosts, conversationType, conversationId, conversationMessageId, teamAadGroupId, serviceUrl, activity, log, preserveFilenames } = params;
1986
- let mediaList = await downloadMSTeamsAttachments({
1987
- attachments,
1988
- maxBytes,
1989
- tokenProvider,
1990
- allowHosts,
1991
- authAllowHosts: params.authAllowHosts,
1992
- preserveFilenames,
1993
- deadline: params.deadline,
1994
- logger: log
1995
- });
1996
- if (!hasMaterializedMedia(mediaList)) {
1997
- const attachmentIds = extractMSTeamsHtmlAttachmentIds(attachments);
1998
- const hasHtmlFileAttachment = attachmentIds.length > 0;
1999
- const hasChannelOrGroupHtml = shouldAttemptMSTeamsGraphMediaFallback({
2000
- conversationType,
2001
- htmlSummary,
2002
- graphMediaFallback: params.graphMediaFallback
2003
- });
2004
- const shouldFetchGraphMessage = hasHtmlFileAttachment || hasChannelOrGroupHtml;
2005
- const isBotFrameworkPersonalChat = isBotFrameworkPersonalChatId(conversationId);
2006
- if (hasHtmlFileAttachment && isBotFrameworkPersonalChat) if (!serviceUrl) log.debug?.("bot framework attachment skipped (missing serviceUrl)", {
2007
- conversationType,
2008
- conversationId
2009
- });
2010
- else {
2011
- const bfMedia = await downloadMSTeamsBotFrameworkAttachments({
2012
- serviceUrl,
2013
- attachmentIds,
2014
- tokenProvider,
2015
- maxBytes,
2016
- allowHosts,
2017
- authAllowHosts: params.authAllowHosts,
2018
- preserveFilenames,
2019
- deadline: params.deadline
2020
- });
2021
- if (bfMedia.media.length > 0) mediaList = mergeMSTeamsMediaFacts(mediaList, bfMedia.media, { positionallyAligned: false });
2022
- if (!hasMaterializedMedia(bfMedia.media)) log.debug?.("bot framework attachments fetch empty", {
2023
- conversationType,
2024
- attachmentCount: bfMedia.attachmentCount ?? attachmentIds.length
2025
- });
2026
- }
2027
- if (shouldFetchGraphMessage && !hasMaterializedMedia(mediaList) && !isBotFrameworkPersonalChat) {
2028
- const graphTeamAadGroupId = conversationType.trim().toLowerCase() === "channel" && !teamAadGroupId ? await params.resolveTeamAadGroupId?.() : teamAadGroupId;
2029
- const messageUrl = buildMSTeamsGraphMessageUrl({
2030
- conversationType,
2031
- conversationId,
2032
- messageId: activity.id ?? void 0,
2033
- threadRootMessageId: conversationMessageId ?? activity.replyToId,
2034
- teamAadGroupId: graphTeamAadGroupId,
2035
- channelId: activity.channelData?.channel?.id
2036
- });
2037
- if (!messageUrl) log.debug?.("graph message url unavailable", {
2038
- conversationType,
2039
- hasChannelData: Boolean(activity.channelData),
2040
- messageId: activity.id ?? void 0,
2041
- replyToId: activity.replyToId ?? void 0
2042
- });
2043
- else {
2044
- const graphMedia = await downloadMSTeamsGraphMedia({
2045
- messageUrl,
2046
- tokenProvider,
2047
- maxBytes,
2048
- allowHosts,
2049
- authAllowHosts: params.authAllowHosts,
2050
- preserveFilenames,
2051
- deadline: params.deadline,
2052
- logger: log
2053
- });
2054
- if (graphMedia.media.length > 0) mediaList = mergeMSTeamsMediaFacts(mediaList, graphMedia.media, { positionallyAligned: false });
2055
- if (!hasMaterializedMedia(mediaList)) log.debug?.("graph media fetch empty", {
2056
- messageUrl,
2057
- hostedStatus: graphMedia.hostedStatus,
2058
- attachmentStatus: graphMedia.attachmentStatus,
2059
- hostedCount: graphMedia.hostedCount,
2060
- attachmentCount: graphMedia.attachmentCount,
2061
- tokenError: graphMedia.tokenError,
2062
- attachmentIdCount: attachmentIds.length
2063
- });
2064
- }
2884
+ /**
2885
+ * Build the single-line `Replying to @sender: body` system event text.
2886
+ * Callers should pass this text to `enqueueSystemEvent` together with a
2887
+ * stable contextKey derived from the parent id.
2888
+ */
2889
+ function formatParentContextEvent(summary) {
2890
+ return `Replying to @${summary.sender}: ${summary.text}`;
2891
+ }
2892
+ /**
2893
+ * Decide whether a parent context event should be enqueued for the current
2894
+ * session. Returns `false` when we already injected the same parent for this
2895
+ * session recently (prevents re-prepending identical context on every reply
2896
+ * in the thread).
2897
+ */
2898
+ function shouldInjectParentContext(sessionKey, parentId) {
2899
+ const key = sessionKey;
2900
+ return injectedParents.get(key) !== parentId;
2901
+ }
2902
+ /**
2903
+ * Record that `parentId` was just injected for `sessionKey` so subsequent
2904
+ * replies with the same parent can short-circuit via `shouldInjectParentContext`.
2905
+ */
2906
+ function markParentContextInjected(sessionKey, parentId) {
2907
+ touchLru(injectedParents, sessionKey, parentId, INJECTED_MAX);
2908
+ }
2909
+ //#endregion
2910
+ //#region extensions/msteams/src/monitor-handler/thread-context.ts
2911
+ function prepareMSTeamsThreadRouting(params) {
2912
+ const route = require_runtime.getMSTeamsRuntime().channel.routing.resolveAgentRoute({
2913
+ cfg: params.cfg,
2914
+ channel: "msteams",
2915
+ teamId: params.teamId,
2916
+ peer: {
2917
+ kind: params.isDirectMessage ? "direct" : params.isChannel ? "channel" : "group",
2918
+ id: params.isDirectMessage ? params.senderId : params.conversationId
2065
2919
  }
2066
- }
2067
- const materializedCount = mediaList.filter((media) => Boolean(media.path)).length;
2068
- if (materializedCount > 0) log.debug?.("downloaded attachments", { count: materializedCount });
2069
- else if (htmlSummary?.imgTags) log.debug?.("inline images detected but none downloaded", {
2070
- imgTags: htmlSummary.imgTags,
2071
- srcHosts: htmlSummary.srcHosts,
2072
- dataImages: htmlSummary.dataImages,
2073
- cidImages: htmlSummary.cidImages
2074
2920
  });
2075
- return mediaList;
2921
+ route.sessionKey = require_channel.resolveMSTeamsRouteSessionKey({
2922
+ baseSessionKey: route.sessionKey,
2923
+ isChannel: params.isChannel,
2924
+ conversationMessageId: params.conversationMessageId,
2925
+ replyToId: params.context.activity.replyToId
2926
+ });
2927
+ const deadline = require_resolve_allowlist.createMSTeamsInboundDeadline();
2928
+ let teamAadGroupId = params.context.activity.channelData?.team?.aadGroupId?.trim() || void 0;
2929
+ const conversationTeamId = params.isChannel ? params.teamId : void 0;
2930
+ let teamGroupIdPromise;
2931
+ const resolveTeamAadGroupId = async () => {
2932
+ if (!conversationTeamId) return;
2933
+ teamGroupIdPromise ??= resolveTeamGroupId({
2934
+ conversationTeamId,
2935
+ aadGroupId: teamAadGroupId,
2936
+ getTeamDetails: params.context.getTeamDetails,
2937
+ deadline
2938
+ }).catch((err) => {
2939
+ params.log.debug?.("failed to resolve Teams AAD group ID", {
2940
+ teamId: conversationTeamId,
2941
+ error: require_setup_surface.formatUnknownError(err)
2942
+ });
2943
+ });
2944
+ teamAadGroupId = await teamGroupIdPromise;
2945
+ return teamAadGroupId;
2946
+ };
2947
+ return {
2948
+ route,
2949
+ deadline,
2950
+ resolveTeamAadGroupId,
2951
+ getTeamAadGroupId: () => teamAadGroupId
2952
+ };
2076
2953
  }
2077
- //#endregion
2078
- //#region extensions/msteams/src/monitor-handler/message-handler.ts
2079
- function extractTextFromHtmlAttachments(attachments) {
2080
- for (const attachment of attachments) {
2081
- const raw = require_resolve_allowlist.extractHtmlFromAttachment(attachment);
2082
- if (!raw) continue;
2083
- const text = raw.replace(/<at[^>]*>.*?<\/at>/gis, " ").replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gis, "$2 $1").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/\s+/g, " ").trim();
2084
- if (text) return text;
2954
+ async function resolveMSTeamsThreadContext(params) {
2955
+ const core = require_runtime.getMSTeamsRuntime();
2956
+ const activity = params.context.activity;
2957
+ const { route, deadline } = params.routing;
2958
+ const teamAadGroupId = await params.routing.resolveTeamAadGroupId();
2959
+ let quoteBodyFull;
2960
+ let quoteSenderId;
2961
+ let quoteSenderName;
2962
+ const quoteMessageId = params.quoteInfo?.id;
2963
+ if (quoteMessageId && params.isDirectMessage && params.conversationId.startsWith("19:")) try {
2964
+ const graphToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2965
+ deadline,
2966
+ label: "MS Teams quote token",
2967
+ work: () => params.tokenProvider.getAccessToken("https://graph.microsoft.com")
2968
+ });
2969
+ quoteBodyFull = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2970
+ deadline,
2971
+ label: "MS Teams quote lookup",
2972
+ work: () => require_messenger.fetchChatMessageText(graphToken, params.conversationId, quoteMessageId, deadline)
2973
+ });
2974
+ } catch (err) {
2975
+ params.log.debug?.("failed to fetch full quoted message text", { error: require_setup_surface.formatUnknownError(err) });
2085
2976
  }
2086
- return "";
2087
- }
2088
- function formatMSTeamsSenderReason(params) {
2089
- switch (params.reasonCode) {
2090
- case "dm_policy_open": return "dmPolicy=open";
2091
- case "dm_policy_disabled": return "dmPolicy=disabled";
2092
- case "dm_policy_pairing_required": return "dmPolicy=pairing (not allowlisted)";
2093
- case "dm_policy_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (allowlisted)`;
2094
- case "dm_policy_not_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (not allowlisted)`;
2095
- case "group_policy_disabled": return "groupPolicy=disabled";
2096
- case "group_policy_empty_allowlist":
2097
- case "route_sender_empty": return "groupPolicy=allowlist (empty allowlist)";
2098
- case "group_policy_not_allowlisted": return "groupPolicy=allowlist (not allowlisted)";
2099
- case "group_policy_open": return "groupPolicy=open";
2100
- case "group_policy_allowed": return `groupPolicy=${params.groupPolicy ?? "allowlist"}`;
2101
- default: return params.reasonCode;
2977
+ let threadContext;
2978
+ const threadParentId = activity.replyToId;
2979
+ if (threadParentId && params.isChannel && teamAadGroupId) try {
2980
+ const graphToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2981
+ deadline,
2982
+ label: "MS Teams thread token",
2983
+ work: () => params.tokenProvider.getAccessToken("https://graph.microsoft.com")
2984
+ });
2985
+ const [parentResult, repliesResult] = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2986
+ deadline,
2987
+ label: "MS Teams thread history",
2988
+ work: () => Promise.allSettled([fetchParentMessageCached(graphToken, teamAadGroupId, params.conversationId, threadParentId, (token, groupId, requestedChannelId, messageId) => require_messenger.fetchChannelMessage(token, groupId, requestedChannelId, messageId, deadline)), require_messenger.fetchThreadReplies(graphToken, teamAadGroupId, params.conversationId, threadParentId, 50, deadline)])
2989
+ });
2990
+ const parentMsg = parentResult.status === "fulfilled" ? parentResult.value : void 0;
2991
+ const replies = repliesResult.status === "fulfilled" ? repliesResult.value : [];
2992
+ if (parentResult.status === "rejected") params.log.debug?.("failed to fetch parent message", { error: require_setup_surface.formatUnknownError(parentResult.reason) });
2993
+ if (repliesResult.status === "rejected") params.log.debug?.("failed to fetch thread replies", { error: require_setup_surface.formatUnknownError(repliesResult.reason) });
2994
+ const isThreadSenderAllowed = (message) => (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2995
+ isGroup: params.isChannel,
2996
+ groupPolicy: params.groupPolicy,
2997
+ allowFrom: params.effectiveGroupAllowFrom,
2998
+ isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2999
+ allowFrom,
3000
+ senderId: message.from?.user?.id ?? "",
3001
+ senderName: message.from?.user?.displayName,
3002
+ allowNameMatching: params.allowNameMatching
3003
+ }).allowed
3004
+ });
3005
+ const parentSummary = summarizeParentMessage(parentMsg);
3006
+ const visibleParentMessages = parentMsg ? (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
3007
+ items: [parentMsg],
3008
+ mode: params.contextVisibilityMode,
3009
+ kind: "thread",
3010
+ isSenderAllowed: isThreadSenderAllowed
3011
+ }).items : [];
3012
+ if (parentSummary && visibleParentMessages.length > 0 && shouldInjectParentContext(route.sessionKey, threadParentId)) {
3013
+ core.system.enqueueSystemEvent(formatParentContextEvent(parentSummary), {
3014
+ sessionKey: route.sessionKey,
3015
+ contextKey: `msteams:thread-parent:${params.conversationId}:${threadParentId}`
3016
+ });
3017
+ markParentContextInjected(route.sessionKey, threadParentId);
3018
+ }
3019
+ const allMessages = parentMsg ? [parentMsg, ...replies] : replies;
3020
+ quoteSenderId = parentMsg?.from?.user?.id ?? parentMsg?.from?.application?.id ?? void 0;
3021
+ quoteSenderName = parentMsg?.from?.user?.displayName ?? parentMsg?.from?.application?.displayName ?? params.quoteInfo?.sender;
3022
+ const { items: threadMessages } = (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
3023
+ items: allMessages,
3024
+ mode: params.contextVisibilityMode,
3025
+ kind: "thread",
3026
+ isSenderAllowed: isThreadSenderAllowed
3027
+ });
3028
+ threadContext = require_messenger.formatThreadContext(threadMessages, activity.id) || void 0;
3029
+ } catch (err) {
3030
+ params.log.debug?.("failed to fetch thread history", { error: require_setup_surface.formatUnknownError(err) });
2102
3031
  }
2103
- }
2104
- function buildStoredConversationReference(params) {
2105
- const { activity, conversationId, conversationType, teamId, threadId } = params;
2106
- const from = activity.from;
2107
- const conversation = activity.conversation;
2108
- const agent = activity.recipient;
2109
- const clientInfo = activity.entities?.find((e) => e.type === "clientInfo");
2110
- const tenantId = activity.channelData?.tenant?.id ?? conversation?.tenantId;
2111
- const aadObjectId = from?.aadObjectId;
2112
- const serviceUrl = require_resolve_allowlist.tryNormalizeBotFrameworkServiceUrl(activity.serviceUrl);
3032
+ quoteSenderName ??= params.quoteInfo?.sender;
2113
3033
  return {
2114
- activityId: activity.id,
2115
- user: from ? {
2116
- id: from.id,
2117
- name: from.name,
2118
- aadObjectId: from.aadObjectId
2119
- } : void 0,
2120
- agent,
2121
- conversation: {
2122
- id: conversationId,
2123
- conversationType,
2124
- tenantId
2125
- },
2126
- ...tenantId ? { tenantId } : {},
2127
- ...aadObjectId ? { aadObjectId } : {},
2128
- teamId,
2129
- channelId: activity.channelId,
2130
- ...serviceUrl ? { serviceUrl } : {},
2131
- locale: activity.locale,
2132
- ...clientInfo?.timezone ? { timezone: clientInfo.timezone } : {},
2133
- ...threadId ? { threadId } : {}
3034
+ teamAadGroupId,
3035
+ quoteBodyFull,
3036
+ quoteSenderId,
3037
+ quoteSenderName,
3038
+ threadContext
2134
3039
  };
2135
3040
  }
3041
+ //#endregion
3042
+ //#region extensions/msteams/src/monitor-handler/message-handler.ts
2136
3043
  function createMSTeamsMessageHandler(deps) {
2137
3044
  const { cfg, runtime, appId, app, tokenProvider, textLimit, mediaMaxBytes, conversationStore, pollStore, log } = deps;
2138
3045
  const core = require_runtime.getMSTeamsRuntime();
@@ -2151,24 +3058,12 @@ function createMSTeamsMessageHandler(deps) {
2151
3058
  channel: "msteams"
2152
3059
  });
2153
3060
  const handleTeamsMessageNow = async (params) => {
2154
- const context = params.context;
2155
- const activity = context.activity;
2156
- const rawText = params.rawText;
2157
- const text = params.text;
2158
- const attachments = params.attachments;
2159
- const advertisedMedia = resolveMSTeamsAdvertisedMedia(attachments, {
2160
- maxInlineBytes: mediaMaxBytes,
2161
- maxInlineTotalBytes: mediaMaxBytes
2162
- });
2163
- const rawBody = text;
3061
+ const facts = assembleMSTeamsInboundFacts({
3062
+ entry: params,
3063
+ mediaMaxBytes
3064
+ });
3065
+ const { context, activity, rawText, text, attachments, advertisedMedia, rawBody, quoteInfo, from, conversation, attachmentTypes, htmlSummary, conversationId, conversationMessageId, conversationType, isChannel, teamId, conversationRef } = facts;
2164
3066
  const historyBody = [text, (0, openclaw_plugin_sdk_channel_inbound.formatMediaPlaceholderText)(advertisedMedia)].filter(Boolean).join("\n");
2165
- const quoteInfo = require_channel.extractMSTeamsQuoteInfo(attachments);
2166
- let quoteSenderId;
2167
- let quoteSenderName;
2168
- const from = activity.from;
2169
- const conversation = activity.conversation;
2170
- const attachmentTypes = attachments.map((att) => typeof att.contentType === "string" ? att.contentType : void 0).filter(Boolean).slice(0, 3);
2171
- const htmlSummary = summarizeMSTeamsHtmlAttachments(attachments);
2172
3067
  log.info("received message", {
2173
3068
  rawText: (0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(rawText, 50),
2174
3069
  text: (0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(text, 50),
@@ -2182,138 +3077,19 @@ function createMSTeamsMessageHandler(deps) {
2182
3077
  log.debug?.("skipping message without from.id");
2183
3078
  return;
2184
3079
  }
2185
- const rawConversationId = conversation?.id ?? "";
2186
- const conversationId = require_channel.normalizeMSTeamsConversationId(rawConversationId);
2187
- const conversationMessageId = require_channel.extractMSTeamsConversationMessageId(rawConversationId);
2188
- const conversationType = conversation?.conversationType ?? "personal";
2189
- const teamId = activity.channelData?.team?.id;
2190
- const graphChannelId = activity.channelData?.channel?.id?.trim() || conversationId;
2191
- const conversationRef = buildStoredConversationReference({
2192
- activity,
2193
- conversationId,
2194
- conversationType,
2195
- teamId,
2196
- threadId: conversationType === "channel" ? conversationMessageId ?? activity.replyToId ?? void 0 : void 0
2197
- });
2198
- const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
2199
- cfg,
2200
- surface: "msteams"
2201
- });
2202
- const isControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(text, cfg);
2203
- const { dmPolicy, senderId, senderName, pairing, isDirectMessage, channelGate, senderAccess, commandAccess, allowNameMatching, groupPolicy } = await resolveMSTeamsSenderAccess({
3080
+ const admission = await admitMSTeamsMessage({
2204
3081
  cfg,
2205
3082
  activity,
2206
- hasControlCommand: isControlCommand
2207
- });
2208
- const commandAuthorized = commandAccess.requested ? commandAccess.authorized : void 0;
2209
- const effectiveDmAllowFrom = senderAccess.effectiveAllowFrom;
2210
- const effectiveGroupAllowFrom = senderAccess.effectiveGroupAllowFrom;
2211
- const isChannel = conversationType === "channel";
2212
- if (isDirectMessage && msteamsCfg && senderAccess.decision !== "allow") {
2213
- if (senderAccess.reasonCode === "dm_policy_disabled") {
2214
- log.info("dropping dm (dms disabled)", {
2215
- sender: senderId,
2216
- label: senderName
2217
- });
2218
- log.debug?.("dropping dm (dms disabled)");
2219
- return;
2220
- }
2221
- const allowMatch = require_channel.resolveMSTeamsAllowlistMatch({
2222
- allowFrom: effectiveDmAllowFrom,
2223
- senderId,
2224
- senderName,
2225
- allowNameMatching
2226
- });
2227
- if (senderAccess.decision === "pairing") {
2228
- conversationStore.upsert(conversationId, conversationRef).catch((err) => {
2229
- log.debug?.("failed to save conversation reference", { error: require_setup_surface.formatUnknownError(err) });
2230
- });
2231
- if (await pairing.upsertPairingRequest({
2232
- id: senderId,
2233
- meta: { name: senderName }
2234
- })) log.info("msteams pairing request created", {
2235
- sender: senderId,
2236
- label: senderName
2237
- });
2238
- }
2239
- log.debug?.("dropping dm (not allowlisted)", {
2240
- sender: senderId,
2241
- label: senderName,
2242
- allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
2243
- });
2244
- log.info("dropping dm (not allowlisted)", {
2245
- sender: senderId,
2246
- label: senderName,
2247
- dmPolicy,
2248
- reason: formatMSTeamsSenderReason({
2249
- reasonCode: senderAccess.reasonCode,
2250
- dmPolicy,
2251
- groupPolicy
2252
- }),
2253
- allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
2254
- });
2255
- return;
2256
- }
2257
- if (!isDirectMessage && msteamsCfg) {
2258
- if (channelGate.allowlistConfigured && !channelGate.allowed) {
2259
- log.info("dropping group message (not in team/channel allowlist)", {
2260
- conversationId,
2261
- teamKey: channelGate.teamKey ?? "none",
2262
- channelKey: channelGate.channelKey ?? "none",
2263
- channelMatchKey: channelGate.channelMatchKey ?? "none",
2264
- channelMatchSource: channelGate.channelMatchSource ?? "none"
2265
- });
2266
- log.debug?.("dropping group message (not in team/channel allowlist)", {
2267
- conversationId,
2268
- teamKey: channelGate.teamKey ?? "none",
2269
- channelKey: channelGate.channelKey ?? "none",
2270
- channelMatchKey: channelGate.channelMatchKey ?? "none",
2271
- channelMatchSource: channelGate.channelMatchSource ?? "none"
2272
- });
2273
- return;
2274
- }
2275
- if (!senderAccess.allowed && senderAccess.reasonCode === "group_policy_disabled") {
2276
- log.info("dropping group message (groupPolicy: disabled)", { conversationId });
2277
- log.debug?.("dropping group message (groupPolicy: disabled)", { conversationId });
2278
- return;
2279
- }
2280
- if (!senderAccess.allowed && (senderAccess.reasonCode === "group_policy_empty_allowlist" || senderAccess.reasonCode === "route_sender_empty")) {
2281
- log.info("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId });
2282
- log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId });
2283
- return;
2284
- }
2285
- if (!senderAccess.allowed && senderAccess.reasonCode === "group_policy_not_allowlisted") {
2286
- const allowMatch = require_channel.resolveMSTeamsAllowlistMatch({
2287
- allowFrom: effectiveGroupAllowFrom,
2288
- senderId,
2289
- senderName,
2290
- allowNameMatching
2291
- });
2292
- log.debug?.("dropping group message (not in groupAllowFrom)", {
2293
- sender: senderId,
2294
- label: senderName,
2295
- allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
2296
- });
2297
- log.info("dropping group message (not in groupAllowFrom)", {
2298
- sender: senderId,
2299
- label: senderName,
2300
- allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
2301
- });
2302
- return;
2303
- }
2304
- }
2305
- if (commandAccess.shouldBlockControlCommand) {
2306
- (0, openclaw_plugin_sdk_channel_inbound.logInboundDrop)({
2307
- log: logVerboseMessage,
2308
- channel: "msteams",
2309
- reason: "control command (unauthorized)",
2310
- target: senderId
2311
- });
2312
- return;
2313
- }
2314
- conversationStore.upsert(conversationId, conversationRef).catch((err) => {
2315
- log.debug?.("failed to save conversation reference", { error: require_setup_surface.formatUnknownError(err) });
3083
+ text,
3084
+ conversationId,
3085
+ conversationRef,
3086
+ isChannel,
3087
+ conversationStore,
3088
+ log,
3089
+ logVerboseMessage
2316
3090
  });
3091
+ if (!admission) return;
3092
+ const { senderId, senderName, isDirectMessage, channelGate, allowNameMatching, groupPolicy, commandAuthorized, effectiveGroupAllowFrom, allowTextCommands, isControlCommand } = admission;
2317
3093
  const pollVote = require_polls.extractMSTeamsPollVote(activity);
2318
3094
  if (pollVote) {
2319
3095
  try {
@@ -2335,33 +3111,18 @@ function createMSTeamsMessageHandler(deps) {
2335
3111
  }
2336
3112
  return;
2337
3113
  }
2338
- const mayRecoverGraphMedia = Boolean(htmlSummary?.attachmentIds.length) || shouldAttemptMSTeamsGraphMediaFallback({
2339
- conversationType,
2340
- htmlSummary: htmlSummary ?? void 0,
2341
- graphMediaFallback: msteamsCfg?.graphMediaFallback
2342
- });
2343
- if (!rawBody && advertisedMedia.length === 0 && !mayRecoverGraphMedia) {
2344
- log.debug?.("skipping empty message after stripping mentions");
2345
- return;
2346
- }
2347
- const teamsFrom = isDirectMessage ? `msteams:${senderId}` : isChannel ? `msteams:channel:${conversationId}` : `msteams:group:${conversationId}`;
2348
- const teamsTo = isDirectMessage ? `user:${senderId}` : `conversation:${conversationId}`;
2349
- const route = core.channel.routing.resolveAgentRoute({
3114
+ const threadRouting = prepareMSTeamsThreadRouting({
2350
3115
  cfg,
2351
- channel: "msteams",
2352
- teamId,
2353
- peer: {
2354
- kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
2355
- id: isDirectMessage ? senderId : conversationId
2356
- }
2357
- });
2358
- route.sessionKey = require_channel.resolveMSTeamsRouteSessionKey({
2359
- baseSessionKey: route.sessionKey,
3116
+ context,
3117
+ isDirectMessage,
2360
3118
  isChannel,
2361
- conversationMessageId,
2362
- replyToId: activity.replyToId
3119
+ senderId,
3120
+ conversationId,
3121
+ conversationMessageId: conversationMessageId ?? void 0,
3122
+ teamId,
3123
+ log
2363
3124
  });
2364
- const preview = (0, openclaw_plugin_sdk_text_utility_runtime.sliceUtf16Safe)(rawBody.replace(/\s+/g, " "), 0, 160);
3125
+ const { route, deadline: preprocessingDeadline } = threadRouting;
2365
3126
  const inboundLabel = isDirectMessage ? `Teams DM from ${senderName}` : `Teams message in ${conversationType} from ${senderName}`;
2366
3127
  const enqueuePrimaryMessageSystemEvent = () => core.system.enqueueSystemEvent(inboundLabel, {
2367
3128
  sessionKey: route.sessionKey,
@@ -2375,7 +3136,7 @@ function createMSTeamsMessageHandler(deps) {
2375
3136
  teamConfig,
2376
3137
  channelConfig
2377
3138
  });
2378
- const timestamp = require_channel.parseMSTeamsActivityTimestamp(activity.timestamp);
3139
+ const timestamp = require_resolve_allowlist.parseMSTeamsActivityTimestamp(activity.timestamp);
2379
3140
  const mentionDecision = (0, openclaw_plugin_sdk_channel_inbound.resolveInboundMentionDecision)({
2380
3141
  facts: {
2381
3142
  canDetectMention: true,
@@ -2415,344 +3176,66 @@ function createMSTeamsMessageHandler(deps) {
2415
3176
  return;
2416
3177
  }
2417
3178
  }
2418
- const preprocessingDeadline = require_resolve_allowlist.createMSTeamsInboundDeadline();
2419
- let teamAadGroupId = activity.channelData?.team?.aadGroupId?.trim() || void 0;
2420
- const conversationTeamId = isChannel ? teamId : void 0;
2421
- let teamGroupIdPromise;
2422
- const resolveChannelTeamGroupId = async () => {
2423
- if (!conversationTeamId) return;
2424
- teamGroupIdPromise ??= resolveTeamGroupId({
2425
- conversationTeamId,
2426
- aadGroupId: teamAadGroupId,
2427
- getTeamDetails: context.getTeamDetails,
2428
- deadline: preprocessingDeadline
2429
- }).catch((err) => {
2430
- log.debug?.("failed to resolve Teams AAD group ID", {
2431
- teamId: conversationTeamId,
2432
- error: require_setup_surface.formatUnknownError(err)
2433
- });
2434
- });
2435
- teamAadGroupId = await teamGroupIdPromise;
2436
- return teamAadGroupId;
2437
- };
2438
- let mediaList = [];
2439
- try {
2440
- mediaList = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2441
- deadline: preprocessingDeadline,
2442
- label: "MS Teams inbound media",
2443
- work: () => resolveMSTeamsInboundMedia({
2444
- attachments,
2445
- htmlSummary: htmlSummary ?? void 0,
2446
- maxBytes: mediaMaxBytes,
2447
- tokenProvider,
2448
- allowHosts: msteamsCfg?.mediaAllowHosts,
2449
- authAllowHosts: msteamsCfg?.mediaAuthAllowHosts,
2450
- graphMediaFallback: msteamsCfg?.graphMediaFallback,
2451
- conversationType,
2452
- conversationId,
2453
- conversationMessageId: conversationMessageId ?? void 0,
2454
- teamAadGroupId,
2455
- resolveTeamAadGroupId: resolveChannelTeamGroupId,
2456
- serviceUrl: activity.serviceUrl,
2457
- activity: {
2458
- id: activity.id,
2459
- replyToId: activity.replyToId,
2460
- channelData: activity.channelData
2461
- },
2462
- log,
2463
- deadline: preprocessingDeadline,
2464
- preserveFilenames: false
2465
- })
2466
- });
2467
- } catch (err) {
2468
- log.debug?.("failed to resolve inbound Teams media", { error: require_setup_surface.formatUnknownError(err) });
2469
- }
2470
- const inboundMedia = mergeMSTeamsMediaFacts(advertisedMedia, mediaList);
2471
- const agentBody = resolveMSTeamsInboundMediaBody({
2472
- body: rawBody,
2473
- nativeMedia: [...advertisedMedia, ...mediaList.slice(advertisedMedia.length).map((media) => ({
2474
- contentType: media.contentType,
2475
- kind: media.kind
2476
- }))],
2477
- materializedMedia: inboundMedia
2478
- });
2479
- if (!agentBody && inboundMedia.length === 0) {
2480
- log.debug?.("skipping empty message after Graph media recovery");
2481
- return;
2482
- }
2483
- enqueuePrimaryMessageSystemEvent();
2484
- teamAadGroupId = await resolveChannelTeamGroupId();
2485
- let quoteBodyFull;
2486
- const quoteMessageId = quoteInfo?.id;
2487
- if (quoteMessageId && isDirectMessage && conversationId.startsWith("19:")) try {
2488
- const graphToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2489
- deadline: preprocessingDeadline,
2490
- label: "MS Teams quote token",
2491
- work: () => tokenProvider.getAccessToken("https://graph.microsoft.com")
2492
- });
2493
- quoteBodyFull = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2494
- deadline: preprocessingDeadline,
2495
- label: "MS Teams quote lookup",
2496
- work: () => require_messenger.fetchChatMessageText(graphToken, conversationId, quoteMessageId, preprocessingDeadline)
2497
- });
2498
- } catch (err) {
2499
- log.debug?.("failed to fetch full quoted message text", { error: require_setup_surface.formatUnknownError(err) });
2500
- }
2501
- let threadContext;
2502
- const threadParentId = activity.replyToId;
2503
- const channelGroupId = teamAadGroupId;
2504
- if (threadParentId && isChannel && channelGroupId) try {
2505
- const graphToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2506
- deadline: preprocessingDeadline,
2507
- label: "MS Teams thread token",
2508
- work: () => tokenProvider.getAccessToken("https://graph.microsoft.com")
2509
- });
2510
- const [parentResult, repliesResult] = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2511
- deadline: preprocessingDeadline,
2512
- label: "MS Teams thread history",
2513
- work: () => Promise.allSettled([fetchParentMessageCached(graphToken, channelGroupId, conversationId, threadParentId, (token, groupId, requestedChannelId, messageId) => require_messenger.fetchChannelMessage(token, groupId, requestedChannelId, messageId, preprocessingDeadline)), require_messenger.fetchThreadReplies(graphToken, channelGroupId, conversationId, threadParentId, 50, preprocessingDeadline)])
2514
- });
2515
- const parentMsg = parentResult.status === "fulfilled" ? parentResult.value : void 0;
2516
- const replies = repliesResult.status === "fulfilled" ? repliesResult.value : [];
2517
- if (parentResult.status === "rejected") log.debug?.("failed to fetch parent message", { error: require_setup_surface.formatUnknownError(parentResult.reason) });
2518
- if (repliesResult.status === "rejected") log.debug?.("failed to fetch thread replies", { error: require_setup_surface.formatUnknownError(repliesResult.reason) });
2519
- const isThreadSenderAllowed = (msg) => (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2520
- isGroup: isChannel,
2521
- groupPolicy,
2522
- allowFrom: effectiveGroupAllowFrom,
2523
- isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2524
- allowFrom,
2525
- senderId: msg.from?.user?.id ?? "",
2526
- senderName: msg.from?.user?.displayName,
2527
- allowNameMatching
2528
- }).allowed
2529
- });
2530
- const parentSummary = summarizeParentMessage(parentMsg);
2531
- const visibleParentMessages = parentMsg ? (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
2532
- items: [parentMsg],
2533
- mode: contextVisibilityMode,
2534
- kind: "thread",
2535
- isSenderAllowed: isThreadSenderAllowed
2536
- }).items : [];
2537
- if (parentSummary && visibleParentMessages.length > 0 && shouldInjectParentContext(route.sessionKey, threadParentId)) {
2538
- core.system.enqueueSystemEvent(formatParentContextEvent(parentSummary), {
2539
- sessionKey: route.sessionKey,
2540
- contextKey: `msteams:thread-parent:${conversationId}:${threadParentId}`
2541
- });
2542
- markParentContextInjected(route.sessionKey, threadParentId);
2543
- }
2544
- const allMessages = parentMsg ? [parentMsg, ...replies] : replies;
2545
- quoteSenderId = parentMsg?.from?.user?.id ?? parentMsg?.from?.application?.id ?? void 0;
2546
- quoteSenderName = parentMsg?.from?.user?.displayName ?? parentMsg?.from?.application?.displayName ?? quoteInfo?.sender;
2547
- const { items: threadMessages } = (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
2548
- items: allMessages,
2549
- mode: contextVisibilityMode,
2550
- kind: "thread",
2551
- isSenderAllowed: isThreadSenderAllowed
2552
- });
2553
- const formatted = require_messenger.formatThreadContext(threadMessages, activity.id);
2554
- if (formatted) threadContext = formatted;
2555
- } catch (err) {
2556
- log.debug?.("failed to fetch thread history", { error: require_setup_surface.formatUnknownError(err) });
2557
- }
2558
- quoteSenderName ??= quoteInfo?.sender;
2559
- const envelopeFrom = isDirectMessage ? senderName : conversationType;
2560
- const buildEnvelope = (0, openclaw_plugin_sdk_channel_inbound.createChannelInboundEnvelopeBuilder)({
2561
- cfg,
2562
- route
2563
- });
2564
- let combinedBody = buildEnvelope({
2565
- channel: "Teams",
2566
- from: envelopeFrom,
2567
- timestamp,
2568
- body: agentBody
2569
- });
2570
- const isRoomish = !isDirectMessage;
2571
- const historyKey = isRoomish ? conversationId : void 0;
2572
- if (isRoomish && historyKey) combinedBody = (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildPendingContext({
2573
- historyKey,
2574
- limit: historyLimit,
2575
- currentMessage: combinedBody,
2576
- formatEntry: (entry) => buildEnvelope({
2577
- channel: "Teams",
2578
- from: conversationType,
2579
- timestamp: entry.timestamp,
2580
- previousTimestamp: null,
2581
- body: `${entry.sender}: ${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`
2582
- })
3179
+ const content = await prepareMSTeamsInboundContent({
3180
+ entry: params,
3181
+ rawBody,
3182
+ advertisedMedia,
3183
+ htmlSummary: htmlSummary ?? void 0,
3184
+ conversationType,
3185
+ conversationId,
3186
+ conversationMessageId: conversationMessageId ?? void 0,
3187
+ teamAadGroupId: threadRouting.getTeamAadGroupId(),
3188
+ resolveTeamAadGroupId: threadRouting.resolveTeamAadGroupId,
3189
+ mediaMaxBytes,
3190
+ tokenProvider,
3191
+ mediaAllowHosts: msteamsCfg?.mediaAllowHosts,
3192
+ mediaAuthAllowHosts: msteamsCfg?.mediaAuthAllowHosts,
3193
+ graphMediaFallback: msteamsCfg?.graphMediaFallback,
3194
+ deadline: preprocessingDeadline,
3195
+ log
2583
3196
  });
2584
- const inboundHistory = isRoomish && historyKey && historyLimit > 0 ? (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildInboundHistory({
2585
- historyKey,
2586
- limit: historyLimit
2587
- }) : void 0;
2588
- const commandBody = text.trim();
2589
- const quoteSenderAllowed = quoteInfo && quoteInfo.sender ? (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2590
- isGroup: !isDirectMessage,
3197
+ if (!content) return;
3198
+ enqueuePrimaryMessageSystemEvent();
3199
+ const thread = await resolveMSTeamsThreadContext({
3200
+ routing: threadRouting,
3201
+ context,
3202
+ tokenProvider,
3203
+ quoteInfo,
3204
+ isDirectMessage,
3205
+ isChannel,
3206
+ conversationId,
3207
+ contextVisibilityMode,
2591
3208
  groupPolicy,
2592
- allowFrom: effectiveGroupAllowFrom,
2593
- isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2594
- allowFrom,
2595
- senderId: quoteSenderId ?? "",
2596
- senderName: quoteSenderName,
2597
- allowNameMatching
2598
- }).allowed
2599
- }) : true;
2600
- const bodyForAgent = threadContext ? `[Thread history]\n${threadContext}\n[/Thread history]\n\n${agentBody}` : agentBody;
2601
- const nativeChannelId = isChannel && teamAadGroupId ? `${teamAadGroupId}/${graphChannelId}` : void 0;
2602
- const ctxPayload = (0, openclaw_plugin_sdk_channel_inbound.buildChannelInboundEventContext)({
2603
- channel: "msteams",
2604
- contextVisibility: contextVisibilityMode,
2605
- supplemental: { quote: quoteInfo ? {
2606
- id: quoteInfo.id ?? activity.replyToId ?? void 0,
2607
- body: quoteBodyFull ?? quoteInfo.body,
2608
- sender: quoteInfo.sender,
2609
- senderAllowed: quoteSenderAllowed,
2610
- isQuote: true
2611
- } : void 0 },
2612
- media: (0, openclaw_plugin_sdk_channel_inbound.toInboundMediaFacts)(inboundMedia),
2613
- messageId: activity.id,
2614
- timestamp: timestamp?.getTime() ?? Date.now(),
2615
- from: teamsFrom,
2616
- sender: {
2617
- id: senderId,
2618
- name: senderName
2619
- },
2620
- conversation: {
2621
- kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
2622
- id: conversationId,
2623
- label: envelopeFrom,
2624
- spaceId: teamId,
2625
- nativeChannelId
2626
- },
2627
- route: {
2628
- agentId: route.agentId,
2629
- dmScope: route.dmScope,
2630
- accountId: route.accountId,
2631
- routeSessionKey: route.sessionKey
2632
- },
2633
- reply: {
2634
- to: teamsTo,
2635
- replyToId: activity.replyToId ?? void 0,
2636
- nativeChannelId
2637
- },
2638
- message: {
2639
- body: combinedBody,
2640
- bodyForAgent,
2641
- inboundHistory,
2642
- rawBody,
2643
- commandBody
2644
- },
2645
- sessionTranscript: { historyLimit: isRoomish ? historyLimit : 0 },
2646
- access: {
2647
- mentions: {
2648
- canDetectMention: !isDirectMessage,
2649
- wasMentioned: isDirectMessage || mentionDecision.effectiveWasMentioned
2650
- },
2651
- commands: { authorized: commandAuthorized === true }
2652
- },
2653
- extra: {
2654
- GroupSubject: !isDirectMessage ? conversationType : void 0,
2655
- ReplyToIsQuote: quoteInfo ? true : void 0
2656
- }
3209
+ effectiveGroupAllowFrom,
3210
+ allowNameMatching,
3211
+ log
2657
3212
  });
2658
- logVerboseMessage(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
2659
- const sharePointSiteId = msteamsCfg?.sharePointSiteId;
2660
- const { dispatcherOptions, delivery, replyOptions } = createMSTeamsReplyDispatcher({
3213
+ await dispatchMSTeamsInboundTurn({
2661
3214
  cfg,
2662
- agentId: route.agentId,
2663
- sessionKey: route.sessionKey,
2664
- accountId: route.accountId,
2665
3215
  runtime,
2666
- log,
2667
- app,
2668
3216
  appId,
2669
- conversationRef,
2670
- context,
2671
- replyStyle,
2672
- textLimit,
2673
- onSentMessageIds: (ids) => {
2674
- for (const id of ids) recordMSTeamsSentMessage(conversationId, id);
2675
- },
3217
+ app,
2676
3218
  tokenProvider,
2677
- sharePointSiteId
2678
- });
2679
- const senderTimezone = (activity.entities?.find((e) => e.type === "clientInfo"))?.timezone || conversationRef.timezone;
2680
- const turnConfig = senderTimezone && !cfg.agents?.defaults?.userTimezone ? {
2681
- ...cfg,
2682
- agents: {
2683
- ...cfg.agents,
2684
- defaults: {
2685
- ...cfg.agents?.defaults,
2686
- userTimezone: senderTimezone
2687
- }
2688
- }
2689
- } : cfg;
2690
- log.info("dispatching to agent", { sessionKey: route.sessionKey });
2691
- try {
2692
- const turnResult = await core.channel.inbound.run({
2693
- channel: "msteams",
2694
- accountId: route.accountId,
2695
- raw: context,
2696
- adapter: {
2697
- ingest: () => ({
2698
- id: activity.id ?? `${teamsFrom}:${Date.now()}`,
2699
- timestamp: timestamp?.getTime(),
2700
- rawText: rawBody,
2701
- textForAgent: bodyForAgent,
2702
- textForCommands: commandBody,
2703
- raw: activity
2704
- }),
2705
- resolveTurn: () => ({
2706
- cfg: turnConfig,
2707
- channel: "msteams",
2708
- accountId: route.accountId,
2709
- route: {
2710
- agentId: route.agentId,
2711
- sessionKey: route.sessionKey
2712
- },
2713
- ctxPayload,
2714
- record: { onRecordError: (err) => {
2715
- logVerboseMessage(`msteams: failed updating session meta: ${require_setup_surface.formatUnknownError(err)}`);
2716
- } },
2717
- history: {
2718
- isGroup: isRoomish,
2719
- historyKey,
2720
- historyMap: conversationHistories,
2721
- limit: historyLimit
2722
- },
2723
- dispatcherOptions,
2724
- delivery,
2725
- replyOptions: {
2726
- ...replyOptions,
2727
- ...params.turnAdoptionLifecycle ? (0, openclaw_plugin_sdk_channel_outbound.bindIngressLifecycleToReplyOptions)(params.turnAdoptionLifecycle) : {}
2728
- }
2729
- })
2730
- }
2731
- });
2732
- const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
2733
- const queuedFinal = dispatchResult?.queuedFinal ?? false;
2734
- const counts = (0, openclaw_plugin_sdk_channel_inbound.resolveInboundReplyDispatchCounts)(dispatchResult);
2735
- const hasFinalResponse = (0, openclaw_plugin_sdk_channel_inbound.hasFinalInboundReplyDispatch)(dispatchResult);
2736
- log.info("dispatch complete", {
2737
- queuedFinal,
2738
- counts
2739
- });
2740
- if (!hasFinalResponse) return;
2741
- const finalCount = counts.final;
2742
- logVerboseMessage(`msteams: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${teamsTo}`);
2743
- } catch (err) {
2744
- log.error("dispatch failed", { error: require_setup_surface.formatUnknownError(err) });
2745
- runtime.error(`msteams dispatch failed: ${require_setup_surface.formatUnknownError(err)}`);
2746
- if (params.turnAdoptionLifecycle) throw err;
2747
- try {
2748
- await context.sendActivity("⚠️ Something went wrong. Please try again.");
2749
- } catch {}
2750
- }
3219
+ textLimit,
3220
+ log,
3221
+ logVerboseMessage,
3222
+ facts,
3223
+ admission,
3224
+ content,
3225
+ routing: threadRouting,
3226
+ thread,
3227
+ replyStyle,
3228
+ timestamp,
3229
+ contextVisibilityMode,
3230
+ mentionWasEffective: mentionDecision.effectiveWasMentioned,
3231
+ conversationHistories,
3232
+ historyLimit
3233
+ });
2751
3234
  };
2752
3235
  const inboundDebouncer = core.channel.debounce.createInboundDebouncer({
2753
3236
  debounceMs: inboundDebounceMs,
2754
3237
  buildKey: (entry) => {
2755
- const conversationId = require_channel.normalizeMSTeamsConversationId(entry.context.activity.conversation?.id ?? "");
3238
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(entry.context.activity.conversation?.id ?? "");
2756
3239
  const senderId = entry.context.activity.from?.aadObjectId ?? entry.context.activity.from?.id ?? "";
2757
3240
  if (!senderId || !conversationId) return null;
2758
3241
  return `msteams:${appId}:${conversationId}:${senderId}`;
@@ -2762,65 +3245,53 @@ function createMSTeamsMessageHandler(deps) {
2762
3245
  if (entry.attachments.length > 0) return false;
2763
3246
  return !core.channel.commands.isControlCommandMessage(entry.text, cfg);
2764
3247
  },
2765
- onFlush: async (entries) => {
3248
+ onFlush: (entries, createFlush) => {
2766
3249
  const last = entries.at(-1);
2767
- if (!last) return;
2768
3250
  const { lifecycle, settle } = (0, openclaw_plugin_sdk_channel_ingress_runtime.fanInChannelIngressLifecycles)(entries.map((entry) => entry.turnAdoptionLifecycle));
2769
- try {
2770
- if (entries.length === 1) await handleTeamsMessageNow(lifecycle ? {
2771
- ...last,
2772
- turnAdoptionLifecycle: lifecycle
2773
- } : last);
2774
- else {
2775
- const combinedText = entries.map((entry) => entry.text).filter(Boolean).join("\n");
2776
- if (combinedText.trim()) {
2777
- const combinedRawText = entries.map((entry) => entry.rawText).filter(Boolean).join("\n");
2778
- const wasMentioned = entries.some((entry) => entry.wasMentioned);
2779
- const implicitMentionKinds = entries.flatMap((entry) => entry.implicitMentionKinds);
2780
- await handleTeamsMessageNow({
2781
- context: last.context,
2782
- rawText: combinedRawText,
2783
- text: combinedText,
2784
- attachments: [],
2785
- wasMentioned,
2786
- implicitMentionKinds,
2787
- ...lifecycle ? { turnAdoptionLifecycle: lifecycle } : {}
3251
+ return createFlush({
3252
+ lifecycle,
3253
+ dispatch: async (admissionLifecycle) => {
3254
+ if (!last) return;
3255
+ try {
3256
+ if (entries.length === 1) await handleTeamsMessageNow({
3257
+ ...last,
3258
+ turnAdoptionLifecycle: admissionLifecycle
2788
3259
  });
3260
+ else {
3261
+ const combinedText = entries.map((entry) => entry.text).filter(Boolean).join("\n");
3262
+ if (combinedText.trim()) {
3263
+ const combinedRawText = entries.map((entry) => entry.rawText).filter(Boolean).join("\n");
3264
+ const wasMentioned = entries.some((entry) => entry.wasMentioned);
3265
+ const implicitMentionKinds = entries.flatMap((entry) => entry.implicitMentionKinds);
3266
+ await handleTeamsMessageNow({
3267
+ context: last.context,
3268
+ rawText: combinedRawText,
3269
+ text: combinedText,
3270
+ attachments: [],
3271
+ wasMentioned,
3272
+ implicitMentionKinds,
3273
+ turnAdoptionLifecycle: admissionLifecycle
3274
+ });
3275
+ }
3276
+ }
3277
+ await settle();
3278
+ } catch (err) {
3279
+ await admissionLifecycle.onAbandoned();
3280
+ throw err;
2789
3281
  }
2790
3282
  }
2791
- await settle();
2792
- } catch (err) {
2793
- await lifecycle?.onAbandoned();
2794
- throw err;
2795
- }
3283
+ });
2796
3284
  },
2797
3285
  onError: (err) => {
2798
3286
  runtime.error(`msteams debounce flush failed: ${require_setup_surface.formatUnknownError(err)}`);
2799
3287
  }
2800
3288
  });
2801
3289
  return async function handleTeamsMessage(context, turnAdoptionLifecycle) {
2802
- const activity = context.activity;
2803
- const attachments = Array.isArray(activity.attachments) ? activity.attachments : [];
2804
- const rawText = activity.text?.trim() ?? "";
2805
- const htmlText = extractTextFromHtmlAttachments(attachments);
2806
- const valueText = rawText || htmlText ? "" : serializeMSTeamsAdaptiveCardActionValue(activity.value);
2807
- const text = require_channel.stripMSTeamsMentionTags(rawText || htmlText || valueText || "");
2808
- const wasMentioned = require_channel.wasMSTeamsBotMentioned(activity);
2809
- const conversationId = require_channel.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
2810
- const replyToId = activity.replyToId ?? void 0;
2811
- const implicitMentionKinds = conversationId && replyToId && await wasMSTeamsMessageSentWithPersistence({
2812
- conversationId,
2813
- messageId: replyToId
2814
- }) ? ["reply_to_bot"] : [];
2815
- await inboundDebouncer.enqueue({
3290
+ const entry = await prepareMSTeamsDebounceEntry({
2816
3291
  context,
2817
- rawText,
2818
- text,
2819
- attachments,
2820
- wasMentioned,
2821
- implicitMentionKinds,
2822
3292
  turnAdoptionLifecycle
2823
3293
  });
3294
+ await inboundDebouncer.enqueue(entry);
2824
3295
  if (turnAdoptionLifecycle) return { kind: "deferred" };
2825
3296
  };
2826
3297
  }
@@ -2846,7 +3317,7 @@ function createMSTeamsReactionHandler(deps) {
2846
3317
  log.debug?.("reaction activity missing from.id; skipping");
2847
3318
  return;
2848
3319
  }
2849
- const conversationId = require_channel.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
3320
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
2850
3321
  const conversationType = activity.conversation?.conversationType ?? "personal";
2851
3322
  const isGroupChat = conversationType === "groupChat" || activity.conversation?.isGroup === true;
2852
3323
  const isChannel = conversationType === "channel";
@@ -3142,7 +3613,7 @@ async function runMSTeamsFeedbackInvokeHandler(context, deps) {
3142
3613
  userComment = JSON.parse(value.actionValue.feedback).feedbackText || void 0;
3143
3614
  } catch {}
3144
3615
  const rawConversationId = activity.conversation?.id ?? "unknown";
3145
- const conversationId = require_channel.normalizeMSTeamsConversationId(rawConversationId);
3616
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(rawConversationId);
3146
3617
  const senderId = activity.from?.aadObjectId ?? activity.from?.id ?? "unknown";
3147
3618
  const messageId = value.replyToId ?? activity.replyToId ?? "unknown";
3148
3619
  const isNegative = reaction === "dislike";
@@ -3157,7 +3628,7 @@ async function runMSTeamsFeedbackInvokeHandler(context, deps) {
3157
3628
  id: isDirectMessage ? senderId : conversationId
3158
3629
  }
3159
3630
  });
3160
- const feedbackThreadId = isChannel ? require_channel.extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId ?? void 0 : void 0;
3631
+ const feedbackThreadId = isChannel ? require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId ?? void 0 : void 0;
3161
3632
  if (feedbackThreadId) route.sessionKey = (0, openclaw_plugin_sdk_routing.resolveThreadSessionKeys)({
3162
3633
  baseSessionKey: route.sessionKey,
3163
3634
  threadId: feedbackThreadId,
@@ -3239,8 +3710,8 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3239
3710
  const fsFile = inMemoryFile ? void 0 : await require_messenger.getPendingUploadFs(uploadId);
3240
3711
  const pendingFile = inMemoryFile ?? fsFile;
3241
3712
  if (pendingFile) {
3242
- const pendingConversationId = require_channel.normalizeMSTeamsConversationId(pendingFile.conversationId);
3243
- const invokeConversationId = require_channel.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
3713
+ const pendingConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(pendingFile.conversationId);
3714
+ const invokeConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
3244
3715
  if (!invokeConversationId || pendingConversationId !== invokeConversationId) {
3245
3716
  log.info("file consent conversation mismatch", {
3246
3717
  uploadId,
@@ -3330,32 +3801,38 @@ async function runMSTeamsFileConsentInvokeHandler(context, log) {
3330
3801
  }
3331
3802
  }
3332
3803
  //#endregion
3804
+ //#region extensions/msteams/src/monitor-status.ts
3805
+ function publishMSTeamsBlocked(statusSink, lastError) {
3806
+ statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelBlockedPatch)(lastError, { running: true }));
3807
+ }
3808
+ function publishMSTeamsReady(statusSink, now = Date.now()) {
3809
+ statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelReadyPatch)({ lastConnectedAt: now }));
3810
+ }
3811
+ function publishMSTeamsRecovering(statusSink, lastError) {
3812
+ statusSink?.({
3813
+ connected: false,
3814
+ lifecycle: "recovering",
3815
+ lastError
3816
+ });
3817
+ }
3818
+ function publishMSTeamsStopped(statusSink) {
3819
+ statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelStoppedPatch)());
3820
+ }
3821
+ //#endregion
3333
3822
  //#region extensions/msteams/src/msteams-ingress.ts
3334
3823
  const MSTEAMS_INGRESS_VERSION = 1;
3335
3824
  const MSTEAMS_INGRESS_DRAIN_INTERVAL_MS = 500;
3336
3825
  const MSTEAMS_INGRESS_MAX_CONCURRENT_DELIVERIES = 8;
3337
3826
  const MSTEAMS_INGRESS_SCAN_LIMIT = 100;
3338
- const MSTEAMS_INGRESS_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
3339
- const MSTEAMS_INGRESS_COMPLETED_MAX_ENTRIES = 2e4;
3340
- const MSTEAMS_INGRESS_FAILED_MAX_ENTRIES = 4096;
3341
- var MSTeamsIngressPayloadError = class extends Error {
3342
- constructor(reason, message, options) {
3343
- super(message, options);
3344
- this.reason = reason;
3345
- this.name = "MSTeamsIngressPayloadError";
3346
- }
3347
- };
3348
- function nonEmptyString(value) {
3349
- return typeof value === "string" && value.trim() ? value.trim() : null;
3350
- }
3827
+ const MSTeamsIngressPayloadError = (0, openclaw_plugin_sdk_channel_outbound.createChannelIngressError)("MSTeamsIngressPayloadError", { withReason: true });
3351
3828
  function isDispatchableActivity(activity) {
3352
3829
  return activity.type === "message" || activity.type === "invoke" && activity.name === "adaptiveCard/action";
3353
3830
  }
3354
3831
  function inspectMSTeamsIngressActivity(activity) {
3355
3832
  if (!isDispatchableActivity(activity)) return null;
3356
- const eventId = nonEmptyString(activity.id);
3833
+ const eventId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeNullableString)(activity.id);
3357
3834
  if (!eventId) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams dispatchable activity is missing activity.id.");
3358
- const laneKey = nonEmptyString(activity.conversation?.id);
3835
+ const laneKey = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeNullableString)(activity.conversation?.id);
3359
3836
  if (!laneKey) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams dispatchable activity is missing conversation.id.");
3360
3837
  return {
3361
3838
  eventId,
@@ -3377,9 +3854,6 @@ function parseClaimedActivity(payload, claimedId) {
3377
3854
  if (facts.eventId !== claimedId) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams activity id changed after durable admission.");
3378
3855
  return parsed;
3379
3856
  }
3380
- function errorText(error) {
3381
- return error instanceof Error ? error.message : String(error);
3382
- }
3383
3857
  function createMSTeamsIngress(options) {
3384
3858
  const queue = options.queue ?? require_runtime.getMSTeamsRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
3385
3859
  const liveContexts = /* @__PURE__ */ new Map();
@@ -3414,10 +3888,7 @@ function createMSTeamsIngress(options) {
3414
3888
  pollIntervalMs: MSTEAMS_INGRESS_DRAIN_INTERVAL_MS,
3415
3889
  retention: {
3416
3890
  pruneIntervalMs: 0,
3417
- completedTtlMs: MSTEAMS_INGRESS_TOMBSTONE_TTL_MS,
3418
- completedMaxEntries: MSTEAMS_INGRESS_COMPLETED_MAX_ENTRIES,
3419
- failedTtlMs: MSTEAMS_INGRESS_TOMBSTONE_TTL_MS,
3420
- failedMaxEntries: MSTEAMS_INGRESS_FAILED_MAX_ENTRIES
3891
+ failedMaxEntries: 4096
3421
3892
  },
3422
3893
  appendRetryDelaysMs: [0],
3423
3894
  waitForDeliveryIdleBeforeRepump: false,
@@ -3433,13 +3904,13 @@ function createMSTeamsIngress(options) {
3433
3904
  };
3434
3905
  return require_setup_surface.classifyMSTeamsSendError(error).kind === "auth" ? {
3435
3906
  reason: "authentication-failed",
3436
- message: errorText(error)
3907
+ message: (0, openclaw_plugin_sdk_error_runtime.formatErrorMessage)(error)
3437
3908
  } : null;
3438
3909
  },
3439
3910
  onLog: (message) => options.runtime.error?.(`msteams: ${message}`)
3440
3911
  },
3441
3912
  createStoppedError: () => /* @__PURE__ */ new Error("Microsoft Teams ingress stopped."),
3442
- onError: (error) => options.runtime.error?.(`msteams ingress drain failed: ${errorText(error)}`)
3913
+ onError: (error) => options.runtime.error?.(`msteams ingress drain failed: ${(0, openclaw_plugin_sdk_error_runtime.formatErrorMessage)(error)}`)
3443
3914
  });
3444
3915
  let stopTask;
3445
3916
  return {
@@ -3505,6 +3976,7 @@ async function monitorMSTeamsProvider(opts) {
3505
3976
  let msteamsCfg = cfg.channels?.msteams;
3506
3977
  if (!msteamsCfg?.enabled) {
3507
3978
  log.debug?.("msteams provider disabled");
3979
+ publishMSTeamsBlocked(opts.statusSink, "Microsoft Teams provider is disabled");
3508
3980
  return {
3509
3981
  app: null,
3510
3982
  shutdown: async () => {}
@@ -3513,6 +3985,7 @@ async function monitorMSTeamsProvider(opts) {
3513
3985
  const creds = require_resolve_allowlist.resolveMSTeamsCredentials(msteamsCfg);
3514
3986
  if (!creds) {
3515
3987
  log.error("msteams credentials not configured");
3988
+ publishMSTeamsBlocked(opts.statusSink, "Microsoft Teams credentials are not configured");
3516
3989
  return {
3517
3990
  app: null,
3518
3991
  shutdown: async () => {}
@@ -3529,13 +4002,13 @@ async function monitorMSTeamsProvider(opts) {
3529
4002
  const configuredAllowFrom = msteamsCfg.allowFrom;
3530
4003
  const configuredGroupAllowFrom = msteamsCfg.groupAllowFrom;
3531
4004
  let allowFrom = require_resolve_allowlist.projectStableMSTeamsUserAllowlist(configuredAllowFrom);
3532
- let groupAllowFrom = require_resolve_allowlist.projectStableMSTeamsUserAllowlist(configuredGroupAllowFrom);
4005
+ let groupAllowFrom = require_resolve_allowlist.projectStableMSTeamsGroupAllowlist(configuredGroupAllowFrom ?? configuredAllowFrom);
3533
4006
  let teamsConfig = require_resolve_allowlist.projectStableMSTeamsTeamsConfig(msteamsCfg.teams);
3534
4007
  const allowNameMatching = (0, openclaw_plugin_sdk_dangerous_name_runtime.isDangerousNameMatchingEnabled)(msteamsCfg);
3535
4008
  const cleanAllowEntry = (entry) => entry.replace(/^(msteams|teams):/i, "").replace(/^user:/i, "").trim();
3536
4009
  const isStableUserId = (entry) => /^[0-9a-fA-F-]{16,}$/.test(entry);
3537
4010
  const cleanAllowEntries = (entries) => entries?.map((entry) => cleanAllowEntry(entry)).filter((entry) => entry && entry !== "*") ?? [];
3538
- const isMutableUserEntry = (entry) => !isStableUserId(entry) && !/^accessGroup:/i.test(entry);
4011
+ const isMutableUserEntry = (entry) => !isStableUserId(entry) && !/^accessGroup:/i.test(entry) && !require_resolve_allowlist.looksLikeMSTeamsConversationId(require_resolve_allowlist.normalizeMSTeamsConversationId(entry));
3539
4012
  const resolveAllowlistUsers = async (label, entries) => {
3540
4013
  if (entries.length === 0) return {
3541
4014
  additions: [],
@@ -3588,6 +4061,10 @@ async function monitorMSTeamsProvider(opts) {
3588
4061
  } catch (err) {
3589
4062
  runtime.error?.(`msteams resolve failed; mutable allowlist entries are disabled. ${require_setup_surface.formatUnknownError(err)}`);
3590
4063
  }
4064
+ if (configuredGroupAllowFrom == null && groupAllowFrom) groupAllowFrom = (0, openclaw_plugin_sdk_allow_from.mergeAllowlist)({
4065
+ existing: groupAllowFrom,
4066
+ additions: allowFrom ?? []
4067
+ });
3591
4068
  msteamsCfg = {
3592
4069
  ...msteamsCfg,
3593
4070
  allowFrom,
@@ -3603,9 +4080,10 @@ async function monitorMSTeamsProvider(opts) {
3603
4080
  };
3604
4081
  const port = msteamsCfg.webhook?.port ?? 3978;
3605
4082
  const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "msteams");
3606
- const MB = 1024 * 1024;
3607
- const agentDefaults = cfg.agents?.defaults;
3608
- const mediaMaxBytes = typeof agentDefaults?.mediaMaxMb === "number" && agentDefaults.mediaMaxMb > 0 ? Math.floor(agentDefaults.mediaMaxMb * MB) : 8 * MB;
4083
+ const mediaMaxBytes = (0, openclaw_plugin_sdk_media_runtime.resolveChannelMediaMaxBytes)({
4084
+ cfg,
4085
+ resolveChannelLimitMb: ({ cfg: channelCfg }) => channelCfg.channels?.msteams?.mediaMaxMb
4086
+ }) ?? 8 * 1024 * 1024;
3609
4087
  const conversationStore = opts.conversationStore ?? require_polls.createMSTeamsConversationStoreState();
3610
4088
  const pollStore = opts.pollStore ?? require_polls.createMSTeamsPollStoreState();
3611
4089
  log.info(`starting provider (port ${port})`);
@@ -3697,8 +4175,8 @@ async function monitorMSTeamsProvider(opts) {
3697
4175
  value: "Poll not found."
3698
4176
  };
3699
4177
  }
3700
- const pollConversationId = existingPoll.conversationId ? require_channel.normalizeMSTeamsConversationId(existingPoll.conversationId) : void 0;
3701
- const activityConversationId = require_channel.normalizeMSTeamsConversationId(activity?.conversation?.id ?? "");
4178
+ const pollConversationId = existingPoll.conversationId ? require_resolve_allowlist.normalizeMSTeamsConversationId(existingPoll.conversationId) : void 0;
4179
+ const activityConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity?.conversation?.id ?? "");
3702
4180
  if (pollConversationId && pollConversationId !== activityConversationId) {
3703
4181
  log.info("poll vote ignored (conversation mismatch)", {
3704
4182
  pollId: vote.pollId,
@@ -3852,17 +4330,21 @@ async function monitorMSTeamsProvider(opts) {
3852
4330
  });
3853
4331
  await app.initialize();
3854
4332
  ingress.start();
4333
+ const privateQaRuntime = require_resolve_allowlist.resolveMSTeamsPrivateQaRuntime();
3855
4334
  const httpServer = await new Promise((resolve, reject) => {
3856
- const server = expressApp.listen(port, (err) => err ? reject(err) : resolve(server));
4335
+ const onListen = (err) => err ? reject(err) : resolve(server);
4336
+ const server = privateQaRuntime ? expressApp.listen(port, privateQaRuntime.listenHost, onListen) : expressApp.listen(port, onListen);
3857
4337
  }).catch(async (err) => {
3858
4338
  log.error("msteams server error", { error: require_setup_surface.formatUnknownError(err) });
3859
4339
  await ingress.stop();
3860
4340
  throw err;
3861
4341
  });
3862
4342
  log.info(`msteams provider started on port ${port}`);
4343
+ publishMSTeamsReady(opts.statusSink);
3863
4344
  applyMSTeamsWebhookTimeouts(httpServer);
3864
4345
  httpServer.on("error", (err) => {
3865
4346
  log.error("msteams server error", { error: require_setup_surface.formatUnknownError(err) });
4347
+ publishMSTeamsRecovering(opts.statusSink, require_setup_surface.formatUnknownError(err));
3866
4348
  });
3867
4349
  const shutdown = async () => {
3868
4350
  log.info("shutting down msteams provider");
@@ -3873,6 +4355,7 @@ async function monitorMSTeamsProvider(opts) {
3873
4355
  });
3874
4356
  });
3875
4357
  await ingress.stop();
4358
+ publishMSTeamsStopped(opts.statusSink);
3876
4359
  };
3877
4360
  await (0, openclaw_plugin_sdk_channel_outbound.keepHttpServerTaskAlive)({
3878
4361
  server: httpServer,
@@ -3930,9 +4413,9 @@ function buildActivityHandler() {
3930
4413
  }
3931
4414
  function createMSTeamsReplayContext(activity, app, serviceUrlBoundary) {
3932
4415
  const rawConversationId = activity.conversation?.id ?? "";
3933
- const conversationId = require_channel.normalizeMSTeamsConversationId(rawConversationId);
4416
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(rawConversationId);
3934
4417
  const conversationType = activity.conversation?.conversationType ?? "personal";
3935
- const threadActivityId = conversationType.toLowerCase() === "channel" ? require_channel.extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId : void 0;
4418
+ const threadActivityId = conversationType.toLowerCase() === "channel" ? require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId : void 0;
3936
4419
  const tenantId = activity.channelData?.tenant?.id ?? activity.conversation?.tenantId;
3937
4420
  const reference = {
3938
4421
  activityId: activity.id,