@letta-ai/letta-code 0.28.7 → 0.28.9
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/agent-presets.js +80 -1
- package/dist/agent-presets.js.map +2 -2
- package/dist/channels-public.js +1053 -0
- package/dist/channels-public.js.map +13 -0
- package/dist/channels-slack.js +534 -0
- package/dist/channels-slack.js.map +15 -0
- package/dist/types/channels/core-stream.d.ts +35 -0
- package/dist/types/channels/core-stream.d.ts.map +1 -0
- package/dist/types/channels/plugin-types.d.ts +226 -0
- package/dist/types/channels/plugin-types.d.ts.map +1 -0
- package/dist/types/channels/processor.d.ts +27 -0
- package/dist/types/channels/processor.d.ts.map +1 -0
- package/dist/types/channels/progress-builder.d.ts +13 -0
- package/dist/types/channels/progress-builder.d.ts.map +1 -0
- package/dist/types/channels/progress-formatting.d.ts +34 -0
- package/dist/types/channels/progress-formatting.d.ts.map +1 -0
- package/dist/types/channels/slack/ingress-policy.d.ts +72 -0
- package/dist/types/channels/slack/ingress-policy.d.ts.map +1 -0
- package/dist/types/channels/slack/progress.d.ts +7 -0
- package/dist/types/channels/slack/progress.d.ts.map +1 -0
- package/dist/types/channels/slack/public-utils.d.ts +11 -0
- package/dist/types/channels/slack/public-utils.d.ts.map +1 -0
- package/dist/types/channels/slack/sender.d.ts +39 -0
- package/dist/types/channels/slack/sender.d.ts.map +1 -0
- package/dist/types/channels/slack/status-controller.d.ts +35 -0
- package/dist/types/channels/slack/status-controller.d.ts.map +1 -0
- package/dist/types/channels/types.d.ts +766 -0
- package/dist/types/channels/types.d.ts.map +1 -0
- package/dist/types/channels-public.d.ts +8 -0
- package/dist/types/channels-public.d.ts.map +1 -0
- package/dist/types/channels-slack.d.ts +9 -0
- package/dist/types/channels-slack.d.ts.map +1 -0
- package/dist/types/permissions/mode.d.ts +52 -0
- package/dist/types/permissions/mode.d.ts.map +1 -0
- package/dist/types/types/protocol_v2.d.ts +4 -0
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/letta.js +2817 -1954
- package/package.json +33 -2
- package/scripts/isolated-unit-tests.json +12 -0
- package/scripts/run-unit-tests.cjs +5 -2
- package/scripts/source-file-size-baseline.json +6 -6
- package/skills/converting-mcps-to-skills/SKILL.md +44 -3
- package/skills/converting-mcps-to-skills/scripts/mcp-http.ts +696 -145
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
// src/channels/slack/public-utils.ts
|
|
2
|
+
function isNonEmptyString(value) {
|
|
3
|
+
return typeof value === "string" && value.length > 0;
|
|
4
|
+
}
|
|
5
|
+
function firstNonEmptyString(...values) {
|
|
6
|
+
return values.find(isNonEmptyString);
|
|
7
|
+
}
|
|
8
|
+
function normalizeSlackText(text) {
|
|
9
|
+
return text.replace(/^(?:\s*<@[A-Z0-9]+>\s*)+/, "").trim();
|
|
10
|
+
}
|
|
11
|
+
function resolveSlackChatType(chatId) {
|
|
12
|
+
return chatId.startsWith("D") ? "direct" : "channel";
|
|
13
|
+
}
|
|
14
|
+
function resolveSlackSourceThreadTs(source) {
|
|
15
|
+
if (source.chatType === "direct" || resolveSlackChatType(source.chatId) === "direct") {
|
|
16
|
+
return firstNonEmptyString(source.threadId);
|
|
17
|
+
}
|
|
18
|
+
return firstNonEmptyString(source.threadId, source.messageId);
|
|
19
|
+
}
|
|
20
|
+
function resolveSlackProgressThreadTs(source) {
|
|
21
|
+
if (source.chatType === "direct" || resolveSlackChatType(source.chatId) === "direct") {
|
|
22
|
+
return firstNonEmptyString(source.threadId, source.messageId);
|
|
23
|
+
}
|
|
24
|
+
return resolveSlackSourceThreadTs(source);
|
|
25
|
+
}
|
|
26
|
+
function resolveSlackOutboundThreadTs(msg) {
|
|
27
|
+
if (resolveSlackChatType(msg.chatId) === "direct") {
|
|
28
|
+
return firstNonEmptyString(msg.threadId);
|
|
29
|
+
}
|
|
30
|
+
return firstNonEmptyString(msg.threadId, msg.replyToMessageId);
|
|
31
|
+
}
|
|
32
|
+
function normalizeSlackReactionName(value) {
|
|
33
|
+
return value.trim().replace(/^:+|:+$/g, "");
|
|
34
|
+
}
|
|
35
|
+
function slackTimestampToMillis(value) {
|
|
36
|
+
return Math.round(Number.parseFloat(value) * 1000);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/channels/slack/ingress-policy.ts
|
|
40
|
+
var IGNORED_SLACK_MESSAGE_SUBTYPES = new Set([
|
|
41
|
+
"assistant_app_thread",
|
|
42
|
+
"channel_archive",
|
|
43
|
+
"channel_convert_to_private",
|
|
44
|
+
"channel_convert_to_public",
|
|
45
|
+
"channel_join",
|
|
46
|
+
"channel_leave",
|
|
47
|
+
"channel_name",
|
|
48
|
+
"channel_posting_permissions",
|
|
49
|
+
"channel_purpose",
|
|
50
|
+
"channel_topic",
|
|
51
|
+
"channel_unarchive",
|
|
52
|
+
"document_mention",
|
|
53
|
+
"ekm_access_denied",
|
|
54
|
+
"file_comment",
|
|
55
|
+
"group_archive",
|
|
56
|
+
"group_join",
|
|
57
|
+
"group_leave",
|
|
58
|
+
"group_name",
|
|
59
|
+
"group_purpose",
|
|
60
|
+
"group_topic",
|
|
61
|
+
"group_unarchive",
|
|
62
|
+
"pinned_item",
|
|
63
|
+
"reminder_add",
|
|
64
|
+
"unpinned_item"
|
|
65
|
+
]);
|
|
66
|
+
var WRAPPER_SLACK_MESSAGE_SUBTYPES = new Set([
|
|
67
|
+
"message_changed",
|
|
68
|
+
"message_deleted",
|
|
69
|
+
"message_replied"
|
|
70
|
+
]);
|
|
71
|
+
function hasRecordValue(value) {
|
|
72
|
+
return value !== null && typeof value === "object";
|
|
73
|
+
}
|
|
74
|
+
function hasSlackMention(text, userId) {
|
|
75
|
+
return isNonEmptyString(text) && isNonEmptyString(userId) && (text.includes(`<@${userId}>`) || text.includes(`<@${userId}|`));
|
|
76
|
+
}
|
|
77
|
+
function isBotAuthoredMessage(message) {
|
|
78
|
+
return isNonEmptyString(message.bot_id) || message.subtype === "bot_message";
|
|
79
|
+
}
|
|
80
|
+
function resolveMessageSubtypeIgnoreReason(message) {
|
|
81
|
+
const subtype = isNonEmptyString(message.subtype) ? message.subtype : null;
|
|
82
|
+
if (!subtype) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (IGNORED_SLACK_MESSAGE_SUBTYPES.has(subtype)) {
|
|
86
|
+
return "ignored_subtype";
|
|
87
|
+
}
|
|
88
|
+
if (WRAPPER_SLACK_MESSAGE_SUBTYPES.has(subtype) && hasRecordValue(message.message)) {
|
|
89
|
+
return "wrapper_message";
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function isProcessableSlackInboundMessage(message) {
|
|
94
|
+
return resolveSlackMessageIngressPolicy({ message }).shouldRoute;
|
|
95
|
+
}
|
|
96
|
+
function shouldSkipSlackMessageByLastSeen(params) {
|
|
97
|
+
return Boolean(params.lastSeenMessageTs && params.lastSeenMessageTs >= params.messageTs);
|
|
98
|
+
}
|
|
99
|
+
function resolveSlackMessageIngressPolicy(params) {
|
|
100
|
+
const { message } = params;
|
|
101
|
+
if (!isNonEmptyString(message.channel)) {
|
|
102
|
+
return { shouldRoute: false, reason: "missing_channel" };
|
|
103
|
+
}
|
|
104
|
+
const senderId = firstNonEmptyString(message.user, message.bot_id);
|
|
105
|
+
if (!senderId) {
|
|
106
|
+
return { shouldRoute: false, reason: "missing_sender" };
|
|
107
|
+
}
|
|
108
|
+
if (!isNonEmptyString(message.ts)) {
|
|
109
|
+
return { shouldRoute: false, reason: "missing_timestamp" };
|
|
110
|
+
}
|
|
111
|
+
if (message.hidden === true) {
|
|
112
|
+
return { shouldRoute: false, reason: "hidden_message" };
|
|
113
|
+
}
|
|
114
|
+
const subtypeIgnoreReason = resolveMessageSubtypeIgnoreReason(message);
|
|
115
|
+
if (subtypeIgnoreReason) {
|
|
116
|
+
return { shouldRoute: false, reason: subtypeIgnoreReason };
|
|
117
|
+
}
|
|
118
|
+
const chatType = resolveSlackChatType(message.channel);
|
|
119
|
+
const threadId = chatType === "direct" ? firstNonEmptyString(message.thread_ts) ?? null : firstNonEmptyString(message.thread_ts, message.ts) ?? null;
|
|
120
|
+
if (chatType === "channel" && !isNonEmptyString(message.thread_ts)) {
|
|
121
|
+
return { shouldRoute: false, reason: "top_level_channel_message" };
|
|
122
|
+
}
|
|
123
|
+
const rawText = isNonEmptyString(message.text) ? message.text : "";
|
|
124
|
+
const wasMentioned = hasSlackMention(rawText, params.botUserId);
|
|
125
|
+
const isAgentThread = params.isAgentThread === true;
|
|
126
|
+
const effectiveMention = isBotAuthoredMessage(message) ? wasMentioned : wasMentioned || isAgentThread;
|
|
127
|
+
return {
|
|
128
|
+
shouldRoute: true,
|
|
129
|
+
channelId: message.channel,
|
|
130
|
+
senderId,
|
|
131
|
+
...isNonEmptyString(message.user) ? { senderUserId: message.user } : {},
|
|
132
|
+
...isNonEmptyString(message.bot_id) ? { senderBotId: message.bot_id } : {},
|
|
133
|
+
messageId: message.ts,
|
|
134
|
+
threadId,
|
|
135
|
+
chatType,
|
|
136
|
+
text: wasMentioned ? normalizeSlackText(rawText) : rawText,
|
|
137
|
+
rawText,
|
|
138
|
+
wasMentioned,
|
|
139
|
+
effectiveMention,
|
|
140
|
+
isAgentThread
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function resolveSlackAppMentionIngressPolicy(params) {
|
|
144
|
+
const { event } = params;
|
|
145
|
+
if (!isNonEmptyString(event.channel)) {
|
|
146
|
+
return { shouldRoute: false, reason: "missing_channel" };
|
|
147
|
+
}
|
|
148
|
+
const senderId = firstNonEmptyString(event.user, event.bot_id);
|
|
149
|
+
if (!senderId) {
|
|
150
|
+
return { shouldRoute: false, reason: "missing_sender" };
|
|
151
|
+
}
|
|
152
|
+
if (!isNonEmptyString(event.ts)) {
|
|
153
|
+
return { shouldRoute: false, reason: "missing_timestamp" };
|
|
154
|
+
}
|
|
155
|
+
const rawText = isNonEmptyString(event.text) ? event.text : "";
|
|
156
|
+
return {
|
|
157
|
+
shouldRoute: true,
|
|
158
|
+
channelId: event.channel,
|
|
159
|
+
senderId,
|
|
160
|
+
...isNonEmptyString(event.user) ? { senderUserId: event.user } : {},
|
|
161
|
+
...isNonEmptyString(event.bot_id) ? { senderBotId: event.bot_id } : {},
|
|
162
|
+
messageId: event.ts,
|
|
163
|
+
threadId: firstNonEmptyString(event.thread_ts, event.ts) ?? event.ts,
|
|
164
|
+
chatType: "channel",
|
|
165
|
+
text: normalizeSlackText(rawText),
|
|
166
|
+
rawText,
|
|
167
|
+
wasMentioned: true,
|
|
168
|
+
effectiveMention: true,
|
|
169
|
+
isAgentThread: false
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// src/channels/progress-formatting.ts
|
|
173
|
+
var ESCAPE_CODE = String.fromCharCode(27);
|
|
174
|
+
var ANSI_ESCAPE_RE = new RegExp(`${ESCAPE_CODE}\\[[0-9;?]*[ -/]*[@-~]`, "g");
|
|
175
|
+
var SECRET_ASSIGNMENT_RE = /\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASS|API[_-]?KEY|ACCESS[_-]?KEY)[A-Z0-9_]*)\s*=\s*("[^"]*"|'[^']*'|\S+)/gi;
|
|
176
|
+
var SECRET_JSON_RE = /(["']?(?:token|secret|password|api[_-]?key|access[_-]?key)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|\S+)/gi;
|
|
177
|
+
function truncateChannelProgressText(value, maxLength, marker = "...") {
|
|
178
|
+
if (value.length <= maxLength) {
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
if (maxLength <= marker.length) {
|
|
182
|
+
return marker.slice(0, Math.max(0, maxLength));
|
|
183
|
+
}
|
|
184
|
+
return `${value.slice(0, maxLength - marker.length).trimEnd()}${marker}`;
|
|
185
|
+
}
|
|
186
|
+
function replaceControlCharacters(value) {
|
|
187
|
+
let result = "";
|
|
188
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
189
|
+
const character = value[index] ?? "";
|
|
190
|
+
const code = character.charCodeAt(0);
|
|
191
|
+
result += code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code === 127 ? " " : character;
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
function sanitizeChannelProgressCore(value) {
|
|
196
|
+
const raw = typeof value === "string" ? value : String(value ?? "");
|
|
197
|
+
const redacted = raw.replace(ANSI_ESCAPE_RE, "").replace(SECRET_ASSIGNMENT_RE, "$1=[redacted]").replace(SECRET_JSON_RE, "$1[redacted]");
|
|
198
|
+
return replaceControlCharacters(redacted).replace(/[\r\n\t]+/g, " ").replace(/@(?=channel|here|everyone|[A-Za-z0-9._-]+)/gi, "@").replace(/\s+/g, " ").trim();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/channels/slack/progress.ts
|
|
202
|
+
var SLACK_ASSISTANT_STARTUP_STATUS = "is thinking...";
|
|
203
|
+
var SLACK_ASSISTANT_WORKING_STATUS = "is working...";
|
|
204
|
+
var SLACK_STATUS_TEXT_MAX = 300;
|
|
205
|
+
function sanitizeSlackStatusText(text, maxLength) {
|
|
206
|
+
const normalized = sanitizeChannelProgressCore(text).replace(/[<>]/g, "").replace(/&/g, "and").replace(/\s+/g, " ").trim();
|
|
207
|
+
return truncateChannelProgressText(normalized, maxLength, "...");
|
|
208
|
+
}
|
|
209
|
+
function formatSlackToolNameForDisplay(toolName) {
|
|
210
|
+
if (toolName === "Task" || toolName === "task" || toolName === "Agent") {
|
|
211
|
+
return "Subagent";
|
|
212
|
+
}
|
|
213
|
+
if (toolName === "Bash" || toolName === "bash" || toolName === "exec_command" || toolName === "shell_command" || toolName === "ShellCommand") {
|
|
214
|
+
return "Bash";
|
|
215
|
+
}
|
|
216
|
+
return toolName;
|
|
217
|
+
}
|
|
218
|
+
function isSlackShellTool(toolName) {
|
|
219
|
+
const normalized = toolName.toLowerCase();
|
|
220
|
+
return normalized === "bash" || normalized === "exec_command" || normalized === "shell_command" || normalized === "shell";
|
|
221
|
+
}
|
|
222
|
+
function resolveSlackConcreteActivity(event) {
|
|
223
|
+
if (event.kind === "command" && isNonEmptyString(event.command)) {
|
|
224
|
+
return sanitizeSlackStatusText(formatSlackToolNameForDisplay(event.command), SLACK_STATUS_TEXT_MAX);
|
|
225
|
+
}
|
|
226
|
+
if (event.kind !== "tool" || !isNonEmptyString(event.toolName) || event.toolName.toLowerCase() === "messagechannel") {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
for (const description of [
|
|
230
|
+
event.toolTitle,
|
|
231
|
+
event.toolDetails,
|
|
232
|
+
isSlackShellTool(event.toolName) ? event.message : undefined,
|
|
233
|
+
formatSlackToolNameForDisplay(event.toolName)
|
|
234
|
+
]) {
|
|
235
|
+
if (!isNonEmptyString(description)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const sanitized = sanitizeSlackStatusText(description, SLACK_STATUS_TEXT_MAX);
|
|
239
|
+
if (sanitized) {
|
|
240
|
+
return sanitized;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
// src/channels/slack/sender.ts
|
|
246
|
+
async function sendSlackReaction(client, message) {
|
|
247
|
+
const targetMessageId = message.targetMessageId ?? message.replyToMessageId;
|
|
248
|
+
if (!targetMessageId) {
|
|
249
|
+
throw new Error("Slack reactions require message_id (or reply_to_message_id) to identify the target message.");
|
|
250
|
+
}
|
|
251
|
+
const name = normalizeSlackReactionName(message.reaction ?? "");
|
|
252
|
+
if (!name) {
|
|
253
|
+
throw new Error("Slack reaction emoji cannot be empty.");
|
|
254
|
+
}
|
|
255
|
+
const params = {
|
|
256
|
+
channel: message.chatId,
|
|
257
|
+
timestamp: targetMessageId,
|
|
258
|
+
name
|
|
259
|
+
};
|
|
260
|
+
if (message.removeReaction) {
|
|
261
|
+
if (!client.removeReaction) {
|
|
262
|
+
throw new Error("Slack sender client does not support removing reactions.");
|
|
263
|
+
}
|
|
264
|
+
await client.removeReaction(params);
|
|
265
|
+
} else {
|
|
266
|
+
if (!client.addReaction) {
|
|
267
|
+
throw new Error("Slack sender client does not support adding reactions.");
|
|
268
|
+
}
|
|
269
|
+
await client.addReaction(params);
|
|
270
|
+
}
|
|
271
|
+
return { messageId: targetMessageId };
|
|
272
|
+
}
|
|
273
|
+
function createSlackChannelSender(params) {
|
|
274
|
+
const { client } = params;
|
|
275
|
+
return {
|
|
276
|
+
async sendMessage(message) {
|
|
277
|
+
if (message.reaction) {
|
|
278
|
+
return await sendSlackReaction(client, message);
|
|
279
|
+
}
|
|
280
|
+
const threadTs = resolveSlackOutboundThreadTs({
|
|
281
|
+
chatId: message.chatId,
|
|
282
|
+
threadId: message.threadId,
|
|
283
|
+
replyToMessageId: message.replyToMessageId
|
|
284
|
+
});
|
|
285
|
+
return await client.postMessage({
|
|
286
|
+
channel: message.chatId,
|
|
287
|
+
text: message.text,
|
|
288
|
+
...threadTs ? { threadTs } : {}
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
async sendDirectReply(params2) {
|
|
292
|
+
const threadTs = resolveSlackOutboundThreadTs({
|
|
293
|
+
chatId: params2.chatId,
|
|
294
|
+
threadId: params2.threadId,
|
|
295
|
+
replyToMessageId: params2.replyToMessageId
|
|
296
|
+
});
|
|
297
|
+
await client.postMessage({
|
|
298
|
+
channel: params2.chatId,
|
|
299
|
+
text: params2.text,
|
|
300
|
+
...params2.blocks ? { blocks: params2.blocks } : {},
|
|
301
|
+
...threadTs ? { threadTs } : {}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// src/channels/slack/status-controller.ts
|
|
307
|
+
var SLACK_ASSISTANT_STATUS_KEEPALIVE_MS = 90000;
|
|
308
|
+
function createSlackStatusController(params) {
|
|
309
|
+
const stateByConversation = new Map;
|
|
310
|
+
const sourceByConversation = new Map;
|
|
311
|
+
const signatureByConversation = new Map;
|
|
312
|
+
const writePromiseByConversation = new Map;
|
|
313
|
+
const keepaliveByConversation = new Map;
|
|
314
|
+
const clearedStaleReplyKeys = new Set;
|
|
315
|
+
function getConversationKey(source) {
|
|
316
|
+
return source.channel === "slack" && isNonEmptyString(source.agentId) && isNonEmptyString(source.conversationId) ? `${source.agentId}:${source.conversationId}` : null;
|
|
317
|
+
}
|
|
318
|
+
function getLifecycleReplyKey(source) {
|
|
319
|
+
if (source.channel !== "slack" || !isNonEmptyString(source.chatId)) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const replyToMessageId = resolveSlackProgressThreadTs(source);
|
|
323
|
+
return isNonEmptyString(replyToMessageId) ? `${source.chatId}:${replyToMessageId}` : null;
|
|
324
|
+
}
|
|
325
|
+
function getLifecycleErrorReplyKey(source) {
|
|
326
|
+
if (source.channel !== "slack" || !isNonEmptyString(source.chatId)) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
if (source.chatType === "direct" || resolveSlackChatType(source.chatId) === "direct") {
|
|
330
|
+
const replyToMessageId = resolveSlackSourceThreadTs(source);
|
|
331
|
+
return isNonEmptyString(replyToMessageId) ? `${source.chatId}:${replyToMessageId}` : `${source.chatId}:direct`;
|
|
332
|
+
}
|
|
333
|
+
return getLifecycleReplyKey(source);
|
|
334
|
+
}
|
|
335
|
+
function getUniqueSources(sources) {
|
|
336
|
+
const seen = new Set;
|
|
337
|
+
const unique = [];
|
|
338
|
+
for (const source of sources) {
|
|
339
|
+
const key = getConversationKey(source);
|
|
340
|
+
if (!key || seen.has(key) || !getLifecycleReplyKey(source))
|
|
341
|
+
continue;
|
|
342
|
+
seen.add(key);
|
|
343
|
+
unique.push(source);
|
|
344
|
+
}
|
|
345
|
+
return unique;
|
|
346
|
+
}
|
|
347
|
+
function clearKeepalive(key) {
|
|
348
|
+
const timer = keepaliveByConversation.get(key);
|
|
349
|
+
if (timer) {
|
|
350
|
+
clearTimeout(timer);
|
|
351
|
+
keepaliveByConversation.delete(key);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function writeStatus(source, footerText, loadingText, options = {}) {
|
|
355
|
+
const stateKey = getConversationKey(source);
|
|
356
|
+
const replyKey = getLifecycleReplyKey(source);
|
|
357
|
+
const threadTs = resolveSlackProgressThreadTs(source);
|
|
358
|
+
if (!stateKey || !replyKey || !threadTs)
|
|
359
|
+
return false;
|
|
360
|
+
const signature = `${footerText}
|
|
361
|
+
${loadingText}`;
|
|
362
|
+
if (!options.force && signatureByConversation.get(stateKey) === signature) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
await params.ensureApp();
|
|
366
|
+
const slackClient = await params.ensureWriteClient();
|
|
367
|
+
const setStatus = slackClient.assistant?.threads?.setStatus;
|
|
368
|
+
if (!setStatus)
|
|
369
|
+
return false;
|
|
370
|
+
signatureByConversation.set(stateKey, signature);
|
|
371
|
+
const previous = writePromiseByConversation.get(stateKey) ?? Promise.resolve();
|
|
372
|
+
const operation = previous.then(async () => {
|
|
373
|
+
try {
|
|
374
|
+
await setStatus.call(slackClient.assistant?.threads, {
|
|
375
|
+
channel_id: source.chatId,
|
|
376
|
+
thread_ts: threadTs,
|
|
377
|
+
status: footerText,
|
|
378
|
+
...footerText ? { loading_messages: [loadingText] } : {}
|
|
379
|
+
});
|
|
380
|
+
if (footerText)
|
|
381
|
+
clearedStaleReplyKeys.delete(replyKey);
|
|
382
|
+
else
|
|
383
|
+
clearedStaleReplyKeys.add(replyKey);
|
|
384
|
+
return true;
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (signatureByConversation.get(stateKey) === signature) {
|
|
387
|
+
signatureByConversation.delete(stateKey);
|
|
388
|
+
}
|
|
389
|
+
console.warn("[Slack] Failed to update assistant thread status:", error instanceof Error ? error.message : error);
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
const settled = operation.then(() => {
|
|
394
|
+
return;
|
|
395
|
+
});
|
|
396
|
+
writePromiseByConversation.set(stateKey, settled);
|
|
397
|
+
settled.then(() => {
|
|
398
|
+
if (writePromiseByConversation.get(stateKey) === settled) {
|
|
399
|
+
writePromiseByConversation.delete(stateKey);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
return operation;
|
|
403
|
+
}
|
|
404
|
+
function scheduleKeepalive(key) {
|
|
405
|
+
clearKeepalive(key);
|
|
406
|
+
const timer = setTimeout(() => {
|
|
407
|
+
keepaliveByConversation.delete(key);
|
|
408
|
+
(async () => {
|
|
409
|
+
const state = stateByConversation.get(key);
|
|
410
|
+
const source = sourceByConversation.get(key);
|
|
411
|
+
if (!state?.isThinkingActive || !source)
|
|
412
|
+
return;
|
|
413
|
+
await writeStatus(source, state.typingFooterText, state.thinkingText, {
|
|
414
|
+
force: true
|
|
415
|
+
});
|
|
416
|
+
if (state.isThinkingActive)
|
|
417
|
+
scheduleKeepalive(key);
|
|
418
|
+
})();
|
|
419
|
+
}, SLACK_ASSISTANT_STATUS_KEEPALIVE_MS);
|
|
420
|
+
timer.unref?.();
|
|
421
|
+
keepaliveByConversation.set(key, timer);
|
|
422
|
+
}
|
|
423
|
+
async function activate(source, footerText, loadingText) {
|
|
424
|
+
const key = getConversationKey(source);
|
|
425
|
+
if (!key || !getLifecycleReplyKey(source))
|
|
426
|
+
return;
|
|
427
|
+
const state = stateByConversation.get(key) ?? {
|
|
428
|
+
isThinkingActive: false,
|
|
429
|
+
thinkingText: "",
|
|
430
|
+
typingFooterText: ""
|
|
431
|
+
};
|
|
432
|
+
if (state.isThinkingActive && state.thinkingText === loadingText && state.typingFooterText === footerText) {
|
|
433
|
+
sourceByConversation.set(key, source);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
state.isThinkingActive = true;
|
|
437
|
+
state.thinkingText = loadingText;
|
|
438
|
+
state.typingFooterText = footerText;
|
|
439
|
+
stateByConversation.set(key, state);
|
|
440
|
+
sourceByConversation.set(key, source);
|
|
441
|
+
const sent = await writeStatus(source, footerText, loadingText);
|
|
442
|
+
if (sent && state.isThinkingActive)
|
|
443
|
+
scheduleKeepalive(key);
|
|
444
|
+
else if (!sent)
|
|
445
|
+
state.isThinkingActive = false;
|
|
446
|
+
}
|
|
447
|
+
function markAutoClearedByKey(key) {
|
|
448
|
+
clearKeepalive(key);
|
|
449
|
+
const state = stateByConversation.get(key);
|
|
450
|
+
if (state)
|
|
451
|
+
state.isThinkingActive = false;
|
|
452
|
+
signatureByConversation.delete(key);
|
|
453
|
+
sourceByConversation.delete(key);
|
|
454
|
+
}
|
|
455
|
+
async function deactivate(source) {
|
|
456
|
+
const key = getConversationKey(source);
|
|
457
|
+
if (!key)
|
|
458
|
+
return;
|
|
459
|
+
clearKeepalive(key);
|
|
460
|
+
const state = stateByConversation.get(key);
|
|
461
|
+
if (state)
|
|
462
|
+
state.isThinkingActive = false;
|
|
463
|
+
signatureByConversation.delete(key);
|
|
464
|
+
sourceByConversation.delete(key);
|
|
465
|
+
await writeStatus(source, "", "", { force: true });
|
|
466
|
+
signatureByConversation.delete(key);
|
|
467
|
+
stateByConversation.delete(key);
|
|
468
|
+
}
|
|
469
|
+
async function clearStale(source) {
|
|
470
|
+
const key = getConversationKey(source);
|
|
471
|
+
const replyKey = getLifecycleReplyKey(source);
|
|
472
|
+
if (!key || !replyKey || stateByConversation.get(key)?.isThinkingActive || clearedStaleReplyKeys.has(replyKey)) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
await writeStatus(source, "", "", { force: true });
|
|
476
|
+
signatureByConversation.delete(key);
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
getUniqueSources,
|
|
480
|
+
getLifecycleErrorReplyKey,
|
|
481
|
+
activate,
|
|
482
|
+
deactivate,
|
|
483
|
+
clearStale,
|
|
484
|
+
markAutoCleared(source) {
|
|
485
|
+
const key = getConversationKey(source);
|
|
486
|
+
if (key)
|
|
487
|
+
markAutoClearedByKey(key);
|
|
488
|
+
},
|
|
489
|
+
markAutoClearedForMessage(msg) {
|
|
490
|
+
if (isNonEmptyString(msg.agentId) && isNonEmptyString(msg.conversationId)) {
|
|
491
|
+
markAutoClearedByKey(`${msg.agentId}:${msg.conversationId}`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const anchor = firstNonEmptyString(msg.threadId, msg.replyToMessageId);
|
|
495
|
+
if (!anchor)
|
|
496
|
+
return;
|
|
497
|
+
const root = params.resolveKnownThreadRoot(anchor);
|
|
498
|
+
for (const [key, source] of sourceByConversation) {
|
|
499
|
+
if (source.chatId === msg.chatId && resolveSlackProgressThreadTs(source) === root) {
|
|
500
|
+
markAutoClearedByKey(key);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
activeSources: () => Array.from(sourceByConversation.values()),
|
|
505
|
+
clear() {
|
|
506
|
+
for (const timer of keepaliveByConversation.values())
|
|
507
|
+
clearTimeout(timer);
|
|
508
|
+
stateByConversation.clear();
|
|
509
|
+
sourceByConversation.clear();
|
|
510
|
+
signatureByConversation.clear();
|
|
511
|
+
writePromiseByConversation.clear();
|
|
512
|
+
keepaliveByConversation.clear();
|
|
513
|
+
clearedStaleReplyKeys.clear();
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
export {
|
|
518
|
+
slackTimestampToMillis,
|
|
519
|
+
shouldSkipSlackMessageByLastSeen,
|
|
520
|
+
resolveSlackOutboundThreadTs,
|
|
521
|
+
resolveSlackMessageIngressPolicy,
|
|
522
|
+
resolveSlackConcreteActivity,
|
|
523
|
+
resolveSlackChatType,
|
|
524
|
+
resolveSlackAppMentionIngressPolicy,
|
|
525
|
+
normalizeSlackText,
|
|
526
|
+
normalizeSlackReactionName,
|
|
527
|
+
isProcessableSlackInboundMessage,
|
|
528
|
+
createSlackStatusController,
|
|
529
|
+
createSlackChannelSender,
|
|
530
|
+
SLACK_ASSISTANT_WORKING_STATUS,
|
|
531
|
+
SLACK_ASSISTANT_STARTUP_STATUS
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
//# debugId=29806D7E9D547F5664756E2164756E21
|