@openclaw/mattermost 2026.7.1 → 2026.7.2-beta.1

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,7 +1,7 @@
1
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";
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-B2NRPlqr.js";
3
3
  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";
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-Cr58Xd12.js";
5
5
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries, normalizeTrimmedStringList, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
6
6
  import { formatInboundFromLabel, formatInboundMediaUnavailableText } from "openclaw/plugin-sdk/channel-inbound";
7
7
  import { buildChannelProgressDraftLineForEntry, createChannelProgressDraftCompositor, createFinalizableDraftLifecycle, defineFinalizableLivePreviewAdapter, deliverWithFinalizableLivePreviewAdapter, resolveChannelStreamingPreviewToolProgress } from "openclaw/plugin-sdk/channel-outbound";
@@ -14,10 +14,12 @@ import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
14
14
  import { z } from "zod";
15
15
  import { resolveInboundLastRouteSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
16
16
  import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
17
+ import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
17
18
  import { randomUUID } from "node:crypto";
19
+ import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
18
20
  import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
21
+ import { chunkMarkdownTextWithMode } from "openclaw/plugin-sdk/reply-chunking";
19
22
  import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
20
- import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
21
23
  import { captureWsEvent, createDebugProxyWebSocketAgent, resolveDebugProxySettings } from "openclaw/plugin-sdk/proxy-capture";
22
24
  import WebSocket from "ws";
23
25
  import { createPersistentDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
@@ -111,11 +113,14 @@ async function listMattermostDirectoryPeers(params) {
111
113
  const clients = buildClients(params);
112
114
  if (!clients.length) return [];
113
115
  const client = clients[0];
116
+ if (!client) return [];
114
117
  try {
115
118
  const me = await fetchMattermostMe(client);
116
119
  const teams = await client.request("/users/me/teams");
117
120
  if (!teams.length) return [];
118
- const teamId = teams[0].id;
121
+ const team = teams[0];
122
+ if (!team) return [];
123
+ const teamId = team.id;
119
124
  const q = normalizeLowercaseStringOrEmpty(params.query);
120
125
  let users;
121
126
  if (q) users = await client.request("/users/search", {
@@ -165,6 +170,20 @@ function normalizeMattermostDraftText(text, maxChars) {
165
170
  if (trimmed.length <= maxChars) return trimmed;
166
171
  return `${sliceUtf16Safe(trimmed, 0, Math.max(0, maxChars - 3)).trimEnd()}...`;
167
172
  }
173
+ function createMattermostDraftPreviewBoundaryController(params) {
174
+ let hasStreamedContent = false;
175
+ return {
176
+ noteUpdate() {
177
+ hasStreamedContent = true;
178
+ },
179
+ async noteBoundary() {
180
+ if (!params.enabled) return;
181
+ if (!hasStreamedContent) return;
182
+ hasStreamedContent = false;
183
+ await params.forceNewMessage();
184
+ }
185
+ };
186
+ }
168
187
  function createMattermostDraftStream(params) {
169
188
  const maxChars = Math.min(params.maxChars ?? MATTERMOST_STREAM_MAX_CHARS, MATTERMOST_STREAM_MAX_CHARS);
170
189
  const throttleMs = Math.max(250, params.throttleMs ?? DEFAULT_THROTTLE_MS);
@@ -172,15 +191,22 @@ function createMattermostDraftStream(params) {
172
191
  stopped: false,
173
192
  final: false
174
193
  };
175
- let streamPostId;
176
- let lastSentText = "";
194
+ let currentGeneration = {
195
+ lastSentText: "",
196
+ latestSourceText: "",
197
+ ready: Promise.resolve()
198
+ };
199
+ const sealedAssistantTexts = [];
177
200
  const sendOrEditStreamMessage = async (text) => {
178
201
  if (streamState.stopped && !streamState.final) return false;
202
+ const target = currentGeneration;
179
203
  const normalized = normalizeMattermostDraftText(params.renderText?.(text) ?? text, maxChars);
180
204
  if (!normalized) return false;
181
- if (normalized === lastSentText) return true;
205
+ await target.ready;
206
+ if (streamState.stopped && !streamState.final) return false;
207
+ if (normalized === target.lastSentText) return true;
182
208
  try {
183
- if (streamPostId) await updateMattermostPost(params.client, streamPostId, { message: normalized });
209
+ if (target.postId) await updateMattermostPost(params.client, target.postId, { message: normalized });
184
210
  else {
185
211
  const postId = (await createMattermostPost(params.client, {
186
212
  channelId: params.channelId,
@@ -192,9 +218,9 @@ function createMattermostDraftStream(params) {
192
218
  params.warn?.("mattermost stream preview stopped (missing post id from create)");
193
219
  return false;
194
220
  }
195
- streamPostId = postId;
221
+ target.postId = postId;
196
222
  }
197
- lastSentText = normalized;
223
+ target.lastSentText = normalized;
198
224
  return true;
199
225
  } catch (err) {
200
226
  streamState.stopped = true;
@@ -202,40 +228,333 @@ function createMattermostDraftStream(params) {
202
228
  return false;
203
229
  }
204
230
  };
205
- const { loop, update, stop, clear, discardPending, seal } = createFinalizableDraftLifecycle({
231
+ const clearMessageId = () => {
232
+ currentGeneration.postId = void 0;
233
+ };
234
+ const isValidMessageId = (value) => typeof value === "string" && value.length > 0;
235
+ const deleteMessage = async (postId) => {
236
+ await deleteMattermostPost(params.client, postId);
237
+ };
238
+ const { loop, update: updateLifecycle, stop: stopLifecycle, stopForClear, clearWithStop, seal: sealLifecycle } = createFinalizableDraftLifecycle({
206
239
  throttleMs,
207
240
  state: streamState,
208
241
  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
- },
242
+ readMessageId: () => currentGeneration.postId,
243
+ clearMessageId,
244
+ isValidMessageId,
245
+ deleteMessage,
217
246
  warn: params.warn,
218
247
  warnPrefix: "mattermost stream preview cleanup failed"
219
248
  });
220
249
  const forceNewMessage = () => {
221
- streamPostId = void 0;
222
- lastSentText = "";
223
- loop.resetPending();
250
+ if (streamState.stopped || streamState.final) return Promise.resolve();
251
+ const pendingText = loop.takePending();
252
+ const inFlightAtBoundary = loop.waitForInFlight();
253
+ const sealed = currentGeneration;
254
+ const boundary = (async () => {
255
+ try {
256
+ await sealed.ready;
257
+ await inFlightAtBoundary;
258
+ if (streamState.stopped && !streamState.final) return;
259
+ const sourceText = pendingText.trim() ? pendingText : sealed.latestSourceText;
260
+ const finalizedText = (params.renderText?.(sourceText) ?? sourceText).trim();
261
+ const chunks = params.chunkText?.(finalizedText) ?? chunkMarkdownTextWithMode(finalizedText, maxChars, "length");
262
+ const firstChunk = chunks[0];
263
+ if (!firstChunk) return;
264
+ if (sealed.postId) {
265
+ if (firstChunk !== sealed.lastSentText) await updateMattermostPost(params.client, sealed.postId, { message: firstChunk });
266
+ } else await createMattermostPost(params.client, {
267
+ channelId: params.channelId,
268
+ message: firstChunk,
269
+ rootId: params.rootId
270
+ });
271
+ for (const chunk of chunks.slice(1)) await createMattermostPost(params.client, {
272
+ channelId: params.channelId,
273
+ message: chunk,
274
+ rootId: params.rootId
275
+ });
276
+ const assistantText = sealed.latestAssistantText?.trim();
277
+ if (assistantText) sealedAssistantTexts.push(assistantText);
278
+ } catch (err) {
279
+ params.warn?.(`mattermost stream preview boundary flush failed: ${err instanceof Error ? err.message : String(err)}`);
280
+ }
281
+ })();
282
+ currentGeneration = {
283
+ lastSentText: "",
284
+ latestSourceText: "",
285
+ ready: boundary
286
+ };
224
287
  loop.resetThrottleWindow();
288
+ return boundary;
289
+ };
290
+ const flush = async () => {
291
+ await loop.flush();
292
+ await currentGeneration.ready;
293
+ };
294
+ const discardPending = async () => {
295
+ await stopForClear();
296
+ await currentGeneration.ready;
297
+ };
298
+ const clear = async () => {
299
+ await clearWithStop(discardPending);
300
+ };
301
+ const seal = async () => {
302
+ await sealLifecycle();
303
+ await currentGeneration.ready;
304
+ };
305
+ const stop = async () => {
306
+ await stopLifecycle();
307
+ await currentGeneration.ready;
308
+ };
309
+ const update = (text) => {
310
+ currentGeneration.latestSourceText = text;
311
+ currentGeneration.latestAssistantText = void 0;
312
+ updateLifecycle(text);
313
+ };
314
+ const updateAssistantText = (text) => {
315
+ currentGeneration.latestSourceText = text;
316
+ currentGeneration.latestAssistantText = text;
317
+ updateLifecycle(text);
318
+ };
319
+ const settleBoundaries = async () => {
320
+ await currentGeneration.ready;
321
+ };
322
+ const resolveFinalText = (text) => {
323
+ if (sealedAssistantTexts.length === 0) return {
324
+ kind: "full",
325
+ text
326
+ };
327
+ let remainingText = text.trim();
328
+ for (const sealedText of sealedAssistantTexts) {
329
+ const completed = sealedText.trim();
330
+ if (!completed || !remainingText.startsWith(completed)) return {
331
+ kind: "full",
332
+ text
333
+ };
334
+ const suffix = remainingText.slice(completed.length);
335
+ if (suffix && !/^\r?\n/.test(suffix)) return {
336
+ kind: "full",
337
+ text
338
+ };
339
+ remainingText = suffix.replace(/^(?:\r?\n)+/, "");
340
+ }
341
+ const currentText = currentGeneration.latestAssistantText?.trim() ?? "";
342
+ const remaining = remainingText.trim();
343
+ if (currentText && !remaining.startsWith(currentText)) return {
344
+ kind: "full",
345
+ text
346
+ };
347
+ return remaining ? {
348
+ kind: "remaining",
349
+ text: remaining
350
+ } : { kind: "already-delivered" };
225
351
  };
226
352
  params.log?.(`mattermost stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
227
353
  return {
228
354
  update,
229
- flush: loop.flush,
230
- postId: () => streamPostId,
355
+ updateAssistantText,
356
+ flush,
357
+ postId: () => currentGeneration.postId,
231
358
  clear,
232
359
  discardPending,
233
360
  seal,
234
361
  stop,
235
- forceNewMessage
362
+ forceNewMessage,
363
+ settleBoundaries,
364
+ resolveFinalText
236
365
  };
237
366
  }
238
367
  //#endregion
368
+ //#region extensions/mattermost/src/mattermost/monitor-helpers.ts
369
+ const formatInboundFromLabel$1 = formatInboundFromLabel;
370
+ function resolveThreadSessionKeys$1(params) {
371
+ return resolveThreadSessionKeys({
372
+ ...params,
373
+ normalizeThreadId: (threadId) => threadId
374
+ });
375
+ }
376
+ /**
377
+ * Strip bot mention from message text while preserving newlines and
378
+ * block-level Markdown formatting (headings, lists, blockquotes).
379
+ */
380
+ function normalizeMention(text, mention) {
381
+ if (!mention) return text.trim();
382
+ const escaped = mention.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
383
+ const hasMentionRe = new RegExp(`@${escaped}\\b`, "i");
384
+ const leadingMentionRe = new RegExp(`^([\\t ]*)@${escaped}\\b[\\t ]*`, "i");
385
+ const trailingMentionRe = new RegExp(`[\\t ]*@${escaped}\\b[\\t ]*$`, "i");
386
+ const normalizedLines = text.split("\n").map((line) => {
387
+ const hadMention = hasMentionRe.test(line);
388
+ const normalizedLine = line.replace(leadingMentionRe, "$1").replace(trailingMentionRe, "").replace(new RegExp(`@${escaped}\\b`, "gi"), "").replace(/(\S)[ \t]{2,}/g, "$1 ");
389
+ return {
390
+ text: normalizedLine,
391
+ mentionOnlyBlank: hadMention && normalizedLine.trim() === ""
392
+ };
393
+ });
394
+ while (normalizedLines[0]?.mentionOnlyBlank) normalizedLines.shift();
395
+ while (normalizedLines.at(-1)?.text.trim() === "") normalizedLines.pop();
396
+ return normalizedLines.map((line) => line.text).join("\n");
397
+ }
398
+ function shouldDropEmptyMattermostBody(params) {
399
+ if (/[^\p{White_Space}\p{Cc}\p{Cf}\p{M}]/u.test(params.bodyText)) return false;
400
+ const botUsername = normalizeLowercaseStringOrEmpty(params.botUsername ?? "");
401
+ const bareMention = params.rawText.match(/^[ \t]*(@\S+)[ \t]*$/u)?.[1];
402
+ return !botUsername || normalizeLowercaseStringOrEmpty(bareMention ?? "") !== `@${botUsername}`;
403
+ }
404
+ //#endregion
405
+ //#region extensions/mattermost/src/mattermost/no-visible-reply-diagnostic.ts
406
+ /**
407
+ * Detects the #80501 symptom: `deliverMattermostReplyPayload` accepted a
408
+ * substantive (non-reasoning) payload, called the underlying
409
+ * `deliverTextOrMediaReply`, and the outcome was `"empty"` — meaning the
410
+ * payload had no text and no media to send, so no Mattermost API call
411
+ * happened. The agent's run completes successfully, but no visible
412
+ * channel/thread reply ever surfaces to the user.
413
+ *
414
+ * Returns a structured violation when the outcome is `"empty"` for a payload
415
+ * that nominally carried user-facing content (text or media bytes that ended
416
+ * up dropped by `resolveSendableOutboundReplyParts`/`sendMediaWithLeadingCaption`).
417
+ * Returns `null` for `"reasoning_skipped"` (intentional suppression),
418
+ * `"text"`, or `"media"` (successful visible sends).
419
+ */
420
+ function evaluateMattermostNoVisibleReply(params) {
421
+ if (params.outcome !== "empty") return null;
422
+ const finalText = typeof params.payload.text === "string" ? params.payload.text.trim() : "";
423
+ const mediaUrlCount = countOutboundMedia(params.payload);
424
+ if (finalText.length === 0 && mediaUrlCount === 0) return null;
425
+ return {
426
+ reason: "no-visible-reply-after-final-delivery",
427
+ outcome: params.outcome,
428
+ finalTextLength: finalText.length,
429
+ mediaUrlCount
430
+ };
431
+ }
432
+ function formatMattermostNoVisibleReplyLog(params) {
433
+ 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}`;
434
+ }
435
+ //#endregion
436
+ //#region extensions/mattermost/src/mattermost/monitor-context.ts
437
+ function shouldUpdateMattermostDraftToolProgress(account) {
438
+ return account.streamingMode !== "off" && resolveChannelStreamingPreviewToolProgress(account.config);
439
+ }
440
+ function shouldSuppressMattermostDefaultToolProgressMessages(account) {
441
+ return account.streamingMode !== "off";
442
+ }
443
+ function buildMattermostModelPickerSelectMessageSid(params) {
444
+ const provider = normalizeLowercaseStringOrEmpty(params.provider);
445
+ const model = normalizeLowercaseStringOrEmpty(params.model);
446
+ return `interaction:${params.postId}:select:${provider}/${model}`;
447
+ }
448
+ function resolveMattermostReplyRootId(params) {
449
+ const threadRootId = normalizeOptionalString(params.threadRootId);
450
+ if (params.kind === "direct" && !threadRootId) return;
451
+ if (threadRootId) return threadRootId;
452
+ return normalizeOptionalString(params.replyToId);
453
+ }
454
+ function canFinalizeMattermostPreviewInPlace(params) {
455
+ return resolveMattermostReplyRootId({
456
+ kind: params.kind,
457
+ threadRootId: params.threadRootId,
458
+ replyToId: params.replyToId
459
+ }) === params.previewRootId?.trim();
460
+ }
461
+ function formatMattermostFinalDeliveryOutcomeLog(params) {
462
+ const violation = evaluateMattermostNoVisibleReply({
463
+ outcome: params.outcome,
464
+ payload: params.payload
465
+ });
466
+ if (violation) return formatMattermostNoVisibleReplyLog({
467
+ violation,
468
+ to: params.to,
469
+ accountId: params.accountId,
470
+ agentId: params.agentId
471
+ });
472
+ if (params.outcome === "text" || params.outcome === "media") return `delivered reply to ${params.to}`;
473
+ }
474
+ function resolveMattermostEffectiveReplyToId(params) {
475
+ if (params.kind === "direct" && params.replyToMode === "off") return;
476
+ const threadRootId = normalizeOptionalString(params.threadRootId);
477
+ if (threadRootId) return threadRootId;
478
+ const postId = normalizeOptionalString(params.postId);
479
+ if (!postId) return;
480
+ return params.replyToMode === "all" || params.replyToMode === "first" || params.replyToMode === "batched" ? postId : void 0;
481
+ }
482
+ function resolveMattermostThreadSessionContext(params) {
483
+ const effectiveReplyToId = resolveMattermostEffectiveReplyToId({
484
+ kind: params.kind,
485
+ postId: params.postId,
486
+ replyToMode: params.replyToMode,
487
+ threadRootId: params.threadRootId
488
+ });
489
+ const threadKeys = resolveThreadSessionKeys$1({
490
+ baseSessionKey: params.baseSessionKey,
491
+ threadId: effectiveReplyToId,
492
+ parentSessionKey: effectiveReplyToId && params.kind !== "direct" ? params.baseSessionKey : void 0
493
+ });
494
+ return {
495
+ effectiveReplyToId,
496
+ sessionKey: threadKeys.sessionKey,
497
+ parentSessionKey: threadKeys.parentSessionKey
498
+ };
499
+ }
500
+ function resolveMattermostPendingHistoryKey(params) {
501
+ return params.kind === "direct" ? null : params.sessionKey;
502
+ }
503
+ function resolveMattermostReactionChannelId(payload) {
504
+ return normalizeOptionalString(payload.broadcast?.channel_id) ?? normalizeOptionalString(payload.data?.channel_id);
505
+ }
506
+ //#endregion
507
+ //#region extensions/mattermost/src/mattermost/monitor-draft-delivery.ts
508
+ async function deliverMattermostReplyWithDraftPreview(params) {
509
+ if (isReasoningReplyPayload(params.payload)) return;
510
+ await deliverWithFinalizableLivePreviewAdapter({
511
+ kind: params.info.kind,
512
+ payload: params.payload,
513
+ adapter: defineFinalizableLivePreviewAdapter({
514
+ draft: {
515
+ flush: params.draftStream.flush,
516
+ clear: params.draftStream.clear,
517
+ discardPending: params.draftStream.discardPending,
518
+ seal: params.draftStream.seal,
519
+ id: params.draftStream.postId
520
+ },
521
+ buildFinalEdit: (payload) => {
522
+ const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
523
+ const ttsSupplement = getReplyPayloadTtsSupplement(payload);
524
+ const previewFinalText = params.resolvePreviewFinalText(payload.text ?? ttsSupplement?.spokenText);
525
+ if (hasMedia && !ttsSupplement || typeof previewFinalText !== "string" || payload.isError || !canFinalizeMattermostPreviewInPlace({
526
+ kind: params.kind,
527
+ previewRootId: params.effectiveReplyToId,
528
+ threadRootId: params.effectiveReplyToId,
529
+ replyToId: payload.replyToId
530
+ })) return;
531
+ return { message: previewFinalText };
532
+ },
533
+ editFinal: async (previewPostId, edit) => {
534
+ await updateMattermostPost(params.client, previewPostId, edit);
535
+ },
536
+ onPreviewFinalized: () => {
537
+ params.previewState.finalizedViaPreviewPost = true;
538
+ params.recordThreadParticipation?.();
539
+ },
540
+ buildSupplementalPayload: (payload) => getReplyPayloadTtsSupplement(payload) ? buildTtsSupplementMediaPayload(payload) : void 0,
541
+ deliverSupplemental: async (payload) => {
542
+ await params.deliverPayload(payload);
543
+ },
544
+ logPreviewEditFailure: (err) => {
545
+ params.logVerboseMessage(`mattermost preview final edit failed; falling back to normal send (${String(err)})`);
546
+ }
547
+ }),
548
+ deliverNormally: async (payload) => {
549
+ const supplement = getReplyPayloadTtsSupplement(payload);
550
+ await params.deliverPayload(supplement && !payload.text?.trim() && supplement.visibleTextAlreadyDelivered !== true ? {
551
+ ...payload,
552
+ text: supplement.spokenText
553
+ } : payload);
554
+ }
555
+ });
556
+ }
557
+ //#endregion
239
558
  //#region extensions/mattermost/src/mattermost/monitor-gating.ts
240
559
  function mapMattermostChannelTypeToChatType(channelType) {
241
560
  const normalized = channelType?.trim().toUpperCase();
@@ -279,43 +598,6 @@ function evaluateMattermostMentionGate(params) {
279
598
  };
280
599
  }
281
600
  //#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
601
  //#region extensions/mattermost/src/mattermost/monitor-onchar.ts
320
602
  const DEFAULT_ONCHAR_PREFIXES = [">", "!"];
321
603
  function resolveOncharPrefixes(prefixes) {
@@ -337,6 +619,37 @@ function stripOncharPrefix(text, prefixes) {
337
619
  };
338
620
  }
339
621
  //#endregion
622
+ //#region extensions/mattermost/src/mattermost/monitor-replay.ts
623
+ const recentInboundMessages = createClaimableDedupe({
624
+ ttlMs: 5 * 6e4,
625
+ memoryMaxSize: 2e3
626
+ });
627
+ function buildMattermostInboundReplayKeys(params) {
628
+ return uniqueStrings(params.messageIds.map((id) => `${params.accountId}:${id.trim()}`)).filter((key) => !key.endsWith(":"));
629
+ }
630
+ async function processMattermostReplayGuardedPost(params) {
631
+ const replayGuard = params.replayGuard ?? recentInboundMessages;
632
+ const replayKeys = buildMattermostInboundReplayKeys({
633
+ accountId: params.accountId,
634
+ messageIds: params.messageIds
635
+ });
636
+ if (replayKeys.length === 0) {
637
+ await params.handlePost();
638
+ return "processed";
639
+ }
640
+ const claimedKeys = [];
641
+ for (const replayKey of replayKeys) if ((await replayGuard.claim(replayKey)).kind === "claimed") claimedKeys.push(replayKey);
642
+ if (claimedKeys.length === 0) return "duplicate";
643
+ try {
644
+ await params.handlePost();
645
+ await Promise.all(claimedKeys.map((replayKey) => replayGuard.commit(replayKey)));
646
+ return "processed";
647
+ } catch (error) {
648
+ await Promise.all(claimedKeys.map((replayKey) => replayGuard.commit(replayKey)));
649
+ throw error;
650
+ }
651
+ }
652
+ //#endregion
340
653
  //#region extensions/mattermost/src/mattermost/monitor-resources.ts
341
654
  function formatMattermostInboundMediaText(params) {
342
655
  const unavailableCount = Math.max(0, params.expectedCount - params.mediaCount);
@@ -349,6 +662,9 @@ function formatMattermostInboundMediaText(params) {
349
662
  }
350
663
  const CHANNEL_CACHE_TTL_MS = 5 * 6e4;
351
664
  const USER_CACHE_TTL_MS = 10 * 6e4;
665
+ const MONITOR_RESOURCE_CACHE_MAX_ENTRIES = 1e3;
666
+ const MATTERMOST_MEDIA_RESPONSE_HEADER_TIMEOUT_MS = 12e4;
667
+ const MATTERMOST_MEDIA_READ_IDLE_TIMEOUT_MS = 3e4;
352
668
  function createMattermostMonitorResources(params) {
353
669
  const { accountId, callbackUrl, client, logger, mediaMaxBytes, saveRemoteMedia, mediaKindFromMime } = params;
354
670
  const channelCache = /* @__PURE__ */ new Map();
@@ -361,10 +677,14 @@ function createMattermostMonitorResources(params) {
361
677
  };
362
678
  const setCachedValue = (cache, key, value, ttlMs, rawNowMs) => {
363
679
  const expiresAt = resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: rawNowMs });
364
- if (expiresAt !== void 0) cache.set(key, {
365
- value,
366
- expiresAt
367
- });
680
+ if (expiresAt !== void 0) {
681
+ cache.delete(key);
682
+ cache.set(key, {
683
+ value,
684
+ expiresAt
685
+ });
686
+ pruneMapToMaxSize(cache, MONITOR_RESOURCE_CACHE_MAX_ENTRIES);
687
+ }
368
688
  };
369
689
  const resolveMattermostMedia = async (fileIds) => {
370
690
  const ids = normalizeStringEntries(fileIds ?? []);
@@ -376,7 +696,9 @@ function createMattermostMonitorResources(params) {
376
696
  requestInit: { headers: { Authorization: `Bearer ${client.token}` } },
377
697
  filePathHint: fileId,
378
698
  maxBytes: mediaMaxBytes,
379
- ssrfPolicy: { allowedHostnames: [new URL(client.baseUrl).hostname] }
699
+ ssrfPolicy: { allowedHostnames: [new URL(client.baseUrl).hostname] },
700
+ responseHeaderTimeoutMs: MATTERMOST_MEDIA_RESPONSE_HEADER_TIMEOUT_MS,
701
+ readIdleTimeoutMs: MATTERMOST_MEDIA_READ_IDLE_TIMEOUT_MS
380
702
  });
381
703
  const contentType = saved.contentType ?? void 0;
382
704
  out.push({
@@ -849,37 +1171,6 @@ function reasonToString(reason) {
849
1171
  return reason.length > 0 ? reason.toString("utf8") : "";
850
1172
  }
851
1173
  //#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
1174
  //#region extensions/mattermost/src/mattermost/reconnect.ts
884
1175
  /**
885
1176
  * Reconnection loop with exponential backoff.
@@ -992,58 +1283,9 @@ async function hasMattermostThreadParticipationWithPersistence(params) {
992
1283
  }
993
1284
  //#endregion
994
1285
  //#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
1286
  function normalizeInteractionSourceIps(values) {
1004
1287
  return normalizeTrimmedStringList(values);
1005
1288
  }
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
1289
  function resolveRuntime(opts) {
1048
1290
  return opts.runtime ?? {
1049
1291
  log: console.log,
@@ -1061,126 +1303,32 @@ function channelChatType(kind) {
1061
1303
  if (kind === "group") return "group";
1062
1304
  return "channel";
1063
1305
  }
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
1306
  function createDisabledMattermostDraftStream() {
1078
1307
  const noopAsync = async () => {};
1079
1308
  return {
1080
1309
  update: () => {},
1310
+ updateAssistantText: () => {},
1081
1311
  flush: noopAsync,
1082
1312
  postId: () => void 0,
1083
1313
  clear: noopAsync,
1084
1314
  discardPending: noopAsync,
1085
1315
  seal: noopAsync,
1086
1316
  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
1317
+ forceNewMessage: noopAsync,
1318
+ settleBoundaries: noopAsync,
1319
+ resolveFinalText: (text) => ({
1320
+ kind: "full",
1321
+ text
1322
+ })
1176
1323
  };
1177
1324
  }
1178
- function resolveMattermostReactionChannelId(payload) {
1179
- return normalizeOptionalString(payload.broadcast?.channel_id) ?? normalizeOptionalString(payload.data?.channel_id);
1180
- }
1181
1325
  function buildMattermostAttachmentPlaceholder(mediaList) {
1182
1326
  if (mediaList.length === 0) return "";
1183
- if (mediaList.length === 1) return `<media:${mediaList[0].kind === "unknown" ? "document" : mediaList[0].kind}>`;
1327
+ if (mediaList.length === 1) {
1328
+ const media = mediaList[0];
1329
+ if (!media) return "";
1330
+ return `<media:${media.kind === "unknown" ? "document" : media.kind}>`;
1331
+ }
1184
1332
  const allImages = mediaList.every((media) => media.kind === "image");
1185
1333
  const label = allImages ? "image" : "file";
1186
1334
  const suffix = mediaList.length === 1 ? label : `${label}s`;
@@ -1442,17 +1590,22 @@ async function monitorMattermostProvider(opts = {}) {
1442
1590
  },
1443
1591
  onReplyStart: typingCallbacks?.onReplyStart
1444
1592
  });
1445
- await core.channel.reply.dispatchReplyFromConfig({
1446
- ctx: ctxPayload,
1447
- cfg,
1593
+ await core.channel.reply.withReplyDispatcher({
1448
1594
  dispatcher,
1449
- replyOptions: {
1450
- ...replyOptions,
1451
- disableBlockStreaming: typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
1452
- onModelSelected
1453
- }
1595
+ onSettled: () => {
1596
+ markDispatchIdle();
1597
+ },
1598
+ run: () => core.channel.reply.dispatchReplyFromConfig({
1599
+ ctx: ctxPayload,
1600
+ cfg,
1601
+ dispatcher,
1602
+ replyOptions: {
1603
+ ...replyOptions,
1604
+ disableBlockStreaming: typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
1605
+ onModelSelected
1606
+ }
1607
+ })
1454
1608
  });
1455
- markDispatchIdle();
1456
1609
  },
1457
1610
  log: (msg) => runtime.log?.(msg)
1458
1611
  }),
@@ -1912,7 +2065,10 @@ async function monitorMattermostProvider(opts = {}) {
1912
2065
  replyToMode,
1913
2066
  threadRootId
1914
2067
  });
1915
- const historyKey = kind === "direct" ? null : sessionKey;
2068
+ const historyKey = resolveMattermostPendingHistoryKey({
2069
+ kind,
2070
+ sessionKey
2071
+ });
1916
2072
  const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg, route.agentId);
1917
2073
  const wasMentioned = kind !== "direct" && ((botUsername ? normalizeLowercaseStringOrEmpty(rawText).includes(`@${normalizeLowercaseStringOrEmpty(botUsername)}`) : false) || core.channel.mentions.matchesMentionPatterns(rawText, mentionRegexes));
1918
2074
  const pendingBody = rawText || (post.file_ids?.length ? `[Mattermost ${post.file_ids.length === 1 ? "file" : "files"}]` : "");
@@ -2074,7 +2230,7 @@ async function monitorMattermostProvider(opts = {}) {
2074
2230
  normalizeEntry: normalizeMattermostAllowEntry
2075
2231
  }) : null;
2076
2232
  const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId: route.agentId });
2077
- const previewLine = bodyText.slice(0, 200).replace(/\n/g, "\\n");
2233
+ const previewLine = truncateUtf16Safe(bodyText, 200).replace(/\n/g, "\\n");
2078
2234
  logVerboseMessage(`mattermost inbound: from=${ctxPayload.From} len=${bodyText.length} preview="${previewLine}"`);
2079
2235
  const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "mattermost", account.accountId, { fallbackLimit: account.textChunkLimit ?? 4e3 });
2080
2236
  const tableMode = core.channel.text.resolveMarkdownTableMode({
@@ -2082,7 +2238,8 @@ async function monitorMattermostProvider(opts = {}) {
2082
2238
  channel: "mattermost",
2083
2239
  accountId: account.accountId
2084
2240
  });
2085
- const { onModelSelected, typingCallbacks, ...replyPipeline } = createChannelMessageReplyPipeline({
2241
+ const chunkMode = core.channel.text.resolveChunkMode(cfg, "mattermost", account.accountId);
2242
+ const { onModelSelected, typingCallbacks, resolveResponsePrefix, ...replyPipeline } = createChannelMessageReplyPipeline({
2086
2243
  cfg,
2087
2244
  agentId: route.agentId,
2088
2245
  channel: "mattermost",
@@ -2107,10 +2264,22 @@ async function monitorMattermostProvider(opts = {}) {
2107
2264
  channelId,
2108
2265
  rootId: effectiveReplyToId,
2109
2266
  throttleMs: 1200,
2267
+ chunkText: (value) => core.channel.text.chunkMarkdownTextWithMode(core.channel.text.convertMarkdownTables(value, tableMode), textLimit, chunkMode),
2110
2268
  log: logVerboseMessage,
2111
2269
  warn: logVerboseMessage
2112
2270
  }) : createDisabledMattermostDraftStream();
2271
+ const previewBoundaryController = createMattermostDraftPreviewBoundaryController({
2272
+ enabled: draftPreviewEnabled && account.streamingMode === "block",
2273
+ forceNewMessage: async () => {
2274
+ await draftStream.forceNewMessage();
2275
+ }
2276
+ });
2113
2277
  let lastPartialText = "";
2278
+ let firstAssistantPreviewPrefix;
2279
+ let firstAssistantPreviewPrefixPending = true;
2280
+ let currentAssistantPreviewUsesPrefix = false;
2281
+ let blockPreviewActivity = "none";
2282
+ let blockPreviewAssistantMessagePending = false;
2114
2283
  const progressDraft = createChannelProgressDraftCompositor({
2115
2284
  entry: account.config,
2116
2285
  mode: account.streamingMode,
@@ -2121,11 +2290,28 @@ async function monitorMattermostProvider(opts = {}) {
2121
2290
  if (options?.flush) await draftStream.flush();
2122
2291
  }
2123
2292
  });
2293
+ const enterBlockPreviewActivity = (activity) => {
2294
+ if (account.streamingMode !== "block") return;
2295
+ const continuesCurrentActivity = activity === "tool" && blockPreviewActivity === "tool" || activity === "text" && blockPreviewActivity === "text" && !blockPreviewAssistantMessagePending || activity === "reasoning" && blockPreviewActivity === "reasoning";
2296
+ const startsNewGeneration = !continuesCurrentActivity && !(blockPreviewActivity === "reasoning");
2297
+ if (startsNewGeneration) currentAssistantPreviewUsesPrefix = false;
2298
+ const boundarySettled = startsNewGeneration ? previewBoundaryController.noteBoundary() : void 0;
2299
+ if (!continuesCurrentActivity) progressDraft.reset();
2300
+ blockPreviewActivity = activity;
2301
+ blockPreviewAssistantMessagePending = false;
2302
+ if (activity === "tool") lastPartialText = "";
2303
+ return boundarySettled;
2304
+ };
2124
2305
  const previewState = { finalizedViaPreviewPost: false };
2125
- const resolvePreviewFinalText = (text) => {
2306
+ const resolveFinalDeliveryText = (text) => {
2126
2307
  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);
2308
+ const resolution = draftStream.resolveFinalText(text);
2309
+ return resolution.kind === "already-delivered" ? "" : resolution.text;
2310
+ };
2311
+ const resolvePreviewFinalText = (text) => {
2312
+ const deliveryText = resolveFinalDeliveryText(text);
2313
+ if (typeof deliveryText !== "string") return;
2314
+ const formatted = core.channel.text.convertMarkdownTables(deliveryText, tableMode);
2129
2315
  const chunks = core.channel.text.chunkMarkdownTextWithMode(formatted, textLimit, chunkMode);
2130
2316
  if (!chunks.length && formatted) chunks.push(formatted);
2131
2317
  if (chunks.length != 1) return;
@@ -2139,8 +2325,17 @@ async function monitorMattermostProvider(opts = {}) {
2139
2325
  if (!cleaned) return;
2140
2326
  if (cleaned === lastPartialText) return;
2141
2327
  if (lastPartialText && lastPartialText.startsWith(cleaned) && cleaned.length < lastPartialText.length) return;
2328
+ const boundarySettled = enterBlockPreviewActivity("text");
2142
2329
  lastPartialText = cleaned;
2143
- draftStream.update(cleaned);
2330
+ if (firstAssistantPreviewPrefixPending) {
2331
+ firstAssistantPreviewPrefix = resolveResponsePrefix?.();
2332
+ firstAssistantPreviewPrefixPending = false;
2333
+ currentAssistantPreviewUsesPrefix = Boolean(firstAssistantPreviewPrefix);
2334
+ }
2335
+ const previewText = currentAssistantPreviewUsesPrefix && firstAssistantPreviewPrefix ? cleaned.startsWith(firstAssistantPreviewPrefix) ? cleaned : `${firstAssistantPreviewPrefix} ${cleaned}` : cleaned;
2336
+ draftStream.updateAssistantText(previewText);
2337
+ previewBoundaryController.noteUpdate();
2338
+ return boundarySettled;
2144
2339
  };
2145
2340
  const deliveryBarrier = createMattermostReplyDeliveryBarrier({
2146
2341
  isDirect: kind === "direct",
@@ -2153,7 +2348,11 @@ async function monitorMattermostProvider(opts = {}) {
2153
2348
  humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
2154
2349
  typingCallbacks,
2155
2350
  deliver: async (payloadEntry, info) => {
2156
- if (info.kind === "final") progressDraft.markFinalReplyStarted();
2351
+ if (info.kind === "final") {
2352
+ await enterBlockPreviewActivity("text");
2353
+ await draftStream.settleBoundaries();
2354
+ progressDraft.markFinalReplyStarted();
2355
+ }
2157
2356
  const markThreadParticipation = () => {
2158
2357
  if (kind !== "direct" && effectiveReplyToId) recordMattermostThreadParticipation(account.accountId, channelId, effectiveReplyToId, { agentId: route.agentId });
2159
2358
  };
@@ -2169,10 +2368,15 @@ async function monitorMattermostProvider(opts = {}) {
2169
2368
  logVerboseMessage,
2170
2369
  recordThreadParticipation: markThreadParticipation,
2171
2370
  deliverPayload: async (payloadToDeliver) => {
2371
+ const finalTextResolution = info.kind === "final" && !payloadToDeliver.isError && typeof payloadToDeliver.text === "string" ? draftStream.resolveFinalText(payloadToDeliver.text) : void 0;
2372
+ const resolvedPayload = finalTextResolution ? {
2373
+ ...payloadToDeliver,
2374
+ text: finalTextResolution.kind === "already-delivered" ? "" : finalTextResolution.text
2375
+ } : payloadToDeliver;
2172
2376
  const outcome = await deliverMattermostReplyPayload({
2173
2377
  core,
2174
2378
  cfg,
2175
- payload: payloadToDeliver,
2379
+ payload: resolvedPayload,
2176
2380
  to,
2177
2381
  accountId: account.accountId,
2178
2382
  agentId: route.agentId,
@@ -2187,9 +2391,10 @@ async function monitorMattermostProvider(opts = {}) {
2187
2391
  onDmChannelResolution: deliveryBarrier.trackDmChannelResolution
2188
2392
  });
2189
2393
  if (outcome === "text" || outcome === "media") markThreadParticipation();
2394
+ else if (outcome === "empty" && finalTextResolution?.kind === "already-delivered") markThreadParticipation();
2190
2395
  const deliveryLog = formatMattermostFinalDeliveryOutcomeLog({
2191
2396
  outcome,
2192
- payload: payloadToDeliver,
2397
+ payload: resolvedPayload,
2193
2398
  to,
2194
2399
  accountId: account.accountId,
2195
2400
  agentId: route.agentId
@@ -2275,33 +2480,44 @@ async function monitorMattermostProvider(opts = {}) {
2275
2480
  replyOptions: {
2276
2481
  ...replyOptions,
2277
2482
  allowProgressCallbacksWhenSourceDeliverySuppressed: draftToolProgressEnabled ? true : void 0,
2483
+ preserveProgressCallbackStartOrder: draftPreviewEnabled ? true : void 0,
2278
2484
  onObservedReplyDelivery: draftToolProgressEnabled ? () => draftStream.clear() : void 0,
2279
- disableBlockStreaming: true,
2485
+ disableBlockStreaming: draftPreviewEnabled ? true : typeof account.blockStreaming === "boolean" ? !account.blockStreaming : void 0,
2280
2486
  ...suppressDefaultToolProgressMessages ? { suppressDefaultToolProgressMessages: true } : {},
2281
2487
  onModelSelected,
2282
2488
  onPartialReply: (payloadResult) => {
2283
- if (account.streamingMode !== "progress") updateDraftFromPartial(payloadResult.text);
2489
+ if (account.streamingMode !== "progress") return updateDraftFromPartial(payloadResult.text);
2284
2490
  },
2285
2491
  onAssistantMessageStart: () => {
2286
2492
  lastPartialText = "";
2287
2493
  progressDraft.resetReasoningProgress();
2494
+ if (account.streamingMode === "block") {
2495
+ blockPreviewAssistantMessagePending = true;
2496
+ return;
2497
+ }
2288
2498
  if (account.streamingMode !== "progress") progressDraft.reset();
2289
2499
  },
2290
2500
  onReasoningEnd: () => {
2291
2501
  lastPartialText = "";
2292
2502
  progressDraft.resetReasoningProgress();
2293
- if (account.streamingMode !== "progress") progressDraft.reset();
2503
+ if (account.streamingMode !== "block" && account.streamingMode !== "progress") progressDraft.reset();
2294
2504
  },
2295
2505
  onReasoningStream: async (payloadResult) => {
2296
2506
  if (account.streamingMode === "progress") {
2297
2507
  await progressDraft.pushReasoningProgress(payloadResult.text || "Thinking…", { snapshot: payloadResult.isReasoningSnapshot === true });
2298
2508
  return;
2299
2509
  }
2300
- if (!lastPartialText) draftStream.update("Thinking…");
2510
+ if (!lastPartialText) {
2511
+ const boundarySettled = enterBlockPreviewActivity("reasoning");
2512
+ draftStream.update("Thinking…");
2513
+ previewBoundaryController.noteUpdate();
2514
+ await boundarySettled;
2515
+ }
2301
2516
  },
2302
2517
  onToolStart: async (payloadValue) => {
2303
2518
  if (!draftToolProgressEnabled) return;
2304
- await progressDraft.pushToolProgress(buildChannelProgressDraftLineForEntry(account.config, {
2519
+ const boundarySettled = enterBlockPreviewActivity("tool");
2520
+ const progressSettled = progressDraft.pushToolProgress(buildChannelProgressDraftLineForEntry(account.config, {
2305
2521
  event: "tool",
2306
2522
  itemId: payloadValue.itemId,
2307
2523
  toolCallId: payloadValue.toolCallId,
@@ -2309,10 +2525,13 @@ async function monitorMattermostProvider(opts = {}) {
2309
2525
  phase: payloadValue.phase,
2310
2526
  args: payloadValue.args
2311
2527
  }, payloadValue.detailMode ? { detailMode: payloadValue.detailMode } : void 0), { startImmediately: true });
2528
+ previewBoundaryController.noteUpdate();
2529
+ await Promise.all([boundarySettled, progressSettled]);
2312
2530
  },
2313
2531
  onItemEvent: async (payloadLocal) => {
2314
2532
  if (!draftToolProgressEnabled) return;
2315
- await progressDraft.pushToolProgress(buildChannelProgressDraftLineForEntry(account.config, {
2533
+ const boundarySettled = enterBlockPreviewActivity("tool");
2534
+ const progressSettled = progressDraft.pushToolProgress(buildChannelProgressDraftLineForEntry(account.config, {
2316
2535
  event: "item",
2317
2536
  itemId: payloadLocal.itemId,
2318
2537
  itemKind: payloadLocal.kind,
@@ -2324,6 +2543,8 @@ async function monitorMattermostProvider(opts = {}) {
2324
2543
  progressText: payloadLocal.progressText,
2325
2544
  meta: payloadLocal.meta
2326
2545
  }), { startImmediately: true });
2546
+ previewBoundaryController.noteUpdate();
2547
+ await Promise.all([boundarySettled, progressSettled]);
2327
2548
  }
2328
2549
  }
2329
2550
  })