@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.
- package/dist/accounts-C4Azn6rA.js +83 -0
- package/dist/api.js +3 -2
- package/dist/channel-plugin-api.js +3 -3
- package/dist/{channel-plugin-runtime-DhS8rwfs.js → channel-plugin-runtime-BXmjNn6B.js} +131 -167
- package/dist/channel-plugin-runtime.js +1 -1
- package/dist/{channel.runtime-D0SJSEei.js → channel.runtime-CKb2-z0p.js} +560 -296
- package/dist/{accounts-ITTlduDO.js → client-BzLj4dyf.js} +52 -102
- package/dist/doctor-contract-BaUvVh8e.js +19 -0
- package/dist/doctor-contract-api.js +1 -1
- package/dist/gateway-auth-api.js +1 -1
- package/dist/{gateway-auth-bypass-BIXLORHU.js → gateway-auth-bypass-CL1PxV3z.js} +3 -2
- package/dist/interactions-DO7J-ND1.js +375 -0
- package/dist/policy-api.js +1 -1
- package/dist/secret-contract-B4R5Bmhv.js +28 -0
- package/dist/secret-contract-api.js +1 -1
- package/dist/slash-route-api.js +1 -1
- package/dist/{slash-state-BfOSlkmn.js → slash-state-C9bpBN40.js} +124 -470
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +209 -209
- package/package.json +4 -4
- package/dist/doctor-contract-ttH0DCuq.js +0 -7
- package/dist/secret-contract-Cx0LUNXy.js +0 -44
- package/dist/{monitor-auth-BiDuyvOc.js → monitor-auth-dEHa1_On.js} +3 -3
|
@@ -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 {
|
|
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 {
|
|
17
|
-
import {
|
|
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 {
|
|
20
|
-
import {
|
|
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
|
|
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
|
|
176
|
-
|
|
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
|
-
|
|
208
|
+
await target.ready;
|
|
209
|
+
if (streamState.stopped && !streamState.final) return false;
|
|
210
|
+
if (normalized === target.lastSentText) return true;
|
|
182
211
|
try {
|
|
183
|
-
if (
|
|
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
|
-
|
|
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
|
|
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: () =>
|
|
210
|
-
clearMessageId
|
|
211
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
loop.
|
|
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
|
-
|
|
230
|
-
|
|
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)
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
...
|
|
605
|
-
|
|
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
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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)
|
|
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.
|
|
1446
|
-
ctx: ctxPayload,
|
|
1447
|
-
cfg,
|
|
1594
|
+
await core.channel.reply.withReplyDispatcher({
|
|
1448
1595
|
dispatcher,
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
2307
|
+
const resolveFinalDeliveryText = (text) => {
|
|
2126
2308
|
if (typeof text !== "string") return;
|
|
2127
|
-
const
|
|
2128
|
-
|
|
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
|
-
|
|
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")
|
|
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:
|
|
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:
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|