@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 +1096 -1170
- package/dist/{chunk-THPM7NSG.js → chunk-B5O2EJUV.js} +1 -1
- package/dist/{chunk-JWBWBJYJ.js → chunk-DGN3WLA4.js} +1 -1
- package/dist/{chunk-MCJJKEB3.js → chunk-J7JEFMVD.js} +12 -0
- package/dist/cli/check.js +2 -2
- package/dist/cli/snapshot-warmup.js +2 -2
- package/package.json +19 -19
package/dist/app.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
loadSkillsByName,
|
|
7
7
|
logCapabilityCatalogLoadedOnce,
|
|
8
8
|
parseSkillInvocation
|
|
9
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
|
1376
|
+
function getTerminalAssistantMessages(messages) {
|
|
1377
|
+
let lastToolResultIndex = -1;
|
|
1186
1378
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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
|
|
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
|
|
1261
|
-
|
|
1458
|
+
await postSlackMessage({
|
|
1459
|
+
channelId: input.channelId,
|
|
1262
1460
|
text: input.text,
|
|
1263
|
-
|
|
1461
|
+
threadTs: input.threadTs
|
|
1264
1462
|
});
|
|
1265
1463
|
} else {
|
|
1266
|
-
await
|
|
1267
|
-
|
|
1268
|
-
|
|
1464
|
+
await postSlackEphemeralMessage({
|
|
1465
|
+
channelId: input.channelId,
|
|
1466
|
+
userId: input.userId,
|
|
1269
1467
|
text: input.text,
|
|
1270
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
`
|
|
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 (
|
|
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/
|
|
2708
|
-
var
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
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
|
|
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
|
|
2751
|
-
|
|
2752
|
-
|
|
2947
|
+
function truncateStatusText(text) {
|
|
2948
|
+
const trimmed = text.trim();
|
|
2949
|
+
if (!trimmed) {
|
|
2950
|
+
return "";
|
|
2753
2951
|
}
|
|
2754
|
-
return
|
|
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
|
|
2760
|
-
if (
|
|
2761
|
-
return
|
|
2954
|
+
function compactStatusPath(value) {
|
|
2955
|
+
if (typeof value !== "string") {
|
|
2956
|
+
return void 0;
|
|
2762
2957
|
}
|
|
2763
|
-
const
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
return newlineIndex;
|
|
2958
|
+
const trimmed = value.trim();
|
|
2959
|
+
if (!trimmed) {
|
|
2960
|
+
return void 0;
|
|
2767
2961
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
return spaceIndex;
|
|
2962
|
+
if (trimmed.length <= 80) {
|
|
2963
|
+
return trimmed;
|
|
2771
2964
|
}
|
|
2772
|
-
return
|
|
2965
|
+
return `...${trimmed.slice(-77)}`;
|
|
2773
2966
|
}
|
|
2774
|
-
function
|
|
2775
|
-
if (
|
|
2776
|
-
return
|
|
2967
|
+
function compactStatusText(value, maxLength = 80) {
|
|
2968
|
+
if (typeof value !== "string") {
|
|
2969
|
+
return void 0;
|
|
2777
2970
|
}
|
|
2778
|
-
const
|
|
2779
|
-
if (
|
|
2780
|
-
return
|
|
2971
|
+
const trimmed = value.trim();
|
|
2972
|
+
if (!trimmed) {
|
|
2973
|
+
return void 0;
|
|
2781
2974
|
}
|
|
2782
|
-
return
|
|
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
|
|
2798
|
-
let
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
}
|
|
2812
|
-
if (
|
|
2813
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
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 =
|
|
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
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
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
|
|
7686
|
-
const getClient2 = args
|
|
7687
|
-
return {
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
10616
|
-
|
|
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
|
-
|
|
10625
|
-
|
|
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
|
-
|
|
10729
|
-
|
|
10730
|
-
});
|
|
10907
|
+
const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery(
|
|
10908
|
+
args.reply
|
|
10909
|
+
);
|
|
10731
10910
|
const interrupted = isInterruptedVisibleReply(args.reply);
|
|
10732
10911
|
const posts = [];
|
|
10733
|
-
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10780
|
-
|
|
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
|
-
|
|
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
|
|
10808
|
-
await
|
|
10809
|
-
|
|
10810
|
-
|
|
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
|
|
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,
|
|
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 (
|
|
10885
|
-
|
|
10886
|
-
await replyContext.onStatus?.(
|
|
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
|
|
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
|
-
|
|
11079
|
+
status.start();
|
|
10921
11080
|
const generateReply = args.generateReply ?? generateAssistantReply;
|
|
10922
|
-
const replyContext = createResumeReplyContext(args,
|
|
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
|
|
11099
|
+
await status.stop();
|
|
10941
11100
|
await postSlackApiReplyPosts({
|
|
10942
11101
|
channelId: args.channelId,
|
|
10943
11102
|
threadTs: args.threadTs,
|
|
10944
|
-
posts: planSlackReplyPosts({
|
|
10945
|
-
|
|
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
|
|
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
|
-
() =>
|
|
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.
|
|
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
|
|
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
|
-
}) ??
|
|
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/
|
|
13430
|
-
function
|
|
13431
|
-
const
|
|
13432
|
-
|
|
13433
|
-
|
|
13434
|
-
|
|
13435
|
-
|
|
13436
|
-
|
|
13437
|
-
|
|
13438
|
-
|
|
13439
|
-
|
|
13440
|
-
|
|
13441
|
-
|
|
13442
|
-
|
|
13443
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
|
|
13449
|
-
|
|
13450
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
13453
|
-
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
if (
|
|
13460
|
-
|
|
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
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
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
|
|
13591
|
-
|
|
13592
|
-
|
|
13593
|
-
|
|
13594
|
-
|
|
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
|
-
|
|
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: (
|
|
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
|
-
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
14072
|
+
await status.stop();
|
|
14029
14073
|
}
|
|
14030
14074
|
}
|
|
14031
14075
|
);
|
|
14032
14076
|
};
|
|
14033
14077
|
}
|
|
14034
14078
|
|
|
14035
|
-
// src/chat/
|
|
14079
|
+
// src/chat/slack/assistant-thread/lifecycle.ts
|
|
14036
14080
|
import { ThreadImpl } from "chat";
|
|
14037
|
-
async function
|
|
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
|
-
|
|
14040
|
-
|
|
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 (!
|
|
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
|
|
14108
|
+
channelId,
|
|
14058
14109
|
id: event.threadId,
|
|
14059
|
-
isDM:
|
|
14110
|
+
isDM: channelId.startsWith("D")
|
|
14060
14111
|
});
|
|
14061
14112
|
const currentArtifacts = coerceThreadArtifactsState(await thread.state);
|
|
14062
14113
|
const nextArtifacts = mergeArtifactsState(currentArtifacts, {
|
|
14063
|
-
assistantContextChannelId:
|
|
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
|
|
14410
|
+
const normalized = normalizeIncomingSlackThreadId(
|
|
14337
14411
|
threadId,
|
|
14338
14412
|
message
|
|
14339
14413
|
);
|
|
14340
|
-
if (
|
|
14341
|
-
message.threadId =
|
|
14414
|
+
if (normalized !== threadId && "threadId" in message) {
|
|
14415
|
+
message.threadId = normalized;
|
|
14342
14416
|
}
|
|
14343
|
-
super.processMessage(adapter,
|
|
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
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
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
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
|
|
14391
|
-
|
|
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
|
-
|
|
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
|