@sentry/junior 0.22.0 → 0.24.0

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/app.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  loadSkillsByName,
7
7
  logCapabilityCatalogLoadedOnce,
8
8
  parseSkillInvocation
9
- } from "./chunk-JWBWBJYJ.js";
9
+ } from "./chunk-DGN3WLA4.js";
10
10
  import {
11
11
  SANDBOX_DATA_ROOT,
12
12
  SANDBOX_SKILLS_ROOT,
@@ -27,7 +27,7 @@ import {
27
27
  sandboxSkillDir,
28
28
  sandboxSkillFile,
29
29
  toOptionalTrimmed
30
- } from "./chunk-THPM7NSG.js";
30
+ } from "./chunk-B5O2EJUV.js";
31
31
  import {
32
32
  CredentialUnavailableError,
33
33
  buildOAuthTokenRequest,
@@ -58,7 +58,7 @@ import {
58
58
  toOptionalString,
59
59
  withContext,
60
60
  withSpan
61
- } from "./chunk-MCJJKEB3.js";
61
+ } from "./chunk-J7JEFMVD.js";
62
62
  import "./chunk-Z3YD6NHK.js";
63
63
  import {
64
64
  discoverInstalledPluginPackageContent,
@@ -311,9 +311,6 @@ async function GET3() {
311
311
  });
312
312
  }
313
313
 
314
- // src/handlers/mcp-oauth-callback.ts
315
- import { Buffer as Buffer2 } from "buffer";
316
-
317
314
  // src/chat/state/conversation.ts
