@sentry/junior 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -2
- package/dist/bot-6KXJ366H.js +16 -0
- package/dist/{chunk-7E56WM6K.js → chunk-5LLCJPTH.js} +856 -49
- package/dist/{bot-DLML4Z7F.js → chunk-CJFEZLEN.js} +59 -31
- package/dist/{chunk-OD6TOSY4.js → chunk-OVG2HBNM.js} +1 -1
- package/dist/handlers/queue-callback.js +6 -8
- package/dist/handlers/router.js +2 -2
- package/dist/handlers/webhooks.js +1 -1
- package/dist/{route-XLYK6CKP.js → route-DMVINKJW.js} +4 -9
- package/package.json +3 -3
- package/dist/channel-HJO33DGJ.js +0 -18
- package/dist/chunk-GDNDYMGX.js +0 -333
- package/dist/chunk-MM3YNA4F.js +0 -203
- package/dist/chunk-ZA2IDPVG.js +0 -39
- package/dist/chunk-ZBFSIN6G.js +0 -323
- package/dist/client-3GAEMIQ3.js +0 -10
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getAgentTurnSessionCheckpoint,
|
|
3
|
-
getStateAdapter,
|
|
4
|
-
upsertAgentTurnSessionCheckpoint
|
|
5
|
-
} from "./chunk-ZBFSIN6G.js";
|
|
6
|
-
import {
|
|
7
|
-
addReactionToMessage,
|
|
8
|
-
listChannelMembers,
|
|
9
|
-
listChannelMessages,
|
|
10
|
-
postMessageToChannel
|
|
11
|
-
} from "./chunk-MM3YNA4F.js";
|
|
12
|
-
import {
|
|
13
|
-
SlackActionError,
|
|
14
|
-
botConfig,
|
|
15
|
-
getFilePermalink,
|
|
16
|
-
getSlackClient,
|
|
17
|
-
isConversationChannel,
|
|
18
|
-
isConversationScopedChannel,
|
|
19
|
-
isDmChannel,
|
|
20
|
-
normalizeSlackConversationId,
|
|
21
|
-
withSlackRetries
|
|
22
|
-
} from "./chunk-GDNDYMGX.js";
|
|
23
1
|
import {
|
|
24
2
|
logError,
|
|
25
3
|
logException,
|
|
@@ -31,6 +9,370 @@ import {
|
|
|
31
9
|
withSpan
|
|
32
10
|
} from "./chunk-BBOVH5RF.js";
|
|
33
11
|
|
|
12
|
+
// src/chat/config.ts
|
|
13
|
+
var MIN_AGENT_TURN_TIMEOUT_MS = 10 * 1e3;
|
|
14
|
+
var DEFAULT_AGENT_TURN_TIMEOUT_MS = 12 * 60 * 1e3;
|
|
15
|
+
var DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS = 800;
|
|
16
|
+
var TURN_TIMEOUT_BUFFER_SECONDS = 20;
|
|
17
|
+
function parseAgentTurnTimeoutMs(rawValue, maxTimeoutMs) {
|
|
18
|
+
const value = Number.parseInt(rawValue ?? "", 10);
|
|
19
|
+
if (Number.isNaN(value)) {
|
|
20
|
+
return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(DEFAULT_AGENT_TURN_TIMEOUT_MS, maxTimeoutMs));
|
|
21
|
+
}
|
|
22
|
+
return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(value, maxTimeoutMs));
|
|
23
|
+
}
|
|
24
|
+
function resolveQueueCallbackMaxDurationSeconds() {
|
|
25
|
+
const value = Number.parseInt(process.env.QUEUE_CALLBACK_MAX_DURATION_SECONDS ?? "", 10);
|
|
26
|
+
if (Number.isNaN(value) || value <= 0) {
|
|
27
|
+
return DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds) {
|
|
32
|
+
const budgetSeconds = queueCallbackMaxDurationSeconds - TURN_TIMEOUT_BUFFER_SECONDS;
|
|
33
|
+
return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, budgetSeconds * 1e3);
|
|
34
|
+
}
|
|
35
|
+
function buildBotConfig() {
|
|
36
|
+
const queueCallbackMaxDurationSeconds = resolveQueueCallbackMaxDurationSeconds();
|
|
37
|
+
const maxTurnTimeoutMs = resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds);
|
|
38
|
+
return {
|
|
39
|
+
userName: process.env.JUNIOR_BOT_NAME ?? "junior",
|
|
40
|
+
modelId: process.env.AI_MODEL ?? "anthropic/claude-sonnet-4.6",
|
|
41
|
+
fastModelId: process.env.AI_FAST_MODEL ?? process.env.AI_MODEL ?? "anthropic/claude-haiku-4.5",
|
|
42
|
+
turnTimeoutMs: parseAgentTurnTimeoutMs(process.env.AGENT_TURN_TIMEOUT_MS, maxTurnTimeoutMs)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
var botConfig = buildBotConfig();
|
|
46
|
+
function toOptionalTrimmed(value) {
|
|
47
|
+
if (!value) {
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
const trimmed = value.trim();
|
|
51
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
52
|
+
}
|
|
53
|
+
function getSlackBotToken() {
|
|
54
|
+
return toOptionalTrimmed(process.env.SLACK_BOT_TOKEN) ?? toOptionalTrimmed(process.env.SLACK_BOT_USER_TOKEN);
|
|
55
|
+
}
|
|
56
|
+
function getSlackSigningSecret() {
|
|
57
|
+
return toOptionalTrimmed(process.env.SLACK_SIGNING_SECRET);
|
|
58
|
+
}
|
|
59
|
+
function getSlackClientId() {
|
|
60
|
+
return toOptionalTrimmed(process.env.SLACK_CLIENT_ID);
|
|
61
|
+
}
|
|
62
|
+
function getSlackClientSecret() {
|
|
63
|
+
return toOptionalTrimmed(process.env.SLACK_CLIENT_SECRET);
|
|
64
|
+
}
|
|
65
|
+
function hasRedisConfig() {
|
|
66
|
+
return Boolean(process.env.REDIS_URL);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/chat/state.ts
|
|
70
|
+
import { createRedisState } from "@chat-adapter/state-redis";
|
|
71
|
+
import { createMemoryState } from "@chat-adapter/state-memory";
|
|
72
|
+
var MIN_LOCK_TTL_MS = 1e3 * 60 * 5;
|
|
73
|
+
var QUEUE_INGRESS_DEDUP_PREFIX = "junior:queue_ingress";
|
|
74
|
+
var QUEUE_MESSAGE_PROCESSING_PREFIX = "junior:queue_message";
|
|
75
|
+
var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
|
|
76
|
+
var QUEUE_MESSAGE_PROCESSING_TTL_MS = 30 * 60 * 1e3;
|
|
77
|
+
var QUEUE_MESSAGE_COMPLETED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
78
|
+
var QUEUE_MESSAGE_FAILED_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
79
|
+
var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
80
|
+
var CLAIM_OR_RECLAIM_PROCESSING_SCRIPT = `
|
|
81
|
+
local key = KEYS[1]
|
|
82
|
+
local nowMs = tonumber(ARGV[1])
|
|
83
|
+
local ttlMs = tonumber(ARGV[2])
|
|
84
|
+
local payload = ARGV[3]
|
|
85
|
+
local current = redis.call("get", key)
|
|
86
|
+
|
|
87
|
+
if not current then
|
|
88
|
+
redis.call("set", key, payload, "PX", ttlMs)
|
|
89
|
+
return 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
local ok, parsed = pcall(cjson.decode, current)
|
|
93
|
+
if not ok or type(parsed) ~= "table" then
|
|
94
|
+
return 0
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
local status = parsed["status"]
|
|
98
|
+
if status == "failed" then
|
|
99
|
+
redis.call("set", key, payload, "PX", ttlMs)
|
|
100
|
+
return 3
|
|
101
|
+
end
|
|
102
|
+
if status ~= "processing" then
|
|
103
|
+
return 0
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
local updatedAtMs = tonumber(parsed["updatedAtMs"])
|
|
107
|
+
if not updatedAtMs then
|
|
108
|
+
return 0
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if updatedAtMs + ttlMs < nowMs then
|
|
112
|
+
redis.call("set", key, payload, "PX", ttlMs)
|
|
113
|
+
return 2
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
return 0
|
|
117
|
+
`;
|
|
118
|
+
var UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT = `
|
|
119
|
+
local key = KEYS[1]
|
|
120
|
+
local ownerToken = ARGV[1]
|
|
121
|
+
local ttlMs = tonumber(ARGV[2])
|
|
122
|
+
local payload = ARGV[3]
|
|
123
|
+
local current = redis.call("get", key)
|
|
124
|
+
|
|
125
|
+
if not current then
|
|
126
|
+
return 0
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
local ok, parsed = pcall(cjson.decode, current)
|
|
130
|
+
if not ok or type(parsed) ~= "table" then
|
|
131
|
+
return 0
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
local currentOwner = parsed["ownerToken"]
|
|
135
|
+
local status = parsed["status"]
|
|
136
|
+
if currentOwner ~= ownerToken then
|
|
137
|
+
return 0
|
|
138
|
+
end
|
|
139
|
+
if status ~= "processing" then
|
|
140
|
+
return 0
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
redis.call("set", key, payload, "PX", ttlMs)
|
|
144
|
+
return 1
|
|
145
|
+
`;
|
|
146
|
+
function createQueuedStateAdapter(base) {
|
|
147
|
+
const acquireLock = async (threadId, ttlMs) => {
|
|
148
|
+
const effectiveTtlMs = Math.max(ttlMs, MIN_LOCK_TTL_MS);
|
|
149
|
+
const lock = await base.acquireLock(threadId, effectiveTtlMs);
|
|
150
|
+
return lock;
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
connect: () => base.connect(),
|
|
154
|
+
disconnect: () => base.disconnect(),
|
|
155
|
+
subscribe: (threadId) => base.subscribe(threadId),
|
|
156
|
+
unsubscribe: (threadId) => base.unsubscribe(threadId),
|
|
157
|
+
isSubscribed: (threadId) => base.isSubscribed(threadId),
|
|
158
|
+
acquireLock,
|
|
159
|
+
releaseLock: (lock) => base.releaseLock(lock),
|
|
160
|
+
extendLock: (lock, ttlMs) => base.extendLock(lock, Math.max(ttlMs, MIN_LOCK_TTL_MS)),
|
|
161
|
+
get: (key) => base.get(key),
|
|
162
|
+
set: (key, value, ttlMs) => base.set(key, value, ttlMs),
|
|
163
|
+
setIfNotExists: (key, value, ttlMs) => base.setIfNotExists(key, value, ttlMs),
|
|
164
|
+
delete: (key) => base.delete(key)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function createStateAdapter() {
|
|
168
|
+
if (process.env.JUNIOR_STATE_ADAPTER?.trim().toLowerCase() === "memory") {
|
|
169
|
+
_redisStateAdapter = void 0;
|
|
170
|
+
return createQueuedStateAdapter(createMemoryState());
|
|
171
|
+
}
|
|
172
|
+
if (!hasRedisConfig()) {
|
|
173
|
+
throw new Error("REDIS_URL is required for durable Slack thread state");
|
|
174
|
+
}
|
|
175
|
+
const redisState = createRedisState({
|
|
176
|
+
url: process.env.REDIS_URL
|
|
177
|
+
});
|
|
178
|
+
_redisStateAdapter = redisState;
|
|
179
|
+
return createQueuedStateAdapter(redisState);
|
|
180
|
+
}
|
|
181
|
+
var _stateAdapter;
|
|
182
|
+
var _redisStateAdapter;
|
|
183
|
+
function getRedisStateAdapter() {
|
|
184
|
+
if (!_redisStateAdapter) {
|
|
185
|
+
getStateAdapter();
|
|
186
|
+
}
|
|
187
|
+
if (!_redisStateAdapter) {
|
|
188
|
+
throw new Error("Redis state adapter is unavailable for this runtime");
|
|
189
|
+
}
|
|
190
|
+
return _redisStateAdapter;
|
|
191
|
+
}
|
|
192
|
+
function queueMessageKey(rawKey) {
|
|
193
|
+
return `${QUEUE_MESSAGE_PROCESSING_PREFIX}:${rawKey}`;
|
|
194
|
+
}
|
|
195
|
+
function parseQueueMessageState(value) {
|
|
196
|
+
if (typeof value !== "string") {
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(value);
|
|
201
|
+
if (!parsed || parsed.status !== "processing" && parsed.status !== "completed" && parsed.status !== "failed" || typeof parsed.updatedAtMs !== "number") {
|
|
202
|
+
return void 0;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
status: parsed.status,
|
|
206
|
+
updatedAtMs: parsed.updatedAtMs,
|
|
207
|
+
...typeof parsed.ownerToken === "string" ? { ownerToken: parsed.ownerToken } : {},
|
|
208
|
+
...typeof parsed.queueMessageId === "string" ? { queueMessageId: parsed.queueMessageId } : {},
|
|
209
|
+
...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {}
|
|
210
|
+
};
|
|
211
|
+
} catch {
|
|
212
|
+
return void 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function agentTurnSessionKey(conversationId, sessionId) {
|
|
216
|
+
return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
|
|
217
|
+
}
|
|
218
|
+
function isRecord(value) {
|
|
219
|
+
return typeof value === "object" && value !== null;
|
|
220
|
+
}
|
|
221
|
+
function parseAgentTurnSessionCheckpoint(value) {
|
|
222
|
+
if (typeof value !== "string") {
|
|
223
|
+
return void 0;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(value);
|
|
227
|
+
if (!isRecord(parsed)) {
|
|
228
|
+
return void 0;
|
|
229
|
+
}
|
|
230
|
+
const status = parsed.state;
|
|
231
|
+
if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed") {
|
|
232
|
+
return void 0;
|
|
233
|
+
}
|
|
234
|
+
const conversationId = parsed.conversationId;
|
|
235
|
+
const sessionId = parsed.sessionId;
|
|
236
|
+
const sliceId = parsed.sliceId;
|
|
237
|
+
const checkpointVersion = parsed.checkpointVersion;
|
|
238
|
+
const updatedAtMs = parsed.updatedAtMs;
|
|
239
|
+
if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
|
|
240
|
+
return void 0;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
checkpointVersion,
|
|
244
|
+
conversationId,
|
|
245
|
+
sessionId,
|
|
246
|
+
sliceId,
|
|
247
|
+
state: status,
|
|
248
|
+
updatedAtMs,
|
|
249
|
+
piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
|
|
250
|
+
...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
|
|
251
|
+
...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
|
|
252
|
+
};
|
|
253
|
+
} catch {
|
|
254
|
+
return void 0;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function getStateAdapter() {
|
|
258
|
+
if (!_stateAdapter) {
|
|
259
|
+
_stateAdapter = createStateAdapter();
|
|
260
|
+
}
|
|
261
|
+
return _stateAdapter;
|
|
262
|
+
}
|
|
263
|
+
async function claimQueueIngressDedup(rawKey, ttlMs) {
|
|
264
|
+
await getStateAdapter().connect();
|
|
265
|
+
const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
|
|
266
|
+
const result = await getRedisStateAdapter().getClient().set(key, "1", {
|
|
267
|
+
NX: true,
|
|
268
|
+
PX: ttlMs
|
|
269
|
+
});
|
|
270
|
+
return result === "OK";
|
|
271
|
+
}
|
|
272
|
+
async function hasQueueIngressDedup(rawKey) {
|
|
273
|
+
await getStateAdapter().connect();
|
|
274
|
+
const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
|
|
275
|
+
const value = await getRedisStateAdapter().getClient().get(key);
|
|
276
|
+
return typeof value === "string" && value.length > 0;
|
|
277
|
+
}
|
|
278
|
+
async function getQueueMessageProcessingState(rawKey) {
|
|
279
|
+
await getStateAdapter().connect();
|
|
280
|
+
const state = await getStateAdapter().get(queueMessageKey(rawKey));
|
|
281
|
+
return parseQueueMessageState(state);
|
|
282
|
+
}
|
|
283
|
+
async function acquireQueueMessageProcessingOwnership(args) {
|
|
284
|
+
await getStateAdapter().connect();
|
|
285
|
+
const key = queueMessageKey(args.rawKey);
|
|
286
|
+
const nowMs = Date.now();
|
|
287
|
+
const payload = JSON.stringify({
|
|
288
|
+
status: "processing",
|
|
289
|
+
updatedAtMs: nowMs,
|
|
290
|
+
ownerToken: args.ownerToken,
|
|
291
|
+
...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
|
|
292
|
+
});
|
|
293
|
+
const result = await getRedisStateAdapter().getClient().eval(CLAIM_OR_RECLAIM_PROCESSING_SCRIPT, {
|
|
294
|
+
keys: [key],
|
|
295
|
+
arguments: [String(nowMs), String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
|
|
296
|
+
});
|
|
297
|
+
if (result === 1) {
|
|
298
|
+
return "acquired";
|
|
299
|
+
}
|
|
300
|
+
if (result === 2) {
|
|
301
|
+
return "reclaimed";
|
|
302
|
+
}
|
|
303
|
+
if (result === 3) {
|
|
304
|
+
return "recovered";
|
|
305
|
+
}
|
|
306
|
+
return "blocked";
|
|
307
|
+
}
|
|
308
|
+
async function refreshQueueMessageProcessingOwnership(args) {
|
|
309
|
+
await getStateAdapter().connect();
|
|
310
|
+
const nowMs = Date.now();
|
|
311
|
+
const payload = JSON.stringify({
|
|
312
|
+
status: "processing",
|
|
313
|
+
updatedAtMs: nowMs,
|
|
314
|
+
ownerToken: args.ownerToken,
|
|
315
|
+
...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
|
|
316
|
+
});
|
|
317
|
+
const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
|
|
318
|
+
keys: [queueMessageKey(args.rawKey)],
|
|
319
|
+
arguments: [args.ownerToken, String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
|
|
320
|
+
});
|
|
321
|
+
return result === 1;
|
|
322
|
+
}
|
|
323
|
+
async function completeQueueMessageProcessingOwnership(args) {
|
|
324
|
+
await getStateAdapter().connect();
|
|
325
|
+
const payload = JSON.stringify({
|
|
326
|
+
status: "completed",
|
|
327
|
+
updatedAtMs: Date.now(),
|
|
328
|
+
ownerToken: args.ownerToken,
|
|
329
|
+
...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
|
|
330
|
+
});
|
|
331
|
+
const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
|
|
332
|
+
keys: [queueMessageKey(args.rawKey)],
|
|
333
|
+
arguments: [args.ownerToken, String(QUEUE_MESSAGE_COMPLETED_TTL_MS), payload]
|
|
334
|
+
});
|
|
335
|
+
return result === 1;
|
|
336
|
+
}
|
|
337
|
+
async function failQueueMessageProcessingOwnership(args) {
|
|
338
|
+
await getStateAdapter().connect();
|
|
339
|
+
const payload = JSON.stringify({
|
|
340
|
+
status: "failed",
|
|
341
|
+
updatedAtMs: Date.now(),
|
|
342
|
+
ownerToken: args.ownerToken,
|
|
343
|
+
errorMessage: args.errorMessage,
|
|
344
|
+
...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
|
|
345
|
+
});
|
|
346
|
+
const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
|
|
347
|
+
keys: [queueMessageKey(args.rawKey)],
|
|
348
|
+
arguments: [args.ownerToken, String(QUEUE_MESSAGE_FAILED_TTL_MS), payload]
|
|
349
|
+
});
|
|
350
|
+
return result === 1;
|
|
351
|
+
}
|
|
352
|
+
async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
|
|
353
|
+
await getStateAdapter().connect();
|
|
354
|
+
const value = await getStateAdapter().get(agentTurnSessionKey(conversationId, sessionId));
|
|
355
|
+
return parseAgentTurnSessionCheckpoint(value);
|
|
356
|
+
}
|
|
357
|
+
async function upsertAgentTurnSessionCheckpoint(args) {
|
|
358
|
+
await getStateAdapter().connect();
|
|
359
|
+
const existing = await getAgentTurnSessionCheckpoint(args.conversationId, args.sessionId);
|
|
360
|
+
const checkpoint = {
|
|
361
|
+
checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
|
|
362
|
+
conversationId: args.conversationId,
|
|
363
|
+
sessionId: args.sessionId,
|
|
364
|
+
sliceId: args.sliceId,
|
|
365
|
+
state: args.state,
|
|
366
|
+
updatedAtMs: Date.now(),
|
|
367
|
+
piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
|
|
368
|
+
...args.errorMessage ? { errorMessage: args.errorMessage } : {},
|
|
369
|
+
...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
|
|
370
|
+
};
|
|
371
|
+
const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
|
|
372
|
+
await getStateAdapter().set(agentTurnSessionKey(args.conversationId, args.sessionId), JSON.stringify(checkpoint), ttlMs);
|
|
373
|
+
return checkpoint;
|
|
374
|
+
}
|
|
375
|
+
|
|
34
376
|
// src/chat/plugins/registry.ts
|
|
35
377
|
import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
36
378
|
import path3 from "path";
|
|
@@ -1314,13 +1656,268 @@ function createSkillCapabilityRuntime(options = {}) {
|
|
|
1314
1656
|
});
|
|
1315
1657
|
}
|
|
1316
1658
|
|
|
1659
|
+
// src/chat/slack-actions/client.ts
|
|
1660
|
+
import { WebClient } from "@slack/web-api";
|
|
1661
|
+
var SlackActionError = class extends Error {
|
|
1662
|
+
code;
|
|
1663
|
+
apiError;
|
|
1664
|
+
needed;
|
|
1665
|
+
provided;
|
|
1666
|
+
statusCode;
|
|
1667
|
+
requestId;
|
|
1668
|
+
errorData;
|
|
1669
|
+
retryAfterSeconds;
|
|
1670
|
+
detail;
|
|
1671
|
+
detailLine;
|
|
1672
|
+
detailRule;
|
|
1673
|
+
constructor(message, code, options = {}) {
|
|
1674
|
+
super(message);
|
|
1675
|
+
this.name = "SlackActionError";
|
|
1676
|
+
this.code = code;
|
|
1677
|
+
this.apiError = options.apiError;
|
|
1678
|
+
this.needed = options.needed;
|
|
1679
|
+
this.provided = options.provided;
|
|
1680
|
+
this.statusCode = options.statusCode;
|
|
1681
|
+
this.requestId = options.requestId;
|
|
1682
|
+
this.errorData = options.errorData;
|
|
1683
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
1684
|
+
this.detail = options.detail;
|
|
1685
|
+
this.detailLine = options.detailLine;
|
|
1686
|
+
this.detailRule = options.detailRule;
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
function serializeSlackErrorData(data) {
|
|
1690
|
+
if (!data || typeof data !== "object") {
|
|
1691
|
+
return void 0;
|
|
1692
|
+
}
|
|
1693
|
+
const filtered = Object.fromEntries(
|
|
1694
|
+
Object.entries(data).filter(([key]) => key !== "error")
|
|
1695
|
+
);
|
|
1696
|
+
if (Object.keys(filtered).length === 0) {
|
|
1697
|
+
return void 0;
|
|
1698
|
+
}
|
|
1699
|
+
try {
|
|
1700
|
+
const serialized = JSON.stringify(filtered);
|
|
1701
|
+
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
1702
|
+
} catch {
|
|
1703
|
+
return void 0;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function getHeaderString(headers, name) {
|
|
1707
|
+
if (!headers || typeof headers !== "object") {
|
|
1708
|
+
return void 0;
|
|
1709
|
+
}
|
|
1710
|
+
const key = name.toLowerCase();
|
|
1711
|
+
const entries = headers;
|
|
1712
|
+
for (const [entryKey, value] of Object.entries(entries)) {
|
|
1713
|
+
if (entryKey.toLowerCase() !== key) continue;
|
|
1714
|
+
if (typeof value === "string") return value;
|
|
1715
|
+
if (Array.isArray(value)) {
|
|
1716
|
+
const first = value.find((entry) => typeof entry === "string");
|
|
1717
|
+
return typeof first === "string" ? first : void 0;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
return void 0;
|
|
1721
|
+
}
|
|
1722
|
+
function parseSlackCanvasDetail(detail) {
|
|
1723
|
+
if (typeof detail !== "string") {
|
|
1724
|
+
return {};
|
|
1725
|
+
}
|
|
1726
|
+
const trimmed = detail.trim();
|
|
1727
|
+
if (!trimmed) {
|
|
1728
|
+
return {};
|
|
1729
|
+
}
|
|
1730
|
+
const parsed = {
|
|
1731
|
+
detail: trimmed
|
|
1732
|
+
};
|
|
1733
|
+
const lineMatch = trimmed.match(/line\s+(\d+):/i);
|
|
1734
|
+
if (lineMatch) {
|
|
1735
|
+
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
1736
|
+
if (Number.isFinite(line)) {
|
|
1737
|
+
parsed.detailLine = line;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (/unsupported heading depth/i.test(trimmed)) {
|
|
1741
|
+
parsed.detailRule = "unsupported_heading_depth";
|
|
1742
|
+
}
|
|
1743
|
+
return parsed;
|
|
1744
|
+
}
|
|
1745
|
+
var client = null;
|
|
1746
|
+
function normalizeSlackConversationId(channelId) {
|
|
1747
|
+
if (!channelId) return void 0;
|
|
1748
|
+
const trimmed = channelId.trim();
|
|
1749
|
+
if (!trimmed) return void 0;
|
|
1750
|
+
if (!trimmed.startsWith("slack:")) {
|
|
1751
|
+
return trimmed;
|
|
1752
|
+
}
|
|
1753
|
+
const parts = trimmed.split(":");
|
|
1754
|
+
return parts[1]?.trim() || void 0;
|
|
1755
|
+
}
|
|
1756
|
+
function getClient() {
|
|
1757
|
+
if (client) return client;
|
|
1758
|
+
const token = getSlackBotToken();
|
|
1759
|
+
if (!token) {
|
|
1760
|
+
throw new SlackActionError(
|
|
1761
|
+
"SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack canvas/list actions in this service",
|
|
1762
|
+
"missing_token"
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
client = new WebClient(token);
|
|
1766
|
+
return client;
|
|
1767
|
+
}
|
|
1768
|
+
function mapSlackError(error) {
|
|
1769
|
+
if (error instanceof SlackActionError) {
|
|
1770
|
+
return error;
|
|
1771
|
+
}
|
|
1772
|
+
const candidate = error;
|
|
1773
|
+
const apiError = candidate.data?.error;
|
|
1774
|
+
const message = candidate.message ?? "Slack action failed";
|
|
1775
|
+
const baseOptions = {
|
|
1776
|
+
apiError,
|
|
1777
|
+
statusCode: candidate.statusCode,
|
|
1778
|
+
requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
|
|
1779
|
+
errorData: serializeSlackErrorData(candidate.data),
|
|
1780
|
+
...parseSlackCanvasDetail(candidate.data?.detail)
|
|
1781
|
+
};
|
|
1782
|
+
if (apiError === "missing_scope") {
|
|
1783
|
+
return new SlackActionError(message, "missing_scope", {
|
|
1784
|
+
...baseOptions,
|
|
1785
|
+
needed: candidate.data?.needed,
|
|
1786
|
+
provided: candidate.data?.provided
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
if (apiError === "not_in_channel") {
|
|
1790
|
+
return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
1791
|
+
}
|
|
1792
|
+
if (apiError === "invalid_arguments") {
|
|
1793
|
+
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
1794
|
+
}
|
|
1795
|
+
if (apiError === "invalid_name") {
|
|
1796
|
+
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
1797
|
+
}
|
|
1798
|
+
if (apiError === "not_found") {
|
|
1799
|
+
return new SlackActionError(message, "not_found", baseOptions);
|
|
1800
|
+
}
|
|
1801
|
+
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
|
|
1802
|
+
return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
1803
|
+
}
|
|
1804
|
+
if (apiError === "canvas_creation_failed") {
|
|
1805
|
+
return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
1806
|
+
}
|
|
1807
|
+
if (apiError === "canvas_editing_failed") {
|
|
1808
|
+
return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
1809
|
+
}
|
|
1810
|
+
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
|
|
1811
|
+
return new SlackActionError(message, "rate_limited", {
|
|
1812
|
+
...baseOptions,
|
|
1813
|
+
retryAfterSeconds: candidate.retryAfter
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
return new SlackActionError(message, "internal_error", baseOptions);
|
|
1817
|
+
}
|
|
1818
|
+
function sleep(ms) {
|
|
1819
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1820
|
+
}
|
|
1821
|
+
async function withSlackRetries(task, maxAttempts = 3, context = {}) {
|
|
1822
|
+
let attempt = 0;
|
|
1823
|
+
while (attempt < maxAttempts) {
|
|
1824
|
+
attempt += 1;
|
|
1825
|
+
try {
|
|
1826
|
+
return await task();
|
|
1827
|
+
} catch (error) {
|
|
1828
|
+
const mapped = mapSlackError(error);
|
|
1829
|
+
const isRetryable = mapped.code === "rate_limited";
|
|
1830
|
+
const baseLogAttributes = {
|
|
1831
|
+
"app.slack.action": context.action ?? "unknown",
|
|
1832
|
+
"app.slack.error_code": mapped.code,
|
|
1833
|
+
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : {},
|
|
1834
|
+
...mapped.detail ? { "app.slack.detail": mapped.detail } : {},
|
|
1835
|
+
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : {},
|
|
1836
|
+
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : {},
|
|
1837
|
+
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : {},
|
|
1838
|
+
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : {},
|
|
1839
|
+
...context.attributes ?? {}
|
|
1840
|
+
};
|
|
1841
|
+
if (!isRetryable || attempt >= maxAttempts) {
|
|
1842
|
+
logWarn(
|
|
1843
|
+
"slack_action_failed",
|
|
1844
|
+
{},
|
|
1845
|
+
{
|
|
1846
|
+
...baseLogAttributes,
|
|
1847
|
+
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : {}
|
|
1848
|
+
},
|
|
1849
|
+
"Slack action failed"
|
|
1850
|
+
);
|
|
1851
|
+
throw mapped;
|
|
1852
|
+
}
|
|
1853
|
+
logWarn(
|
|
1854
|
+
"slack_action_retrying",
|
|
1855
|
+
{},
|
|
1856
|
+
{
|
|
1857
|
+
...baseLogAttributes,
|
|
1858
|
+
"app.slack.retry_attempt": attempt
|
|
1859
|
+
},
|
|
1860
|
+
"Retrying Slack action after transient failure"
|
|
1861
|
+
);
|
|
1862
|
+
const retryAfterMs = mapped.code === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
1863
|
+
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
1864
|
+
await sleep(retryAfterMs ?? backoffMs);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
throw new SlackActionError("Slack action exhausted retries", "internal_error");
|
|
1868
|
+
}
|
|
1869
|
+
function getSlackClient() {
|
|
1870
|
+
return getClient();
|
|
1871
|
+
}
|
|
1872
|
+
function isDmChannel(channelId) {
|
|
1873
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
1874
|
+
return Boolean(normalized && normalized.startsWith("D"));
|
|
1875
|
+
}
|
|
1876
|
+
function isConversationScopedChannel(channelId) {
|
|
1877
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
1878
|
+
if (!normalized) return false;
|
|
1879
|
+
return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
|
|
1880
|
+
}
|
|
1881
|
+
function isConversationChannel(channelId) {
|
|
1882
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
1883
|
+
if (!normalized) return false;
|
|
1884
|
+
return normalized.startsWith("C") || normalized.startsWith("G");
|
|
1885
|
+
}
|
|
1886
|
+
async function getFilePermalink(fileId) {
|
|
1887
|
+
const client2 = getClient();
|
|
1888
|
+
const response = await withSlackRetries(
|
|
1889
|
+
() => client2.files.info({
|
|
1890
|
+
file: fileId
|
|
1891
|
+
})
|
|
1892
|
+
);
|
|
1893
|
+
return response.file?.permalink;
|
|
1894
|
+
}
|
|
1895
|
+
async function downloadPrivateSlackFile(url) {
|
|
1896
|
+
const token = getSlackBotToken();
|
|
1897
|
+
if (!token) {
|
|
1898
|
+
throw new SlackActionError(
|
|
1899
|
+
"SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack file downloads in this service",
|
|
1900
|
+
"missing_token"
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
const response = await fetch(url, {
|
|
1904
|
+
headers: {
|
|
1905
|
+
Authorization: `Bearer ${token}`
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
if (!response.ok) {
|
|
1909
|
+
throw new Error(`Slack file download failed: ${response.status}`);
|
|
1910
|
+
}
|
|
1911
|
+
return Buffer.from(await response.arrayBuffer());
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1317
1914
|
// src/chat/capabilities/jr-rpc-command.ts
|
|
1318
1915
|
import { randomBytes } from "crypto";
|
|
1319
1916
|
import { Bash, defineCommand } from "just-bash";
|
|
1320
1917
|
async function deliverPrivateMessage(input) {
|
|
1321
|
-
let
|
|
1918
|
+
let client2;
|
|
1322
1919
|
try {
|
|
1323
|
-
|
|
1920
|
+
client2 = getSlackClient();
|
|
1324
1921
|
} catch {
|
|
1325
1922
|
logWarn("oauth_private_delivery_skip", {}, { "app.reason": "missing_bot_token" }, "Skipped private message delivery \u2014 no SLACK_BOT_TOKEN");
|
|
1326
1923
|
return false;
|
|
@@ -1329,13 +1926,13 @@ async function deliverPrivateMessage(input) {
|
|
|
1329
1926
|
const isDm = isDmChannel(input.channelId);
|
|
1330
1927
|
try {
|
|
1331
1928
|
if (isDm) {
|
|
1332
|
-
await
|
|
1929
|
+
await client2.chat.postMessage({
|
|
1333
1930
|
channel: input.channelId,
|
|
1334
1931
|
text: input.text,
|
|
1335
1932
|
...input.threadTs ? { thread_ts: input.threadTs } : {}
|
|
1336
1933
|
});
|
|
1337
1934
|
} else {
|
|
1338
|
-
await
|
|
1935
|
+
await client2.chat.postEphemeral({
|
|
1339
1936
|
channel: input.channelId,
|
|
1340
1937
|
user: input.userId,
|
|
1341
1938
|
text: input.text,
|
|
@@ -1354,13 +1951,13 @@ async function deliverPrivateMessage(input) {
|
|
|
1354
1951
|
}
|
|
1355
1952
|
}
|
|
1356
1953
|
try {
|
|
1357
|
-
const openResult = await
|
|
1954
|
+
const openResult = await client2.conversations.open({ users: input.userId });
|
|
1358
1955
|
const dmChannelId = openResult.channel?.id;
|
|
1359
1956
|
if (!dmChannelId) {
|
|
1360
1957
|
logWarn("oauth_dm_fallback_failed", {}, { "app.reason": "no_dm_channel_id" }, "conversations.open returned no channel ID");
|
|
1361
1958
|
return false;
|
|
1362
1959
|
}
|
|
1363
|
-
await
|
|
1960
|
+
await client2.chat.postMessage({ channel: dmChannelId, text: input.text });
|
|
1364
1961
|
return "fallback_dm";
|
|
1365
1962
|
} catch (error) {
|
|
1366
1963
|
const slackError = error instanceof Error ? error.message : String(error);
|
|
@@ -3052,7 +3649,7 @@ var GATEWAY_PROVIDER = "vercel-ai-gateway";
|
|
|
3052
3649
|
var GEN_AI_PROVIDER_NAME = GATEWAY_PROVIDER;
|
|
3053
3650
|
var GEN_AI_OPERATION_CHAT = "chat";
|
|
3054
3651
|
var MISSING_GATEWAY_CREDENTIALS_ERROR = "Missing AI gateway credentials (AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN)";
|
|
3055
|
-
function
|
|
3652
|
+
function toOptionalTrimmed2(value) {
|
|
3056
3653
|
if (!value) {
|
|
3057
3654
|
return void 0;
|
|
3058
3655
|
}
|
|
@@ -3063,14 +3660,14 @@ function isVercelRuntime() {
|
|
|
3063
3660
|
return process.env.VERCEL === "1" || Boolean(process.env.VERCEL_ENV) || Boolean(process.env.VERCEL_REGION);
|
|
3064
3661
|
}
|
|
3065
3662
|
function getGatewayApiKey() {
|
|
3066
|
-
const explicitApiKey =
|
|
3663
|
+
const explicitApiKey = toOptionalTrimmed2(getEnvApiKey("vercel-ai-gateway"));
|
|
3067
3664
|
if (explicitApiKey) {
|
|
3068
3665
|
return explicitApiKey;
|
|
3069
3666
|
}
|
|
3070
3667
|
if (!isVercelRuntime()) {
|
|
3071
3668
|
return void 0;
|
|
3072
3669
|
}
|
|
3073
|
-
return
|
|
3670
|
+
return toOptionalTrimmed2(process.env.VERCEL_OIDC_TOKEN);
|
|
3074
3671
|
}
|
|
3075
3672
|
function extractText(message) {
|
|
3076
3673
|
return (message.content ?? []).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text ?? "").join("").trim();
|
|
@@ -3488,6 +4085,197 @@ function createReadFileTool() {
|
|
|
3488
4085
|
|
|
3489
4086
|
// src/chat/tools/slack-channel-list-members.ts
|
|
3490
4087
|
import { Type as Type5 } from "@sinclair/typebox";
|
|
4088
|
+
|
|
4089
|
+
// src/chat/slack-actions/channel.ts
|
|
4090
|
+
async function postMessageToChannel(input) {
|
|
4091
|
+
const client2 = getSlackClient();
|
|
4092
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
4093
|
+
if (!channelId) {
|
|
4094
|
+
throw new Error("Slack channel message posting requires a valid channel ID");
|
|
4095
|
+
}
|
|
4096
|
+
const response = await withSlackRetries(
|
|
4097
|
+
() => client2.chat.postMessage({
|
|
4098
|
+
channel: channelId,
|
|
4099
|
+
text: input.text,
|
|
4100
|
+
mrkdwn: true
|
|
4101
|
+
}),
|
|
4102
|
+
3,
|
|
4103
|
+
{ action: "chat.postMessage" }
|
|
4104
|
+
);
|
|
4105
|
+
if (!response.ts) {
|
|
4106
|
+
throw new Error("Slack channel message posted without ts");
|
|
4107
|
+
}
|
|
4108
|
+
let permalink;
|
|
4109
|
+
try {
|
|
4110
|
+
const permalinkResponse = await withSlackRetries(
|
|
4111
|
+
() => client2.chat.getPermalink({
|
|
4112
|
+
channel: channelId,
|
|
4113
|
+
message_ts: response.ts
|
|
4114
|
+
}),
|
|
4115
|
+
3,
|
|
4116
|
+
{ action: "chat.getPermalink" }
|
|
4117
|
+
);
|
|
4118
|
+
permalink = permalinkResponse.permalink;
|
|
4119
|
+
} catch {
|
|
4120
|
+
}
|
|
4121
|
+
return {
|
|
4122
|
+
ts: response.ts,
|
|
4123
|
+
permalink
|
|
4124
|
+
};
|
|
4125
|
+
}
|
|
4126
|
+
async function addReactionToMessage(input) {
|
|
4127
|
+
const client2 = getSlackClient();
|
|
4128
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
4129
|
+
if (!channelId) {
|
|
4130
|
+
throw new Error("Slack reaction requires a valid channel ID");
|
|
4131
|
+
}
|
|
4132
|
+
const timestamp = input.timestamp.trim();
|
|
4133
|
+
if (!timestamp) {
|
|
4134
|
+
throw new Error("Slack reaction requires a target message timestamp");
|
|
4135
|
+
}
|
|
4136
|
+
const emoji = input.emoji.trim().replaceAll(":", "");
|
|
4137
|
+
if (!emoji) {
|
|
4138
|
+
throw new Error("Slack reaction requires a non-empty emoji name");
|
|
4139
|
+
}
|
|
4140
|
+
await withSlackRetries(
|
|
4141
|
+
() => client2.reactions.add({
|
|
4142
|
+
channel: channelId,
|
|
4143
|
+
timestamp,
|
|
4144
|
+
name: emoji
|
|
4145
|
+
}),
|
|
4146
|
+
3,
|
|
4147
|
+
{ action: "reactions.add" }
|
|
4148
|
+
);
|
|
4149
|
+
return { ok: true };
|
|
4150
|
+
}
|
|
4151
|
+
async function removeReactionFromMessage(input) {
|
|
4152
|
+
const client2 = getSlackClient();
|
|
4153
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
4154
|
+
if (!channelId) {
|
|
4155
|
+
throw new Error("Slack reaction requires a valid channel ID");
|
|
4156
|
+
}
|
|
4157
|
+
const timestamp = input.timestamp.trim();
|
|
4158
|
+
if (!timestamp) {
|
|
4159
|
+
throw new Error("Slack reaction requires a target message timestamp");
|
|
4160
|
+
}
|
|
4161
|
+
const emoji = input.emoji.trim().replaceAll(":", "");
|
|
4162
|
+
if (!emoji) {
|
|
4163
|
+
throw new Error("Slack reaction requires a non-empty emoji name");
|
|
4164
|
+
}
|
|
4165
|
+
await withSlackRetries(
|
|
4166
|
+
() => client2.reactions.remove({
|
|
4167
|
+
channel: channelId,
|
|
4168
|
+
timestamp,
|
|
4169
|
+
name: emoji
|
|
4170
|
+
}),
|
|
4171
|
+
3,
|
|
4172
|
+
{ action: "reactions.remove" }
|
|
4173
|
+
);
|
|
4174
|
+
return { ok: true };
|
|
4175
|
+
}
|
|
4176
|
+
async function listChannelMessages(input) {
|
|
4177
|
+
const client2 = getSlackClient();
|
|
4178
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
4179
|
+
if (!channelId) {
|
|
4180
|
+
throw new Error("Slack channel history lookup requires a valid channel ID");
|
|
4181
|
+
}
|
|
4182
|
+
const targetLimit = Math.max(1, Math.min(input.limit, 1e3));
|
|
4183
|
+
const maxPages = Math.max(1, Math.min(input.maxPages ?? 5, 10));
|
|
4184
|
+
const messages = [];
|
|
4185
|
+
let cursor = input.cursor;
|
|
4186
|
+
let pages = 0;
|
|
4187
|
+
while (messages.length < targetLimit && pages < maxPages) {
|
|
4188
|
+
pages += 1;
|
|
4189
|
+
const pageLimit = Math.max(1, Math.min(200, targetLimit - messages.length));
|
|
4190
|
+
const response = await withSlackRetries(
|
|
4191
|
+
() => client2.conversations.history({
|
|
4192
|
+
channel: channelId,
|
|
4193
|
+
limit: pageLimit,
|
|
4194
|
+
cursor,
|
|
4195
|
+
oldest: input.oldest,
|
|
4196
|
+
latest: input.latest,
|
|
4197
|
+
inclusive: input.inclusive
|
|
4198
|
+
}),
|
|
4199
|
+
3,
|
|
4200
|
+
{ action: "conversations.history" }
|
|
4201
|
+
);
|
|
4202
|
+
const batch = response.messages ?? [];
|
|
4203
|
+
messages.push(...batch);
|
|
4204
|
+
cursor = response.response_metadata?.next_cursor || void 0;
|
|
4205
|
+
if (!cursor) {
|
|
4206
|
+
break;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
return {
|
|
4210
|
+
messages: messages.slice(0, targetLimit),
|
|
4211
|
+
nextCursor: cursor
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
4214
|
+
async function listChannelMembers(input) {
|
|
4215
|
+
const client2 = getSlackClient();
|
|
4216
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
4217
|
+
if (!channelId) {
|
|
4218
|
+
throw new Error("Slack channel member lookup requires a valid channel ID");
|
|
4219
|
+
}
|
|
4220
|
+
const targetLimit = Math.max(1, Math.min(input.limit, 200));
|
|
4221
|
+
const response = await withSlackRetries(
|
|
4222
|
+
() => client2.conversations.members({
|
|
4223
|
+
channel: channelId,
|
|
4224
|
+
limit: targetLimit,
|
|
4225
|
+
cursor: input.cursor
|
|
4226
|
+
}),
|
|
4227
|
+
3,
|
|
4228
|
+
{ action: "conversations.members" }
|
|
4229
|
+
);
|
|
4230
|
+
const members = (response.members ?? []).slice(0, targetLimit);
|
|
4231
|
+
return {
|
|
4232
|
+
members: members.map((userId) => ({ user_id: userId })),
|
|
4233
|
+
nextCursor: response.response_metadata?.next_cursor || void 0
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
async function listThreadReplies(input) {
|
|
4237
|
+
const client2 = getSlackClient();
|
|
4238
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
4239
|
+
if (!channelId) {
|
|
4240
|
+
throw new Error("Slack thread reply lookup requires a valid channel ID");
|
|
4241
|
+
}
|
|
4242
|
+
const targetLimit = Math.max(1, Math.min(input.limit ?? 1e3, 1e3));
|
|
4243
|
+
const maxPages = Math.max(1, Math.min(input.maxPages ?? 10, 10));
|
|
4244
|
+
const pendingTargets = new Set(
|
|
4245
|
+
(input.targetMessageTs ?? []).filter((value) => typeof value === "string" && value.length > 0)
|
|
4246
|
+
);
|
|
4247
|
+
const replies = [];
|
|
4248
|
+
let cursor;
|
|
4249
|
+
let pages = 0;
|
|
4250
|
+
while (replies.length < targetLimit && pages < maxPages) {
|
|
4251
|
+
pages += 1;
|
|
4252
|
+
const pageLimit = Math.max(1, Math.min(200, targetLimit - replies.length));
|
|
4253
|
+
const response = await withSlackRetries(
|
|
4254
|
+
() => client2.conversations.replies({
|
|
4255
|
+
channel: channelId,
|
|
4256
|
+
ts: input.threadTs,
|
|
4257
|
+
limit: pageLimit,
|
|
4258
|
+
cursor
|
|
4259
|
+
}),
|
|
4260
|
+
3,
|
|
4261
|
+
{ action: "conversations.replies" }
|
|
4262
|
+
);
|
|
4263
|
+
const batch = response.messages ?? [];
|
|
4264
|
+
replies.push(...batch);
|
|
4265
|
+
for (const reply of batch) {
|
|
4266
|
+
if (typeof reply.ts === "string" && pendingTargets.size > 0) {
|
|
4267
|
+
pendingTargets.delete(reply.ts);
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
cursor = response.response_metadata?.next_cursor || void 0;
|
|
4271
|
+
if (!cursor || pendingTargets.size === 0) {
|
|
4272
|
+
break;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
return replies.slice(0, targetLimit);
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
// src/chat/tools/slack-channel-list-members.ts
|
|
3491
4279
|
function createSlackChannelListMembersTool(context) {
|
|
3492
4280
|
return tool({
|
|
3493
4281
|
description: "List member IDs in the active Slack channel context. Use when the user asks who is in a channel, who to assign, or who should be notified. Do not use when thread-local participant context is sufficient.",
|
|
@@ -3737,7 +4525,7 @@ function normalizeCanvasMarkdown(markdown) {
|
|
|
3737
4525
|
};
|
|
3738
4526
|
}
|
|
3739
4527
|
async function createCanvas(input) {
|
|
3740
|
-
const
|
|
4528
|
+
const client2 = getSlackClient();
|
|
3741
4529
|
const normalizedChannelId = normalizeSlackConversationId(input.channelId);
|
|
3742
4530
|
const isConversationScoped = isConversationScopedChannel(normalizedChannelId);
|
|
3743
4531
|
if (!isConversationScoped) {
|
|
@@ -3749,7 +4537,7 @@ async function createCanvas(input) {
|
|
|
3749
4537
|
const action = "conversations.canvases.create";
|
|
3750
4538
|
const normalizedContent = normalizeCanvasMarkdown(input.markdown);
|
|
3751
4539
|
const result = await withSlackRetries(async () => {
|
|
3752
|
-
return
|
|
4540
|
+
return client2.conversations.canvases.create({
|
|
3753
4541
|
channel_id: normalizedChannelId,
|
|
3754
4542
|
title: input.title,
|
|
3755
4543
|
document_content: {
|
|
@@ -3782,9 +4570,9 @@ async function createCanvas(input) {
|
|
|
3782
4570
|
};
|
|
3783
4571
|
}
|
|
3784
4572
|
async function lookupCanvasSection(canvasId, containsText) {
|
|
3785
|
-
const
|
|
4573
|
+
const client2 = getSlackClient();
|
|
3786
4574
|
const response = await withSlackRetries(
|
|
3787
|
-
() =>
|
|
4575
|
+
() => client2.canvases.sections.lookup({
|
|
3788
4576
|
canvas_id: canvasId,
|
|
3789
4577
|
criteria: {
|
|
3790
4578
|
contains_text: containsText
|
|
@@ -3802,10 +4590,10 @@ async function lookupCanvasSection(canvasId, containsText) {
|
|
|
3802
4590
|
return response.sections?.[0]?.id;
|
|
3803
4591
|
}
|
|
3804
4592
|
async function updateCanvas(input) {
|
|
3805
|
-
const
|
|
4593
|
+
const client2 = getSlackClient();
|
|
3806
4594
|
const normalizedContent = normalizeCanvasMarkdown(input.markdown);
|
|
3807
4595
|
await withSlackRetries(
|
|
3808
|
-
() =>
|
|
4596
|
+
() => client2.canvases.edit({
|
|
3809
4597
|
canvas_id: input.canvasId,
|
|
3810
4598
|
changes: [
|
|
3811
4599
|
{
|
|
@@ -4048,9 +4836,9 @@ var DEFAULT_TODO_SCHEMA = [
|
|
|
4048
4836
|
{ key: "due_date", name: "Due Date", type: "date" }
|
|
4049
4837
|
];
|
|
4050
4838
|
async function createTodoList(name) {
|
|
4051
|
-
const
|
|
4839
|
+
const client2 = getSlackClient();
|
|
4052
4840
|
const result = await withSlackRetries(
|
|
4053
|
-
() =>
|
|
4841
|
+
() => client2.slackLists.create({
|
|
4054
4842
|
name,
|
|
4055
4843
|
schema: DEFAULT_TODO_SCHEMA,
|
|
4056
4844
|
todo_mode: true
|
|
@@ -4072,7 +4860,7 @@ async function createTodoList(name) {
|
|
|
4072
4860
|
};
|
|
4073
4861
|
}
|
|
4074
4862
|
async function addListItems(input) {
|
|
4075
|
-
const
|
|
4863
|
+
const client2 = getSlackClient();
|
|
4076
4864
|
const listColumnMap = input.listColumnMap ?? {};
|
|
4077
4865
|
if (!listColumnMap.titleColumnId) {
|
|
4078
4866
|
throw new Error("Cannot add list items because title column could not be inferred");
|
|
@@ -4093,7 +4881,7 @@ async function addListItems(input) {
|
|
|
4093
4881
|
});
|
|
4094
4882
|
}
|
|
4095
4883
|
const response = await withSlackRetries(
|
|
4096
|
-
() =>
|
|
4884
|
+
() => client2.slackLists.items.create({
|
|
4097
4885
|
list_id: input.listId,
|
|
4098
4886
|
initial_fields: initialFields
|
|
4099
4887
|
})
|
|
@@ -4108,13 +4896,13 @@ async function addListItems(input) {
|
|
|
4108
4896
|
};
|
|
4109
4897
|
}
|
|
4110
4898
|
async function listItems(listId, limit = 100) {
|
|
4111
|
-
const
|
|
4899
|
+
const client2 = getSlackClient();
|
|
4112
4900
|
const items = [];
|
|
4113
4901
|
let cursor;
|
|
4114
4902
|
const cappedLimit = Math.max(1, Math.min(limit, 200));
|
|
4115
4903
|
do {
|
|
4116
4904
|
const response = await withSlackRetries(
|
|
4117
|
-
() =>
|
|
4905
|
+
() => client2.slackLists.items.list({
|
|
4118
4906
|
list_id: listId,
|
|
4119
4907
|
limit: cappedLimit,
|
|
4120
4908
|
cursor
|
|
@@ -4133,7 +4921,7 @@ async function listItems(listId, limit = 100) {
|
|
|
4133
4921
|
return items;
|
|
4134
4922
|
}
|
|
4135
4923
|
async function updateListItem(input) {
|
|
4136
|
-
const
|
|
4924
|
+
const client2 = getSlackClient();
|
|
4137
4925
|
const cells = [];
|
|
4138
4926
|
if (typeof input.completed === "boolean" && input.listColumnMap.completedColumnId) {
|
|
4139
4927
|
cells.push({
|
|
@@ -4152,7 +4940,7 @@ async function updateListItem(input) {
|
|
|
4152
4940
|
throw new Error("No updatable fields were provided or inferred for this list item");
|
|
4153
4941
|
}
|
|
4154
4942
|
await withSlackRetries(
|
|
4155
|
-
() =>
|
|
4943
|
+
() => client2.slackLists.items.update({
|
|
4156
4944
|
list_id: input.listId,
|
|
4157
4945
|
cells
|
|
4158
4946
|
})
|
|
@@ -5001,7 +5789,7 @@ var SNAPSHOT_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
|
5001
5789
|
var SNAPSHOT_BUILD_LOCK_TTL_MS = 10 * 60 * 1e3;
|
|
5002
5790
|
var SNAPSHOT_WAIT_FOR_LOCK_MS = SNAPSHOT_BUILD_LOCK_TTL_MS + 30 * 1e3;
|
|
5003
5791
|
var DEFAULT_FLOATING_DEP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5004
|
-
function
|
|
5792
|
+
function sleep2(ms) {
|
|
5005
5793
|
return new Promise((resolve) => {
|
|
5006
5794
|
setTimeout(resolve, ms);
|
|
5007
5795
|
});
|
|
@@ -5286,7 +6074,7 @@ async function withBuildLock(profileHash, callback, canUseCachedSnapshot) {
|
|
|
5286
6074
|
await state.releaseLock(lock);
|
|
5287
6075
|
}
|
|
5288
6076
|
}
|
|
5289
|
-
await
|
|
6077
|
+
await sleep2(500);
|
|
5290
6078
|
}
|
|
5291
6079
|
const cached = await getCachedSnapshot(profileHash);
|
|
5292
6080
|
if (cached?.snapshotId && canUseCachedSnapshot(cached)) {
|
|
@@ -6046,14 +6834,14 @@ function createSandboxExecutor(options) {
|
|
|
6046
6834
|
}
|
|
6047
6835
|
|
|
6048
6836
|
// src/chat/runtime-metadata.ts
|
|
6049
|
-
function
|
|
6837
|
+
function toOptionalTrimmed3(value) {
|
|
6050
6838
|
if (!value) return void 0;
|
|
6051
6839
|
const trimmed = value.trim();
|
|
6052
6840
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
6053
6841
|
}
|
|
6054
6842
|
function getRuntimeMetadata() {
|
|
6055
6843
|
return {
|
|
6056
|
-
version:
|
|
6844
|
+
version: toOptionalTrimmed3(process.env.VERCEL_GIT_COMMIT_SHA)
|
|
6057
6845
|
};
|
|
6058
6846
|
}
|
|
6059
6847
|
|
|
@@ -7284,7 +8072,23 @@ async function publishAppHomeView(slackClient, userId, userTokenStore) {
|
|
|
7284
8072
|
|
|
7285
8073
|
export {
|
|
7286
8074
|
isPluginProvider,
|
|
8075
|
+
botConfig,
|
|
8076
|
+
getSlackBotToken,
|
|
8077
|
+
getSlackSigningSecret,
|
|
8078
|
+
getSlackClientId,
|
|
8079
|
+
getSlackClientSecret,
|
|
8080
|
+
getStateAdapter,
|
|
8081
|
+
claimQueueIngressDedup,
|
|
8082
|
+
hasQueueIngressDedup,
|
|
8083
|
+
getQueueMessageProcessingState,
|
|
8084
|
+
acquireQueueMessageProcessingOwnership,
|
|
8085
|
+
refreshQueueMessageProcessingOwnership,
|
|
8086
|
+
completeQueueMessageProcessingOwnership,
|
|
8087
|
+
failQueueMessageProcessingOwnership,
|
|
7287
8088
|
getUserTokenStore,
|
|
8089
|
+
getSlackClient,
|
|
8090
|
+
isDmChannel,
|
|
8091
|
+
downloadPrivateSlackFile,
|
|
7288
8092
|
getOAuthProviderConfig,
|
|
7289
8093
|
resolveBaseUrl,
|
|
7290
8094
|
startOAuthFlow,
|
|
@@ -7295,6 +8099,9 @@ export {
|
|
|
7295
8099
|
GEN_AI_PROVIDER_NAME,
|
|
7296
8100
|
completeText,
|
|
7297
8101
|
completeObject,
|
|
8102
|
+
addReactionToMessage,
|
|
8103
|
+
removeReactionFromMessage,
|
|
8104
|
+
listThreadReplies,
|
|
7298
8105
|
shouldEmitDevAgentTrace,
|
|
7299
8106
|
truncateStatusText,
|
|
7300
8107
|
isRetryableTurnError,
|