@sentry/junior 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  loadSkillsByName,
7
7
  logCapabilityCatalogLoadedOnce,
8
8
  parseSkillInvocation
9
- } from "./chunk-JWBWBJYJ.js";
9
+ } from "./chunk-DGN3WLA4.js";
10
10
  import {
11
11
  SANDBOX_DATA_ROOT,
12
12
  SANDBOX_SKILLS_ROOT,
@@ -27,7 +27,7 @@ import {
27
27
  sandboxSkillDir,
28
28
  sandboxSkillFile,
29
29
  toOptionalTrimmed
30
- } from "./chunk-THPM7NSG.js";
30
+ } from "./chunk-B5O2EJUV.js";
31
31
  import {
32
32
  CredentialUnavailableError,
33
33
  buildOAuthTokenRequest,
@@ -58,7 +58,7 @@ import {
58
58
  toOptionalString,
59
59
  withContext,
60
60
  withSpan
61
- } from "./chunk-MCJJKEB3.js";
61
+ } from "./chunk-J7JEFMVD.js";
62
62
  import "./chunk-Z3YD6NHK.js";
63
63
  import {
64
64
  discoverInstalledPluginPackageContent,
@@ -333,9 +333,17 @@ function coerceAuthor(value) {
333
333
  function coerceMessageMeta(value) {
334
334
  if (!isRecord(value)) return void 0;
335
335
  const meta = {};
336
+ const attachmentCount = toOptionalNumber(value.attachmentCount);
337
+ if (typeof attachmentCount === "number" && attachmentCount > 0) {
338
+ meta.attachmentCount = attachmentCount;
339
+ }
336
340
  if (typeof value.explicitMention === "boolean") {
337
341
  meta.explicitMention = value.explicitMention;
338
342
  }
343
+ const imageAttachmentCount = toOptionalNumber(value.imageAttachmentCount);
344
+ if (typeof imageAttachmentCount === "number" && imageAttachmentCount > 0) {
345
+ meta.imageAttachmentCount = imageAttachmentCount;
346
+ }
339
347
  if (typeof value.replied === "boolean") {
340
348
  meta.replied = value.replied;
341
349
  }
@@ -356,7 +364,7 @@ function coerceMessageMeta(value) {
356
364
  if (typeof value.imagesHydrated === "boolean") {
357
365
  meta.imagesHydrated = value.imagesHydrated;
358
366
  }
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) {
367
+ if (meta.attachmentCount === void 0 && meta.explicitMention === void 0 && meta.imageAttachmentCount === void 0 && meta.replied === void 0 && meta.skippedReason === void 0 && meta.slackTs === void 0 && meta.imageFileIds === void 0 && meta.imagesHydrated === void 0) {
360
368
  return void 0;
361
369
  }
362
370
  return meta;
@@ -868,13 +876,19 @@ function mapSlackError(error) {
868
876
  if (apiError === "not_in_channel") {
869
877
  return new SlackActionError(message, "not_in_channel", baseOptions);
870
878
  }
879
+ if (apiError === "already_reacted") {
880
+ return new SlackActionError(message, "already_reacted", baseOptions);
881
+ }
882
+ if (apiError === "no_reaction") {
883
+ return new SlackActionError(message, "no_reaction", baseOptions);
884
+ }
871
885
  if (apiError === "invalid_arguments") {
872
886
  return new SlackActionError(message, "invalid_arguments", baseOptions);
873
887
  }
874
888
  if (apiError === "invalid_name") {
875
889
  return new SlackActionError(message, "invalid_arguments", baseOptions);
876
890
  }
877
- if (apiError === "not_found") {
891
+ if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
878
892
  return new SlackActionError(message, "not_found", baseOptions);
879
893
  }
880
894
  if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
@@ -974,19 +988,6 @@ async function getFilePermalink(fileId) {
974
988
  );
975
989
  return response.file?.permalink;
976
990
  }
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
991
  async function downloadPrivateSlackFile(url) {
991
992
  const token = getSlackBotToken();
992
993
  if (!token) {
@@ -1006,6 +1007,196 @@ async function downloadPrivateSlackFile(url) {
1006
1007
  return Buffer.from(await response.arrayBuffer());
1007
1008
  }
1008
1009
 
1010
+ // src/chat/slack/emoji.ts
1011
+ var SLACK_EMOJI_NAME_RE = /^(?:[a-z0-9_+-]+)(?:::(?:skin-tone-[2-6]))?$/;
1012
+ function normalizeSlackEmojiName(value) {
1013
+ const trimmed = value.trim().toLowerCase();
1014
+ if (!trimmed) {
1015
+ return null;
1016
+ }
1017
+ const normalized = trimmed.startsWith(":") && trimmed.endsWith(":") ? trimmed.slice(1, -1) : trimmed;
1018
+ return SLACK_EMOJI_NAME_RE.test(normalized) ? normalized : null;
1019
+ }
1020
+
1021
+ // src/chat/slack/outbound.ts
1022
+ var MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
1023
+ function requireSlackConversationId(channelId, action) {
1024
+ const normalized = normalizeSlackConversationId(channelId);
1025
+ if (!normalized) {
1026
+ throw new Error(`${action} requires a valid channel ID`);
1027
+ }
1028
+ return normalized;
1029
+ }
1030
+ function requireSlackThreadTimestamp(threadTs, action) {
1031
+ const normalized = threadTs.trim();
1032
+ if (!normalized) {
1033
+ throw new Error(`${action} requires a thread timestamp`);
1034
+ }
1035
+ return normalized;
1036
+ }
1037
+ function requireSlackMessageTimestamp(timestamp, action) {
1038
+ const normalized = timestamp.trim();
1039
+ if (!normalized) {
1040
+ throw new Error(`${action} requires a target message timestamp`);
1041
+ }
1042
+ return normalized;
1043
+ }
1044
+ function requireSlackMessageText(text, action) {
1045
+ if (text.trim().length === 0) {
1046
+ throw new Error(`${action} requires non-empty text`);
1047
+ }
1048
+ if (text.length > MAX_SLACK_MESSAGE_TEXT_CHARS) {
1049
+ throw new Error(
1050
+ `${action} text exceeds Slack's 40000 character truncation limit`
1051
+ );
1052
+ }
1053
+ return text;
1054
+ }
1055
+ async function getPermalinkBestEffort(args) {
1056
+ try {
1057
+ const response = await withSlackRetries(
1058
+ () => getSlackClient().chat.getPermalink({
1059
+ channel: args.channelId,
1060
+ message_ts: args.messageTs
1061
+ }),
1062
+ 3,
1063
+ { action: "chat.getPermalink" }
1064
+ );
1065
+ return response.permalink;
1066
+ } catch {
1067
+ return void 0;
1068
+ }
1069
+ }
1070
+ async function postSlackMessage(input) {
1071
+ const channelId = requireSlackConversationId(
1072
+ input.channelId,
1073
+ "Slack message posting"
1074
+ );
1075
+ const text = requireSlackMessageText(input.text, "Slack message posting");
1076
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(
1077
+ input.threadTs,
1078
+ "Slack thread message posting"
1079
+ ) : void 0;
1080
+ const response = await withSlackRetries(
1081
+ () => getSlackClient().chat.postMessage({
1082
+ channel: channelId,
1083
+ text,
1084
+ mrkdwn: true,
1085
+ ...threadTs ? { thread_ts: threadTs } : {}
1086
+ }),
1087
+ 3,
1088
+ { action: "chat.postMessage" }
1089
+ );
1090
+ if (!response.ts) {
1091
+ throw new Error("Slack message posted without ts");
1092
+ }
1093
+ return {
1094
+ ts: response.ts,
1095
+ ...input.includePermalink ? {
1096
+ permalink: await getPermalinkBestEffort({
1097
+ channelId,
1098
+ messageTs: response.ts
1099
+ })
1100
+ } : {}
1101
+ };
1102
+ }
1103
+ async function postSlackEphemeralMessage(input) {
1104
+ const channelId = requireSlackConversationId(
1105
+ input.channelId,
1106
+ "Slack ephemeral message posting"
1107
+ );
1108
+ const userId = input.userId.trim();
1109
+ if (!userId) {
1110
+ throw new Error("Slack ephemeral message posting requires a user ID");
1111
+ }
1112
+ const text = requireSlackMessageText(
1113
+ input.text,
1114
+ "Slack ephemeral message posting"
1115
+ );
1116
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(
1117
+ input.threadTs,
1118
+ "Slack ephemeral thread message posting"
1119
+ ) : void 0;
1120
+ const response = await withSlackRetries(
1121
+ () => getSlackClient().chat.postEphemeral({
1122
+ channel: channelId,
1123
+ user: userId,
1124
+ text,
1125
+ ...threadTs ? { thread_ts: threadTs } : {}
1126
+ }),
1127
+ 3,
1128
+ { action: "chat.postEphemeral" }
1129
+ );
1130
+ return {
1131
+ messageTs: response.message_ts
1132
+ };
1133
+ }
1134
+ async function uploadFilesToThread(input) {
1135
+ const channelId = requireSlackConversationId(
1136
+ input.channelId,
1137
+ "Slack file upload"
1138
+ );
1139
+ const threadTs = requireSlackThreadTimestamp(
1140
+ input.threadTs,
1141
+ "Slack file upload"
1142
+ );
1143
+ if (input.files.length === 0) {
1144
+ throw new Error("Slack file upload requires at least one file");
1145
+ }
1146
+ const fileUploads = input.files.map((file) => {
1147
+ const filename = file.filename.trim();
1148
+ if (!filename) {
1149
+ throw new Error(
1150
+ "Slack file upload requires every file to have a filename"
1151
+ );
1152
+ }
1153
+ return {
1154
+ file: file.data,
1155
+ filename
1156
+ };
1157
+ });
1158
+ await withSlackRetries(
1159
+ () => getSlackClient().filesUploadV2({
1160
+ channel_id: channelId,
1161
+ thread_ts: threadTs,
1162
+ file_uploads: fileUploads
1163
+ }),
1164
+ 3,
1165
+ { action: "filesUploadV2" }
1166
+ );
1167
+ }
1168
+ async function addReactionToMessage(input) {
1169
+ const channelId = requireSlackConversationId(
1170
+ input.channelId,
1171
+ "Slack reaction"
1172
+ );
1173
+ const timestamp = requireSlackMessageTimestamp(
1174
+ input.timestamp,
1175
+ "Slack reaction"
1176
+ );
1177
+ const emoji = normalizeSlackEmojiName(input.emoji);
1178
+ if (!emoji) {
1179
+ throw new Error("Slack reaction requires a valid emoji alias name");
1180
+ }
1181
+ try {
1182
+ await withSlackRetries(
1183
+ () => getSlackClient().reactions.add({
1184
+ channel: channelId,
1185
+ timestamp,
1186
+ name: emoji
1187
+ }),
1188
+ 3,
1189
+ { action: "reactions.add" }
1190
+ );
1191
+ } catch (error) {
1192
+ if (error instanceof SlackActionError && error.code === "already_reacted") {
1193
+ return { ok: true };
1194
+ }
1195
+ throw error;
1196
+ }
1197
+ return { ok: true };
1198
+ }
1199
+
1009
1200
  // src/chat/respond-helpers.ts
1010
1201
  var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
1011
1202
  function getSessionIdentifiers(context) {
@@ -1182,16 +1373,23 @@ function extractAssistantText(message) {
1182
1373
  (part) => part.type === "text" && typeof part.text === "string"
1183
1374
  ).map((part) => part.text).join("\n");
1184
1375
  }
1185
- function hasCompletedAssistantTurn(messages) {
1376
+ function getTerminalAssistantMessages(messages) {
1377
+ let lastToolResultIndex = -1;
1186
1378
  for (let index = messages.length - 1; index >= 0; index -= 1) {
1187
- const message = messages[index];
1188
- if (!isAssistantMessage(message)) {
1189
- continue;
1379
+ if (isToolResultMessage(messages[index])) {
1380
+ lastToolResultIndex = index;
1381
+ break;
1190
1382
  }
1191
- const stopReason = message.stopReason;
1192
- return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
1193
1383
  }
1194
- return false;
1384
+ return messages.slice(lastToolResultIndex + 1).filter(isAssistantMessage);
1385
+ }
1386
+ function hasCompletedAssistantTurn(messages) {
1387
+ const message = getTerminalAssistantMessages(messages).at(-1);
1388
+ if (!message) {
1389
+ return false;
1390
+ }
1391
+ const stopReason = message.stopReason;
1392
+ return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
1195
1393
  }
1196
1394
  function upsertActiveSkill(activeSkills, next) {
1197
1395
  const existing = activeSkills.find((skill) => skill.name === next.name);
@@ -1257,17 +1455,17 @@ async function deliverPrivateMessage(input) {
1257
1455
  if (input.channelId) {
1258
1456
  try {
1259
1457
  if (isDmChannel(input.channelId)) {
1260
- await client2.chat.postMessage({
1261
- channel: input.channelId,
1458
+ await postSlackMessage({
1459
+ channelId: input.channelId,
1262
1460
  text: input.text,
1263
- ...input.threadTs ? { thread_ts: input.threadTs } : {}
1461
+ threadTs: input.threadTs
1264
1462
  });
1265
1463
  } else {
1266
- await client2.chat.postEphemeral({
1267
- channel: input.channelId,
1268
- user: input.userId,
1464
+ await postSlackEphemeralMessage({
1465
+ channelId: input.channelId,
1466
+ userId: input.userId,
1269
1467
  text: input.text,
1270
- ...input.threadTs ? { thread_ts: input.threadTs } : {}
1468
+ threadTs: input.threadTs
1271
1469
  });
1272
1470
  }
1273
1471
  return "in_context";
@@ -1294,7 +1492,7 @@ async function deliverPrivateMessage(input) {
1294
1492
  );
1295
1493
  return false;
1296
1494
  }
1297
- await client2.chat.postMessage({ channel: dmChannelId, text: input.text });
1495
+ await postSlackMessage({ channelId: dmChannelId, text: input.text });
1298
1496
  return "fallback_dm";
1299
1497
  } catch (error) {
1300
1498
  logWarn(
@@ -1914,6 +2112,7 @@ function coerceThreadArtifactsState(value) {
1914
2112
  }
1915
2113
  return {
1916
2114
  assistantContextChannelId: typeof artifacts.assistantContextChannelId === "string" ? artifacts.assistantContextChannelId : void 0,
2115
+ assistantTitleSourceMessageId: typeof artifacts.assistantTitleSourceMessageId === "string" ? artifacts.assistantTitleSourceMessageId : void 0,
1917
2116
  lastCanvasId: typeof artifacts.lastCanvasId === "string" ? artifacts.lastCanvasId : void 0,
1918
2117
  lastCanvasUrl: typeof artifacts.lastCanvasUrl === "string" ? artifacts.lastCanvasUrl : void 0,
1919
2118
  recentCanvases,
@@ -2115,6 +2314,15 @@ function getTurnUserMessage(conversation, sessionId) {
2115
2314
  function getTurnUserMessageId(conversation, sessionId) {
2116
2315
  return getTurnUserMessage(conversation, sessionId)?.id;
2117
2316
  }
2317
+ function getTurnUserReplyAttachmentContext(message) {
2318
+ const inboundAttachmentCount = message?.meta?.attachmentCount ?? 0;
2319
+ const imageAttachmentCount = message?.meta?.imageAttachmentCount ?? 0;
2320
+ const imagesHydrated = message?.meta?.imagesHydrated === true;
2321
+ return {
2322
+ ...inboundAttachmentCount > 0 ? { inboundAttachmentCount } : {},
2323
+ ...!imagesHydrated && imageAttachmentCount > 0 ? { omittedImageAttachmentCount: imageAttachmentCount } : {}
2324
+ };
2325
+ }
2118
2326
 
2119
2327
  // src/chat/pi/client.ts
2120
2328
  import {
@@ -2535,7 +2743,7 @@ async function summarizeConversationChunk(messages, conversation, context, deps)
2535
2743
  }
2536
2744
  return transcript.slice(0, 2800);
2537
2745
  }
2538
- async function generateThreadTitleWithDeps(userText, assistantText, deps) {
2746
+ async function generateThreadTitleWithDeps(sourceText, deps) {
2539
2747
  const result = await deps.completeText({
2540
2748
  modelId: botConfig.fastModelId,
2541
2749
  temperature: 0,
@@ -2543,17 +2751,41 @@ async function generateThreadTitleWithDeps(userText, assistantText, deps) {
2543
2751
  {
2544
2752
  role: "user",
2545
2753
  content: [
2546
- "Generate a concise 5-8 word title for this conversation. Reply with ONLY the title, no quotes or punctuation.",
2754
+ "Generate a concise 5-8 word Slack conversation title from the first user message below.",
2755
+ "Capture the user's main request.",
2756
+ "Reply with ONLY the title, with no quotes or trailing punctuation.",
2547
2757
  "",
2548
- `User: ${userText.slice(0, 500)}`,
2549
- `Assistant: ${assistantText.slice(0, 500)}`
2758
+ `First user message: ${sourceText.slice(0, 500)}`
2550
2759
  ].join("\n"),
2551
2760
  timestamp: Date.now()
2552
2761
  }
2553
- ]
2762
+ ],
2763
+ metadata: {
2764
+ modelId: botConfig.fastModelId
2765
+ }
2554
2766
  });
2555
2767
  return result.text.trim().slice(0, 60);
2556
2768
  }
2769
+ function getThreadTitleSourceMessage(conversation) {
2770
+ let firstMessage;
2771
+ for (const message of conversation.messages) {
2772
+ if (!isHumanConversationMessage(message)) {
2773
+ continue;
2774
+ }
2775
+ if (!firstMessage) {
2776
+ firstMessage = message;
2777
+ continue;
2778
+ }
2779
+ if (message.createdAtMs < firstMessage.createdAtMs) {
2780
+ firstMessage = message;
2781
+ continue;
2782
+ }
2783
+ if (message.createdAtMs === firstMessage.createdAtMs && message.id < firstMessage.id) {
2784
+ firstMessage = message;
2785
+ }
2786
+ }
2787
+ return firstMessage;
2788
+ }
2557
2789
  async function compactConversationIfNeededWithDeps(conversation, context, deps) {
2558
2790
  updateConversationStats(conversation);
2559
2791
  let estimatedTokens = conversation.stats.estimatedContextTokens;
@@ -2598,7 +2830,7 @@ async function compactConversationIfNeededWithDeps(conversation, context, deps)
2598
2830
  function createConversationMemoryService(deps) {
2599
2831
  return {
2600
2832
  compactConversationIfNeeded: async (conversation, context) => await compactConversationIfNeededWithDeps(conversation, context, deps),
2601
- generateThreadTitle: async (userText, assistantText) => await generateThreadTitleWithDeps(userText, assistantText, deps)
2833
+ generateThreadTitle: async (sourceText) => await generateThreadTitleWithDeps(sourceText, deps)
2602
2834
  };
2603
2835
  }
2604
2836
  var defaultConversationMemoryService = createConversationMemoryService({
@@ -2704,114 +2936,267 @@ import { Agent } from "@mariozechner/pi-agent-core";
2704
2936
  import fs from "fs";
2705
2937
  import path2 from "path";
2706
2938
 
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);
2939
+ // src/chat/runtime/status-format.ts
2940
+ var SLACK_STATUS_MAX_LENGTH = 50;
2941
+ function truncateWithEllipsis(text, maxLength) {
2942
+ if (text.length <= maxLength) {
2943
+ return text;
2742
2944
  }
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();
2945
+ return `${text.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
2749
2946
  }
2750
- function countSlackLines(text) {
2751
- if (!text) {
2752
- return 0;
2947
+ function truncateStatusText(text) {
2948
+ const trimmed = text.trim();
2949
+ if (!trimmed) {
2950
+ return "";
2753
2951
  }
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;
2952
+ return truncateWithEllipsis(trimmed, SLACK_STATUS_MAX_LENGTH);
2758
2953
  }
2759
- function findSplitIndex(text, maxChars) {
2760
- if (text.length <= maxChars) {
2761
- return text.length;
2954
+ function compactStatusPath(value) {
2955
+ if (typeof value !== "string") {
2956
+ return void 0;
2762
2957
  }
2763
- const bounded = text.slice(0, maxChars);
2764
- const newlineIndex = bounded.lastIndexOf("\n");
2765
- if (newlineIndex > 0) {
2766
- return newlineIndex;
2958
+ const trimmed = value.trim();
2959
+ if (!trimmed) {
2960
+ return void 0;
2767
2961
  }
2768
- const spaceIndex = bounded.lastIndexOf(" ");
2769
- if (spaceIndex > 0) {
2770
- return spaceIndex;
2962
+ if (trimmed.length <= 80) {
2963
+ return trimmed;
2771
2964
  }
2772
- return maxChars;
2965
+ return `...${trimmed.slice(-77)}`;
2773
2966
  }
2774
- function splitByLineBudget(text, maxLines) {
2775
- if (maxLines <= 0) {
2776
- return "";
2967
+ function compactStatusText(value, maxLength = 80) {
2968
+ if (typeof value !== "string") {
2969
+ return void 0;
2777
2970
  }
2778
- const lines = text.split("\n");
2779
- if (lines.length <= maxLines) {
2780
- return text;
2971
+ const trimmed = value.trim();
2972
+ if (!trimmed) {
2973
+ return void 0;
2781
2974
  }
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
- };
2975
+ return truncateWithEllipsis(trimmed, maxLength);
2796
2976
  }
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
- }
2805
- if (!open) {
2806
- open = {
2807
- fence: openerMatch[1],
2808
- openerLine: trimmed
2809
- };
2810
- continue;
2811
- }
2812
- if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
2813
- open = void 0;
2814
- }
2977
+ function readShellToken(command, startIndex) {
2978
+ let index = startIndex;
2979
+ while (index < command.length && /\s/.test(command[index] ?? "")) {
2980
+ index += 1;
2981
+ }
2982
+ if (index >= command.length) {
2983
+ return void 0;
2984
+ }
2985
+ let token = "";
2986
+ let quote;
2987
+ while (index < command.length) {
2988
+ const char = command[index];
2989
+ if (!char) {
2990
+ break;
2991
+ }
2992
+ if (quote) {
2993
+ if (char === quote) {
2994
+ quote = void 0;
2995
+ index += 1;
2996
+ continue;
2997
+ }
2998
+ if (char === "\\" && quote === '"' && index + 1 < command.length) {
2999
+ token += command[index + 1];
3000
+ index += 2;
3001
+ continue;
3002
+ }
3003
+ token += char;
3004
+ index += 1;
3005
+ continue;
3006
+ }
3007
+ if (/\s/.test(char)) {
3008
+ break;
3009
+ }
3010
+ if (char === '"' || char === "'") {
3011
+ quote = char;
3012
+ index += 1;
3013
+ continue;
3014
+ }
3015
+ if (char === "\\" && index + 1 < command.length) {
3016
+ token += command[index + 1];
3017
+ index += 2;
3018
+ continue;
3019
+ }
3020
+ token += char;
3021
+ index += 1;
3022
+ }
3023
+ return { token, nextIndex: index };
3024
+ }
3025
+ function compactStatusCommand(value) {
3026
+ if (typeof value !== "string") {
3027
+ return void 0;
3028
+ }
3029
+ const trimmed = value.trim();
3030
+ if (!trimmed) {
3031
+ return void 0;
3032
+ }
3033
+ let index = 0;
3034
+ while (index < trimmed.length) {
3035
+ const parsed = readShellToken(trimmed, index);
3036
+ if (!parsed) {
3037
+ return void 0;
3038
+ }
3039
+ index = parsed.nextIndex;
3040
+ if (!parsed.token) {
3041
+ continue;
3042
+ }
3043
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(parsed.token)) {
3044
+ continue;
3045
+ }
3046
+ const normalized = parsed.token.replace(/[\\/]+$/g, "");
3047
+ if (!normalized) {
3048
+ return void 0;
3049
+ }
3050
+ const parts = normalized.split(/[\\/]/).filter((part) => part.length > 0);
3051
+ const command = parts.length > 0 ? parts[parts.length - 1] : normalized;
3052
+ return compactStatusText(command, 40);
3053
+ }
3054
+ return void 0;
3055
+ }
3056
+ function compactStatusFilename(value) {
3057
+ if (typeof value !== "string") {
3058
+ return void 0;
3059
+ }
3060
+ const trimmed = value.trim().replace(/[\\/]+$/g, "");
3061
+ if (!trimmed) {
3062
+ return void 0;
3063
+ }
3064
+ const parts = trimmed.split(/[\\/]/).filter((part) => part.length > 0);
3065
+ const filename = parts.length > 0 ? parts[parts.length - 1] : trimmed;
3066
+ return compactStatusText(filename, 80);
3067
+ }
3068
+ function extractStatusUrlDomain(value) {
3069
+ if (typeof value !== "string") {
3070
+ return void 0;
3071
+ }
3072
+ const trimmed = value.trim();
3073
+ if (!trimmed) {
3074
+ return void 0;
3075
+ }
3076
+ try {
3077
+ const parsed = new URL(trimmed);
3078
+ return parsed.hostname || void 0;
3079
+ } catch {
3080
+ return void 0;
3081
+ }
3082
+ }
3083
+
3084
+ // src/chat/slack/mrkdwn.ts
3085
+ function ensureBlockSpacing(text) {
3086
+ const codeBlockPattern = /^```/;
3087
+ const listItemPattern = /^[-*•]\s|^\d+\.\s/;
3088
+ const lines = text.split("\n");
3089
+ const result = [];
3090
+ let inCodeBlock = false;
3091
+ for (let i = 0; i < lines.length; i++) {
3092
+ const line = lines[i];
3093
+ const isCodeFence = codeBlockPattern.test(line.trimStart());
3094
+ if (isCodeFence) {
3095
+ if (!inCodeBlock) {
3096
+ const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
3097
+ if (prev2 !== void 0 && prev2.trim() !== "") {
3098
+ result.push("");
3099
+ }
3100
+ }
3101
+ inCodeBlock = !inCodeBlock;
3102
+ result.push(line);
3103
+ continue;
3104
+ }
3105
+ if (inCodeBlock) {
3106
+ result.push(line);
3107
+ continue;
3108
+ }
3109
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
3110
+ if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
3111
+ result.push("");
3112
+ }
3113
+ result.push(line);
3114
+ }
3115
+ return result.join("\n");
3116
+ }
3117
+ function renderSlackMrkdwn(text) {
3118
+ let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
3119
+ normalized = ensureBlockSpacing(normalized);
3120
+ return normalized.replace(/\n{3,}/g, "\n\n").trim();
3121
+ }
3122
+ function normalizeSlackStatusText(text) {
3123
+ const trimmed = text.trim();
3124
+ if (!trimmed) {
3125
+ return "";
3126
+ }
3127
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
3128
+ }
3129
+
3130
+ // src/chat/slack/output.ts
3131
+ var MAX_INLINE_CHARS = 2200;
3132
+ var MAX_INLINE_LINES = 45;
3133
+ var CONTINUED_MARKER = "\n\n[Continued below]";
3134
+ var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
3135
+ function countSlackLines(text) {
3136
+ if (!text) {
3137
+ return 0;
3138
+ }
3139
+ return text.split("\n").length;
3140
+ }
3141
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
3142
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
3143
+ }
3144
+ function findSplitIndex(text, maxChars) {
3145
+ if (text.length <= maxChars) {
3146
+ return text.length;
3147
+ }
3148
+ const bounded = text.slice(0, maxChars);
3149
+ const newlineIndex = bounded.lastIndexOf("\n");
3150
+ if (newlineIndex > 0) {
3151
+ return newlineIndex;
3152
+ }
3153
+ const spaceIndex = bounded.lastIndexOf(" ");
3154
+ if (spaceIndex > 0) {
3155
+ return spaceIndex;
3156
+ }
3157
+ return maxChars;
3158
+ }
3159
+ function splitByLineBudget(text, maxLines) {
3160
+ if (maxLines <= 0) {
3161
+ return "";
3162
+ }
3163
+ const lines = text.split("\n");
3164
+ if (lines.length <= maxLines) {
3165
+ return text;
3166
+ }
3167
+ return lines.slice(0, maxLines).join("\n");
3168
+ }
3169
+ function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
3170
+ return {
3171
+ maxChars: Math.max(1, maxChars - suffix.length),
3172
+ maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
3173
+ };
3174
+ }
3175
+ function forceSplitBudget(text, budget) {
3176
+ const lineCount = countSlackLines(text);
3177
+ return {
3178
+ maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
3179
+ maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
3180
+ };
3181
+ }
3182
+ function getFenceContinuation(text) {
3183
+ let open;
3184
+ for (const line of text.split("\n")) {
3185
+ const trimmed = line.trimStart();
3186
+ const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
3187
+ if (!openerMatch) {
3188
+ continue;
3189
+ }
3190
+ if (!open) {
3191
+ open = {
3192
+ fence: openerMatch[1],
3193
+ openerLine: trimmed
3194
+ };
3195
+ continue;
3196
+ }
3197
+ if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
3198
+ open = void 0;
3199
+ }
2815
3200
  }
2816
3201
  if (!open) {
2817
3202
  return null;
@@ -2902,7 +3287,7 @@ function takeSlackInlinePrefix(text, options) {
2902
3287
  };
2903
3288
  }
2904
3289
  function splitSlackReplyText(text, options) {
2905
- const normalized = normalizeForSlack(text);
3290
+ const normalized = renderSlackMrkdwn(text);
2906
3291
  if (!normalized) {
2907
3292
  return [];
2908
3293
  }
@@ -2926,19 +3311,11 @@ function splitSlackReplyText(text, options) {
2926
3311
  }
2927
3312
  return chunks;
2928
3313
  }
2929
- function getSlackInterruptionMarker() {
2930
- return INTERRUPTED_MARKER;
2931
- }
2932
3314
  function getSlackContinuationBudget() {
2933
3315
  return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
2934
3316
  }
2935
- function getSlackStreamingContinuationBudget() {
2936
- return reserveInlineBudgetForSuffix(
2937
- `${STREAMING_FENCE_CLOSE_GUARD}${CONTINUED_MARKER}`
2938
- );
2939
- }
2940
3317
  function buildSlackOutputMessage(text, files) {
2941
- const normalized = normalizeForSlack(text);
3318
+ const normalized = renderSlackMrkdwn(text);
2942
3319
  const fileCount = files?.length ?? 0;
2943
3320
  if (!normalized) {
2944
3321
  if (fileCount > 0) {
@@ -3401,12 +3778,12 @@ function buildSystemPrompt(params) {
3401
3778
  [
3402
3779
  "Always produce output that follows this contract:",
3403
3780
  `<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).",
3781
+ "- Use Slack-friendly markdown, not full CommonMark. Prefer bold section labels over markdown headings, and use bullets and short code blocks when helpful.",
3405
3782
  "- Keep normal responses brief and scannable.",
3406
3783
  "- If depth is needed, start with a concise summary and then provide fuller detail.",
3407
3784
  "- For tool-heavy research, discovery, or source-checking requests, do not send an initial acknowledgment. Start the visible reply only once you can present the actual answer.",
3408
3785
  "- Do not narrate tool execution or repeated status updates in the visible reply.",
3409
- "- Avoid tables unless explicitly requested.",
3786
+ "- Avoid tables and markdown links like `[label](url)` unless explicitly requested. Prefer plain URLs or Slack-native entities when exact rendering matters.",
3410
3787
  "- End every turn with a final user-facing markdown response.",
3411
3788
  "</output>"
3412
3789
  ].join("\n")
@@ -5557,86 +5934,12 @@ function createSearchToolsTool(mcpToolManager, getActiveSkills) {
5557
5934
  // src/chat/tools/slack/channel-list-messages.ts
5558
5935
  import { Type as Type7 } from "@sinclair/typebox";
5559
5936
 
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
5937
  // src/chat/slack/channel.ts
5572
- async function postMessageToChannel(input) {
5938
+ async function listChannelMessages(input) {
5573
5939
  const client2 = getSlackClient();
5574
5940
  const channelId = normalizeSlackConversationId(input.channelId);
5575
5941
  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");
5591
- }
5592
- let permalink;
5593
- try {
5594
- const permalinkResponse = await withSlackRetries(
5595
- () => client2.chat.getPermalink({
5596
- 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");
5942
+ throw new Error("Slack channel history lookup requires a valid channel ID");
5640
5943
  }
5641
5944
  const targetLimit = Math.max(1, Math.min(input.limit, 1e3));
5642
5945
  const maxPages = Math.max(1, Math.min(input.maxPages ?? 5, 10));
@@ -5841,9 +6144,10 @@ function createSlackChannelPostMessageTool(context, state) {
5841
6144
  deduplicated: true
5842
6145
  };
5843
6146
  }
5844
- const posted = await postMessageToChannel({
6147
+ const posted = await postSlackMessage({
5845
6148
  channelId: targetChannelId,
5846
- text
6149
+ text,
6150
+ includePermalink: true
5847
6151
  });
5848
6152
  const response = {
5849
6153
  ok: true,
@@ -7450,152 +7754,7 @@ function throwSandboxOperationError(action, error, includeMissingPath = false) {
7450
7754
  import { Sandbox } from "@vercel/sandbox";
7451
7755
  import { createBashTool as createBashTool2 } from "bash-tool";
7452
7756
 
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
7757
+ // src/chat/slack/assistant-thread/status-render.ts
7599
7758
  var STATUS_PATTERNS = {
7600
7759
  thinking: {
7601
7760
  defaultContext: "\u2026",
@@ -7649,17 +7808,10 @@ var STATUS_PATTERNS = {
7649
7808
  function makeAssistantStatus(kind, context) {
7650
7809
  return { kind, ...context ? { context } : {} };
7651
7810
  }
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) {
7811
+ function renderAssistantStatus(args) {
7660
7812
  const random = args.random ?? Math.random;
7661
7813
  const pattern = STATUS_PATTERNS[args.status.kind];
7662
- const context = normalizeAssistantStatusText(args.status.context ?? "") || pattern.defaultContext;
7814
+ const context = normalizeSlackStatusText(args.status.context ?? "") || pattern.defaultContext;
7663
7815
  const index = Math.floor(random() * pattern.variants.length);
7664
7816
  const verb = pattern.variants[index] ?? pattern.variants[0];
7665
7817
  const visible = truncateStatusText(`${verb} ${context}`);
@@ -7671,46 +7823,274 @@ function buildAssistantStatusPresentation(args) {
7671
7823
  suggestions: Array.from(/* @__PURE__ */ new Set([visible, hint]))
7672
7824
  };
7673
7825
  }
7674
- function createSlackAdapterAssistantStatusTransport(args) {
7675
- 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);
7681
- }
7826
+
7827
+ // src/chat/slack/assistant-thread/status-scheduler.ts
7828
+ var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
7829
+ var STATUS_MIN_VISIBLE_MS = 1200;
7830
+ var STATUS_ROTATION_INTERVAL_MS = 3e4;
7831
+ function createAssistantStatusScheduler(args) {
7832
+ const now = args.now ?? (() => Date.now());
7833
+ const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
7834
+ const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
7835
+ const random = args.random ?? Math.random;
7836
+ let active = false;
7837
+ let currentKey = "";
7838
+ let currentStatus = makeAssistantStatus("thinking");
7839
+ let currentVisibleStatus = "";
7840
+ let lastStatusAt = 0;
7841
+ let pendingStatus = null;
7842
+ let pendingKey = "";
7843
+ let pendingTimer = null;
7844
+ let rotationTimer = null;
7845
+ let inflightStatusUpdate = Promise.resolve();
7846
+ const enqueueStatusUpdate = (task) => {
7847
+ const request = inflightStatusUpdate.catch(() => void 0).then(async () => {
7848
+ await task();
7849
+ });
7850
+ inflightStatusUpdate = request.catch(() => void 0);
7851
+ return request;
7852
+ };
7853
+ const scheduleRotation = () => {
7854
+ if (rotationTimer) {
7855
+ clearTimer(rotationTimer);
7856
+ rotationTimer = null;
7857
+ }
7858
+ if (!active || !currentVisibleStatus) {
7859
+ return;
7860
+ }
7861
+ rotationTimer = setTimer(() => {
7862
+ rotationTimer = null;
7863
+ if (!active || !currentVisibleStatus) {
7864
+ return;
7865
+ }
7866
+ void postRenderedStatus(currentStatus);
7867
+ }, STATUS_ROTATION_INTERVAL_MS);
7868
+ };
7869
+ const postStatus = async (text, suggestions) => {
7870
+ if (!text && !currentVisibleStatus) {
7871
+ return;
7872
+ }
7873
+ currentVisibleStatus = text;
7874
+ lastStatusAt = now();
7875
+ scheduleRotation();
7876
+ await enqueueStatusUpdate(async () => {
7877
+ await args.sendStatus(text, suggestions);
7878
+ });
7879
+ };
7880
+ const postRenderedStatus = async (status) => {
7881
+ const presentation = renderAssistantStatus({
7882
+ status,
7883
+ random
7884
+ });
7885
+ currentStatus = status;
7886
+ currentKey = presentation.key;
7887
+ await postStatus(presentation.visible, presentation.suggestions);
7888
+ };
7889
+ const clearPending = () => {
7890
+ if (pendingTimer) {
7891
+ clearTimer(pendingTimer);
7892
+ pendingTimer = null;
7893
+ }
7894
+ pendingStatus = null;
7895
+ pendingKey = "";
7896
+ };
7897
+ const flushPending = async () => {
7898
+ if (!active || !pendingStatus) {
7899
+ clearPending();
7900
+ return;
7901
+ }
7902
+ const next = pendingStatus;
7903
+ clearPending();
7904
+ const nextPresentation = renderAssistantStatus({
7905
+ status: next,
7906
+ random
7907
+ });
7908
+ if (nextPresentation.key !== currentKey) {
7909
+ await postRenderedStatus(next);
7910
+ }
7911
+ };
7912
+ return {
7913
+ start() {
7914
+ active = true;
7915
+ clearPending();
7916
+ currentStatus = makeAssistantStatus("thinking");
7917
+ currentKey = "";
7918
+ void postRenderedStatus(currentStatus);
7919
+ },
7920
+ async stop() {
7921
+ active = false;
7922
+ clearPending();
7923
+ if (rotationTimer) {
7924
+ clearTimer(rotationTimer);
7925
+ rotationTimer = null;
7926
+ }
7927
+ currentKey = "";
7928
+ await postStatus("");
7929
+ },
7930
+ update(status) {
7931
+ if (!active) {
7932
+ return;
7933
+ }
7934
+ const presentation = renderAssistantStatus({
7935
+ status,
7936
+ random
7937
+ });
7938
+ if (!presentation.visible) {
7939
+ return;
7940
+ }
7941
+ if (presentation.key === currentKey || presentation.key === pendingKey) {
7942
+ return;
7943
+ }
7944
+ const elapsed = now() - lastStatusAt;
7945
+ const waitMs = Math.max(
7946
+ STATUS_UPDATE_DEBOUNCE_MS - elapsed,
7947
+ STATUS_MIN_VISIBLE_MS - elapsed,
7948
+ 0
7949
+ );
7950
+ if (waitMs <= 0) {
7951
+ clearPending();
7952
+ void postRenderedStatus(status);
7953
+ return;
7954
+ }
7955
+ pendingStatus = status;
7956
+ pendingKey = presentation.key;
7957
+ if (pendingTimer) {
7958
+ return;
7959
+ }
7960
+ pendingTimer = setTimer(
7961
+ () => {
7962
+ pendingTimer = null;
7963
+ void flushPending();
7964
+ },
7965
+ Math.max(1, waitMs)
7966
+ );
7967
+ }
7968
+ };
7969
+ }
7970
+
7971
+ // src/chat/slack/assistant-thread/status-send.ts
7972
+ function createSlackAdapterStatusSender(args) {
7973
+ const adapter = args.getSlackAdapter();
7974
+ const boundToken = getSlackAdapterRequestToken(adapter);
7975
+ return async (text, suggestions) => {
7976
+ const channelId = args.channelId;
7977
+ const threadTs = args.threadTs;
7978
+ if (!channelId || !threadTs) {
7979
+ return;
7980
+ }
7981
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
7982
+ if (!normalizedChannelId) {
7983
+ return;
7984
+ }
7985
+ try {
7986
+ await runWithBoundSlackToken(
7987
+ adapter,
7988
+ boundToken,
7989
+ () => adapter.setAssistantStatus(
7990
+ normalizedChannelId,
7991
+ threadTs,
7992
+ text,
7993
+ suggestions
7994
+ )
7995
+ );
7996
+ } catch (error) {
7997
+ logAssistantStatusFailure({
7998
+ status: text,
7999
+ error,
8000
+ channelId,
8001
+ normalizedChannelId,
8002
+ threadTs
8003
+ });
7682
8004
  }
7683
8005
  };
7684
8006
  }
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
- }
8007
+ function createSlackWebApiStatusSender(args) {
8008
+ const getClient2 = args.getSlackClient ?? getSlackClient;
8009
+ return async (text, suggestions) => {
8010
+ const channelId = args.channelId;
8011
+ const threadTs = args.threadTs;
8012
+ if (!channelId || !threadTs) {
8013
+ return;
8014
+ }
8015
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
8016
+ if (!normalizedChannelId) {
8017
+ return;
8018
+ }
8019
+ try {
8020
+ await getClient2().assistant.threads.setStatus({
8021
+ channel_id: normalizedChannelId,
8022
+ thread_ts: threadTs,
8023
+ status: text,
8024
+ ...suggestions ? { loading_messages: suggestions } : {}
8025
+ });
8026
+ } catch (error) {
8027
+ logAssistantStatusFailure({
8028
+ status: text,
8029
+ error,
8030
+ channelId,
8031
+ normalizedChannelId,
8032
+ threadTs
8033
+ });
7699
8034
  }
7700
8035
  };
7701
8036
  }
7702
- function logAssistantStatusFailure(status, error) {
8037
+ function getSlackAdapterRequestToken(adapter) {
8038
+ const token = adapter.requestContext?.getStore()?.token;
8039
+ if (typeof token !== "string") {
8040
+ return void 0;
8041
+ }
8042
+ const trimmed = token.trim();
8043
+ return trimmed || void 0;
8044
+ }
8045
+ async function runWithBoundSlackToken(adapter, token, task) {
8046
+ if (!token) {
8047
+ return await task();
8048
+ }
8049
+ return await adapter.withBotToken(token, task);
8050
+ }
8051
+ function logAssistantStatusFailure(args) {
7703
8052
  logWarn(
7704
8053
  "assistant_status_update_failed",
7705
8054
  {},
7706
8055
  {
7707
- "app.slack.status_text": status || "(clear)",
7708
- "error.message": error instanceof Error ? error.message : String(error)
8056
+ "app.slack.status_text": args.status || "(clear)",
8057
+ "app.slack.channel_id_raw": args.channelId,
8058
+ "app.slack.channel_id": args.normalizedChannelId,
8059
+ "app.slack.thread_ts": args.threadTs,
8060
+ "error.message": args.error instanceof Error ? args.error.message : String(args.error)
7709
8061
  },
7710
- "Failed to update assistant status"
8062
+ `Failed to update assistant status channel=${args.normalizedChannelId} raw=${args.channelId} thread=${args.threadTs}`
7711
8063
  );
7712
8064
  }
7713
8065
 
8066
+ // src/chat/slack/assistant-thread/status.ts
8067
+ function createSlackAdapterAssistantStatusSession(args) {
8068
+ return createAssistantStatusScheduler({
8069
+ sendStatus: createSlackAdapterStatusSender({
8070
+ channelId: args.channelId,
8071
+ threadTs: args.threadTs,
8072
+ getSlackAdapter: args.getSlackAdapter
8073
+ }),
8074
+ now: args.now,
8075
+ setTimer: args.setTimer,
8076
+ clearTimer: args.clearTimer,
8077
+ random: args.random
8078
+ });
8079
+ }
8080
+ function createSlackWebApiAssistantStatusSession(args) {
8081
+ return createAssistantStatusScheduler({
8082
+ sendStatus: createSlackWebApiStatusSender({
8083
+ channelId: args.channelId,
8084
+ threadTs: args.threadTs,
8085
+ getSlackClient: args.getSlackClient
8086
+ }),
8087
+ now: args.now,
8088
+ setTimer: args.setTimer,
8089
+ clearTimer: args.clearTimer,
8090
+ random: args.random
8091
+ });
8092
+ }
8093
+
7714
8094
  // src/chat/sandbox/skill-sync.ts
7715
8095
  import fs3 from "fs/promises";
7716
8096
  import path5 from "path";
@@ -7832,7 +8212,11 @@ function pickFields(record, csv) {
7832
8212
  }
7833
8213
 
7834
8214
  function outputJson(value) {
7835
- process.stdout.write(JSON.stringify(value, null, 2) + "\\n");
8215
+ fs.writeFileSync(process.stdout.fd, JSON.stringify(value, null, 2) + "\\n");
8216
+ }
8217
+
8218
+ function outputText(value) {
8219
+ fs.writeFileSync(process.stdout.fd, value);
7836
8220
  }
7837
8221
 
7838
8222
  function fallbackToRealGh() {
@@ -7848,12 +8232,12 @@ function fallbackToRealGh() {
7848
8232
  }
7849
8233
 
7850
8234
  if (args.length === 0 || args[0] === "--version" || args[0] === "version") {
7851
- process.stdout.write("gh version 2.0.0 (junior-eval)\\n");
8235
+ outputText("gh version 2.0.0 (junior-eval)\\n");
7852
8236
  process.exit(0);
7853
8237
  }
7854
8238
 
7855
8239
  if (args[0] === "auth" && args[1] === "status") {
7856
- process.stdout.write("github.com\\n \u2713 Logged in to github.com as junior-eval\\n");
8240
+ outputText("github.com\\n \u2713 Logged in to github.com as junior-eval\\n");
7857
8241
  process.exit(0);
7858
8242
  }
7859
8243
 
@@ -7877,7 +8261,7 @@ if (args[0] === "repo" && args[1] === "view") {
7877
8261
  if (jsonFields) {
7878
8262
  outputJson(pickFields(record, jsonFields));
7879
8263
  } else {
7880
- process.stdout.write(record.url + "\\n");
8264
+ outputText(record.url + "\\n");
7881
8265
  }
7882
8266
  process.exit(0);
7883
8267
  }
@@ -7929,7 +8313,7 @@ if (args[0] === "issue") {
7929
8313
  if (jsonFields) {
7930
8314
  outputJson(pickFields(record, jsonFields));
7931
8315
  } else {
7932
- process.stdout.write(record.url + "\\n");
8316
+ outputText(record.url + "\\n");
7933
8317
  }
7934
8318
  process.exit(0);
7935
8319
  }
@@ -7945,7 +8329,7 @@ if (args[0] === "issue") {
7945
8329
  if (jsonFields) {
7946
8330
  outputJson(pickFields(record, jsonFields));
7947
8331
  } else {
7948
- process.stdout.write(record.url + "\\n");
8332
+ outputText(record.url + "\\n");
7949
8333
  }
7950
8334
  process.exit(0);
7951
8335
  }
@@ -7962,7 +8346,7 @@ if (args[0] === "issue") {
7962
8346
  }
7963
8347
 
7964
8348
  if (subcommand === "comment") {
7965
- process.stdout.write(record.url + "#issuecomment-1\\n");
8349
+ outputText(record.url + "#issuecomment-1\\n");
7966
8350
  process.exit(0);
7967
8351
  }
7968
8352
 
@@ -9293,7 +9677,6 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
9293
9677
  // src/chat/services/reply-delivery-plan.ts
9294
9678
  var REACTION_ONLY_ACK_RE = /^(?::[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D]+)$/u;
9295
9679
  var REDUNDANT_REACTION_ACK_TEXT = ["done", "got it", "ok", "okay"];
9296
- var REACTION_ALIAS_PREFIX_RE = /^:[a-z0-9_+-]*$/i;
9297
9680
  function normalizeReactionAckText(text) {
9298
9681
  return text.trim().toLowerCase().replace(/[!.]+$/g, "");
9299
9682
  }
@@ -9310,24 +9693,11 @@ function isRedundantReactionAckText(text) {
9310
9693
  normalized
9311
9694
  );
9312
9695
  }
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
9696
  function buildReplyDeliveryPlan(args) {
9327
9697
  const mode = args.explicitChannelPostIntent && args.channelPostPerformed ? "channel_only" : "thread";
9328
9698
  let attachFiles = "none";
9329
9699
  if (args.hasFiles && mode === "thread") {
9330
- attachFiles = args.streamingThreadReply ? "followup" : "inline";
9700
+ attachFiles = "inline";
9331
9701
  }
9332
9702
  return {
9333
9703
  mode,
@@ -9335,27 +9705,6 @@ function buildReplyDeliveryPlan(args) {
9335
9705
  attachFiles
9336
9706
  };
9337
9707
  }
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
9708
 
9360
9709
  // src/chat/services/channel-intent.ts
9361
9710
  function isExplicitChannelPostIntent(text) {
@@ -9381,6 +9730,10 @@ function sentenceClaimsAttachment(sentence) {
9381
9730
  if (!hasAttachmentNoun) {
9382
9731
  return false;
9383
9732
  }
9733
+ const hasNegativeAttachmentPhrase = /\bno (?:screenshot|image|file|attachment)\b/i.test(sentence) || /\b(?:isn['’]t|is not|wasn['’]t|was not)\s+attached\b/i.test(sentence) || /\bwithout (?:an? )?(?:screenshot|image|file|attachment)\b/i.test(sentence);
9734
+ if (hasNegativeAttachmentPhrase) {
9735
+ return false;
9736
+ }
9384
9737
  const hasPositiveAttachmentVerb = /\b(attached|shared|uploaded|included)\b/i.test(sentence);
9385
9738
  const hasDeicticSharePhrase = /\bhere(?:'s| is)\b/i.test(sentence);
9386
9739
  return hasPositiveAttachmentVerb || hasDeicticSharePhrase;
@@ -9409,7 +9762,6 @@ function buildTurnResult(input) {
9409
9762
  toolCalls,
9410
9763
  sandboxId,
9411
9764
  sandboxDependencyProfileHash,
9412
- hasTextDeltaCallback,
9413
9765
  shouldTrace,
9414
9766
  spanContext,
9415
9767
  correlation,
@@ -9417,7 +9769,8 @@ function buildTurnResult(input) {
9417
9769
  } = input;
9418
9770
  const toolResults = newMessages.filter(isToolResultMessage);
9419
9771
  const assistantMessages = newMessages.filter(isAssistantMessage);
9420
- const primaryText = assistantMessages.map((message) => extractAssistantText(message)).join("\n\n").trim();
9772
+ const terminalAssistantMessages = getTerminalAssistantMessages(newMessages);
9773
+ const primaryText = terminalAssistantMessages.map((message) => extractAssistantText(message)).join("\n\n").trim();
9421
9774
  const oauthStartedMessage = extractOAuthStartedMessageFromToolResults(toolResults);
9422
9775
  const toolErrorCount = toolResults.filter((result) => result.isError).length;
9423
9776
  const explicitChannelPostIntent = isExplicitChannelPostIntent(userInput);
@@ -9430,8 +9783,7 @@ function buildTurnResult(input) {
9430
9783
  const deliveryPlan = buildReplyDeliveryPlan({
9431
9784
  explicitChannelPostIntent,
9432
9785
  channelPostPerformed,
9433
- hasFiles: replyFiles.length > 0,
9434
- streamingThreadReply: hasTextDeltaCallback
9786
+ hasFiles: replyFiles.length > 0
9435
9787
  });
9436
9788
  const deliveryMode = deliveryPlan.mode;
9437
9789
  if (!primaryText && !oauthStartedMessage) {
@@ -9453,7 +9805,7 @@ function buildTurnResult(input) {
9453
9805
  "Model returned empty text response"
9454
9806
  );
9455
9807
  }
9456
- const lastAssistant = assistantMessages.at(-1);
9808
+ const lastAssistant = terminalAssistantMessages.at(-1);
9457
9809
  const stopReason = typeof lastAssistant?.stopReason === "string" ? lastAssistant.stopReason : void 0;
9458
9810
  const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
9459
9811
  const usedPrimaryText = Boolean(primaryText);
@@ -9779,6 +10131,16 @@ function createMcpAuthOrchestration(deps, abortAgent) {
9779
10131
 
9780
10132
  // src/chat/respond.ts
9781
10133
  var startupDiscoveryLogged = false;
10134
+ function buildOmittedImageAttachmentNotice(count) {
10135
+ return [
10136
+ "<omitted-image-attachments>",
10137
+ `count: ${count}`,
10138
+ "Slack included image attachments with this turn, but this runtime cannot analyze images because no vision model is configured.",
10139
+ "Do not claim that no image was attached.",
10140
+ "If the user asks about image contents, explain that image analysis is unavailable in this runtime and continue with any text or non-image files that are still available.",
10141
+ "</omitted-image-attachments>"
10142
+ ].join("\n");
10143
+ }
9782
10144
  function mcpToolsToDefinitions(mcpTools) {
9783
10145
  const defs = {};
9784
10146
  for (const tool2 of mcpTools) {
@@ -9850,6 +10212,8 @@ async function generateAssistantReply(messageText, context = {}) {
9850
10212
  let configurationValues;
9851
10213
  const userInput = messageText;
9852
10214
  if (shouldTrace) {
10215
+ const inboundAttachmentCount = context.inboundAttachmentCount ?? 0;
10216
+ const promptAttachmentCount = context.userAttachments?.length ?? 0;
9853
10217
  logInfo(
9854
10218
  "agent_message_in",
9855
10219
  spanContext,
@@ -9857,7 +10221,10 @@ async function generateAssistantReply(messageText, context = {}) {
9857
10221
  "app.message.kind": "user_inbound",
9858
10222
  "app.message.length": userInput.length,
9859
10223
  "app.message.input": summarizeMessageText(userInput),
9860
- "app.message.attachment_count": context.userAttachments?.length ?? 0,
10224
+ // Log both counts so image uploads filtered by vision/config do not
10225
+ // look indistinguishable from Slack ingress dropping attachments.
10226
+ "app.message.attachment_count": inboundAttachmentCount,
10227
+ "app.message.prompt_attachment_count": promptAttachmentCount,
9861
10228
  "messaging.message.id": context.correlation?.messageTs ?? ""
9862
10229
  },
9863
10230
  "Agent message received"
@@ -10110,6 +10477,13 @@ async function generateAssistantReply(messageText, context = {}) {
10110
10477
  threadParticipants: context.threadParticipants
10111
10478
  });
10112
10479
  const userContentParts = [{ type: "text", text: userTurnText }];
10480
+ const omittedImageAttachmentCount = context.omittedImageAttachmentCount ?? 0;
10481
+ if (omittedImageAttachmentCount > 0) {
10482
+ userContentParts.push({
10483
+ type: "text",
10484
+ text: buildOmittedImageAttachmentNotice(omittedImageAttachmentCount)
10485
+ });
10486
+ }
10113
10487
  for (const attachment of context.userAttachments ?? []) {
10114
10488
  if (attachment.promptText) {
10115
10489
  userContentParts.push({
@@ -10251,7 +10625,12 @@ async function generateAssistantReply(messageText, context = {}) {
10251
10625
  spanContext,
10252
10626
  async () => {
10253
10627
  let promptResult;
10254
- const promptPromise = resumedFromCheckpoint ? agent.continue() : agent.prompt({
10628
+ const promptPromise = resumedFromCheckpoint ? (
10629
+ // Checkpoint resumes continue from the persisted Pi message
10630
+ // state. Any reconstructed replyContext only matters when the
10631
+ // turn parked before the initial user prompt was recorded.
10632
+ agent.continue()
10633
+ ) : agent.prompt({
10255
10634
  role: "user",
10256
10635
  content: userContentParts,
10257
10636
  timestamp: Date.now()
@@ -10346,7 +10725,6 @@ async function generateAssistantReply(messageText, context = {}) {
10346
10725
  sandboxId: currentSandboxExecutor.getSandboxId(),
10347
10726
  sandboxDependencyProfileHash: currentSandboxExecutor.getDependencyProfileHash(),
10348
10727
  generatedFileCount: generatedFiles.length,
10349
- hasTextDeltaCallback: Boolean(context.onTextDelta),
10350
10728
  shouldTrace,
10351
10729
  spanContext,
10352
10730
  correlation: context.correlation,
@@ -10459,172 +10837,40 @@ async function generateAssistantReply(messageText, context = {}) {
10459
10837
  }
10460
10838
  }
10461
10839
 
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
10521
- });
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
10544
- });
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;
10563
- }
10564
- currentKey = "";
10565
- await postStatus("");
10566
- },
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
- );
10604
- }
10605
- };
10606
- }
10607
-
10608
10840
  // src/chat/slack/reply.ts
10609
10841
  import { Buffer as Buffer2 } from "buffer";
10610
10842
  function isInterruptedVisibleReply(reply) {
10611
10843
  return reply.diagnostics.outcome === "provider_error";
10612
10844
  }
10613
- function buildChunkMessage(chunk, files) {
10845
+ function resolveReplyDelivery(reply) {
10846
+ const replyHasFiles = Boolean(reply.files && reply.files.length > 0);
10847
+ const deliveryPlan = reply.deliveryPlan ?? {
10848
+ mode: reply.deliveryMode ?? "thread",
10849
+ postThreadText: (reply.deliveryMode ?? "thread") !== "channel_only",
10850
+ attachFiles: replyHasFiles ? "inline" : "none"
10851
+ };
10614
10852
  return {
10615
- markdown: chunk,
10616
- ...files ? { files } : {}
10853
+ shouldPostThreadReply: deliveryPlan.postThreadText,
10854
+ attachFiles: replyHasFiles && deliveryPlan.attachFiles !== "none" ? "inline" : "none"
10617
10855
  };
10618
10856
  }
10857
+ function buildReplyText(text) {
10858
+ const message = buildSlackOutputMessage(text);
10859
+ if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
10860
+ return message.markdown;
10861
+ }
10862
+ if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
10863
+ return message.raw;
10864
+ }
10865
+ return "";
10866
+ }
10619
10867
  function buildTextPosts(args) {
10620
10868
  const chunks = splitSlackReplyText(args.text, {
10621
10869
  interrupted: args.interrupted
10622
10870
  });
10623
10871
  return chunks.map((chunk, index) => ({
10624
- message: buildChunkMessage(
10625
- chunk,
10626
- index === 0 ? args.firstFiles : void 0
10627
- ),
10872
+ text: chunk,
10873
+ ...index === 0 && args.firstFiles ? { files: args.firstFiles } : {},
10628
10874
  stage: index === 0 ? args.firstStage ?? "thread_reply" : "thread_reply_continuation"
10629
10875
  }));
10630
10876
  }
@@ -10656,138 +10902,52 @@ async function uploadReplyFilesBestEffort(args) {
10656
10902
  } catch {
10657
10903
  }
10658
10904
  }
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;
10722
- }
10723
- };
10724
- }
10725
10905
  function planSlackReplyPosts(args) {
10726
10906
  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
- });
10907
+ const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery(
10908
+ args.reply
10909
+ );
10731
10910
  const interrupted = isInterruptedVisibleReply(args.reply);
10732
10911
  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
- }
10912
+ const textPosts = shouldPostThreadReply ? buildTextPosts({
10913
+ text: args.reply.text,
10914
+ interrupted,
10915
+ firstFiles: attachFiles === "inline" ? replyFiles : void 0
10916
+ }) : [];
10917
+ posts.push(...textPosts);
10918
+ if (attachFiles === "inline" && replyFiles && textPosts.length === 0) {
10919
+ posts.push({
10920
+ files: replyFiles,
10921
+ stage: "thread_reply",
10922
+ text: ""
10923
+ });
10924
+ } else if (shouldPostThreadReply && textPosts.length === 0) {
10925
+ posts.push({
10926
+ text: buildReplyText(args.reply.text),
10927
+ stage: "thread_reply"
10928
+ });
10768
10929
  }
10769
10930
  if (attachFiles === "followup" && replyFiles) {
10770
10931
  posts.push({
10771
- message: buildSlackOutputMessage("", replyFiles),
10772
- stage: "thread_reply_files_followup"
10932
+ files: replyFiles,
10933
+ stage: "thread_reply_files_followup",
10934
+ text: ""
10773
10935
  });
10774
10936
  }
10775
10937
  return posts;
10776
10938
  }
10777
10939
  async function postSlackApiReplyPosts(args) {
10778
10940
  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);
10941
+ if (post.text.trim().length > 0) {
10942
+ await args.postMessage(args.channelId, args.threadTs, post.text);
10782
10943
  }
10783
- const files = getReplyMessageFiles(post.message);
10784
- if (!files?.length) {
10944
+ if (!post.files?.length) {
10785
10945
  continue;
10786
10946
  }
10787
10947
  await uploadReplyFilesBestEffort({
10788
10948
  channelId: args.channelId,
10789
10949
  threadTs: args.threadTs,
10790
- files
10950
+ files: post.files
10791
10951
  });
10792
10952
  }
10793
10953
  }
@@ -10804,16 +10964,16 @@ function resolveReplyTimeoutMs(explicitTimeoutMs) {
10804
10964
  const parsed = Number.parseInt(raw, 10);
10805
10965
  return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
10806
10966
  }
10807
- async function postSlackMessage(channelId, threadTs, text) {
10808
- await getSlackClient().chat.postMessage({
10809
- channel: channelId,
10810
- thread_ts: threadTs,
10967
+ async function postSlackMessage2(channelId, threadTs, text) {
10968
+ await postSlackMessage({
10969
+ channelId,
10970
+ threadTs,
10811
10971
  text
10812
10972
  });
10813
10973
  }
10814
10974
  async function postSlackMessageBestEffort(channelId, threadTs, text) {
10815
10975
  try {
10816
- await postSlackMessage(channelId, threadTs, text);
10976
+ await postSlackMessage2(channelId, threadTs, text);
10817
10977
  } catch {
10818
10978
  }
10819
10979
  }
@@ -10852,7 +11012,7 @@ var ResumeTurnBusyError = class extends Error {
10852
11012
  function getDefaultLockKey(channelId, threadTs) {
10853
11013
  return `slack:${channelId}:${threadTs}`;
10854
11014
  }
10855
- function createResumeReplyContext(args, progress) {
11015
+ function createResumeReplyContext(args, statusSession) {
10856
11016
  const replyContext = args.replyContext ?? {};
10857
11017
  const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
10858
11018
  const persistedChannelConfiguration = replyContext.channelConfiguration ?? (replyContext.configuration ? createReadOnlyConfigService(replyContext.configuration) : void 0);
@@ -10881,9 +11041,9 @@ function createResumeReplyContext(args, progress) {
10881
11041
  await persistThreadStateById(threadId, { artifacts });
10882
11042
  await replyContext.onArtifactStateUpdated?.(artifacts);
10883
11043
  },
10884
- onStatus: async (status) => {
10885
- await progress.setStatus(status);
10886
- await replyContext.onStatus?.(status);
11044
+ onStatus: async (nextStatus) => {
11045
+ statusSession.update(nextStatus);
11046
+ await replyContext.onStatus?.(nextStatus);
10887
11047
  }
10888
11048
  };
10889
11049
  }
@@ -10902,10 +11062,9 @@ async function resumeSlackTurn(args) {
10902
11062
  if (!lock) {
10903
11063
  throw new ResumeTurnBusyError(lockKey);
10904
11064
  }
10905
- const progress = createProgressReporter({
11065
+ const status = createSlackWebApiAssistantStatusSession({
10906
11066
  channelId: args.channelId,
10907
- threadTs: args.threadTs,
10908
- transport: createSlackWebApiAssistantStatusTransport()
11067
+ threadTs: args.threadTs
10909
11068
  });
10910
11069
  let deferredPauseHandler;
10911
11070
  let deferredFailureHandler;
@@ -10917,9 +11076,9 @@ async function resumeSlackTurn(args) {
10917
11076
  args.initialText
10918
11077
  );
10919
11078
  }
10920
- await progress.start();
11079
+ status.start();
10921
11080
  const generateReply = args.generateReply ?? generateAssistantReply;
10922
- const replyContext = createResumeReplyContext(args, progress);
11081
+ const replyContext = createResumeReplyContext(args, status);
10923
11082
  const replyPromise = generateReply(args.messageText, {
10924
11083
  ...replyContext
10925
11084
  });
@@ -10937,19 +11096,16 @@ async function resumeSlackTurn(args) {
10937
11096
  )
10938
11097
  )
10939
11098
  ]) : await replyPromise;
10940
- await progress.stop();
11099
+ await status.stop();
10941
11100
  await postSlackApiReplyPosts({
10942
11101
  channelId: args.channelId,
10943
11102
  threadTs: args.threadTs,
10944
- posts: planSlackReplyPosts({
10945
- reply,
10946
- hasStreamedThreadReply: false
10947
- }),
10948
- postMessage: postSlackMessage
11103
+ posts: planSlackReplyPosts({ reply }),
11104
+ postMessage: postSlackMessage2
10949
11105
  });
10950
11106
  await args.onSuccess?.(reply);
10951
11107
  } catch (error) {
10952
- await progress.stop();
11108
+ await status.stop();
10953
11109
  if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
10954
11110
  deferredPauseHandler = async () => {
10955
11111
  await args.onAuthPause?.(error);
@@ -11275,7 +11431,8 @@ async function resumeAuthorizedMcpTurn(args) {
11275
11431
  configuration: authSession.configuration,
11276
11432
  channelConfiguration,
11277
11433
  sandbox: getPersistedSandboxState(currentState),
11278
- threadParticipants: buildThreadParticipants(conversation.messages)
11434
+ threadParticipants: buildThreadParticipants(conversation.messages),
11435
+ ...getTurnUserReplyAttachmentContext(userMessage)
11279
11436
  },
11280
11437
  onSuccess: async (reply) => {
11281
11438
  try {
@@ -11746,7 +11903,7 @@ async function GET5(request, provider, waitUntil) {
11746
11903
  } else if (stored.channelId && stored.threadTs) {
11747
11904
  const { channelId, threadTs } = stored;
11748
11905
  waitUntil(
11749
- () => postSlackMessage(
11906
+ () => postSlackMessage2(
11750
11907
  channelId,
11751
11908
  threadTs,
11752
11909
  `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
@@ -11929,7 +12086,8 @@ async function resumeTimedOutTurn(payload) {
11929
12086
  conversationContext,
11930
12087
  channelConfiguration,
11931
12088
  sandbox,
11932
- threadParticipants: buildThreadParticipants(conversation.messages)
12089
+ threadParticipants: buildThreadParticipants(conversation.messages),
12090
+ ...getTurnUserReplyAttachmentContext(userMessage)
11933
12091
  },
11934
12092
  onSuccess: async (reply) => {
11935
12093
  try {
@@ -12386,11 +12544,21 @@ function getRunId(thread, message) {
12386
12544
  return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
12387
12545
  }
12388
12546
  function getChannelId(thread, message) {
12389
- return thread.channelId ?? resolveSlackChannelIdFromMessage(message);
12547
+ return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
12390
12548
  }
12391
12549
  function getThreadTs(threadId) {
12392
12550
  return parseSlackThreadId(threadId)?.threadTs;
12393
12551
  }
12552
+ function getAssistantThreadContext(message) {
12553
+ const raw = message.raw;
12554
+ const rawRecord = raw && typeof raw === "object" ? raw : void 0;
12555
+ const channelId = toOptionalString(rawRecord?.channel);
12556
+ const threadTs = toOptionalString(rawRecord?.thread_ts);
12557
+ if (!channelId || !threadTs) {
12558
+ return void 0;
12559
+ }
12560
+ return { channelId, threadTs };
12561
+ }
12394
12562
  function getMessageTs(message) {
12395
12563
  const directTs = toOptionalString(
12396
12564
  message.ts
@@ -12743,7 +12911,7 @@ function createSlackTurnRuntime(deps) {
12743
12911
  },
12744
12912
  async handleAssistantContextChanged(event) {
12745
12913
  try {
12746
- await deps.initializeAssistantThread({
12914
+ await deps.refreshAssistantThreadContext({
12747
12915
  threadId: event.threadId,
12748
12916
  channelId: event.channelId,
12749
12917
  threadTs: event.threadTs,
@@ -12886,13 +13054,16 @@ var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
12886
13054
  var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
12887
13055
  var MAX_VISION_SUMMARY_CHARS = 500;
12888
13056
  function hasPotentialImageAttachment(attachments) {
12889
- return attachments?.some((attachment) => {
13057
+ return countPotentialImageAttachments(attachments) > 0;
13058
+ }
13059
+ function countPotentialImageAttachments(attachments) {
13060
+ return attachments?.filter((attachment) => {
12890
13061
  if (attachment.type === "image") {
12891
13062
  return true;
12892
13063
  }
12893
13064
  const mimeType = attachment.mimeType ?? "";
12894
13065
  return attachment.type === "file" && mimeType.startsWith("image/");
12895
- }) ?? false;
13066
+ }).length ?? 0;
12896
13067
  }
12897
13068
  function isVisionEnabled() {
12898
13069
  return Boolean(botConfig.visionModelId);
@@ -13426,45 +13597,71 @@ function createJuniorRuntimeServices(overrides = {}) {
13426
13597
  };
13427
13598
  }
13428
13599
 
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;
13600
+ // src/chat/slack/assistant-thread/title.ts
13601
+ function maybeUpdateAssistantTitle(args) {
13602
+ const assistantThreadContext = args.assistantThreadContext;
13603
+ if (!assistantThreadContext?.channelId || !assistantThreadContext.threadTs || !isDmChannel(assistantThreadContext.channelId)) {
13604
+ return Promise.resolve(void 0);
13605
+ }
13606
+ const titleSourceMessage = getThreadTitleSourceMessage(args.conversation);
13607
+ if (!titleSourceMessage) {
13608
+ return Promise.resolve(void 0);
13609
+ }
13610
+ if (args.artifacts.assistantTitleSourceMessageId === titleSourceMessage.id) {
13611
+ return Promise.resolve(void 0);
13612
+ }
13613
+ return (async () => {
13614
+ try {
13615
+ const title = await args.generateThreadTitle(titleSourceMessage.text);
13616
+ await args.getSlackAdapter().setAssistantTitle(
13617
+ assistantThreadContext.channelId,
13618
+ assistantThreadContext.threadTs,
13619
+ title
13620
+ );
13621
+ return titleSourceMessage.id;
13622
+ } catch (error) {
13623
+ const slackErrorCode = getSlackApiErrorCode(error);
13624
+ const assistantTitleErrorAttributes = {
13625
+ "app.slack.assistant_title.outcome": "permission_denied",
13626
+ ...slackErrorCode ? {
13627
+ "app.slack.assistant_title.error_code": slackErrorCode
13628
+ } : {}
13629
+ };
13630
+ if (isSlackTitlePermissionError(error)) {
13631
+ setSpanAttributes(assistantTitleErrorAttributes);
13632
+ logError(
13633
+ "thread_title_generation_permission_denied",
13634
+ {
13635
+ slackThreadId: args.threadId,
13636
+ slackUserId: args.requesterId,
13637
+ slackChannelId: args.channelId,
13638
+ runId: args.runId,
13639
+ assistantUserName: args.assistantUserName,
13640
+ modelId: args.modelId
13641
+ },
13642
+ assistantTitleErrorAttributes,
13643
+ "Skipping thread title update due to Slack permission error"
13644
+ );
13645
+ return titleSourceMessage.id;
13461
13646
  }
13462
- ended = true;
13463
- const wake = wakeConsumer;
13464
- wakeConsumer = null;
13465
- wake?.();
13647
+ logWarn(
13648
+ "thread_title_generation_failed",
13649
+ {
13650
+ slackThreadId: args.threadId,
13651
+ slackUserId: args.requesterId,
13652
+ slackChannelId: args.channelId,
13653
+ runId: args.runId,
13654
+ assistantUserName: args.assistantUserName,
13655
+ modelId: args.modelId
13656
+ },
13657
+ {
13658
+ "error.message": error instanceof Error ? error.message : String(error)
13659
+ },
13660
+ "Thread title generation failed"
13661
+ );
13662
+ return void 0;
13466
13663
  }
13467
- };
13664
+ })();
13468
13665
  }
13469
13666
 
13470
13667
  // src/chat/runtime/reply-executor.ts
@@ -13481,14 +13678,6 @@ function getExecutionFailureReason(reply) {
13481
13678
  }
13482
13679
  return "empty assistant turn";
13483
13680
  }
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
13681
  function createReplyToThread(deps) {
13493
13682
  return async function replyToThread(thread, message, options = {}) {
13494
13683
  if (message.author.isMe) {
@@ -13497,6 +13686,7 @@ function createReplyToThread(deps) {
13497
13686
  const threadId = getThreadId(thread, message);
13498
13687
  const channelId = getChannelId(thread, message);
13499
13688
  const threadTs = getThreadTs(threadId);
13689
+ const assistantThreadContext = getAssistantThreadContext(message);
13500
13690
  const messageTs = getMessageTs(message);
13501
13691
  const runId = getRunId(thread, message);
13502
13692
  const conversationId = threadId ?? runId;
@@ -13516,7 +13706,6 @@ function createReplyToThread(deps) {
13516
13706
  const userText = stripLeadingBotMention(message.text, {
13517
13707
  stripLeadingSlackMentionToken: options.explicitMention || Boolean(message.isMention)
13518
13708
  });
13519
- const explicitChannelPostIntent = isExplicitChannelPostIntent(userText);
13520
13709
  const preparedState = options.preparedState ?? await deps.prepareTurnState({
13521
13710
  thread,
13522
13711
  message,
@@ -13587,20 +13776,13 @@ function createReplyToThread(deps) {
13587
13776
  messageTs: slackMessageTs
13588
13777
  }
13589
13778
  );
13590
- const progress = createProgressReporter({
13591
- channelId,
13592
- threadTs,
13593
- transport: createSlackAdapterAssistantStatusTransport({
13594
- getSlackAdapter: deps.getSlackAdapter
13595
- })
13779
+ const omittedImageAttachmentCount = !isVisionEnabled() && hasPotentialImageAttachment(message.attachments) ? countPotentialImageAttachments(message.attachments) : 0;
13780
+ const status = createSlackAdapterAssistantStatusSession({
13781
+ channelId: assistantThreadContext?.channelId,
13782
+ threadTs: assistantThreadContext?.threadTs,
13783
+ getSlackAdapter: deps.getSlackAdapter
13596
13784
  });
13597
- const textStream = createTextStreamBridge();
13598
- let streamedReplyPromise;
13599
- let pendingStreamText = "";
13600
- let pendingStreamDeltaCount = 0;
13601
- let awaitingPostToolAssistantMessage = false;
13602
13785
  let beforeFirstResponsePostCalled = false;
13603
- let streamedReplyState = createSlackStreamAccumulator();
13604
13786
  const beforeFirstResponsePost = async () => {
13605
13787
  if (beforeFirstResponsePostCalled) {
13606
13788
  return;
@@ -13608,70 +13790,6 @@ function createReplyToThread(deps) {
13608
13790
  beforeFirstResponsePostCalled = true;
13609
13791
  await options.beforeFirstResponsePost?.();
13610
13792
  };
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
13793
  const postThreadReply = async (payload, stage) => {
13676
13794
  await beforeFirstResponsePost();
13677
13795
  try {
@@ -13691,7 +13809,20 @@ function createReplyToThread(deps) {
13691
13809
  throw error;
13692
13810
  }
13693
13811
  };
13694
- await progress.start();
13812
+ status.start();
13813
+ const assistantTitleTask = maybeUpdateAssistantTitle({
13814
+ assistantThreadContext,
13815
+ assistantUserName: botConfig.userName,
13816
+ artifacts: preparedState.artifacts,
13817
+ channelId,
13818
+ conversation: preparedState.conversation,
13819
+ generateThreadTitle: deps.services.generateThreadTitle,
13820
+ getSlackAdapter: deps.getSlackAdapter,
13821
+ modelId: botConfig.fastModelId,
13822
+ requesterId: message.author.userId,
13823
+ runId,
13824
+ threadId
13825
+ });
13695
13826
  let persistedAtLeastOnce = false;
13696
13827
  let shouldPersistFailureState = true;
13697
13828
  try {
@@ -13712,6 +13843,8 @@ function createReplyToThread(deps) {
13712
13843
  artifactState: preparedState.artifacts,
13713
13844
  configuration: preparedState.configuration,
13714
13845
  channelConfiguration: preparedState.channelConfiguration,
13846
+ inboundAttachmentCount: message.attachments.length,
13847
+ omittedImageAttachmentCount,
13715
13848
  userAttachments,
13716
13849
  correlation: {
13717
13850
  conversationId,
@@ -13738,33 +13871,8 @@ function createReplyToThread(deps) {
13738
13871
  await persistThreadState(thread, { artifacts });
13739
13872
  },
13740
13873
  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
- }
13874
+ onStatus: (nextStatus) => status.update(nextStatus)
13761
13875
  });
13762
- if (streamedReplyPromise) {
13763
- flushPendingStreamText();
13764
- } else {
13765
- finalizePendingStreamText();
13766
- }
13767
- textStream.end();
13768
13876
  const diagnosticsContext = {
13769
13877
  slackThreadId: threadId,
13770
13878
  slackUserId: message.author.userId,
@@ -13839,30 +13947,23 @@ function createReplyToThread(deps) {
13839
13947
  const reactionPerformed = reply.diagnostics.toolCalls.includes(
13840
13948
  "slackMessageAddReaction"
13841
13949
  );
13842
- const plannedPosts = planSlackReplyPosts({
13843
- reply,
13844
- hasStreamedThreadReply: Boolean(streamedReplyPromise),
13845
- streamedOverflowText: streamedReplyState.getOverflowText()
13846
- });
13847
- if (streamedReplyPromise) {
13848
- await streamedReplyPromise;
13849
- }
13950
+ const plannedPosts = planSlackReplyPosts({ reply });
13850
13951
  if (plannedPosts.length > 0) {
13851
- if (!streamedReplyPromise) {
13852
- let sent;
13853
- for (const post of plannedPosts) {
13854
- sent = await postThreadReply(post.message, post.stage);
13855
- }
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();
13860
- }
13861
- } else {
13862
- for (const post of plannedPosts) {
13863
- await postThreadReply(post.message, post.stage);
13864
- }
13952
+ let sent;
13953
+ for (const post of plannedPosts) {
13954
+ sent = await postThreadReply(
13955
+ buildSlackOutputMessage(post.text, post.files),
13956
+ post.stage
13957
+ );
13865
13958
  }
13959
+ const firstPlannedMessageHasFiles = (plannedPosts[0]?.files?.length ?? 0) > 0;
13960
+ if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
13961
+ await sent.delete();
13962
+ }
13963
+ }
13964
+ const titleUpdateResult = await assistantTitleTask;
13965
+ if (titleUpdateResult) {
13966
+ artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult;
13866
13967
  }
13867
13968
  const shouldPersistArtifacts = Object.keys(artifactStatePatch).length > 0;
13868
13969
  const nextArtifacts = shouldPersistArtifacts ? mergeArtifactsState(preparedState.artifacts, artifactStatePatch) : void 0;
@@ -13891,73 +13992,17 @@ function createReplyToThread(deps) {
13891
13992
  "Agent turn completed"
13892
13993
  );
13893
13994
  }
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
13995
  } catch (error) {
13941
13996
  if (isRetryableTurnError(error, "mcp_auth_resume")) {
13942
13997
  shouldPersistFailureState = false;
13943
13998
  throw error;
13944
13999
  }
13945
14000
  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
14001
  const conversationIdForResume = error.metadata?.conversationId;
13957
14002
  const sessionIdForResume = error.metadata?.sessionId;
13958
14003
  const checkpointVersion = error.metadata?.checkpointVersion;
13959
14004
  const nextSliceId = error.metadata?.sliceId;
13960
- if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
14005
+ if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
13961
14006
  try {
13962
14007
  await deps.services.scheduleTurnTimeoutResume({
13963
14008
  conversationId: conversationIdForResume,
@@ -13978,7 +14023,7 @@ function createReplyToThread(deps) {
13978
14023
  "Failed to schedule timeout resume callback"
13979
14024
  );
13980
14025
  }
13981
- } else if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
14026
+ } else if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
13982
14027
  logWarn(
13983
14028
  "agent_turn_timeout_resume_slice_limit_reached",
13984
14029
  turnTraceContext,
@@ -14000,7 +14045,6 @@ function createReplyToThread(deps) {
14000
14045
  shouldPersistFailureState = true;
14001
14046
  throw error;
14002
14047
  } finally {
14003
- textStream.end();
14004
14048
  if (!persistedAtLeastOnce && shouldPersistFailureState) {
14005
14049
  markTurnFailed({
14006
14050
  conversation: preparedState.conversation,
@@ -14025,19 +14069,26 @@ function createReplyToThread(deps) {
14025
14069
  );
14026
14070
  }
14027
14071
  }
14028
- await progress.stop();
14072
+ await status.stop();
14029
14073
  }
14030
14074
  }
14031
14075
  );
14032
14076
  };
14033
14077
  }
14034
14078
 
14035
- // src/chat/runtime/assistant-lifecycle.ts
14079
+ // src/chat/slack/assistant-thread/lifecycle.ts
14036
14080
  import { ThreadImpl } from "chat";
14037
- async function initializeAssistantThread(event) {
14081
+ async function syncAssistantThreadContext(event, options) {
14082
+ const channelId = normalizeSlackConversationId(event.channelId);
14083
+ if (!channelId) {
14084
+ throw new Error("Assistant thread initialization requires a channel ID");
14085
+ }
14086
+ const sourceChannelId = event.sourceChannelId ? normalizeSlackConversationId(event.sourceChannelId) : void 0;
14038
14087
  const slack = event.getSlackAdapter();
14039
- await slack.setAssistantTitle(event.channelId, event.threadTs, "Junior");
14040
- await slack.setSuggestedPrompts(event.channelId, event.threadTs, [
14088
+ if (options.setInitialTitle) {
14089
+ await slack.setAssistantTitle(channelId, event.threadTs, "Junior");
14090
+ }
14091
+ await slack.setSuggestedPrompts(channelId, event.threadTs, [
14041
14092
  {
14042
14093
  title: "Summarize thread",
14043
14094
  message: "Summarize the latest discussion in this thread."
@@ -14048,24 +14099,30 @@ async function initializeAssistantThread(event) {
14048
14099
  message: "Generate an image based on this conversation."
14049
14100
  }
14050
14101
  ]);
14051
- if (!event.sourceChannelId) {
14102
+ if (!sourceChannelId) {
14052
14103
  return;
14053
14104
  }
14054
14105
  const thread = ThreadImpl.fromJSON({
14055
14106
  _type: "chat:Thread",
14056
14107
  adapterName: "slack",
14057
- channelId: event.channelId,
14108
+ channelId,
14058
14109
  id: event.threadId,
14059
- isDM: event.channelId.startsWith("D")
14110
+ isDM: channelId.startsWith("D")
14060
14111
  });
14061
14112
  const currentArtifacts = coerceThreadArtifactsState(await thread.state);
14062
14113
  const nextArtifacts = mergeArtifactsState(currentArtifacts, {
14063
- assistantContextChannelId: event.sourceChannelId
14114
+ assistantContextChannelId: sourceChannelId
14064
14115
  });
14065
14116
  await persistThreadState(thread, {
14066
14117
  artifacts: nextArtifacts
14067
14118
  });
14068
14119
  }
14120
+ async function initializeAssistantThread(event) {
14121
+ await syncAssistantThreadContext(event, { setInitialTitle: true });
14122
+ }
14123
+ async function refreshAssistantThreadContext(event) {
14124
+ await syncAssistantThreadContext(event, { setInitialTitle: false });
14125
+ }
14069
14126
 
14070
14127
  // src/chat/runtime/turn-preparation.ts
14071
14128
  function hasPendingImageHydration(conversation) {
@@ -14093,6 +14150,7 @@ function createPrepareTurnState(deps) {
14093
14150
  const messageHasPotentialImageAttachment = hasPotentialImageAttachment(
14094
14151
  args.message.attachments
14095
14152
  );
14153
+ const imageAttachmentCount = messageHasPotentialImageAttachment ? countPotentialImageAttachments(args.message.attachments) : 0;
14096
14154
  const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
14097
14155
  const slackTs = getSlackMessageTs(args.message);
14098
14156
  const incomingUserMessage = {
@@ -14107,7 +14165,9 @@ function createPrepareTurnState(deps) {
14107
14165
  isBot: typeof args.message.author.isBot === "boolean" ? args.message.author.isBot : void 0
14108
14166
  },
14109
14167
  meta: {
14168
+ attachmentCount: args.message.attachments.length,
14110
14169
  explicitMention: args.explicitMention,
14170
+ imageAttachmentCount: imageAttachmentCount > 0 ? imageAttachmentCount : void 0,
14111
14171
  slackTs,
14112
14172
  imagesHydrated: !messageHasPotentialImageAttachment
14113
14173
  }
@@ -14261,6 +14321,20 @@ function createSlackRuntime(options) {
14261
14321
  sourceChannelId,
14262
14322
  getSlackAdapter: options.getSlackAdapter
14263
14323
  });
14324
+ },
14325
+ refreshAssistantThreadContext: async ({
14326
+ threadId,
14327
+ channelId,
14328
+ threadTs,
14329
+ sourceChannelId
14330
+ }) => {
14331
+ await refreshAssistantThreadContext({
14332
+ threadId,
14333
+ channelId,
14334
+ threadTs,
14335
+ sourceChannelId,
14336
+ getSlackAdapter: options.getSlackAdapter
14337
+ });
14264
14338
  }
14265
14339
  });
14266
14340
  }
@@ -14333,14 +14407,14 @@ var JuniorChat = class extends Chat {
14333
14407
  (async () => {
14334
14408
  try {
14335
14409
  const message = await messageOrFactory();
14336
- const normalized2 = normalizeIncomingSlackThreadId(
14410
+ const normalized = normalizeIncomingSlackThreadId(
14337
14411
  threadId,
14338
14412
  message
14339
14413
  );
14340
- if (normalized2 !== threadId && "threadId" in message) {
14341
- message.threadId = normalized2;
14414
+ if (normalized !== threadId && "threadId" in message) {
14415
+ message.threadId = normalized;
14342
14416
  }
14343
- super.processMessage(adapter, normalized2, message, options);
14417
+ super.processMessage(adapter, normalized, message, options);
14344
14418
  } catch (error) {
14345
14419
  runtime.logger?.error?.("Message factory resolution error", {
14346
14420
  error,
@@ -14351,14 +14425,19 @@ var JuniorChat = class extends Chat {
14351
14425
  );
14352
14426
  return;
14353
14427
  }
14354
- const normalized = normalizeIncomingSlackThreadId(
14355
- threadId,
14356
- messageOrFactory
14428
+ enqueueBackgroundTask(
14429
+ options,
14430
+ (async () => {
14431
+ const normalized = normalizeIncomingSlackThreadId(
14432
+ threadId,
14433
+ messageOrFactory
14434
+ );
14435
+ if (normalized !== threadId && "threadId" in messageOrFactory) {
14436
+ messageOrFactory.threadId = normalized;
14437
+ }
14438
+ super.processMessage(adapter, normalized, messageOrFactory, options);
14439
+ })()
14357
14440
  );
14358
- if (normalized !== threadId && "threadId" in messageOrFactory) {
14359
- messageOrFactory.threadId = normalized;
14360
- }
14361
- super.processMessage(adapter, normalized, messageOrFactory, options);
14362
14441
  }
14363
14442
  processReaction(event, options) {
14364
14443
  const runtime = this;
@@ -14379,20 +14458,19 @@ var JuniorChat = class extends Chat {
14379
14458
  }
14380
14459
  processAction(event, options) {
14381
14460
  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
- );
14461
+ const task = (async () => {
14462
+ try {
14463
+ await runtime.handleActionEvent(event, options);
14464
+ } catch (error) {
14465
+ runtime.logger?.error?.("Action processing error", {
14466
+ error,
14467
+ actionId: event.actionId,
14468
+ messageId: event.messageId
14469
+ });
14470
+ }
14471
+ })();
14472
+ enqueueBackgroundTask(options, task);
14473
+ return task;
14396
14474
  }
14397
14475
  processModalClose(event, contextId, options) {
14398
14476
  const runtime = this;
@@ -14430,7 +14508,7 @@ var JuniorChat = class extends Chat {
14430
14508
  options,
14431
14509
  (async () => {
14432
14510
  try {
14433
- await runtime.handleSlashCommandEvent(event);
14511
+ await runtime.handleSlashCommandEvent(event, options);
14434
14512
  } catch (error) {
14435
14513
  runtime.logger?.error?.("Slash command processing error", {
14436
14514
  error,
@@ -14501,160 +14579,8 @@ var JuniorChat = class extends Chat {
14501
14579
  import {
14502
14580
  createSlackAdapter
14503
14581
  } 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
14582
  function createJuniorSlackAdapter(config) {
14652
- const adapter = createSlackAdapter(config);
14653
- const internals = adapter;
14654
- assertSlackAdapterInternals(internals);
14655
- patchSlackClientStream(adapter);
14656
- patchSlackAdapterStream(adapter);
14657
- return adapter;
14583
+ return createSlackAdapter(config);
14658
14584
  }
14659
14585
 
14660
14586
  // src/chat/queue/thread-message-dispatcher.ts