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