@sentry/junior 0.23.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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-O5N42P7K.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-RMVXZMXQ.js";
31
31
  import {
32
32
  CredentialUnavailableError,
33
33
  buildOAuthTokenRequest,
@@ -35,6 +35,7 @@ import {
35
35
  createPluginBroker,
36
36
  createRequestContext,
37
37
  extractGenAiUsageAttributes,
38
+ extractGenAiUsageSummary,
38
39
  getActiveTraceId,
39
40
  getPluginDefinition,
40
41
  getPluginMcpProviders,
@@ -58,7 +59,7 @@ import {
58
59
  toOptionalString,
59
60
  withContext,
60
61
  withSpan
61
- } from "./chunk-MCJJKEB3.js";
62
+ } from "./chunk-I3WA75AD.js";
62
63
  import "./chunk-Z3YD6NHK.js";
63
64
  import {
64
65
  discoverInstalledPluginPackageContent,
@@ -333,9 +334,17 @@ function coerceAuthor(value) {
333
334
  function coerceMessageMeta(value) {
334
335
  if (!isRecord(value)) return void 0;
335
336
  const meta = {};
337
+ const attachmentCount = toOptionalNumber(value.attachmentCount);
338
+ if (typeof attachmentCount === "number" && attachmentCount > 0) {
339
+ meta.attachmentCount = attachmentCount;
340
+ }
336
341
  if (typeof value.explicitMention === "boolean") {
337
342
  meta.explicitMention = value.explicitMention;
338
343
  }
344
+ const imageAttachmentCount = toOptionalNumber(value.imageAttachmentCount);
345
+ if (typeof imageAttachmentCount === "number" && imageAttachmentCount > 0) {
346
+ meta.imageAttachmentCount = imageAttachmentCount;
347
+ }
339
348
  if (typeof value.replied === "boolean") {
340
349
  meta.replied = value.replied;
341
350
  }
@@ -356,7 +365,7 @@ function coerceMessageMeta(value) {
356
365
  if (typeof value.imagesHydrated === "boolean") {
357
366
  meta.imagesHydrated = value.imagesHydrated;
358
367
  }
359
- 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) {
368
+ 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) {
360
369
  return void 0;
361
370
  }
362
371
  return meta;
@@ -868,13 +877,19 @@ function mapSlackError(error) {
868
877
  if (apiError === "not_in_channel") {
869
878
  return new SlackActionError(message, "not_in_channel", baseOptions);
870
879
  }
880
+ if (apiError === "already_reacted") {
881
+ return new SlackActionError(message, "already_reacted", baseOptions);
882
+ }
883
+ if (apiError === "no_reaction") {
884
+ return new SlackActionError(message, "no_reaction", baseOptions);
885
+ }
871
886
  if (apiError === "invalid_arguments") {
872
887
  return new SlackActionError(message, "invalid_arguments", baseOptions);
873
888
  }
874
889
  if (apiError === "invalid_name") {
875
890
  return new SlackActionError(message, "invalid_arguments", baseOptions);
876
891
  }
877
- if (apiError === "not_found") {
892
+ if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
878
893
  return new SlackActionError(message, "not_found", baseOptions);
879
894
  }
880
895
  if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
@@ -974,19 +989,6 @@ async function getFilePermalink(fileId) {
974
989
  );
975
990
  return response.file?.permalink;
976
991
  }
977
- async function uploadFilesToThread(args) {
978
- const client2 = getClient();
979
- await withSlackRetries(
980
- () => client2.filesUploadV2({
981
- channel_id: args.channelId,
982
- thread_ts: args.threadTs,
983
- file_uploads: args.files.map((f) => ({
984
- file: f.data,
985
- filename: f.filename
986
- }))
987
- })
988
- );
989
- }
990
992
  async function downloadPrivateSlackFile(url) {
991
993
  const token = getSlackBotToken();
992
994
  if (!token) {
@@ -1006,6 +1008,217 @@ async function downloadPrivateSlackFile(url) {
1006
1008
  return Buffer.from(await response.arrayBuffer());
1007
1009
  }
1008
1010
 
1011
+ // src/chat/slack/emoji.ts
1012
+ var SLACK_EMOJI_NAME_RE = /^(?:[a-z0-9_+-]+)(?:::(?:skin-tone-[2-6]))?$/;
1013
+ function normalizeSlackEmojiName(value) {
1014
+ const trimmed = value.trim().toLowerCase();
1015
+ if (!trimmed) {
1016
+ return null;
1017
+ }
1018
+ const normalized = trimmed.startsWith(":") && trimmed.endsWith(":") ? trimmed.slice(1, -1) : trimmed;
1019
+ return SLACK_EMOJI_NAME_RE.test(normalized) ? normalized : null;
1020
+ }
1021
+
1022
+ // src/chat/slack/outbound.ts
1023
+ var MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
1024
+ function requireSlackConversationId(channelId, action) {
1025
+ const normalized = normalizeSlackConversationId(channelId);
1026
+ if (!normalized) {
1027
+ throw new Error(`${action} requires a valid channel ID`);
1028
+ }
1029
+ return normalized;
1030
+ }
1031
+ function requireSlackThreadTimestamp(threadTs, action) {
1032
+ const normalized = threadTs.trim();
1033
+ if (!normalized) {
1034
+ throw new Error(`${action} requires a thread timestamp`);
1035
+ }
1036
+ return normalized;
1037
+ }
1038
+ function requireSlackMessageTimestamp(timestamp, action) {
1039
+ const normalized = timestamp.trim();
1040
+ if (!normalized) {
1041
+ throw new Error(`${action} requires a target message timestamp`);
1042
+ }
1043
+ return normalized;
1044
+ }
1045
+ function requireSlackMessageText(text, action) {
1046
+ if (text.trim().length === 0) {
1047
+ throw new Error(`${action} requires non-empty text`);
1048
+ }
1049
+ if (text.length > MAX_SLACK_MESSAGE_TEXT_CHARS) {
1050
+ throw new Error(
1051
+ `${action} text exceeds Slack's 40000 character truncation limit`
1052
+ );
1053
+ }
1054
+ return text;
1055
+ }
1056
+ async function getPermalinkBestEffort(args) {
1057
+ try {
1058
+ const response = await withSlackRetries(
1059
+ () => getSlackClient().chat.getPermalink({
1060
+ channel: args.channelId,
1061
+ message_ts: args.messageTs
1062
+ }),
1063
+ 3,
1064
+ { action: "chat.getPermalink" }
1065
+ );
1066
+ return response.permalink;
1067
+ } catch {
1068
+ return void 0;
1069
+ }
1070
+ }
1071
+ async function postSlackMessage(input) {
1072
+ const channelId = requireSlackConversationId(
1073
+ input.channelId,
1074
+ "Slack message posting"
1075
+ );
1076
+ const text = requireSlackMessageText(input.text, "Slack message posting");
1077
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(
1078
+ input.threadTs,
1079
+ "Slack thread message posting"
1080
+ ) : void 0;
1081
+ const response = await withSlackRetries(
1082
+ () => getSlackClient().chat.postMessage({
1083
+ channel: channelId,
1084
+ text,
1085
+ mrkdwn: true,
1086
+ ...input.blocks?.length ? {
1087
+ blocks: input.blocks
1088
+ } : {},
1089
+ ...threadTs ? { thread_ts: threadTs } : {}
1090
+ }),
1091
+ 3,
1092
+ { action: "chat.postMessage" }
1093
+ );
1094
+ if (!response.ts) {
1095
+ throw new Error("Slack message posted without ts");
1096
+ }
1097
+ return {
1098
+ ts: response.ts,
1099
+ ...input.includePermalink ? {
1100
+ permalink: await getPermalinkBestEffort({
1101
+ channelId,
1102
+ messageTs: response.ts
1103
+ })
1104
+ } : {}
1105
+ };
1106
+ }
1107
+ async function deleteSlackMessage(input) {
1108
+ const channelId = requireSlackConversationId(
1109
+ input.channelId,
1110
+ "Slack message deletion"
1111
+ );
1112
+ const timestamp = requireSlackMessageTimestamp(
1113
+ input.timestamp,
1114
+ "Slack message deletion"
1115
+ );
1116
+ await withSlackRetries(
1117
+ () => getSlackClient().chat.delete({
1118
+ channel: channelId,
1119
+ ts: timestamp
1120
+ }),
1121
+ 3,
1122
+ { action: "chat.delete" }
1123
+ );
1124
+ }
1125
+ async function postSlackEphemeralMessage(input) {
1126
+ const channelId = requireSlackConversationId(
1127
+ input.channelId,
1128
+ "Slack ephemeral message posting"
1129
+ );
1130
+ const userId = input.userId.trim();
1131
+ if (!userId) {
1132
+ throw new Error("Slack ephemeral message posting requires a user ID");
1133
+ }
1134
+ const text = requireSlackMessageText(
1135
+ input.text,
1136
+ "Slack ephemeral message posting"
1137
+ );
1138
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(
1139
+ input.threadTs,
1140
+ "Slack ephemeral thread message posting"
1141
+ ) : void 0;
1142
+ const response = await withSlackRetries(
1143
+ () => getSlackClient().chat.postEphemeral({
1144
+ channel: channelId,
1145
+ user: userId,
1146
+ text,
1147
+ ...threadTs ? { thread_ts: threadTs } : {}
1148
+ }),
1149
+ 3,
1150
+ { action: "chat.postEphemeral" }
1151
+ );
1152
+ return {
1153
+ messageTs: response.message_ts
1154
+ };
1155
+ }
1156
+ async function uploadFilesToThread(input) {
1157
+ const channelId = requireSlackConversationId(
1158
+ input.channelId,
1159
+ "Slack file upload"
1160
+ );
1161
+ const threadTs = requireSlackThreadTimestamp(
1162
+ input.threadTs,
1163
+ "Slack file upload"
1164
+ );
1165
+ if (input.files.length === 0) {
1166
+ throw new Error("Slack file upload requires at least one file");
1167
+ }
1168
+ const fileUploads = input.files.map((file) => {
1169
+ const filename = file.filename.trim();
1170
+ if (!filename) {
1171
+ throw new Error(
1172
+ "Slack file upload requires every file to have a filename"
1173
+ );
1174
+ }
1175
+ return {
1176
+ file: file.data,
1177
+ filename
1178
+ };
1179
+ });
1180
+ await withSlackRetries(
1181
+ () => getSlackClient().filesUploadV2({
1182
+ channel_id: channelId,
1183
+ thread_ts: threadTs,
1184
+ file_uploads: fileUploads
1185
+ }),
1186
+ 3,
1187
+ { action: "filesUploadV2" }
1188
+ );
1189
+ }
1190
+ async function addReactionToMessage(input) {
1191
+ const channelId = requireSlackConversationId(
1192
+ input.channelId,
1193
+ "Slack reaction"
1194
+ );
1195
+ const timestamp = requireSlackMessageTimestamp(
1196
+ input.timestamp,
1197
+ "Slack reaction"
1198
+ );
1199
+ const emoji = normalizeSlackEmojiName(input.emoji);
1200
+ if (!emoji) {
1201
+ throw new Error("Slack reaction requires a valid emoji alias name");
1202
+ }
1203
+ try {
1204
+ await withSlackRetries(
1205
+ () => getSlackClient().reactions.add({
1206
+ channel: channelId,
1207
+ timestamp,
1208
+ name: emoji
1209
+ }),
1210
+ 3,
1211
+ { action: "reactions.add" }
1212
+ );
1213
+ } catch (error) {
1214
+ if (error instanceof SlackActionError && error.code === "already_reacted") {
1215
+ return { ok: true };
1216
+ }
1217
+ throw error;
1218
+ }
1219
+ return { ok: true };
1220
+ }
1221
+
1009
1222
  // src/chat/respond-helpers.ts
1010
1223
  var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
1011
1224
  function getSessionIdentifiers(context) {
@@ -1182,16 +1395,23 @@ function extractAssistantText(message) {
1182
1395
  (part) => part.type === "text" && typeof part.text === "string"
1183
1396
  ).map((part) => part.text).join("\n");
1184
1397
  }
1185
- function hasCompletedAssistantTurn(messages) {
1398
+ function getTerminalAssistantMessages(messages) {
1399
+ let lastToolResultIndex = -1;
1186
1400
  for (let index = messages.length - 1; index >= 0; index -= 1) {
1187
- const message = messages[index];
1188
- if (!isAssistantMessage(message)) {
1189
- continue;
1401
+ if (isToolResultMessage(messages[index])) {
1402
+ lastToolResultIndex = index;
1403
+ break;
1190
1404
  }
1191
- const stopReason = message.stopReason;
1192
- return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
1193
1405
  }
1194
- return false;
1406
+ return messages.slice(lastToolResultIndex + 1).filter(isAssistantMessage);
1407
+ }
1408
+ function hasCompletedAssistantTurn(messages) {
1409
+ const message = getTerminalAssistantMessages(messages).at(-1);
1410
+ if (!message) {
1411
+ return false;
1412
+ }
1413
+ const stopReason = message.stopReason;
1414
+ return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
1195
1415
  }
1196
1416
  function upsertActiveSkill(activeSkills, next) {
1197
1417
  const existing = activeSkills.find((skill) => skill.name === next.name);
@@ -1257,17 +1477,17 @@ async function deliverPrivateMessage(input) {
1257
1477
  if (input.channelId) {
1258
1478
  try {
1259
1479
  if (isDmChannel(input.channelId)) {
1260
- await client2.chat.postMessage({
1261
- channel: input.channelId,
1480
+ await postSlackMessage({
1481
+ channelId: input.channelId,
1262
1482
  text: input.text,
1263
- ...input.threadTs ? { thread_ts: input.threadTs } : {}
1483
+ threadTs: input.threadTs
1264
1484
  });
1265
1485
  } else {
1266
- await client2.chat.postEphemeral({
1267
- channel: input.channelId,
1268
- user: input.userId,
1486
+ await postSlackEphemeralMessage({
1487
+ channelId: input.channelId,
1488
+ userId: input.userId,
1269
1489
  text: input.text,
1270
- ...input.threadTs ? { thread_ts: input.threadTs } : {}
1490
+ threadTs: input.threadTs
1271
1491
  });
1272
1492
  }
1273
1493
  return "in_context";
@@ -1294,7 +1514,7 @@ async function deliverPrivateMessage(input) {
1294
1514
  );
1295
1515
  return false;
1296
1516
  }
1297
- await client2.chat.postMessage({ channel: dmChannelId, text: input.text });
1517
+ await postSlackMessage({ channelId: dmChannelId, text: input.text });
1298
1518
  return "fallback_dm";
1299
1519
  } catch (error) {
1300
1520
  logWarn(
@@ -1914,6 +2134,7 @@ function coerceThreadArtifactsState(value) {
1914
2134
  }
1915
2135
  return {
1916
2136
  assistantContextChannelId: typeof artifacts.assistantContextChannelId === "string" ? artifacts.assistantContextChannelId : void 0,
2137
+ assistantTitleSourceMessageId: typeof artifacts.assistantTitleSourceMessageId === "string" ? artifacts.assistantTitleSourceMessageId : void 0,
1917
2138
  lastCanvasId: typeof artifacts.lastCanvasId === "string" ? artifacts.lastCanvasId : void 0,
1918
2139
  lastCanvasUrl: typeof artifacts.lastCanvasUrl === "string" ? artifacts.lastCanvasUrl : void 0,
1919
2140
  recentCanvases,
@@ -2115,6 +2336,15 @@ function getTurnUserMessage(conversation, sessionId) {
2115
2336
  function getTurnUserMessageId(conversation, sessionId) {
2116
2337
  return getTurnUserMessage(conversation, sessionId)?.id;
2117
2338
  }
2339
+ function getTurnUserReplyAttachmentContext(message) {
2340
+ const inboundAttachmentCount = message?.meta?.attachmentCount ?? 0;
2341
+ const imageAttachmentCount = message?.meta?.imageAttachmentCount ?? 0;
2342
+ const imagesHydrated = message?.meta?.imagesHydrated === true;
2343
+ return {
2344
+ ...inboundAttachmentCount > 0 ? { inboundAttachmentCount } : {},
2345
+ ...!imagesHydrated && imageAttachmentCount > 0 ? { omittedImageAttachmentCount: imageAttachmentCount } : {}
2346
+ };
2347
+ }
2118
2348
 
2119
2349
  // src/chat/pi/client.ts
2120
2350
  import {
@@ -2535,7 +2765,7 @@ async function summarizeConversationChunk(messages, conversation, context, deps)
2535
2765
  }
2536
2766
  return transcript.slice(0, 2800);
2537
2767
  }
2538
- async function generateThreadTitleWithDeps(userText, assistantText, deps) {
2768
+ async function generateThreadTitleWithDeps(sourceText, deps) {
2539
2769
  const result = await deps.completeText({
2540
2770
  modelId: botConfig.fastModelId,
2541
2771
  temperature: 0,
@@ -2543,17 +2773,41 @@ async function generateThreadTitleWithDeps(userText, assistantText, deps) {
2543
2773
  {
2544
2774
  role: "user",
2545
2775
  content: [
2546
- "Generate a concise 5-8 word title for this conversation. Reply with ONLY the title, no quotes or punctuation.",
2776
+ "Generate a concise 5-8 word Slack conversation title from the first user message below.",
2777
+ "Capture the user's main request.",
2778
+ "Reply with ONLY the title, with no quotes or trailing punctuation.",
2547
2779
  "",
2548
- `User: ${userText.slice(0, 500)}`,
2549
- `Assistant: ${assistantText.slice(0, 500)}`
2780
+ `First user message: ${sourceText.slice(0, 500)}`
2550
2781
  ].join("\n"),
2551
2782
  timestamp: Date.now()
2552
2783
  }
2553
- ]
2784
+ ],
2785
+ metadata: {
2786
+ modelId: botConfig.fastModelId
2787
+ }
2554
2788
  });
2555
2789
  return result.text.trim().slice(0, 60);
2556
2790
  }
2791
+ function getThreadTitleSourceMessage(conversation) {
2792
+ let firstMessage;
2793
+ for (const message of conversation.messages) {
2794
+ if (!isHumanConversationMessage(message)) {
2795
+ continue;
2796
+ }
2797
+ if (!firstMessage) {
2798
+ firstMessage = message;
2799
+ continue;
2800
+ }
2801
+ if (message.createdAtMs < firstMessage.createdAtMs) {
2802
+ firstMessage = message;
2803
+ continue;
2804
+ }
2805
+ if (message.createdAtMs === firstMessage.createdAtMs && message.id < firstMessage.id) {
2806
+ firstMessage = message;
2807
+ }
2808
+ }
2809
+ return firstMessage;
2810
+ }
2557
2811
  async function compactConversationIfNeededWithDeps(conversation, context, deps) {
2558
2812
  updateConversationStats(conversation);
2559
2813
  let estimatedTokens = conversation.stats.estimatedContextTokens;
@@ -2598,7 +2852,7 @@ async function compactConversationIfNeededWithDeps(conversation, context, deps)
2598
2852
  function createConversationMemoryService(deps) {
2599
2853
  return {
2600
2854
  compactConversationIfNeeded: async (conversation, context) => await compactConversationIfNeededWithDeps(conversation, context, deps),
2601
- generateThreadTitle: async (userText, assistantText) => await generateThreadTitleWithDeps(userText, assistantText, deps)
2855
+ generateThreadTitle: async (sourceText) => await generateThreadTitleWithDeps(sourceText, deps)
2602
2856
  };
2603
2857
  }
2604
2858
  var defaultConversationMemoryService = createConversationMemoryService({
@@ -2704,104 +2958,257 @@ import { Agent } from "@mariozechner/pi-agent-core";
2704
2958
  import fs from "fs";
2705
2959
  import path2 from "path";
2706
2960
 
2707
- // src/chat/slack/output.ts
2708
- var MAX_INLINE_CHARS = 2200;
2709
- var MAX_INLINE_LINES = 45;
2710
- var CONTINUED_MARKER = "\n\n[Continued below]";
2711
- var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
2712
- var STREAMING_FENCE_CLOSE_GUARD = "\n```";
2713
- function ensureBlockSpacing(text) {
2714
- const codeBlockPattern = /^```/;
2715
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
2716
- const lines = text.split("\n");
2717
- const result = [];
2718
- let inCodeBlock = false;
2719
- for (let i = 0; i < lines.length; i++) {
2720
- const line = lines[i];
2721
- const isCodeFence = codeBlockPattern.test(line.trimStart());
2722
- if (isCodeFence) {
2723
- if (!inCodeBlock) {
2724
- const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
2725
- if (prev2 !== void 0 && prev2.trim() !== "") {
2726
- result.push("");
2727
- }
2728
- }
2729
- inCodeBlock = !inCodeBlock;
2730
- result.push(line);
2731
- continue;
2732
- }
2733
- if (inCodeBlock) {
2734
- result.push(line);
2735
- continue;
2736
- }
2737
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
2738
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
2739
- result.push("");
2740
- }
2741
- result.push(line);
2961
+ // src/chat/runtime/status-format.ts
2962
+ var SLACK_STATUS_MAX_LENGTH = 50;
2963
+ function truncateWithEllipsis(text, maxLength) {
2964
+ if (text.length <= maxLength) {
2965
+ return text;
2742
2966
  }
2743
- return result.join("\n");
2744
- }
2745
- function normalizeForSlack(text) {
2746
- let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
2747
- normalized = ensureBlockSpacing(normalized);
2748
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
2967
+ return `${text.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
2749
2968
  }
2750
- function countSlackLines(text) {
2751
- if (!text) {
2752
- return 0;
2969
+ function truncateStatusText(text) {
2970
+ const trimmed = text.trim();
2971
+ if (!trimmed) {
2972
+ return "";
2753
2973
  }
2754
- return text.split("\n").length;
2755
- }
2756
- function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
2757
- return text.length <= maxChars && countSlackLines(text) <= maxLines;
2974
+ return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
2758
2975
  }
2759
- function findSplitIndex(text, maxChars) {
2760
- if (text.length <= maxChars) {
2761
- return text.length;
2976
+ function compactStatusPath(value) {
2977
+ if (typeof value !== "string") {
2978
+ return void 0;
2762
2979
  }
2763
- const bounded = text.slice(0, maxChars);
2764
- const newlineIndex = bounded.lastIndexOf("\n");
2765
- if (newlineIndex > 0) {
2766
- return newlineIndex;
2980
+ const trimmed = value.trim();
2981
+ if (!trimmed) {
2982
+ return void 0;
2767
2983
  }
2768
- const spaceIndex = bounded.lastIndexOf(" ");
2769
- if (spaceIndex > 0) {
2770
- return spaceIndex;
2984
+ if (trimmed.length <= 80) {
2985
+ return trimmed;
2771
2986
  }
2772
- return maxChars;
2987
+ return `...${trimmed.slice(-77)}`;
2773
2988
  }
2774
- function splitByLineBudget(text, maxLines) {
2775
- if (maxLines <= 0) {
2776
- return "";
2989
+ function compactStatusText(value, maxLength = 80) {
2990
+ if (typeof value !== "string") {
2991
+ return void 0;
2777
2992
  }
2778
- const lines = text.split("\n");
2779
- if (lines.length <= maxLines) {
2780
- return text;
2993
+ const trimmed = value.trim();
2994
+ if (!trimmed) {
2995
+ return void 0;
2781
2996
  }
2782
- return lines.slice(0, maxLines).join("\n");
2783
- }
2784
- function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
2785
- return {
2786
- maxChars: Math.max(1, maxChars - suffix.length),
2787
- maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
2788
- };
2789
- }
2790
- function forceSplitBudget(text, budget) {
2791
- const lineCount = countSlackLines(text);
2792
- return {
2793
- maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
2794
- maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
2795
- };
2997
+ return truncateWithEllipsis(trimmed, maxLength);
2796
2998
  }
2797
- function getFenceContinuation(text) {
2798
- let open;
2799
- for (const line of text.split("\n")) {
2800
- const trimmed = line.trimStart();
2801
- const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
2802
- if (!openerMatch) {
2803
- continue;
2804
- }
2999
+ function readShellToken(command, startIndex) {
3000
+ let index = startIndex;
3001
+ while (index < command.length && /\s/.test(command[index] ?? "")) {
3002
+ index += 1;
3003
+ }
3004
+ if (index >= command.length) {
3005
+ return void 0;
3006
+ }
3007
+ let token = "";
3008
+ let quote;
3009
+ while (index < command.length) {
3010
+ const char = command[index];
3011
+ if (!char) {
3012
+ break;
3013
+ }
3014
+ if (quote) {
3015
+ if (char === quote) {
3016
+ quote = void 0;
3017
+ index += 1;
3018
+ continue;
3019
+ }
3020
+ if (char === "\\" && quote === '"' && index + 1 < command.length) {
3021
+ token += command[index + 1];
3022
+ index += 2;
3023
+ continue;
3024
+ }
3025
+ token += char;
3026
+ index += 1;
3027
+ continue;
3028
+ }
3029
+ if (/\s/.test(char)) {
3030
+ break;
3031
+ }
3032
+ if (char === '"' || char === "'") {
3033
+ quote = char;
3034
+ index += 1;
3035
+ continue;
3036
+ }
3037
+ if (char === "\\" && index + 1 < command.length) {
3038
+ token += command[index + 1];
3039
+ index += 2;
3040
+ continue;
3041
+ }
3042
+ token += char;
3043
+ index += 1;
3044
+ }
3045
+ return { token, nextIndex: index };
3046
+ }
3047
+ function compactStatusCommand(value) {
3048
+ if (typeof value !== "string") {
3049
+ return void 0;
3050
+ }
3051
+ const trimmed = value.trim();
3052
+ if (!trimmed) {
3053
+ return void 0;
3054
+ }
3055
+ let index = 0;
3056
+ while (index < trimmed.length) {
3057
+ const parsed = readShellToken(trimmed, index);
3058
+ if (!parsed) {
3059
+ return void 0;
3060
+ }
3061
+ index = parsed.nextIndex;
3062
+ if (!parsed.token) {
3063
+ continue;
3064
+ }
3065
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(parsed.token)) {
3066
+ continue;
3067
+ }
3068
+ const normalized = parsed.token.replace(/[\\/]+$/g, "");
3069
+ if (!normalized) {
3070
+ return void 0;
3071
+ }
3072
+ const parts = normalized.split(/[\\/]/).filter((part) => part.length > 0);
3073
+ const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
3074
+ return compactStatusText(command, 40);
3075
+ }
3076
+ return void 0;
3077
+ }
3078
+ function compactStatusFilename(value) {
3079
+ if (typeof value !== "string") {
3080
+ return void 0;
3081
+ }
3082
+ const trimmed = value.trim().replace(/[\\/]+$/g, "");
3083
+ if (!trimmed) {
3084
+ return void 0;
3085
+ }
3086
+ const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
3087
+ const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
3088
+ return compactStatusText(filename, 80);
3089
+ }
3090
+ function extractStatusUrlDomain(value) {
3091
+ if (typeof value !== "string") {
3092
+ return void 0;
3093
+ }
3094
+ const trimmed = value.trim();
3095
+ if (!trimmed) {
3096
+ return void 0;
3097
+ }
3098
+ try {
3099
+ const parsed = new URL(trimmed);
3100
+ return parsed.hostname || void 0;
3101
+ } catch {
3102
+ return void 0;
3103
+ }
3104
+ }
3105
+
3106
+ // src/chat/slack/mrkdwn.ts
3107
+ function ensureBlockSpacing(text) {
3108
+ const codeBlockPattern = /^```/;
3109
+ const listItemPattern = /^[-*•]\s|^\d+\.\s/;
3110
+ const lines = text.split("\n");
3111
+ const result = [];
3112
+ let inCodeBlock = false;
3113
+ for (let i = 0; i < lines.length; i++) {
3114
+ const line = lines[i];
3115
+ const isCodeFence = codeBlockPattern.test(line.trimStart());
3116
+ if (isCodeFence) {
3117
+ if (!inCodeBlock) {
3118
+ const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
3119
+ if (prev2 !== void 0 && prev2.trim() !== "") {
3120
+ result.push("");
3121
+ }
3122
+ }
3123
+ inCodeBlock = !inCodeBlock;
3124
+ result.push(line);
3125
+ continue;
3126
+ }
3127
+ if (inCodeBlock) {
3128
+ result.push(line);
3129
+ continue;
3130
+ }
3131
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
3132
+ if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
3133
+ result.push("");
3134
+ }
3135
+ result.push(line);
3136
+ }
3137
+ return result.join("\n");
3138
+ }
3139
+ function renderSlackMrkdwn(text) {
3140
+ let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
3141
+ normalized = ensureBlockSpacing(normalized);
3142
+ return normalized.replace(/\n{3,}/g, "\n\n").trim();
3143
+ }
3144
+ function normalizeSlackStatusText(text) {
3145
+ const trimmed = text.trim();
3146
+ if (!trimmed) {
3147
+ return "";
3148
+ }
3149
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
3150
+ }
3151
+
3152
+ // src/chat/slack/output.ts
3153
+ var MAX_INLINE_CHARS = 2200;
3154
+ var MAX_INLINE_LINES = 45;
3155
+ var CONTINUED_MARKER = "\n\n[Continued below]";
3156
+ var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
3157
+ function countSlackLines(text) {
3158
+ if (!text) {
3159
+ return 0;
3160
+ }
3161
+ return text.split("\n").length;
3162
+ }
3163
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
3164
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
3165
+ }
3166
+ function findSplitIndex(text, maxChars) {
3167
+ if (text.length <= maxChars) {
3168
+ return text.length;
3169
+ }
3170
+ const bounded = text.slice(0, maxChars);
3171
+ const newlineIndex = bounded.lastIndexOf("\n");
3172
+ if (newlineIndex > 0) {
3173
+ return newlineIndex;
3174
+ }
3175
+ const spaceIndex = bounded.lastIndexOf(" ");
3176
+ if (spaceIndex > 0) {
3177
+ return spaceIndex;
3178
+ }
3179
+ return maxChars;
3180
+ }
3181
+ function splitByLineBudget(text, maxLines) {
3182
+ if (maxLines <= 0) {
3183
+ return "";
3184
+ }
3185
+ const lines = text.split("\n");
3186
+ if (lines.length <= maxLines) {
3187
+ return text;
3188
+ }
3189
+ return lines.slice(0, maxLines).join("\n");
3190
+ }
3191
+ function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
3192
+ return {
3193
+ maxChars: Math.max(1, maxChars - suffix.length),
3194
+ maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
3195
+ };
3196
+ }
3197
+ function forceSplitBudget(text, budget) {
3198
+ const lineCount = countSlackLines(text);
3199
+ return {
3200
+ maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
3201
+ maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
3202
+ };
3203
+ }
3204
+ function getFenceContinuation(text) {
3205
+ let open;
3206
+ for (const line of text.split("\n")) {
3207
+ const trimmed = line.trimStart();
3208
+ const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
3209
+ if (!openerMatch) {
3210
+ continue;
3211
+ }
2805
3212
  if (!open) {
2806
3213
  open = {
2807
3214
  fence: openerMatch[1],
@@ -2827,6 +3234,9 @@ function appendSlackSuffix(text, marker) {
2827
3234
  const carryover = getFenceContinuation(text);
2828
3235
  return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
2829
3236
  }
3237
+ function stripTrailingContinuationMarker(text) {
3238
+ return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
3239
+ }
2830
3240
  function takeSlackContinuationChunk(text, budget) {
2831
3241
  let { prefix, rest } = takeSlackInlinePrefix(text, budget);
2832
3242
  if (!rest) {
@@ -2902,7 +3312,7 @@ function takeSlackInlinePrefix(text, options) {
2902
3312
  };
2903
3313
  }
2904
3314
  function splitSlackReplyText(text, options) {
2905
- const normalized = normalizeForSlack(text);
3315
+ const normalized = renderSlackMrkdwn(text);
2906
3316
  if (!normalized) {
2907
3317
  return [];
2908
3318
  }
@@ -2924,21 +3334,16 @@ function splitSlackReplyText(text, options) {
2924
3334
  chunks.push(renderedPrefix);
2925
3335
  remaining = rest;
2926
3336
  }
3337
+ if (chunks.length === 2) {
3338
+ chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
3339
+ }
2927
3340
  return chunks;
2928
3341
  }
2929
- function getSlackInterruptionMarker() {
2930
- return INTERRUPTED_MARKER;
2931
- }
2932
3342
  function getSlackContinuationBudget() {
2933
3343
  return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
2934
3344
  }
2935
- function getSlackStreamingContinuationBudget() {
2936
- return reserveInlineBudgetForSuffix(
2937
- `${STREAMING_FENCE_CLOSE_GUARD}${CONTINUED_MARKER}`
2938
- );
2939
- }
2940
3345
  function buildSlackOutputMessage(text, files) {
2941
- const normalized = normalizeForSlack(text);
3346
+ const normalized = renderSlackMrkdwn(text);
2942
3347
  const fileCount = files?.length ?? 0;
2943
3348
  if (!normalized) {
2944
3349
  if (fileCount > 0) {
@@ -3401,12 +3806,12 @@ function buildSystemPrompt(params) {
3401
3806
  [
3402
3807
  "Always produce output that follows this contract:",
3403
3808
  `<output format="slack-mrkdwn" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`,
3404
- "- Use plain Slack-safe markdown (headings, bullets, short code blocks).",
3809
+ "- Use Slack-friendly markdown, not full CommonMark. Prefer bold section labels over markdown headings, and use bullets and short code blocks when helpful.",
3405
3810
  "- Keep normal responses brief and scannable.",
3406
3811
  "- If depth is needed, start with a concise summary and then provide fuller detail.",
3407
3812
  "- 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.",
3408
3813
  "- Do not narrate tool execution or repeated status updates in the visible reply.",
3409
- "- Avoid tables unless explicitly requested.",
3814
+ "- Avoid tables and markdown links like `[label](url)` unless explicitly requested. Prefer plain URLs or Slack-native entities when exact rendering matters.",
3410
3815
  "- End every turn with a final user-facing markdown response.",
3411
3816
  "</output>"
3412
3817
  ].join("\n")
@@ -5557,103 +5962,29 @@ function createSearchToolsTool(mcpToolManager, getActiveSkills) {
5557
5962
  // src/chat/tools/slack/channel-list-messages.ts
5558
5963
  import { Type as Type7 } from "@sinclair/typebox";
5559
5964
 
5560
- // src/chat/slack/emoji.ts
5561
- var SLACK_EMOJI_NAME_RE = /^(?:[a-z0-9_+-]+)(?:::(?:skin-tone-[2-6]))?$/;
5562
- function normalizeSlackEmojiName(value) {
5563
- const trimmed = value.trim().toLowerCase();
5564
- if (!trimmed) {
5565
- return null;
5566
- }
5567
- const normalized = trimmed.startsWith(":") && trimmed.endsWith(":") ? trimmed.slice(1, -1) : trimmed;
5568
- return SLACK_EMOJI_NAME_RE.test(normalized) ? normalized : null;
5569
- }
5570
-
5571
5965
  // src/chat/slack/channel.ts
5572
- async function postMessageToChannel(input) {
5966
+ async function listChannelMessages(input) {
5573
5967
  const client2 = getSlackClient();
5574
5968
  const channelId = normalizeSlackConversationId(input.channelId);
5575
5969
  if (!channelId) {
5576
- throw new Error(
5577
- "Slack channel message posting requires a valid channel ID"
5578
- );
5579
- }
5580
- const response = await withSlackRetries(
5581
- () => client2.chat.postMessage({
5582
- channel: channelId,
5583
- text: input.text,
5584
- mrkdwn: true
5585
- }),
5586
- 3,
5587
- { action: "chat.postMessage" }
5588
- );
5589
- if (!response.ts) {
5590
- throw new Error("Slack channel message posted without ts");
5970
+ throw new Error("Slack channel history lookup requires a valid channel ID");
5591
5971
  }
5592
- let permalink;
5593
- try {
5594
- const permalinkResponse = await withSlackRetries(
5595
- () => client2.chat.getPermalink({
5972
+ const targetLimit = Math.max(1, Math.min(input.limit, 1e3));
5973
+ const maxPages = Math.max(1, Math.min(input.maxPages ?? 5, 10));
5974
+ const messages = [];
5975
+ let cursor = input.cursor;
5976
+ let pages = 0;
5977
+ while (messages.length < targetLimit && pages < maxPages) {
5978
+ pages += 1;
5979
+ const pageLimit = Math.max(1, Math.min(200, targetLimit - messages.length));
5980
+ const response = await withSlackRetries(
5981
+ () => client2.conversations.history({
5596
5982
  channel: channelId,
5597
- message_ts: response.ts
5598
- }),
5599
- 3,
5600
- { action: "chat.getPermalink" }
5601
- );
5602
- permalink = permalinkResponse.permalink;
5603
- } catch {
5604
- }
5605
- return {
5606
- ts: response.ts,
5607
- permalink
5608
- };
5609
- }
5610
- async function addReactionToMessage(input) {
5611
- const client2 = getSlackClient();
5612
- const channelId = normalizeSlackConversationId(input.channelId);
5613
- if (!channelId) {
5614
- throw new Error("Slack reaction requires a valid channel ID");
5615
- }
5616
- const timestamp = input.timestamp.trim();
5617
- if (!timestamp) {
5618
- throw new Error("Slack reaction requires a target message timestamp");
5619
- }
5620
- const emoji = normalizeSlackEmojiName(input.emoji);
5621
- if (!emoji) {
5622
- throw new Error("Slack reaction requires a valid emoji alias name");
5623
- }
5624
- await withSlackRetries(
5625
- () => client2.reactions.add({
5626
- channel: channelId,
5627
- timestamp,
5628
- name: emoji
5629
- }),
5630
- 3,
5631
- { action: "reactions.add" }
5632
- );
5633
- return { ok: true };
5634
- }
5635
- async function listChannelMessages(input) {
5636
- const client2 = getSlackClient();
5637
- const channelId = normalizeSlackConversationId(input.channelId);
5638
- if (!channelId) {
5639
- throw new Error("Slack channel history lookup requires a valid channel ID");
5640
- }
5641
- const targetLimit = Math.max(1, Math.min(input.limit, 1e3));
5642
- const maxPages = Math.max(1, Math.min(input.maxPages ?? 5, 10));
5643
- const messages = [];
5644
- let cursor = input.cursor;
5645
- let pages = 0;
5646
- while (messages.length < targetLimit && pages < maxPages) {
5647
- pages += 1;
5648
- const pageLimit = Math.max(1, Math.min(200, targetLimit - messages.length));
5649
- const response = await withSlackRetries(
5650
- () => client2.conversations.history({
5651
- channel: channelId,
5652
- limit: pageLimit,
5653
- cursor,
5654
- oldest: input.oldest,
5655
- latest: input.latest,
5656
- inclusive: input.inclusive
5983
+ limit: pageLimit,
5984
+ cursor,
5985
+ oldest: input.oldest,
5986
+ latest: input.latest,
5987
+ inclusive: input.inclusive
5657
5988
  }),
5658
5989
  3,
5659
5990
  { action: "conversations.history" }
@@ -5841,9 +6172,10 @@ function createSlackChannelPostMessageTool(context, state) {
5841
6172
  deduplicated: true
5842
6173
  };
5843
6174
  }
5844
- const posted = await postMessageToChannel({
6175
+ const posted = await postSlackMessage({
5845
6176
  channelId: targetChannelId,
5846
- text
6177
+ text,
6178
+ includePermalink: true
5847
6179
  });
5848
6180
  const response = {
5849
6181
  ok: true,
@@ -7450,152 +7782,7 @@ function throwSandboxOperationError(action, error, includeMissingPath = false) {
7450
7782
  import { Sandbox } from "@vercel/sandbox";
7451
7783
  import { createBashTool as createBashTool2 } from "bash-tool";
7452
7784
 
7453
- // src/chat/runtime/status-format.ts
7454
- var SLACK_STATUS_MAX_LENGTH = 50;
7455
- function truncateWithEllipsis(text, maxLength) {
7456
- if (text.length <= maxLength) {
7457
- return text;
7458
- }
7459
- return `${text.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
7460
- }
7461
- function truncateStatusText(text) {
7462
- const trimmed = text.trim();
7463
- if (!trimmed) {
7464
- return "";
7465
- }
7466
- return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
7467
- }
7468
- function compactStatusPath(value) {
7469
- if (typeof value !== "string") {
7470
- return void 0;
7471
- }
7472
- const trimmed = value.trim();
7473
- if (!trimmed) {
7474
- return void 0;
7475
- }
7476
- if (trimmed.length <= 80) {
7477
- return trimmed;
7478
- }
7479
- return `...${trimmed.slice(-77)}`;
7480
- }
7481
- function compactStatusText(value, maxLength = 80) {
7482
- if (typeof value !== "string") {
7483
- return void 0;
7484
- }
7485
- const trimmed = value.trim();
7486
- if (!trimmed) {
7487
- return void 0;
7488
- }
7489
- return truncateWithEllipsis(trimmed, maxLength);
7490
- }
7491
- function readShellToken(command, startIndex) {
7492
- let index = startIndex;
7493
- while (index < command.length && /\s/.test(command[index] ?? "")) {
7494
- index += 1;
7495
- }
7496
- if (index >= command.length) {
7497
- return void 0;
7498
- }
7499
- let token = "";
7500
- let quote;
7501
- while (index < command.length) {
7502
- const char = command[index];
7503
- if (!char) {
7504
- break;
7505
- }
7506
- if (quote) {
7507
- if (char === quote) {
7508
- quote = void 0;
7509
- index += 1;
7510
- continue;
7511
- }
7512
- if (char === "\\" && quote === '"' && index + 1 < command.length) {
7513
- token += command[index + 1];
7514
- index += 2;
7515
- continue;
7516
- }
7517
- token += char;
7518
- index += 1;
7519
- continue;
7520
- }
7521
- if (/\s/.test(char)) {
7522
- break;
7523
- }
7524
- if (char === '"' || char === "'") {
7525
- quote = char;
7526
- index += 1;
7527
- continue;
7528
- }
7529
- if (char === "\\" && index + 1 < command.length) {
7530
- token += command[index + 1];
7531
- index += 2;
7532
- continue;
7533
- }
7534
- token += char;
7535
- index += 1;
7536
- }
7537
- return { token, nextIndex: index };
7538
- }
7539
- function compactStatusCommand(value) {
7540
- if (typeof value !== "string") {
7541
- return void 0;
7542
- }
7543
- const trimmed = value.trim();
7544
- if (!trimmed) {
7545
- return void 0;
7546
- }
7547
- let index = 0;
7548
- while (index < trimmed.length) {
7549
- const parsed = readShellToken(trimmed, index);
7550
- if (!parsed) {
7551
- return void 0;
7552
- }
7553
- index = parsed.nextIndex;
7554
- if (!parsed.token) {
7555
- continue;
7556
- }
7557
- if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(parsed.token)) {
7558
- continue;
7559
- }
7560
- const normalized = parsed.token.replace(/[\\/]+$/g, "");
7561
- if (!normalized) {
7562
- return void 0;
7563
- }
7564
- const parts = normalized.split(/[\\/]/).filter((part) => part.length > 0);
7565
- const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
7566
- return compactStatusText(command, 40);
7567
- }
7568
- return void 0;
7569
- }
7570
- function compactStatusFilename(value) {
7571
- if (typeof value !== "string") {
7572
- return void 0;
7573
- }
7574
- const trimmed = value.trim().replace(/[\\/]+$/g, "");
7575
- if (!trimmed) {
7576
- return void 0;
7577
- }
7578
- const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
7579
- const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
7580
- return compactStatusText(filename, 80);
7581
- }
7582
- function extractStatusUrlDomain(value) {
7583
- if (typeof value !== "string") {
7584
- return void 0;
7585
- }
7586
- const trimmed = value.trim();
7587
- if (!trimmed) {
7588
- return void 0;
7589
- }
7590
- try {
7591
- const parsed = new URL(trimmed);
7592
- return parsed.hostname || void 0;
7593
- } catch {
7594
- return void 0;
7595
- }
7596
- }
7597
-
7598
- // src/chat/runtime/assistant-status.ts
7785
+ // src/chat/slack/assistant-thread/status-render.ts
7599
7786
  var STATUS_PATTERNS = {
7600
7787
  thinking: {
7601
7788
  defaultContext: "\u2026",
@@ -7649,17 +7836,10 @@ var STATUS_PATTERNS = {
7649
7836
  function makeAssistantStatus(kind, context) {
7650
7837
  return { kind, ...context ? { context } : {} };
7651
7838
  }
7652
- function normalizeAssistantStatusText(text) {
7653
- const trimmed = text.trim();
7654
- if (!trimmed) {
7655
- return "";
7656
- }
7657
- return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
7658
- }
7659
- function buildAssistantStatusPresentation(args) {
7839
+ function renderAssistantStatus(args) {
7660
7840
  const random = args.random ?? Math.random;
7661
7841
  const pattern = STATUS_PATTERNS[args.status.kind];
7662
- const context = normalizeAssistantStatusText(args.status.context ?? "") || pattern.defaultContext;
7842
+ const context = normalizeSlackStatusText(args.status.context ?? "") || pattern.defaultContext;
7663
7843
  const index = Math.floor(random() * pattern.variants.length);
7664
7844
  const verb = pattern.variants[index] ?? pattern.variants[0];
7665
7845
  const visible = truncateStatusText(`${verb} ${context}`);
@@ -7671,46 +7851,274 @@ function buildAssistantStatusPresentation(args) {
7671
7851
  suggestions: Array.from(/* @__PURE__ */ new Set([visible, hint]))
7672
7852
  };
7673
7853
  }
7674
- function createSlackAdapterAssistantStatusTransport(args) {
7854
+
7855
+ // src/chat/slack/assistant-thread/status-scheduler.ts
7856
+ var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
7857
+ var STATUS_MIN_VISIBLE_MS = 1200;
7858
+ var STATUS_ROTATION_INTERVAL_MS = 3e4;
7859
+ function createAssistantStatusScheduler(args) {
7860
+ const now = args.now ?? (() => Date.now());
7861
+ const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
7862
+ const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
7863
+ const random = args.random ?? Math.random;
7864
+ let active = false;
7865
+ let currentKey = "";
7866
+ let currentStatus = makeAssistantStatus("thinking");
7867
+ let currentVisibleStatus = "";
7868
+ let lastStatusAt = 0;
7869
+ let pendingStatus = null;
7870
+ let pendingKey = "";
7871
+ let pendingTimer = null;
7872
+ let rotationTimer = null;
7873
+ let inflightStatusUpdate = Promise.resolve();
7874
+ const enqueueStatusUpdate = (task) => {
7875
+ const request = inflightStatusUpdate.catch(() => void 0).then(async () => {
7876
+ await task();
7877
+ });
7878
+ inflightStatusUpdate = request.catch(() => void 0);
7879
+ return request;
7880
+ };
7881
+ const scheduleRotation = () => {
7882
+ if (rotationTimer) {
7883
+ clearTimer(rotationTimer);
7884
+ rotationTimer = null;
7885
+ }
7886
+ if (!active || !currentVisibleStatus) {
7887
+ return;
7888
+ }
7889
+ rotationTimer = setTimer(() => {
7890
+ rotationTimer = null;
7891
+ if (!active || !currentVisibleStatus) {
7892
+ return;
7893
+ }
7894
+ void postRenderedStatus(currentStatus);
7895
+ }, STATUS_ROTATION_INTERVAL_MS);
7896
+ };
7897
+ const postStatus = async (text, suggestions) => {
7898
+ if (!text && !currentVisibleStatus) {
7899
+ return;
7900
+ }
7901
+ currentVisibleStatus = text;
7902
+ lastStatusAt = now();
7903
+ scheduleRotation();
7904
+ await enqueueStatusUpdate(async () => {
7905
+ await args.sendStatus(text, suggestions);
7906
+ });
7907
+ };
7908
+ const postRenderedStatus = async (status) => {
7909
+ const presentation = renderAssistantStatus({
7910
+ status,
7911
+ random
7912
+ });
7913
+ currentStatus = status;
7914
+ currentKey = presentation.key;
7915
+ await postStatus(presentation.visible, presentation.suggestions);
7916
+ };
7917
+ const clearPending = () => {
7918
+ if (pendingTimer) {
7919
+ clearTimer(pendingTimer);
7920
+ pendingTimer = null;
7921
+ }
7922
+ pendingStatus = null;
7923
+ pendingKey = "";
7924
+ };
7925
+ const flushPending = async () => {
7926
+ if (!active || !pendingStatus) {
7927
+ clearPending();
7928
+ return;
7929
+ }
7930
+ const next = pendingStatus;
7931
+ clearPending();
7932
+ const nextPresentation = renderAssistantStatus({
7933
+ status: next,
7934
+ random
7935
+ });
7936
+ if (nextPresentation.key !== currentKey) {
7937
+ await postRenderedStatus(next);
7938
+ }
7939
+ };
7675
7940
  return {
7676
- async setStatus(channelId, threadTs, status, suggestions) {
7677
- try {
7678
- await args.getSlackAdapter().setAssistantStatus(channelId, threadTs, status, suggestions);
7679
- } catch (error) {
7680
- logAssistantStatusFailure(status, error);
7941
+ start() {
7942
+ active = true;
7943
+ clearPending();
7944
+ currentStatus = makeAssistantStatus("thinking");
7945
+ currentKey = "";
7946
+ void postRenderedStatus(currentStatus);
7947
+ },
7948
+ async stop() {
7949
+ active = false;
7950
+ clearPending();
7951
+ if (rotationTimer) {
7952
+ clearTimer(rotationTimer);
7953
+ rotationTimer = null;
7954
+ }
7955
+ currentKey = "";
7956
+ await postStatus("");
7957
+ },
7958
+ update(status) {
7959
+ if (!active) {
7960
+ return;
7961
+ }
7962
+ const presentation = renderAssistantStatus({
7963
+ status,
7964
+ random
7965
+ });
7966
+ if (!presentation.visible) {
7967
+ return;
7968
+ }
7969
+ if (presentation.key === currentKey || presentation.key === pendingKey) {
7970
+ return;
7971
+ }
7972
+ const elapsed = now() - lastStatusAt;
7973
+ const waitMs = Math.max(
7974
+ STATUS_UPDATE_DEBOUNCE_MS - elapsed,
7975
+ STATUS_MIN_VISIBLE_MS - elapsed,
7976
+ 0
7977
+ );
7978
+ if (waitMs <= 0) {
7979
+ clearPending();
7980
+ void postRenderedStatus(status);
7981
+ return;
7982
+ }
7983
+ pendingStatus = status;
7984
+ pendingKey = presentation.key;
7985
+ if (pendingTimer) {
7986
+ return;
7681
7987
  }
7988
+ pendingTimer = setTimer(
7989
+ () => {
7990
+ pendingTimer = null;
7991
+ void flushPending();
7992
+ },
7993
+ Math.max(1, waitMs)
7994
+ );
7995
+ }
7996
+ };
7997
+ }
7998
+
7999
+ // src/chat/slack/assistant-thread/status-send.ts
8000
+ function createSlackAdapterStatusSender(args) {
8001
+ const adapter = args.getSlackAdapter();
8002
+ const boundToken = getSlackAdapterRequestToken(adapter);
8003
+ return async (text, suggestions) => {
8004
+ const channelId = args.channelId;
8005
+ const threadTs = args.threadTs;
8006
+ if (!channelId || !threadTs) {
8007
+ return;
8008
+ }
8009
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
8010
+ if (!normalizedChannelId) {
8011
+ return;
8012
+ }
8013
+ try {
8014
+ await runWithBoundSlackToken(
8015
+ adapter,
8016
+ boundToken,
8017
+ () => adapter.setAssistantStatus(
8018
+ normalizedChannelId,
8019
+ threadTs,
8020
+ text,
8021
+ suggestions
8022
+ )
8023
+ );
8024
+ } catch (error) {
8025
+ logAssistantStatusFailure({
8026
+ status: text,
8027
+ error,
8028
+ channelId,
8029
+ normalizedChannelId,
8030
+ threadTs
8031
+ });
8032
+ }
8033
+ };
8034
+ }
8035
+ function createSlackWebApiStatusSender(args) {
8036
+ const getClient2 = args.getSlackClient ?? getSlackClient;
8037
+ return async (text, suggestions) => {
8038
+ const channelId = args.channelId;
8039
+ const threadTs = args.threadTs;
8040
+ if (!channelId || !threadTs) {
8041
+ return;
7682
8042
  }
7683
- };
7684
- }
7685
- function createSlackWebApiAssistantStatusTransport(args) {
7686
- const getClient2 = args?.getSlackClient ?? getSlackClient;
7687
- return {
7688
- async setStatus(channelId, threadTs, status, suggestions) {
7689
- try {
7690
- await getClient2().assistant.threads.setStatus({
7691
- channel_id: channelId,
7692
- thread_ts: threadTs,
7693
- status,
7694
- ...suggestions ? { loading_messages: suggestions } : {}
7695
- });
7696
- } catch (error) {
7697
- logAssistantStatusFailure(status, error);
7698
- }
8043
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
8044
+ if (!normalizedChannelId) {
8045
+ return;
8046
+ }
8047
+ try {
8048
+ await getClient2().assistant.threads.setStatus({
8049
+ channel_id: normalizedChannelId,
8050
+ thread_ts: threadTs,
8051
+ status: text,
8052
+ ...suggestions ? { loading_messages: suggestions } : {}
8053
+ });
8054
+ } catch (error) {
8055
+ logAssistantStatusFailure({
8056
+ status: text,
8057
+ error,
8058
+ channelId,
8059
+ normalizedChannelId,
8060
+ threadTs
8061
+ });
7699
8062
  }
7700
8063
  };
7701
8064
  }
7702
- function logAssistantStatusFailure(status, error) {
8065
+ function getSlackAdapterRequestToken(adapter) {
8066
+ const token = adapter.requestContext?.getStore()?.token;
8067
+ if (typeof token !== "string") {
8068
+ return void 0;
8069
+ }
8070
+ const trimmed = token.trim();
8071
+ return trimmed || void 0;
8072
+ }
8073
+ async function runWithBoundSlackToken(adapter, token, task) {
8074
+ if (!token) {
8075
+ return await task();
8076
+ }
8077
+ return await adapter.withBotToken(token, task);
8078
+ }
8079
+ function logAssistantStatusFailure(args) {
7703
8080
  logWarn(
7704
8081
  "assistant_status_update_failed",
7705
8082
  {},
7706
8083
  {
7707
- "app.slack.status_text": status || "(clear)",
7708
- "error.message": error instanceof Error ? error.message : String(error)
8084
+ "app.slack.status_text": args.status || "(clear)",
8085
+ "app.slack.channel_id_raw": args.channelId,
8086
+ "app.slack.channel_id": args.normalizedChannelId,
8087
+ "app.slack.thread_ts": args.threadTs,
8088
+ "error.message": args.error instanceof Error ? args.error.message : String(args.error)
7709
8089
  },
7710
- "Failed to update assistant status"
8090
+ `Failed to update assistant status channel=${args.normalizedChannelId} raw=${args.channelId} thread=${args.threadTs}`
7711
8091
  );
7712
8092
  }
7713
8093
 
8094
+ // src/chat/slack/assistant-thread/status.ts
8095
+ function createSlackAdapterAssistantStatusSession(args) {
8096
+ return createAssistantStatusScheduler({
8097
+ sendStatus: createSlackAdapterStatusSender({
8098
+ channelId: args.channelId,
8099
+ threadTs: args.threadTs,
8100
+ getSlackAdapter: args.getSlackAdapter
8101
+ }),
8102
+ now: args.now,
8103
+ setTimer: args.setTimer,
8104
+ clearTimer: args.clearTimer,
8105
+ random: args.random
8106
+ });
8107
+ }
8108
+ function createSlackWebApiAssistantStatusSession(args) {
8109
+ return createAssistantStatusScheduler({
8110
+ sendStatus: createSlackWebApiStatusSender({
8111
+ channelId: args.channelId,
8112
+ threadTs: args.threadTs,
8113
+ getSlackClient: args.getSlackClient
8114
+ }),
8115
+ now: args.now,
8116
+ setTimer: args.setTimer,
8117
+ clearTimer: args.clearTimer,
8118
+ random: args.random
8119
+ });
8120
+ }
8121
+
7714
8122
  // src/chat/sandbox/skill-sync.ts
7715
8123
  import fs3 from "fs/promises";
7716
8124
  import path5 from "path";
@@ -7832,7 +8240,11 @@ function pickFields(record, csv) {
7832
8240
  }
7833
8241
 
7834
8242
  function outputJson(value) {
7835
- process.stdout.write(JSON.stringify(value, null, 2) + "\\n");
8243
+ fs.writeFileSync(process.stdout.fd, JSON.stringify(value, null, 2) + "\\n");
8244
+ }
8245
+
8246
+ function outputText(value) {
8247
+ fs.writeFileSync(process.stdout.fd, value);
7836
8248
  }
7837
8249
 
7838
8250
  function fallbackToRealGh() {
@@ -7848,12 +8260,12 @@ function fallbackToRealGh() {
7848
8260
  }
7849
8261
 
7850
8262
  if (args.length === 0 || args[0] === "--version" || args[0] === "version") {
7851
- process.stdout.write("gh version 2.0.0 (junior-eval)\\n");
8263
+ outputText("gh version 2.0.0 (junior-eval)\\n");
7852
8264
  process.exit(0);
7853
8265
  }
7854
8266
 
7855
8267
  if (args[0] === "auth" && args[1] === "status") {
7856
- process.stdout.write("github.com\\n \u2713 Logged in to github.com as junior-eval\\n");
8268
+ outputText("github.com\\n \u2713 Logged in to github.com as junior-eval\\n");
7857
8269
  process.exit(0);
7858
8270
  }
7859
8271
 
@@ -7877,7 +8289,7 @@ if (args[0] === "repo" && args[1] === "view") {
7877
8289
  if (jsonFields) {
7878
8290
  outputJson(pickFields(record, jsonFields));
7879
8291
  } else {
7880
- process.stdout.write(record.url + "\\n");
8292
+ outputText(record.url + "\\n");
7881
8293
  }
7882
8294
  process.exit(0);
7883
8295
  }
@@ -7929,7 +8341,7 @@ if (args[0] === "issue") {
7929
8341
  if (jsonFields) {
7930
8342
  outputJson(pickFields(record, jsonFields));
7931
8343
  } else {
7932
- process.stdout.write(record.url + "\\n");
8344
+ outputText(record.url + "\\n");
7933
8345
  }
7934
8346
  process.exit(0);
7935
8347
  }
@@ -7945,7 +8357,7 @@ if (args[0] === "issue") {
7945
8357
  if (jsonFields) {
7946
8358
  outputJson(pickFields(record, jsonFields));
7947
8359
  } else {
7948
- process.stdout.write(record.url + "\\n");
8360
+ outputText(record.url + "\\n");
7949
8361
  }
7950
8362
  process.exit(0);
7951
8363
  }
@@ -7962,7 +8374,7 @@ if (args[0] === "issue") {
7962
8374
  }
7963
8375
 
7964
8376
  if (subcommand === "comment") {
7965
- process.stdout.write(record.url + "#issuecomment-1\\n");
8377
+ outputText(record.url + "#issuecomment-1\\n");
7966
8378
  process.exit(0);
7967
8379
  }
7968
8380
 
@@ -9293,7 +9705,6 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
9293
9705
  // src/chat/services/reply-delivery-plan.ts
9294
9706
  var REACTION_ONLY_ACK_RE = /^(?::[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D]+)$/u;
9295
9707
  var REDUNDANT_REACTION_ACK_TEXT = ["done", "got it", "ok", "okay"];
9296
- var REACTION_ALIAS_PREFIX_RE = /^:[a-z0-9_+-]*$/i;
9297
9708
  function normalizeReactionAckText(text) {
9298
9709
  return text.trim().toLowerCase().replace(/[!.]+$/g, "");
9299
9710
  }
@@ -9310,24 +9721,11 @@ function isRedundantReactionAckText(text) {
9310
9721
  normalized
9311
9722
  );
9312
9723
  }
9313
- function isPotentialRedundantReactionAckText(text) {
9314
- const trimmed = text.trim();
9315
- if (!trimmed) {
9316
- return true;
9317
- }
9318
- if (REACTION_ONLY_ACK_RE.test(trimmed) || REACTION_ALIAS_PREFIX_RE.test(trimmed)) {
9319
- return true;
9320
- }
9321
- const normalized = normalizeReactionAckText(text);
9322
- return REDUNDANT_REACTION_ACK_TEXT.some(
9323
- (candidate) => candidate.startsWith(normalized)
9324
- );
9325
- }
9326
9724
  function buildReplyDeliveryPlan(args) {
9327
9725
  const mode = args.explicitChannelPostIntent && args.channelPostPerformed ? "channel_only" : "thread";
9328
9726
  let attachFiles = "none";
9329
9727
  if (args.hasFiles && mode === "thread") {
9330
- attachFiles = args.streamingThreadReply ? "followup" : "inline";
9728
+ attachFiles = "inline";
9331
9729
  }
9332
9730
  return {
9333
9731
  mode,
@@ -9335,27 +9733,6 @@ function buildReplyDeliveryPlan(args) {
9335
9733
  attachFiles
9336
9734
  };
9337
9735
  }
9338
- function resolveReplyDelivery(args) {
9339
- const replyHasFiles = Boolean(
9340
- args.reply.files && args.reply.files.length > 0
9341
- );
9342
- const deliveryPlan = args.reply.deliveryPlan ?? {
9343
- mode: args.reply.deliveryMode ?? "thread",
9344
- postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
9345
- attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
9346
- };
9347
- let attachFiles = replyHasFiles ? deliveryPlan.attachFiles : "none";
9348
- if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
9349
- attachFiles = "inline";
9350
- }
9351
- if (attachFiles === "inline" && args.hasStreamedThreadReply) {
9352
- attachFiles = "followup";
9353
- }
9354
- return {
9355
- shouldPostThreadReply: deliveryPlan.postThreadText,
9356
- attachFiles
9357
- };
9358
- }
9359
9736
 
9360
9737
  // src/chat/services/channel-intent.ts
9361
9738
  function isExplicitChannelPostIntent(text) {
@@ -9381,6 +9758,10 @@ function sentenceClaimsAttachment(sentence) {
9381
9758
  if (!hasAttachmentNoun) {
9382
9759
  return false;
9383
9760
  }
9761
+ 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);
9762
+ if (hasNegativeAttachmentPhrase) {
9763
+ return false;
9764
+ }
9384
9765
  const hasPositiveAttachmentVerb = /\b(attached|shared|uploaded|included)\b/i.test(sentence);
9385
9766
  const hasDeicticSharePhrase = /\bhere(?:'s| is)\b/i.test(sentence);
9386
9767
  return hasPositiveAttachmentVerb || hasDeicticSharePhrase;
@@ -9409,15 +9790,17 @@ function buildTurnResult(input) {
9409
9790
  toolCalls,
9410
9791
  sandboxId,
9411
9792
  sandboxDependencyProfileHash,
9412
- hasTextDeltaCallback,
9793
+ durationMs,
9413
9794
  shouldTrace,
9414
9795
  spanContext,
9796
+ usage,
9415
9797
  correlation,
9416
9798
  assistantUserName
9417
9799
  } = input;
9418
9800
  const toolResults = newMessages.filter(isToolResultMessage);
9419
9801
  const assistantMessages = newMessages.filter(isAssistantMessage);
9420
- const primaryText = assistantMessages.map((message) => extractAssistantText(message)).join("\n\n").trim();
9802
+ const terminalAssistantMessages = getTerminalAssistantMessages(newMessages);
9803
+ const primaryText = terminalAssistantMessages.map((message) => extractAssistantText(message)).join("\n\n").trim();
9421
9804
  const oauthStartedMessage = extractOAuthStartedMessageFromToolResults(toolResults);
9422
9805
  const toolErrorCount = toolResults.filter((result) => result.isError).length;
9423
9806
  const explicitChannelPostIntent = isExplicitChannelPostIntent(userInput);
@@ -9430,8 +9813,7 @@ function buildTurnResult(input) {
9430
9813
  const deliveryPlan = buildReplyDeliveryPlan({
9431
9814
  explicitChannelPostIntent,
9432
9815
  channelPostPerformed,
9433
- hasFiles: replyFiles.length > 0,
9434
- streamingThreadReply: hasTextDeltaCallback
9816
+ hasFiles: replyFiles.length > 0
9435
9817
  });
9436
9818
  const deliveryMode = deliveryPlan.mode;
9437
9819
  if (!primaryText && !oauthStartedMessage) {
@@ -9453,7 +9835,7 @@ function buildTurnResult(input) {
9453
9835
  "Model returned empty text response"
9454
9836
  );
9455
9837
  }
9456
- const lastAssistant = assistantMessages.at(-1);
9838
+ const lastAssistant = terminalAssistantMessages.at(-1);
9457
9839
  const stopReason = typeof lastAssistant?.stopReason === "string" ? lastAssistant.stopReason : void 0;
9458
9840
  const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
9459
9841
  const usedPrimaryText = Boolean(primaryText);
@@ -9486,6 +9868,8 @@ function buildTurnResult(input) {
9486
9868
  toolResultCount: toolResults.length,
9487
9869
  toolErrorCount,
9488
9870
  usedPrimaryText,
9871
+ durationMs,
9872
+ usage,
9489
9873
  stopReason,
9490
9874
  errorMessage,
9491
9875
  providerError: void 0
@@ -9779,6 +10163,16 @@ function createMcpAuthOrchestration(deps, abortAgent) {
9779
10163
 
9780
10164
  // src/chat/respond.ts
9781
10165
  var startupDiscoveryLogged = false;
10166
+ function buildOmittedImageAttachmentNotice(count) {
10167
+ return [
10168
+ "<omitted-image-attachments>",
10169
+ `count: ${count}`,
10170
+ "Slack included image attachments with this turn, but this runtime cannot analyze images because no vision model is configured.",
10171
+ "Do not claim that no image was attached.",
10172
+ "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.",
10173
+ "</omitted-image-attachments>"
10174
+ ].join("\n");
10175
+ }
9782
10176
  function mcpToolsToDefinitions(mcpTools) {
9783
10177
  const defs = {};
9784
10178
  for (const tool2 of mcpTools) {
@@ -9794,6 +10188,7 @@ function mcpToolsToDefinitions(mcpTools) {
9794
10188
  return defs;
9795
10189
  }
9796
10190
  async function generateAssistantReply(messageText, context = {}) {
10191
+ const replyStartedAtMs = Date.now();
9797
10192
  let timeoutResumeConversationId;
9798
10193
  let timeoutResumeSessionId;
9799
10194
  let timeoutResumeSliceId = 1;
@@ -9804,6 +10199,7 @@ async function generateAssistantReply(messageText, context = {}) {
9804
10199
  let mcpToolManager;
9805
10200
  let sandboxExecutor;
9806
10201
  let timedOut = false;
10202
+ let turnUsage;
9807
10203
  const getSandboxMetadata = () => sandboxExecutor ? {
9808
10204
  sandboxId: sandboxExecutor.getSandboxId(),
9809
10205
  sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
@@ -9850,6 +10246,8 @@ async function generateAssistantReply(messageText, context = {}) {
9850
10246
  let configurationValues;
9851
10247
  const userInput = messageText;
9852
10248
  if (shouldTrace) {
10249
+ const inboundAttachmentCount = context.inboundAttachmentCount ?? 0;
10250
+ const promptAttachmentCount = context.userAttachments?.length ?? 0;
9853
10251
  logInfo(
9854
10252
  "agent_message_in",
9855
10253
  spanContext,
@@ -9857,7 +10255,10 @@ async function generateAssistantReply(messageText, context = {}) {
9857
10255
  "app.message.kind": "user_inbound",
9858
10256
  "app.message.length": userInput.length,
9859
10257
  "app.message.input": summarizeMessageText(userInput),
9860
- "app.message.attachment_count": context.userAttachments?.length ?? 0,
10258
+ // Log both counts so image uploads filtered by vision/config do not
10259
+ // look indistinguishable from Slack ingress dropping attachments.
10260
+ "app.message.attachment_count": inboundAttachmentCount,
10261
+ "app.message.prompt_attachment_count": promptAttachmentCount,
9861
10262
  "messaging.message.id": context.correlation?.messageTs ?? ""
9862
10263
  },
9863
10264
  "Agent message received"
@@ -10110,6 +10511,13 @@ async function generateAssistantReply(messageText, context = {}) {
10110
10511
  threadParticipants: context.threadParticipants
10111
10512
  });
10112
10513
  const userContentParts = [{ type: "text", text: userTurnText }];
10514
+ const omittedImageAttachmentCount = context.omittedImageAttachmentCount ?? 0;
10515
+ if (omittedImageAttachmentCount > 0) {
10516
+ userContentParts.push({
10517
+ type: "text",
10518
+ text: buildOmittedImageAttachmentNotice(omittedImageAttachmentCount)
10519
+ });
10520
+ }
10113
10521
  for (const attachment of context.userAttachments ?? []) {
10114
10522
  if (attachment.promptText) {
10115
10523
  userContentParts.push({
@@ -10251,7 +10659,12 @@ async function generateAssistantReply(messageText, context = {}) {
10251
10659
  spanContext,
10252
10660
  async () => {
10253
10661
  let promptResult;
10254
- const promptPromise = resumedFromCheckpoint ? agent.continue() : agent.prompt({
10662
+ const promptPromise = resumedFromCheckpoint ? (
10663
+ // Checkpoint resumes continue from the persisted Pi message
10664
+ // state. Any reconstructed replyContext only matters when the
10665
+ // turn parked before the initial user prompt was recorded.
10666
+ agent.continue()
10667
+ ) : agent.prompt({
10255
10668
  role: "user",
10256
10669
  content: userContentParts,
10257
10670
  timestamp: Date.now()
@@ -10305,14 +10718,16 @@ async function generateAssistantReply(messageText, context = {}) {
10305
10718
  }
10306
10719
  const outputMessages = newMessages.filter(isAssistantMessage);
10307
10720
  const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
10308
- const usageAttributes = extractGenAiUsageAttributes(
10721
+ const usageSummary = extractGenAiUsageSummary(
10309
10722
  promptResult,
10310
10723
  agent.state,
10311
10724
  ...outputMessages
10312
10725
  );
10726
+ turnUsage = usageSummary.inputTokens !== void 0 || usageSummary.outputTokens !== void 0 || usageSummary.totalTokens !== void 0 ? usageSummary : void 0;
10313
10727
  setSpanAttributes({
10314
10728
  ...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
10315
- ...usageAttributes
10729
+ ...usageSummary.inputTokens !== void 0 ? { "gen_ai.usage.input_tokens": usageSummary.inputTokens } : {},
10730
+ ...usageSummary.outputTokens !== void 0 ? { "gen_ai.usage.output_tokens": usageSummary.outputTokens } : {}
10316
10731
  });
10317
10732
  },
10318
10733
  {
@@ -10345,10 +10760,11 @@ async function generateAssistantReply(messageText, context = {}) {
10345
10760
  toolCalls,
10346
10761
  sandboxId: currentSandboxExecutor.getSandboxId(),
10347
10762
  sandboxDependencyProfileHash: currentSandboxExecutor.getDependencyProfileHash(),
10763
+ durationMs: Date.now() - replyStartedAtMs,
10348
10764
  generatedFileCount: generatedFiles.length,
10349
- hasTextDeltaCallback: Boolean(context.onTextDelta),
10350
10765
  shouldTrace,
10351
10766
  spanContext,
10767
+ usage: turnUsage,
10352
10768
  correlation: context.correlation,
10353
10769
  assistantUserName: context.assistant?.userName
10354
10770
  });
@@ -10439,6 +10855,7 @@ async function generateAssistantReply(messageText, context = {}) {
10439
10855
  toolResultCount: 0,
10440
10856
  toolErrorCount: 0,
10441
10857
  usedPrimaryText: false,
10858
+ durationMs: Date.now() - replyStartedAtMs,
10442
10859
  errorMessage: message,
10443
10860
  providerError: error
10444
10861
  }
@@ -10459,150 +10876,84 @@ async function generateAssistantReply(messageText, context = {}) {
10459
10876
  }
10460
10877
  }
10461
10878
 
10462
- // src/chat/runtime/progress-reporter.ts
10463
- var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
10464
- var STATUS_MIN_VISIBLE_MS = 1200;
10465
- var STATUS_ROTATION_INTERVAL_MS = 3e4;
10466
- function createProgressReporter(args) {
10467
- const now = args.now ?? (() => Date.now());
10468
- const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
10469
- const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
10470
- const random = args.random ?? Math.random;
10471
- let active = false;
10472
- let currentKey = "";
10473
- let currentStatus = makeAssistantStatus("thinking");
10474
- let currentVisibleStatus = "";
10475
- let lastStatusAt = 0;
10476
- let pendingStatus = null;
10477
- let pendingKey = "";
10478
- let pendingTimer = null;
10479
- let rotationTimer = null;
10480
- let inflightStatusUpdate = Promise.resolve();
10481
- const scheduleRotation = () => {
10482
- if (rotationTimer) {
10483
- clearTimer(rotationTimer);
10484
- rotationTimer = null;
10485
- }
10486
- if (!active || !currentVisibleStatus) {
10487
- return;
10488
- }
10489
- rotationTimer = setTimer(() => {
10490
- rotationTimer = null;
10491
- if (!active || !currentVisibleStatus) {
10492
- return;
10493
- }
10494
- void postRenderedStatus(currentStatus);
10495
- }, STATUS_ROTATION_INTERVAL_MS);
10496
- };
10497
- const postStatus = async (text, suggestions) => {
10498
- const channelId = args.channelId;
10499
- const threadTs = args.threadTs;
10500
- if (!channelId || !threadTs) {
10501
- return;
10502
- }
10503
- if (!text && !currentVisibleStatus) {
10504
- return;
10505
- }
10506
- currentVisibleStatus = text;
10507
- lastStatusAt = now();
10508
- scheduleRotation();
10509
- const previous = inflightStatusUpdate;
10510
- const request = (async () => {
10511
- await previous;
10512
- await args.transport.setStatus(channelId, threadTs, text, suggestions);
10513
- })();
10514
- inflightStatusUpdate = request;
10515
- await request;
10516
- };
10517
- const postRenderedStatus = async (status) => {
10518
- const presentation = buildAssistantStatusPresentation({
10519
- status,
10520
- random
10879
+ // src/chat/slack/footer.ts
10880
+ function escapeSlackMrkdwn(text) {
10881
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
10882
+ }
10883
+ function formatSlackTokenCount(value) {
10884
+ return new Intl.NumberFormat("en-US").format(value);
10885
+ }
10886
+ function formatSlackDuration(durationMs) {
10887
+ if (durationMs < 1e3) {
10888
+ return `${durationMs}ms`;
10889
+ }
10890
+ const durationSeconds = durationMs / 1e3;
10891
+ if (durationSeconds < 10) {
10892
+ return `${durationSeconds.toFixed(1).replace(/\.0$/, "")}s`;
10893
+ }
10894
+ return `${Math.round(durationSeconds)}s`;
10895
+ }
10896
+ function resolveTotalTokens(usage) {
10897
+ if (usage?.totalTokens !== void 0) {
10898
+ return usage.totalTokens;
10899
+ }
10900
+ if (usage?.inputTokens !== void 0 && usage.outputTokens !== void 0) {
10901
+ return usage.inputTokens + usage.outputTokens;
10902
+ }
10903
+ return void 0;
10904
+ }
10905
+ function buildSlackReplyFooter(args) {
10906
+ const items = [];
10907
+ const conversationId = args.conversationId?.trim();
10908
+ if (conversationId) {
10909
+ items.push({
10910
+ label: "ID",
10911
+ value: conversationId
10521
10912
  });
10522
- currentStatus = status;
10523
- currentKey = presentation.key;
10524
- await postStatus(presentation.visible, presentation.suggestions);
10525
- };
10526
- const clearPending = () => {
10527
- if (pendingTimer) {
10528
- clearTimer(pendingTimer);
10529
- pendingTimer = null;
10530
- }
10531
- pendingStatus = null;
10532
- pendingKey = "";
10533
- };
10534
- const flushPending = async () => {
10535
- if (!active || !pendingStatus) {
10536
- clearPending();
10537
- return;
10538
- }
10539
- const next = pendingStatus;
10540
- clearPending();
10541
- const nextPresentation = buildAssistantStatusPresentation({
10542
- status: next,
10543
- random
10913
+ }
10914
+ const totalTokens = resolveTotalTokens(args.usage);
10915
+ if (totalTokens !== void 0) {
10916
+ items.push({
10917
+ label: "Tokens",
10918
+ value: formatSlackTokenCount(totalTokens)
10544
10919
  });
10545
- if (nextPresentation.key !== currentKey) {
10546
- await postRenderedStatus(next);
10547
- }
10548
- };
10549
- return {
10550
- async start() {
10551
- active = true;
10552
- clearPending();
10553
- currentStatus = makeAssistantStatus("thinking");
10554
- currentKey = "";
10555
- void postRenderedStatus(currentStatus);
10556
- },
10557
- async stop() {
10558
- active = false;
10559
- clearPending();
10560
- if (rotationTimer) {
10561
- clearTimer(rotationTimer);
10562
- rotationTimer = null;
10920
+ }
10921
+ if (typeof args.durationMs === "number" && Number.isFinite(args.durationMs)) {
10922
+ const durationMs = Math.max(0, Math.floor(args.durationMs));
10923
+ items.push({
10924
+ label: "Time",
10925
+ value: formatSlackDuration(durationMs)
10926
+ });
10927
+ }
10928
+ const traceId = args.traceId?.trim();
10929
+ if (traceId) {
10930
+ items.push({
10931
+ label: "Trace",
10932
+ value: traceId
10933
+ });
10934
+ }
10935
+ return items.length > 0 ? { items } : void 0;
10936
+ }
10937
+ function buildSlackReplyBlocks(text, footer) {
10938
+ if (!text.trim() || !footer?.items.length) {
10939
+ return void 0;
10940
+ }
10941
+ return [
10942
+ {
10943
+ type: "section",
10944
+ text: {
10945
+ type: "mrkdwn",
10946
+ text
10563
10947
  }
10564
- currentKey = "";
10565
- await postStatus("");
10566
10948
  },
10567
- async setStatus(status) {
10568
- if (!active) {
10569
- return;
10570
- }
10571
- const presentation = buildAssistantStatusPresentation({
10572
- status,
10573
- random
10574
- });
10575
- if (!presentation.visible) {
10576
- return;
10577
- }
10578
- if (presentation.key === currentKey || presentation.key === pendingKey) {
10579
- return;
10580
- }
10581
- const elapsed = now() - lastStatusAt;
10582
- const waitMs = Math.max(
10583
- STATUS_UPDATE_DEBOUNCE_MS - elapsed,
10584
- STATUS_MIN_VISIBLE_MS - elapsed,
10585
- 0
10586
- );
10587
- if (waitMs <= 0) {
10588
- clearPending();
10589
- void postRenderedStatus(status);
10590
- return;
10591
- }
10592
- pendingStatus = status;
10593
- pendingKey = presentation.key;
10594
- if (pendingTimer) {
10595
- return;
10596
- }
10597
- pendingTimer = setTimer(
10598
- () => {
10599
- pendingTimer = null;
10600
- void flushPending();
10601
- },
10602
- Math.max(1, waitMs)
10603
- );
10949
+ {
10950
+ type: "context",
10951
+ elements: footer.items.map((item) => ({
10952
+ type: "mrkdwn",
10953
+ text: `*${escapeSlackMrkdwn(item.label)}:* ${escapeSlackMrkdwn(item.value)}`
10954
+ }))
10604
10955
  }
10605
- };
10956
+ ];
10606
10957
  }
10607
10958
 
10608
10959
  // src/chat/slack/reply.ts
@@ -10610,21 +10961,35 @@ import { Buffer as Buffer2 } from "buffer";
10610
10961
  function isInterruptedVisibleReply(reply) {
10611
10962
  return reply.diagnostics.outcome === "provider_error";
10612
10963
  }
10613
- function buildChunkMessage(chunk, files) {
10964
+ function resolveReplyDelivery(reply) {
10965
+ const replyHasFiles = Boolean(reply.files && reply.files.length > 0);
10966
+ const deliveryPlan = reply.deliveryPlan ?? {
10967
+ mode: reply.deliveryMode ?? "thread",
10968
+ postThreadText: (reply.deliveryMode ?? "thread") !== "channel_only",
10969
+ attachFiles: replyHasFiles ? "inline" : "none"
10970
+ };
10614
10971
  return {
10615
- markdown: chunk,
10616
- ...files ? { files } : {}
10972
+ shouldPostThreadReply: deliveryPlan.postThreadText,
10973
+ attachFiles: replyHasFiles && deliveryPlan.attachFiles !== "none" ? "inline" : "none"
10617
10974
  };
10618
10975
  }
10976
+ function buildReplyText(text) {
10977
+ const message = buildSlackOutputMessage(text);
10978
+ if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
10979
+ return message.markdown;
10980
+ }
10981
+ if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
10982
+ return message.raw;
10983
+ }
10984
+ return "";
10985
+ }
10619
10986
  function buildTextPosts(args) {
10620
10987
  const chunks = splitSlackReplyText(args.text, {
10621
10988
  interrupted: args.interrupted
10622
10989
  });
10623
10990
  return chunks.map((chunk, index) => ({
10624
- message: buildChunkMessage(
10625
- chunk,
10626
- index === 0 ? args.firstFiles : void 0
10627
- ),
10991
+ text: chunk,
10992
+ ...index === 0 && args.firstFiles ? { files: args.firstFiles } : {},
10628
10993
  stage: index === 0 ? args.firstStage ?? "thread_reply" : "thread_reply_continuation"
10629
10994
  }));
10630
10995
  }
@@ -10646,150 +11011,100 @@ async function normalizeFileUploads(files) {
10646
11011
  })
10647
11012
  );
10648
11013
  }
10649
- async function uploadReplyFilesBestEffort(args) {
11014
+ function findLastTextPostIndex(posts) {
11015
+ for (let index = posts.length - 1; index >= 0; index -= 1) {
11016
+ if (posts[index]?.text.trim().length) {
11017
+ return index;
11018
+ }
11019
+ }
11020
+ return -1;
11021
+ }
11022
+ async function uploadReplyFiles(args) {
10650
11023
  try {
10651
11024
  await uploadFilesToThread({
10652
11025
  channelId: args.channelId,
10653
11026
  threadTs: args.threadTs,
10654
11027
  files: await normalizeFileUploads(args.files)
10655
11028
  });
10656
- } catch {
10657
- }
10658
- }
10659
- function getReplyMessageText(message) {
10660
- if (typeof message !== "object" || message === null) {
10661
- return void 0;
10662
- }
10663
- if ("markdown" in message && typeof message.markdown === "string") {
10664
- return message.markdown;
10665
- }
10666
- if ("raw" in message && typeof message.raw === "string") {
10667
- return message.raw;
10668
- }
10669
- return void 0;
10670
- }
10671
- function getReplyMessageFiles(message) {
10672
- if (typeof message === "object" && message !== null && "files" in message && Array.isArray(message.files)) {
10673
- return message.files;
10674
- }
10675
- return void 0;
10676
- }
10677
- function createSlackStreamAccumulator() {
10678
- let pendingCarriageReturn = false;
10679
- let streamedVisibleText = "";
10680
- let streamedRenderedText = "";
10681
- let overflowText = "";
10682
- let streamOverflowed = false;
10683
- const continuationBudget = getSlackStreamingContinuationBudget();
10684
- const normalizeDelta = (deltaText) => {
10685
- let text = deltaText;
10686
- if (pendingCarriageReturn) {
10687
- text = `\r${text}`;
10688
- pendingCarriageReturn = false;
10689
- }
10690
- if (text.endsWith("\r")) {
10691
- text = text.slice(0, -1);
10692
- pendingCarriageReturn = true;
10693
- }
10694
- return text.replace(/\r\n?/g, "\n");
10695
- };
10696
- return {
10697
- append(deltaText) {
10698
- const normalizedDeltaText = normalizeDelta(deltaText);
10699
- if (!normalizedDeltaText) {
10700
- return "";
10701
- }
10702
- if (streamOverflowed) {
10703
- overflowText += normalizedDeltaText;
10704
- return "";
10705
- }
10706
- const candidate = `${streamedVisibleText}${normalizedDeltaText}`;
10707
- const { prefix, renderedPrefix, rest } = takeSlackContinuationPrefix(
10708
- candidate,
10709
- continuationBudget
10710
- );
10711
- const additional = renderedPrefix.length > streamedRenderedText.length ? renderedPrefix.slice(streamedRenderedText.length) : "";
10712
- streamedVisibleText = prefix;
10713
- streamedRenderedText = renderedPrefix;
10714
- if (rest) {
10715
- overflowText += rest;
10716
- streamOverflowed = true;
10717
- }
10718
- return additional;
10719
- },
10720
- getOverflowText() {
10721
- return overflowText;
11029
+ } catch (error) {
11030
+ if (args.failureMode === "strict") {
11031
+ throw error;
10722
11032
  }
10723
- };
11033
+ }
10724
11034
  }
10725
11035
  function planSlackReplyPosts(args) {
10726
11036
  const replyFiles = args.reply.files && args.reply.files.length > 0 ? args.reply.files : void 0;
10727
- const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
10728
- reply: args.reply,
10729
- hasStreamedThreadReply: args.hasStreamedThreadReply
10730
- });
11037
+ const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery(
11038
+ args.reply
11039
+ );
10731
11040
  const interrupted = isInterruptedVisibleReply(args.reply);
10732
11041
  const posts = [];
10733
- if (args.hasStreamedThreadReply) {
10734
- if (shouldPostThreadReply && args.streamedOverflowText) {
10735
- posts.push(
10736
- ...buildTextPosts({
10737
- text: args.streamedOverflowText,
10738
- interrupted,
10739
- firstStage: "thread_reply_continuation"
10740
- })
10741
- );
10742
- } else if (shouldPostThreadReply && interrupted) {
10743
- posts.push({
10744
- message: buildSlackOutputMessage(
10745
- getSlackInterruptionMarker().trimStart()
10746
- ),
10747
- stage: "thread_reply_continuation"
10748
- });
10749
- }
10750
- } else {
10751
- const textPosts = shouldPostThreadReply ? buildTextPosts({
10752
- text: args.reply.text,
10753
- interrupted,
10754
- firstFiles: attachFiles === "inline" ? replyFiles : void 0
10755
- }) : [];
10756
- posts.push(...textPosts);
10757
- if (attachFiles === "inline" && replyFiles && textPosts.length === 0) {
10758
- posts.push({
10759
- message: buildSlackOutputMessage("", replyFiles),
10760
- stage: "thread_reply"
10761
- });
10762
- } else if (shouldPostThreadReply && textPosts.length === 0) {
10763
- posts.push({
10764
- message: buildSlackOutputMessage(args.reply.text),
10765
- stage: "thread_reply"
10766
- });
10767
- }
11042
+ const textPosts = shouldPostThreadReply ? buildTextPosts({
11043
+ text: args.reply.text,
11044
+ interrupted,
11045
+ firstFiles: attachFiles === "inline" ? replyFiles : void 0
11046
+ }) : [];
11047
+ posts.push(...textPosts);
11048
+ if (attachFiles === "inline" && replyFiles && textPosts.length === 0) {
11049
+ posts.push({
11050
+ files: replyFiles,
11051
+ stage: "thread_reply",
11052
+ text: ""
11053
+ });
11054
+ } else if (shouldPostThreadReply && textPosts.length === 0) {
11055
+ posts.push({
11056
+ text: buildReplyText(args.reply.text),
11057
+ stage: "thread_reply"
11058
+ });
10768
11059
  }
10769
11060
  if (attachFiles === "followup" && replyFiles) {
10770
11061
  posts.push({
10771
- message: buildSlackOutputMessage("", replyFiles),
10772
- stage: "thread_reply_files_followup"
11062
+ files: replyFiles,
11063
+ stage: "thread_reply_files_followup",
11064
+ text: ""
10773
11065
  });
10774
11066
  }
10775
11067
  return posts;
10776
11068
  }
10777
11069
  async function postSlackApiReplyPosts(args) {
10778
- for (const post of args.posts) {
10779
- const text = getReplyMessageText(post.message);
10780
- if (text && text.trim().length > 0) {
10781
- await args.postMessage(args.channelId, args.threadTs, text);
10782
- }
10783
- const files = getReplyMessageFiles(post.message);
10784
- if (!files?.length) {
10785
- continue;
11070
+ const lastTextPostIndex = findLastTextPostIndex(args.posts);
11071
+ let lastPostedMessageTs;
11072
+ for (const [index, post] of args.posts.entries()) {
11073
+ const hasVisibleDelivery = post.text.trim().length > 0 || post.files?.length;
11074
+ if (hasVisibleDelivery) {
11075
+ await args.beforePost?.();
11076
+ }
11077
+ let messageTs;
11078
+ try {
11079
+ if (post.text.trim().length > 0) {
11080
+ const response = await postSlackMessage({
11081
+ channelId: args.channelId,
11082
+ threadTs: args.threadTs,
11083
+ text: post.text,
11084
+ ...index === lastTextPostIndex && args.footer ? { blocks: buildSlackReplyBlocks(post.text, args.footer) } : {}
11085
+ });
11086
+ messageTs = response.ts;
11087
+ lastPostedMessageTs = response.ts;
11088
+ }
11089
+ if (!post.files?.length) {
11090
+ continue;
11091
+ }
11092
+ await uploadReplyFiles({
11093
+ channelId: args.channelId,
11094
+ failureMode: args.fileUploadFailureMode ?? "best_effort",
11095
+ threadTs: args.threadTs,
11096
+ files: post.files
11097
+ });
11098
+ } catch (error) {
11099
+ await args.onPostError?.({
11100
+ error,
11101
+ messageTs,
11102
+ stage: post.stage
11103
+ });
11104
+ throw error;
10786
11105
  }
10787
- await uploadReplyFilesBestEffort({
10788
- channelId: args.channelId,
10789
- threadTs: args.threadTs,
10790
- files
10791
- });
10792
11106
  }
11107
+ return lastPostedMessageTs;
10793
11108
  }
10794
11109
 
10795
11110
  // src/chat/slack/resume.ts
@@ -10804,16 +11119,13 @@ function resolveReplyTimeoutMs(explicitTimeoutMs) {
10804
11119
  const parsed = Number.parseInt(raw, 10);
10805
11120
  return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
10806
11121
  }
10807
- async function postSlackMessage(channelId, threadTs, text) {
10808
- await getSlackClient().chat.postMessage({
10809
- channel: channelId,
10810
- thread_ts: threadTs,
10811
- text
10812
- });
10813
- }
10814
11122
  async function postSlackMessageBestEffort(channelId, threadTs, text) {
10815
11123
  try {
10816
- await postSlackMessage(channelId, threadTs, text);
11124
+ await postSlackMessage({
11125
+ channelId,
11126
+ threadTs,
11127
+ text
11128
+ });
10817
11129
  } catch {
10818
11130
  }
10819
11131
  }
@@ -10852,7 +11164,7 @@ var ResumeTurnBusyError = class extends Error {
10852
11164
  function getDefaultLockKey(channelId, threadTs) {
10853
11165
  return `slack:${channelId}:${threadTs}`;
10854
11166
  }
10855
- function createResumeReplyContext(args, progress) {
11167
+ function createResumeReplyContext(args, statusSession) {
10856
11168
  const replyContext = args.replyContext ?? {};
10857
11169
  const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
10858
11170
  const persistedChannelConfiguration = replyContext.channelConfiguration ?? (replyContext.configuration ? createReadOnlyConfigService(replyContext.configuration) : void 0);
@@ -10881,9 +11193,9 @@ function createResumeReplyContext(args, progress) {
10881
11193
  await persistThreadStateById(threadId, { artifacts });
10882
11194
  await replyContext.onArtifactStateUpdated?.(artifacts);
10883
11195
  },
10884
- onStatus: async (status) => {
10885
- await progress.setStatus(status);
10886
- await replyContext.onStatus?.(status);
11196
+ onStatus: async (nextStatus) => {
11197
+ statusSession.update(nextStatus);
11198
+ await replyContext.onStatus?.(nextStatus);
10887
11199
  }
10888
11200
  };
10889
11201
  }
@@ -10902,10 +11214,9 @@ async function resumeSlackTurn(args) {
10902
11214
  if (!lock) {
10903
11215
  throw new ResumeTurnBusyError(lockKey);
10904
11216
  }
10905
- const progress = createProgressReporter({
11217
+ const status = createSlackWebApiAssistantStatusSession({
10906
11218
  channelId: args.channelId,
10907
- threadTs: args.threadTs,
10908
- transport: createSlackWebApiAssistantStatusTransport()
11219
+ threadTs: args.threadTs
10909
11220
  });
10910
11221
  let deferredPauseHandler;
10911
11222
  let deferredFailureHandler;
@@ -10917,9 +11228,9 @@ async function resumeSlackTurn(args) {
10917
11228
  args.initialText
10918
11229
  );
10919
11230
  }
10920
- await progress.start();
11231
+ status.start();
10921
11232
  const generateReply = args.generateReply ?? generateAssistantReply;
10922
- const replyContext = createResumeReplyContext(args, progress);
11233
+ const replyContext = createResumeReplyContext(args, status);
10923
11234
  const replyPromise = generateReply(args.messageText, {
10924
11235
  ...replyContext
10925
11236
  });
@@ -10937,19 +11248,23 @@ async function resumeSlackTurn(args) {
10937
11248
  )
10938
11249
  )
10939
11250
  ]) : await replyPromise;
10940
- await progress.stop();
11251
+ await status.stop();
11252
+ const footer = buildSlackReplyFooter({
11253
+ conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
11254
+ durationMs: reply.diagnostics.durationMs,
11255
+ traceId: getActiveTraceId(),
11256
+ usage: reply.diagnostics.usage
11257
+ });
10941
11258
  await postSlackApiReplyPosts({
10942
11259
  channelId: args.channelId,
10943
11260
  threadTs: args.threadTs,
10944
- posts: planSlackReplyPosts({
10945
- reply,
10946
- hasStreamedThreadReply: false
10947
- }),
10948
- postMessage: postSlackMessage
11261
+ posts: planSlackReplyPosts({ reply }),
11262
+ fileUploadFailureMode: "best_effort",
11263
+ footer
10949
11264
  });
10950
11265
  await args.onSuccess?.(reply);
10951
11266
  } catch (error) {
10952
- await progress.stop();
11267
+ await status.stop();
10953
11268
  if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
10954
11269
  deferredPauseHandler = async () => {
10955
11270
  await args.onAuthPause?.(error);
@@ -11275,7 +11590,8 @@ async function resumeAuthorizedMcpTurn(args) {
11275
11590
  configuration: authSession.configuration,
11276
11591
  channelConfiguration,
11277
11592
  sandbox: getPersistedSandboxState(currentState),
11278
- threadParticipants: buildThreadParticipants(conversation.messages)
11593
+ threadParticipants: buildThreadParticipants(conversation.messages),
11594
+ ...getTurnUserReplyAttachmentContext(userMessage)
11279
11595
  },
11280
11596
  onSuccess: async (reply) => {
11281
11597
  try {
@@ -11746,11 +12062,11 @@ async function GET5(request, provider, waitUntil) {
11746
12062
  } else if (stored.channelId && stored.threadTs) {
11747
12063
  const { channelId, threadTs } = stored;
11748
12064
  waitUntil(
11749
- () => postSlackMessage(
12065
+ () => postSlackMessage({
11750
12066
  channelId,
11751
12067
  threadTs,
11752
- `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
11753
- )
12068
+ text: `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
12069
+ })
11754
12070
  );
11755
12071
  }
11756
12072
  const statusMessage = stored.pendingMessage ? "Your request is being processed in Slack." : "You can close this tab and return to Slack.";
@@ -11929,7 +12245,8 @@ async function resumeTimedOutTurn(payload) {
11929
12245
  conversationContext,
11930
12246
  channelConfiguration,
11931
12247
  sandbox,
11932
- threadParticipants: buildThreadParticipants(conversation.messages)
12248
+ threadParticipants: buildThreadParticipants(conversation.messages),
12249
+ ...getTurnUserReplyAttachmentContext(userMessage)
11933
12250
  },
11934
12251
  onSuccess: async (reply) => {
11935
12252
  try {
@@ -12386,11 +12703,30 @@ function getRunId(thread, message) {
12386
12703
  return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
12387
12704
  }
12388
12705
  function getChannelId(thread, message) {
12389
- return thread.channelId ?? resolveSlackChannelIdFromMessage(message);
12706
+ return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
12390
12707
  }
12391
12708
  function getThreadTs(threadId) {
12392
12709
  return parseSlackThreadId(threadId)?.threadTs;
12393
12710
  }
12711
+ function getAssistantThreadContext(message) {
12712
+ const raw = message.raw;
12713
+ const rawRecord = raw && typeof raw === "object" ? raw : void 0;
12714
+ const channelId = toOptionalString(rawRecord?.channel);
12715
+ if (channelId) {
12716
+ const rawThreadTs = toOptionalString(rawRecord?.thread_ts);
12717
+ const threadTs = isDmChannel(channelId) ? rawThreadTs : rawThreadTs ?? toOptionalString(rawRecord?.ts);
12718
+ if (threadTs) {
12719
+ return { channelId, threadTs };
12720
+ }
12721
+ }
12722
+ const parsedThreadId = parseSlackThreadId(
12723
+ toOptionalString(message.threadId)
12724
+ );
12725
+ if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
12726
+ return void 0;
12727
+ }
12728
+ return parsedThreadId;
12729
+ }
12394
12730
  function getMessageTs(message) {
12395
12731
  const directTs = toOptionalString(
12396
12732
  message.ts
@@ -12743,7 +13079,7 @@ function createSlackTurnRuntime(deps) {
12743
13079
  },
12744
13080
  async handleAssistantContextChanged(event) {
12745
13081
  try {
12746
- await deps.initializeAssistantThread({
13082
+ await deps.refreshAssistantThreadContext({
12747
13083
  threadId: event.threadId,
12748
13084
  channelId: event.channelId,
12749
13085
  threadTs: event.threadTs,
@@ -12886,13 +13222,16 @@ var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
12886
13222
  var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
12887
13223
  var MAX_VISION_SUMMARY_CHARS = 500;
12888
13224
  function hasPotentialImageAttachment(attachments) {
12889
- return attachments?.some((attachment) => {
13225
+ return countPotentialImageAttachments(attachments) > 0;
13226
+ }
13227
+ function countPotentialImageAttachments(attachments) {
13228
+ return attachments?.filter((attachment) => {
12890
13229
  if (attachment.type === "image") {
12891
13230
  return true;
12892
13231
  }
12893
13232
  const mimeType = attachment.mimeType ?? "";
12894
13233
  return attachment.type === "file" && mimeType.startsWith("image/");
12895
- }) ?? false;
13234
+ }).length ?? 0;
12896
13235
  }
12897
13236
  function isVisionEnabled() {
12898
13237
  return Boolean(botConfig.visionModelId);
@@ -13426,45 +13765,71 @@ function createJuniorRuntimeServices(overrides = {}) {
13426
13765
  };
13427
13766
  }
13428
13767
 
13429
- // src/chat/runtime/streaming.ts
13430
- function createTextStreamBridge() {
13431
- const queue = [];
13432
- let ended = false;
13433
- let wakeConsumer = null;
13434
- const iterable = {
13435
- async *[Symbol.asyncIterator]() {
13436
- while (!ended || queue.length > 0) {
13437
- if (queue.length > 0) {
13438
- yield queue.shift();
13439
- continue;
13440
- }
13441
- await new Promise((resolve) => {
13442
- wakeConsumer = resolve;
13443
- });
13444
- }
13445
- }
13446
- };
13447
- return {
13448
- iterable,
13449
- push(delta) {
13450
- if (!delta || ended) {
13451
- return;
13452
- }
13453
- queue.push(delta);
13454
- const wake = wakeConsumer;
13455
- wakeConsumer = null;
13456
- wake?.();
13457
- },
13458
- end() {
13459
- if (ended) {
13460
- return;
13768
+ // src/chat/slack/assistant-thread/title.ts
13769
+ function maybeUpdateAssistantTitle(args) {
13770
+ const assistantThreadContext = args.assistantThreadContext;
13771
+ if (!assistantThreadContext?.channelId || !assistantThreadContext.threadTs || !isDmChannel(assistantThreadContext.channelId)) {
13772
+ return Promise.resolve(void 0);
13773
+ }
13774
+ const titleSourceMessage = getThreadTitleSourceMessage(args.conversation);
13775
+ if (!titleSourceMessage) {
13776
+ return Promise.resolve(void 0);
13777
+ }
13778
+ if (args.artifacts.assistantTitleSourceMessageId === titleSourceMessage.id) {
13779
+ return Promise.resolve(void 0);
13780
+ }
13781
+ return (async () => {
13782
+ try {
13783
+ const title = await args.generateThreadTitle(titleSourceMessage.text);
13784
+ await args.getSlackAdapter().setAssistantTitle(
13785
+ assistantThreadContext.channelId,
13786
+ assistantThreadContext.threadTs,
13787
+ title
13788
+ );
13789
+ return titleSourceMessage.id;
13790
+ } catch (error) {
13791
+ const slackErrorCode = getSlackApiErrorCode(error);
13792
+ const assistantTitleErrorAttributes = {
13793
+ "app.slack.assistant_title.outcome": "permission_denied",
13794
+ ...slackErrorCode ? {
13795
+ "app.slack.assistant_title.error_code": slackErrorCode
13796
+ } : {}
13797
+ };
13798
+ if (isSlackTitlePermissionError(error)) {
13799
+ setSpanAttributes(assistantTitleErrorAttributes);
13800
+ logError(
13801
+ "thread_title_generation_permission_denied",
13802
+ {
13803
+ slackThreadId: args.threadId,
13804
+ slackUserId: args.requesterId,
13805
+ slackChannelId: args.channelId,
13806
+ runId: args.runId,
13807
+ assistantUserName: args.assistantUserName,
13808
+ modelId: args.modelId
13809
+ },
13810
+ assistantTitleErrorAttributes,
13811
+ "Skipping thread title update due to Slack permission error"
13812
+ );
13813
+ return titleSourceMessage.id;
13461
13814
  }
13462
- ended = true;
13463
- const wake = wakeConsumer;
13464
- wakeConsumer = null;
13465
- wake?.();
13815
+ logWarn(
13816
+ "thread_title_generation_failed",
13817
+ {
13818
+ slackThreadId: args.threadId,
13819
+ slackUserId: args.requesterId,
13820
+ slackChannelId: args.channelId,
13821
+ runId: args.runId,
13822
+ assistantUserName: args.assistantUserName,
13823
+ modelId: args.modelId
13824
+ },
13825
+ {
13826
+ "error.message": error instanceof Error ? error.message : String(error)
13827
+ },
13828
+ "Thread title generation failed"
13829
+ );
13830
+ return void 0;
13466
13831
  }
13467
- };
13832
+ })();
13468
13833
  }
13469
13834
 
13470
13835
  // src/chat/runtime/reply-executor.ts
@@ -13481,14 +13846,6 @@ function getExecutionFailureReason(reply) {
13481
13846
  }
13482
13847
  return "empty assistant turn";
13483
13848
  }
13484
- function shouldAutoStartStreaming(args) {
13485
- const { text, deltaCount } = args;
13486
- const trimmed = text.trim();
13487
- if (!trimmed || isPotentialRedundantReactionAckText(trimmed)) {
13488
- return false;
13489
- }
13490
- return deltaCount >= 2;
13491
- }
13492
13849
  function createReplyToThread(deps) {
13493
13850
  return async function replyToThread(thread, message, options = {}) {
13494
13851
  if (message.author.isMe) {
@@ -13497,6 +13854,7 @@ function createReplyToThread(deps) {
13497
13854
  const threadId = getThreadId(thread, message);
13498
13855
  const channelId = getChannelId(thread, message);
13499
13856
  const threadTs = getThreadTs(threadId);
13857
+ const assistantThreadContext = getAssistantThreadContext(message);
13500
13858
  const messageTs = getMessageTs(message);
13501
13859
  const runId = getRunId(thread, message);
13502
13860
  const conversationId = threadId ?? runId;
@@ -13516,7 +13874,6 @@ function createReplyToThread(deps) {
13516
13874
  const userText = stripLeadingBotMention(message.text, {
13517
13875
  stripLeadingSlackMentionToken: options.explicitMention || Boolean(message.isMention)
13518
13876
  });
13519
- const explicitChannelPostIntent = isExplicitChannelPostIntent(userText);
13520
13877
  const preparedState = options.preparedState ?? await deps.prepareTurnState({
13521
13878
  thread,
13522
13879
  message,
@@ -13587,20 +13944,13 @@ function createReplyToThread(deps) {
13587
13944
  messageTs: slackMessageTs
13588
13945
  }
13589
13946
  );
13590
- const progress = createProgressReporter({
13591
- channelId,
13592
- threadTs,
13593
- transport: createSlackAdapterAssistantStatusTransport({
13594
- getSlackAdapter: deps.getSlackAdapter
13595
- })
13947
+ const omittedImageAttachmentCount = !isVisionEnabled() && hasPotentialImageAttachment(message.attachments) ? countPotentialImageAttachments(message.attachments) : 0;
13948
+ const status = createSlackAdapterAssistantStatusSession({
13949
+ channelId: assistantThreadContext?.channelId,
13950
+ threadTs: assistantThreadContext?.threadTs,
13951
+ getSlackAdapter: deps.getSlackAdapter
13596
13952
  });
13597
- const textStream = createTextStreamBridge();
13598
- let streamedReplyPromise;
13599
- let pendingStreamText = "";
13600
- let pendingStreamDeltaCount = 0;
13601
- let awaitingPostToolAssistantMessage = false;
13602
13953
  let beforeFirstResponsePostCalled = false;
13603
- let streamedReplyState = createSlackStreamAccumulator();
13604
13954
  const beforeFirstResponsePost = async () => {
13605
13955
  if (beforeFirstResponsePostCalled) {
13606
13956
  return;
@@ -13608,70 +13958,6 @@ function createReplyToThread(deps) {
13608
13958
  beforeFirstResponsePostCalled = true;
13609
13959
  await options.beforeFirstResponsePost?.();
13610
13960
  };
13611
- const startStreamingReply = () => {
13612
- if (!streamedReplyPromise) {
13613
- const streamingReply = (async () => {
13614
- return await postThreadReply(
13615
- textStream.iterable,
13616
- "streaming_initial_post"
13617
- );
13618
- })();
13619
- streamedReplyPromise = streamingReply;
13620
- }
13621
- };
13622
- const flushPendingStreamText = () => {
13623
- if (!pendingStreamText) {
13624
- return;
13625
- }
13626
- startStreamingReply();
13627
- textStream.push(pendingStreamText);
13628
- pendingStreamText = "";
13629
- pendingStreamDeltaCount = 0;
13630
- };
13631
- const clearPendingStreamText = () => {
13632
- pendingStreamText = "";
13633
- pendingStreamDeltaCount = 0;
13634
- };
13635
- const discardPendingStreamPreview = () => {
13636
- clearPendingStreamText();
13637
- streamedReplyState = createSlackStreamAccumulator();
13638
- };
13639
- const finalizePendingStreamText = () => {
13640
- if (!pendingStreamText || streamedReplyPromise || isPotentialRedundantReactionAckText(pendingStreamText)) {
13641
- return;
13642
- }
13643
- flushPendingStreamText();
13644
- };
13645
- const queueVisibleStreamText = (text) => {
13646
- if (!text) {
13647
- return;
13648
- }
13649
- if (awaitingPostToolAssistantMessage) {
13650
- return;
13651
- }
13652
- if (streamedReplyPromise) {
13653
- textStream.push(text);
13654
- return;
13655
- }
13656
- pendingStreamText += text;
13657
- pendingStreamDeltaCount += 1;
13658
- if (isPotentialRedundantReactionAckText(pendingStreamText)) {
13659
- return;
13660
- }
13661
- if (!shouldAutoStartStreaming({
13662
- text: pendingStreamText,
13663
- deltaCount: pendingStreamDeltaCount
13664
- })) {
13665
- return;
13666
- }
13667
- flushPendingStreamText();
13668
- };
13669
- const appendVisibleStreamDelta = (deltaText) => {
13670
- if (awaitingPostToolAssistantMessage && !streamedReplyPromise) {
13671
- return;
13672
- }
13673
- queueVisibleStreamText(streamedReplyState.append(deltaText));
13674
- };
13675
13961
  const postThreadReply = async (payload, stage) => {
13676
13962
  await beforeFirstResponsePost();
13677
13963
  try {
@@ -13691,7 +13977,20 @@ function createReplyToThread(deps) {
13691
13977
  throw error;
13692
13978
  }
13693
13979
  };
13694
- await progress.start();
13980
+ status.start();
13981
+ const assistantTitleTask = maybeUpdateAssistantTitle({
13982
+ assistantThreadContext,
13983
+ assistantUserName: botConfig.userName,
13984
+ artifacts: preparedState.artifacts,
13985
+ channelId,
13986
+ conversation: preparedState.conversation,
13987
+ generateThreadTitle: deps.services.generateThreadTitle,
13988
+ getSlackAdapter: deps.getSlackAdapter,
13989
+ modelId: botConfig.fastModelId,
13990
+ requesterId: message.author.userId,
13991
+ runId,
13992
+ threadId
13993
+ });
13695
13994
  let persistedAtLeastOnce = false;
13696
13995
  let shouldPersistFailureState = true;
13697
13996
  try {
@@ -13712,6 +14011,8 @@ function createReplyToThread(deps) {
13712
14011
  artifactState: preparedState.artifacts,
13713
14012
  configuration: preparedState.configuration,
13714
14013
  channelConfiguration: preparedState.channelConfiguration,
14014
+ inboundAttachmentCount: message.attachments.length,
14015
+ omittedImageAttachmentCount,
13715
14016
  userAttachments,
13716
14017
  correlation: {
13717
14018
  conversationId,
@@ -13738,33 +14039,8 @@ function createReplyToThread(deps) {
13738
14039
  await persistThreadState(thread, { artifacts });
13739
14040
  },
13740
14041
  threadParticipants,
13741
- onStatus: (status) => progress.setStatus(status),
13742
- onTextDelta: (deltaText) => {
13743
- if (explicitChannelPostIntent) {
13744
- return;
13745
- }
13746
- appendVisibleStreamDelta(deltaText);
13747
- },
13748
- onAssistantMessageStart: () => {
13749
- if (!awaitingPostToolAssistantMessage) {
13750
- return;
13751
- }
13752
- awaitingPostToolAssistantMessage = false;
13753
- discardPendingStreamPreview();
13754
- },
13755
- onToolCall: () => {
13756
- if (!streamedReplyPromise) {
13757
- awaitingPostToolAssistantMessage = true;
13758
- discardPendingStreamPreview();
13759
- }
13760
- }
14042
+ onStatus: (nextStatus) => status.update(nextStatus)
13761
14043
  });
13762
- if (streamedReplyPromise) {
13763
- flushPendingStreamText();
13764
- } else {
13765
- finalizePendingStreamText();
13766
- }
13767
- textStream.end();
13768
14044
  const diagnosticsContext = {
13769
14045
  slackThreadId: threadId,
13770
14046
  slackUserId: message.author.userId,
@@ -13839,30 +14115,73 @@ function createReplyToThread(deps) {
13839
14115
  const reactionPerformed = reply.diagnostics.toolCalls.includes(
13840
14116
  "slackMessageAddReaction"
13841
14117
  );
13842
- const plannedPosts = planSlackReplyPosts({
13843
- reply,
13844
- hasStreamedThreadReply: Boolean(streamedReplyPromise),
13845
- streamedOverflowText: streamedReplyState.getOverflowText()
14118
+ const plannedPosts = planSlackReplyPosts({ reply });
14119
+ const replyFooter = buildSlackReplyFooter({
14120
+ conversationId,
14121
+ durationMs: reply.diagnostics.durationMs,
14122
+ traceId: getActiveTraceId(),
14123
+ usage: reply.diagnostics.usage
13846
14124
  });
13847
- if (streamedReplyPromise) {
13848
- await streamedReplyPromise;
13849
- }
14125
+ const shouldUseSlackFooter = Boolean(replyFooter) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
13850
14126
  if (plannedPosts.length > 0) {
13851
- if (!streamedReplyPromise) {
13852
- let sent;
13853
- for (const post of plannedPosts) {
13854
- sent = await postThreadReply(post.message, post.stage);
14127
+ let sent;
14128
+ if (shouldUseSlackFooter) {
14129
+ const slackChannelId = channelId;
14130
+ const slackThreadTs = threadTs;
14131
+ if (!slackChannelId || !slackThreadTs) {
14132
+ throw new Error(
14133
+ "Slack footer delivery requires a concrete channel and thread timestamp"
14134
+ );
13855
14135
  }
13856
- const firstPlannedMessage = plannedPosts[0]?.message;
13857
- const firstPlannedMessageHasFiles = typeof firstPlannedMessage === "object" && firstPlannedMessage !== null && "files" in firstPlannedMessage && Array.isArray(firstPlannedMessage.files) && firstPlannedMessage.files.length > 0;
13858
- if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
13859
- await sent.delete();
14136
+ const sentMessageTs = await postSlackApiReplyPosts({
14137
+ beforePost: beforeFirstResponsePost,
14138
+ channelId: slackChannelId,
14139
+ threadTs: slackThreadTs,
14140
+ posts: plannedPosts,
14141
+ fileUploadFailureMode: "strict",
14142
+ footer: replyFooter,
14143
+ onPostError: ({ error, messageTs: messageTs2, stage }) => {
14144
+ logException(
14145
+ error,
14146
+ "slack_thread_post_failed",
14147
+ turnTraceContext,
14148
+ {
14149
+ "app.slack.reply_stage": stage,
14150
+ ...messageTs2 ? { "messaging.message.id": messageTs2 } : {},
14151
+ ...getSlackErrorObservabilityAttributes(error)
14152
+ },
14153
+ "Failed to post Slack thread reply"
14154
+ );
14155
+ }
14156
+ });
14157
+ if (sentMessageTs) {
14158
+ sent = {
14159
+ id: sentMessageTs,
14160
+ text: reply.text,
14161
+ delete: async () => {
14162
+ await deleteSlackMessage({
14163
+ channelId: slackChannelId,
14164
+ timestamp: sentMessageTs
14165
+ });
14166
+ }
14167
+ };
13860
14168
  }
13861
14169
  } else {
13862
14170
  for (const post of plannedPosts) {
13863
- await postThreadReply(post.message, post.stage);
14171
+ sent = await postThreadReply(
14172
+ buildSlackOutputMessage(post.text, post.files),
14173
+ post.stage
14174
+ );
13864
14175
  }
13865
14176
  }
14177
+ const firstPlannedMessageHasFiles = (plannedPosts[0]?.files?.length ?? 0) > 0;
14178
+ if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
14179
+ await sent.delete();
14180
+ }
14181
+ }
14182
+ const titleUpdateResult = await assistantTitleTask;
14183
+ if (titleUpdateResult) {
14184
+ artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult;
13866
14185
  }
13867
14186
  const shouldPersistArtifacts = Object.keys(artifactStatePatch).length > 0;
13868
14187
  const nextArtifacts = shouldPersistArtifacts ? mergeArtifactsState(preparedState.artifacts, artifactStatePatch) : void 0;
@@ -13891,73 +14210,17 @@ function createReplyToThread(deps) {
13891
14210
  "Agent turn completed"
13892
14211
  );
13893
14212
  }
13894
- const isFirstAssistantReply = preparedState.conversation.stats.compactedMessageCount === 0 && preparedState.conversation.messages.filter(
13895
- (m) => m.role === "assistant"
13896
- ).length === 1;
13897
- if (isFirstAssistantReply && channelId && isDmChannel(channelId) && threadTs) {
13898
- void deps.services.generateThreadTitle(userText, reply.text).then(
13899
- (title) => deps.getSlackAdapter().setAssistantTitle(channelId, threadTs, title)
13900
- ).catch((error) => {
13901
- const slackErrorCode = getSlackApiErrorCode(error);
13902
- const assistantTitleErrorAttributes = {
13903
- "app.slack.assistant_title.outcome": "permission_denied",
13904
- ...slackErrorCode ? { "app.slack.assistant_title.error_code": slackErrorCode } : {}
13905
- };
13906
- if (isSlackTitlePermissionError(error)) {
13907
- setSpanAttributes(assistantTitleErrorAttributes);
13908
- logError(
13909
- "thread_title_generation_permission_denied",
13910
- {
13911
- slackThreadId: threadId,
13912
- slackUserId: message.author.userId,
13913
- slackChannelId: channelId,
13914
- runId,
13915
- assistantUserName: botConfig.userName,
13916
- modelId: botConfig.fastModelId
13917
- },
13918
- assistantTitleErrorAttributes,
13919
- "Skipping thread title update due to Slack permission error"
13920
- );
13921
- return;
13922
- }
13923
- logWarn(
13924
- "thread_title_generation_failed",
13925
- {
13926
- slackThreadId: threadId,
13927
- slackUserId: message.author.userId,
13928
- slackChannelId: channelId,
13929
- runId,
13930
- assistantUserName: botConfig.userName,
13931
- modelId: botConfig.fastModelId
13932
- },
13933
- {
13934
- "error.message": error instanceof Error ? error.message : String(error)
13935
- },
13936
- "Thread title generation failed"
13937
- );
13938
- });
13939
- }
13940
14213
  } catch (error) {
13941
14214
  if (isRetryableTurnError(error, "mcp_auth_resume")) {
13942
14215
  shouldPersistFailureState = false;
13943
14216
  throw error;
13944
14217
  }
13945
14218
  if (isRetryableTurnError(error, "turn_timeout_resume")) {
13946
- textStream.end();
13947
- const hasVisibleAssistantOutput = Boolean(streamedReplyPromise);
13948
- if (hasVisibleAssistantOutput) {
13949
- logWarn(
13950
- "agent_turn_timeout_resume_skipped_after_visible_output",
13951
- turnTraceContext,
13952
- messageTs ? { "messaging.message.id": messageTs } : {},
13953
- "Skipped automatic timeout resume because assistant text had already started streaming"
13954
- );
13955
- }
13956
14219
  const conversationIdForResume = error.metadata?.conversationId;
13957
14220
  const sessionIdForResume = error.metadata?.sessionId;
13958
14221
  const checkpointVersion = error.metadata?.checkpointVersion;
13959
14222
  const nextSliceId = error.metadata?.sliceId;
13960
- if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
14223
+ if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
13961
14224
  try {
13962
14225
  await deps.services.scheduleTurnTimeoutResume({
13963
14226
  conversationId: conversationIdForResume,
@@ -13978,7 +14241,7 @@ function createReplyToThread(deps) {
13978
14241
  "Failed to schedule timeout resume callback"
13979
14242
  );
13980
14243
  }
13981
- } else if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
14244
+ } else if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
13982
14245
  logWarn(
13983
14246
  "agent_turn_timeout_resume_slice_limit_reached",
13984
14247
  turnTraceContext,
@@ -14000,7 +14263,6 @@ function createReplyToThread(deps) {
14000
14263
  shouldPersistFailureState = true;
14001
14264
  throw error;
14002
14265
  } finally {
14003
- textStream.end();
14004
14266
  if (!persistedAtLeastOnce && shouldPersistFailureState) {
14005
14267
  markTurnFailed({
14006
14268
  conversation: preparedState.conversation,
@@ -14025,19 +14287,26 @@ function createReplyToThread(deps) {
14025
14287
  );
14026
14288
  }
14027
14289
  }
14028
- await progress.stop();
14290
+ await status.stop();
14029
14291
  }
14030
14292
  }
14031
14293
  );
14032
14294
  };
14033
14295
  }
14034
14296
 
14035
- // src/chat/runtime/assistant-lifecycle.ts
14297
+ // src/chat/slack/assistant-thread/lifecycle.ts
14036
14298
  import { ThreadImpl } from "chat";
14037
- async function initializeAssistantThread(event) {
14299
+ async function syncAssistantThreadContext(event, options) {
14300
+ const channelId = normalizeSlackConversationId(event.channelId);
14301
+ if (!channelId) {
14302
+ throw new Error("Assistant thread initialization requires a channel ID");
14303
+ }
14304
+ const sourceChannelId = event.sourceChannelId ? normalizeSlackConversationId(event.sourceChannelId) : void 0;
14038
14305
  const slack = event.getSlackAdapter();
14039
- await slack.setAssistantTitle(event.channelId, event.threadTs, "Junior");
14040
- await slack.setSuggestedPrompts(event.channelId, event.threadTs, [
14306
+ if (options.setInitialTitle) {
14307
+ await slack.setAssistantTitle(channelId, event.threadTs, "Junior");
14308
+ }
14309
+ await slack.setSuggestedPrompts(channelId, event.threadTs, [
14041
14310
  {
14042
14311
  title: "Summarize thread",
14043
14312
  message: "Summarize the latest discussion in this thread."
@@ -14048,24 +14317,30 @@ async function initializeAssistantThread(event) {
14048
14317
  message: "Generate an image based on this conversation."
14049
14318
  }
14050
14319
  ]);
14051
- if (!event.sourceChannelId) {
14320
+ if (!sourceChannelId) {
14052
14321
  return;
14053
14322
  }
14054
14323
  const thread = ThreadImpl.fromJSON({
14055
14324
  _type: "chat:Thread",
14056
14325
  adapterName: "slack",
14057
- channelId: event.channelId,
14326
+ channelId,
14058
14327
  id: event.threadId,
14059
- isDM: event.channelId.startsWith("D")
14328
+ isDM: channelId.startsWith("D")
14060
14329
  });
14061
14330
  const currentArtifacts = coerceThreadArtifactsState(await thread.state);
14062
14331
  const nextArtifacts = mergeArtifactsState(currentArtifacts, {
14063
- assistantContextChannelId: event.sourceChannelId
14332
+ assistantContextChannelId: sourceChannelId
14064
14333
  });
14065
14334
  await persistThreadState(thread, {
14066
14335
  artifacts: nextArtifacts
14067
14336
  });
14068
14337
  }
14338
+ async function initializeAssistantThread(event) {
14339
+ await syncAssistantThreadContext(event, { setInitialTitle: true });
14340
+ }
14341
+ async function refreshAssistantThreadContext(event) {
14342
+ await syncAssistantThreadContext(event, { setInitialTitle: false });
14343
+ }
14069
14344
 
14070
14345
  // src/chat/runtime/turn-preparation.ts
14071
14346
  function hasPendingImageHydration(conversation) {
@@ -14093,6 +14368,7 @@ function createPrepareTurnState(deps) {
14093
14368
  const messageHasPotentialImageAttachment = hasPotentialImageAttachment(
14094
14369
  args.message.attachments
14095
14370
  );
14371
+ const imageAttachmentCount = messageHasPotentialImageAttachment ? countPotentialImageAttachments(args.message.attachments) : 0;
14096
14372
  const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
14097
14373
  const slackTs = getSlackMessageTs(args.message);
14098
14374
  const incomingUserMessage = {
@@ -14107,7 +14383,9 @@ function createPrepareTurnState(deps) {
14107
14383
  isBot: typeof args.message.author.isBot === "boolean" ? args.message.author.isBot : void 0
14108
14384
  },
14109
14385
  meta: {
14386
+ attachmentCount: args.message.attachments.length,
14110
14387
  explicitMention: args.explicitMention,
14388
+ imageAttachmentCount: imageAttachmentCount > 0 ? imageAttachmentCount : void 0,
14111
14389
  slackTs,
14112
14390
  imagesHydrated: !messageHasPotentialImageAttachment
14113
14391
  }
@@ -14261,6 +14539,20 @@ function createSlackRuntime(options) {
14261
14539
  sourceChannelId,
14262
14540
  getSlackAdapter: options.getSlackAdapter
14263
14541
  });
14542
+ },
14543
+ refreshAssistantThreadContext: async ({
14544
+ threadId,
14545
+ channelId,
14546
+ threadTs,
14547
+ sourceChannelId
14548
+ }) => {
14549
+ await refreshAssistantThreadContext({
14550
+ threadId,
14551
+ channelId,
14552
+ threadTs,
14553
+ sourceChannelId,
14554
+ getSlackAdapter: options.getSlackAdapter
14555
+ });
14264
14556
  }
14265
14557
  });
14266
14558
  }
@@ -14333,14 +14625,14 @@ var JuniorChat = class extends Chat {
14333
14625
  (async () => {
14334
14626
  try {
14335
14627
  const message = await messageOrFactory();
14336
- const normalized2 = normalizeIncomingSlackThreadId(
14628
+ const normalized = normalizeIncomingSlackThreadId(
14337
14629
  threadId,
14338
14630
  message
14339
14631
  );
14340
- if (normalized2 !== threadId && "threadId" in message) {
14341
- message.threadId = normalized2;
14632
+ if (normalized !== threadId && "threadId" in message) {
14633
+ message.threadId = normalized;
14342
14634
  }
14343
- super.processMessage(adapter, normalized2, message, options);
14635
+ super.processMessage(adapter, normalized, message, options);
14344
14636
  } catch (error) {
14345
14637
  runtime.logger?.error?.("Message factory resolution error", {
14346
14638
  error,
@@ -14351,14 +14643,19 @@ var JuniorChat = class extends Chat {
14351
14643
  );
14352
14644
  return;
14353
14645
  }
14354
- const normalized = normalizeIncomingSlackThreadId(
14355
- threadId,
14356
- messageOrFactory
14646
+ enqueueBackgroundTask(
14647
+ options,
14648
+ (async () => {
14649
+ const normalized = normalizeIncomingSlackThreadId(
14650
+ threadId,
14651
+ messageOrFactory
14652
+ );
14653
+ if (normalized !== threadId && "threadId" in messageOrFactory) {
14654
+ messageOrFactory.threadId = normalized;
14655
+ }
14656
+ super.processMessage(adapter, normalized, messageOrFactory, options);
14657
+ })()
14357
14658
  );
14358
- if (normalized !== threadId && "threadId" in messageOrFactory) {
14359
- messageOrFactory.threadId = normalized;
14360
- }
14361
- super.processMessage(adapter, normalized, messageOrFactory, options);
14362
14659
  }
14363
14660
  processReaction(event, options) {
14364
14661
  const runtime = this;
@@ -14379,20 +14676,19 @@ var JuniorChat = class extends Chat {
14379
14676
  }
14380
14677
  processAction(event, options) {
14381
14678
  const runtime = this;
14382
- enqueueBackgroundTask(
14383
- options,
14384
- (async () => {
14385
- try {
14386
- await runtime.handleActionEvent(event);
14387
- } catch (error) {
14388
- runtime.logger?.error?.("Action processing error", {
14389
- error,
14390
- actionId: event.actionId,
14391
- messageId: event.messageId
14392
- });
14393
- }
14394
- })()
14395
- );
14679
+ const task = (async () => {
14680
+ try {
14681
+ await runtime.handleActionEvent(event, options);
14682
+ } catch (error) {
14683
+ runtime.logger?.error?.("Action processing error", {
14684
+ error,
14685
+ actionId: event.actionId,
14686
+ messageId: event.messageId
14687
+ });
14688
+ }
14689
+ })();
14690
+ enqueueBackgroundTask(options, task);
14691
+ return task;
14396
14692
  }
14397
14693
  processModalClose(event, contextId, options) {
14398
14694
  const runtime = this;
@@ -14430,7 +14726,7 @@ var JuniorChat = class extends Chat {
14430
14726
  options,
14431
14727
  (async () => {
14432
14728
  try {
14433
- await runtime.handleSlashCommandEvent(event);
14729
+ await runtime.handleSlashCommandEvent(event, options);
14434
14730
  } catch (error) {
14435
14731
  runtime.logger?.error?.("Slash command processing error", {
14436
14732
  error,
@@ -14501,160 +14797,8 @@ var JuniorChat = class extends Chat {
14501
14797
  import {
14502
14798
  createSlackAdapter
14503
14799
  } from "@chat-adapter/slack";
14504
- import {
14505
- StreamingMarkdownRenderer
14506
- } from "chat";
14507
- var STREAM_BUFFER_SIZE = 64;
14508
- var CLIENT_STREAM_PATCHED = /* @__PURE__ */ Symbol("junior.slack.client_stream_patched");
14509
- var ADAPTER_STREAM_PATCHED = /* @__PURE__ */ Symbol("junior.slack.adapter_stream_patched");
14510
- function assertSlackAdapterInternals(internals) {
14511
- if (!internals.client || typeof internals.client.chatStream !== "function") {
14512
- throw new Error("Slack adapter client does not expose chatStream()");
14513
- }
14514
- if (typeof internals.stream !== "function") {
14515
- throw new Error("Slack adapter does not expose stream()");
14516
- }
14517
- if (typeof internals.decodeThreadId !== "function") {
14518
- throw new Error("Slack adapter does not expose decodeThreadId()");
14519
- }
14520
- if (typeof internals.getToken !== "function") {
14521
- throw new Error("Slack adapter does not expose getToken()");
14522
- }
14523
- if (!internals.logger || typeof internals.logger.debug !== "function" || typeof internals.logger.warn !== "function") {
14524
- throw new Error("Slack adapter does not expose logger debug/warn methods");
14525
- }
14526
- }
14527
- function shouldEagerFlushPlainText(text) {
14528
- return text.length > 0 && !text.includes("\n") && !/[`*~[\]|]/.test(text);
14529
- }
14530
- function getNextRenderableDelta(renderer, lastAppended) {
14531
- const committable = renderer.getCommittableText();
14532
- if (committable.startsWith(lastAppended)) {
14533
- const delta = committable.slice(lastAppended.length);
14534
- if (delta) {
14535
- return { delta, nextAppended: committable };
14536
- }
14537
- }
14538
- const rawText = renderer.getText();
14539
- if (shouldEagerFlushPlainText(rawText) && rawText.startsWith(lastAppended) && rawText.length > lastAppended.length) {
14540
- return {
14541
- delta: rawText.slice(lastAppended.length),
14542
- nextAppended: rawText
14543
- };
14544
- }
14545
- return { delta: "", nextAppended: lastAppended };
14546
- }
14547
- function patchSlackClientStream(adapter) {
14548
- const internals = adapter;
14549
- const { client: client2 } = internals;
14550
- if (client2[CLIENT_STREAM_PATCHED]) {
14551
- return;
14552
- }
14553
- const originalChatStream = client2.chatStream.bind(client2);
14554
- client2.chatStream = (params) => originalChatStream({
14555
- ...params,
14556
- buffer_size: STREAM_BUFFER_SIZE
14557
- });
14558
- client2[CLIENT_STREAM_PATCHED] = true;
14559
- }
14560
- function patchSlackAdapterStream(adapter) {
14561
- const internals = adapter;
14562
- if (internals[ADAPTER_STREAM_PATCHED]) {
14563
- return;
14564
- }
14565
- const originalStream = internals.stream.bind(adapter);
14566
- internals.stream = async function(threadId, textStream, options) {
14567
- if (!(options?.recipientUserId && options?.recipientTeamId)) {
14568
- return originalStream(threadId, textStream, options);
14569
- }
14570
- const { channel, threadTs } = internals.decodeThreadId(threadId);
14571
- internals.logger.debug("Slack: starting stream", { channel, threadTs });
14572
- const token = internals.getToken();
14573
- const streamer = internals.client.chatStream({
14574
- channel,
14575
- thread_ts: threadTs,
14576
- recipient_user_id: options.recipientUserId,
14577
- recipient_team_id: options.recipientTeamId,
14578
- ...options.taskDisplayMode ? { task_display_mode: options.taskDisplayMode } : {}
14579
- });
14580
- let first = true;
14581
- let lastAppended = "";
14582
- let structuredChunksSupported = true;
14583
- const renderer = new StreamingMarkdownRenderer();
14584
- const flushMarkdownDelta = async (delta) => {
14585
- if (delta.length === 0) {
14586
- return;
14587
- }
14588
- if (first) {
14589
- await streamer.append({ markdown_text: delta, token, chunks: [] });
14590
- first = false;
14591
- return;
14592
- }
14593
- await streamer.append({ markdown_text: delta });
14594
- };
14595
- const flushText = async () => {
14596
- const { delta, nextAppended } = getNextRenderableDelta(
14597
- renderer,
14598
- lastAppended
14599
- );
14600
- await flushMarkdownDelta(delta);
14601
- lastAppended = nextAppended;
14602
- };
14603
- const sendStructuredChunk = async (chunk) => {
14604
- if (!structuredChunksSupported) {
14605
- return;
14606
- }
14607
- await flushText();
14608
- try {
14609
- if (first) {
14610
- await streamer.append({ chunks: [chunk], token });
14611
- first = false;
14612
- return;
14613
- }
14614
- await streamer.append({ chunks: [chunk] });
14615
- } catch (error) {
14616
- structuredChunksSupported = false;
14617
- internals.logger.warn(
14618
- "Structured streaming chunk failed, falling back to text-only streaming. Ensure your Slack app manifest includes assistant_view, assistant:write scope, and @slack/web-api >= 7.14.0",
14619
- { chunkType: chunk.type, error }
14620
- );
14621
- }
14622
- };
14623
- const pushTextAndFlush = async (text) => {
14624
- renderer.push(text);
14625
- await flushText();
14626
- };
14627
- for await (const chunk of textStream) {
14628
- if (typeof chunk === "string") {
14629
- await pushTextAndFlush(chunk);
14630
- } else if (chunk.type === "markdown_text") {
14631
- await pushTextAndFlush(chunk.text);
14632
- } else {
14633
- await sendStructuredChunk(chunk);
14634
- }
14635
- }
14636
- renderer.finish();
14637
- await flushText();
14638
- const result = await streamer.stop(
14639
- options?.stopBlocks ? { blocks: options.stopBlocks } : void 0
14640
- );
14641
- const messageTs = result.message?.ts ?? result.ts;
14642
- internals.logger.debug("Slack: stream complete", { messageId: messageTs });
14643
- return {
14644
- id: messageTs,
14645
- threadId,
14646
- raw: result
14647
- };
14648
- };
14649
- internals[ADAPTER_STREAM_PATCHED] = true;
14650
- }
14651
14800
  function createJuniorSlackAdapter(config) {
14652
- const adapter = createSlackAdapter(config);
14653
- const internals = adapter;
14654
- assertSlackAdapterInternals(internals);
14655
- patchSlackClientStream(adapter);
14656
- patchSlackAdapterStream(adapter);
14657
- return adapter;
14801
+ return createSlackAdapter(config);
14658
14802
  }
14659
14803
 
14660
14804
  // src/chat/queue/thread-message-dispatcher.ts