@openclaw/mattermost 2026.7.1 → 2026.7.2-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,23 +1,28 @@
1
- import { C as resolveChannelMediaMaxBytes, E as warnMissingProviderGroupPolicyFallbackOnce, S as resolveAllowlistProviderRuntimeGroupPolicy, T as resolveDefaultGroupPolicy, _ as logInboundDrop, c as buildAgentMediaPayload, d as createChannelMessageReplyPipeline, f as createChannelPairingController, h as listSkillCommandsForAgents, i as normalizeMattermostAllowEntry, l as buildModelsProviderData, n as formatMattermostDirectMessageDropLog, o as resolveMattermostMonitorInboundAccess, s as DEFAULT_GROUP_HISTORY_LIMIT, t as authorizeMattermostCommandInvocation, u as createChannelHistoryWindow, v as logTypingFailure, x as registerPluginHttpRoute, y as parseTcpPort } from "./monitor-auth-BiDuyvOc.js";
2
- import { _ as readMattermostError, a as MattermostPostSchema, b as updateMattermostPost, c as createMattermostPost, f as fetchMattermostMe, g as normalizeMattermostBaseUrl, h as fetchMattermostUserTeams, i as resolveMattermostReplyToMode, l as deleteMattermostPost, o as createMattermostClient, p as fetchMattermostUser, r as resolveMattermostAccount, t as listMattermostAccountIds, u as fetchMattermostChannel, y as sendMattermostTyping } from "./accounts-ITTlduDO.js";
3
1
  import { n as getOptionalMattermostRuntime, t as getMattermostRuntime } from "./runtime-CNB4YGqJ.js";