318
315
  function coerceRole(value) {
319
316
  return value === "assistant" || value === "system" || value === "user" ? value : "user";
@@ -336,9 +333,17 @@ function coerceAuthor(value) {
336
333
  function coerceMessageMeta(value) {
337
334
  if (!isRecord(value)) return void 0;
338
335
  const meta = {};
336
+ const attachmentCount = toOptionalNumber(value.attachmentCount);
337
+ if (typeof attachmentCount === "number" && attachmentCount > 0) {
338
+ meta.attachmentCount = attachmentCount;
339
+ }
339
340
  if (typeof value.explicitMention === "boolean") {
340
341
  meta.explicitMention = value.explicitMention;
341
342
  }
343
+ const imageAttachmentCount = toOptionalNumber(value.imageAttachmentCount);
344
+ if (typeof imageAttachmentCount === "number" && imageAttachmentCount > 0) {
345
+ meta.imageAttachmentCount = imageAttachmentCount;
346
+ }
342
347
  if (typeof value.replied === "boolean") {
343
348
  meta.replied = value.replied;
344
349
  }
@@ -359,7 +364,7 @@ function coerceMessageMeta(value) {
359
364
  if (typeof value.imagesHydrated === "boolean") {
360
365
  meta.imagesHydrated = value.imagesHydrated;
361
366
  }
362
- if (meta.explicitMention === void 0 && meta.replied === void 0 && meta.skippedReason === void 0 && meta.slackTs === void 0 && meta.imageFileIds === void 0 && meta.imagesHydrated === void 0) {
367
+ if (meta.attachmentCount === void 0 && meta.explicitMention === void 0 && meta.imageAttachmentCount === void 0 && meta.replied === void 0 && meta.skippedReason === void 0 && meta.slackTs === void 0 && meta.imageFileIds === void 0 && meta.imagesHydrated === void 0) {
363
368
  return void 0;
364
369
  }
365
370
  return meta;
@@ -729,81 +734,6 @@ async function deleteMcpServerSessionId(userId, provider) {
729
734
  await stateAdapter.delete(serverSessionKey(userId, provider));
730
735
  }
731
736
 
732
- // src/chat/slack/output.ts
733
- var MAX_INLINE_CHARS = 2200;
734
- var MAX_INLINE_LINES = 45;
735
- function ensureBlockSpacing(text) {
736
- const codeBlockPattern = /^```/;
737
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
738
- const lines = text.split("\n");
739
- const result = [];
740
- let inCodeBlock = false;
741
- for (let i = 0; i < lines.length; i++) {
742
- const line = lines[i];
743
- const isCodeFence = codeBlockPattern.test(line.trimStart());
744
- if (isCodeFence) {
745
- if (!inCodeBlock) {
746
- const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
747
- if (prev2 !== void 0 && prev2.trim() !== "") {
748
- result.push("");
749
- }
750
- }
751
- inCodeBlock = !inCodeBlock;
752
- result.push(line);
753
- continue;
754
- }
755
- if (inCodeBlock) {
756
- result.push(line);
757
- continue;
758
- }
759
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
760
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
761
- result.push("");
762
- }
763
- result.push(line);
764
- }
765
- return result.join("\n");
766
- }
767
- function normalizeForSlack(text) {
768
- let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
769
- normalized = ensureBlockSpacing(normalized);
770
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
771
- }
772
- function buildSlackOutputMessage(text, files) {
773
- const normalized = normalizeForSlack(text);
774
- const fileCount = files?.length ?? 0;
775
- if (!normalized) {
776
- if (fileCount > 0) {
777
- return {
778
- raw: "",
779
- files
780
- };
781
- }
782
- logWarn(
783
- "slack_output_normalized_empty",
784
- {},
785
- {
786
- "app.output.original_length": text.length,
787
- "app.output.parsed_length": normalized.length,
788
- "app.output.file_count": fileCount
789
- },
790
- "Slack output normalized to empty content"
791
- );
792
- return {
793
- markdown: "I couldn't produce a response.",
794
- files
795
- };
796
- }
797
- return {
798
- markdown: normalized,
799
- files
800
- };
801
- }
802
- var slackOutputPolicy = {
803
- maxInlineChars: MAX_INLINE_CHARS,
804
- maxInlineLines: MAX_INLINE_LINES
805
- };
806
-
807
737
  // src/chat/mcp/oauth.ts
808
738
  import { randomUUID } from "crypto";
809
739
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -946,13 +876,19 @@ function mapSlackError(error) {
946
876
  if (apiError === "not_in_channel") {
947
877
  return new SlackActionError(message, "not_in_channel", baseOptions);
948
878
  }
879
+ if (apiError === "already_reacted") {
880
+ return new SlackActionError(message, "already_reacted", baseOptions);
881
+ }
882
+ if (apiError === "no_reaction") {
883
+ return new SlackActionError(message, "no_reaction", baseOptions);
884
+ }
949
885
  if (apiError === "invalid_arguments") {
950
886
  return new SlackActionError(message, "invalid_arguments", baseOptions);
951
887
  }
952
888
  if (apiError === "invalid_name") {
953
889
  return new SlackActionError(message, "invalid_arguments", baseOptions);
954
890
  }
955
- if (apiError === "not_found") {
891
+ if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
956
892
  return new SlackActionError(message, "not_found", baseOptions);
957
893
  }
958
894
  if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
@@ -1052,19 +988,6 @@ async function getFilePermalink(fileId) {
1052
988
  );
1053
989
  return response.file?.permalink;
1054
990
  }
1055
- async function uploadFilesToThread(args) {
1056
- const client2 = getClient();
1057
- await withSlackRetries(
1058
- () => client2.filesUploadV2({
1059
- channel_id: args.channelId,
1060
- thread_ts: args.threadTs,
1061
- file_uploads: args.files.map((f) => ({
1062
- file: f.data,
1063
- filename: f.filename
1064
- }))
1065
- })
1066
- );
1067
- }
1068
991
  async function downloadPrivateSlackFile(url) {
1069
992
  const token = getSlackBotToken();
1070
993
  if (!token) {
@@ -1084,6 +1007,196 @@ async function downloadPrivateSlackFile(url) {
1084
1007
  return Buffer.from(await response.arrayBuffer());
1085
1008
  }
1086
1009
 
1010
+ // src/chat/slack/emoji.ts
1011
+ var SLACK_EMOJI_NAME_RE = /^(?:[a-z0-9_+-]+)(?:::(?:skin-tone-[2-6]))?$/;
1012
+ function normalizeSlackEmojiName(value) {
1013
+ const trimmed = value.trim().toLowerCase();
1014
+ if (!trimmed) {
1015
+ return null;
1016
+ }
1017
+ const normalized = trimmed.startsWith(":") && trimmed.endsWith(":") ? trimmed.slice(1, -1) : trimmed;
1018
+ return SLACK_EMOJI_NAME_RE.test(normalized) ? normalized : null;
1019
+ }
1020
+
1021
+ // src/chat/slack/outbound.ts
1022
+ var MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
1023
+ function requireSlackConversationId(channelId, action) {
1024
+ const normalized = normalizeSlackConversationId(channelId);
1025
+ if (!normalized) {
1026
+ throw new Error(`${action} requires a valid channel ID`);
1027
+ }
1028
+ return normalized;
1029
+ }
1030
+ function requireSlackThreadTimestamp(threadTs, action) {
1031
+ const normalized = threadTs.trim();
1032
+ if (!normalized) {
1033
+ throw new Error(`${action} requires a thread timestamp`);
1034
+ }
1035
+ return normalized;
1036
+ }
1037
+ function requireSlackMessageTimestamp(timestamp, action) {
1038
+ const normalized = timestamp.trim();
1039
+ if (!normalized) {
1040
+ throw new Error(`${action} requires a target message timestamp`);
1041
+ }
1042
+ return normalized;
1043
+ }
1044
+ function requireSlackMessageText(text, action) {
1045
+ if (text.trim().length === 0) {
1046
+ throw new Error(`${action} requires non-empty text`);
1047
+ }
1048
+ if (text.length > MAX_SLACK_MESSAGE_TEXT_CHARS) {
1049
+ throw new Error(
1050
+ `${action} text exceeds Slack's 40000 character truncation limit`
1051
+ );
1052
+ }
1053
+ return text;
1054
+ }
1055
+ async function getPermalinkBestEffort(args) {
1056
+ try {
1057
+ const response = await withSlackRetries(
1058
+ () => getSlackClient().chat.getPermalink({
1059
+ channel: args.channelId,
1060
+ message_ts: args.messageTs
1061
+ }),
1062
+ 3,
1063
+ { action: "chat.getPermalink" }
1064
+ );
1065
+ return response.permalink;
1066
+ } catch {
1067
+ return void 0;
1068
+ }
1069
+ }
1070
+ async function postSlackMessage(input) {
1071
+ const channelId = requireSlackConversationId(
1072
+ input.channelId,
1073
+ "Slack message posting"
1074
+ );
1075
+ const text = requireSlackMessageText(input.text, "Slack message posting");
1076
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(
1077
+ input.threadTs,
1078
+ "Slack thread message posting"
1079
+ ) : void 0;
1080
+ const response = await withSlackRetries(
1081
+ () => getSlackClient().chat.postMessage({
1082
+ channel: channelId,
1083
+ text,
1084
+ mrkdwn: true,
1085
+ ...threadTs ? { thread_ts: threadTs } : {}
1086
+ }),
1087
+ 3,
1088
+ { action: "chat.postMessage" }
1089
+ );
1090
+ if (!response.ts) {
1091
+ throw new Error("Slack message posted without ts");
1092
+ }
1093
+ return {
1094
+ ts: response.ts,
1095
+ ...input.includePermalink ? {
1096
+ permalink: await getPermalinkBestEffort({
1097
+ channelId,
1098
+ messageTs: response.ts
1099
+ })
1100
+ } : {}
1101
+ };
1102
+ }
1103
+ async function postSlackEphemeralMessage(input) {
1104
+ const channelId = requireSlackConversationId(
1105
+ input.channelId,
1106
+ "Slack ephemeral message posting"
1107
+ );
1108
+ const userId = input.userId.trim();
1109
+ if (!userId) {
1110
+ throw new Error("Slack ephemeral message posting requires a user ID");
1111
+ }
1112
+ const text = requireSlackMessageText(
1113
+ input.text,
1114
+ "Slack ephemeral message posting"
1115
+ );
1116
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(
1117
+ input.threadTs,
1118
+ "Slack ephemeral thread message posting"
1119
+ ) : void 0;
1120
+ const response = await withSlackRetries(
1121
+ () => getSlackClient().chat.postEphemeral({
1122
+ channel: channelId,
1123
+ user: userId,
1124
+ text,
1125
+ ...threadTs ? { thread_ts: threadTs } : {}
1126
+ }),
1127
+ 3,
1128
+ { action: "chat.postEphemeral" }
1129
+ );
1130
+ return {
1131
+ messageTs: response.message_ts
1132
+ };
1133
+ }
1134
+ async function uploadFilesToThread(input) {
1135
+ const channelId = requireSlackConversationId(
1136
+ input.channelId,
1137
+ "Slack file upload"
1138
+ );
1139
+ const threadTs = requireSlackThreadTimestamp(
1140
+ input.threadTs,
1141
+ "Slack file upload"
1142
+ );
1143
+ if (input.files.length === 0) {
1144
+ throw new Error("Slack file upload requires at least one file");
1145
+ }
1146
+ const fileUploads = input.files.map((file) => {
1147
+ const filename = file.filename.trim();
1148
+ if (!filename) {
1149
+ throw new Error(
1150
+ "Slack file upload requires every file to have a filename"
1151
+ );
1152
+ }
1153
+ return {
1154
+ file: file.data,
1155
+ filename
1156
+ };
1157
+ });
1158
+ await withSlackRetries(
1159
+ () => getSlackClient().filesUploadV2({
1160
+ channel_id: channelId,
1161
+ thread_ts: threadTs,
1162
+ file_uploads: fileUploads
1163
+ }),
1164
+ 3,
1165
+ { action: "filesUploadV2" }
1166
+ );
1167
+ }
1168
+ async function addReactionToMessage(input) {
1169
+ const channelId = requireSlackConversationId(
1170
+ input.channelId,
1171
+ "Slack reaction"
1172
+ );
1173
+ const timestamp = requireSlackMessageTimestamp(
1174
+ input.timestamp,
1175
+ "Slack reaction"
1176
+ );
1177
+ const emoji = normalizeSlackEmojiName(input.emoji);
1178
+ if (!emoji) {
1179
+ throw new Error("Slack reaction requires a valid emoji alias name");
1180
+ }
1181
+ try {
1182
+ await withSlackRetries(
1183
+ () => getSlackClient().reactions.add({
1184
+ channel: channelId,
1185
+ timestamp,
1186
+ name: emoji
1187
+ }),
1188
+ 3,
1189
+ { action: "reactions.add" }
1190
+ );
1191
+ } catch (error) {
1192
+ if (error instanceof SlackActionError && error.code === "already_reacted") {
1193
+ return { ok: true };
1194
+ }
1195
+ throw error;
1196
+ }
1197
+ return { ok: true };
1198
+ }
1199
+
1087
1200
  // src/chat/respond-helpers.ts
1088
1201
  var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
1089
1202
  function getSessionIdentifiers(context) {
@@ -1260,16 +1373,23 @@ function extractAssistantText(message) {
1260
1373
  (part) => part.type === "text" && typeof part.text === "string"
1261
1374
  ).map((part) => part.text).join("\n");
1262
1375
  }
1263
- function hasCompletedAssistantTurn(messages) {
1376
+ function getTerminalAssistantMessages(messages) {
1377
+ let lastToolResultIndex = -1;
1264
1378
  for (let index = messages.length - 1; index >= 0; index -= 1) {
1265
- const message = messages[index];
1266
- if (!isAssistantMessage(message)) {
1267
- continue;
1379
+ if (isToolResultMessage(messages[index])) {
1380
+ lastToolResultIndex = index;
1381
+ break;
1268
1382
  }
1269
- const stopReason = message.stopReason;
1270
- return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
1271
1383
  }
1272
- return false;
1384
+ return messages.slice(lastToolResultIndex + 1).filter(isAssistantMessage);
1385
+ }
1386
+ function hasCompletedAssistantTurn(messages) {
1387
+ const message = getTerminalAssistantMessages(messages).at(-1);
1388
+ if (!message) {
1389
+ return false;
1390
+ }
1391
+ const stopReason = message.stopReason;
1392
+ return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
1273
1393
  }
1274
1394
  function upsertActiveSkill(activeSkills, next) {
1275
1395
  const existing = activeSkills.find((skill) => skill.name === next.name);
@@ -1335,17 +1455,17 @@ async function deliverPrivateMessage(input) {
1335
1455
  if (input.channelId) {
1336
1456
  try {
1337
1457
  if (isDmChannel(input.channelId)) {
1338
- await client2.chat.postMessage({
1339
- channel: input.channelId,
1458
+ await postSlackMessage({
1459
+ channelId: input.channelId,
1340
1460
  text: input.text,
1341
- ...input.threadTs ? { thread_ts: input.threadTs } : {}
1461
+ threadTs: input.threadTs
1342
1462
  });
1343
1463
  } else {
1344
- await client2.chat.postEphemeral({
1345
- channel: input.channelId,
1346
- user: input.userId,
1464
+ await postSlackEphemeralMessage({
1465
+ channelId: input.channelId,
1466
+ userId: input.userId,
1347
1467
  text: input.text,
1348
- ...input.threadTs ? { thread_ts: input.threadTs } : {}
1468
+ threadTs: input.threadTs
1349
1469
  });
1350
1470
  }
1351
1471
  return "in_context";
@@ -1372,7 +1492,7 @@ async function deliverPrivateMessage(input) {
1372
1492
  );
1373
1493
  return false;
1374
1494
  }
1375
- await client2.chat.postMessage({ channel: dmChannelId, text: input.text });
1495
+ await postSlackMessage({ channelId: dmChannelId, text: input.text });
1376
1496
  return "fallback_dm";
1377
1497
  } catch (error) {
1378
1498
  logWarn(
@@ -1992,6 +2112,7 @@ function coerceThreadArtifactsState(value) {
1992
2112
  }
1993
2113
  return {
1994
2114
  assistantContextChannelId: typeof artifacts.assistantContextChannelId === "string" ? artifacts.assistantContextChannelId : void 0,
2115
+ assistantTitleSourceMessageId: typeof artifacts.assistantTitleSourceMessageId === "string" ? artifacts.assistantTitleSourceMessageId : void 0,
1995
2116
  lastCanvasId: typeof artifacts.lastCanvasId === "string" ? artifacts.lastCanvasId : void 0,
1996
2117
  lastCanvasUrl: typeof artifacts.lastCanvasUrl === "string" ? artifacts.lastCanvasUrl : void 0,
1997
2118
  recentCanvases,
@@ -2176,24 +2297,6 @@ function markTurnFailed(args) {
2176
2297
  });
2177
2298
  args.updateConversationStats(args.conversation);
2178
2299
  }
2179
- function resolveReplyDelivery(args) {
2180
- const replyHasFiles = Boolean(
2181
- args.reply.files && args.reply.files.length > 0
2182
- );
2183
- const deliveryPlan = args.reply.deliveryPlan ?? {
2184
- mode: args.reply.deliveryMode ?? "thread",
2185
- postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
2186
- attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
2187
- };
2188
- let attachFiles = replyHasFiles ? deliveryPlan.attachFiles : "none";
2189
- if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
2190
- attachFiles = "inline";
2191
- }
2192
- return {
2193
- shouldPostThreadReply: deliveryPlan.postThreadText,
2194
- attachFiles
2195
- };
2196
- }
2197
2300
 
2198
2301
  // src/chat/runtime/turn-user-message.ts
2199
2302
  function getTurnUserMessage(conversation, sessionId) {
@@ -2211,6 +2314,15 @@ function getTurnUserMessage(conversation, sessionId) {
2211
2314
  function getTurnUserMessageId(conversation, sessionId) {
2212
2315
  return getTurnUserMessage(conversation, sessionId)?.id;
2213
2316
  }
2317
+ function getTurnUserReplyAttachmentContext(message) {
2318
+ const inboundAttachmentCount = message?.meta?.attachmentCount ?? 0;
2319
+ const imageAttachmentCount = message?.meta?.imageAttachmentCount ?? 0;
2320
+ const imagesHydrated = message?.meta?.imagesHydrated === true;
2321
+ return {
2322
+ ...inboundAttachmentCount > 0 ? { inboundAttachmentCount } : {},
2323
+ ...!imagesHydrated && imageAttachmentCount > 0 ? { omittedImageAttachmentCount: imageAttachmentCount } : {}
2324
+ };
2325
+ }
2214
2326
 
2215
2327
  // src/chat/pi/client.ts
2216
2328
  import {
@@ -2631,7 +2743,7 @@ async function summarizeConversationChunk(messages, conversation, context, deps)
2631
2743
  }
2632
2744
  return transcript.slice(0, 2800);
2633
2745
  }
2634
- async function generateThreadTitleWithDeps(userText, assistantText, deps) {
2746
+ async function generateThreadTitleWithDeps(sourceText, deps) {
2635
2747
  const result = await deps.completeText({
2636
2748
  modelId: botConfig.fastModelId,
2637
2749
  temperature: 0,
@@ -2639,17 +2751,41 @@ async function generateThreadTitleWithDeps(userText, assistantText, deps) {
2639
2751
  {
2640
2752
  role: "user",
2641
2753
  content: [
2642
- "Generate a concise 5-8 word title for this conversation. Reply with ONLY the title, no quotes or punctuation.",
2754
+ "Generate a concise 5-8 word Slack conversation title from the first user message below.",
2755
+ "Capture the user's main request.",
2756
+ "Reply with ONLY the title, with no quotes or trailing punctuation.",
2643
2757
  "",
2644
- `User: ${userText.slice(0, 500)}`,
2645
- `Assistant: ${assistantText.slice(0, 500)}`
2758
+ `First user message: ${sourceText.slice(0, 500)}`
2646
2759
  ].join("\n"),
2647
2760
  timestamp: Date.now()
2648
2761
  }
2649
- ]
2762
+ ],
2763
+ metadata: {
2764
+ modelId: botConfig.fastModelId
2765
+ }
2650
2766
  });
2651
2767
  return result.text.trim().slice(0, 60);
2652
2768
  }
2769
+ function getThreadTitleSourceMessage(conversation) {
2770
+ let firstMessage;
2771
+ for (const message of conversation.messages) {
2772
+ if (!isHumanConversationMessage(message)) {
2773
+ continue;
2774
+ }
2775
+ if (!firstMessage) {
2776
+ firstMessage = message;
2777
+ continue;
2778
+ }
2779
+ if (message.createdAtMs < firstMessage.createdAtMs) {
2780
+ firstMessage = message;
2781
+ continue;
2782
+ }
2783
+ if (message.createdAtMs === firstMessage.createdAtMs && message.id < firstMessage.id) {
2784
+ firstMessage = message;
2785
+ }
2786
+ }
2787
+ return firstMessage;
2788
+ }
2653
2789
  async function compactConversationIfNeededWithDeps(conversation, context, deps) {
2654
2790
  updateConversationStats(conversation);
2655
2791
  let estimatedTokens = conversation.stats.estimatedContextTokens;
@@ -2694,7 +2830,7 @@ async function compactConversationIfNeededWithDeps(conversation, context, deps)
2694
2830
  function createConversationMemoryService(deps) {
2695
2831
  return {
2696
2832
  compactConversationIfNeeded: async (conversation, context) => await compactConversationIfNeededWithDeps(conversation, context, deps),
2697
- generateThreadTitle: async (userText, assistantText) => await generateThreadTitleWithDeps(userText, assistantText, deps)
2833
+ generateThreadTitle: async (sourceText) => await generateThreadTitleWithDeps(sourceText, deps)
2698
2834
  };
2699
2835
  }
2700
2836
  var defaultConversationMemoryService = createConversationMemoryService({
@@ -2799,33 +2935,448 @@ import { Agent } from "@mariozechner/pi-agent-core";
2799
2935
  // src/chat/prompt.ts
2800
2936
  import fs from "fs";
2801
2937
  import path2 from "path";
2802
- var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
2803
- function getLoggedMarkdownFiles() {
2804
- const globalState = globalThis;
2805
- globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
2806
- return globalState.__juniorLoggedMarkdownFiles;
2938
+
2939
+ // src/chat/runtime/status-format.ts
2940
+ var SLACK_STATUS_MAX_LENGTH = 50;
2941
+ function truncateWithEllipsis(text, maxLength) {
2942
+ if (text.length <= maxLength) {
2943
+ return text;
2944
+ }
2945
+ return `${text.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
2807
2946
  }
2808
- function loadOptionalMarkdownFile(candidates, fileName) {
2809
- for (const resolved of candidates) {
2810
- try {
2811
- const raw = fs.readFileSync(resolved, "utf8").trim();
2812
- if (raw.length > 0) {
2813
- const loggedMarkdownFiles = getLoggedMarkdownFiles();
2814
- const logKey = `${fileName}:${resolved}`;
2815
- if (!loggedMarkdownFiles.has(logKey)) {
2816
- loggedMarkdownFiles.add(logKey);
2817
- logInfo(
2818
- `${fileName.toLowerCase()}_loaded`,
2819
- {},
2820
- {
2821
- "file.path": resolved
2822
- },
2823
- `Loaded ${fileName}`
2824
- );
2825
- }
2826
- return raw;
2827
- }
2828
- } catch {
2947
+ function truncateStatusText(text) {
2948
+ const trimmed = text.trim();
2949
+ if (!trimmed) {
2950
+ return "";
2951
+ }
2952
+ return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
2953
+ }
2954
+ function compactStatusPath(value) {
2955
+ if (typeof value !== "string") {
2956
+ return void 0;
2957
+ }
2958
+ const trimmed = value.trim();
2959
+ if (!trimmed) {
2960
+ return void 0;
2961
+ }
2962
+ if (trimmed.length <= 80) {
2963
+ return trimmed;
2964
+ }
2965
+ return `...${trimmed.slice(-77)}`;
2966
+ }
2967
+ function compactStatusText(value, maxLength = 80) {
2968
+ if (typeof value !== "string") {
2969
+ return void 0;
2970
+ }
2971
+ const trimmed = value.trim();
2972
+ if (!trimmed) {
2973
+ return void 0;
2974
+ }
2975
+ return truncateWithEllipsis(trimmed, maxLength);
2976
+ }
2977
+ function readShellToken(command, startIndex) {
2978
+ let index = startIndex;
2979
+ while (index < command.length && /\s/.test(command[index] ?? "")) {
2980
+ index += 1;
2981
+ }
2982
+ if (index >= command.length) {
2983
+ return void 0;
2984
+ }
2985
+ let token = "";
2986
+ let quote;
2987
+ while (index < command.length) {
2988
+ const char = command[index];
2989
+ if (!char) {
2990
+ break;
2991
+ }
2992
+ if (quote) {
2993
+ if (char === quote) {
2994
+ quote = void 0;
2995
+ index += 1;
2996
+ continue;
2997
+ }
2998
+ if (char === "\\" && quote === '"' && index + 1 < command.length) {
2999
+ token += command[index + 1];
3000
+ index += 2;
3001
+ continue;
3002
+ }
3003
+ token += char;
3004
+ index += 1;
3005
+ continue;
3006
+ }
3007
+ if (/\s/.test(char)) {
3008
+ break;
3009
+ }
3010
+ if (char === '"' || char === "'") {
3011
+ quote = char;
3012
+ index += 1;
3013
+ continue;
3014
+ }
3015
+ if (char === "\\" && index + 1 < command.length) {
3016
+ token += command[index + 1];
3017
+ index += 2;
3018
+ continue;
3019
+ }
3020
+ token += char;
3021
+ index += 1;
3022
+ }
3023
+ return { token, nextIndex: index };
3024
+ }
3025
+ function compactStatusCommand(value) {
3026
+ if (typeof value !== "string") {
3027
+ return void 0;
3028
+ }
3029
+ const trimmed = value.trim();
3030
+ if (!trimmed) {
3031
+ return void 0;
3032
+ }
3033
+ let index = 0;
3034
+ while (index < trimmed.length) {
3035
+ const parsed = readShellToken(trimmed, index);
3036
+ if (!parsed) {
3037
+ return void 0;
3038
+ }
3039
+ index = parsed.nextIndex;
3040
+ if (!parsed.token) {
3041
+ continue;
3042
+ }
3043
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(parsed.token)) {
3044
+ continue;
3045
+ }
3046
+ const normalized = parsed.token.replace(/[\\/]+$/g, "");
3047
+ if (!normalized) {
3048
+ return void 0;
3049
+ }
3050
+ const parts = normalized.split(/[\\/]/).filter((part) => part.length > 0);
3051
+ const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
3052
+ return compactStatusText(command, 40);
3053
+ }
3054
+ return void 0;
3055
+ }
3056
+ function compactStatusFilename(value) {
3057
+ if (typeof value !== "string") {
3058
+ return void 0;
3059
+ }
3060
+ const trimmed = value.trim().replace(/[\\/]+$/g, "");
3061
+ if (!trimmed) {
3062
+ return void 0;
3063
+ }
3064
+ const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
3065
+ const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
3066
+ return compactStatusText(filename, 80);
3067
+ }
3068
+ function extractStatusUrlDomain(value) {
3069
+ if (typeof value !== "string") {
3070
+ return void 0;
3071
+ }
3072
+ const trimmed = value.trim();
3073
+ if (!trimmed) {
3074
+ return void 0;
3075
+ }
3076
+ try {
3077
+ const parsed = new URL(trimmed);
3078
+ return parsed.hostname || void 0;
3079
+ } catch {
3080
+ return void 0;
3081
+ }
3082
+ }
3083
+
3084
+ // src/chat/slack/mrkdwn.ts
3085
+ function ensureBlockSpacing(text) {
3086
+ const codeBlockPattern = /^```/;
3087
+ const listItemPattern = /^[-*•]\s|^\d+\.\s/;
3088
+ const lines = text.split("\n");
3089
+ const result = [];
3090
+ let inCodeBlock = false;
3091
+ for (let i = 0; i < lines.length; i++) {
3092
+ const line = lines[i];
3093
+ const isCodeFence = codeBlockPattern.test(line.trimStart());
3094
+ if (isCodeFence) {
3095
+ if (!inCodeBlock) {
3096
+ const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
3097
+ if (prev2 !== void 0 && prev2.trim() !== "") {
3098
+ result.push("");
3099
+ }
3100
+ }
3101
+ inCodeBlock = !inCodeBlock;
3102
+ result.push(line);
3103
+ continue;
3104
+ }
3105
+ if (inCodeBlock) {
3106
+ result.push(line);
3107
+ continue;
3108
+ }
3109
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
3110
+ if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
3111
+ result.push("");
3112
+ }
3113
+ result.push(line);
3114
+ }
3115
+ return result.join("\n");
3116
+ }
3117
+ function renderSlackMrkdwn(text) {
3118
+ let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
3119
+ normalized = ensureBlockSpacing(normalized);
3120
+ return normalized.replace(/\n{3,}/g, "\n\n").trim();
3121
+ }
3122
+ function normalizeSlackStatusText(text) {
3123
+ const trimmed = text.trim();
3124
+ if (!trimmed) {
3125
+ return "";
3126
+ }
3127
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
3128
+ }
3129
+
3130
+ // src/chat/slack/output.ts
3131
+ var MAX_INLINE_CHARS = 2200;
3132
+ var MAX_INLINE_LINES = 45;
3133
+ var CONTINUED_MARKER = "\n\n[Continued below]";
3134
+ var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
3135
+ function countSlackLines(text) {
3136
+ if (!text) {
3137
+ return 0;
3138
+ }
3139
+ return text.split("\n").length;
3140
+ }
3141
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
3142
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
3143
+ }
3144
+ function findSplitIndex(text, maxChars) {
3145
+ if (text.length <= maxChars) {
3146
+ return text.length;
3147
+ }
3148
+ const bounded = text.slice(0, maxChars);
3149
+ const newlineIndex = bounded.lastIndexOf("\n");
3150
+ if (newlineIndex > 0) {
3151
+ return newlineIndex;
3152
+ }
3153
+ const spaceIndex = bounded.lastIndexOf(" ");
3154
+ if (spaceIndex > 0) {
3155
+ return spaceIndex;
3156
+ }
3157
+ return maxChars;
3158
+ }
3159
+ function splitByLineBudget(text, maxLines) {
3160
+ if (maxLines <= 0) {
3161
+ return "";
3162
+ }
3163
+ const lines = text.split("\n");
3164
+ if (lines.length <= maxLines) {
3165
+ return text;
3166
+ }
3167
+ return lines.slice(0, maxLines).join("\n");
3168
+ }
3169
+ function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
3170
+ return {
3171
+ maxChars: Math.max(1, maxChars - suffix.length),
3172
+ maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
3173
+ };
3174
+ }
3175
+ function forceSplitBudget(text, budget) {
3176
+ const lineCount = countSlackLines(text);
3177
+ return {
3178
+ maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
3179
+ maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
3180
+ };
3181
+ }
3182
+ function getFenceContinuation(text) {
3183
+ let open;
3184
+ for (const line of text.split("\n")) {
3185
+ const trimmed = line.trimStart();
3186
+ const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
3187
+ if (!openerMatch) {
3188
+ continue;
3189
+ }
3190
+ if (!open) {
3191
+ open = {
3192
+ fence: openerMatch[1],
3193
+ openerLine: trimmed
3194
+ };
3195
+ continue;
3196
+ }
3197
+ if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
3198
+ open = void 0;
3199
+ }
3200
+ }
3201
+ if (!open) {
3202
+ return null;
3203
+ }
3204
+ return {
3205
+ closeSuffix: text.endsWith("\n") ? open.fence : `
3206
+ ${open.fence}`,
3207
+ reopenPrefix: `${open.openerLine}
3208
+ `
3209
+ };
3210
+ }
3211
+ function appendSlackSuffix(text, marker) {
3212
+ const carryover = getFenceContinuation(text);
3213
+ return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
3214
+ }
3215
+ function takeSlackContinuationChunk(text, budget) {
3216
+ let { prefix, rest } = takeSlackInlinePrefix(text, budget);
3217
+ if (!rest) {
3218
+ ({ prefix, rest } = takeSlackInlinePrefix(
3219
+ text,
3220
+ forceSplitBudget(text, budget)
3221
+ ));
3222
+ }
3223
+ let carryover = rest ? getFenceContinuation(prefix) : null;
3224
+ if (!carryover) {
3225
+ return { prefix, rest };
3226
+ }
3227
+ const carryoverBudget = reserveInlineBudgetForSuffix(
3228
+ `${carryover.closeSuffix}${CONTINUED_MARKER}`
3229
+ );
3230
+ ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
3231
+ if (!rest) {
3232
+ ({ prefix, rest } = takeSlackInlinePrefix(
3233
+ text,
3234
+ forceSplitBudget(text, carryoverBudget)
3235
+ ));
3236
+ }
3237
+ carryover = rest ? getFenceContinuation(prefix) : null;
3238
+ if (!carryover) {
3239
+ return { prefix, rest };
3240
+ }
3241
+ return {
3242
+ prefix,
3243
+ rest: `${carryover.reopenPrefix}${rest}`
3244
+ };
3245
+ }
3246
+ function takeSlackContinuationPrefix(text, options) {
3247
+ const budget = {
3248
+ maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
3249
+ maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
3250
+ };
3251
+ const { prefix, rest } = (() => {
3252
+ if (options?.forceSplit) {
3253
+ return takeSlackContinuationChunk(text, budget);
3254
+ }
3255
+ const initial = takeSlackInlinePrefix(text, budget);
3256
+ return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
3257
+ })();
3258
+ return {
3259
+ prefix,
3260
+ renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
3261
+ rest
3262
+ };
3263
+ }
3264
+ function takeSlackInlinePrefix(text, options) {
3265
+ const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
3266
+ const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
3267
+ const normalized = text.replace(/\r\n?/g, "\n");
3268
+ if (!normalized) {
3269
+ return { prefix: "", rest: "" };
3270
+ }
3271
+ if (fitsInlineBudget(normalized, maxChars, maxLines)) {
3272
+ return { prefix: normalized, rest: "" };
3273
+ }
3274
+ const lineBounded = splitByLineBudget(normalized, maxLines);
3275
+ const cutIndex = findSplitIndex(lineBounded, maxChars);
3276
+ const prefix = lineBounded.slice(0, cutIndex).trimEnd();
3277
+ if (prefix) {
3278
+ return {
3279
+ prefix,
3280
+ rest: normalized.slice(prefix.length).trimStart()
3281
+ };
3282
+ }
3283
+ const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
3284
+ return {
3285
+ prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
3286
+ rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
3287
+ };
3288
+ }
3289
+ function splitSlackReplyText(text, options) {
3290
+ const normalized = renderSlackMrkdwn(text);
3291
+ if (!normalized) {
3292
+ return [];
3293
+ }
3294
+ const chunks = [];
3295
+ const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
3296
+ let remaining = normalized;
3297
+ while (remaining) {
3298
+ const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, INTERRUPTED_MARKER)) : fitsInlineBudget(remaining);
3299
+ if (fitsFinalChunk) {
3300
+ chunks.push(
3301
+ options?.interrupted ? appendSlackSuffix(remaining, INTERRUPTED_MARKER) : remaining
3302
+ );
3303
+ break;
3304
+ }
3305
+ const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
3306
+ ...continuationBudget,
3307
+ forceSplit: true
3308
+ });
3309
+ chunks.push(renderedPrefix);
3310
+ remaining = rest;
3311
+ }
3312
+ return chunks;
3313
+ }
3314
+ function getSlackContinuationBudget() {
3315
+ return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
3316
+ }
3317
+ function buildSlackOutputMessage(text, files) {
3318
+ const normalized = renderSlackMrkdwn(text);
3319
+ const fileCount = files?.length ?? 0;
3320
+ if (!normalized) {
3321
+ if (fileCount > 0) {
3322
+ return {
3323
+ raw: "",
3324
+ files
3325
+ };
3326
+ }
3327
+ logWarn(
3328
+ "slack_output_normalized_empty",
3329
+ {},
3330
+ {
3331
+ "app.output.original_length": text.length,
3332
+ "app.output.parsed_length": normalized.length,
3333
+ "app.output.file_count": fileCount
3334
+ },
3335
+ "Slack output normalized to empty content"
3336
+ );
3337
+ return {
3338
+ markdown: "I couldn't produce a response.",
3339
+ files
3340
+ };
3341
+ }
3342
+ return {
3343
+ markdown: normalized,
3344
+ files
3345
+ };
3346
+ }
3347
+ var slackOutputPolicy = {
3348
+ maxInlineChars: MAX_INLINE_CHARS,
3349
+ maxInlineLines: MAX_INLINE_LINES
3350
+ };
3351
+
3352
+ // src/chat/prompt.ts
3353
+ var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
3354
+ function getLoggedMarkdownFiles() {
3355
+ const globalState = globalThis;
3356
+ globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
3357
+ return globalState.__juniorLoggedMarkdownFiles;
3358
+ }
3359
+ function loadOptionalMarkdownFile(candidates, fileName) {
3360
+ for (const resolved of candidates) {
3361
+ try {
3362
+ const raw = fs.readFileSync(resolved, "utf8").trim();
3363
+ if (raw.length > 0) {
3364
+ const loggedMarkdownFiles = getLoggedMarkdownFiles();
3365
+ const logKey = `${fileName}:${resolved}`;
3366
+ if (!loggedMarkdownFiles.has(logKey)) {
3367
+ loggedMarkdownFiles.add(logKey);
3368
+ logInfo(
3369
+ `${fileName.toLowerCase()}_loaded`,
3370
+ {},
3371
+ {
3372
+ "file.path": resolved
3373
+ },
3374
+ `Loaded ${fileName}`
3375
+ );
3376
+ }
3377
+ return raw;
3378
+ }
3379
+ } catch {
2829
3380
  continue;
2830
3381
  }
2831
3382
  }
@@ -3169,6 +3720,9 @@ function buildSystemPrompt(params) {
3169
3720
  "- Keep routine setup and research steps silent in user-facing replies. Do not narrate duplicate checks, credential issuance, file writes, or similar internal progress unless the result is user-relevant.",
3170
3721
  "- If a routine prerequisite check finds nothing notable, omit it entirely from the final reply and report only the user-relevant outcome.",
3171
3722
  "- Prefer a single result-focused reply after tool work completes. Only send an interim reply when you need user input or have a concrete blocking problem to report.",
3723
+ "- For external/factual research requests that require tools, do not send any preliminary conclusion, 'let me check', or progress narration before the evidence-gathering work is done. Use assistant status for in-progress work and make the first visible reply the researched answer.",
3724
+ "- For evidence-gathering tasks, never state a factual conclusion before you have actually gathered and checked the sources.",
3725
+ "- Do not include internal process chatter such as 'let me find', 'fetching now', 'good, I have sources', 'trying smaller limits', or 'I now have sufficient context' in the final user-facing reply.",
3172
3726
  "- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
3173
3727
  "- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
3174
3728
  "- Never claim a screenshot/file is attached unless `attachFile` succeeded in this turn.",
@@ -3224,11 +3778,12 @@ function buildSystemPrompt(params) {
3224
3778
  [
3225
3779
  "Always produce output that follows this contract:",
3226
3780
  `<output format="slack-mrkdwn" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`,
3227
- "- Use plain Slack-safe markdown (headings, bullets, short code blocks).",
3781
+ "- Use Slack-friendly markdown, not full CommonMark. Prefer bold section labels over markdown headings, and use bullets and short code blocks when helpful.",
3228
3782
  "- Keep normal responses brief and scannable.",
3229
3783
  "- If depth is needed, start with a concise summary and then provide fuller detail.",
3230
- "- A brief initial acknowledgment before significant tool work is fine; avoid extended process chatter or repeated status updates.",
3231
- "- Avoid tables unless explicitly requested.",
3784
+ "- For tool-heavy research, discovery, or source-checking requests, do not send an initial acknowledgment. Start the visible reply only once you can present the actual answer.",
3785
+ "- Do not narrate tool execution or repeated status updates in the visible reply.",
3786
+ "- Avoid tables and markdown links like `[label](url)` unless explicitly requested. Prefer plain URLs or Slack-native entities when exact rendering matters.",
3232
3787
  "- End every turn with a final user-facing markdown response.",
3233
3788
  "</output>"
3234
3789
  ].join("\n")
@@ -5379,81 +5934,7 @@ function createSearchToolsTool(mcpToolManager, getActiveSkills) {
5379
5934
  // src/chat/tools/slack/channel-list-messages.ts
5380
5935
  import { Type as Type7 } from "@sinclair/typebox";
5381
5936
 
5382
- // src/chat/slack/emoji.ts
5383
- var SLACK_EMOJI_NAME_RE = /^(?:[a-z0-9_+-]+)(?:::(?:skin-tone-[2-6]))?$/;
5384
- function normalizeSlackEmojiName(value) {
5385
- const trimmed = value.trim().toLowerCase();
5386
- if (!trimmed) {
5387
- return null;
5388
- }
5389
- const normalized = trimmed.startsWith(":") && trimmed.endsWith(":") ? trimmed.slice(1, -1) : trimmed;
5390
- return SLACK_EMOJI_NAME_RE.test(normalized) ? normalized : null;
5391
- }
5392
-
5393
5937
  // src/chat/slack/channel.ts
5394
- async function postMessageToChannel(input) {
5395
- const client2 = getSlackClient();
5396
- const channelId = normalizeSlackConversationId(input.channelId);
5397
- if (!channelId) {
5398
- throw new Error(
5399
- "Slack channel message posting requires a valid channel ID"
5400
- );
5401
- }
5402
- const response = await withSlackRetries(
5403
- () => client2.chat.postMessage({
5404
- channel: channelId,
5405
- text: input.text,
5406
- mrkdwn: true
5407
- }),
5408
- 3,
5409
- { action: "chat.postMessage" }
5410
- );
5411
- if (!response.ts) {
5412
- throw new Error("Slack channel message posted without ts");
5413
- }
5414
- let permalink;
5415
- try {
5416
- const permalinkResponse = await withSlackRetries(
5417
- () => client2.chat.getPermalink({
5418
- channel: channelId,
5419
- message_ts: response.ts
5420
- }),
5421
- 3,
5422
- { action: "chat.getPermalink" }
5423
- );
5424
- permalink = permalinkResponse.permalink;
5425
- } catch {
5426
- }
5427
- return {
5428
- ts: response.ts,
5429
- permalink
5430
- };
5431
- }
5432
- async function addReactionToMessage(input) {
5433
- const client2 = getSlackClient();
5434
- const channelId = normalizeSlackConversationId(input.channelId);
5435
- if (!channelId) {
5436
- throw new Error("Slack reaction requires a valid channel ID");
5437
- }
5438
- const timestamp = input.timestamp.trim();
5439
- if (!timestamp) {
5440
- throw new Error("Slack reaction requires a target message timestamp");
5441
- }
5442
- const emoji = normalizeSlackEmojiName(input.emoji);
5443
- if (!emoji) {
5444
- throw new Error("Slack reaction requires a valid emoji alias name");
5445
- }
5446
- await withSlackRetries(
5447
- () => client2.reactions.add({
5448
- channel: channelId,
5449
- timestamp,
5450
- name: emoji
5451
- }),
5452
- 3,
5453
- { action: "reactions.add" }
5454
- );
5455
- return { ok: true };
5456
- }
5457
5938
  async function listChannelMessages(input) {
5458
5939
  const client2 = getSlackClient();
5459
5940
  const channelId = normalizeSlackConversationId(input.channelId);
@@ -5663,9 +6144,10 @@ function createSlackChannelPostMessageTool(context, state) {
5663
6144
  deduplicated: true
5664
6145
  };
5665
6146
  }
5666
- const posted = await postMessageToChannel({
6147
+ const posted = await postSlackMessage({
5667
6148
  channelId: targetChannelId,
5668
- text
6149
+ text,
6150
+ includePermalink: true
5669
6151
  });
5670
6152
  const response = {
5671
6153
  ok: true,
@@ -7181,243 +7663,98 @@ function getSandboxErrorDetails(error) {
7181
7663
  return extractHttpErrorDetails(error, {
7182
7664
  attributePrefix: "app.sandbox.api_error",
7183
7665
  extraFields: [...SANDBOX_ERROR_FIELDS]
7184
- });
7185
- }
7186
- function findInErrorChain(error, predicate) {
7187
- const seen = /* @__PURE__ */ new Set();
7188
- let current = error;
7189
- while (current && !seen.has(current)) {
7190
- if (predicate(current)) {
7191
- return true;
7192
- }
7193
- seen.add(current);
7194
- current = typeof current === "object" ? current.cause : void 0;
7195
- }
7196
- return false;
7197
- }
7198
- function getFirstErrorMessage(error) {
7199
- const seen = /* @__PURE__ */ new Set();
7200
- let current = error;
7201
- while (current && !seen.has(current)) {
7202
- if (current instanceof Error) {
7203
- const message = current.message.trim();
7204
- if (message) {
7205
- return message;
7206
- }
7207
- }
7208
- seen.add(current);
7209
- current = typeof current === "object" ? current.cause : void 0;
7210
- }
7211
- return void 0;
7212
- }
7213
- function isAlreadyExistsError(error) {
7214
- const details = getSandboxErrorDetails(error);
7215
- return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
7216
- }
7217
- function isSandboxUnavailableError(error) {
7218
- return findInErrorChain(error, (candidate) => {
7219
- const details = getSandboxErrorDetails(candidate);
7220
- const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7221
- return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
7222
- });
7223
- }
7224
- function isSnapshottingError(error) {
7225
- return findInErrorChain(error, (candidate) => {
7226
- const details = getSandboxErrorDetails(candidate);
7227
- const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7228
- return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
7229
- });
7230
- }
7231
- function wrapSandboxSetupError(error) {
7232
- try {
7233
- const details = getSandboxErrorDetails(error);
7234
- if (details.summary) {
7235
- return new Error(`sandbox setup failed (${details.summary})`, {
7236
- cause: error
7237
- });
7238
- }
7239
- } catch {
7240
- }
7241
- let causeMessage;
7242
- try {
7243
- causeMessage = getFirstErrorMessage(error);
7244
- } catch (cause) {
7245
- causeMessage = cause instanceof Error ? cause.message : void 0;
7246
- }
7247
- if (causeMessage && causeMessage.trim() && causeMessage !== "sandbox setup failed") {
7248
- const oneLine = causeMessage.replace(/\s+/g, " ").trim();
7249
- return new Error(`sandbox setup failed (${oneLine})`, { cause: error });
7250
- }
7251
- return new Error("sandbox setup failed", { cause: error });
7252
- }
7253
- function throwSandboxOperationError(action, error, includeMissingPath = false) {
7254
- const details = getSandboxErrorDetails(error);
7255
- setSpanAttributes({
7256
- ...details.attributes,
7257
- ...includeMissingPath ? {
7258
- "app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
7259
- } : {},
7260
- "app.sandbox.success": false
7261
- });
7262
- setSpanStatus("error");
7263
- throw new Error(
7264
- details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
7265
- {
7266
- cause: error
7267
- }
7268
- );
7269
- }
7270
-
7271
- // src/chat/sandbox/session.ts
7272
- import { Sandbox } from "@vercel/sandbox";
7273
- import { createBashTool as createBashTool2 } from "bash-tool";
7274
-
7275
- // src/chat/runtime/status-format.ts
7276
- var SLACK_STATUS_MAX_LENGTH = 50;
7277
- function truncateWithEllipsis(text, maxLength) {
7278
- if (text.length <= maxLength) {
7279
- return text;
7280
- }
7281
- return `${text.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
7282
- }
7283
- function truncateStatusText(text) {
7284
- const trimmed = text.trim();
7285
- if (!trimmed) {
7286
- return "";
7287
- }
7288
- return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
7289
- }
7290
- function compactStatusPath(value) {
7291
- if (typeof value !== "string") {
7292
- return void 0;
7293
- }
7294
- const trimmed = value.trim();
7295
- if (!trimmed) {
7296
- return void 0;
7297
- }
7298
- if (trimmed.length <= 80) {
7299
- return trimmed;
7300
- }
7301
- return `...${trimmed.slice(-77)}`;
7302
- }
7303
- function compactStatusText(value, maxLength = 80) {
7304
- if (typeof value !== "string") {
7305
- return void 0;
7306
- }
7307
- const trimmed = value.trim();
7308
- if (!trimmed) {
7309
- return void 0;
7310
- }
7311
- return truncateWithEllipsis(trimmed, maxLength);
7312
- }
7313
- function readShellToken(command, startIndex) {
7314
- let index = startIndex;
7315
- while (index < command.length && /\s/.test(command[index] ?? "")) {
7316
- index += 1;
7317
- }
7318
- if (index >= command.length) {
7319
- return void 0;
7320
- }
7321
- let token = "";
7322
- let quote;
7323
- while (index < command.length) {
7324
- const char = command[index];
7325
- if (!char) {
7326
- break;
7327
- }
7328
- if (quote) {
7329
- if (char === quote) {
7330
- quote = void 0;
7331
- index += 1;
7332
- continue;
7333
- }
7334
- if (char === "\\" && quote === '"' && index + 1 < command.length) {
7335
- token += command[index + 1];
7336
- index += 2;
7337
- continue;
7338
- }
7339
- token += char;
7340
- index += 1;
7341
- continue;
7342
- }
7343
- if (/\s/.test(char)) {
7344
- break;
7345
- }
7346
- if (char === '"' || char === "'") {
7347
- quote = char;
7348
- index += 1;
7349
- continue;
7350
- }
7351
- if (char === "\\" && index + 1 < command.length) {
7352
- token += command[index + 1];
7353
- index += 2;
7354
- continue;
7666
+ });
7667
+ }
7668
+ function findInErrorChain(error, predicate) {
7669
+ const seen = /* @__PURE__ */ new Set();
7670
+ let current = error;
7671
+ while (current && !seen.has(current)) {
7672
+ if (predicate(current)) {
7673
+ return true;
7355
7674
  }
7356
- token += char;
7357
- index += 1;
7675
+ seen.add(current);
7676
+ current = typeof current === "object" ? current.cause : void 0;
7358
7677
  }
7359
- return { token, nextIndex: index };
7678
+ return false;
7360
7679
  }
7361
- function compactStatusCommand(value) {
7362
- if (typeof value !== "string") {
7363
- return void 0;
7364
- }
7365
- const trimmed = value.trim();
7366
- if (!trimmed) {
7367
- return void 0;
7368
- }
7369
- let index = 0;
7370
- while (index < trimmed.length) {
7371
- const parsed = readShellToken(trimmed, index);
7372
- if (!parsed) {
7373
- return void 0;
7374
- }
7375
- index = parsed.nextIndex;
7376
- if (!parsed.token) {
7377
- continue;
7378
- }
7379
- if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(parsed.token)) {
7380
- continue;
7381
- }
7382
- const normalized = parsed.token.replace(/[\\/]+$/g, "");
7383
- if (!normalized) {
7384
- return void 0;
7680
+ function getFirstErrorMessage(error) {
7681
+ const seen = /* @__PURE__ */ new Set();
7682
+ let current = error;
7683
+ while (current && !seen.has(current)) {
7684
+ if (current instanceof Error) {
7685
+ const message = current.message.trim();
7686
+ if (message) {
7687
+ return message;
7688
+ }
7385
7689
  }
7386
- const parts = normalized.split(/[\\/]/).filter((part) => part.length > 0);
7387
- const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
7388
- return compactStatusText(command, 40);
7690
+ seen.add(current);
7691
+ current = typeof current === "object" ? current.cause : void 0;
7389
7692
  }
7390
7693
  return void 0;
7391
7694
  }
7392
- function compactStatusFilename(value) {
7393
- if (typeof value !== "string") {
7394
- return void 0;
7395
- }
7396
- const trimmed = value.trim().replace(/[\\/]+$/g, "");
7397
- if (!trimmed) {
7398
- return void 0;
7399
- }
7400
- const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
7401
- const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
7402
- return compactStatusText(filename, 80);
7695
+ function isAlreadyExistsError(error) {
7696
+ const details = getSandboxErrorDetails(error);
7697
+ return details.searchableText.includes("already exists") || details.searchableText.includes("file exists") || details.searchableText.includes("eexist");
7403
7698
  }
7404
- function extractStatusUrlDomain(value) {
7405
- if (typeof value !== "string") {
7406
- return void 0;
7407
- }
7408
- const trimmed = value.trim();
7409
- if (!trimmed) {
7410
- return void 0;
7411
- }
7699
+ function isSandboxUnavailableError(error) {
7700
+ return findInErrorChain(error, (candidate) => {
7701
+ const details = getSandboxErrorDetails(candidate);
7702
+ const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7703
+ return searchable.includes("sandbox_stopped") || searchable.includes("status=410") || searchable.includes("status code 410") || searchable.includes("no longer available");
7704
+ });
7705
+ }
7706
+ function isSnapshottingError(error) {
7707
+ return findInErrorChain(error, (candidate) => {
7708
+ const details = getSandboxErrorDetails(candidate);
7709
+ const searchable = `${details.searchableText} ${details.summary}`.toLowerCase();
7710
+ return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
7711
+ });
7712
+ }
7713
+ function wrapSandboxSetupError(error) {
7412
7714
  try {
7413
- const parsed = new URL(trimmed);
7414
- return parsed.hostname || void 0;
7715
+ const details = getSandboxErrorDetails(error);
7716
+ if (details.summary) {
7717
+ return new Error(`sandbox setup failed (${details.summary})`, {
7718
+ cause: error
7719
+ });
7720
+ }
7415
7721
  } catch {
7416
- return void 0;
7417
7722
  }
7723
+ let causeMessage;
7724
+ try {
7725
+ causeMessage = getFirstErrorMessage(error);
7726
+ } catch (cause) {
7727
+ causeMessage = cause instanceof Error ? cause.message : void 0;
7728
+ }
7729
+ if (causeMessage && causeMessage.trim() && causeMessage !== "sandbox setup failed") {
7730
+ const oneLine = causeMessage.replace(/\s+/g, " ").trim();
7731
+ return new Error(`sandbox setup failed (${oneLine})`, { cause: error });
7732
+ }
7733
+ return new Error("sandbox setup failed", { cause: error });
7734
+ }
7735
+ function throwSandboxOperationError(action, error, includeMissingPath = false) {
7736
+ const details = getSandboxErrorDetails(error);
7737
+ setSpanAttributes({
7738
+ ...details.attributes,
7739
+ ...includeMissingPath ? {
7740
+ "app.sandbox.api_error.missing_path": details.searchableText.includes("no such file") || details.searchableText.includes("enoent")
7741
+ } : {},
7742
+ "app.sandbox.success": false
7743
+ });
7744
+ setSpanStatus("error");
7745
+ throw new Error(
7746
+ details.summary ? `${action} failed (${details.summary})` : `${action} failed`,
7747
+ {
7748
+ cause: error
7749
+ }
7750
+ );
7418
7751
  }
7419
7752
 
7420
- // src/chat/runtime/assistant-status.ts
7753
+ // src/chat/sandbox/session.ts
7754
+ import { Sandbox } from "@vercel/sandbox";
7755
+ import { createBashTool as createBashTool2 } from "bash-tool";
7756
+
7757
+ // src/chat/slack/assistant-thread/status-render.ts
7421
7758
  var STATUS_PATTERNS = {
7422
7759
  thinking: {
7423
7760
  defaultContext: "\u2026",
@@ -7471,17 +7808,10 @@ var STATUS_PATTERNS = {
7471
7808
  function makeAssistantStatus(kind, context) {
7472
7809
  return { kind, ...context ? { context } : {} };
7473
7810
  }
7474
- function normalizeAssistantStatusText(text) {
7475
- const trimmed = text.trim();
7476
- if (!trimmed) {
7477
- return "";
7478
- }
7479
- return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
7480
- }
7481
- function buildAssistantStatusPresentation(args) {
7811
+ function renderAssistantStatus(args) {
7482
7812
  const random = args.random ?? Math.random;
7483
7813
  const pattern = STATUS_PATTERNS[args.status.kind];
7484
- const context = normalizeAssistantStatusText(args.status.context ?? "") || pattern.defaultContext;
7814
+ const context = normalizeSlackStatusText(args.status.context ?? "") || pattern.defaultContext;
7485
7815
  const index = Math.floor(random() * pattern.variants.length);
7486
7816
  const verb = pattern.variants[index] ?? pattern.variants[0];
7487
7817
  const visible = truncateStatusText(`${verb} ${context}`);
@@ -7493,46 +7823,274 @@ function buildAssistantStatusPresentation(args) {
7493
7823
  suggestions: Array.from(/* @__PURE__ */ new Set([visible, hint]))
7494
7824
  };
7495
7825
  }
7496
- function createSlackAdapterAssistantStatusTransport(args) {
7497
- return {
7498
- async setStatus(channelId, threadTs, status, suggestions) {
7499
- try {
7500
- await args.getSlackAdapter().setAssistantStatus(channelId, threadTs, status, suggestions);
7501
- } catch (error) {
7502
- logAssistantStatusFailure(status, error);
7503
- }
7826
+
7827
+ // src/chat/slack/assistant-thread/status-scheduler.ts
7828
+ var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
7829
+ var STATUS_MIN_VISIBLE_MS = 1200;
7830
+ var STATUS_ROTATION_INTERVAL_MS = 3e4;
7831
+ function createAssistantStatusScheduler(args) {
7832
+ const now = args.now ?? (() => Date.now());
7833
+ const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
7834
+ const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
7835
+ const random = args.random ?? Math.random;
7836
+ let active = false;
7837
+ let currentKey = "";
7838
+ let currentStatus = makeAssistantStatus("thinking");
7839
+ let currentVisibleStatus = "";
7840
+ let lastStatusAt = 0;
7841
+ let pendingStatus = null;
7842
+ let pendingKey = "";
7843
+ let pendingTimer = null;
7844
+ let rotationTimer = null;
7845
+ let inflightStatusUpdate = Promise.resolve();
7846
+ const enqueueStatusUpdate = (task) => {
7847
+ const request = inflightStatusUpdate.catch(() => void 0).then(async () => {
7848
+ await task();
7849
+ });
7850
+ inflightStatusUpdate = request.catch(() => void 0);
7851
+ return request;
7852
+ };
7853
+ const scheduleRotation = () => {
7854
+ if (rotationTimer) {
7855
+ clearTimer(rotationTimer);
7856
+ rotationTimer = null;
7857
+ }
7858
+ if (!active || !currentVisibleStatus) {
7859
+ return;
7860
+ }
7861
+ rotationTimer = setTimer(() => {
7862
+ rotationTimer = null;
7863
+ if (!active || !currentVisibleStatus) {
7864
+ return;
7865
+ }
7866
+ void postRenderedStatus(currentStatus);
7867
+ }, STATUS_ROTATION_INTERVAL_MS);
7868
+ };
7869
+ const postStatus = async (text, suggestions) => {
7870
+ if (!text && !currentVisibleStatus) {
7871
+ return;
7872
+ }
7873
+ currentVisibleStatus = text;
7874
+ lastStatusAt = now();
7875
+ scheduleRotation();
7876
+ await enqueueStatusUpdate(async () => {
7877
+ await args.sendStatus(text, suggestions);
7878
+ });
7879
+ };
7880
+ const postRenderedStatus = async (status) => {
7881
+ const presentation = renderAssistantStatus({
7882
+ status,
7883
+ random
7884
+ });
7885
+ currentStatus = status;
7886
+ currentKey = presentation.key;
7887
+ await postStatus(presentation.visible, presentation.suggestions);
7888
+ };
7889
+ const clearPending = () => {
7890
+ if (pendingTimer) {
7891
+ clearTimer(pendingTimer);
7892
+ pendingTimer = null;
7893
+ }
7894
+ pendingStatus = null;
7895
+ pendingKey = "";
7896
+ };
7897
+ const flushPending = async () => {
7898
+ if (!active || !pendingStatus) {
7899
+ clearPending();
7900
+ return;
7901
+ }
7902
+ const next = pendingStatus;
7903
+ clearPending();
7904
+ const nextPresentation = renderAssistantStatus({
7905
+ status: next,
7906
+ random
7907
+ });
7908
+ if (nextPresentation.key !== currentKey) {
7909
+ await postRenderedStatus(next);
7910
+ }
7911
+ };
7912
+ return {
7913
+ start() {
7914
+ active = true;
7915
+ clearPending();
7916
+ currentStatus = makeAssistantStatus("thinking");
7917
+ currentKey = "";
7918
+ void postRenderedStatus(currentStatus);
7919
+ },
7920
+ async stop() {
7921
+ active = false;
7922
+ clearPending();
7923
+ if (rotationTimer) {
7924
+ clearTimer(rotationTimer);
7925
+ rotationTimer = null;
7926
+ }
7927
+ currentKey = "";
7928
+ await postStatus("");
7929
+ },
7930
+ update(status) {
7931
+ if (!active) {
7932
+ return;
7933
+ }
7934
+ const presentation = renderAssistantStatus({
7935
+ status,
7936
+ random
7937
+ });
7938
+ if (!presentation.visible) {
7939
+ return;
7940
+ }
7941
+ if (presentation.key === currentKey || presentation.key === pendingKey) {
7942
+ return;
7943
+ }
7944
+ const elapsed = now() - lastStatusAt;
7945
+ const waitMs = Math.max(
7946
+ STATUS_UPDATE_DEBOUNCE_MS - elapsed,
7947
+ STATUS_MIN_VISIBLE_MS - elapsed,
7948
+ 0
7949
+ );
7950
+ if (waitMs <= 0) {
7951
+ clearPending();
7952
+ void postRenderedStatus(status);
7953
+ return;
7954
+ }
7955
+ pendingStatus = status;
7956
+ pendingKey = presentation.key;
7957
+ if (pendingTimer) {
7958
+ return;
7959
+ }
7960
+ pendingTimer = setTimer(
7961
+ () => {
7962
+ pendingTimer = null;
7963
+ void flushPending();
7964
+ },
7965
+ Math.max(1, waitMs)
7966
+ );
7967
+ }
7968
+ };
7969
+ }
7970
+
7971
+ // src/chat/slack/assistant-thread/status-send.ts
7972
+ function createSlackAdapterStatusSender(args) {
7973
+ const adapter = args.getSlackAdapter();
7974
+ const boundToken = getSlackAdapterRequestToken(adapter);
7975
+ return async (text, suggestions) => {
7976
+ const channelId = args.channelId;
7977
+ const threadTs = args.threadTs;
7978
+ if (!channelId || !threadTs) {
7979
+ return;
7980
+ }
7981
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
7982
+ if (!normalizedChannelId) {
7983
+ return;
7984
+ }
7985
+ try {
7986
+ await runWithBoundSlackToken(
7987
+ adapter,
7988
+ boundToken,
7989
+ () => adapter.setAssistantStatus(
7990
+ normalizedChannelId,
7991
+ threadTs,
7992
+ text,
7993
+ suggestions
7994
+ )
7995
+ );
7996
+ } catch (error) {
7997
+ logAssistantStatusFailure({
7998
+ status: text,
7999
+ error,
8000
+ channelId,
8001
+ normalizedChannelId,
8002
+ threadTs
8003
+ });
7504
8004
  }
7505
8005
  };
7506
8006
  }
7507
- function createSlackWebApiAssistantStatusTransport(args) {
7508
- const getClient2 = args?.getSlackClient ?? getSlackClient;
7509
- return {
7510
- async setStatus(channelId, threadTs, status, suggestions) {
7511
- try {
7512
- await getClient2().assistant.threads.setStatus({
7513
- channel_id: channelId,
7514
- thread_ts: threadTs,
7515
- status,
7516
- ...suggestions ? { loading_messages: suggestions } : {}
7517
- });
7518
- } catch (error) {
7519
- logAssistantStatusFailure(status, error);
7520
- }
8007
+ function createSlackWebApiStatusSender(args) {
8008
+ const getClient2 = args.getSlackClient ?? getSlackClient;
8009
+ return async (text, suggestions) => {
8010
+ const channelId = args.channelId;
8011
+ const threadTs = args.threadTs;
8012
+ if (!channelId || !threadTs) {
8013
+ return;
8014
+ }
8015
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
8016
+ if (!normalizedChannelId) {
8017
+ return;
8018
+ }
8019
+ try {
8020
+ await getClient2().assistant.threads.setStatus({
8021
+ channel_id: normalizedChannelId,
8022
+ thread_ts: threadTs,
8023
+ status: text,
8024
+ ...suggestions ? { loading_messages: suggestions } : {}
8025
+ });
8026
+ } catch (error) {
8027
+ logAssistantStatusFailure({
8028
+ status: text,
8029
+ error,
8030
+ channelId,
8031
+ normalizedChannelId,
8032
+ threadTs
8033
+ });
7521
8034
  }
7522
8035
  };
7523
8036
  }
7524
- function logAssistantStatusFailure(status, error) {
8037
+ function getSlackAdapterRequestToken(adapter) {
8038
+ const token = adapter.requestContext?.getStore()?.token;
8039
+ if (typeof token !== "string") {
8040
+ return void 0;
8041
+ }
8042
+ const trimmed = token.trim();
8043
+ return trimmed || void 0;
8044
+ }
8045
+ async function runWithBoundSlackToken(adapter, token, task) {
8046
+ if (!token) {
8047
+ return await task();
8048
+ }
8049
+ return await adapter.withBotToken(token, task);
8050
+ }
8051
+ function logAssistantStatusFailure(args) {
7525
8052
  logWarn(
7526
8053
  "assistant_status_update_failed",
7527
8054
  {},
7528
8055
  {
7529
- "app.slack.status_text": status || "(clear)",
7530
- "error.message": error instanceof Error ? error.message : String(error)
8056
+ "app.slack.status_text": args.status || "(clear)",
8057
+ "app.slack.channel_id_raw": args.channelId,
8058
+ "app.slack.channel_id": args.normalizedChannelId,
8059
+ "app.slack.thread_ts": args.threadTs,
8060
+ "error.message": args.error instanceof Error ? args.error.message : String(args.error)
7531
8061
  },
7532
- "Failed to update assistant status"
8062
+ `Failed to update assistant status channel=${args.normalizedChannelId} raw=${args.channelId} thread=${args.threadTs}`
7533
8063
  );
7534
8064
  }
7535
8065
 
8066
+ // src/chat/slack/assistant-thread/status.ts
8067
+ function createSlackAdapterAssistantStatusSession(args) {
8068
+ return createAssistantStatusScheduler({
8069
+ sendStatus: createSlackAdapterStatusSender({
8070
+ channelId: args.channelId,
8071
+ threadTs: args.threadTs,
8072
+ getSlackAdapter: args.getSlackAdapter
8073
+ }),
8074
+ now: args.now,
8075
+ setTimer: args.setTimer,
8076
+ clearTimer: args.clearTimer,
8077
+ random: args.random
8078
+ });
8079
+ }
8080
+ function createSlackWebApiAssistantStatusSession(args) {
8081
+ return createAssistantStatusScheduler({
8082
+ sendStatus: createSlackWebApiStatusSender({
8083
+ channelId: args.channelId,
8084
+ threadTs: args.threadTs,
8085
+ getSlackClient: args.getSlackClient
8086
+ }),
8087
+ now: args.now,
8088
+ setTimer: args.setTimer,
8089
+ clearTimer: args.clearTimer,
8090
+ random: args.random
8091
+ });
8092
+ }
8093
+
7536
8094
  // src/chat/sandbox/skill-sync.ts
7537
8095
  import fs3 from "fs/promises";
7538
8096
  import path5 from "path";
@@ -7654,7 +8212,11 @@ function pickFields(record, csv) {
7654
8212
  }
7655
8213
 
7656
8214
  function outputJson(value) {
7657
- process.stdout.write(JSON.stringify(value, null, 2) + "\\n");
8215
+ fs.writeFileSync(process.stdout.fd, JSON.stringify(value, null, 2) + "\\n");
8216
+ }
8217
+
8218
+ function outputText(value) {
8219
+ fs.writeFileSync(process.stdout.fd, value);
7658
8220
  }
7659
8221
 
7660
8222
  function fallbackToRealGh() {
@@ -7670,12 +8232,12 @@ function fallbackToRealGh() {
7670
8232
  }
7671
8233
 
7672
8234
  if (args.length === 0 || args[0] === "--version" || args[0] === "version") {
7673
- process.stdout.write("gh version 2.0.0 (junior-eval)\\n");
8235
+ outputText("gh version 2.0.0 (junior-eval)\\n");
7674
8236
  process.exit(0);
7675
8237
  }
7676
8238
 
7677
8239
  if (args[0] === "auth" && args[1] === "status") {
7678
- process.stdout.write("github.com\\n \u2713 Logged in to github.com as junior-eval\\n");
8240
+ outputText("github.com\\n \u2713 Logged in to github.com as junior-eval\\n");
7679
8241
  process.exit(0);
7680
8242
  }
7681
8243
 
@@ -7699,7 +8261,7 @@ if (args[0] === "repo" && args[1] === "view") {
7699
8261
  if (jsonFields) {
7700
8262
  outputJson(pickFields(record, jsonFields));
7701
8263
  } else {
7702
- process.stdout.write(record.url + "\\n");
8264
+ outputText(record.url + "\\n");
7703
8265
  }
7704
8266
  process.exit(0);
7705
8267
  }
@@ -7751,7 +8313,7 @@ if (args[0] === "issue") {
7751
8313
  if (jsonFields) {
7752
8314
  outputJson(pickFields(record, jsonFields));
7753
8315
  } else {
7754
- process.stdout.write(record.url + "\\n");
8316
+ outputText(record.url + "\\n");
7755
8317
  }
7756
8318
  process.exit(0);
7757
8319
  }
@@ -7767,7 +8329,7 @@ if (args[0] === "issue") {
7767
8329
  if (jsonFields) {
7768
8330
  outputJson(pickFields(record, jsonFields));
7769
8331
  } else {
7770
- process.stdout.write(record.url + "\\n");
8332
+ outputText(record.url + "\\n");
7771
8333
  }
7772
8334
  process.exit(0);
7773
8335
  }
@@ -7784,7 +8346,7 @@ if (args[0] === "issue") {
7784
8346
  }
7785
8347
 
7786
8348
  if (subcommand === "comment") {
7787
- process.stdout.write(record.url + "#issuecomment-1\\n");
8349
+ outputText(record.url + "#issuecomment-1\\n");
7788
8350
  process.exit(0);
7789
8351
  }
7790
8352
 
@@ -8847,13 +9409,13 @@ function buildToolStatus(toolName, input) {
8847
9409
  return makeAssistantStatus("loading", skillName);
8848
9410
  }
8849
9411
  if (query && toolName === "webSearch") {
8850
- return makeAssistantStatus("searching", `"${query}"`);
8851
- }
8852
- if (query && provider && toolName === "searchTools") {
8853
- return makeAssistantStatus("searching", `${provider} "${query}"`);
9412
+ return makeAssistantStatus("searching", "sources");
8854
9413
  }
8855
9414
  if (query && toolName === "searchTools") {
8856
- return makeAssistantStatus("searching", `"${query}"`);
9415
+ return makeAssistantStatus(
9416
+ "searching",
9417
+ provider ? `${provider} tools` : "tools"
9418
+ );
8857
9419
  }
8858
9420
  if (domain && toolName === "webFetch") {
8859
9421
  return makeAssistantStatus("fetching", domain);
@@ -9115,7 +9677,6 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
9115
9677
  // src/chat/services/reply-delivery-plan.ts
9116
9678
  var REACTION_ONLY_ACK_RE = /^(?::[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D]+)$/u;
9117
9679
  var REDUNDANT_REACTION_ACK_TEXT = ["done", "got it", "ok", "okay"];
9118
- var REACTION_ALIAS_PREFIX_RE = /^:[a-z0-9_+-]*$/i;
9119
9680
  function normalizeReactionAckText(text) {
9120
9681
  return text.trim().toLowerCase().replace(/[!.]+$/g, "");
9121
9682
  }
@@ -9132,24 +9693,11 @@ function isRedundantReactionAckText(text) {
9132
9693
  normalized
9133
9694
  );
9134
9695
  }
9135
- function isPotentialRedundantReactionAckText(text) {
9136
- const trimmed = text.trim();
9137
- if (!trimmed) {
9138
- return true;
9139
- }
9140
- if (REACTION_ONLY_ACK_RE.test(trimmed) || REACTION_ALIAS_PREFIX_RE.test(trimmed)) {
9141
- return true;
9142
- }
9143
- const normalized = normalizeReactionAckText(text);
9144
- return REDUNDANT_REACTION_ACK_TEXT.some(
9145
- (candidate) => candidate.startsWith(normalized)
9146
- );
9147
- }
9148
9696
  function buildReplyDeliveryPlan(args) {
9149
9697
  const mode = args.explicitChannelPostIntent && args.channelPostPerformed ? "channel_only" : "thread";
9150
9698
  let attachFiles = "none";
9151
9699
  if (args.hasFiles && mode === "thread") {
9152
- attachFiles = args.streamingThreadReply ? "followup" : "inline";
9700
+ attachFiles = "inline";
9153
9701
  }
9154
9702
  return {
9155
9703
  mode,
@@ -9182,6 +9730,10 @@ function sentenceClaimsAttachment(sentence) {
9182
9730
  if (!hasAttachmentNoun) {
9183
9731
  return false;
9184
9732
  }
9733
+ const hasNegativeAttachmentPhrase = /\bno (?:screenshot|image|file|attachment)\b/i.test(sentence) || /\b(?:isn['’]t|is not|wasn['’]t|was not)\s+attached\b/i.test(sentence) || /\bwithout (?:an? )?(?:screenshot|image|file|attachment)\b/i.test(sentence);
9734
+ if (hasNegativeAttachmentPhrase) {
9735
+ return false;
9736
+ }
9185
9737
  const hasPositiveAttachmentVerb = /\b(attached|shared|uploaded|included)\b/i.test(sentence);
9186
9738
  const hasDeicticSharePhrase = /\bhere(?:'s| is)\b/i.test(sentence);
9187
9739
  return hasPositiveAttachmentVerb || hasDeicticSharePhrase;
@@ -9210,7 +9762,6 @@ function buildTurnResult(input) {
9210
9762
  toolCalls,
9211
9763
  sandboxId,
9212
9764
  sandboxDependencyProfileHash,
9213
- hasTextDeltaCallback,
9214
9765
  shouldTrace,
9215
9766
  spanContext,
9216
9767
  correlation,
@@ -9218,7 +9769,8 @@ function buildTurnResult(input) {
9218
9769
  } = input;
9219
9770
  const toolResults = newMessages.filter(isToolResultMessage);
9220
9771
  const assistantMessages = newMessages.filter(isAssistantMessage);
9221
- const primaryText = assistantMessages.map((message) => extractAssistantText(message)).join("\n\n").trim();
9772
+ const terminalAssistantMessages = getTerminalAssistantMessages(newMessages);
9773
+ const primaryText = terminalAssistantMessages.map((message) => extractAssistantText(message)).join("\n\n").trim();
9222
9774
  const oauthStartedMessage = extractOAuthStartedMessageFromToolResults(toolResults);
9223
9775
  const toolErrorCount = toolResults.filter((result) => result.isError).length;
9224
9776
  const explicitChannelPostIntent = isExplicitChannelPostIntent(userInput);
@@ -9231,8 +9783,7 @@ function buildTurnResult(input) {
9231
9783
  const deliveryPlan = buildReplyDeliveryPlan({
9232
9784
  explicitChannelPostIntent,
9233
9785
  channelPostPerformed,
9234
- hasFiles: replyFiles.length > 0,
9235
- streamingThreadReply: hasTextDeltaCallback
9786
+ hasFiles: replyFiles.length > 0
9236
9787
  });
9237
9788
  const deliveryMode = deliveryPlan.mode;
9238
9789
  if (!primaryText && !oauthStartedMessage) {
@@ -9254,7 +9805,7 @@ function buildTurnResult(input) {
9254
9805
  "Model returned empty text response"
9255
9806
  );
9256
9807
  }
9257
- const lastAssistant = assistantMessages.at(-1);
9808
+ const lastAssistant = terminalAssistantMessages.at(-1);
9258
9809
  const stopReason = typeof lastAssistant?.stopReason === "string" ? lastAssistant.stopReason : void 0;
9259
9810
  const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
9260
9811
  const usedPrimaryText = Boolean(primaryText);
@@ -9580,6 +10131,16 @@ function createMcpAuthOrchestration(deps, abortAgent) {
9580
10131
 
9581
10132
  // src/chat/respond.ts
9582
10133
  var startupDiscoveryLogged = false;
10134
+ function buildOmittedImageAttachmentNotice(count) {
10135
+ return [
10136
+ "<omitted-image-attachments>",
10137
+ `count: ${count}`,
10138
+ "Slack included image attachments with this turn, but this runtime cannot analyze images because no vision model is configured.",
10139
+ "Do not claim that no image was attached.",
10140
+ "If the user asks about image contents, explain that image analysis is unavailable in this runtime and continue with any text or non-image files that are still available.",
10141
+ "</omitted-image-attachments>"
10142
+ ].join("\n");
10143
+ }
9583
10144
  function mcpToolsToDefinitions(mcpTools) {
9584
10145
  const defs = {};
9585
10146
  for (const tool2 of mcpTools) {
@@ -9651,6 +10212,8 @@ async function generateAssistantReply(messageText, context = {}) {
9651
10212
  let configurationValues;
9652
10213
  const userInput = messageText;
9653
10214
  if (shouldTrace) {
10215
+ const inboundAttachmentCount = context.inboundAttachmentCount ?? 0;
10216
+ const promptAttachmentCount = context.userAttachments?.length ?? 0;
9654
10217
  logInfo(
9655
10218
  "agent_message_in",
9656
10219
  spanContext,
@@ -9658,7 +10221,10 @@ async function generateAssistantReply(messageText, context = {}) {
9658
10221
  "app.message.kind": "user_inbound",
9659
10222
  "app.message.length": userInput.length,
9660
10223
  "app.message.input": summarizeMessageText(userInput),
9661
- "app.message.attachment_count": context.userAttachments?.length ?? 0,
10224
+ // Log both counts so image uploads filtered by vision/config do not
10225
+ // look indistinguishable from Slack ingress dropping attachments.
10226
+ "app.message.attachment_count": inboundAttachmentCount,
10227
+ "app.message.prompt_attachment_count": promptAttachmentCount,
9662
10228
  "messaging.message.id": context.correlation?.messageTs ?? ""
9663
10229
  },
9664
10230
  "Agent message received"
@@ -9911,6 +10477,13 @@ async function generateAssistantReply(messageText, context = {}) {
9911
10477
  threadParticipants: context.threadParticipants
9912
10478
  });
9913
10479
  const userContentParts = [{ type: "text", text: userTurnText }];
10480
+ const omittedImageAttachmentCount = context.omittedImageAttachmentCount ?? 0;
10481
+ if (omittedImageAttachmentCount > 0) {
10482
+ userContentParts.push({
10483
+ type: "text",
10484
+ text: buildOmittedImageAttachmentNotice(omittedImageAttachmentCount)
10485
+ });
10486
+ }
9914
10487
  for (const attachment of context.userAttachments ?? []) {
9915
10488
  if (attachment.promptText) {
9916
10489
  userContentParts.push({
@@ -9954,6 +10527,17 @@ async function generateAssistantReply(messageText, context = {}) {
9954
10527
  const agentToolHooks = {
9955
10528
  onToolCall: (toolName) => {
9956
10529
  toolCalls.push(toolName);
10530
+ Promise.resolve(context.onToolCall?.(toolName)).catch((error) => {
10531
+ logWarn(
10532
+ "streaming_tool_call_error",
10533
+ {},
10534
+ {
10535
+ "error.message": error instanceof Error ? error.message : String(error),
10536
+ "gen_ai.tool.name": toolName
10537
+ },
10538
+ "Failed to deliver tool call event to stream coordinator"
10539
+ );
10540
+ });
9957
10541
  }
9958
10542
  };
9959
10543
  const baseAgentTools = createAgentTools(
@@ -9994,6 +10578,16 @@ async function generateAssistantReply(messageText, context = {}) {
9994
10578
  let needsSeparator = false;
9995
10579
  const unsubscribe = agent.subscribe((event) => {
9996
10580
  if (event.type === "message_start") {
10581
+ Promise.resolve(context.onAssistantMessageStart?.()).catch((error) => {
10582
+ logWarn(
10583
+ "streaming_message_start_error",
10584
+ {},
10585
+ {
10586
+ "error.message": error instanceof Error ? error.message : String(error)
10587
+ },
10588
+ "Failed to deliver assistant message start to stream coordinator"
10589
+ );
10590
+ });
9997
10591
  if (hasEmittedText) {
9998
10592
  needsSeparator = true;
9999
10593
  }
@@ -10031,7 +10625,12 @@ async function generateAssistantReply(messageText, context = {}) {
10031
10625
  spanContext,
10032
10626
  async () => {
10033
10627
  let promptResult;
10034
- const promptPromise = resumedFromCheckpoint ? agent.continue() : agent.prompt({
10628
+ const promptPromise = resumedFromCheckpoint ? (
10629
+ // Checkpoint resumes continue from the persisted Pi message
10630
+ // state. Any reconstructed replyContext only matters when the
10631
+ // turn parked before the initial user prompt was recorded.
10632
+ agent.continue()
10633
+ ) : agent.prompt({
10035
10634
  role: "user",
10036
10635
  content: userContentParts,
10037
10636
  timestamp: Date.now()
@@ -10126,7 +10725,6 @@ async function generateAssistantReply(messageText, context = {}) {
10126
10725
  sandboxId: currentSandboxExecutor.getSandboxId(),
10127
10726
  sandboxDependencyProfileHash: currentSandboxExecutor.getDependencyProfileHash(),
10128
10727
  generatedFileCount: generatedFiles.length,
10129
- hasTextDeltaCallback: Boolean(context.onTextDelta),
10130
10728
  shouldTrace,
10131
10729
  spanContext,
10132
10730
  correlation: context.correlation,
@@ -10239,153 +10837,122 @@ async function generateAssistantReply(messageText, context = {}) {
10239
10837
  }
10240
10838
  }
10241
10839
 
10242
- // src/chat/runtime/progress-reporter.ts
10243
- var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
10244
- var STATUS_MIN_VISIBLE_MS = 1200;
10245
- var STATUS_ROTATION_INTERVAL_MS = 3e4;
10246
- function createProgressReporter(args) {
10247
- const now = args.now ?? (() => Date.now());
10248
- const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
10249
- const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
10250
- const random = args.random ?? Math.random;
10251
- let active = false;
10252
- let currentKey = "";
10253
- let currentStatus = makeAssistantStatus("thinking");
10254
- let currentVisibleStatus = "";
10255
- let lastStatusAt = 0;
10256
- let pendingStatus = null;
10257
- let pendingKey = "";
10258
- let pendingTimer = null;
10259
- let rotationTimer = null;
10260
- let inflightStatusUpdate = Promise.resolve();
10261
- const scheduleRotation = () => {
10262
- if (rotationTimer) {
10263
- clearTimer(rotationTimer);
10264
- rotationTimer = null;
10265
- }
10266
- if (!active || !currentVisibleStatus) {
10267
- return;
10268
- }
10269
- rotationTimer = setTimer(() => {
10270
- rotationTimer = null;
10271
- if (!active || !currentVisibleStatus) {
10272
- return;
10273
- }
10274
- void postRenderedStatus(currentStatus);
10275
- }, STATUS_ROTATION_INTERVAL_MS);
10276
- };
10277
- const postStatus = async (text, suggestions) => {
10278
- const channelId = args.channelId;
10279
- const threadTs = args.threadTs;
10280
- if (!channelId || !threadTs) {
10281
- return;
10282
- }
10283
- if (!text && !currentVisibleStatus) {
10284
- return;
10285
- }
10286
- currentVisibleStatus = text;
10287
- lastStatusAt = now();
10288
- scheduleRotation();
10289
- const previous = inflightStatusUpdate;
10290
- const request = (async () => {
10291
- await previous;
10292
- await args.transport.setStatus(channelId, threadTs, text, suggestions);
10293
- })();
10294
- inflightStatusUpdate = request;
10295
- await request;
10296
- };
10297
- const postRenderedStatus = async (status) => {
10298
- const presentation = buildAssistantStatusPresentation({
10299
- status,
10300
- random
10301
- });
10302
- currentStatus = status;
10303
- currentKey = presentation.key;
10304
- await postStatus(presentation.visible, presentation.suggestions);
10305
- };
10306
- const clearPending = () => {
10307
- if (pendingTimer) {
10308
- clearTimer(pendingTimer);
10309
- pendingTimer = null;
10310
- }
10311
- pendingStatus = null;
10312
- pendingKey = "";
10313
- };
10314
- const flushPending = async () => {
10315
- if (!active || !pendingStatus) {
10316
- clearPending();
10317
- return;
10318
- }
10319
- const next = pendingStatus;
10320
- clearPending();
10321
- const nextPresentation = buildAssistantStatusPresentation({
10322
- status: next,
10323
- random
10324
- });
10325
- if (nextPresentation.key !== currentKey) {
10326
- await postRenderedStatus(next);
10327
- }
10840
+ // src/chat/slack/reply.ts
10841
+ import { Buffer as Buffer2 } from "buffer";
10842
+ function isInterruptedVisibleReply(reply) {
10843
+ return reply.diagnostics.outcome === "provider_error";
10844
+ }
10845
+ function resolveReplyDelivery(reply) {
10846
+ const replyHasFiles = Boolean(reply.files && reply.files.length > 0);
10847
+ const deliveryPlan = reply.deliveryPlan ?? {
10848
+ mode: reply.deliveryMode ?? "thread",
10849
+ postThreadText: (reply.deliveryMode ?? "thread") !== "channel_only",
10850
+ attachFiles: replyHasFiles ? "inline" : "none"
10328
10851
  };
10329
10852
  return {
10330
- async start() {
10331
- active = true;
10332
- clearPending();
10333
- currentStatus = makeAssistantStatus("thinking");
10334
- currentKey = "";
10335
- void postRenderedStatus(currentStatus);
10336
- },
10337
- async stop() {
10338
- active = false;
10339
- clearPending();
10340
- if (rotationTimer) {
10341
- clearTimer(rotationTimer);
10342
- rotationTimer = null;
10343
- }
10344
- currentKey = "";
10345
- await postStatus("");
10346
- },
10347
- async setStatus(status) {
10348
- if (!active) {
10349
- return;
10350
- }
10351
- const presentation = buildAssistantStatusPresentation({
10352
- status,
10353
- random
10354
- });
10355
- if (!presentation.visible) {
10356
- return;
10357
- }
10358
- if (presentation.key === currentKey || presentation.key === pendingKey) {
10359
- return;
10360
- }
10361
- const elapsed = now() - lastStatusAt;
10362
- const waitMs = Math.max(
10363
- STATUS_UPDATE_DEBOUNCE_MS - elapsed,
10364
- STATUS_MIN_VISIBLE_MS - elapsed,
10365
- 0
10366
- );
10367
- if (waitMs <= 0) {
10368
- clearPending();
10369
- void postRenderedStatus(status);
10370
- return;
10371
- }
10372
- pendingStatus = status;
10373
- pendingKey = presentation.key;
10374
- if (pendingTimer) {
10375
- return;
10853
+ shouldPostThreadReply: deliveryPlan.postThreadText,
10854
+ attachFiles: replyHasFiles && deliveryPlan.attachFiles !== "none" ? "inline" : "none"
10855
+ };
10856
+ }
10857
+ function buildReplyText(text) {
10858
+ const message = buildSlackOutputMessage(text);
10859
+ if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
10860
+ return message.markdown;
10861
+ }
10862
+ if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
10863
+ return message.raw;
10864
+ }
10865
+ return "";
10866
+ }
10867
+ function buildTextPosts(args) {
10868
+ const chunks = splitSlackReplyText(args.text, {
10869
+ interrupted: args.interrupted
10870
+ });
10871
+ return chunks.map((chunk, index) => ({
10872
+ text: chunk,
10873
+ ...index === 0 && args.firstFiles ? { files: args.firstFiles } : {},
10874
+ stage: index === 0 ? args.firstStage ?? "thread_reply" : "thread_reply_continuation"
10875
+ }));
10876
+ }
10877
+ async function normalizeFileUploads(files) {
10878
+ return await Promise.all(
10879
+ files.map(async (file) => {
10880
+ let data;
10881
+ if (Buffer2.isBuffer(file.data)) {
10882
+ data = file.data;
10883
+ } else if (file.data instanceof ArrayBuffer) {
10884
+ data = Buffer2.from(file.data);
10885
+ } else {
10886
+ data = Buffer2.from(await file.data.arrayBuffer());
10376
10887
  }
10377
- pendingTimer = setTimer(
10378
- () => {
10379
- pendingTimer = null;
10380
- void flushPending();
10381
- },
10382
- Math.max(1, waitMs)
10383
- );
10888
+ return {
10889
+ data,
10890
+ filename: file.filename
10891
+ };
10892
+ })
10893
+ );
10894
+ }
10895
+ async function uploadReplyFilesBestEffort(args) {
10896
+ try {
10897
+ await uploadFilesToThread({
10898
+ channelId: args.channelId,
10899
+ threadTs: args.threadTs,
10900
+ files: await normalizeFileUploads(args.files)
10901
+ });
10902
+ } catch {
10903
+ }
10904
+ }
10905
+ function planSlackReplyPosts(args) {
10906
+ const replyFiles = args.reply.files && args.reply.files.length > 0 ? args.reply.files : void 0;
10907
+ const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery(
10908
+ args.reply
10909
+ );
10910
+ const interrupted = isInterruptedVisibleReply(args.reply);
10911
+ const posts = [];
10912
+ const textPosts = shouldPostThreadReply ? buildTextPosts({
10913
+ text: args.reply.text,
10914
+ interrupted,
10915
+ firstFiles: attachFiles === "inline" ? replyFiles : void 0
10916
+ }) : [];
10917
+ posts.push(...textPosts);
10918
+ if (attachFiles === "inline" && replyFiles && textPosts.length === 0) {
10919
+ posts.push({
10920
+ files: replyFiles,
10921
+ stage: "thread_reply",
10922
+ text: ""
10923
+ });
10924
+ } else if (shouldPostThreadReply && textPosts.length === 0) {
10925
+ posts.push({
10926
+ text: buildReplyText(args.reply.text),
10927
+ stage: "thread_reply"
10928
+ });
10929
+ }
10930
+ if (attachFiles === "followup" && replyFiles) {
10931
+ posts.push({
10932
+ files: replyFiles,
10933
+ stage: "thread_reply_files_followup",
10934
+ text: ""
10935
+ });
10936
+ }
10937
+ return posts;
10938
+ }
10939
+ async function postSlackApiReplyPosts(args) {
10940
+ for (const post of args.posts) {
10941
+ if (post.text.trim().length > 0) {
10942
+ await args.postMessage(args.channelId, args.threadTs, post.text);
10384
10943
  }
10385
- };
10944
+ if (!post.files?.length) {
10945
+ continue;
10946
+ }
10947
+ await uploadReplyFilesBestEffort({
10948
+ channelId: args.channelId,
10949
+ threadTs: args.threadTs,
10950
+ files: post.files
10951
+ });
10952
+ }
10386
10953
  }
10387
10954
 
10388
- // src/handlers/oauth-resume.ts
10955
+ // src/chat/slack/resume.ts
10389
10956
  function resolveReplyTimeoutMs(explicitTimeoutMs) {
10390
10957
  if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
10391
10958
  return explicitTimeoutMs;
@@ -10397,16 +10964,16 @@ function resolveReplyTimeoutMs(explicitTimeoutMs) {
10397
10964
  const parsed = Number.parseInt(raw, 10);
10398
10965
  return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
10399
10966
  }
10400
- async function postSlackMessage(channelId, threadTs, text) {
10401
- await getSlackClient().chat.postMessage({
10402
- channel: channelId,
10403
- thread_ts: threadTs,
10967
+ async function postSlackMessage2(channelId, threadTs, text) {
10968
+ await postSlackMessage({
10969
+ channelId,
10970
+ threadTs,
10404
10971
  text
10405
10972
  });
10406
10973
  }
10407
10974
  async function postSlackMessageBestEffort(channelId, threadTs, text) {
10408
10975
  try {
10409
- await postSlackMessage(channelId, threadTs, text);
10976
+ await postSlackMessage2(channelId, threadTs, text);
10410
10977
  } catch {
10411
10978
  }
10412
10979
  }
@@ -10445,7 +11012,7 @@ var ResumeTurnBusyError = class extends Error {
10445
11012
  function getDefaultLockKey(channelId, threadTs) {
10446
11013
  return `slack:${channelId}:${threadTs}`;
10447
11014
  }
10448
- function createResumeReplyContext(args, progress) {
11015
+ function createResumeReplyContext(args, statusSession) {
10449
11016
  const replyContext = args.replyContext ?? {};
10450
11017
  const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
10451
11018
  const persistedChannelConfiguration = replyContext.channelConfiguration ?? (replyContext.configuration ? createReadOnlyConfigService(replyContext.configuration) : void 0);
@@ -10474,9 +11041,9 @@ function createResumeReplyContext(args, progress) {
10474
11041
  await persistThreadStateById(threadId, { artifacts });
10475
11042
  await replyContext.onArtifactStateUpdated?.(artifacts);
10476
11043
  },
10477
- onStatus: async (status) => {
10478
- await progress.setStatus(status);
10479
- await replyContext.onStatus?.(status);
11044
+ onStatus: async (nextStatus) => {
11045
+ statusSession.update(nextStatus);
11046
+ await replyContext.onStatus?.(nextStatus);
10480
11047
  }
10481
11048
  };
10482
11049
  }
@@ -10495,10 +11062,9 @@ async function resumeSlackTurn(args) {
10495
11062
  if (!lock) {
10496
11063
  throw new ResumeTurnBusyError(lockKey);
10497
11064
  }
10498
- const progress = createProgressReporter({
11065
+ const status = createSlackWebApiAssistantStatusSession({
10499
11066
  channelId: args.channelId,
10500
- threadTs: args.threadTs,
10501
- transport: createSlackWebApiAssistantStatusTransport()
11067
+ threadTs: args.threadTs
10502
11068
  });
10503
11069
  let deferredPauseHandler;
10504
11070
  let deferredFailureHandler;
@@ -10510,9 +11076,9 @@ async function resumeSlackTurn(args) {
10510
11076
  args.initialText
10511
11077
  );
10512
11078
  }
10513
- await progress.start();
11079
+ status.start();
10514
11080
  const generateReply = args.generateReply ?? generateAssistantReply;
10515
- const replyContext = createResumeReplyContext(args, progress);
11081
+ const replyContext = createResumeReplyContext(args, status);
10516
11082
  const replyPromise = generateReply(args.messageText, {
10517
11083
  ...replyContext
10518
11084
  });
@@ -10530,15 +11096,16 @@ async function resumeSlackTurn(args) {
10530
11096
  )
10531
11097
  )
10532
11098
  ]) : await replyPromise;
10533
- await progress.stop();
10534
- if (args.onReply) {
10535
- await args.onReply(reply);
10536
- } else if (reply.text) {
10537
- await postSlackMessage(args.channelId, args.threadTs, reply.text);
10538
- }
11099
+ await status.stop();
11100
+ await postSlackApiReplyPosts({
11101
+ channelId: args.channelId,
11102
+ threadTs: args.threadTs,
11103
+ posts: planSlackReplyPosts({ reply }),
11104
+ postMessage: postSlackMessage2
11105
+ });
10539
11106
  await args.onSuccess?.(reply);
10540
11107
  } catch (error) {
10541
- await progress.stop();
11108
+ await status.stop();
10542
11109
  if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
10543
11110
  deferredPauseHandler = async () => {
10544
11111
  await args.onAuthPause?.(error);
@@ -10592,7 +11159,6 @@ async function resumeAuthorizedRequest(args) {
10592
11159
  initialText: args.connectedText,
10593
11160
  failureText: args.failureText,
10594
11161
  generateReply: args.generateReply,
10595
- onReply: args.onReply,
10596
11162
  onSuccess: args.onSuccess,
10597
11163
  onFailure: args.onFailure,
10598
11164
  onAuthPause: args.onAuthPause,
@@ -10756,65 +11322,6 @@ function htmlResponse(kind) {
10756
11322
  const page = CALLBACK_PAGES[kind];
10757
11323
  return htmlCallbackResponse(page.title, page.message, page.status);
10758
11324
  }
10759
- function extractSlackText(text, files) {
10760
- const message = buildSlackOutputMessage(text, files);
10761
- if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
10762
- return message.markdown;
10763
- }
10764
- if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
10765
- return message.raw;
10766
- }
10767
- return text;
10768
- }
10769
- async function normalizeFileUploads(files) {
10770
- const normalized = [];
10771
- for (const file of files) {
10772
- let data;
10773
- if (Buffer2.isBuffer(file.data)) {
10774
- data = file.data;
10775
- } else if (file.data instanceof ArrayBuffer) {
10776
- data = Buffer2.from(file.data);
10777
- } else {
10778
- data = Buffer2.from(await file.data.arrayBuffer());
10779
- }
10780
- normalized.push({
10781
- data,
10782
- filename: file.filename
10783
- });
10784
- }
10785
- return normalized;
10786
- }
10787
- async function deliverReplyToThread(channelId, threadTs, reply) {
10788
- const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
10789
- const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
10790
- reply,
10791
- hasStreamedThreadReply: false
10792
- });
10793
- if (shouldPostThreadReply) {
10794
- const text = extractSlackText(
10795
- reply.text,
10796
- attachFiles === "inline" ? replyFiles : void 0
10797
- );
10798
- if (text.trim().length > 0) {
10799
- await postSlackMessage(channelId, threadTs, text);
10800
- }
10801
- }
10802
- if (!replyFiles || attachFiles === "none") {
10803
- return;
10804
- }
10805
- const files = await normalizeFileUploads(replyFiles);
10806
- if (files.length === 0) {
10807
- return;
10808
- }
10809
- try {
10810
- await uploadFilesToThread({
10811
- channelId,
10812
- threadTs,
10813
- files
10814
- });
10815
- } catch {
10816
- }
10817
- }
10818
11325
  async function buildResumeConversationContext(channelId, threadTs, sessionId) {
10819
11326
  const threadId = `slack:${channelId}:${threadTs}`;
10820
11327
  const conversation = coerceThreadConversationState(
@@ -10899,7 +11406,6 @@ async function resumeAuthorizedMcpTurn(args) {
10899
11406
  );
10900
11407
  await resumeAuthorizedRequest({
10901
11408
  messageText: authSession.userMessage,
10902
- provider,
10903
11409
  channelId: authSession.channelId,
10904
11410
  threadTs: authSession.threadTs,
10905
11411
  lockKey: authSession.conversationId,
@@ -10925,14 +11431,8 @@ async function resumeAuthorizedMcpTurn(args) {
10925
11431
  configuration: authSession.configuration,
10926
11432
  channelConfiguration,
10927
11433
  sandbox: getPersistedSandboxState(currentState),
10928
- threadParticipants: buildThreadParticipants(conversation.messages)
10929
- },
10930
- onReply: async (reply) => {
10931
- await deliverReplyToThread(
10932
- authSession.channelId,
10933
- authSession.threadTs,
10934
- reply
10935
- );
11434
+ threadParticipants: buildThreadParticipants(conversation.messages),
11435
+ ...getTurnUserReplyAttachmentContext(userMessage)
10936
11436
  },
10937
11437
  onSuccess: async (reply) => {
10938
11438
  try {
@@ -11230,7 +11730,6 @@ async function resumePendingOAuthMessage(stored) {
11230
11730
  );
11231
11731
  await resumeAuthorizedRequest({
11232
11732
  messageText: stored.pendingMessage,
11233
- provider: stored.provider,
11234
11733
  channelId: stored.channelId,
11235
11734
  threadTs: stored.threadTs,
11236
11735
  connectedText: `Your ${providerLabel} account is now connected. Processing your request...`,
@@ -11404,7 +11903,7 @@ async function GET5(request, provider, waitUntil) {
11404
11903
  } else if (stored.channelId && stored.threadTs) {
11405
11904
  const { channelId, threadTs } = stored;
11406
11905
  waitUntil(
11407
- () => postSlackMessage(
11906
+ () => postSlackMessage2(
11408
11907
  channelId,
11409
11908
  threadTs,
11410
11909
  `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
@@ -11428,9 +11927,6 @@ async function GET5(request, provider, waitUntil) {
11428
11927
  });
11429
11928
  }
11430
11929
 
11431
- // src/handlers/turn-resume.ts
11432
- import { Buffer as Buffer3 } from "buffer";
11433
-
11434
11930
  // src/chat/slack/context.ts
11435
11931
  function toTrimmedSlackString(value) {
11436
11932
  const normalized = toOptionalString(value);
@@ -11478,65 +11974,6 @@ function resolveSlackChannelIdFromMessage(message) {
11478
11974
  }
11479
11975
 
11480
11976
  // src/handlers/turn-resume.ts
11481
- function extractSlackText2(text, files) {
11482
- const message = buildSlackOutputMessage(text, files);
11483
- if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
11484
- return message.markdown;
11485
- }
11486
- if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
11487
- return message.raw;
11488
- }
11489
- return text;
11490
- }
11491
- async function normalizeFileUploads2(files) {
11492
- const normalized = [];
11493
- for (const file of files) {
11494
- let data;
11495
- if (Buffer3.isBuffer(file.data)) {
11496
- data = file.data;
11497
- } else if (file.data instanceof ArrayBuffer) {
11498
- data = Buffer3.from(file.data);
11499
- } else {
11500
- data = Buffer3.from(await file.data.arrayBuffer());
11501
- }
11502
- normalized.push({
11503
- data,
11504
- filename: file.filename
11505
- });
11506
- }
11507
- return normalized;
11508
- }
11509
- async function deliverReplyToThread2(args) {
11510
- const replyFiles = args.reply.files && args.reply.files.length > 0 ? args.reply.files : void 0;
11511
- const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
11512
- reply: args.reply,
11513
- hasStreamedThreadReply: false
11514
- });
11515
- if (shouldPostThreadReply) {
11516
- const text = extractSlackText2(
11517
- args.reply.text,
11518
- attachFiles === "inline" ? replyFiles : void 0
11519
- );
11520
- if (text.trim().length > 0) {
11521
- await postSlackMessage(args.channelId, args.threadTs, text);
11522
- }
11523
- }
11524
- if (!replyFiles || attachFiles === "none") {
11525
- return;
11526
- }
11527
- const files = await normalizeFileUploads2(replyFiles);
11528
- if (files.length === 0) {
11529
- return;
11530
- }
11531
- try {
11532
- await uploadFilesToThread({
11533
- channelId: args.channelId,
11534
- threadTs: args.threadTs,
11535
- files
11536
- });
11537
- } catch {
11538
- }
11539
- }
11540
11977
  async function persistCompletedReplyState2(args) {
11541
11978
  const currentState = await getPersistedThreadState(
11542
11979
  args.checkpoint.conversationId
@@ -11649,14 +12086,8 @@ async function resumeTimedOutTurn(payload) {
11649
12086
  conversationContext,
11650
12087
  channelConfiguration,
11651
12088
  sandbox,
11652
- threadParticipants: buildThreadParticipants(conversation.messages)
11653
- },
11654
- onReply: async (reply) => {
11655
- await deliverReplyToThread2({
11656
- channelId: thread.channelId,
11657
- threadTs: thread.threadTs,
11658
- reply
11659
- });
12089
+ threadParticipants: buildThreadParticipants(conversation.messages),
12090
+ ...getTurnUserReplyAttachmentContext(userMessage)
11660
12091
  },
11661
12092
  onSuccess: async (reply) => {
11662
12093
  try {
@@ -11766,9 +12197,6 @@ async function POST(request, waitUntil) {
11766
12197
  return new Response("Accepted", { status: 202 });
11767
12198
  }
11768
12199
 
11769
- // src/chat/app/production.ts
11770
- import { createSlackAdapter } from "@chat-adapter/slack";
11771
-
11772
12200
  // src/chat/services/subscribed-decision.ts
11773
12201
  import { z } from "zod";
11774
12202
  var replyDecisionSchema = z.object({
@@ -12116,11 +12544,21 @@ function getRunId(thread, message) {
12116
12544
  return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
12117
12545
  }
12118
12546
  function getChannelId(thread, message) {
12119
- return thread.channelId ?? resolveSlackChannelIdFromMessage(message);
12547
+ return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
12120
12548
  }
12121
12549
  function getThreadTs(threadId) {
12122
12550
  return parseSlackThreadId(threadId)?.threadTs;
12123
12551
  }
12552
+ function getAssistantThreadContext(message) {
12553
+ const raw = message.raw;
12554
+ const rawRecord = raw && typeof raw === "object" ? raw : void 0;
12555
+ const channelId = toOptionalString(rawRecord?.channel);
12556
+ const threadTs = toOptionalString(rawRecord?.thread_ts);
12557
+ if (!channelId || !threadTs) {
12558
+ return void 0;
12559
+ }
12560
+ return { channelId, threadTs };
12561
+ }
12124
12562
  function getMessageTs(message) {
12125
12563
  const directTs = toOptionalString(
12126
12564
  message.ts
@@ -12473,7 +12911,7 @@ function createSlackTurnRuntime(deps) {
12473
12911
  },
12474
12912
  async handleAssistantContextChanged(event) {
12475
12913
  try {
12476
- await deps.initializeAssistantThread({
12914
+ await deps.refreshAssistantThreadContext({
12477
12915
  threadId: event.threadId,
12478
12916
  channelId: event.channelId,
12479
12917
  threadTs: event.threadTs,
@@ -12615,6 +13053,18 @@ var MAX_USER_ATTACHMENTS = 3;
12615
13053
  var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
12616
13054
  var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
12617
13055
  var MAX_VISION_SUMMARY_CHARS = 500;
13056
+ function hasPotentialImageAttachment(attachments) {
13057
+ return countPotentialImageAttachments(attachments) > 0;
13058
+ }
13059
+ function countPotentialImageAttachments(attachments) {
13060
+ return attachments?.filter((attachment) => {
13061
+ if (attachment.type === "image") {
13062
+ return true;
13063
+ }
13064
+ const mimeType = attachment.mimeType ?? "";
13065
+ return attachment.type === "file" && mimeType.startsWith("image/");
13066
+ }).length ?? 0;
13067
+ }
12618
13068
  function isVisionEnabled() {
12619
13069
  return Boolean(botConfig.visionModelId);
12620
13070
  }
@@ -12957,6 +13407,7 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
12957
13407
  continue;
12958
13408
  }
12959
13409
  hydratedMessageIds.add(conversationMessage.id);
13410
+ const existingMeta = conversationMessage.meta ?? {};
12960
13411
  const imageFiles = (reply.files ?? []).filter((file) => {
12961
13412
  const mimeType = toOptionalString(file.mimetype);
12962
13413
  return Boolean(
@@ -12964,10 +13415,15 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
12964
13415
  );
12965
13416
  }).slice(0, MAX_MESSAGE_IMAGE_ATTACHMENTS);
12966
13417
  if (imageFiles.length === 0) {
13418
+ conversationMessage.meta = {
13419
+ ...existingMeta,
13420
+ slackTs: existingMeta.slackTs ?? ts,
13421
+ imagesHydrated: true
13422
+ };
13423
+ mutated = true;
12967
13424
  continue;
12968
13425
  }
12969
13426
  const imageFileIds = imageFiles.map((file) => toOptionalString(file.id)).filter((fileId) => Boolean(fileId));
12970
- const existingMeta = conversationMessage.meta ?? {};
12971
13427
  conversationMessage.meta = {
12972
13428
  ...existingMeta,
12973
13429
  slackTs: existingMeta.slackTs ?? ts,
@@ -13141,45 +13597,71 @@ function createJuniorRuntimeServices(overrides = {}) {
13141
13597
  };
13142
13598
  }
13143
13599
 
13144
- // src/chat/runtime/streaming.ts
13145
- function createTextStreamBridge() {
13146
- const queue = [];
13147
- let ended = false;
13148
- let wakeConsumer = null;
13149
- const iterable = {
13150
- async *[Symbol.asyncIterator]() {
13151
- while (!ended || queue.length > 0) {
13152
- if (queue.length > 0) {
13153
- yield queue.shift();
13154
- continue;
13155
- }
13156
- await new Promise((resolve) => {
13157
- wakeConsumer = resolve;
13158
- });
13159
- }
13160
- }
13161
- };
13162
- return {
13163
- iterable,
13164
- push(delta) {
13165
- if (!delta || ended) {
13166
- return;
13167
- }
13168
- queue.push(delta);
13169
- const wake = wakeConsumer;
13170
- wakeConsumer = null;
13171
- wake?.();
13172
- },
13173
- end() {
13174
- if (ended) {
13175
- return;
13600
+ // src/chat/slack/assistant-thread/title.ts
13601
+ function maybeUpdateAssistantTitle(args) {
13602
+ const assistantThreadContext = args.assistantThreadContext;
13603
+ if (!assistantThreadContext?.channelId || !assistantThreadContext.threadTs || !isDmChannel(assistantThreadContext.channelId)) {
13604
+ return Promise.resolve(void 0);
13605
+ }
13606
+ const titleSourceMessage = getThreadTitleSourceMessage(args.conversation);
13607
+ if (!titleSourceMessage) {
13608
+ return Promise.resolve(void 0);
13609
+ }
13610
+ if (args.artifacts.assistantTitleSourceMessageId === titleSourceMessage.id) {
13611
+ return Promise.resolve(void 0);
13612
+ }
13613
+ return (async () => {
13614
+ try {
13615
+ const title = await args.generateThreadTitle(titleSourceMessage.text);
13616
+ await args.getSlackAdapter().setAssistantTitle(
13617
+ assistantThreadContext.channelId,
13618
+ assistantThreadContext.threadTs,
13619
+ title
13620
+ );
13621
+ return titleSourceMessage.id;
13622
+ } catch (error) {
13623
+ const slackErrorCode = getSlackApiErrorCode(error);
13624
+ const assistantTitleErrorAttributes = {
13625
+ "app.slack.assistant_title.outcome": "permission_denied",
13626
+ ...slackErrorCode ? {
13627
+ "app.slack.assistant_title.error_code": slackErrorCode
13628
+ } : {}
13629
+ };
13630
+ if (isSlackTitlePermissionError(error)) {
13631
+ setSpanAttributes(assistantTitleErrorAttributes);
13632
+ logError(
13633
+ "thread_title_generation_permission_denied",
13634
+ {
13635
+ slackThreadId: args.threadId,
13636
+ slackUserId: args.requesterId,
13637
+ slackChannelId: args.channelId,
13638
+ runId: args.runId,
13639
+ assistantUserName: args.assistantUserName,
13640
+ modelId: args.modelId
13641
+ },
13642
+ assistantTitleErrorAttributes,
13643
+ "Skipping thread title update due to Slack permission error"
13644
+ );
13645
+ return titleSourceMessage.id;
13176
13646
  }
13177
- ended = true;
13178
- const wake = wakeConsumer;
13179
- wakeConsumer = null;
13180
- wake?.();
13647
+ logWarn(
13648
+ "thread_title_generation_failed",
13649
+ {
13650
+ slackThreadId: args.threadId,
13651
+ slackUserId: args.requesterId,
13652
+ slackChannelId: args.channelId,
13653
+ runId: args.runId,
13654
+ assistantUserName: args.assistantUserName,
13655
+ modelId: args.modelId
13656
+ },
13657
+ {
13658
+ "error.message": error instanceof Error ? error.message : String(error)
13659
+ },
13660
+ "Thread title generation failed"
13661
+ );
13662
+ return void 0;
13181
13663
  }
13182
- };
13664
+ })();
13183
13665
  }
13184
13666
 
13185
13667
  // src/chat/runtime/reply-executor.ts
@@ -13204,6 +13686,7 @@ function createReplyToThread(deps) {
13204
13686
  const threadId = getThreadId(thread, message);
13205
13687
  const channelId = getChannelId(thread, message);
13206
13688
  const threadTs = getThreadTs(threadId);
13689
+ const assistantThreadContext = getAssistantThreadContext(message);
13207
13690
  const messageTs = getMessageTs(message);
13208
13691
  const runId = getRunId(thread, message);
13209
13692
  const conversationId = threadId ?? runId;
@@ -13223,7 +13706,6 @@ function createReplyToThread(deps) {
13223
13706
  const userText = stripLeadingBotMention(message.text, {
13224
13707
  stripLeadingSlackMentionToken: options.explicitMention || Boolean(message.isMention)
13225
13708
  });
13226
- const explicitChannelPostIntent = isExplicitChannelPostIntent(userText);
13227
13709
  const preparedState = options.preparedState ?? await deps.prepareTurnState({
13228
13710
  thread,
13229
13711
  message,
@@ -13294,16 +13776,12 @@ function createReplyToThread(deps) {
13294
13776
  messageTs: slackMessageTs
13295
13777
  }
13296
13778
  );
13297
- const progress = createProgressReporter({
13298
- channelId,
13299
- threadTs,
13300
- transport: createSlackAdapterAssistantStatusTransport({
13301
- getSlackAdapter: deps.getSlackAdapter
13302
- })
13779
+ const omittedImageAttachmentCount = !isVisionEnabled() && hasPotentialImageAttachment(message.attachments) ? countPotentialImageAttachments(message.attachments) : 0;
13780
+ const status = createSlackAdapterAssistantStatusSession({
13781
+ channelId: assistantThreadContext?.channelId,
13782
+ threadTs: assistantThreadContext?.threadTs,
13783
+ getSlackAdapter: deps.getSlackAdapter
13303
13784
  });
13304
- const textStream = createTextStreamBridge();
13305
- let streamedReplyPromise;
13306
- let pendingStreamText = "";
13307
13785
  let beforeFirstResponsePostCalled = false;
13308
13786
  const beforeFirstResponsePost = async () => {
13309
13787
  if (beforeFirstResponsePostCalled) {
@@ -13312,25 +13790,6 @@ function createReplyToThread(deps) {
13312
13790
  beforeFirstResponsePostCalled = true;
13313
13791
  await options.beforeFirstResponsePost?.();
13314
13792
  };
13315
- const startStreamingReply = () => {
13316
- if (!streamedReplyPromise) {
13317
- const streamingReply = (async () => {
13318
- return await postThreadReply(
13319
- textStream.iterable,
13320
- "streaming_initial_post"
13321
- );
13322
- })();
13323
- streamedReplyPromise = streamingReply;
13324
- }
13325
- };
13326
- const flushPendingStreamText = () => {
13327
- if (!pendingStreamText) {
13328
- return;
13329
- }
13330
- startStreamingReply();
13331
- textStream.push(pendingStreamText);
13332
- pendingStreamText = "";
13333
- };
13334
13793
  const postThreadReply = async (payload, stage) => {
13335
13794
  await beforeFirstResponsePost();
13336
13795
  try {
@@ -13350,7 +13809,20 @@ function createReplyToThread(deps) {
13350
13809
  throw error;
13351
13810
  }
13352
13811
  };
13353
- await progress.start();
13812
+ status.start();
13813
+ const assistantTitleTask = maybeUpdateAssistantTitle({
13814
+ assistantThreadContext,
13815
+ assistantUserName: botConfig.userName,
13816
+ artifacts: preparedState.artifacts,
13817
+ channelId,
13818
+ conversation: preparedState.conversation,
13819
+ generateThreadTitle: deps.services.generateThreadTitle,
13820
+ getSlackAdapter: deps.getSlackAdapter,
13821
+ modelId: botConfig.fastModelId,
13822
+ requesterId: message.author.userId,
13823
+ runId,
13824
+ threadId
13825
+ });
13354
13826
  let persistedAtLeastOnce = false;
13355
13827
  let shouldPersistFailureState = true;
13356
13828
  try {
@@ -13371,6 +13843,8 @@ function createReplyToThread(deps) {
13371
13843
  artifactState: preparedState.artifacts,
13372
13844
  configuration: preparedState.configuration,
13373
13845
  channelConfiguration: preparedState.channelConfiguration,
13846
+ inboundAttachmentCount: message.attachments.length,
13847
+ omittedImageAttachmentCount,
13374
13848
  userAttachments,
13375
13849
  correlation: {
13376
13850
  conversationId,
@@ -13397,26 +13871,8 @@ function createReplyToThread(deps) {
13397
13871
  await persistThreadState(thread, { artifacts });
13398
13872
  },
13399
13873
  threadParticipants,
13400
- onStatus: (status) => progress.setStatus(status),
13401
- onTextDelta: (deltaText) => {
13402
- if (explicitChannelPostIntent) {
13403
- return;
13404
- }
13405
- if (streamedReplyPromise) {
13406
- textStream.push(deltaText);
13407
- return;
13408
- }
13409
- pendingStreamText += deltaText;
13410
- if (isPotentialRedundantReactionAckText(pendingStreamText)) {
13411
- return;
13412
- }
13413
- flushPendingStreamText();
13414
- }
13874
+ onStatus: (nextStatus) => status.update(nextStatus)
13415
13875
  });
13416
- if (streamedReplyPromise) {
13417
- flushPendingStreamText();
13418
- }
13419
- textStream.end();
13420
13876
  const diagnosticsContext = {
13421
13877
  slackThreadId: threadId,
13422
13878
  slackUserId: message.author.userId,
@@ -13488,30 +13944,27 @@ function createReplyToThread(deps) {
13488
13944
  }
13489
13945
  });
13490
13946
  const artifactStatePatch = reply.artifactStatePatch ? { ...reply.artifactStatePatch } : {};
13491
- const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
13492
- const { shouldPostThreadReply, attachFiles: resolvedAttachFiles } = resolveReplyDelivery({
13493
- reply,
13494
- hasStreamedThreadReply: Boolean(streamedReplyPromise)
13495
- });
13496
13947
  const reactionPerformed = reply.diagnostics.toolCalls.includes(
13497
13948
  "slackMessageAddReaction"
13498
13949
  );
13499
- if (shouldPostThreadReply) {
13500
- if (!streamedReplyPromise) {
13501
- const sent = await postThreadReply(
13502
- buildSlackOutputMessage(
13503
- reply.text,
13504
- resolvedAttachFiles === "inline" ? replyFiles : void 0
13505
- ),
13506
- "thread_reply"
13950
+ const plannedPosts = planSlackReplyPosts({ reply });
13951
+ if (plannedPosts.length > 0) {
13952
+ let sent;
13953
+ for (const post of plannedPosts) {
13954
+ sent = await postThreadReply(
13955
+ buildSlackOutputMessage(post.text, post.files),
13956
+ post.stage
13507
13957
  );
13508
- if (reactionPerformed && isRedundantReactionAckText(reply.text)) {
13509
- await sent.delete();
13510
- }
13511
- } else {
13512
- await streamedReplyPromise;
13958
+ }
13959
+ const firstPlannedMessageHasFiles = (plannedPosts[0]?.files?.length ?? 0) > 0;
13960
+ if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
13961
+ await sent.delete();
13513
13962
  }
13514
13963
  }
13964
+ const titleUpdateResult = await assistantTitleTask;
13965
+ if (titleUpdateResult) {
13966
+ artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult;
13967
+ }
13515
13968
  const shouldPersistArtifacts = Object.keys(artifactStatePatch).length > 0;
13516
13969
  const nextArtifacts = shouldPersistArtifacts ? mergeArtifactsState(preparedState.artifacts, artifactStatePatch) : void 0;
13517
13970
  markTurnCompleted({
@@ -13539,79 +13992,17 @@ function createReplyToThread(deps) {
13539
13992
  "Agent turn completed"
13540
13993
  );
13541
13994
  }
13542
- const isFirstAssistantReply = preparedState.conversation.stats.compactedMessageCount === 0 && preparedState.conversation.messages.filter(
13543
- (m) => m.role === "assistant"
13544
- ).length === 1;
13545
- if (isFirstAssistantReply && channelId && isDmChannel(channelId) && threadTs) {
13546
- void deps.services.generateThreadTitle(userText, reply.text).then(
13547
- (title) => deps.getSlackAdapter().setAssistantTitle(channelId, threadTs, title)
13548
- ).catch((error) => {
13549
- const slackErrorCode = getSlackApiErrorCode(error);
13550
- const assistantTitleErrorAttributes = {
13551
- "app.slack.assistant_title.outcome": "permission_denied",
13552
- ...slackErrorCode ? { "app.slack.assistant_title.error_code": slackErrorCode } : {}
13553
- };
13554
- if (isSlackTitlePermissionError(error)) {
13555
- setSpanAttributes(assistantTitleErrorAttributes);
13556
- logError(
13557
- "thread_title_generation_permission_denied",
13558
- {
13559
- slackThreadId: threadId,
13560
- slackUserId: message.author.userId,
13561
- slackChannelId: channelId,
13562
- runId,
13563
- assistantUserName: botConfig.userName,
13564
- modelId: botConfig.fastModelId
13565
- },
13566
- assistantTitleErrorAttributes,
13567
- "Skipping thread title update due to Slack permission error"
13568
- );
13569
- return;
13570
- }
13571
- logWarn(
13572
- "thread_title_generation_failed",
13573
- {
13574
- slackThreadId: threadId,
13575
- slackUserId: message.author.userId,
13576
- slackChannelId: channelId,
13577
- runId,
13578
- assistantUserName: botConfig.userName,
13579
- modelId: botConfig.fastModelId
13580
- },
13581
- {
13582
- "error.message": error instanceof Error ? error.message : String(error)
13583
- },
13584
- "Thread title generation failed"
13585
- );
13586
- });
13587
- }
13588
- if (shouldPostThreadReply && resolvedAttachFiles === "followup" && replyFiles) {
13589
- await postThreadReply(
13590
- buildSlackOutputMessage("", replyFiles),
13591
- "thread_reply_files_followup"
13592
- );
13593
- }
13594
13995
  } catch (error) {
13595
13996
  if (isRetryableTurnError(error, "mcp_auth_resume")) {
13596
13997
  shouldPersistFailureState = false;
13597
13998
  throw error;
13598
13999
  }
13599
14000
  if (isRetryableTurnError(error, "turn_timeout_resume")) {
13600
- textStream.end();
13601
- const hasVisibleAssistantOutput = Boolean(streamedReplyPromise);
13602
- if (hasVisibleAssistantOutput) {
13603
- logWarn(
13604
- "agent_turn_timeout_resume_skipped_after_visible_output",
13605
- turnTraceContext,
13606
- messageTs ? { "messaging.message.id": messageTs } : {},
13607
- "Skipped automatic timeout resume because assistant text had already started streaming"
13608
- );
13609
- }
13610
14001
  const conversationIdForResume = error.metadata?.conversationId;
13611
14002
  const sessionIdForResume = error.metadata?.sessionId;
13612
14003
  const checkpointVersion = error.metadata?.checkpointVersion;
13613
14004
  const nextSliceId = error.metadata?.sliceId;
13614
- if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
14005
+ if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
13615
14006
  try {
13616
14007
  await deps.services.scheduleTurnTimeoutResume({
13617
14008
  conversationId: conversationIdForResume,
@@ -13632,7 +14023,7 @@ function createReplyToThread(deps) {
13632
14023
  "Failed to schedule timeout resume callback"
13633
14024
  );
13634
14025
  }
13635
- } else if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
14026
+ } else if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
13636
14027
  logWarn(
13637
14028
  "agent_turn_timeout_resume_slice_limit_reached",
13638
14029
  turnTraceContext,
@@ -13654,7 +14045,6 @@ function createReplyToThread(deps) {
13654
14045
  shouldPersistFailureState = true;
13655
14046
  throw error;
13656
14047
  } finally {
13657
- textStream.end();
13658
14048
  if (!persistedAtLeastOnce && shouldPersistFailureState) {
13659
14049
  markTurnFailed({
13660
14050
  conversation: preparedState.conversation,
@@ -13679,19 +14069,26 @@ function createReplyToThread(deps) {
13679
14069
  );
13680
14070
  }
13681
14071
  }
13682
- await progress.stop();
14072
+ await status.stop();
13683
14073
  }
13684
14074
  }
13685
14075
  );
13686
14076
  };
13687
14077
  }
13688
14078
 
13689
- // src/chat/runtime/assistant-lifecycle.ts
14079
+ // src/chat/slack/assistant-thread/lifecycle.ts
13690
14080
  import { ThreadImpl } from "chat";
13691
- async function initializeAssistantThread(event) {
14081
+ async function syncAssistantThreadContext(event, options) {
14082
+ const channelId = normalizeSlackConversationId(event.channelId);
14083
+ if (!channelId) {
14084
+ throw new Error("Assistant thread initialization requires a channel ID");
14085
+ }
14086
+ const sourceChannelId = event.sourceChannelId ? normalizeSlackConversationId(event.sourceChannelId) : void 0;
13692
14087
  const slack = event.getSlackAdapter();
13693
- await slack.setAssistantTitle(event.channelId, event.threadTs, "Junior");
13694
- await slack.setSuggestedPrompts(event.channelId, event.threadTs, [
14088
+ if (options.setInitialTitle) {
14089
+ await slack.setAssistantTitle(channelId, event.threadTs, "Junior");
14090
+ }
14091
+ await slack.setSuggestedPrompts(channelId, event.threadTs, [
13695
14092
  {
13696
14093
  title: "Summarize thread",
13697
14094
  message: "Summarize the latest discussion in this thread."
@@ -13702,26 +14099,37 @@ async function initializeAssistantThread(event) {
13702
14099
  message: "Generate an image based on this conversation."
13703
14100
  }
13704
14101
  ]);
13705
- if (!event.sourceChannelId) {
14102
+ if (!sourceChannelId) {
13706
14103
  return;
13707
14104
  }
13708
14105
  const thread = ThreadImpl.fromJSON({
13709
14106
  _type: "chat:Thread",
13710
14107
  adapterName: "slack",
13711
- channelId: event.channelId,
14108
+ channelId,
13712
14109
  id: event.threadId,
13713
- isDM: event.channelId.startsWith("D")
14110
+ isDM: channelId.startsWith("D")
13714
14111
  });
13715
14112
  const currentArtifacts = coerceThreadArtifactsState(await thread.state);
13716
14113
  const nextArtifacts = mergeArtifactsState(currentArtifacts, {
13717
- assistantContextChannelId: event.sourceChannelId
14114
+ assistantContextChannelId: sourceChannelId
13718
14115
  });
13719
14116
  await persistThreadState(thread, {
13720
14117
  artifacts: nextArtifacts
13721
14118
  });
13722
14119
  }
14120
+ async function initializeAssistantThread(event) {
14121
+ await syncAssistantThreadContext(event, { setInitialTitle: true });
14122
+ }
14123
+ async function refreshAssistantThreadContext(event) {
14124
+ await syncAssistantThreadContext(event, { setInitialTitle: false });
14125
+ }
13723
14126
 
13724
14127
  // src/chat/runtime/turn-preparation.ts
14128
+ function hasPendingImageHydration(conversation) {
14129
+ return conversation.messages.some(
14130
+ (message) => isHumanConversationMessage(message) && !message.meta?.imagesHydrated
14131
+ );
14132
+ }
13725
14133
  function createPrepareTurnState(deps) {
13726
14134
  return async function prepareTurnState(args) {
13727
14135
  const existingState = await args.thread.state;
@@ -13739,15 +14147,10 @@ function createPrepareTurnState(deps) {
13739
14147
  messageId: args.message.id,
13740
14148
  messageCreatedAtMs: args.message.metadata.dateSent.getTime()
13741
14149
  });
13742
- const messageHasPotentialImageAttachment = args.message.attachments.some(
13743
- (attachment) => {
13744
- if (attachment.type === "image") {
13745
- return true;
13746
- }
13747
- const mimeType = attachment.mimeType ?? "";
13748
- return attachment.type === "file" && mimeType.startsWith("image/");
13749
- }
14150
+ const messageHasPotentialImageAttachment = hasPotentialImageAttachment(
14151
+ args.message.attachments
13750
14152
  );
14153
+ const imageAttachmentCount = messageHasPotentialImageAttachment ? countPotentialImageAttachments(args.message.attachments) : 0;
13751
14154
  const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
13752
14155
  const slackTs = getSlackMessageTs(args.message);
13753
14156
  const incomingUserMessage = {
@@ -13762,7 +14165,9 @@ function createPrepareTurnState(deps) {
13762
14165
  isBot: typeof args.message.author.isBot === "boolean" ? args.message.author.isBot : void 0
13763
14166
  },
13764
14167
  meta: {
14168
+ attachmentCount: args.message.attachments.length,
13765
14169
  explicitMention: args.explicitMention,
14170
+ imageAttachmentCount: imageAttachmentCount > 0 ? imageAttachmentCount : void 0,
13766
14171
  slackTs,
13767
14172
  imagesHydrated: !messageHasPotentialImageAttachment
13768
14173
  }
@@ -13771,7 +14176,8 @@ function createPrepareTurnState(deps) {
13771
14176
  conversation,
13772
14177
  incomingUserMessage
13773
14178
  );
13774
- if (isVisionEnabled() && (!conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment)) {
14179
+ const shouldHydrateVisionContext = !conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment || hasPendingImageHydration(conversation);
14180
+ if (isVisionEnabled() && shouldHydrateVisionContext) {
13775
14181
  await deps.hydrateConversationVisionContext(conversation, {
13776
14182
  threadId: args.context.threadId,
13777
14183
  channelId: args.context.channelId,
@@ -13867,7 +14273,7 @@ function createSlackRuntime(options) {
13867
14273
  slackTs,
13868
14274
  replied: false,
13869
14275
  skippedReason: decision.reason,
13870
- imagesHydrated: true
14276
+ imagesHydrated: !hasPotentialImageAttachment(message.attachments)
13871
14277
  }
13872
14278
  });
13873
14279
  conversation.processing.activeTurnId = void 0;
@@ -13915,6 +14321,20 @@ function createSlackRuntime(options) {
13915
14321
  sourceChannelId,
13916
14322
  getSlackAdapter: options.getSlackAdapter
13917
14323
  });
14324
+ },
14325
+ refreshAssistantThreadContext: async ({
14326
+ threadId,
14327
+ channelId,
14328
+ threadTs,
14329
+ sourceChannelId
14330
+ }) => {
14331
+ await refreshAssistantThreadContext({
14332
+ threadId,
14333
+ channelId,
14334
+ threadTs,
14335
+ sourceChannelId,
14336
+ getSlackAdapter: options.getSlackAdapter
14337
+ });
13918
14338
  }
13919
14339
  });
13920
14340
  }
@@ -13987,14 +14407,14 @@ var JuniorChat = class extends Chat {
13987
14407
  (async () => {
13988
14408
  try {
13989
14409
  const message = await messageOrFactory();
13990
- const normalized2 = normalizeIncomingSlackThreadId(
14410
+ const normalized = normalizeIncomingSlackThreadId(
13991
14411
  threadId,
13992
14412
  message
13993
14413
  );
13994
- if (normalized2 !== threadId && "threadId" in message) {
13995
- message.threadId = normalized2;
14414
+ if (normalized !== threadId && "threadId" in message) {
14415
+ message.threadId = normalized;
13996
14416
  }
13997
- super.processMessage(adapter, normalized2, message, options);
14417
+ super.processMessage(adapter, normalized, message, options);
13998
14418
  } catch (error) {
13999
14419
  runtime.logger?.error?.("Message factory resolution error", {
14000
14420
  error,
@@ -14005,14 +14425,19 @@ var JuniorChat = class extends Chat {
14005
14425
  );
14006
14426
  return;
14007
14427
  }
14008
- const normalized = normalizeIncomingSlackThreadId(
14009
- threadId,
14010
- messageOrFactory
14428
+ enqueueBackgroundTask(
14429
+ options,
14430
+ (async () => {
14431
+ const normalized = normalizeIncomingSlackThreadId(
14432
+ threadId,
14433
+ messageOrFactory
14434
+ );
14435
+ if (normalized !== threadId && "threadId" in messageOrFactory) {
14436
+ messageOrFactory.threadId = normalized;
14437
+ }
14438
+ super.processMessage(adapter, normalized, messageOrFactory, options);
14439
+ })()
14011
14440
  );
14012
- if (normalized !== threadId && "threadId" in messageOrFactory) {
14013
- messageOrFactory.threadId = normalized;
14014
- }
14015
- super.processMessage(adapter, normalized, messageOrFactory, options);
14016
14441
  }
14017
14442
  processReaction(event, options) {
14018
14443
  const runtime = this;
@@ -14033,20 +14458,19 @@ var JuniorChat = class extends Chat {
14033
14458
  }
14034
14459
  processAction(event, options) {
14035
14460
  const runtime = this;
14036
- enqueueBackgroundTask(
14037
- options,
14038
- (async () => {
14039
- try {
14040
- await runtime.handleActionEvent(event);
14041
- } catch (error) {
14042
- runtime.logger?.error?.("Action processing error", {
14043
- error,
14044
- actionId: event.actionId,
14045
- messageId: event.messageId
14046
- });
14047
- }
14048
- })()
14049
- );
14461
+ const task = (async () => {
14462
+ try {
14463
+ await runtime.handleActionEvent(event, options);
14464
+ } catch (error) {
14465
+ runtime.logger?.error?.("Action processing error", {
14466
+ error,
14467
+ actionId: event.actionId,
14468
+ messageId: event.messageId
14469
+ });
14470
+ }
14471
+ })();
14472
+ enqueueBackgroundTask(options, task);
14473
+ return task;
14050
14474
  }
14051
14475
  processModalClose(event, contextId, options) {
14052
14476
  const runtime = this;
@@ -14084,7 +14508,7 @@ var JuniorChat = class extends Chat {
14084
14508
  options,
14085
14509
  (async () => {
14086
14510
  try {
14087
- await runtime.handleSlashCommandEvent(event);
14511
+ await runtime.handleSlashCommandEvent(event, options);
14088
14512
  } catch (error) {
14089
14513
  runtime.logger?.error?.("Slash command processing error", {
14090
14514
  error,
@@ -14151,6 +14575,14 @@ var JuniorChat = class extends Chat {
14151
14575
  }
14152
14576
  };
14153
14577
 
14578
+ // src/chat/slack/adapter.ts
14579
+ import {
14580
+ createSlackAdapter
14581
+ } from "@chat-adapter/slack";
14582
+ function createJuniorSlackAdapter(config) {
14583
+ return createSlackAdapter(config);
14584
+ }
14585
+
14154
14586
  // src/chat/queue/thread-message-dispatcher.ts
14155
14587
  function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downloadPrivateSlackFile) {
14156
14588
  for (const attachment of message.attachments) {
@@ -14269,7 +14701,7 @@ function createProductionBot() {
14269
14701
  if (!signingSecret) {
14270
14702
  throw new Error("SLACK_SIGNING_SECRET is required");
14271
14703
  }
14272
- return createSlackAdapter({
14704
+ return createJuniorSlackAdapter({
14273
14705
  logger: logger.child("slack"),
14274
14706
  signingSecret,
14275
14707
  ...botToken ? { botToken } : {},
@@ -14405,6 +14837,32 @@ function isMessageChangedEnvelope(value) {
14405
14837
  function textMentionsBot(text, botUserId) {
14406
14838
  return text.includes(`<@${botUserId}>`);
14407
14839
  }
14840
+ function getAttachmentType(mimeType) {
14841
+ if (mimeType?.startsWith("image/")) {
14842
+ return "image";
14843
+ }
14844
+ if (mimeType?.startsWith("video/")) {
14845
+ return "video";
14846
+ }
14847
+ if (mimeType?.startsWith("audio/")) {
14848
+ return "audio";
14849
+ }
14850
+ return "file";
14851
+ }
14852
+ function extractEditedMessageAttachments(files) {
14853
+ if (!files || files.length === 0) {
14854
+ return [];
14855
+ }
14856
+ return files.map((file) => ({
14857
+ type: getAttachmentType(file.mimetype),
14858
+ url: file.url_private_download ?? file.url_private,
14859
+ name: file.name,
14860
+ mimeType: file.mimetype,
14861
+ size: file.size,
14862
+ width: file.original_w,
14863
+ height: file.original_h
14864
+ }));
14865
+ }
14408
14866
  function extractMessageChangedMention(body, botUserId, adapter) {
14409
14867
  if (!isMessageChangedEnvelope(body)) return null;
14410
14868
  const { event } = body;
@@ -14430,7 +14888,7 @@ function extractMessageChangedMention(body, botUserId, adapter) {
14430
14888
  threadId,
14431
14889
  text: newText,
14432
14890
  isMention: true,
14433
- attachments: [],
14891
+ attachments: extractEditedMessageAttachments(event.message.files),
14434
14892
  metadata: { dateSent: new Date(Number(messageTs) * 1e3), edited: true },
14435
14893
  formatted: { type: "root", children: [] },
14436
14894
  raw,
@@ -14483,6 +14941,7 @@ async function handleAuthenticatedSlackMessageChangedMention(args) {
14483
14941
  if (!result) {
14484
14942
  return false;
14485
14943
  }
14944
+ rehydrateAttachmentFetchers(result.message);
14486
14945
  args.bot.processMessage(
14487
14946
  slackAdapter,
14488
14947
  result.threadId,