4
- import { C as registerSlashCommands, S as isSlashCommandsEnabled, T as resolveSlashCommandConfig, _ as renderMattermostModelsPickerView, a as sendMessageMattermost, b as DEFAULT_COMMAND_SPECS, c as computeInteractionCallbackUrl, d as setInteractionCallbackUrl, f as setInteractionSecret, g as parseMattermostModelPickerContext, h as buildMattermostAllowedModelRefs, l as createMattermostInteractionHandler, m as deliverMattermostReplyPayload, n as deactivateSlashCommands, o as resolveMattermostOpaqueTarget, p as createMattermostReplyDeliveryBarrier, r as getSlashCommandState, s as buildButtonProps, t as activateSlashCommands, u as resolveInteractionCallbackPath, v as renderMattermostProviderPickerView, w as resolveCallbackUrl, x as cleanupSlashCommands, y as resolveMattermostModelPickerCurrentModel } from "./slash-state-BfOSlkmn.js";
2
+ import { a as deleteMattermostPost, c as fetchMattermostMe, d as fetchMattermostUserTeams, f as normalizeMattermostBaseUrl, g as updateMattermostPost, h as sendMattermostTyping, i as createMattermostPost, l as fetchMattermostUser, n as createMattermostClient, o as fetchMattermostChannel, p as readMattermostError, t as MattermostPostSchema } from "./client-BzLj4dyf.js";
3
+ import { C as resolveChannelMediaMaxBytes, E as warnMissingProviderGroupPolicyFallbackOnce, S as resolveAllowlistProviderRuntimeGroupPolicy, T as resolveDefaultGroupPolicy, _ as logInboundDrop, c as buildAgentMediaPayload, d as createChannelMessageReplyPipeline, f as createChannelPairingController, h as listSkillCommandsForAgents, i as normalizeMattermostAllowEntry, l as buildModelsProviderData, n as formatMattermostDirectMessageDropLog, o as resolveMattermostMonitorInboundAccess, s as DEFAULT_GROUP_HISTORY_LIMIT, t as authorizeMattermostCommandInvocation, u as createChannelHistoryWindow, v as logTypingFailure, x as registerPluginHttpRoute, y as parseTcpPort } from "./monitor-auth-dEHa1_On.js";
4
+ import { a as resolveInteractionCallbackPath, c as setInteractionSecret, i as createMattermostInteractionHandler, n as buildButtonProps, r as computeInteractionCallbackUrl, s as setInteractionCallbackUrl } from "./interactions-DO7J-ND1.js";
5
+ import { i as resolveMattermostReplyToMode, r as resolveMattermostAccount, t as listMattermostAccountIds } from "./accounts-C4Azn6rA.js";
6
+ import { i as normalizeMattermostMessagingTarget } from "./channel-plugin-runtime-BXmjNn6B.js";
7
+ import { _ as registerSlashCommands, a as sendMessageMattermost, c as deliverMattermostReplyPayload, d as renderMattermostModelsPickerView, f as renderMattermostProviderPickerView, g as isSlashCommandsEnabled, h as cleanupSlashCommands, l as buildMattermostAllowedModelRefs, m as DEFAULT_COMMAND_SPECS, n as deactivateSlashCommands, o as resolveMattermostOpaqueTarget, p as resolveMattermostModelPickerCurrentModel, r as getSlashCommandState, s as createMattermostReplyDeliveryBarrier, t as activateSlashCommands, u as parseMattermostModelPickerContext, v as resolveCallbackUrl, y as resolveSlashCommandConfig } from "./slash-state-C9bpBN40.js";
8
+ import { randomUUID } from "node:crypto";
9
+ import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
5
10
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries, normalizeTrimmedStringList, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
11
+ import { safeParseJsonWithSchema, safeParseWithSchema } from "openclaw/plugin-sdk/extension-shared";
12
+ import { asDateTimestampMs, resolveExpiresAtMsFromDurationMs, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
13
+ import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
14
+ import { fetchWithSsrFGuard, isPrivateNetworkOptInEnabled, ssrfPolicyFromPrivateNetworkOptIn } from "openclaw/plugin-sdk/ssrf-runtime";
15
+ import { z } from "zod";
6
16
  import { formatInboundFromLabel, formatInboundMediaUnavailableText } from "openclaw/plugin-sdk/channel-inbound";
7
17
  import { buildChannelProgressDraftLineForEntry, createChannelProgressDraftCompositor, createFinalizableDraftLifecycle, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, resolveChannelStreamingPreviewToolProgress } from "openclaw/plugin-sdk/channel-outbound";
8
18
  import { rawDataToString } from "openclaw/plugin-sdk/webhook-ingress";
9
- import { asDateTimestampMs, resolveExpiresAtMsFromDurationMs, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
10
- import { safeParseJsonWithSchema, safeParseWithSchema } from "openclaw/plugin-sdk/extension-shared";
11
19
  import { buildTtsSupplementMediaPayload, countOutboundMedia, getReplyPayloadTtsSupplement, isReasoningReplyPayload } from "openclaw/plugin-sdk/reply-payload";
12
- import { fetchWithSsrFGuard, isPrivateNetworkOptInEnabled, ssrfPolicyFromPrivateNetworkOptIn } from "openclaw/plugin-sdk/ssrf-runtime";
13
- import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
14
- import { z } from "zod";
15
20
  import { resolveInboundLastRouteSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
16
- import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
17
- import { randomUUID } from "node:crypto";
21
+ import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
22
+ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
18
23
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
19
- import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
20
- import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
24
+ import { chunkMarkdownTextWithMode } from "openclaw/plugin-sdk/reply-chunking";
25
+ import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
21
26
  import { captureWsEvent, createDebugProxyWebSocketAgent, resolveDebugProxySettings } from "openclaw/plugin-sdk/proxy-capture";
22
27
  import WebSocket from "ws";
23
28
  import { createPersistentDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
@@ -111,11 +116,14 @@ async function listMattermostDirectoryPeers(params) {
111
116
  const clients = buildClients(params);
112
117
  if (!clients.length) return [];
113
118
  const client = clients[0];
119
+ if (!client) return [];
114
120
  try {
115
121
  const me = await fetchMattermostMe(client);
116
122
  const teams = await client.request("/users/me/teams");
117
123
  if (!teams.length) return [];
118
- const teamId = teams[0].id;
124
+ const team = teams[0];
125
+ if (!team) return [];
126
+ const teamId = team.id;
119
127
  const q = normalizeLowercaseStringOrEmpty(params.query);
120
128
  let users;
121
129
  if (q) users = await client.request("/users/search", {
@@ -165,6 +173,20 @@ function normalizeMattermostDraftText(text, maxChars) {
165
173
  if (trimmed.length <= maxChars) return trimmed;
166
174
  return `${sliceUtf16Safe(trimmed, 0, Math.max(0, maxChars - 3)).trimEnd()}...`;
167
175
  }
176
+ function createMattermostDraftPreviewBoundaryController(params) {
177
+ let hasStreamedContent = false;
178
+ return {
179
+ noteUpdate() {
180
+ hasStreamedContent = true;
181
+ },
182
+ async noteBoundary() {
183
+ if (!params.enabled) return;
184
+ if (!hasStreamedContent) return;
185
+ hasStreamedContent = false;
186
+ await params.forceNewMessage();
187
+ }
188
+ };
189
+ }
168
190
  function createMattermostDraftStream(params) {
169
191
  const maxChars = Math.min(params.maxChars ?? MATTERMOST_STREAM_MAX_CHARS, MATTERMOST_STREAM_MAX_CHARS);
170
192
  const throttleMs = Math.max(250, params.throttleMs ?? DEFAULT_THROTTLE_MS);
@@ -172,15 +194,22 @@ function createMattermostDraftStream(params) {
172
194
  stopped: false,
173
195
  final: false
174
196
  };
175
- let streamPostId;
176
- let lastSentText = "";
197
+ let currentGeneration = {
198
+ lastSentText: "",
199
+ latestSourceText: "",
200
+ ready: Promise.resolve()
201
+ };
202
+ const sealedAssistantTexts = [];
177
203
  const sendOrEditStreamMessage = async (text) => {
178
204
  if (streamState.stopped && !streamState.final) return false;
205
+ const target = currentGeneration;
179
206
  const normalized = normalizeMattermostDraftText(params.renderText?.(text) ?? text, maxChars);
180
207
  if (!normalized) return false;
181
- if (normalized === lastSentText) return true;
208
+ await target.ready;
209
+ if (streamState.stopped && !streamState.final) return false;
210
+ if (normalized === target.lastSentText) return true;
182
211
  try {
183
- if (streamPostId) await updateMattermostPost(params.client, streamPostId, { message: normalized });
212
+ if (target.postId) await updateMattermostPost(params.client, target.postId, { message: normalized });
184
213
  else {
185
214
  const postId = (await createMattermostPost(params.client, {
186
215
  channelId: params.channelId,
@@ -192,9 +221,9 @@ function createMattermostDraftStream(params) {
192
221
  params.warn?.("mattermost stream preview stopped (missing post id from create)");
193
222
  return false;
194
223
  }
195
- streamPostId = postId;
224
+ target.postId = postId;
196
225
  }
197
- lastSentText = normalized;
226
+ target.lastSentText = normalized;
198
227
  return true;
199
228
  } catch (err) {
200
229
  streamState.stopped = true;
@@ -202,40 +231,333 @@ function createMattermostDraftStream(params) {
202
231
  return false;
203
232
  }
204
233
  };
205
- const { loop, update, stop, clear, discardPending, seal } = createFinalizableDraftLifecycle({
234
+ const clearMessageId = () => {
235
+ currentGeneration.postId = void 0;
236
+ };
237
+ const isValidMessageId = (value) => typeof value === "string" && value.length > 0;
238
+ const deleteMessage = async (postId) => {
239
+ await deleteMattermostPost(params.client, postId);
240
+ };
241
+ const { loop, update: updateLifecycle, stop: stopLifecycle, stopForClear, clearWithStop, seal: sealLifecycle } = createFinalizableDraftLifecycle({
206
242
  throttleMs,
207
243
  state: streamState,
208
244
  sendOrEditStreamMessage,
209
- readMessageId: () => streamPostId,
210
- clearMessageId: () => {
211
- streamPostId = void 0;
212
- },
213
- isValidMessageId: (value) => typeof value === "string" && value.length > 0,
214
- deleteMessage: async (postId) => {
215
- await deleteMattermostPost(params.client, postId);
216
- },
245
+ readMessageId: () => currentGeneration.postId,
246
+ clearMessageId,
247
+ isValidMessageId,
248
+ deleteMessage,
217
249
  warn: params.warn,
218
250
  warnPrefix: "mattermost stream preview cleanup failed"
219
251
  });
220
252
  const forceNewMessage = () => {
221
- streamPostId = void 0;
222
- lastSentText = "";
223
- loop.resetPending();
253
+ if (streamState.stopped || streamState.final) return Promise.resolve();
254
+ const pendingText = loop.takePending();
255
+ const inFlightAtBoundary = loop.waitForInFlight();
256
+ const sealed = currentGeneration;
257
+ const boundary = (async () => {
258
+ try {
259
+ await sealed.ready;
260
+ await inFlightAtBoundary;
261
+ if (streamState.stopped && !streamState.final) return;
262
+ const sourceText = pendingText.trim() ? pendingText : sealed.latestSourceText;
263
+ const finalizedText = (params.renderText?.(sourceText) ?? sourceText).trim();
264
+ const chunks = params.chunkText?.(finalizedText) ?? chunkMarkdownTextWithMode(finalizedText, maxChars, "length");
265
+ const firstChunk = chunks[0];
266
+ if (!firstChunk) return;
267
+ if (sealed.postId) {
268
+ if (firstChunk !== sealed.lastSentText) await updateMattermostPost(params.client, sealed.postId, { message: firstChunk });
269
+ } else await createMattermostPost(params.client, {
270
+ channelId: params.channelId,
271
+ message: firstChunk,
272
+ rootId: params.rootId
273
+ });
274
+ for (const chunk of chunks.slice(1)) await createMattermostPost(params.client, {
275
+ channelId: params.channelId,
276
+ message: chunk,
277
+ rootId: params.rootId
278
+ });
279
+ const assistantText = sealed.latestAssistantText?.trim();
280
+ if (assistantText) sealedAssistantTexts.push(assistantText);
281
+ } catch (err) {
282
+ params.warn?.(`mattermost stream preview boundary flush failed: ${err instanceof Error ? err.message : String(err)}`);
283
+ }
284
+ })();
285
+ currentGeneration = {
286
+ lastSentText: "",
287
+ latestSourceText: "",
288
+ ready: boundary
289
+ };
224
290
  loop.resetThrottleWindow();
291
+ return boundary;
292
+ };
293
+ const flush = async () => {
294
+ await loop.flush();
295
+ await currentGeneration.ready;
296
+ };
297
+ const discardPending = async () => {
298
+ await stopForClear();
299
+ await currentGeneration.ready;
300
+ };
301
+ const clear = async () => {
302
+ await clearWithStop(discardPending);
303
+ };
304
+ const seal = async () => {
305
+ await sealLifecycle();
306
+ await currentGeneration.ready;
307
+ };
308
+ const stop = async () => {
309
+ await stopLifecycle();
310
+ await currentGeneration.ready;
311
+ };
312
+ const update = (text) => {
313
+ currentGeneration.latestSourceText = text;
314
+ currentGeneration.latestAssistantText = void 0;
315
+ updateLifecycle(text);
316
+ };
317
+ const updateAssistantText = (text) => {
318
+ currentGeneration.latestSourceText = text;
319
+ currentGeneration.latestAssistantText = text;
320
+ updateLifecycle(text);
321
+ };
322
+ const settleBoundaries = async () => {
323
+ await currentGeneration.ready;
324
+ };
325
+ const resolveFinalText = (text) => {
326
+ if (sealedAssistantTexts.length === 0) return {
327
+ kind: "full",
328
+ text
329
+ };
330
+ let remainingText = text.trim();
331
+ for (const sealedText of sealedAssistantTexts) {
332
+ const completed = sealedText.trim();
333
+ if (!completed || !remainingText.startsWith(completed)) return {
334
+ kind: "full",
335
+ text
336
+ };
337
+ const suffix = remainingText.slice(completed.length);
338
+ if (suffix && !/^\r?\n/.test(suffix)) return {
339
+ kind: "full",
340
+ text
341
+ };
342
+ remainingText = suffix.replace(/^(?:\r?\n)+/, "");
343
+ }
344
+ const currentText = currentGeneration.latestAssistantText?.trim() ?? "";
345
+ const remaining = remainingText.trim();
346
+ if (currentText && !remaining.startsWith(currentText)) return {
347
+ kind: "full",
348
+ text
349
+ };
350
+ return remaining ? {
351
+ kind: "remaining",
352
+ text: remaining
353
+ } : { kind: "already-delivered" };
225
354
  };
226
355
  params.log?.(`mattermost stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
227
356
  return {
228
357
  update,
229
- flush: loop.flush,
230
- postId: () => streamPostId,
358
+ updateAssistantText,
359
+ flush,
360
+ postId: () => currentGeneration.postId,
231
361
  clear,
232
362
  discardPending,
233
363
  seal,
234
364
  stop,
235
- forceNewMessage
365
+ forceNewMessage,
366
+ settleBoundaries,
367
+ resolveFinalText
236
368
  };
237
369
  }
238
370
  //#endregion
371
+ //#region extensions/mattermost/src/mattermost/monitor-helpers.ts
372
+ const formatInboundFromLabel$1 = formatInboundFromLabel;
373
+ function resolveThreadSessionKeys$1(params) {
374
+ return resolveThreadSessionKeys({
375
+ ...params,
376
+ normalizeThreadId: (threadId) => threadId
377
+ });
378
+ }
379
+ /**
380
+ * Strip bot mention from message text while preserving newlines and
381
+ * block-level Markdown formatting (headings, lists, blockquotes).
382
+ */
383
+ function normalizeMention(text, mention) {
384
+ if (!mention) return text.trim();
385
+ const escaped = mention.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
386
+ const hasMentionRe = new RegExp(`@${escaped}\\b`, "i");
387
+ const leadingMentionRe = new RegExp(`^([\\t ]*)@${escaped}\\b[\\t ]*`, "i");
388
+ const trailingMentionRe = new RegExp(`[\\t ]*@${escaped}\\b[\\t ]*$`, "i");
389
+ const normalizedLines = text.split("\n").map((line) => {
390
+ const hadMention = hasMentionRe.test(line);
391
+ const normalizedLine = line.replace(leadingMentionRe, "$1").replace(trailingMentionRe, "").replace(new RegExp(`@${escaped}\\b`, "gi"), "").replace(/(\S)[ \t]{2,}/g, "$1 ");
392
+ return {
393
+ text: normalizedLine,
394
+ mentionOnlyBlank: hadMention && normalizedLine.trim() === ""
395
+ };
396
+ });
397
+ while (normalizedLines[0]?.mentionOnlyBlank) normalizedLines.shift();
398
+ while (normalizedLines.at(-1)?.text.trim() === "") normalizedLines.pop();
399
+ return normalizedLines.map((line) => line.text).join("\n");
400
+ }
401
+ function shouldDropEmptyMattermostBody(params) {
402
+ if (/[^\p{White_Space}\p{Cc}\p{Cf}\p{M}]/u.test(params.bodyText)) return false;
403
+ const botUsername = normalizeLowercaseStringOrEmpty(params.botUsername ?? "");
404
+ const bareMention = params.rawText.match(/^[ \t]*(@\S+)[ \t]*$/u)?.[1];
405
+ return !botUsername || normalizeLowercaseStringOrEmpty(bareMention ?? "") !== `@${botUsername}`;
406
+ }
407
+ //#endregion
408
+ //#region extensions/mattermost/src/mattermost/no-visible-reply-diagnostic.ts
409
+ /**
410
+ * Detects the #80501 symptom: `deliverMattermostReplyPayload` accepted a
411
+ * substantive (non-reasoning) payload, called the underlying
412
+ * `deliverTextOrMediaReply`, and the outcome was `"empty"` — meaning the
413
+ * payload had no text and no media to send, so no Mattermost API call
414
+ * happened. The agent's run completes successfully, but no visible
415
+ * channel/thread reply ever surfaces to the user.
416
+ *
417
+ * Returns a structured violation when the outcome is `"empty"` for a payload
418
+ * that nominally carried user-facing content (text or media bytes that ended
419
+ * up dropped by `resolveSendableOutboundReplyParts`/`sendMediaWithLeadingCaption`).
420
+ * Returns `null` for `"reasoning_skipped"` (intentional suppression),
421
+ * `"text"`, or `"media"` (successful visible sends).
422
+ */
423
+ function evaluateMattermostNoVisibleReply(params) {
424
+ if (params.outcome !== "empty") return null;
425
+ const finalText = typeof params.payload.text === "string" ? params.payload.text.trim() : "";
426
+ const mediaUrlCount = countOutboundMedia(params.payload);
427
+ if (finalText.length === 0 && mediaUrlCount === 0) return null;
428
+ return {
429
+ reason: "no-visible-reply-after-final-delivery",
430
+ outcome: params.outcome,
431
+ finalTextLength: finalText.length,
432
+ mediaUrlCount
433
+ };
434
+ }
435
+ function formatMattermostNoVisibleReplyLog(params) {
436
+ return `mattermost no-visible-reply: ${params.violation.reason} to=${params.to} accountId=${params.accountId} agentId=${params.agentId ?? "unknown"} outcome=${params.violation.outcome} finalTextLength=${params.violation.finalTextLength} mediaUrlCount=${params.violation.mediaUrlCount}`;
437
+ }
438
+ //#endregion
439
+ //#region extensions/mattermost/src/mattermost/monitor-context.ts
440
+ function shouldUpdateMattermostDraftToolProgress(account) {
441
+ return account.streamingMode !== "off" && resolveChannelStreamingPreviewToolProgress(account.config);
442
+ }
443
+ function shouldSuppressMattermostDefaultToolProgressMessages(account) {
444
+ return account.streamingMode !== "off";
445
+ }
446
+ function buildMattermostModelPickerSelectMessageSid(params) {
447
+ const provider = normalizeLowercaseStringOrEmpty(params.provider);
448
+ const model = normalizeLowercaseStringOrEmpty(params.model);
449
+ return `interaction:${params.postId}:select:${provider}/${model}`;
450
+ }
451
+ function resolveMattermostReplyRootId(params) {
452
+ const threadRootId = normalizeOptionalString(params.threadRootId);
453
+ if (params.kind === "direct" && !threadRootId) return;
454
+ if (threadRootId) return threadRootId;
455
+ return normalizeOptionalString(params.replyToId);
456
+ }
457
+ function canFinalizeMattermostPreviewInPlace(params) {
458
+ return resolveMattermostReplyRootId({
459
+ kind: params.kind,
460
+ threadRootId: params.threadRootId,
461
+ replyToId: params.replyToId
462
+ }) === params.previewRootId?.trim();
463
+ }
464
+ function formatMattermostFinalDeliveryOutcomeLog(params) {
465
+ const violation = evaluateMattermostNoVisibleReply({
466
+ outcome: params.outcome,
467
+ payload: params.payload
468
+ });
469
+ if (violation) return formatMattermostNoVisibleReplyLog({
470
+ violation,
471
+ to: params.to,
472
+ accountId: params.accountId,
473
+ agentId: params.agentId
474
+ });
475
+ if (params.outcome === "text" || params.outcome === "media") return `delivered reply to ${params.to}`;
476
+ }
477
+ function resolveMattermostEffectiveReplyToId(params) {
478
+ if (params.kind === "direct" && params.replyToMode === "off") return;
479
+ const threadRootId = normalizeOptionalString(params.threadRootId);
480
+ if (threadRootId) return threadRootId;
481
+ const postId = normalizeOptionalString(params.postId);
482
+ if (!postId) return;
483
+ return params.replyToMode === "all" || params.replyToMode === "first" || params.replyToMode === "batched" ? postId : void 0;
484
+ }
485
+ function resolveMattermostThreadSessionContext(params) {
486
+ const effectiveReplyToId = resolveMattermostEffectiveReplyToId({
487
+ kind: params.kind,
488
+ postId: params.postId,
489
+ replyToMode: params.replyToMode,
490
+ threadRootId: params.threadRootId
491
+ });
492
+ const threadKeys = resolveThreadSessionKeys$1({
493
+ baseSessionKey: params.baseSessionKey,
494
+ threadId: effectiveReplyToId,
495
+ parentSessionKey: effectiveReplyToId && params.kind !== "direct" ? params.baseSessionKey : void 0
496
+ });
497
+ return {
498
+ effectiveReplyToId,
499
+ sessionKey: threadKeys.sessionKey,
500
+ parentSessionKey: threadKeys.parentSessionKey
501
+ };
502
+ }
503
+ function resolveMattermostPendingHistoryKey(params) {
504
+ return params.kind === "direct" ? null : params.sessionKey;
505
+ }
506
+ function resolveMattermostReactionChannelId(payload) {
507
+ return normalizeOptionalString(payload.broadcast?.channel_id) ?? normalizeOptionalString(payload.data?.channel_id);
508
+ }
509
+ //#endregion
510
+ //#region extensions/mattermost/src/mattermost/monitor-draft-delivery.ts
511
+ async function deliverMattermostReplyWithDraftPreview(params) {
512
+ if (isReasoningReplyPayload(params.payload)) return;
513
+ await deliverWithFinalizableLivePreviewAdapter({
514
+ kind: params.info.kind,
515
+ payload: params.payload,
516
+ adapter: defineFinalizableLivePreviewAdapter({
517
+ draft: {
518
+ flush: params.draftStream.flush,
519
+ clear: params.draftStream.clear,
520
+ discardPending: params.draftStream.discardPending,
521
+ seal: params.draftStream.seal,
522
+ id: params.draftStream.postId
523
+ },
524
+ buildFinalEdit: (payload) => {
525
+ const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
526
+ const ttsSupplement = getReplyPayloadTtsSupplement(payload);
527
+ const previewFinalText = params.resolvePreviewFinalText(payload.text ?? ttsSupplement?.spokenText);
528
+ if (hasMedia && !ttsSupplement || typeof previewFinalText !== "string" || payload.isError || !canFinalizeMattermostPreviewInPlace({
529
+ kind: params.kind,
530
+ previewRootId: params.effectiveReplyToId,
531
+ threadRootId: params.effectiveReplyToId,
532
+ replyToId: payload.replyToId
533
+ })) return;
534
+ return { message: previewFinalText };
535
+ },
536
+ editFinal: async (previewPostId, edit) => {
537
+ await updateMattermostPost(params.client, previewPostId, edit);
538
+ },
539
+ onPreviewFinalized: () => {
540
+ params.previewState.finalizedViaPreviewPost = true;
541
+ params.recordThreadParticipation?.();
542
+ },
543
+ buildSupplementalPayload: (payload) => getReplyPayloadTtsSupplement(payload) ? buildTtsSupplementMediaPayload(payload) : void 0,
544
+ deliverSupplemental: async (payload) => {
545
+ await params.deliverPayload(payload);
546
+ },
547
+ logPreviewEditFailure: (err) => {
548
+ params.logVerboseMessage(`mattermost preview final edit failed; falling back to normal send (${String(err)})`);
549
+ }
550
+ }),
551
+ deliverNormally: async (payload) => {
552
+ const supplement = getReplyPayloadTtsSupplement(payload);
553
+ await params.deliverPayload(supplement && !payload.text?.trim() && supplement.visibleTextAlreadyDelivered !== true ? {
554
+ ...payload,
555
+ text: supplement.spokenText
556
+ } : payload);
557
+ }
558
+ });
559
+ }
560
+ //#endregion
239
561
  //#region extensions/mattermost/src/mattermost/monitor-gating.ts
240
562
  function mapMattermostChannelTypeToChatType(channelType) {
241
563
  const normalized = channelType?.trim().toUpperCase();
@@ -279,43 +601,6 @@ function evaluateMattermostMentionGate(params) {
279
601
  };
280
602
  }
281
603
  //#endregion
282
- //#region extensions/mattermost/src/mattermost/monitor-helpers.ts
283
- const formatInboundFromLabel$1 = formatInboundFromLabel;
284
- function resolveThreadSessionKeys$1(params) {
285
- return resolveThreadSessionKeys({
286
- ...params,
287
- normalizeThreadId: (threadId) => threadId
288
- });
289
- }
290
- /**
291
- * Strip bot mention from message text while preserving newlines and
292
- * block-level Markdown formatting (headings, lists, blockquotes).
293
- */
294
- function normalizeMention(text, mention) {
295
- if (!mention) return text.trim();
296
- const escaped = mention.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
297
- const hasMentionRe = new RegExp(`@${escaped}\\b`, "i");
298
- const leadingMentionRe = new RegExp(`^([\\t ]*)@${escaped}\\b[\\t ]*`, "i");
299
- const trailingMentionRe = new RegExp(`[\\t ]*@${escaped}\\b[\\t ]*$`, "i");
300
- const normalizedLines = text.split("\n").map((line) => {
301
- const hadMention = hasMentionRe.test(line);
302
- const normalizedLine = line.replace(leadingMentionRe, "$1").replace(trailingMentionRe, "").replace(new RegExp(`@${escaped}\\b`, "gi"), "").replace(/(\S)[ \t]{2,}/g, "$1 ");
303
- return {
304
- text: normalizedLine,
305
- mentionOnlyBlank: hadMention && normalizedLine.trim() === ""
306
- };
307
- });
308
- while (normalizedLines[0]?.mentionOnlyBlank) normalizedLines.shift();
309
- while (normalizedLines.at(-1)?.text.trim() === "") normalizedLines.pop();
310
- return normalizedLines.map((line) => line.text).join("\n");
311
- }
312
- function shouldDropEmptyMattermostBody(params) {
313
- if (/[^\p{White_Space}\p{Cc}\p{Cf}\p{M}]/u.test(params.bodyText)) return false;
314
- const botUsername = normalizeLowercaseStringOrEmpty(params.botUsername ?? "");
315
- const bareMention = params.rawText.match(/^[ \t]*(@\S+)[ \t]*$/u)?.[1];
316
- return !botUsername || normalizeLowercaseStringOrEmpty(bareMention ?? "") !== `@${botUsername}`;
317
- }
318
- //#endregion
319
604
  //#region extensions/mattermost/src/mattermost/monitor-onchar.ts
320
605
  const DEFAULT_ONCHAR_PREFIXES = [">", "!"];
321
606
  function resolveOncharPrefixes(prefixes) {
@@ -337,6 +622,31 @@ function stripOncharPrefix(text, prefixes) {
337
622
  };
338
623
  }
339
624
  //#endregion
625
+ //#region extensions/mattermost/src/mattermost/monitor-replay.ts
626
+ const RECENT_MATTERMOST_MESSAGE_TTL_MS = 5 * 6e4;
627
+ const RECENT_MATTERMOST_MESSAGE_MAX = 2e3;
628
+ function buildMattermostInboundReplayKeys(params) {
629
+ return params.messageIds.map((id) => id.trim() ? `${params.accountId}:${id.trim()}` : "");
630
+ }
631
+ function createMattermostInboundReplayGuard() {
632
+ return createChannelReplayGuard({
633
+ dedupe: {
634
+ ttlMs: RECENT_MATTERMOST_MESSAGE_TTL_MS,
635
+ memoryMaxSize: RECENT_MATTERMOST_MESSAGE_MAX
636
+ },
637
+ buildReplayKey: buildMattermostInboundReplayKeys
638
+ });
639
+ }
640
+ const recentInboundMessages = createMattermostInboundReplayGuard();
641
+ async function processMattermostReplayGuardedPost(params) {
642
+ const replayGuard = params.replayGuard ?? recentInboundMessages;
643
+ const event = {
644
+ accountId: params.accountId,
645
+ messageIds: params.messageIds
646
+ };
647
+ return (await replayGuard.processGuarded(event, params.handlePost, { onError: "commit" })).kind === "processed" ? "processed" : "duplicate";
648
+ }
649
+ //#endregion
340
650
  //#region extensions/mattermost/src/mattermost/monitor-resources.ts
341
651
  function formatMattermostInboundMediaText(params) {
342
652
  const unavailableCount = Math.max(0, params.expectedCount - params.mediaCount);
@@ -349,6 +659,9 @@ function formatMattermostInboundMediaText(params) {
349
659
  }
350
660
  const CHANNEL_CACHE_TTL_MS = 5 * 6e4;
351
661
  const USER_CACHE_TTL_MS = 10 * 6e4;
662
+ const MONITOR_RESOURCE_CACHE_MAX_ENTRIES = 1e3;
663
+ const MATTERMOST_MEDIA_RESPONSE_HEADER_TIMEOUT_MS = 12e4;
664
+ const MATTERMOST_MEDIA_READ_IDLE_TIMEOUT_MS = 3e4;
352
665
  function createMattermostMonitorResources(params) {
353
666
  const { accountId, callbackUrl, client, logger, mediaMaxBytes, saveRemoteMedia, mediaKindFromMime } = params;
354
667
  const channelCache = /* @__PURE__ */ new Map();
@@ -361,10 +674,14 @@ function createMattermostMonitorResources(params) {
361
674
  };
362
675
  const setCachedValue = (cache, key, value, ttlMs, rawNowMs) => {
363
676
  const expiresAt = resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: rawNowMs });
364
- if (expiresAt !== void 0) cache.set(key, {
365
- value,
366
- expiresAt
367
- });
677
+ if (expiresAt !== void 0) {
678
+ cache.delete(key);
679
+ cache.set(key, {
680
+ value,
681
+ expiresAt
682
+ });
683
+ pruneMapToMaxSize(cache, MONITOR_RESOURCE_CACHE_MAX_ENTRIES);
684
+ }
368
685
  };
369
686
  const resolveMattermostMedia = async (fileIds) => {
370
687
  const ids = normalizeStringEntries(fileIds ?? []);
@@ -376,7 +693,9 @@ function createMattermostMonitorResources(params) {
376
693
  requestInit: { headers: { Authorization: `Bearer ${client.token}` } },
377
694
  filePathHint: fileId,
378
695
  maxBytes: mediaMaxBytes,
379
- ssrfPolicy: { allowedHostnames: [new URL(client.baseUrl).hostname] }
696
+ ssrfPolicy: { allowedHostnames: [new URL(client.baseUrl).hostname] },
697
+ responseHeaderTimeoutMs: MATTERMOST_MEDIA_RESPONSE_HEADER_TIMEOUT_MS,
698
+ readIdleTimeoutMs: MATTERMOST_MEDIA_READ_IDLE_TIMEOUT_MS
380
699
  });
381
700
  const contentType = saved.contentType ?? void 0;
382
701
  out.push({
@@ -565,6 +884,7 @@ async function registerMattermostMonitorSlashCommands(params) {
565
884
  //#endregion
566
885
  //#region extensions/mattermost/src/mattermost/monitor-websocket.ts
567
886
  const MATTERMOST_WEBSOCKET_MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
887
+ const MATTERMOST_WEBSOCKET_HANDSHAKE_TIMEOUT_MS = 3e4;
568
888
  const MattermostEventPayloadSchema = z.object({
569
889
  event: z.string().optional(),
570
890
  data: z.object({
@@ -598,11 +918,11 @@ var WebSocketClosedBeforeOpenError = class extends Error {
598
918
  this.name = "WebSocketClosedBeforeOpenError";
599
919
  }
600
920
  };
601
- const defaultMattermostWebSocketFactory = (url) => {
921
+ const defaultMattermostWebSocketFactory = (url, options) => {
602
922
  const agent = createDebugProxyWebSocketAgent(resolveDebugProxySettings());
603
923
  return new WebSocket(url, {
604
- ...agent ? { agent } : {},
605
- maxPayload: MATTERMOST_WEBSOCKET_MAX_PAYLOAD_BYTES
924
+ ...options,
925
+ ...agent ? { agent } : {}
606
926
  });
607
927
  };
608
928
  function parsePostedPayload(payload) {
@@ -623,7 +943,10 @@ function createMattermostConnectOnce(opts) {
623
943
  const pongTimeoutMs = opts.pongTimeoutMs ?? 1e4;
624
944
  return async () => {
625
945
  const flowId = randomUUID();
626
- const ws = webSocketFactory(opts.wsUrl);
946
+ const ws = webSocketFactory(opts.wsUrl, {
947
+ maxPayload: MATTERMOST_WEBSOCKET_MAX_PAYLOAD_BYTES,
948
+ handshakeTimeout: MATTERMOST_WEBSOCKET_HANDSHAKE_TIMEOUT_MS
949
+ });
627
950
  const onAbort = () => ws.terminate();
628
951
  opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
629
952
  const getBotUpdateAt = opts.getBotUpdateAt;
@@ -849,37 +1172,6 @@ function reasonToString(reason) {
849
1172
  return reason.length > 0 ? reason.toString("utf8") : "";
850
1173
  }
851
1174
  //#endregion
852
- //#region extensions/mattermost/src/mattermost/no-visible-reply-diagnostic.ts
853
- /**
854
- * Detects the #80501 symptom: `deliverMattermostReplyPayload` accepted a
855
- * substantive (non-reasoning) payload, called the underlying
856
- * `deliverTextOrMediaReply`, and the outcome was `"empty"` — meaning the
857
- * payload had no text and no media to send, so no Mattermost API call
858
- * happened. The agent's run completes successfully, but no visible
859
- * channel/thread reply ever surfaces to the user.
860
- *
861
- * Returns a structured violation when the outcome is `"empty"` for a payload
862
- * that nominally carried user-facing content (text or media bytes that ended
863
- * up dropped by `resolveSendableOutboundReplyParts`/`sendMediaWithLeadingCaption`).
864
- * Returns `null` for `"reasoning_skipped"` (intentional suppression),
865
- * `"text"`, or `"media"` (successful visible sends).
866
- */
867
- function evaluateMattermostNoVisibleReply(params) {
868
- if (params.outcome !== "empty") return null;
869
- const finalText = typeof params.payload.text === "string" ? params.payload.text.trim() : "";
870
- const mediaUrlCount = countOutboundMedia(params.payload);
871
- if (finalText.length === 0 && mediaUrlCount === 0) return null;
872
- return {
873
- reason: "no-visible-reply-after-final-delivery",
874
- outcome: params.outcome,
875
- finalTextLength: finalText.length,
876
- mediaUrlCount
877
- };
878
- }
879
- function formatMattermostNoVisibleReplyLog(params) {
880
- return `mattermost no-visible-reply: ${params.violation.reason} to=${params.to} accountId=${params.accountId} agentId=${params.agentId ?? "unknown"} outcome=${params.violation.outcome} finalTextLength=${params.violation.finalTextLength} mediaUrlCount=${params.violation.mediaUrlCount}`;
881
- }
882
- //#endregion
883
1175
  //#region extensions/mattermost/src/mattermost/reconnect.ts
884
1176
  /**
885
1177
  * Reconnection loop with exponential backoff.
@@ -992,58 +1284,9 @@ async function hasMattermostThreadParticipationWithPersistence(params) {
992
1284
  }
993
1285
  //#endregion
994
1286
  //#region extensions/mattermost/src/mattermost/monitor.ts
995
- function shouldUpdateMattermostDraftToolProgress(account) {
996
- return account.streamingMode !== "off" && resolveChannelStreamingPreviewToolProgress(account.config);
997
- }
998
- function shouldSuppressMattermostDefaultToolProgressMessages(account) {
999
- return account.streamingMode !== "off";
1000
- }
1001
- const RECENT_MATTERMOST_MESSAGE_TTL_MS = 5 * 6e4;
1002
- const RECENT_MATTERMOST_MESSAGE_MAX = 2e3;
1003
1287
  function normalizeInteractionSourceIps(values) {
1004
1288
  return normalizeTrimmedStringList(values);
1005
1289
  }
1006
- const recentInboundMessages = createClaimableDedupe({
1007
- ttlMs: RECENT_MATTERMOST_MESSAGE_TTL_MS,
1008
- memoryMaxSize: RECENT_MATTERMOST_MESSAGE_MAX
1009
- });
1010
- var MattermostRetryableInboundError = class extends Error {
1011
- constructor(message, options) {
1012
- super(message, options);
1013
- this.name = "MattermostRetryableInboundError";
1014
- }
1015
- };
1016
- function buildMattermostModelPickerSelectMessageSid(params) {
1017
- const provider = normalizeLowercaseStringOrEmpty(params.provider);
1018
- const model = normalizeLowercaseStringOrEmpty(params.model);
1019
- return `interaction:${params.postId}:select:${provider}/${model}`;
1020
- }
1021
- function buildMattermostInboundReplayKeys(params) {
1022
- return uniqueStrings(params.messageIds.map((id) => `${params.accountId}:${id.trim()}`)).filter((key) => !key.endsWith(":"));
1023
- }
1024
- async function processMattermostReplayGuardedPost(params) {
1025
- const replayGuard = params.replayGuard ?? recentInboundMessages;
1026
- const replayKeys = buildMattermostInboundReplayKeys({
1027
- accountId: params.accountId,
1028
- messageIds: params.messageIds
1029
- });
1030
- if (replayKeys.length === 0) {
1031
- await params.handlePost();
1032
- return "processed";
1033
- }
1034
- const claimedKeys = [];
1035
- for (const replayKey of replayKeys) if ((await replayGuard.claim(replayKey)).kind === "claimed") claimedKeys.push(replayKey);
1036
- if (claimedKeys.length === 0) return "duplicate";
1037
- try {
1038
- await params.handlePost();
1039
- await Promise.all(claimedKeys.map((replayKey) => replayGuard.commit(replayKey)));
1040
- return "processed";
1041
- } catch (error) {
1042
- if (error instanceof MattermostRetryableInboundError) claimedKeys.forEach((replayKey) => replayGuard.release(replayKey, { error }));
1043
- else await Promise.all(claimedKeys.map((replayKey) => replayGuard.commit(replayKey)));
1044
- throw error;
1045
- }
1046
- }
1047
1290
  function resolveRuntime(opts) {
1048
1291
  return opts.runtime ?? {
1049
1292
  log: console.log,
@@ -1061,126 +1304,32 @@ function channelChatType(kind) {
1061
1304
  if (kind === "group") return "group";
1062
1305
  return "channel";
1063
1306
  }
1064
- function resolveMattermostReplyRootId(params) {
1065
- if (params.kind === "direct") return;
1066
- const threadRootId = normalizeOptionalString(params.threadRootId);
1067
- if (threadRootId) return threadRootId;
1068
- return normalizeOptionalString(params.replyToId);
1069
- }
1070
- function canFinalizeMattermostPreviewInPlace(params) {
1071
- return resolveMattermostReplyRootId({
1072
- kind: params.kind,
1073
- threadRootId: params.threadRootId,
1074
- replyToId: params.replyToId
1075
- }) === params.previewRootId?.trim();
1076
- }
1077
1307
  function createDisabledMattermostDraftStream() {
1078
1308
  const noopAsync = async () => {};
1079
1309
  return {
1080
1310
  update: () => {},
1311
+ updateAssistantText: () => {},
1081
1312
  flush: noopAsync,
1082
1313
  postId: () => void 0,
1083
1314
  clear: noopAsync,
1084
1315
  discardPending: noopAsync,
1085
1316
  seal: noopAsync,
1086
1317
  stop: noopAsync,
1087
- forceNewMessage: () => {}
1088
- };
1089
- }
1090
- async function deliverMattermostReplyWithDraftPreview(params) {
1091
- if (isReasoningReplyPayload(params.payload)) return;
1092
- await deliverWithFinalizableLivePreviewAdapter({
1093
- kind: params.info.kind,
1094
- payload: params.payload,
1095
- adapter: defineFinalizableLivePreviewAdapter({
1096
- draft: {
1097
- flush: params.draftStream.flush,
1098
- clear: params.draftStream.clear,
1099
- discardPending: params.draftStream.discardPending,
1100
- seal: params.draftStream.seal,
1101
- id: params.draftStream.postId
1102
- },
1103
- buildFinalEdit: (payload) => {
1104
- const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
1105
- const ttsSupplement = getReplyPayloadTtsSupplement(payload);
1106
- const previewFinalText = params.resolvePreviewFinalText(payload.text ?? ttsSupplement?.spokenText);
1107
- if (hasMedia && !ttsSupplement || typeof previewFinalText !== "string" || payload.isError || !canFinalizeMattermostPreviewInPlace({
1108
- kind: params.kind,
1109
- previewRootId: params.effectiveReplyToId,
1110
- threadRootId: params.effectiveReplyToId,
1111
- replyToId: payload.replyToId
1112
- })) return;
1113
- return { message: previewFinalText };
1114
- },
1115
- editFinal: async (previewPostId, edit) => {
1116
- await updateMattermostPost(params.client, previewPostId, edit);
1117
- },
1118
- onPreviewFinalized: () => {
1119
- params.previewState.finalizedViaPreviewPost = true;
1120
- params.recordThreadParticipation?.();
1121
- },
1122
- buildSupplementalPayload: (payload) => getReplyPayloadTtsSupplement(payload) ? buildTtsSupplementMediaPayload(payload) : void 0,
1123
- deliverSupplemental: async (payload) => {
1124
- await params.deliverPayload(payload);
1125
- },
1126
- logPreviewEditFailure: (err) => {
1127
- params.logVerboseMessage(`mattermost preview final edit failed; falling back to normal send (${String(err)})`);
1128
- }
1129
- }),
1130
- deliverNormally: async (payload) => {
1131
- const supplement = getReplyPayloadTtsSupplement(payload);
1132
- await params.deliverPayload(supplement && !payload.text?.trim() && supplement.visibleTextAlreadyDelivered !== true ? {
1133
- ...payload,
1134
- text: supplement.spokenText
1135
- } : payload);
1136
- }
1137
- });
1138
- }
1139
- function formatMattermostFinalDeliveryOutcomeLog(params) {
1140
- const violation = evaluateMattermostNoVisibleReply({
1141
- outcome: params.outcome,
1142
- payload: params.payload
1143
- });
1144
- if (violation) return formatMattermostNoVisibleReplyLog({
1145
- violation,
1146
- to: params.to,
1147
- accountId: params.accountId,
1148
- agentId: params.agentId
1149
- });
1150
- if (params.outcome === "text" || params.outcome === "media") return `delivered reply to ${params.to}`;
1151
- }
1152
- function resolveMattermostEffectiveReplyToId(params) {
1153
- if (params.kind === "direct") return;
1154
- const threadRootId = normalizeOptionalString(params.threadRootId);
1155
- if (threadRootId) return threadRootId;
1156
- const postId = normalizeOptionalString(params.postId);
1157
- if (!postId) return;
1158
- return params.replyToMode === "all" || params.replyToMode === "first" || params.replyToMode === "batched" ? postId : void 0;
1159
- }
1160
- function resolveMattermostThreadSessionContext(params) {
1161
- const effectiveReplyToId = resolveMattermostEffectiveReplyToId({
1162
- kind: params.kind,
1163
- postId: params.postId,
1164
- replyToMode: params.replyToMode,
1165
- threadRootId: params.threadRootId
1166
- });
1167
- const threadKeys = resolveThreadSessionKeys$1({
1168
- baseSessionKey: params.baseSessionKey,
1169
- threadId: effectiveReplyToId,
1170
- parentSessionKey: effectiveReplyToId ? params.baseSessionKey : void 0
1171
- });
1172
- return {
1173
- effectiveReplyToId,
1174
- sessionKey: threadKeys.sessionKey,
1175
- parentSessionKey: threadKeys.parentSessionKey
1318
+ forceNewMessage: noopAsync,
1319
+ settleBoundaries: noopAsync,
1320
+ resolveFinalText: (text) => ({
1321
+ kind: "full",
1322
+ text
1323
+ })
1176
1324
  };
1177
1325
  }
1178
- function resolveMattermostReactionChannelId(payload) {
1179
- return normalizeOptionalString(payload.broadcast?.channel_id) ?? normalizeOptionalString(payload.data?.channel_id);
1180
- }
1181
1326
  function buildMattermostAttachmentPlaceholder(mediaList) {
1182
1327
  if (mediaList.length === 0) return "";
1183
- if (mediaList.length === 1) return `<media:${mediaList[0].kind === "unknown" ? "document" : mediaList[0].kind}>`;
1328
+ if (mediaList.length === 1) {
1329
+ const media = mediaList[0];
1330
+ if (!media) return "";
1331
+ return `<media:${media.kind === "unknown" ? "document" : media.kind}>`;
1332
+ }
1184
1333
  const allImages = mediaList.every((media) => media.kind === "image");
1185
1334
  const label = allImages ? "image" : "file";
1186
1335
  const suffix = mediaList.length === 1 ? label : `${label}s`;
@@ -1442,17 +1591,22 @@ async function monitorMattermostProvider(opts = {}) {
1442
1591
  },
1443
1592
  onReplyStart: typingCallbacks?.onReplyStart
1444
1593
  });
1445
- await core.channel.reply.dispatchReplyFromConfig({
1446
- ctx: ctxPayload,
1447
- cfg,
1594
+ await core.channel.reply.withReplyDispatcher({
1448
1595
  dispatcher,
1449
- replyOptions: {
1450
- ...replyOptions,
1451
- disableBlockStreaming: typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
1452
- onModelSelected
1453
- }
1596
+ onSettled: () => {
1597
+ markDispatchIdle();
1598
+ },
1599
+ run: () => core.channel.reply.dispatchReplyFromConfig({
1600
+ ctx: ctxPayload,
1601
+ cfg,
1602
+ dispatcher,
1603
+ replyOptions: {
1604
+ ...replyOptions,
1605
+ disableBlockStreaming: typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
1606
+ onModelSelected
1607
+ }
1608
+ })
1454
1609
  });
1455
- markDispatchIdle();
1456
1610
  },
1457
1611
  log: (msg) => runtime.log?.(msg)
1458
1612
  }),
@@ -1912,7 +2066,10 @@ async function monitorMattermostProvider(opts = {}) {
1912
2066
  replyToMode,
1913
2067
  threadRootId
1914
2068
  });
1915
- const historyKey = kind === "direct" ? null : sessionKey;
2069
+ const historyKey = resolveMattermostPendingHistoryKey({
2070
+ kind,
2071
+ sessionKey
2072
+ });
1916
2073
  const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, route.agentId);
1917
2074
  const wasMentioned = kind !== "direct" && ((botUsername ? normalizeLowercaseStringOrEmpty(rawText).includes(`@${normalizeLowercaseStringOrEmpty(botUsername)}`) : false) || core.channel.mentions.matchesMentionPatterns(rawText, mentionRegexes));
1918
2075
  const pendingBody = rawText || (post.file_ids?.length ? `[Mattermost ${post.file_ids.length === 1 ? "file" : "files"}]` : "");
@@ -2074,7 +2231,7 @@ async function monitorMattermostProvider(opts = {}) {
2074
2231
  normalizeEntry: normalizeMattermostAllowEntry
2075
2232
  }) : null;
2076
2233
  const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId: route.agentId });
2077
- const previewLine = bodyText.slice(0, 200).replace(/\n/g, "\\n");
2234
+ const previewLine = truncateUtf16Safe(bodyText, 200).replace(/\n/g, "\\n");
2078
2235
  logVerboseMessage(`mattermost inbound: from=${ctxPayload.From} len=${bodyText.length} preview="${previewLine}"`);
2079
2236
  const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, { fallbackLimit: account.textChunkLimit ?? 4e3 });
2080
2237
  const tableMode = core.channel.text.resolveMarkdownTableMode({
@@ -2082,7 +2239,8 @@ async function monitorMattermostProvider(opts = {}) {
2082
2239
  channel: "mattermost",
2083
2240
  accountId: account.accountId
2084
2241
  });
2085
- const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelMessageReplyPipeline({
2242
+ const chunkMode = core.channel.text.resolveChunkMode(cfg, "mattermost", account.accountId);
2243
+ const { onModelSelected, typingCallbacks, resolveResponsePrefix, ...replyPipeline } = createChannelMessageReplyPipeline({
2086
2244
  cfg,
2087
2245
  agentId: route.agentId,
2088
2246
  channel: "mattermost",
@@ -2107,10 +2265,22 @@ async function monitorMattermostProvider(opts = {}) {
2107
2265
  channelId,
2108
2266
  rootId: effectiveReplyToId,
2109
2267
  throttleMs: 1200,
2268
+ chunkText: (value) => core.channel.text.chunkMarkdownTextWithMode(core.channel.text.convertMarkdownTables(value, tableMode), textLimit, chunkMode),
2110
2269
  log: logVerboseMessage,
2111
2270
  warn: logVerboseMessage
2112
2271
  }) : createDisabledMattermostDraftStream();
2272
+ const previewBoundaryController = createMattermostDraftPreviewBoundaryController({
2273
+ enabled: draftPreviewEnabled && account.streamingMode === "block",
2274
+ forceNewMessage: async () => {
2275
+ await draftStream.forceNewMessage();
2276
+ }
2277
+ });
2113
2278
  let lastPartialText = "";
2279
+ let firstAssistantPreviewPrefix;
2280
+ let firstAssistantPreviewPrefixPending = true;
2281
+ let currentAssistantPreviewUsesPrefix = false;
2282
+ let blockPreviewActivity = "none";
2283
+ let blockPreviewAssistantMessagePending = false;
2114
2284
  const progressDraft = createChannelProgressDraftCompositor({
2115
2285
  entry: account.config,
2116
2286
  mode: account.streamingMode,
@@ -2121,11 +2291,28 @@ async function monitorMattermostProvider(opts = {}) {
2121
2291
  if (options?.flush) await draftStream.flush();
2122
2292
  }
2123
2293
  });
2294
+ const enterBlockPreviewActivity = (activity) => {
2295
+ if (account.streamingMode !== "block") return;
2296
+ const continuesCurrentActivity = activity === "tool" && blockPreviewActivity === "tool" || activity === "text" && blockPreviewActivity === "text" && !blockPreviewAssistantMessagePending || activity === "reasoning" && blockPreviewActivity === "reasoning";
2297
+ const startsNewGeneration = !continuesCurrentActivity && !(blockPreviewActivity === "reasoning");
2298
+ if (startsNewGeneration) currentAssistantPreviewUsesPrefix = false;
2299
+ const boundarySettled = startsNewGeneration ? previewBoundaryController.noteBoundary() : void 0;
2300
+ if (!continuesCurrentActivity) progressDraft.reset();
2301
+ blockPreviewActivity = activity;
2302
+ blockPreviewAssistantMessagePending = false;
2303
+ if (activity === "tool") lastPartialText = "";
2304
+ return boundarySettled;
2305
+ };
2124
2306
  const previewState = { finalizedViaPreviewPost: false };
2125
- const resolvePreviewFinalText = (text) => {
2307
+ const resolveFinalDeliveryText = (text) => {
2126
2308
  if (typeof text !== "string") return;
2127
- const formatted = core.channel.text.convertMarkdownTables(text, tableMode);
2128
- const chunkMode = core.channel.text.resolveChunkMode(cfg, "mattermost", account.accountId);
2309
+ const resolution = draftStream.resolveFinalText(text);
2310
+ return resolution.kind === "already-delivered" ? "" : resolution.text;
2311
+ };
2312
+ const resolvePreviewFinalText = (text) => {
2313
+ const deliveryText = resolveFinalDeliveryText(text);
2314
+ if (typeof deliveryText !== "string") return;
2315
+ const formatted = core.channel.text.convertMarkdownTables(deliveryText, tableMode);
2129
2316
  const chunks = core.channel.text.chunkMarkdownTextWithMode(formatted, textLimit, chunkMode);
2130
2317
  if (!chunks.length && formatted) chunks.push(formatted);
2131
2318
  if (chunks.length != 1) return;
@@ -2139,8 +2326,17 @@ async function monitorMattermostProvider(opts = {}) {
2139
2326
  if (!cleaned) return;
2140
2327
  if (cleaned === lastPartialText) return;
2141
2328
  if (lastPartialText && lastPartialText.startsWith(cleaned) && cleaned.length < lastPartialText.length) return;
2329
+ const boundarySettled = enterBlockPreviewActivity("text");
2142
2330
  lastPartialText = cleaned;
2143
- draftStream.update(cleaned);
2331
+ if (firstAssistantPreviewPrefixPending) {
2332
+ firstAssistantPreviewPrefix = resolveResponsePrefix?.();
2333
+ firstAssistantPreviewPrefixPending = false;
2334
+ currentAssistantPreviewUsesPrefix = Boolean(firstAssistantPreviewPrefix);
2335
+ }
2336
+ const previewText = currentAssistantPreviewUsesPrefix && firstAssistantPreviewPrefix ? cleaned.startsWith(firstAssistantPreviewPrefix) ? cleaned : `${firstAssistantPreviewPrefix} ${cleaned}` : cleaned;
2337
+ draftStream.updateAssistantText(previewText);
2338
+ previewBoundaryController.noteUpdate();
2339
+ return boundarySettled;
2144
2340
  };
2145
2341
  const deliveryBarrier = createMattermostReplyDeliveryBarrier({
2146
2342
  isDirect: kind === "direct",
@@ -2153,7 +2349,11 @@ async function monitorMattermostProvider(opts = {}) {
2153
2349
  humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
2154
2350
  typingCallbacks,
2155
2351
  deliver: async (payloadEntry, info) => {
2156
- if (info.kind === "final") progressDraft.markFinalReplyStarted();
2352
+ if (info.kind === "final") {
2353
+ await enterBlockPreviewActivity("text");
2354
+ await draftStream.settleBoundaries();
2355
+ progressDraft.markFinalReplyStarted();
2356
+ }
2157
2357
  const markThreadParticipation = () => {
2158
2358
  if (kind !== "direct" && effectiveReplyToId) recordMattermostThreadParticipation(account.accountId, channelId, effectiveReplyToId, { agentId: route.agentId });
2159
2359
  };
@@ -2169,10 +2369,15 @@ async function monitorMattermostProvider(opts = {}) {
2169
2369
  logVerboseMessage,
2170
2370
  recordThreadParticipation: markThreadParticipation,
2171
2371
  deliverPayload: async (payloadToDeliver) => {
2372
+ const finalTextResolution = info.kind === "final" && !payloadToDeliver.isError && typeof payloadToDeliver.text === "string" ? draftStream.resolveFinalText(payloadToDeliver.text) : void 0;
2373
+ const resolvedPayload = finalTextResolution ? {
2374
+ ...payloadToDeliver,
2375
+ text: finalTextResolution.kind === "already-delivered" ? "" : finalTextResolution.text
2376
+ } : payloadToDeliver;
2172
2377
  const outcome = await deliverMattermostReplyPayload({
2173
2378
  core,
2174
2379
  cfg,
2175
- payload: payloadToDeliver,
2380
+ payload: resolvedPayload,
2176
2381
  to,
2177
2382
  accountId: account.accountId,
2178
2383
  agentId: route.agentId,
@@ -2187,9 +2392,10 @@ async function monitorMattermostProvider(opts = {}) {
2187
2392
  onDmChannelResolution: deliveryBarrier.trackDmChannelResolution
2188
2393
  });
2189
2394
  if (outcome === "text" || outcome === "media") markThreadParticipation();
2395
+ else if (outcome === "empty" && finalTextResolution?.kind === "already-delivered") markThreadParticipation();
2190
2396
  const deliveryLog = formatMattermostFinalDeliveryOutcomeLog({
2191
2397
  outcome,
2192
- payload: payloadToDeliver,
2398
+ payload: resolvedPayload,
2193
2399
  to,
2194
2400
  accountId: account.accountId,
2195
2401
  agentId: route.agentId
@@ -2275,33 +2481,44 @@ async function monitorMattermostProvider(opts = {}) {
2275
2481
  replyOptions: {
2276
2482
  ...replyOptions,
2277
2483
  allowProgressCallbacksWhenSourceDeliverySuppressed: draftToolProgressEnabled ? true : void 0,
2484
+ preserveProgressCallbackStartOrder: draftPreviewEnabled ? true : void 0,
2278
2485
  onObservedReplyDelivery: draftToolProgressEnabled ? () => draftStream.clear() : void 0,
2279
- disableBlockStreaming: true,
2486
+ disableBlockStreaming: draftPreviewEnabled ? true : typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
2280
2487
  ...suppressDefaultToolProgressMessages ? { suppressDefaultToolProgressMessages: true } : {},
2281
2488
  onModelSelected,
2282
2489
  onPartialReply: (payloadResult) => {
2283
- if (account.streamingMode !== "progress") updateDraftFromPartial(payloadResult.text);
2490
+ if (account.streamingMode !== "progress") return updateDraftFromPartial(payloadResult.text);
2284
2491
  },
2285
2492
  onAssistantMessageStart: () => {
2286
2493
  lastPartialText = "";
2287
2494
  progressDraft.resetReasoningProgress();
2495
+ if (account.streamingMode === "block") {
2496
+ blockPreviewAssistantMessagePending = true;
2497
+ return;
2498
+ }
2288
2499
  if (account.streamingMode !== "progress") progressDraft.reset();
2289
2500
  },
2290
2501
  onReasoningEnd: () => {
2291
2502
  lastPartialText = "";
2292
2503
  progressDraft.resetReasoningProgress();
2293
- if (account.streamingMode !== "progress") progressDraft.reset();
2504
+ if (account.streamingMode !== "block" && account.streamingMode !== "progress") progressDraft.reset();
2294
2505
  },
2295
2506
  onReasoningStream: async (payloadResult) => {
2296
2507
  if (account.streamingMode === "progress") {
2297
2508
  await progressDraft.pushReasoningProgress(payloadResult.text || "Thinking…", { snapshot: payloadResult.isReasoningSnapshot === true });
2298
2509
  return;
2299
2510
  }
2300
- if (!lastPartialText) draftStream.update("Thinking…");
2511
+ if (!lastPartialText) {
2512
+ const boundarySettled = enterBlockPreviewActivity("reasoning");
2513
+ draftStream.update("Thinking…");
2514
+ previewBoundaryController.noteUpdate();
2515
+ await boundarySettled;
2516
+ }
2301
2517
  },
2302
2518
  onToolStart: async (payloadValue) => {
2303
2519
  if (!draftToolProgressEnabled) return;
2304
- await progressDraft.pushToolProgress(buildChannelProgressDraftLineForEntry(account.config, {
2520
+ const boundarySettled = enterBlockPreviewActivity("tool");
2521
+ const progressSettled = progressDraft.pushToolProgress(buildChannelProgressDraftLineForEntry(account.config, {
2305
2522
  event: "tool",
2306
2523
  itemId: payloadValue.itemId,
2307
2524
  toolCallId: payloadValue.toolCallId,
@@ -2309,10 +2526,13 @@ async function monitorMattermostProvider(opts = {}) {
2309
2526
  phase: payloadValue.phase,
2310
2527
  args: payloadValue.args
2311
2528
  }, payloadValue.detailMode ? { detailMode: payloadValue.detailMode } : void 0), { startImmediately: true });
2529
+ previewBoundaryController.noteUpdate();
2530
+ await Promise.all([boundarySettled, progressSettled]);
2312
2531
  },
2313
2532
  onItemEvent: async (payloadLocal) => {
2314
2533
  if (!draftToolProgressEnabled) return;
2315
- await progressDraft.pushToolProgress(buildChannelProgressDraftLineForEntry(account.config, {
2534
+ const boundarySettled = enterBlockPreviewActivity("tool");
2535
+ const progressSettled = progressDraft.pushToolProgress(buildChannelProgressDraftLineForEntry(account.config, {
2316
2536
  event: "item",
2317
2537
  itemId: payloadLocal.itemId,
2318
2538
  itemKind: payloadLocal.kind,
@@ -2324,6 +2544,8 @@ async function monitorMattermostProvider(opts = {}) {
2324
2544
  progressText: payloadLocal.progressText,
2325
2545
  meta: payloadLocal.meta
2326
2546
  }), { startImmediately: true });
2547
+ previewBoundaryController.noteUpdate();
2548
+ await Promise.all([boundarySettled, progressSettled]);
2327
2549
  }
2328
2550
  }
2329
2551
  })
@@ -2597,6 +2819,42 @@ async function removeMattermostReaction(params) {
2597
2819
  mutation: deleteReaction
2598
2820
  });
2599
2821
  }
2822
+ function parseAuthorizedReactionTarget(rawTarget) {
2823
+ const normalized = rawTarget ? normalizeMattermostMessagingTarget(rawTarget) : void 0;
2824
+ if (!normalized) return null;
2825
+ if (normalized.startsWith("channel:")) {
2826
+ const id = normalized.slice(8).trim();
2827
+ return id ? {
2828
+ kind: "channel",
2829
+ id
2830
+ } : null;
2831
+ }
2832
+ if (normalized.startsWith("user:")) {
2833
+ const id = normalized.slice(5).trim();
2834
+ return id ? {
2835
+ kind: "user",
2836
+ id
2837
+ } : null;
2838
+ }
2839
+ return null;
2840
+ }
2841
+ async function authorizeMattermostReactionResource(params) {
2842
+ const target = parseAuthorizedReactionTarget(params.authorizedTarget);
2843
+ if (!target) throw new Error("Mattermost delegated reactions require a canonical authorized conversation target.");
2844
+ const postChannelId = (await params.client.request(`/posts/${encodeURIComponent(params.postId)}`)).channel_id?.trim();
2845
+ if (!postChannelId) throw new Error("Mattermost reaction post is missing its conversation binding.");
2846
+ if (target.kind === "channel") {
2847
+ if (postChannelId !== target.id) throw new Error("Mattermost reaction post belongs to a different conversation.");
2848
+ return;
2849
+ }
2850
+ const botUserId = await resolveBotUserId(params.client, params.cacheKey);
2851
+ if (!botUserId) throw new Error("Mattermost reactions failed: could not resolve bot user id.");
2852
+ const channel = await fetchMattermostChannel(params.client, postChannelId);
2853
+ const participants = channel.name?.split("__").map((entry) => entry.trim()).filter(Boolean).toSorted() ?? [];
2854
+ const authorizedParticipants = [botUserId, target.id].toSorted();
2855
+ if (channel.type !== "D" || participants.length !== 2 || participants[0] !== authorizedParticipants[0] || participants[1] !== authorizedParticipants[1]) throw new Error("Mattermost reaction post belongs to a different direct conversation.");
2856
+ return botUserId;
2857
+ }
2600
2858
  async function runMattermostReaction(params, options) {
2601
2859
  const resolved = resolveMattermostAccount({
2602
2860
  cfg: params.cfg,
@@ -2614,12 +2872,18 @@ async function runMattermostReaction(params, options) {
2614
2872
  fetchImpl: params.fetchImpl,
2615
2873
  allowPrivateNetwork: isPrivateNetworkOptInEnabled(resolved.config)
2616
2874
  });
2617
- const userId = await resolveBotUserId(client, `${baseUrl}:${botToken}`);
2618
- if (!userId) return {
2619
- ok: false,
2620
- error: "Mattermost reactions failed: could not resolve bot user id."
2621
- };
2875
+ const cacheKey = `${baseUrl}:${botToken}`;
2622
2876
  try {
2877
+ const userId = (params.conversationReadOrigin === "direct-operator" ? void 0 : await authorizeMattermostReactionResource({
2878
+ client,
2879
+ cacheKey,
2880
+ postId: params.postId,
2881
+ authorizedTarget: params.authorizedTarget
2882
+ })) ?? await resolveBotUserId(client, cacheKey);
2883
+ if (!userId) return {
2884
+ ok: false,
2885
+ error: "Mattermost reactions failed: could not resolve bot user id."
2886
+ };
2623
2887
  await options.mutation(client, {
2624
2888
  userId,
2625
2889
  postId: params.postId,