@sentry/junior 0.1.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.
@@ -0,0 +1,3108 @@
1
+ import {
2
+ GEN_AI_PROVIDER_NAME,
3
+ buildSlackOutputMessage,
4
+ completeObject,
5
+ completeText,
6
+ ensureBlockSpacing,
7
+ escapeXml,
8
+ generateAssistantReply,
9
+ getOAuthProviderConfig,
10
+ getUserTokenStore,
11
+ isExplicitChannelPostIntent,
12
+ isPluginProvider,
13
+ isRetryableTurnError,
14
+ publishAppHomeView,
15
+ shouldEmitDevAgentTrace,
16
+ startOAuthFlow,
17
+ truncateStatusText
18
+ } from "./chunk-7E56WM6K.js";
19
+ import {
20
+ claimQueueIngressDedup,
21
+ getStateAdapter,
22
+ hasQueueIngressDedup
23
+ } from "./chunk-ZBFSIN6G.js";
24
+ import {
25
+ listThreadReplies
26
+ } from "./chunk-MM3YNA4F.js";
27
+ import {
28
+ botConfig,
29
+ downloadPrivateSlackFile,
30
+ getSlackBotToken,
31
+ getSlackClient,
32
+ getSlackClientId,
33
+ getSlackClientSecret,
34
+ getSlackSigningSecret,
35
+ isDmChannel
36
+ } from "./chunk-GDNDYMGX.js";
37
+ import {
38
+ logError,
39
+ logException,
40
+ logInfo,
41
+ logWarn,
42
+ setSpanAttributes,
43
+ setTags,
44
+ toOptionalString,
45
+ withContext,
46
+ withSpan
47
+ } from "./chunk-BBOVH5RF.js";
48
+
49
+ // src/chat/bot.ts
50
+ import { Chat as Chat2 } from "chat";
51
+ import { createSlackAdapter } from "@chat-adapter/slack";
52
+
53
+ // src/chat/chat-background-patch.ts
54
+ import { Chat } from "chat";
55
+
56
+ // src/chat/conversation-state.ts
57
+ function isRecord(value) {
58
+ return Boolean(value) && typeof value === "object";
59
+ }
60
+ function toOptionalString2(value) {
61
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
62
+ }
63
+ function toOptionalNumber(value) {
64
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
65
+ }
66
+ function coerceRole(value) {
67
+ return value === "assistant" || value === "system" || value === "user" ? value : "user";
68
+ }
69
+ function coerceAuthor(value) {
70
+ if (!isRecord(value)) return void 0;
71
+ const author = {
72
+ fullName: toOptionalString2(value.fullName),
73
+ userId: toOptionalString2(value.userId),
74
+ userName: toOptionalString2(value.userName)
75
+ };
76
+ if (typeof value.isBot === "boolean") {
77
+ author.isBot = value.isBot;
78
+ }
79
+ if (!author.fullName && !author.userId && !author.userName && author.isBot === void 0) {
80
+ return void 0;
81
+ }
82
+ return author;
83
+ }
84
+ function coerceMessageMeta(value) {
85
+ if (!isRecord(value)) return void 0;
86
+ const meta = {};
87
+ if (typeof value.explicitMention === "boolean") {
88
+ meta.explicitMention = value.explicitMention;
89
+ }
90
+ if (typeof value.replied === "boolean") {
91
+ meta.replied = value.replied;
92
+ }
93
+ if (typeof value.skippedReason === "string" && value.skippedReason.trim().length > 0) {
94
+ meta.skippedReason = value.skippedReason;
95
+ }
96
+ if (typeof value.slackTs === "string" && value.slackTs.trim().length > 0) {
97
+ meta.slackTs = value.slackTs;
98
+ }
99
+ if (Array.isArray(value.imageFileIds)) {
100
+ const imageFileIds = value.imageFileIds.filter(
101
+ (entry) => typeof entry === "string" && entry.trim().length > 0
102
+ );
103
+ if (imageFileIds.length > 0) {
104
+ meta.imageFileIds = imageFileIds;
105
+ }
106
+ }
107
+ if (typeof value.imagesHydrated === "boolean") {
108
+ meta.imagesHydrated = value.imagesHydrated;
109
+ }
110
+ 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) {
111
+ return void 0;
112
+ }
113
+ return meta;
114
+ }
115
+ function defaultConversationState() {
116
+ const nowMs = Date.now();
117
+ return {
118
+ schemaVersion: 1,
119
+ messages: [],
120
+ compactions: [],
121
+ backfill: {},
122
+ processing: {},
123
+ stats: {
124
+ estimatedContextTokens: 0,
125
+ totalMessageCount: 0,
126
+ compactedMessageCount: 0,
127
+ updatedAtMs: nowMs
128
+ },
129
+ vision: {
130
+ byFileId: {}
131
+ }
132
+ };
133
+ }
134
+ function coerceThreadConversationState(value) {
135
+ if (!isRecord(value)) {
136
+ return defaultConversationState();
137
+ }
138
+ const root = value;
139
+ const rawConversation = isRecord(root.conversation) ? root.conversation : {};
140
+ const base = defaultConversationState();
141
+ const rawMessages = Array.isArray(rawConversation.messages) ? rawConversation.messages : [];
142
+ const messages = [];
143
+ for (const item of rawMessages) {
144
+ if (!isRecord(item)) continue;
145
+ const id = toOptionalString2(item.id);
146
+ const text = toOptionalString2(item.text);
147
+ const createdAtMs = toOptionalNumber(item.createdAtMs);
148
+ if (!id || !text || !createdAtMs) continue;
149
+ messages.push({
150
+ id,
151
+ role: coerceRole(item.role),
152
+ text,
153
+ createdAtMs,
154
+ author: coerceAuthor(item.author),
155
+ meta: coerceMessageMeta(item.meta)
156
+ });
157
+ }
158
+ const rawCompactions = Array.isArray(rawConversation.compactions) ? rawConversation.compactions : [];
159
+ const compactions = [];
160
+ for (const item of rawCompactions) {
161
+ if (!isRecord(item)) continue;
162
+ const id = toOptionalString2(item.id);
163
+ const summary = toOptionalString2(item.summary);
164
+ const createdAtMs = toOptionalNumber(item.createdAtMs);
165
+ if (!id || !summary || !createdAtMs) continue;
166
+ const coveredMessageIds = Array.isArray(item.coveredMessageIds) ? item.coveredMessageIds.filter((entry) => typeof entry === "string" && entry.length > 0) : [];
167
+ compactions.push({
168
+ id,
169
+ summary,
170
+ createdAtMs,
171
+ coveredMessageIds
172
+ });
173
+ }
174
+ const rawBackfill = isRecord(rawConversation.backfill) ? rawConversation.backfill : {};
175
+ const backfill = {
176
+ completedAtMs: toOptionalNumber(rawBackfill.completedAtMs),
177
+ source: rawBackfill.source === "recent_messages" || rawBackfill.source === "thread_fetch" ? rawBackfill.source : void 0
178
+ };
179
+ const rawProcessing = isRecord(rawConversation.processing) ? rawConversation.processing : {};
180
+ const processing = {
181
+ activeTurnId: toOptionalString2(rawProcessing.activeTurnId),
182
+ lastCompletedAtMs: toOptionalNumber(rawProcessing.lastCompletedAtMs)
183
+ };
184
+ const rawStats = isRecord(rawConversation.stats) ? rawConversation.stats : {};
185
+ const stats = {
186
+ estimatedContextTokens: toOptionalNumber(rawStats.estimatedContextTokens) ?? base.stats.estimatedContextTokens,
187
+ totalMessageCount: toOptionalNumber(rawStats.totalMessageCount) ?? messages.length,
188
+ compactedMessageCount: toOptionalNumber(rawStats.compactedMessageCount) ?? 0,
189
+ updatedAtMs: toOptionalNumber(rawStats.updatedAtMs) ?? base.stats.updatedAtMs
190
+ };
191
+ const rawVision = isRecord(rawConversation.vision) ? rawConversation.vision : {};
192
+ const rawVisionByFileId = isRecord(rawVision.byFileId) ? rawVision.byFileId : {};
193
+ const byFileId = {};
194
+ for (const [fileId, value2] of Object.entries(rawVisionByFileId)) {
195
+ if (typeof fileId !== "string" || fileId.trim().length === 0) continue;
196
+ if (!isRecord(value2)) continue;
197
+ const summary = toOptionalString2(value2.summary);
198
+ const analyzedAtMs = toOptionalNumber(value2.analyzedAtMs);
199
+ if (!summary || !analyzedAtMs) continue;
200
+ byFileId[fileId] = {
201
+ summary,
202
+ analyzedAtMs
203
+ };
204
+ }
205
+ return {
206
+ schemaVersion: 1,
207
+ messages,
208
+ compactions,
209
+ backfill,
210
+ processing,
211
+ stats,
212
+ vision: {
213
+ backfillCompletedAtMs: toOptionalNumber(rawVision.backfillCompletedAtMs),
214
+ byFileId
215
+ }
216
+ };
217
+ }
218
+ function buildConversationStatePatch(conversation) {
219
+ return {
220
+ conversation: {
221
+ ...conversation,
222
+ schemaVersion: 1,
223
+ stats: {
224
+ ...conversation.stats,
225
+ totalMessageCount: conversation.messages.length,
226
+ updatedAtMs: Date.now()
227
+ }
228
+ }
229
+ };
230
+ }
231
+
232
+ // src/chat/routing/subscribed-decision.ts
233
+ import { z } from "zod";
234
+ var replyDecisionSchema = z.object({
235
+ should_reply: z.boolean().describe("Whether Junior should respond to this thread message."),
236
+ confidence: z.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
237
+ reason: z.string().max(160).optional().describe("Short reason for the decision.")
238
+ });
239
+ var ROUTER_CONFIDENCE_THRESHOLD = 0.72;
240
+ var ACK_REGEXES = [
241
+ /^(thanks|thank you|thx|ty|tysm|much appreciated)[!. ]*$/i,
242
+ /^(ok|okay|k|got it|sgtm|lgtm|sounds good|works for me|works|done|resolved|perfect|great|nice|cool)[!. ]*$/i,
243
+ /^(\+1|\+\+|ack|roger|copy that)[!. ]*$/i,
244
+ /^(:[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D])+[!. ]*$/u
245
+ ];
246
+ var QUESTION_PREFIX_RE = /^(what|why|how|when|where|which|who|can|could|would|should|do|does|did|is|are|was|were|will)\b/i;
247
+ var FOLLOW_UP_REF_RE = /\b(you|your|that|this|it|above|previous|earlier|last|just\s+said)\b/i;
248
+ function tokenizeForOverlap(value) {
249
+ return value.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length >= 4);
250
+ }
251
+ function getLastAssistantLine(conversationContext) {
252
+ if (!conversationContext) return void 0;
253
+ const lines = conversationContext.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
254
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
255
+ const line = lines[index];
256
+ if (line.startsWith("[assistant]")) {
257
+ return line;
258
+ }
259
+ }
260
+ return void 0;
261
+ }
262
+ function isLikelyAcknowledgment(text) {
263
+ const trimmed = text.trim();
264
+ if (!trimmed) return false;
265
+ if (trimmed.includes("?")) return false;
266
+ for (const regex of ACK_REGEXES) {
267
+ if (regex.test(trimmed)) {
268
+ return true;
269
+ }
270
+ }
271
+ return false;
272
+ }
273
+ function isLikelyAssistantDirectedFollowUp(text, conversationContext) {
274
+ const trimmed = text.trim();
275
+ if (!trimmed) return false;
276
+ const isQuestion = trimmed.includes("?") || QUESTION_PREFIX_RE.test(trimmed);
277
+ if (!isQuestion) {
278
+ return false;
279
+ }
280
+ const lastAssistantLine = getLastAssistantLine(conversationContext);
281
+ if (!lastAssistantLine) {
282
+ return false;
283
+ }
284
+ if (FOLLOW_UP_REF_RE.test(trimmed)) {
285
+ return true;
286
+ }
287
+ const questionTokens = tokenizeForOverlap(trimmed);
288
+ const assistantTokens = new Set(tokenizeForOverlap(lastAssistantLine));
289
+ for (const token of questionTokens) {
290
+ if (assistantTokens.has(token)) {
291
+ return true;
292
+ }
293
+ }
294
+ return false;
295
+ }
296
+ function buildRouterSystemPrompt(botUserName, conversationContext) {
297
+ return [
298
+ "You are a message router for a Slack assistant named Junior in a subscribed Slack thread.",
299
+ "Decide whether Junior should reply to the latest message.",
300
+ "Default to should_reply=false unless the user is clearly asking Junior for help or follow-up.",
301
+ "",
302
+ "Reply should be true only when the user is clearly asking Junior a question, requesting help,",
303
+ "or when a direct follow-up is contextually aimed at Junior's previous response in the thread context.",
304
+ "",
305
+ "Reply should be false for side conversations between humans, acknowledgements (thanks, +1),",
306
+ "status chatter, or messages not seeking assistant input.",
307
+ "Junior must not participate in casual banter.",
308
+ "If uncertain, set should_reply=false and use low confidence.",
309
+ "",
310
+ "Return JSON with should_reply, confidence, and a short reason. Do not return any extra keys.",
311
+ "",
312
+ `<assistant-name>${escapeXml(botUserName)}</assistant-name>`,
313
+ `<thread-context>${escapeXml(conversationContext?.trim() || "[none]")}</thread-context>`
314
+ ].join("\n");
315
+ }
316
+ async function decideSubscribedThreadReply(args) {
317
+ const text = args.input.text.trim();
318
+ const rawText = args.input.rawText.trim();
319
+ if (args.input.isExplicitMention) {
320
+ return { shouldReply: true, reason: "explicit_mention" /* ExplicitMention */ };
321
+ }
322
+ if (!text && !args.input.hasAttachments) {
323
+ return { shouldReply: false, reason: "empty_message" /* EmptyMessage */ };
324
+ }
325
+ if (!text && args.input.hasAttachments) {
326
+ return { shouldReply: true, reason: "attachment_only" /* AttachmentOnly */ };
327
+ }
328
+ if (isLikelyAcknowledgment(text)) {
329
+ return { shouldReply: false, reason: "acknowledgment" /* Acknowledgment */ };
330
+ }
331
+ if (isLikelyAssistantDirectedFollowUp(text, args.input.conversationContext)) {
332
+ return { shouldReply: true, reason: "follow_up_question" /* FollowUpQuestion */ };
333
+ }
334
+ try {
335
+ const result = await args.completeObject({
336
+ modelId: args.modelId,
337
+ schema: replyDecisionSchema,
338
+ maxTokens: 120,
339
+ temperature: 0,
340
+ system: buildRouterSystemPrompt(args.botUserName, args.input.conversationContext),
341
+ prompt: rawText,
342
+ metadata: {
343
+ modelId: args.modelId,
344
+ threadId: args.input.context.threadId ?? "",
345
+ channelId: args.input.context.channelId ?? "",
346
+ requesterId: args.input.context.requesterId ?? "",
347
+ runId: args.input.context.runId ?? ""
348
+ }
349
+ });
350
+ const parsed = replyDecisionSchema.parse(result.object);
351
+ const reason = parsed.reason?.trim() || "classifier";
352
+ if (!parsed.should_reply) {
353
+ return {
354
+ shouldReply: false,
355
+ reason: "side_conversation" /* SideConversation */,
356
+ reasonDetail: reason
357
+ };
358
+ }
359
+ if (parsed.confidence < ROUTER_CONFIDENCE_THRESHOLD) {
360
+ return {
361
+ shouldReply: false,
362
+ reason: "low_confidence" /* LowConfidence */,
363
+ reasonDetail: `${parsed.confidence.toFixed(2)}: ${reason}`
364
+ };
365
+ }
366
+ return {
367
+ shouldReply: true,
368
+ reason: "llm_classifier" /* Classifier */,
369
+ reasonDetail: reason
370
+ };
371
+ } catch (error) {
372
+ args.logClassifierFailure(error, args.input);
373
+ return {
374
+ shouldReply: false,
375
+ reason: "classifier_error" /* ClassifierError */
376
+ };
377
+ }
378
+ }
379
+
380
+ // src/chat/slack-user.ts
381
+ var USER_CACHE_TTL_MS = 5 * 60 * 1e3;
382
+ var userCache = /* @__PURE__ */ new Map();
383
+ function readFromCache(userId) {
384
+ const hit = userCache.get(userId);
385
+ if (!hit) return null;
386
+ if (hit.expiresAt < Date.now()) {
387
+ userCache.delete(userId);
388
+ return null;
389
+ }
390
+ return hit.value;
391
+ }
392
+ function writeToCache(userId, value) {
393
+ userCache.set(userId, {
394
+ value,
395
+ expiresAt: Date.now() + USER_CACHE_TTL_MS
396
+ });
397
+ }
398
+ async function lookupSlackUser(userId) {
399
+ if (!userId) {
400
+ return null;
401
+ }
402
+ const cached = readFromCache(userId);
403
+ if (cached) {
404
+ return cached;
405
+ }
406
+ const token = getSlackBotToken();
407
+ if (!token) {
408
+ return null;
409
+ }
410
+ try {
411
+ const response = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, {
412
+ headers: {
413
+ authorization: `Bearer ${token}`
414
+ }
415
+ });
416
+ if (!response.ok) {
417
+ logWarn(
418
+ "slack_user_lookup_failed",
419
+ {},
420
+ {
421
+ "enduser.id": userId,
422
+ "http.response.status_code": response.status
423
+ },
424
+ "Slack user lookup request failed"
425
+ );
426
+ return null;
427
+ }
428
+ const payload = await response.json();
429
+ if (!payload.ok || !payload.user) {
430
+ return null;
431
+ }
432
+ const userName = payload.user.name?.trim() || void 0;
433
+ const fullName = payload.user.profile?.display_name?.trim() || payload.user.profile?.real_name?.trim() || payload.user.real_name?.trim() || void 0;
434
+ const result = {
435
+ userName,
436
+ fullName
437
+ };
438
+ writeToCache(userId, result);
439
+ return result;
440
+ } catch (error) {
441
+ logWarn(
442
+ "slack_user_lookup_failed",
443
+ {},
444
+ {
445
+ "enduser.id": userId,
446
+ "error.message": error instanceof Error ? error.message : String(error)
447
+ },
448
+ "Slack user lookup failed with exception"
449
+ );
450
+ return null;
451
+ }
452
+ }
453
+
454
+ // src/chat/runtime/deps.ts
455
+ var defaultBotDeps = {
456
+ completeObject,
457
+ completeText,
458
+ downloadPrivateSlackFile,
459
+ generateAssistantReply,
460
+ listThreadReplies,
461
+ lookupSlackUser
462
+ };
463
+ var botDeps = defaultBotDeps;
464
+ function setBotDepsForTests(overrides) {
465
+ botDeps = {
466
+ ...defaultBotDeps,
467
+ ...overrides
468
+ };
469
+ }
470
+ function resetBotDepsForTests() {
471
+ botDeps = defaultBotDeps;
472
+ }
473
+ function getBotDeps() {
474
+ return botDeps;
475
+ }
476
+
477
+ // src/chat/runtime/subscribed-routing.ts
478
+ async function shouldReplyInSubscribedThread(args) {
479
+ const decision = await decideSubscribedThreadReply({
480
+ botUserName: botConfig.userName,
481
+ modelId: botConfig.fastModelId,
482
+ input: args,
483
+ completeObject: (input) => getBotDeps().completeObject(input),
484
+ logClassifierFailure: (error, input) => {
485
+ logWarn(
486
+ "subscribed_reply_classifier_failed",
487
+ {
488
+ slackThreadId: input.context.threadId,
489
+ slackUserId: input.context.requesterId,
490
+ slackChannelId: input.context.channelId,
491
+ runId: input.context.runId,
492
+ assistantUserName: botConfig.userName,
493
+ modelId: botConfig.fastModelId
494
+ },
495
+ {
496
+ "error.message": error instanceof Error ? error.message : String(error)
497
+ },
498
+ "Subscribed-thread reply classifier failed; skipping reply"
499
+ );
500
+ }
501
+ });
502
+ const reason = decision.reasonDetail ? `${decision.reason}:${decision.reasonDetail}` : decision.reason;
503
+ return {
504
+ shouldReply: decision.shouldReply,
505
+ reason
506
+ };
507
+ }
508
+
509
+ // src/chat/slack-context.ts
510
+ function toOptionalString3(value) {
511
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
512
+ }
513
+ function parseSlackThreadId(threadId) {
514
+ const normalizedThreadId = toOptionalString3(threadId);
515
+ if (!normalizedThreadId) {
516
+ return void 0;
517
+ }
518
+ const parts = normalizedThreadId.split(":");
519
+ if (parts.length !== 3 || parts[0] !== "slack") {
520
+ return void 0;
521
+ }
522
+ const channelId = toOptionalString3(parts[1]);
523
+ const threadTs = toOptionalString3(parts[2]);
524
+ if (!channelId || !threadTs) {
525
+ return void 0;
526
+ }
527
+ return { channelId, threadTs };
528
+ }
529
+ function resolveSlackChannelIdFromThreadId(threadId) {
530
+ return parseSlackThreadId(threadId)?.channelId;
531
+ }
532
+ function resolveSlackChannelIdFromMessage(message) {
533
+ const messageChannelId = toOptionalString3(message.channelId);
534
+ if (messageChannelId) {
535
+ return messageChannelId;
536
+ }
537
+ const raw = message.raw;
538
+ if (raw && typeof raw === "object") {
539
+ const rawChannel = toOptionalString3(raw.channel);
540
+ if (rawChannel) {
541
+ return rawChannel;
542
+ }
543
+ }
544
+ const threadId = toOptionalString3(message.threadId);
545
+ return resolveSlackChannelIdFromThreadId(threadId);
546
+ }
547
+
548
+ // src/chat/runtime/thread-context.ts
549
+ function escapeRegExp(value) {
550
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
551
+ }
552
+ function stripLeadingBotMention(text, options = {}) {
553
+ if (!text.trim()) return text;
554
+ let next = text;
555
+ if (options.stripLeadingSlackMentionToken) {
556
+ next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
557
+ }
558
+ const mentionByNameRe = new RegExp(`^\\s*@${escapeRegExp(botConfig.userName)}\\b[\\s,:-]*`, "i");
559
+ next = next.replace(mentionByNameRe, "").trim();
560
+ const mentionByLabeledEntityRe = new RegExp(
561
+ `^\\s*<@[^>|]+\\|${escapeRegExp(botConfig.userName)}>[\\s,:-]*`,
562
+ "i"
563
+ );
564
+ next = next.replace(mentionByLabeledEntityRe, "").trim();
565
+ return next;
566
+ }
567
+ function getThreadId(thread, _message) {
568
+ return toOptionalString(thread.id);
569
+ }
570
+ function getRunId(thread, message) {
571
+ return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
572
+ }
573
+ function getChannelId(thread, message) {
574
+ return thread.channelId ?? resolveSlackChannelIdFromMessage(message);
575
+ }
576
+ function getThreadTs(threadId) {
577
+ return parseSlackThreadId(threadId)?.threadTs;
578
+ }
579
+ function getMessageTs(message) {
580
+ const directTs = toOptionalString(message.ts);
581
+ if (directTs) {
582
+ return directTs;
583
+ }
584
+ const raw = message.raw;
585
+ if (!raw || typeof raw !== "object") {
586
+ return void 0;
587
+ }
588
+ const rawRecord = raw;
589
+ return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
590
+ }
591
+ function getSlackApiErrorCode(error) {
592
+ if (!error || typeof error !== "object") {
593
+ return void 0;
594
+ }
595
+ const candidate = error;
596
+ if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
597
+ return candidate.data.error;
598
+ }
599
+ if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
600
+ return candidate.code;
601
+ }
602
+ return void 0;
603
+ }
604
+ function isSlackTitlePermissionError(error) {
605
+ const code = getSlackApiErrorCode(error);
606
+ return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
607
+ }
608
+
609
+ // src/chat/chat-background-patch.ts
610
+ var PATCH_FLAG = /* @__PURE__ */ Symbol.for("junior.chat.backgroundPatch");
611
+ var QUEUE_INGRESS_DEDUP_TTL_MS = 24 * 60 * 60 * 1e3;
612
+ function nonEmptyString(value) {
613
+ if (typeof value !== "string") return void 0;
614
+ const trimmed = value.trim();
615
+ return trimmed || void 0;
616
+ }
617
+ function buildRoutingConversationContext(conversation) {
618
+ if (conversation.messages.length === 0 && conversation.compactions.length === 0) {
619
+ return void 0;
620
+ }
621
+ const lines = [];
622
+ if (conversation.compactions.length > 0) {
623
+ lines.push("<thread-compactions>");
624
+ for (const [index, compaction] of conversation.compactions.entries()) {
625
+ lines.push(
626
+ [
627
+ `summary_${index + 1}:`,
628
+ compaction.summary,
629
+ `covered_messages: ${compaction.coveredMessageIds.length}`,
630
+ `created_at: ${new Date(compaction.createdAtMs).toISOString()}`
631
+ ].join(" ")
632
+ );
633
+ }
634
+ lines.push("</thread-compactions>");
635
+ lines.push("");
636
+ }
637
+ lines.push("<thread-transcript>");
638
+ for (const message of conversation.messages) {
639
+ const displayName = message.author?.fullName || message.author?.userName || message.role;
640
+ lines.push(`[${message.role}] ${displayName}: ${message.text}`);
641
+ }
642
+ lines.push("</thread-transcript>");
643
+ return lines.join("\n");
644
+ }
645
+ function serializeMessageForQueue(message) {
646
+ const candidate = message;
647
+ if (typeof candidate.toJSON === "function") {
648
+ return candidate.toJSON();
649
+ }
650
+ return {
651
+ _type: "chat:Message",
652
+ ...message
653
+ };
654
+ }
655
+ function serializeThreadForQueue(thread) {
656
+ const candidate = thread;
657
+ if (typeof candidate.toJSON === "function") {
658
+ return candidate.toJSON();
659
+ }
660
+ return {
661
+ _type: "chat:Thread",
662
+ ...thread
663
+ };
664
+ }
665
+ function normalizeIncomingSlackThreadId(threadId, message) {
666
+ if (!threadId.startsWith("slack:")) {
667
+ return threadId;
668
+ }
669
+ if (!message || typeof message !== "object") {
670
+ return threadId;
671
+ }
672
+ const raw = message.raw;
673
+ if (!raw || typeof raw !== "object") {
674
+ return threadId;
675
+ }
676
+ const channelId = nonEmptyString(raw.channel);
677
+ const threadTs = nonEmptyString(raw.thread_ts) ?? nonEmptyString(raw.ts);
678
+ if (!channelId || !threadTs) {
679
+ return threadId;
680
+ }
681
+ return `slack:${channelId}:${threadTs}`;
682
+ }
683
+ function buildQueueIngressDedupKey(normalizedThreadId, messageId) {
684
+ return `${normalizedThreadId}:${messageId}`;
685
+ }
686
+ function determineThreadMessageKind(args) {
687
+ if (args.isSubscribed) {
688
+ return "subscribed_message";
689
+ }
690
+ if (args.isMention) {
691
+ return "new_mention";
692
+ }
693
+ return void 0;
694
+ }
695
+ var defaultQueueRoutingDeps = {
696
+ hasDedup: (key) => hasQueueIngressDedup(key),
697
+ markDedup: (key, ttlMs) => claimQueueIngressDedup(key, ttlMs),
698
+ getIsSubscribed: (threadId) => getStateAdapter().isSubscribed(threadId),
699
+ logInfo,
700
+ logWarn,
701
+ enqueueThreadMessage: async (payload, dedupKey) => {
702
+ const { enqueueThreadMessage } = await import("./client-3GAEMIQ3.js");
703
+ return await enqueueThreadMessage(payload, {
704
+ idempotencyKey: dedupKey
705
+ });
706
+ },
707
+ shouldReplyInSubscribedThread: async ({ message, normalizedThreadId, thread }) => {
708
+ const rawText = message.text;
709
+ const text = stripLeadingBotMention(rawText, {
710
+ stripLeadingSlackMentionToken: Boolean(message.isMention)
711
+ });
712
+ const conversation = coerceThreadConversationState(await thread.state);
713
+ const conversationContext = buildRoutingConversationContext(conversation);
714
+ const channelId = getChannelId(thread, message);
715
+ const runId = getRunId(thread, message);
716
+ return await shouldReplyInSubscribedThread({
717
+ rawText,
718
+ text,
719
+ conversationContext,
720
+ hasAttachments: message.attachments.length > 0,
721
+ isExplicitMention: Boolean(message.isMention),
722
+ context: {
723
+ threadId: normalizedThreadId,
724
+ requesterId: message.author.userId,
725
+ channelId,
726
+ runId
727
+ }
728
+ });
729
+ },
730
+ addProcessingReaction: async ({ channelId, timestamp }) => {
731
+ const { addReactionToMessage } = await import("./channel-HJO33DGJ.js");
732
+ await addReactionToMessage({
733
+ channelId,
734
+ timestamp,
735
+ emoji: "eyes"
736
+ });
737
+ },
738
+ removeProcessingReaction: async ({ channelId, timestamp }) => {
739
+ const { removeReactionFromMessage } = await import("./channel-HJO33DGJ.js");
740
+ await removeReactionFromMessage({
741
+ channelId,
742
+ timestamp,
743
+ emoji: "eyes"
744
+ });
745
+ }
746
+ };
747
+ async function routeIncomingMessageToQueue(args) {
748
+ const deps = args.deps ?? defaultQueueRoutingDeps;
749
+ const { adapter, runtime } = args;
750
+ const message = args.message;
751
+ if (!message || typeof message !== "object") {
752
+ return "ignored_non_object";
753
+ }
754
+ const normalizedThreadId = normalizeIncomingSlackThreadId(args.threadId, message);
755
+ if ("threadId" in message) {
756
+ message.threadId = normalizedThreadId;
757
+ }
758
+ const typedMessage = message;
759
+ if (typedMessage.author?.isMe) {
760
+ return "ignored_self_message";
761
+ }
762
+ const messageId = nonEmptyString(typedMessage.id);
763
+ if (!messageId) {
764
+ return "ignored_missing_message_id";
765
+ }
766
+ const isSubscribed = await deps.getIsSubscribed(normalizedThreadId);
767
+ const isMention = Boolean(typedMessage.isMention || runtime.detectMention?.(adapter, message));
768
+ const kind = determineThreadMessageKind({
769
+ isSubscribed,
770
+ isMention
771
+ });
772
+ if (!kind) {
773
+ return "ignored_unsubscribed_non_mention";
774
+ }
775
+ const dedupKey = buildQueueIngressDedupKey(normalizedThreadId, messageId);
776
+ const alreadyDeduped = await deps.hasDedup(dedupKey);
777
+ if (alreadyDeduped) {
778
+ deps.logInfo(
779
+ "queue_ingress_dedup_hit",
780
+ {
781
+ slackThreadId: normalizedThreadId,
782
+ slackUserId: message.author.userId
783
+ },
784
+ {
785
+ "messaging.message.id": messageId,
786
+ "app.queue.message_kind": kind,
787
+ "app.queue.dedup_key": dedupKey,
788
+ "app.queue.dedup_outcome": "duplicate"
789
+ },
790
+ "Skipping duplicate incoming message before queue enqueue"
791
+ );
792
+ return "ignored_duplicate";
793
+ }
794
+ const thread = await runtime.createThread(adapter, normalizedThreadId, message, isSubscribed);
795
+ const serializedMessage = serializeMessageForQueue(message);
796
+ const serializedThread = serializeThreadForQueue(thread);
797
+ let payloadKind = kind;
798
+ if (kind === "subscribed_message" && !isMention) {
799
+ const decision = await deps.shouldReplyInSubscribedThread({
800
+ message,
801
+ normalizedThreadId,
802
+ thread
803
+ });
804
+ if (!decision.shouldReply) {
805
+ return "ignored_passive_no_reply";
806
+ }
807
+ payloadKind = "subscribed_reply";
808
+ }
809
+ const payload = {
810
+ dedupKey,
811
+ kind: payloadKind,
812
+ message: serializedMessage,
813
+ normalizedThreadId,
814
+ thread: serializedThread
815
+ };
816
+ await withContext(
817
+ {
818
+ slackThreadId: normalizedThreadId,
819
+ slackChannelId: thread.channelId,
820
+ slackUserId: message.author.userId
821
+ },
822
+ async () => {
823
+ let processingReactionAdded = false;
824
+ let queueMessageId;
825
+ try {
826
+ await deps.addProcessingReaction({
827
+ channelId: thread.channelId,
828
+ timestamp: messageId
829
+ });
830
+ processingReactionAdded = true;
831
+ } catch (error) {
832
+ const errorMessage = error instanceof Error ? error.message : String(error);
833
+ deps.logWarn(
834
+ "queue_ingress_reaction_add_failed",
835
+ {},
836
+ {
837
+ "messaging.message.id": messageId,
838
+ "app.queue.message_kind": payloadKind,
839
+ "error.message": errorMessage
840
+ },
841
+ "Failed to add ingress processing reaction"
842
+ );
843
+ }
844
+ try {
845
+ await withSpan(
846
+ "queue.enqueue_message",
847
+ "queue.enqueue_message",
848
+ {
849
+ slackThreadId: normalizedThreadId,
850
+ slackChannelId: thread.channelId,
851
+ slackUserId: message.author.userId
852
+ },
853
+ async () => {
854
+ queueMessageId = await deps.enqueueThreadMessage(payload, dedupKey);
855
+ if (queueMessageId) {
856
+ setSpanAttributes({
857
+ "app.queue.message_id": queueMessageId
858
+ });
859
+ }
860
+ },
861
+ {
862
+ "messaging.message.id": messageId,
863
+ "app.queue.message_kind": payloadKind
864
+ }
865
+ );
866
+ } catch (error) {
867
+ if (processingReactionAdded) {
868
+ try {
869
+ await deps.removeProcessingReaction({
870
+ channelId: thread.channelId,
871
+ timestamp: messageId
872
+ });
873
+ } catch (cleanupError) {
874
+ const cleanupErrorMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
875
+ deps.logWarn(
876
+ "queue_ingress_reaction_cleanup_failed",
877
+ {},
878
+ {
879
+ "messaging.message.id": messageId,
880
+ "app.queue.message_kind": payloadKind,
881
+ "error.message": cleanupErrorMessage
882
+ },
883
+ "Failed to remove ingress processing reaction after enqueue failure"
884
+ );
885
+ }
886
+ }
887
+ throw error;
888
+ }
889
+ deps.logInfo(
890
+ "queue_ingress_enqueued",
891
+ {},
892
+ {
893
+ "messaging.message.id": messageId,
894
+ "app.queue.message_kind": payloadKind,
895
+ "app.queue.dedup_key": dedupKey,
896
+ "app.queue.dedup_outcome": "primary",
897
+ ...queueMessageId ? { "app.queue.message_id": queueMessageId } : {}
898
+ },
899
+ "Routing incoming message to queue"
900
+ );
901
+ const marked = await deps.markDedup(dedupKey, QUEUE_INGRESS_DEDUP_TTL_MS);
902
+ if (!marked) {
903
+ deps.logInfo(
904
+ "queue_ingress_dedup_mark_failed",
905
+ {},
906
+ {
907
+ "messaging.message.id": messageId,
908
+ "app.queue.message_kind": payloadKind,
909
+ "app.queue.dedup_key": dedupKey
910
+ },
911
+ "Queue ingress dedup state write failed after enqueue"
912
+ );
913
+ }
914
+ }
915
+ );
916
+ return "routed";
917
+ }
918
+ function scheduleBackgroundWork(options, run) {
919
+ if (!options?.waitUntil) {
920
+ throw new Error("Chat background processing requires waitUntil");
921
+ }
922
+ options.waitUntil(run);
923
+ return;
924
+ }
925
+ function installChatBackgroundPatch() {
926
+ const target = Chat.prototype;
927
+ if (target[PATCH_FLAG]) {
928
+ return;
929
+ }
930
+ target[PATCH_FLAG] = true;
931
+ const chatProto = Chat.prototype;
932
+ chatProto.processMessage = function processMessage(adapter, threadId, messageOrFactory, options) {
933
+ const run = async () => {
934
+ try {
935
+ const message = typeof messageOrFactory === "function" ? await messageOrFactory() : messageOrFactory;
936
+ const result = await routeIncomingMessageToQueue({
937
+ adapter,
938
+ threadId,
939
+ message,
940
+ runtime: {
941
+ createThread: this.createThread.bind(this),
942
+ detectMention: this.detectMention?.bind(this)
943
+ }
944
+ });
945
+ if (result === "ignored_missing_message_id") {
946
+ const normalizedThreadId = normalizeIncomingSlackThreadId(threadId, message);
947
+ this.logger?.error?.("Message processing error", {
948
+ threadId: normalizedThreadId,
949
+ reason: "missing_message_id"
950
+ });
951
+ }
952
+ } catch (err) {
953
+ this.logger?.error?.("Message processing error", { error: err, threadId });
954
+ }
955
+ };
956
+ scheduleBackgroundWork(options, run);
957
+ };
958
+ chatProto.processReaction = function processReaction(event, options) {
959
+ const run = async () => {
960
+ try {
961
+ await this.handleReactionEvent(event);
962
+ } catch (err) {
963
+ this.logger?.error?.("Reaction processing error", {
964
+ error: err,
965
+ emoji: event.emoji,
966
+ messageId: event.messageId
967
+ });
968
+ }
969
+ };
970
+ scheduleBackgroundWork(options, run);
971
+ };
972
+ chatProto.processAction = function processAction(event, options) {
973
+ const run = async () => {
974
+ try {
975
+ await this.handleActionEvent(event);
976
+ } catch (err) {
977
+ this.logger?.error?.("Action processing error", {
978
+ error: err,
979
+ actionId: event.actionId,
980
+ messageId: event.messageId
981
+ });
982
+ }
983
+ };
984
+ scheduleBackgroundWork(options, run);
985
+ };
986
+ chatProto.processModalClose = function processModalClose(event, contextId, options) {
987
+ const run = async () => {
988
+ try {
989
+ const { relatedThread, relatedMessage, relatedChannel } = await this.retrieveModalContext(event.adapter.name, contextId);
990
+ const fullEvent = { ...event, relatedThread, relatedMessage, relatedChannel };
991
+ for (const { callbackIds, handler } of this.modalCloseHandlers) {
992
+ if (callbackIds.length === 0 || callbackIds.includes(event.callbackId)) {
993
+ await handler(fullEvent);
994
+ }
995
+ }
996
+ } catch (err) {
997
+ this.logger?.error?.("Modal close handler error", {
998
+ error: err,
999
+ callbackId: event.callbackId
1000
+ });
1001
+ }
1002
+ };
1003
+ scheduleBackgroundWork(options, run);
1004
+ };
1005
+ chatProto.processSlashCommand = function processSlashCommand(event, options) {
1006
+ const run = async () => {
1007
+ try {
1008
+ await this.handleSlashCommandEvent(event);
1009
+ } catch (err) {
1010
+ this.logger?.error?.("Slash command processing error", {
1011
+ error: err,
1012
+ command: event.command,
1013
+ text: event.text
1014
+ });
1015
+ }
1016
+ };
1017
+ scheduleBackgroundWork(options, run);
1018
+ };
1019
+ chatProto.processAssistantThreadStarted = function processAssistantThreadStarted(event, options) {
1020
+ const run = async () => {
1021
+ try {
1022
+ for (const handler of this.assistantThreadStartedHandlers) {
1023
+ await handler(event);
1024
+ }
1025
+ } catch (err) {
1026
+ this.logger?.error?.("Assistant thread started handler error", {
1027
+ error: err,
1028
+ threadId: event.threadId
1029
+ });
1030
+ }
1031
+ };
1032
+ scheduleBackgroundWork(options, run);
1033
+ };
1034
+ chatProto.processAssistantContextChanged = function processAssistantContextChanged(event, options) {
1035
+ const run = async () => {
1036
+ try {
1037
+ for (const handler of this.assistantContextChangedHandlers) {
1038
+ await handler(event);
1039
+ }
1040
+ } catch (err) {
1041
+ this.logger?.error?.("Assistant context changed handler error", {
1042
+ error: err,
1043
+ threadId: event.threadId
1044
+ });
1045
+ }
1046
+ };
1047
+ scheduleBackgroundWork(options, run);
1048
+ };
1049
+ chatProto.processAppHomeOpened = function processAppHomeOpened(event, options) {
1050
+ const run = async () => {
1051
+ try {
1052
+ for (const handler of this.appHomeOpenedHandlers) {
1053
+ await handler(event);
1054
+ }
1055
+ } catch (err) {
1056
+ this.logger?.error?.("App home opened handler error", {
1057
+ error: err,
1058
+ userId: event.userId
1059
+ });
1060
+ }
1061
+ };
1062
+ scheduleBackgroundWork(options, run);
1063
+ };
1064
+ }
1065
+ installChatBackgroundPatch();
1066
+
1067
+ // src/chat/app-runtime.ts
1068
+ function isExplicitMentionDecision(reason) {
1069
+ return reason === "explicit mention" || reason === "explicit_mention" || reason.startsWith("explicit_mention:");
1070
+ }
1071
+ function buildLogContext(deps, args) {
1072
+ return {
1073
+ slackThreadId: args.threadId,
1074
+ slackUserId: args.requesterId,
1075
+ slackUserName: args.requesterUserName,
1076
+ slackChannelId: args.channelId,
1077
+ runId: args.runId,
1078
+ assistantUserName: deps.assistantUserName,
1079
+ modelId: deps.modelId
1080
+ };
1081
+ }
1082
+ function createAppSlackRuntime(deps) {
1083
+ const logContext = (args) => buildLogContext(deps, args);
1084
+ return {
1085
+ async handleNewMention(thread, message, hooks) {
1086
+ try {
1087
+ const threadId = deps.getThreadId(thread, message);
1088
+ const channelId = deps.getChannelId(thread, message);
1089
+ const runId = deps.getRunId(thread, message);
1090
+ const context = logContext({
1091
+ threadId,
1092
+ channelId,
1093
+ requesterId: message.author.userId,
1094
+ requesterUserName: message.author.userName,
1095
+ runId
1096
+ });
1097
+ await deps.withSpan(
1098
+ "chat.turn",
1099
+ "chat.turn",
1100
+ context,
1101
+ async () => {
1102
+ await thread.subscribe();
1103
+ await deps.replyToThread(thread, message, {
1104
+ explicitMention: true,
1105
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost
1106
+ });
1107
+ }
1108
+ );
1109
+ } catch (error) {
1110
+ const errorContext = logContext({
1111
+ threadId: deps.getThreadId(thread, message),
1112
+ requesterId: message.author.userId,
1113
+ requesterUserName: message.author.userName,
1114
+ channelId: deps.getChannelId(thread, message),
1115
+ runId: deps.getRunId(thread, message)
1116
+ });
1117
+ if (isRetryableTurnError(error)) {
1118
+ deps.logException(
1119
+ error,
1120
+ "mention_handler_retryable_failure",
1121
+ errorContext,
1122
+ { "app.turn.retryable_reason": error.reason },
1123
+ "onNewMention failed with retryable error"
1124
+ );
1125
+ throw error;
1126
+ }
1127
+ deps.logException(
1128
+ error,
1129
+ "mention_handler_failed",
1130
+ errorContext,
1131
+ {},
1132
+ "onNewMention failed"
1133
+ );
1134
+ const errorMessage = error instanceof Error ? error.message : String(error);
1135
+ await hooks?.beforeFirstResponsePost?.();
1136
+ await thread.post(`Error: ${errorMessage}`);
1137
+ }
1138
+ },
1139
+ async handleSubscribedMessage(thread, message, hooks) {
1140
+ try {
1141
+ const threadId = deps.getThreadId(thread, message);
1142
+ const channelId = deps.getChannelId(thread, message);
1143
+ const runId = deps.getRunId(thread, message);
1144
+ const rawUserText = message.text;
1145
+ const userText = deps.stripLeadingBotMention(rawUserText, {
1146
+ stripLeadingSlackMentionToken: Boolean(message.isMention)
1147
+ });
1148
+ const context = {
1149
+ threadId,
1150
+ requesterId: message.author.userId,
1151
+ channelId,
1152
+ runId
1153
+ };
1154
+ const preparedState = await deps.prepareTurnState({
1155
+ thread,
1156
+ message,
1157
+ userText,
1158
+ explicitMention: Boolean(message.isMention),
1159
+ context
1160
+ });
1161
+ await deps.persistPreparedState({
1162
+ thread,
1163
+ preparedState
1164
+ });
1165
+ const decision = hooks?.preApprovedReply ? {
1166
+ shouldReply: true,
1167
+ reason: "pre_approved_reply"
1168
+ } : await deps.shouldReplyInSubscribedThread({
1169
+ rawText: rawUserText,
1170
+ text: userText,
1171
+ conversationContext: deps.getPreparedConversationContext(preparedState),
1172
+ hasAttachments: message.attachments.length > 0,
1173
+ isExplicitMention: Boolean(message.isMention),
1174
+ context
1175
+ });
1176
+ if (!decision.shouldReply) {
1177
+ deps.logWarn(
1178
+ "subscribed_message_reply_skipped",
1179
+ logContext({
1180
+ threadId,
1181
+ requesterId: message.author.userId,
1182
+ requesterUserName: message.author.userName,
1183
+ channelId,
1184
+ runId
1185
+ }),
1186
+ {
1187
+ "app.decision.reason": decision.reason
1188
+ },
1189
+ "Skipping subscribed message reply"
1190
+ );
1191
+ await deps.onSubscribedMessageSkipped({
1192
+ thread,
1193
+ message,
1194
+ preparedState,
1195
+ decision,
1196
+ completedAtMs: deps.now()
1197
+ });
1198
+ return;
1199
+ }
1200
+ await deps.withSpan(
1201
+ "chat.turn",
1202
+ "chat.turn",
1203
+ logContext({
1204
+ threadId,
1205
+ requesterId: message.author.userId,
1206
+ requesterUserName: message.author.userName,
1207
+ channelId,
1208
+ runId
1209
+ }),
1210
+ async () => {
1211
+ await deps.replyToThread(thread, message, {
1212
+ explicitMention: isExplicitMentionDecision(decision.reason),
1213
+ preparedState,
1214
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost
1215
+ });
1216
+ }
1217
+ );
1218
+ } catch (error) {
1219
+ const errorContext = logContext({
1220
+ threadId: deps.getThreadId(thread, message),
1221
+ requesterId: message.author.userId,
1222
+ requesterUserName: message.author.userName,
1223
+ channelId: deps.getChannelId(thread, message),
1224
+ runId: deps.getRunId(thread, message)
1225
+ });
1226
+ if (isRetryableTurnError(error)) {
1227
+ deps.logException(
1228
+ error,
1229
+ "subscribed_message_handler_retryable_failure",
1230
+ errorContext,
1231
+ { "app.turn.retryable_reason": error.reason },
1232
+ "onSubscribedMessage failed with retryable error"
1233
+ );
1234
+ throw error;
1235
+ }
1236
+ deps.logException(
1237
+ error,
1238
+ "subscribed_message_handler_failed",
1239
+ errorContext,
1240
+ {},
1241
+ "onSubscribedMessage failed"
1242
+ );
1243
+ const errorMessage = error instanceof Error ? error.message : String(error);
1244
+ await hooks?.beforeFirstResponsePost?.();
1245
+ await thread.post(`Error: ${errorMessage}`);
1246
+ }
1247
+ },
1248
+ async handleAssistantThreadStarted(event) {
1249
+ try {
1250
+ await deps.initializeAssistantThread({
1251
+ threadId: event.threadId,
1252
+ channelId: event.channelId,
1253
+ threadTs: event.threadTs,
1254
+ sourceChannelId: event.context?.channelId
1255
+ });
1256
+ } catch (error) {
1257
+ deps.logException(
1258
+ error,
1259
+ "assistant_thread_started_handler_failed",
1260
+ {
1261
+ slackThreadId: event.threadId,
1262
+ slackUserId: event.userId,
1263
+ slackChannelId: event.channelId,
1264
+ assistantUserName: deps.assistantUserName,
1265
+ modelId: deps.modelId
1266
+ },
1267
+ {},
1268
+ "onAssistantThreadStarted failed"
1269
+ );
1270
+ }
1271
+ },
1272
+ async handleAssistantContextChanged(event) {
1273
+ try {
1274
+ await deps.initializeAssistantThread({
1275
+ threadId: event.threadId,
1276
+ channelId: event.channelId,
1277
+ threadTs: event.threadTs,
1278
+ sourceChannelId: event.context?.channelId
1279
+ });
1280
+ } catch (error) {
1281
+ deps.logException(
1282
+ error,
1283
+ "assistant_context_changed_handler_failed",
1284
+ {
1285
+ slackThreadId: event.threadId,
1286
+ slackUserId: event.userId,
1287
+ slackChannelId: event.channelId,
1288
+ assistantUserName: deps.assistantUserName,
1289
+ modelId: deps.modelId
1290
+ },
1291
+ {},
1292
+ "onAssistantContextChanged failed"
1293
+ );
1294
+ }
1295
+ }
1296
+ };
1297
+ }
1298
+
1299
+ // src/chat/slash-command.ts
1300
+ function providerLabel(provider) {
1301
+ return provider.charAt(0).toUpperCase() + provider.slice(1);
1302
+ }
1303
+ async function postEphemeral(event, text) {
1304
+ await event.channel.postEphemeral(event.user, text, { fallbackToDM: false });
1305
+ }
1306
+ async function handleLink(event, provider) {
1307
+ if (!isPluginProvider(provider)) {
1308
+ await postEphemeral(event, `Unknown provider: \`${provider}\``);
1309
+ return;
1310
+ }
1311
+ if (!getOAuthProviderConfig(provider)) {
1312
+ await postEphemeral(
1313
+ event,
1314
+ `${providerLabel(provider)} uses app-level authentication and doesn't require account linking.`
1315
+ );
1316
+ return;
1317
+ }
1318
+ const raw = event.raw;
1319
+ const result = await startOAuthFlow(provider, {
1320
+ requesterId: event.user.userId,
1321
+ channelId: raw.channel_id
1322
+ });
1323
+ if (!result.ok) {
1324
+ await postEphemeral(event, `Failed to start linking: ${result.error}`);
1325
+ return;
1326
+ }
1327
+ if (result.delivery === "fallback_dm") {
1328
+ await postEphemeral(event, `Check your DMs for a ${providerLabel(provider)} authorization link.`);
1329
+ } else if (result.delivery === false) {
1330
+ await postEphemeral(
1331
+ event,
1332
+ "I wasn't able to send you a private authorization link. Please try again in a direct message."
1333
+ );
1334
+ }
1335
+ }
1336
+ async function handleUnlink(event, provider) {
1337
+ if (!isPluginProvider(provider)) {
1338
+ await postEphemeral(event, `Unknown provider: \`${provider}\``);
1339
+ return;
1340
+ }
1341
+ if (!getOAuthProviderConfig(provider)) {
1342
+ await postEphemeral(
1343
+ event,
1344
+ `${providerLabel(provider)} uses app-level authentication and can't be unlinked.`
1345
+ );
1346
+ return;
1347
+ }
1348
+ const tokenStore = getUserTokenStore();
1349
+ await tokenStore.delete(event.user.userId, provider);
1350
+ logInfo(
1351
+ "slash_command_unlink",
1352
+ { slackUserId: event.user.userId },
1353
+ { "app.credential.provider": provider },
1354
+ `Unlinked ${providerLabel(provider)} account via /jr slash command`
1355
+ );
1356
+ await postEphemeral(event, `Your ${providerLabel(provider)} account has been unlinked.`);
1357
+ }
1358
+ async function handleSlashCommand(event) {
1359
+ const [subcommand, provider, ...rest] = event.text.trim().split(/\s+/);
1360
+ if (!subcommand || !["link", "unlink"].includes(subcommand)) {
1361
+ await postEphemeral(event, "Usage: `/jr link <provider>` or `/jr unlink <provider>`");
1362
+ return;
1363
+ }
1364
+ if (!provider || rest.length > 0) {
1365
+ await postEphemeral(event, `Usage: \`/jr ${subcommand} <provider>\``);
1366
+ return;
1367
+ }
1368
+ const normalized = provider.toLowerCase();
1369
+ if (subcommand === "link") {
1370
+ await handleLink(event, normalized);
1371
+ } else {
1372
+ await handleUnlink(event, normalized);
1373
+ }
1374
+ }
1375
+
1376
+ // src/chat/bootstrap/register-handlers.ts
1377
+ function registerBotHandlers(args) {
1378
+ const { bot: bot2, appSlackRuntime: appSlackRuntime2 } = args;
1379
+ bot2.onNewMention(appSlackRuntime2.handleNewMention);
1380
+ bot2.onSubscribedMessage(appSlackRuntime2.handleSubscribedMessage);
1381
+ bot2.onAssistantThreadStarted(
1382
+ (event) => appSlackRuntime2.handleAssistantThreadStarted(event)
1383
+ );
1384
+ bot2.onAssistantContextChanged(
1385
+ (event) => appSlackRuntime2.handleAssistantContextChanged(event)
1386
+ );
1387
+ bot2.onSlashCommand(
1388
+ "/jr",
1389
+ (event) => withSpan(
1390
+ "chat.slash_command",
1391
+ "chat.slash_command",
1392
+ { slackUserId: event.user.userId },
1393
+ async () => {
1394
+ try {
1395
+ await handleSlashCommand(event);
1396
+ } catch (error) {
1397
+ logException(error, "slash_command_failed", { slackUserId: event.user.userId });
1398
+ throw error;
1399
+ }
1400
+ }
1401
+ )
1402
+ );
1403
+ bot2.onAppHomeOpened(
1404
+ (event) => withSpan(
1405
+ "chat.app_home_opened",
1406
+ "chat.app_home_opened",
1407
+ { slackUserId: event.userId },
1408
+ async () => {
1409
+ try {
1410
+ await publishAppHomeView(getSlackClient(), event.userId, getUserTokenStore());
1411
+ } catch (error) {
1412
+ logException(error, "app_home_opened_failed", { slackUserId: event.userId });
1413
+ }
1414
+ }
1415
+ )
1416
+ );
1417
+ bot2.onAction("app_home_disconnect", async (event) => {
1418
+ const provider = event.value;
1419
+ if (!provider) return;
1420
+ const userId = event.user.userId;
1421
+ await withSpan(
1422
+ "chat.app_home_disconnect",
1423
+ "chat.app_home_disconnect",
1424
+ { slackUserId: userId },
1425
+ async () => {
1426
+ try {
1427
+ await getUserTokenStore().delete(userId, provider);
1428
+ await publishAppHomeView(getSlackClient(), userId, getUserTokenStore());
1429
+ } catch (error) {
1430
+ logException(error, "app_home_disconnect_failed", { slackUserId: userId }, {
1431
+ "app.credential.provider": provider
1432
+ });
1433
+ }
1434
+ }
1435
+ );
1436
+ });
1437
+ }
1438
+
1439
+ // src/chat/runtime/assistant-lifecycle.ts
1440
+ import { ThreadImpl } from "chat";
1441
+
1442
+ // src/chat/slack-actions/types.ts
1443
+ function coerceThreadArtifactsState(value) {
1444
+ if (!value || typeof value !== "object") {
1445
+ return {};
1446
+ }
1447
+ const raw = value;
1448
+ const artifacts = raw.artifacts ?? {};
1449
+ const listColumnMap = artifacts.listColumnMap ?? {};
1450
+ const recentCanvases = [];
1451
+ if (Array.isArray(artifacts.recentCanvases)) {
1452
+ for (const entry of artifacts.recentCanvases) {
1453
+ if (!entry || typeof entry !== "object") {
1454
+ continue;
1455
+ }
1456
+ const candidate = entry;
1457
+ if (typeof candidate.id !== "string" || candidate.id.trim().length === 0) {
1458
+ continue;
1459
+ }
1460
+ recentCanvases.push({
1461
+ id: candidate.id,
1462
+ title: typeof candidate.title === "string" ? candidate.title : void 0,
1463
+ url: typeof candidate.url === "string" ? candidate.url : void 0,
1464
+ createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : void 0
1465
+ });
1466
+ }
1467
+ }
1468
+ return {
1469
+ assistantContextChannelId: typeof artifacts.assistantContextChannelId === "string" ? artifacts.assistantContextChannelId : void 0,
1470
+ lastCanvasId: typeof artifacts.lastCanvasId === "string" ? artifacts.lastCanvasId : void 0,
1471
+ lastCanvasUrl: typeof artifacts.lastCanvasUrl === "string" ? artifacts.lastCanvasUrl : void 0,
1472
+ recentCanvases,
1473
+ lastListId: typeof artifacts.lastListId === "string" ? artifacts.lastListId : void 0,
1474
+ lastListUrl: typeof artifacts.lastListUrl === "string" ? artifacts.lastListUrl : void 0,
1475
+ listColumnMap: {
1476
+ titleColumnId: typeof listColumnMap.titleColumnId === "string" ? listColumnMap.titleColumnId : void 0,
1477
+ completedColumnId: typeof listColumnMap.completedColumnId === "string" ? listColumnMap.completedColumnId : void 0,
1478
+ assigneeColumnId: typeof listColumnMap.assigneeColumnId === "string" ? listColumnMap.assigneeColumnId : void 0,
1479
+ dueDateColumnId: typeof listColumnMap.dueDateColumnId === "string" ? listColumnMap.dueDateColumnId : void 0
1480
+ },
1481
+ updatedAt: typeof artifacts.updatedAt === "string" ? artifacts.updatedAt : void 0
1482
+ };
1483
+ }
1484
+ function buildArtifactStatePatch(patch) {
1485
+ return {
1486
+ artifacts: {
1487
+ ...patch,
1488
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1489
+ }
1490
+ };
1491
+ }
1492
+
1493
+ // src/chat/configuration/validation.ts
1494
+ var CONFIG_KEY_RE = /^[a-z0-9]+(?:\.[a-z0-9-]+)+$/;
1495
+ var SECRET_KEY_RE = /(?:^|[_.-])(token|secret|password|passphrase|api[-_]?key|private[-_]?key|credential|auth)(?:$|[_.-])/i;
1496
+ var SECRET_VALUE_PATTERNS = [
1497
+ /\bBearer\s+[A-Za-z0-9._-]{16,}\b/i,
1498
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
1499
+ /\b(?:ghp|ghs|github_pat)_[A-Za-z0-9_]{20,}\b/,
1500
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/i,
1501
+ /\bsk-[A-Za-z0-9]{20,}\b/,
1502
+ /\bAIza[0-9A-Za-z\-_]{30,}\b/,
1503
+ /\bAKIA[0-9A-Z]{16}\b/,
1504
+ /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/
1505
+ ];
1506
+ function validateConfigKey(key) {
1507
+ const trimmed = key.trim();
1508
+ if (!trimmed) {
1509
+ return "Configuration key must not be empty";
1510
+ }
1511
+ if (!CONFIG_KEY_RE.test(trimmed)) {
1512
+ return `Invalid configuration key "${key}"; expected dotted lowercase namespace (for example "github.repo")`;
1513
+ }
1514
+ if (SECRET_KEY_RE.test(trimmed)) {
1515
+ return `Configuration key "${key}" appears to be secret-related and is not allowed`;
1516
+ }
1517
+ return void 0;
1518
+ }
1519
+ function collectStringValues(value, output, depth = 0) {
1520
+ if (depth > 5) {
1521
+ return;
1522
+ }
1523
+ if (typeof value === "string") {
1524
+ output.push(value);
1525
+ return;
1526
+ }
1527
+ if (Array.isArray(value)) {
1528
+ for (const item of value) {
1529
+ collectStringValues(item, output, depth + 1);
1530
+ }
1531
+ return;
1532
+ }
1533
+ if (value && typeof value === "object") {
1534
+ for (const [key, nested] of Object.entries(value)) {
1535
+ output.push(key);
1536
+ collectStringValues(nested, output, depth + 1);
1537
+ }
1538
+ }
1539
+ }
1540
+ function validateConfigValue(value) {
1541
+ const stringValues = [];
1542
+ collectStringValues(value, stringValues);
1543
+ for (const text of stringValues) {
1544
+ for (const pattern of SECRET_VALUE_PATTERNS) {
1545
+ if (pattern.test(text)) {
1546
+ return "Configuration value appears to contain secret material and is not allowed";
1547
+ }
1548
+ }
1549
+ }
1550
+ return void 0;
1551
+ }
1552
+
1553
+ // src/chat/configuration/service.ts
1554
+ function isRecord2(value) {
1555
+ return Boolean(value) && typeof value === "object";
1556
+ }
1557
+ function toOptionalString4(value) {
1558
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
1559
+ }
1560
+ function defaultState() {
1561
+ return {
1562
+ schemaVersion: 1,
1563
+ entries: {}
1564
+ };
1565
+ }
1566
+ function sanitizeEntry(value) {
1567
+ if (!isRecord2(value)) {
1568
+ return void 0;
1569
+ }
1570
+ const key = toOptionalString4(value.key);
1571
+ if (!key) {
1572
+ return void 0;
1573
+ }
1574
+ if (validateConfigKey(key)) {
1575
+ return void 0;
1576
+ }
1577
+ const updatedAt = toOptionalString4(value.updatedAt);
1578
+ if (!updatedAt) {
1579
+ return void 0;
1580
+ }
1581
+ if (value.scope !== "channel" && value.scope !== "conversation") {
1582
+ return void 0;
1583
+ }
1584
+ const scope = "conversation";
1585
+ return {
1586
+ key,
1587
+ value: value.value,
1588
+ scope,
1589
+ updatedAt,
1590
+ updatedBy: toOptionalString4(value.updatedBy),
1591
+ source: toOptionalString4(value.source),
1592
+ expiresAt: toOptionalString4(value.expiresAt)
1593
+ };
1594
+ }
1595
+ function coerceState(raw) {
1596
+ if (!isRecord2(raw)) {
1597
+ return defaultState();
1598
+ }
1599
+ const rawConfig = isRecord2(raw.configuration) ? raw.configuration : {};
1600
+ const rawEntries = isRecord2(rawConfig.entries) ? rawConfig.entries : {};
1601
+ const entries = {};
1602
+ for (const [key, value] of Object.entries(rawEntries)) {
1603
+ const entry = sanitizeEntry(value);
1604
+ if (!entry) {
1605
+ continue;
1606
+ }
1607
+ entries[key] = entry;
1608
+ }
1609
+ return {
1610
+ schemaVersion: 1,
1611
+ entries
1612
+ };
1613
+ }
1614
+ function createChannelConfigurationService(storage) {
1615
+ const getState = async () => {
1616
+ const loaded = await storage.load();
1617
+ return coerceState(loaded);
1618
+ };
1619
+ const saveState = async (state) => {
1620
+ await storage.save({
1621
+ schemaVersion: 1,
1622
+ entries: state.entries
1623
+ });
1624
+ };
1625
+ const get = async (key) => {
1626
+ const normalizedKey = key.trim();
1627
+ const state = await getState();
1628
+ return state.entries[normalizedKey];
1629
+ };
1630
+ const set = async (input) => {
1631
+ const normalizedKey = input.key.trim();
1632
+ const keyError = validateConfigKey(normalizedKey);
1633
+ if (keyError) {
1634
+ throw new Error(keyError);
1635
+ }
1636
+ const valueError = validateConfigValue(input.value);
1637
+ if (valueError) {
1638
+ throw new Error(valueError);
1639
+ }
1640
+ const state = await getState();
1641
+ const nextEntry = {
1642
+ key: normalizedKey,
1643
+ value: input.value,
1644
+ scope: "conversation",
1645
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1646
+ updatedBy: toOptionalString4(input.updatedBy),
1647
+ source: toOptionalString4(input.source),
1648
+ expiresAt: toOptionalString4(input.expiresAt)
1649
+ };
1650
+ state.entries[normalizedKey] = nextEntry;
1651
+ await saveState(state);
1652
+ return nextEntry;
1653
+ };
1654
+ const unset = async (key) => {
1655
+ const normalizedKey = key.trim();
1656
+ const state = await getState();
1657
+ if (!state.entries[normalizedKey]) {
1658
+ return false;
1659
+ }
1660
+ delete state.entries[normalizedKey];
1661
+ await saveState(state);
1662
+ return true;
1663
+ };
1664
+ const list = async (options = {}) => {
1665
+ const state = await getState();
1666
+ const prefix = options.prefix?.trim();
1667
+ return Object.values(state.entries).filter((entry) => prefix ? entry.key.startsWith(prefix) : true).sort((a, b) => a.key.localeCompare(b.key));
1668
+ };
1669
+ const resolve = async (key) => {
1670
+ const entry = await get(key);
1671
+ return entry?.value;
1672
+ };
1673
+ const resolveValues = async (options = {}) => {
1674
+ const keys = Array.isArray(options.keys) ? options.keys.map((entry) => entry.trim()).filter((entry) => entry.length > 0) : void 0;
1675
+ const entries = await list({
1676
+ ...options.prefix ? { prefix: options.prefix } : {}
1677
+ });
1678
+ const filtered = keys ? entries.filter((entry) => keys.includes(entry.key)) : entries;
1679
+ const resolved = {};
1680
+ for (const entry of filtered) {
1681
+ resolved[entry.key] = entry.value;
1682
+ }
1683
+ return resolved;
1684
+ };
1685
+ return {
1686
+ get,
1687
+ set,
1688
+ unset,
1689
+ list,
1690
+ resolve,
1691
+ resolveValues
1692
+ };
1693
+ }
1694
+
1695
+ // src/chat/runtime/thread-state.ts
1696
+ function mergeArtifactsState(artifacts, patch) {
1697
+ if (!patch) {
1698
+ return artifacts;
1699
+ }
1700
+ return {
1701
+ ...artifacts,
1702
+ ...patch,
1703
+ listColumnMap: {
1704
+ ...artifacts.listColumnMap,
1705
+ ...patch.listColumnMap
1706
+ }
1707
+ };
1708
+ }
1709
+ async function persistThreadState(thread, patch) {
1710
+ const payload = {};
1711
+ if (patch.artifacts) {
1712
+ Object.assign(payload, buildArtifactStatePatch(patch.artifacts));
1713
+ }
1714
+ if (patch.conversation) {
1715
+ Object.assign(payload, buildConversationStatePatch(patch.conversation));
1716
+ }
1717
+ if (patch.sandboxId) {
1718
+ payload.app_sandbox_id = patch.sandboxId;
1719
+ }
1720
+ if (Object.keys(payload).length === 0) {
1721
+ return;
1722
+ }
1723
+ await thread.setState(payload);
1724
+ }
1725
+ function getChannelConfigurationService(thread) {
1726
+ const channel = thread.channel;
1727
+ return createChannelConfigurationService({
1728
+ load: async () => channel.state,
1729
+ save: async (state) => {
1730
+ await channel.setState({
1731
+ configuration: state
1732
+ });
1733
+ }
1734
+ });
1735
+ }
1736
+
1737
+ // src/chat/runtime/assistant-lifecycle.ts
1738
+ async function initializeAssistantThread(event) {
1739
+ const slack = event.getSlackAdapter();
1740
+ await slack.setAssistantTitle(event.channelId, event.threadTs, "Junior");
1741
+ await slack.setSuggestedPrompts(event.channelId, event.threadTs, [
1742
+ { title: "Summarize thread", message: "Summarize the latest discussion in this thread." },
1743
+ { title: "Draft a reply", message: "Draft a concise reply I can send." },
1744
+ { title: "Generate image", message: "Generate an image based on this conversation." }
1745
+ ]);
1746
+ if (!event.sourceChannelId) {
1747
+ return;
1748
+ }
1749
+ const thread = ThreadImpl.fromJSON({
1750
+ _type: "chat:Thread",
1751
+ adapterName: "slack",
1752
+ channelId: event.channelId,
1753
+ id: event.threadId,
1754
+ isDM: event.channelId.startsWith("D")
1755
+ });
1756
+ const currentArtifacts = coerceThreadArtifactsState(await thread.state);
1757
+ const nextArtifacts = mergeArtifactsState(currentArtifacts, {
1758
+ assistantContextChannelId: event.sourceChannelId
1759
+ });
1760
+ await persistThreadState(thread, {
1761
+ artifacts: nextArtifacts
1762
+ });
1763
+ }
1764
+
1765
+ // src/chat/progress-reporter.ts
1766
+ var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
1767
+ var STATUS_MIN_VISIBLE_MS = 1200;
1768
+ function createProgressReporter(args) {
1769
+ const now = args.now ?? (() => Date.now());
1770
+ const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
1771
+ const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
1772
+ let active = false;
1773
+ let currentStatus = "";
1774
+ let lastStatusAt = 0;
1775
+ let pendingStatus = null;
1776
+ let pendingTimer = null;
1777
+ const postStatus = async (text) => {
1778
+ if (!args.channelId || !args.threadTs) {
1779
+ return;
1780
+ }
1781
+ currentStatus = text;
1782
+ lastStatusAt = now();
1783
+ try {
1784
+ await args.setAssistantStatus(args.channelId, args.threadTs, text, [text]);
1785
+ } catch {
1786
+ }
1787
+ };
1788
+ const clearPending = () => {
1789
+ if (pendingTimer) {
1790
+ clearTimer(pendingTimer);
1791
+ pendingTimer = null;
1792
+ }
1793
+ pendingStatus = null;
1794
+ };
1795
+ const flushPending = async () => {
1796
+ if (!active || !pendingStatus) {
1797
+ clearPending();
1798
+ return;
1799
+ }
1800
+ const next = pendingStatus;
1801
+ clearPending();
1802
+ if (next !== currentStatus) {
1803
+ await postStatus(next);
1804
+ }
1805
+ };
1806
+ return {
1807
+ async start() {
1808
+ active = true;
1809
+ clearPending();
1810
+ void postStatus("Thinking...");
1811
+ },
1812
+ async stop() {
1813
+ active = false;
1814
+ clearPending();
1815
+ },
1816
+ async setStatus(text) {
1817
+ const truncated = truncateStatusText(text);
1818
+ if (!active || !truncated || truncated === currentStatus || truncated === pendingStatus) {
1819
+ return;
1820
+ }
1821
+ const elapsed = now() - lastStatusAt;
1822
+ const waitMs = Math.max(
1823
+ STATUS_UPDATE_DEBOUNCE_MS - elapsed,
1824
+ STATUS_MIN_VISIBLE_MS - elapsed,
1825
+ 0
1826
+ );
1827
+ if (waitMs <= 0) {
1828
+ clearPending();
1829
+ void postStatus(truncated);
1830
+ return;
1831
+ }
1832
+ pendingStatus = truncated;
1833
+ if (pendingTimer) {
1834
+ return;
1835
+ }
1836
+ pendingTimer = setTimer(() => {
1837
+ pendingTimer = null;
1838
+ void flushPending();
1839
+ }, Math.max(1, waitMs));
1840
+ }
1841
+ };
1842
+ }
1843
+
1844
+ // src/chat/runtime/streaming.ts
1845
+ function createTextStreamBridge() {
1846
+ const queue = [];
1847
+ let ended = false;
1848
+ let wakeConsumer = null;
1849
+ const iterable = {
1850
+ async *[Symbol.asyncIterator]() {
1851
+ while (!ended || queue.length > 0) {
1852
+ if (queue.length > 0) {
1853
+ yield queue.shift();
1854
+ continue;
1855
+ }
1856
+ await new Promise((resolve) => {
1857
+ wakeConsumer = resolve;
1858
+ });
1859
+ }
1860
+ }
1861
+ };
1862
+ return {
1863
+ iterable,
1864
+ push(delta) {
1865
+ if (!delta || ended) {
1866
+ return;
1867
+ }
1868
+ queue.push(delta);
1869
+ const wake = wakeConsumer;
1870
+ wakeConsumer = null;
1871
+ wake?.();
1872
+ },
1873
+ end() {
1874
+ if (ended) {
1875
+ return;
1876
+ }
1877
+ ended = true;
1878
+ const wake = wakeConsumer;
1879
+ wakeConsumer = null;
1880
+ wake?.();
1881
+ }
1882
+ };
1883
+ }
1884
+ function createNormalizingStream(inner, normalize) {
1885
+ return {
1886
+ async *[Symbol.asyncIterator]() {
1887
+ let accumulated = "";
1888
+ let emitted = 0;
1889
+ for await (const chunk of inner) {
1890
+ accumulated += chunk;
1891
+ const lastNewline = accumulated.lastIndexOf("\n");
1892
+ if (lastNewline === -1) {
1893
+ const delta2 = accumulated.slice(emitted);
1894
+ if (delta2) {
1895
+ yield delta2;
1896
+ emitted = accumulated.length;
1897
+ }
1898
+ continue;
1899
+ }
1900
+ const stable = accumulated.slice(0, lastNewline + 1);
1901
+ const normalized = normalize(stable);
1902
+ const delta = normalized.slice(emitted);
1903
+ emitted = normalized.length;
1904
+ if (delta) yield delta;
1905
+ }
1906
+ if (accumulated) {
1907
+ const normalized = normalize(accumulated);
1908
+ const delta = normalized.slice(emitted);
1909
+ if (delta) yield delta;
1910
+ }
1911
+ }
1912
+ };
1913
+ }
1914
+
1915
+ // src/chat/services/conversation-memory.ts
1916
+ var CONTEXT_COMPACTION_TRIGGER_TOKENS = 9e3;
1917
+ var CONTEXT_COMPACTION_TARGET_TOKENS = 7e3;
1918
+ var CONTEXT_MIN_LIVE_MESSAGES = 12;
1919
+ var CONTEXT_COMPACTION_BATCH_SIZE = 24;
1920
+ var CONTEXT_MAX_COMPACTIONS = 16;
1921
+ var CONTEXT_MAX_MESSAGE_CHARS = 3200;
1922
+ var BACKFILL_MESSAGE_LIMIT = 80;
1923
+ function generateConversationId(prefix) {
1924
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
1925
+ }
1926
+ function normalizeConversationText(text) {
1927
+ return text.trim().replace(/\s+/g, " ").slice(0, CONTEXT_MAX_MESSAGE_CHARS);
1928
+ }
1929
+ function estimateTokenCount(text) {
1930
+ return Math.ceil(text.length / 4);
1931
+ }
1932
+ function buildImageContextSuffix(message, conversation) {
1933
+ const byFileId = conversation?.vision.byFileId;
1934
+ const imageFileIds = message.meta?.imageFileIds ?? [];
1935
+ if (!byFileId || imageFileIds.length === 0) {
1936
+ return "";
1937
+ }
1938
+ const summaries = imageFileIds.map((fileId) => byFileId[fileId]?.summary?.trim()).filter((summary) => Boolean(summary));
1939
+ if (summaries.length === 0) {
1940
+ return "";
1941
+ }
1942
+ return ` [image context: ${summaries.join(" | ")}]`;
1943
+ }
1944
+ function renderConversationMessageLine(message, conversation) {
1945
+ const displayName = message.author?.fullName || message.author?.userName || (message.role === "assistant" ? botConfig.userName : message.role);
1946
+ const markers = [];
1947
+ if (message.meta?.replied === false) {
1948
+ markers.push(`assistant skipped: ${message.meta?.skippedReason ?? "no-reply route"}`);
1949
+ }
1950
+ if (message.meta?.explicitMention) {
1951
+ markers.push("explicit mention");
1952
+ }
1953
+ const markerSuffix = markers.length > 0 ? ` (${markers.join("; ")})` : "";
1954
+ const imageContext = buildImageContextSuffix(message, conversation);
1955
+ return `[${message.role}] ${displayName}: ${message.text}${imageContext}${markerSuffix}`;
1956
+ }
1957
+ function updateConversationStats(conversation) {
1958
+ const contextText = buildConversationContext(conversation);
1959
+ conversation.stats.estimatedContextTokens = estimateTokenCount(contextText ?? "");
1960
+ conversation.stats.totalMessageCount = conversation.messages.length;
1961
+ conversation.stats.updatedAtMs = Date.now();
1962
+ }
1963
+ function upsertConversationMessage(conversation, message) {
1964
+ const existingIndex = conversation.messages.findIndex((entry) => entry.id === message.id);
1965
+ if (existingIndex >= 0) {
1966
+ conversation.messages[existingIndex] = {
1967
+ ...conversation.messages[existingIndex],
1968
+ ...message,
1969
+ meta: {
1970
+ ...conversation.messages[existingIndex]?.meta,
1971
+ ...message.meta
1972
+ }
1973
+ };
1974
+ updateConversationStats(conversation);
1975
+ return message.id;
1976
+ }
1977
+ conversation.messages.push(message);
1978
+ updateConversationStats(conversation);
1979
+ return message.id;
1980
+ }
1981
+ function markConversationMessage(conversation, messageId, patch) {
1982
+ if (!messageId) return;
1983
+ const messageIndex = conversation.messages.findIndex((entry) => entry.id === messageId);
1984
+ if (messageIndex < 0) return;
1985
+ const current = conversation.messages[messageIndex];
1986
+ conversation.messages[messageIndex] = {
1987
+ ...current,
1988
+ meta: {
1989
+ ...current.meta ?? {},
1990
+ ...patch
1991
+ }
1992
+ };
1993
+ updateConversationStats(conversation);
1994
+ }
1995
+ function buildConversationContext(conversation, options = {}) {
1996
+ const messages = conversation.messages.filter((entry) => entry.id !== options.excludeMessageId);
1997
+ if (messages.length === 0 && conversation.compactions.length === 0) {
1998
+ return void 0;
1999
+ }
2000
+ const lines = [];
2001
+ if (conversation.compactions.length > 0) {
2002
+ lines.push("<thread-compactions>");
2003
+ for (const [index, compaction] of conversation.compactions.entries()) {
2004
+ lines.push(
2005
+ [
2006
+ `summary_${index + 1}:`,
2007
+ compaction.summary,
2008
+ `covered_messages: ${compaction.coveredMessageIds.length}`,
2009
+ `created_at: ${new Date(compaction.createdAtMs).toISOString()}`
2010
+ ].join(" ")
2011
+ );
2012
+ }
2013
+ lines.push("</thread-compactions>");
2014
+ lines.push("");
2015
+ }
2016
+ lines.push("<thread-transcript>");
2017
+ for (const message of messages) {
2018
+ lines.push(renderConversationMessageLine(message, conversation));
2019
+ }
2020
+ lines.push("</thread-transcript>");
2021
+ return lines.join("\n");
2022
+ }
2023
+ function pruneCompactions(compactions) {
2024
+ if (compactions.length <= CONTEXT_MAX_COMPACTIONS) {
2025
+ return compactions;
2026
+ }
2027
+ const overflowCount = compactions.length - CONTEXT_MAX_COMPACTIONS + 1;
2028
+ const merged = compactions.slice(0, overflowCount);
2029
+ const mergedSummary = merged.map((entry) => entry.summary).join("\n").slice(0, 3500);
2030
+ const mergedIds = merged.flatMap((entry) => entry.coveredMessageIds).slice(0, 500);
2031
+ const compacted = {
2032
+ id: generateConversationId("compaction"),
2033
+ createdAtMs: Date.now(),
2034
+ summary: mergedSummary,
2035
+ coveredMessageIds: mergedIds
2036
+ };
2037
+ return [compacted, ...compactions.slice(overflowCount)];
2038
+ }
2039
+ async function summarizeConversationChunk(messages, conversation, context) {
2040
+ const transcript = messages.map((message) => renderConversationMessageLine(message, conversation)).join("\n");
2041
+ try {
2042
+ const result = await getBotDeps().completeText({
2043
+ modelId: botConfig.fastModelId,
2044
+ temperature: 0,
2045
+ messages: [
2046
+ {
2047
+ role: "user",
2048
+ content: [
2049
+ "Summarize the following older Slack thread transcript segment for future assistant turns.",
2050
+ "Keep the summary factual and concise.",
2051
+ "Preserve decisions, commitments, constraints, locations, hiring criteria, and unresolved asks.",
2052
+ "Do not invent details.",
2053
+ "",
2054
+ transcript
2055
+ ].join("\n"),
2056
+ timestamp: Date.now()
2057
+ }
2058
+ ],
2059
+ metadata: {
2060
+ modelId: botConfig.fastModelId,
2061
+ threadId: context.threadId ?? "",
2062
+ channelId: context.channelId ?? "",
2063
+ requesterId: context.requesterId ?? "",
2064
+ runId: context.runId ?? ""
2065
+ }
2066
+ });
2067
+ const summary = result.text.trim();
2068
+ if (summary.length > 0) {
2069
+ return summary.slice(0, 3500);
2070
+ }
2071
+ } catch (error) {
2072
+ logWarn(
2073
+ "conversation_compaction_summary_failed",
2074
+ {
2075
+ slackThreadId: context.threadId,
2076
+ slackUserId: context.requesterId,
2077
+ slackChannelId: context.channelId,
2078
+ runId: context.runId,
2079
+ assistantUserName: botConfig.userName,
2080
+ modelId: botConfig.fastModelId
2081
+ },
2082
+ {
2083
+ "error.message": error instanceof Error ? error.message : String(error),
2084
+ "app.compaction_messages_covered": messages.length
2085
+ },
2086
+ "Compaction summarization failed; using fallback summary"
2087
+ );
2088
+ }
2089
+ return transcript.slice(0, 2800);
2090
+ }
2091
+ async function generateThreadTitle(userText, assistantText) {
2092
+ const result = await getBotDeps().completeText({
2093
+ modelId: botConfig.fastModelId,
2094
+ temperature: 0,
2095
+ messages: [
2096
+ {
2097
+ role: "user",
2098
+ content: [
2099
+ "Generate a concise 5-8 word title for this conversation. Reply with ONLY the title, no quotes or punctuation.",
2100
+ "",
2101
+ `User: ${userText.slice(0, 500)}`,
2102
+ `Assistant: ${assistantText.slice(0, 500)}`
2103
+ ].join("\n"),
2104
+ timestamp: Date.now()
2105
+ }
2106
+ ]
2107
+ });
2108
+ return result.text.trim().slice(0, 60);
2109
+ }
2110
+ async function compactConversationIfNeeded(conversation, context) {
2111
+ updateConversationStats(conversation);
2112
+ let estimatedTokens = conversation.stats.estimatedContextTokens;
2113
+ setSpanAttributes({
2114
+ "app.context_tokens_estimated": estimatedTokens
2115
+ });
2116
+ while (estimatedTokens > CONTEXT_COMPACTION_TRIGGER_TOKENS && conversation.messages.length > CONTEXT_MIN_LIVE_MESSAGES) {
2117
+ const compactCount = Math.min(
2118
+ CONTEXT_COMPACTION_BATCH_SIZE,
2119
+ conversation.messages.length - CONTEXT_MIN_LIVE_MESSAGES
2120
+ );
2121
+ if (compactCount <= 0) {
2122
+ break;
2123
+ }
2124
+ const compactedChunk = conversation.messages.slice(0, compactCount);
2125
+ const summary = await summarizeConversationChunk(compactedChunk, conversation, context);
2126
+ conversation.compactions.push({
2127
+ id: generateConversationId("compaction"),
2128
+ createdAtMs: Date.now(),
2129
+ summary,
2130
+ coveredMessageIds: compactedChunk.map((entry) => entry.id)
2131
+ });
2132
+ conversation.compactions = pruneCompactions(conversation.compactions);
2133
+ conversation.messages = conversation.messages.slice(compactCount);
2134
+ conversation.stats.compactedMessageCount += compactCount;
2135
+ updateConversationStats(conversation);
2136
+ estimatedTokens = conversation.stats.estimatedContextTokens;
2137
+ setSpanAttributes({
2138
+ "app.compaction_messages_covered": compactCount,
2139
+ "app.context_tokens_estimated": estimatedTokens
2140
+ });
2141
+ if (estimatedTokens <= CONTEXT_COMPACTION_TARGET_TOKENS) {
2142
+ break;
2143
+ }
2144
+ }
2145
+ }
2146
+ function createConversationMessageFromSdkMessage(entry) {
2147
+ const rawText = normalizeConversationText(entry.text);
2148
+ if (!rawText) {
2149
+ return null;
2150
+ }
2151
+ return {
2152
+ id: entry.id,
2153
+ role: entry.author.isMe ? "assistant" : "user",
2154
+ text: rawText,
2155
+ createdAtMs: entry.metadata.dateSent.getTime(),
2156
+ author: {
2157
+ userId: entry.author.userId,
2158
+ userName: entry.author.userName,
2159
+ fullName: entry.author.fullName,
2160
+ isBot: typeof entry.author.isBot === "boolean" ? entry.author.isBot : void 0
2161
+ },
2162
+ meta: {
2163
+ slackTs: entry.id
2164
+ }
2165
+ };
2166
+ }
2167
+ async function seedConversationBackfill(thread, conversation, currentTurn) {
2168
+ if (conversation.backfill.completedAtMs) {
2169
+ return;
2170
+ }
2171
+ if (conversation.messages.length > 0 || conversation.compactions.length > 0) {
2172
+ conversation.backfill = {
2173
+ completedAtMs: Date.now(),
2174
+ source: "recent_messages"
2175
+ };
2176
+ updateConversationStats(conversation);
2177
+ return;
2178
+ }
2179
+ const seeded = [];
2180
+ let source = "recent_messages";
2181
+ try {
2182
+ const fetchedNewestFirst = [];
2183
+ for await (const entry of thread.messages) {
2184
+ fetchedNewestFirst.push(entry);
2185
+ if (fetchedNewestFirst.length >= BACKFILL_MESSAGE_LIMIT) {
2186
+ break;
2187
+ }
2188
+ }
2189
+ fetchedNewestFirst.reverse();
2190
+ for (const entry of fetchedNewestFirst) {
2191
+ const message = createConversationMessageFromSdkMessage(entry);
2192
+ if (message) {
2193
+ seeded.push(message);
2194
+ }
2195
+ }
2196
+ if (seeded.length > 0) {
2197
+ source = "thread_fetch";
2198
+ }
2199
+ } catch {
2200
+ }
2201
+ if (seeded.length === 0) {
2202
+ try {
2203
+ await thread.refresh();
2204
+ } catch {
2205
+ }
2206
+ const fromRecent = thread.recentMessages.slice(-BACKFILL_MESSAGE_LIMIT);
2207
+ for (const entry of fromRecent) {
2208
+ const message = createConversationMessageFromSdkMessage(entry);
2209
+ if (message) {
2210
+ seeded.push(message);
2211
+ }
2212
+ }
2213
+ source = "recent_messages";
2214
+ }
2215
+ for (const message of seeded) {
2216
+ if (message.id !== currentTurn.messageId && message.createdAtMs > currentTurn.messageCreatedAtMs) {
2217
+ continue;
2218
+ }
2219
+ if (message.id !== currentTurn.messageId && message.createdAtMs === currentTurn.messageCreatedAtMs && message.id > currentTurn.messageId) {
2220
+ continue;
2221
+ }
2222
+ upsertConversationMessage(conversation, message);
2223
+ }
2224
+ conversation.backfill = {
2225
+ completedAtMs: Date.now(),
2226
+ source
2227
+ };
2228
+ updateConversationStats(conversation);
2229
+ }
2230
+ function isHumanConversationMessage(message) {
2231
+ return message.role === "user" && message.author?.isBot !== true;
2232
+ }
2233
+ function getConversationMessageSlackTs(message) {
2234
+ return message.meta?.slackTs ?? toOptionalString(message.id);
2235
+ }
2236
+
2237
+ // src/chat/services/vision-context.ts
2238
+ var MAX_USER_ATTACHMENTS = 3;
2239
+ var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
2240
+ var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
2241
+ var MAX_VISION_SUMMARY_CHARS = 500;
2242
+ async function resolveUserAttachments(attachments, context) {
2243
+ if (!attachments || attachments.length === 0) {
2244
+ return [];
2245
+ }
2246
+ const results = [];
2247
+ for (const attachment of attachments) {
2248
+ if (results.length >= MAX_USER_ATTACHMENTS) break;
2249
+ if (attachment.type !== "image" && attachment.type !== "file") continue;
2250
+ const mediaType = attachment.mimeType ?? "application/octet-stream";
2251
+ try {
2252
+ let data = null;
2253
+ if (attachment.fetchData) {
2254
+ data = await attachment.fetchData();
2255
+ } else if (attachment.data instanceof Buffer) {
2256
+ data = attachment.data;
2257
+ }
2258
+ if (!data) continue;
2259
+ if (data.byteLength > MAX_USER_ATTACHMENT_BYTES) {
2260
+ logWarn(
2261
+ "attachment_skipped_size_limit",
2262
+ {
2263
+ slackThreadId: context.threadId,
2264
+ slackUserId: context.requesterId,
2265
+ slackChannelId: context.channelId,
2266
+ runId: context.runId,
2267
+ assistantUserName: botConfig.userName,
2268
+ modelId: botConfig.modelId
2269
+ },
2270
+ {
2271
+ "file.size": data.byteLength,
2272
+ "file.mime_type": mediaType
2273
+ },
2274
+ "Skipping user attachment that exceeds size limit"
2275
+ );
2276
+ continue;
2277
+ }
2278
+ results.push({
2279
+ data,
2280
+ mediaType,
2281
+ filename: attachment.name
2282
+ });
2283
+ } catch (error) {
2284
+ logWarn(
2285
+ "attachment_resolution_failed",
2286
+ {
2287
+ slackThreadId: context.threadId,
2288
+ slackUserId: context.requesterId,
2289
+ slackChannelId: context.channelId,
2290
+ runId: context.runId,
2291
+ assistantUserName: botConfig.userName,
2292
+ modelId: botConfig.modelId
2293
+ },
2294
+ {
2295
+ "error.message": error instanceof Error ? error.message : String(error),
2296
+ "file.mime_type": mediaType
2297
+ },
2298
+ "Failed to resolve user attachment"
2299
+ );
2300
+ }
2301
+ }
2302
+ return results;
2303
+ }
2304
+ async function summarizeConversationImage(args) {
2305
+ try {
2306
+ const result = await getBotDeps().completeText({
2307
+ modelId: botConfig.modelId,
2308
+ temperature: 0,
2309
+ maxTokens: 220,
2310
+ messages: [
2311
+ {
2312
+ role: "user",
2313
+ content: [
2314
+ {
2315
+ type: "text",
2316
+ text: [
2317
+ "Extract concise, factual context from this image for future thread turns.",
2318
+ "Focus on visible text, names, titles, companies, and candidate-identifying details.",
2319
+ "Do not speculate.",
2320
+ "Return plain text only."
2321
+ ].join(" ")
2322
+ },
2323
+ {
2324
+ type: "image",
2325
+ data: args.imageData.toString("base64"),
2326
+ mimeType: args.mimeType
2327
+ }
2328
+ ],
2329
+ timestamp: Date.now()
2330
+ }
2331
+ ],
2332
+ metadata: {
2333
+ modelId: botConfig.modelId,
2334
+ threadId: args.context.threadId ?? "",
2335
+ channelId: args.context.channelId ?? "",
2336
+ requesterId: args.context.requesterId ?? "",
2337
+ runId: args.context.runId ?? "",
2338
+ fileId: args.fileId
2339
+ }
2340
+ });
2341
+ const summary = result.text.trim().replace(/\s+/g, " ");
2342
+ if (!summary) {
2343
+ return void 0;
2344
+ }
2345
+ return summary.slice(0, MAX_VISION_SUMMARY_CHARS);
2346
+ } catch (error) {
2347
+ logWarn(
2348
+ "conversation_image_vision_failed",
2349
+ {
2350
+ slackThreadId: args.context.threadId,
2351
+ slackUserId: args.context.requesterId,
2352
+ slackChannelId: args.context.channelId,
2353
+ runId: args.context.runId,
2354
+ assistantUserName: botConfig.userName,
2355
+ modelId: botConfig.modelId
2356
+ },
2357
+ {
2358
+ "error.message": error instanceof Error ? error.message : String(error),
2359
+ "file.id": args.fileId,
2360
+ "file.mime_type": args.mimeType
2361
+ },
2362
+ "Image analysis failed while hydrating conversation context"
2363
+ );
2364
+ return void 0;
2365
+ }
2366
+ }
2367
+ async function hydrateConversationVisionContext(conversation, context) {
2368
+ if (!context.channelId || !context.threadTs) {
2369
+ return;
2370
+ }
2371
+ const messagesByTs = /* @__PURE__ */ new Map();
2372
+ for (const message of conversation.messages) {
2373
+ if (!isHumanConversationMessage(message)) continue;
2374
+ if (message.meta?.imagesHydrated) continue;
2375
+ const slackTs = getConversationMessageSlackTs(message);
2376
+ if (!slackTs) continue;
2377
+ messagesByTs.set(slackTs, message);
2378
+ }
2379
+ if (messagesByTs.size === 0) {
2380
+ return;
2381
+ }
2382
+ let replies;
2383
+ try {
2384
+ replies = await getBotDeps().listThreadReplies({
2385
+ channelId: context.channelId,
2386
+ threadTs: context.threadTs,
2387
+ limit: 1e3,
2388
+ maxPages: 10,
2389
+ targetMessageTs: [...messagesByTs.keys()]
2390
+ });
2391
+ } catch (error) {
2392
+ logWarn(
2393
+ "conversation_image_replies_fetch_failed",
2394
+ {
2395
+ slackThreadId: context.threadId,
2396
+ slackUserId: context.requesterId,
2397
+ slackChannelId: context.channelId,
2398
+ runId: context.runId,
2399
+ assistantUserName: botConfig.userName,
2400
+ modelId: botConfig.modelId
2401
+ },
2402
+ {
2403
+ "error.message": error instanceof Error ? error.message : String(error)
2404
+ },
2405
+ "Failed to fetch thread replies for image context hydration"
2406
+ );
2407
+ return;
2408
+ }
2409
+ let cacheHits = 0;
2410
+ let cacheMisses = 0;
2411
+ let analyzed = 0;
2412
+ let mutated = false;
2413
+ const hydratedMessageIds = /* @__PURE__ */ new Set();
2414
+ for (const reply of replies) {
2415
+ const ts = toOptionalString(reply.ts);
2416
+ if (!ts || reply.bot_id || reply.subtype === "bot_message") {
2417
+ continue;
2418
+ }
2419
+ const conversationMessage = messagesByTs.get(ts);
2420
+ if (!conversationMessage) {
2421
+ continue;
2422
+ }
2423
+ hydratedMessageIds.add(conversationMessage.id);
2424
+ const imageFiles = (reply.files ?? []).filter((file) => {
2425
+ const mimeType = toOptionalString(file.mimetype);
2426
+ return Boolean(toOptionalString(file.id) && mimeType?.startsWith("image/"));
2427
+ }).slice(0, MAX_MESSAGE_IMAGE_ATTACHMENTS);
2428
+ if (imageFiles.length === 0) {
2429
+ continue;
2430
+ }
2431
+ const imageFileIds = imageFiles.map((file) => toOptionalString(file.id)).filter((fileId) => Boolean(fileId));
2432
+ const existingMeta = conversationMessage.meta ?? {};
2433
+ conversationMessage.meta = {
2434
+ ...existingMeta,
2435
+ slackTs: existingMeta.slackTs ?? ts,
2436
+ imageFileIds,
2437
+ imagesHydrated: true
2438
+ };
2439
+ mutated = true;
2440
+ for (const file of imageFiles) {
2441
+ const fileId = toOptionalString(file.id);
2442
+ if (!fileId) continue;
2443
+ if (conversation.vision.byFileId[fileId]) {
2444
+ cacheHits += 1;
2445
+ continue;
2446
+ }
2447
+ cacheMisses += 1;
2448
+ const mimeType = toOptionalString(file.mimetype) ?? "application/octet-stream";
2449
+ const fileSize = typeof file.size === "number" && Number.isFinite(file.size) ? file.size : void 0;
2450
+ if (fileSize && fileSize > MAX_USER_ATTACHMENT_BYTES) {
2451
+ logWarn(
2452
+ "conversation_image_skipped_size_limit",
2453
+ {
2454
+ slackThreadId: context.threadId,
2455
+ slackUserId: context.requesterId,
2456
+ slackChannelId: context.channelId,
2457
+ runId: context.runId,
2458
+ assistantUserName: botConfig.userName,
2459
+ modelId: botConfig.modelId
2460
+ },
2461
+ {
2462
+ "file.id": fileId,
2463
+ "file.size": fileSize,
2464
+ "file.mime_type": mimeType
2465
+ },
2466
+ "Skipping thread image that exceeds size limit"
2467
+ );
2468
+ continue;
2469
+ }
2470
+ const downloadUrl = toOptionalString(file.url_private_download) ?? toOptionalString(file.url_private);
2471
+ if (!downloadUrl) {
2472
+ continue;
2473
+ }
2474
+ let imageData;
2475
+ try {
2476
+ imageData = await getBotDeps().downloadPrivateSlackFile(downloadUrl);
2477
+ } catch (error) {
2478
+ logWarn(
2479
+ "conversation_image_download_failed",
2480
+ {
2481
+ slackThreadId: context.threadId,
2482
+ slackUserId: context.requesterId,
2483
+ slackChannelId: context.channelId,
2484
+ runId: context.runId,
2485
+ assistantUserName: botConfig.userName,
2486
+ modelId: botConfig.modelId
2487
+ },
2488
+ {
2489
+ "error.message": error instanceof Error ? error.message : String(error),
2490
+ "file.id": fileId,
2491
+ "file.mime_type": mimeType
2492
+ },
2493
+ "Failed to download thread image for context hydration"
2494
+ );
2495
+ continue;
2496
+ }
2497
+ if (imageData.byteLength > MAX_USER_ATTACHMENT_BYTES) {
2498
+ logWarn(
2499
+ "conversation_image_skipped_size_limit",
2500
+ {
2501
+ slackThreadId: context.threadId,
2502
+ slackUserId: context.requesterId,
2503
+ slackChannelId: context.channelId,
2504
+ runId: context.runId,
2505
+ assistantUserName: botConfig.userName,
2506
+ modelId: botConfig.modelId
2507
+ },
2508
+ {
2509
+ "file.id": fileId,
2510
+ "file.size": imageData.byteLength,
2511
+ "file.mime_type": mimeType
2512
+ },
2513
+ "Skipping downloaded thread image that exceeds size limit"
2514
+ );
2515
+ continue;
2516
+ }
2517
+ const summary = await summarizeConversationImage({
2518
+ imageData,
2519
+ mimeType,
2520
+ fileId,
2521
+ context
2522
+ });
2523
+ if (!summary) {
2524
+ continue;
2525
+ }
2526
+ conversation.vision.byFileId[fileId] = {
2527
+ summary,
2528
+ analyzedAtMs: Date.now()
2529
+ };
2530
+ analyzed += 1;
2531
+ mutated = true;
2532
+ }
2533
+ }
2534
+ if (mutated) {
2535
+ updateConversationStats(conversation);
2536
+ }
2537
+ if (cacheHits > 0 || cacheMisses > 0 || analyzed > 0 || hydratedMessageIds.size > 0) {
2538
+ logInfo(
2539
+ "conversation_image_context_hydrated",
2540
+ {
2541
+ slackThreadId: context.threadId,
2542
+ slackUserId: context.requesterId,
2543
+ slackChannelId: context.channelId,
2544
+ runId: context.runId,
2545
+ assistantUserName: botConfig.userName,
2546
+ modelId: botConfig.modelId
2547
+ },
2548
+ {
2549
+ "app.conversation_image.cache_hits": cacheHits,
2550
+ "app.conversation_image.cache_misses": cacheMisses,
2551
+ "app.conversation_image.analyzed": analyzed,
2552
+ "app.conversation_image.messages_hydrated": hydratedMessageIds.size
2553
+ },
2554
+ "Hydrated conversation image context"
2555
+ );
2556
+ }
2557
+ if (!conversation.vision.backfillCompletedAtMs) {
2558
+ conversation.vision.backfillCompletedAtMs = Date.now();
2559
+ }
2560
+ }
2561
+
2562
+ // src/chat/turn/execute.ts
2563
+ function resolveReplyDelivery(args) {
2564
+ const replyHasFiles = Boolean(args.reply.files && args.reply.files.length > 0);
2565
+ const deliveryPlan = args.reply.deliveryPlan ?? {
2566
+ mode: args.reply.deliveryMode ?? "thread",
2567
+ ack: args.reply.ackStrategy ?? "none",
2568
+ postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
2569
+ attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
2570
+ };
2571
+ let attachFiles = deliveryPlan.attachFiles;
2572
+ if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
2573
+ attachFiles = "inline";
2574
+ }
2575
+ return {
2576
+ shouldPostThreadReply: deliveryPlan.postThreadText,
2577
+ attachFiles
2578
+ };
2579
+ }
2580
+
2581
+ // src/chat/turn/persist.ts
2582
+ function markTurnCompleted(args) {
2583
+ args.conversation.processing.activeTurnId = void 0;
2584
+ args.conversation.processing.lastCompletedAtMs = args.nowMs;
2585
+ args.updateConversationStats(args.conversation);
2586
+ }
2587
+ function markTurnFailed(args) {
2588
+ args.conversation.processing.activeTurnId = void 0;
2589
+ args.conversation.processing.lastCompletedAtMs = args.nowMs;
2590
+ args.markConversationMessage(args.conversation, args.userMessageId, {
2591
+ replied: false,
2592
+ skippedReason: "reply failed"
2593
+ });
2594
+ args.updateConversationStats(args.conversation);
2595
+ }
2596
+
2597
+ // src/chat/turn/prepare.ts
2598
+ function startActiveTurn(args) {
2599
+ args.conversation.processing.activeTurnId = args.nextTurnId;
2600
+ args.updateConversationStats(args.conversation);
2601
+ }
2602
+
2603
+ // src/chat/runtime/reply-executor.ts
2604
+ function buildDeterministicTurnId(messageId) {
2605
+ const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
2606
+ return `turn_${sanitized}`;
2607
+ }
2608
+ function createReplyToThread(deps) {
2609
+ return async function replyToThread2(thread, message, options = {}) {
2610
+ if (message.author.isMe) {
2611
+ return;
2612
+ }
2613
+ const threadId = getThreadId(thread, message);
2614
+ const channelId = getChannelId(thread, message);
2615
+ const threadTs = getThreadTs(threadId);
2616
+ const messageTs = getMessageTs(message);
2617
+ const runId = getRunId(thread, message);
2618
+ const conversationId = threadId ?? runId;
2619
+ await withSpan(
2620
+ "chat.reply",
2621
+ "chat.reply",
2622
+ {
2623
+ conversationId,
2624
+ slackThreadId: threadId,
2625
+ slackUserId: message.author.userId,
2626
+ slackChannelId: channelId,
2627
+ runId,
2628
+ assistantUserName: botConfig.userName,
2629
+ modelId: botConfig.modelId
2630
+ },
2631
+ async () => {
2632
+ const userText = stripLeadingBotMention(message.text, {
2633
+ stripLeadingSlackMentionToken: options.explicitMention || Boolean(message.isMention)
2634
+ });
2635
+ const explicitChannelPostIntent = isExplicitChannelPostIntent(userText);
2636
+ const preparedState = options.preparedState ?? await deps.prepareTurnState({
2637
+ thread,
2638
+ message,
2639
+ userText,
2640
+ explicitMention: Boolean(options.explicitMention || message.isMention),
2641
+ context: {
2642
+ threadId,
2643
+ requesterId: message.author.userId,
2644
+ channelId,
2645
+ runId
2646
+ }
2647
+ });
2648
+ const turnId = buildDeterministicTurnId(message.id);
2649
+ startActiveTurn({
2650
+ conversation: preparedState.conversation,
2651
+ nextTurnId: turnId,
2652
+ updateConversationStats
2653
+ });
2654
+ const turnStartedAtMs = Date.now();
2655
+ const turnTraceContext = {
2656
+ conversationId,
2657
+ turnId,
2658
+ agentId: turnId,
2659
+ slackThreadId: threadId,
2660
+ slackUserId: message.author.userId,
2661
+ slackChannelId: channelId,
2662
+ runId,
2663
+ assistantUserName: botConfig.userName,
2664
+ modelId: botConfig.modelId
2665
+ };
2666
+ setTags({
2667
+ conversationId,
2668
+ turnId,
2669
+ agentId: turnId
2670
+ });
2671
+ if (shouldEmitDevAgentTrace()) {
2672
+ logInfo(
2673
+ "agent_turn_started",
2674
+ turnTraceContext,
2675
+ {
2676
+ "app.message.id": message.id,
2677
+ ...messageTs ? { "messaging.message.id": messageTs } : {}
2678
+ },
2679
+ "Agent turn started"
2680
+ );
2681
+ }
2682
+ await persistThreadState(thread, {
2683
+ conversation: preparedState.conversation
2684
+ });
2685
+ const fallbackIdentity = await getBotDeps().lookupSlackUser(message.author.userId);
2686
+ const resolvedUserName = message.author.userName ?? fallbackIdentity?.userName;
2687
+ if (resolvedUserName) {
2688
+ setTags({ slackUserName: resolvedUserName });
2689
+ }
2690
+ const userAttachments = await resolveUserAttachments(message.attachments, {
2691
+ threadId,
2692
+ requesterId: message.author.userId,
2693
+ channelId,
2694
+ runId
2695
+ });
2696
+ const progress = createProgressReporter({
2697
+ channelId,
2698
+ threadTs,
2699
+ setAssistantStatus: (channel, thread2, text, suggestions) => deps.getSlackAdapter().setAssistantStatus(channel, thread2, text, suggestions)
2700
+ });
2701
+ const textStream = createTextStreamBridge();
2702
+ let streamedReplyPromise;
2703
+ let beforeFirstResponsePostCalled = false;
2704
+ const beforeFirstResponsePost = async () => {
2705
+ if (beforeFirstResponsePostCalled) {
2706
+ return;
2707
+ }
2708
+ beforeFirstResponsePostCalled = true;
2709
+ await options.beforeFirstResponsePost?.();
2710
+ };
2711
+ const startStreamingReply = () => {
2712
+ if (!streamedReplyPromise) {
2713
+ const streamingReply = (async () => {
2714
+ await beforeFirstResponsePost();
2715
+ return await thread.post(
2716
+ createNormalizingStream(textStream.iterable, ensureBlockSpacing)
2717
+ );
2718
+ })();
2719
+ streamedReplyPromise = streamingReply;
2720
+ }
2721
+ };
2722
+ const postThreadReply = async (payload) => {
2723
+ await beforeFirstResponsePost();
2724
+ return await thread.post(payload);
2725
+ };
2726
+ await progress.start();
2727
+ let persistedAtLeastOnce = false;
2728
+ let shouldPersistFailureState = true;
2729
+ try {
2730
+ const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
2731
+ const reply = await getBotDeps().generateAssistantReply(userText, {
2732
+ assistant: {
2733
+ userName: botConfig.userName
2734
+ },
2735
+ requester: {
2736
+ userId: message.author.userId,
2737
+ userName: message.author.userName ?? fallbackIdentity?.userName,
2738
+ fullName: message.author.fullName ?? fallbackIdentity?.fullName
2739
+ },
2740
+ conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
2741
+ artifactState: preparedState.artifacts,
2742
+ configuration: preparedState.configuration,
2743
+ channelConfiguration: preparedState.channelConfiguration,
2744
+ userAttachments,
2745
+ correlation: {
2746
+ conversationId,
2747
+ threadId,
2748
+ turnId,
2749
+ threadTs,
2750
+ messageTs,
2751
+ runId,
2752
+ channelId,
2753
+ requesterId: message.author.userId
2754
+ },
2755
+ toolChannelId,
2756
+ sandbox: {
2757
+ sandboxId: preparedState.sandboxId
2758
+ },
2759
+ onStatus: (status) => progress.setStatus(status),
2760
+ onTextDelta: (deltaText) => {
2761
+ if (explicitChannelPostIntent) {
2762
+ return;
2763
+ }
2764
+ startStreamingReply();
2765
+ textStream.push(deltaText);
2766
+ }
2767
+ });
2768
+ textStream.end();
2769
+ const diagnosticsContext = {
2770
+ slackThreadId: threadId,
2771
+ slackUserId: message.author.userId,
2772
+ slackChannelId: channelId,
2773
+ runId,
2774
+ assistantUserName: botConfig.userName,
2775
+ modelId: botConfig.modelId
2776
+ };
2777
+ const diagnosticsAttributes = {
2778
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
2779
+ "gen_ai.operation.name": "invoke_agent",
2780
+ "app.ai.outcome": reply.diagnostics.outcome,
2781
+ "app.ai.assistant_messages": reply.diagnostics.assistantMessageCount,
2782
+ "app.ai.tool_results": reply.diagnostics.toolResultCount,
2783
+ "app.ai.tool_error_results": reply.diagnostics.toolErrorCount,
2784
+ "app.ai.tool_call_count": reply.diagnostics.toolCalls.length,
2785
+ "app.ai.used_primary_text": reply.diagnostics.usedPrimaryText,
2786
+ ...reply.diagnostics.stopReason ? { "app.ai.stop_reason": reply.diagnostics.stopReason } : {},
2787
+ ...reply.diagnostics.errorMessage ? { "error.message": reply.diagnostics.errorMessage } : {}
2788
+ };
2789
+ setSpanAttributes(diagnosticsAttributes);
2790
+ if (reply.diagnostics.outcome === "provider_error") {
2791
+ const providerError = reply.diagnostics.providerError ?? new Error(reply.diagnostics.errorMessage ?? "Provider error without explicit message");
2792
+ logException(
2793
+ providerError,
2794
+ "agent_turn_provider_error",
2795
+ diagnosticsContext,
2796
+ diagnosticsAttributes,
2797
+ "Agent turn failed with provider error"
2798
+ );
2799
+ } else if (reply.diagnostics.outcome !== "success") {
2800
+ logWarn(
2801
+ "agent_turn_diagnostics",
2802
+ diagnosticsContext,
2803
+ diagnosticsAttributes,
2804
+ "Agent turn completed with execution failure"
2805
+ );
2806
+ }
2807
+ markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
2808
+ replied: true,
2809
+ skippedReason: void 0
2810
+ });
2811
+ upsertConversationMessage(preparedState.conversation, {
2812
+ id: generateConversationId("assistant"),
2813
+ role: "assistant",
2814
+ text: normalizeConversationText(reply.text) || "[empty response]",
2815
+ createdAtMs: Date.now(),
2816
+ author: {
2817
+ userName: botConfig.userName,
2818
+ isBot: true
2819
+ },
2820
+ meta: {
2821
+ replied: true
2822
+ }
2823
+ });
2824
+ const artifactStatePatch = reply.artifactStatePatch ? { ...reply.artifactStatePatch } : {};
2825
+ const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
2826
+ const { shouldPostThreadReply, attachFiles: resolvedAttachFiles } = resolveReplyDelivery({
2827
+ reply,
2828
+ hasStreamedThreadReply: Boolean(streamedReplyPromise)
2829
+ });
2830
+ if (shouldPostThreadReply) {
2831
+ if (!streamedReplyPromise) {
2832
+ await postThreadReply(
2833
+ buildSlackOutputMessage(reply.text, {
2834
+ files: resolvedAttachFiles === "inline" ? replyFiles : void 0
2835
+ })
2836
+ );
2837
+ } else {
2838
+ await streamedReplyPromise;
2839
+ if (reply.diagnostics.outcome !== "success" && reply.text.trim().length > 0) {
2840
+ await postThreadReply(buildSlackOutputMessage(reply.text));
2841
+ }
2842
+ }
2843
+ }
2844
+ const shouldPersistArtifacts = Object.keys(artifactStatePatch).length > 0;
2845
+ const nextArtifacts = shouldPersistArtifacts ? mergeArtifactsState(preparedState.artifacts, artifactStatePatch) : void 0;
2846
+ markTurnCompleted({
2847
+ conversation: preparedState.conversation,
2848
+ nowMs: Date.now(),
2849
+ updateConversationStats
2850
+ });
2851
+ await persistThreadState(thread, {
2852
+ artifacts: nextArtifacts,
2853
+ conversation: preparedState.conversation,
2854
+ sandboxId: reply.sandboxId
2855
+ });
2856
+ persistedAtLeastOnce = true;
2857
+ if (shouldEmitDevAgentTrace()) {
2858
+ logInfo(
2859
+ "agent_turn_completed",
2860
+ turnTraceContext,
2861
+ {
2862
+ "app.turn.duration_ms": Date.now() - turnStartedAtMs,
2863
+ "app.ai.outcome": reply.diagnostics.outcome,
2864
+ "app.ai.tool_call_count": reply.diagnostics.toolCalls.length,
2865
+ "app.ai.tool_error_results": reply.diagnostics.toolErrorCount
2866
+ },
2867
+ "Agent turn completed"
2868
+ );
2869
+ }
2870
+ const isFirstAssistantReply = preparedState.conversation.stats.compactedMessageCount === 0 && preparedState.conversation.messages.filter((m) => m.role === "assistant").length === 1;
2871
+ if (isFirstAssistantReply && channelId && isDmChannel(channelId) && threadTs) {
2872
+ void generateThreadTitle(userText, reply.text).then((title) => deps.getSlackAdapter().setAssistantTitle(channelId, threadTs, title)).catch((error) => {
2873
+ const slackErrorCode = getSlackApiErrorCode(error);
2874
+ const assistantTitleErrorAttributes = {
2875
+ "app.slack.assistant_title.outcome": "permission_denied",
2876
+ ...slackErrorCode ? { "app.slack.assistant_title.error_code": slackErrorCode } : {}
2877
+ };
2878
+ if (isSlackTitlePermissionError(error)) {
2879
+ setSpanAttributes(assistantTitleErrorAttributes);
2880
+ logError(
2881
+ "thread_title_generation_permission_denied",
2882
+ {
2883
+ slackThreadId: threadId,
2884
+ slackUserId: message.author.userId,
2885
+ slackChannelId: channelId,
2886
+ runId,
2887
+ assistantUserName: botConfig.userName,
2888
+ modelId: botConfig.fastModelId
2889
+ },
2890
+ assistantTitleErrorAttributes,
2891
+ "Skipping thread title update due to Slack permission error"
2892
+ );
2893
+ return;
2894
+ }
2895
+ logWarn(
2896
+ "thread_title_generation_failed",
2897
+ {
2898
+ slackThreadId: threadId,
2899
+ slackUserId: message.author.userId,
2900
+ slackChannelId: channelId,
2901
+ runId,
2902
+ assistantUserName: botConfig.userName,
2903
+ modelId: botConfig.fastModelId
2904
+ },
2905
+ { "error.message": error instanceof Error ? error.message : String(error) },
2906
+ "Thread title generation failed"
2907
+ );
2908
+ });
2909
+ }
2910
+ if (shouldPostThreadReply && resolvedAttachFiles === "followup" && replyFiles) {
2911
+ await postThreadReply({ files: replyFiles });
2912
+ }
2913
+ } catch (error) {
2914
+ shouldPersistFailureState = !isRetryableTurnError(error);
2915
+ throw error;
2916
+ } finally {
2917
+ textStream.end();
2918
+ if (!persistedAtLeastOnce && shouldPersistFailureState) {
2919
+ markTurnFailed({
2920
+ conversation: preparedState.conversation,
2921
+ nowMs: Date.now(),
2922
+ userMessageId: preparedState.userMessageId,
2923
+ markConversationMessage: (conversation, messageId, patch) => {
2924
+ markConversationMessage(conversation, messageId, patch);
2925
+ },
2926
+ updateConversationStats
2927
+ });
2928
+ await persistThreadState(thread, {
2929
+ conversation: preparedState.conversation
2930
+ });
2931
+ if (shouldEmitDevAgentTrace()) {
2932
+ logWarn(
2933
+ "agent_turn_failed",
2934
+ turnTraceContext,
2935
+ {
2936
+ "app.turn.duration_ms": Date.now() - turnStartedAtMs
2937
+ },
2938
+ "Agent turn failed"
2939
+ );
2940
+ }
2941
+ }
2942
+ await progress.stop();
2943
+ }
2944
+ }
2945
+ );
2946
+ };
2947
+ }
2948
+
2949
+ // src/chat/runtime/turn-preparation.ts
2950
+ async function prepareTurnState(args) {
2951
+ const existingState = await args.thread.state;
2952
+ const existingSandboxId = existingState ? toOptionalString(existingState.app_sandbox_id) : void 0;
2953
+ const artifacts = coerceThreadArtifactsState(existingState);
2954
+ const conversation = coerceThreadConversationState(existingState);
2955
+ const channelConfiguration = getChannelConfigurationService(args.thread);
2956
+ const configuration = await channelConfiguration.resolveValues();
2957
+ await seedConversationBackfill(args.thread, conversation, {
2958
+ messageId: args.message.id,
2959
+ messageCreatedAtMs: args.message.metadata.dateSent.getTime()
2960
+ });
2961
+ const messageHasPotentialImageAttachment = args.message.attachments.some((attachment) => {
2962
+ if (attachment.type === "image") {
2963
+ return true;
2964
+ }
2965
+ const mimeType = attachment.mimeType ?? "";
2966
+ return attachment.type === "file" && mimeType.startsWith("image/");
2967
+ });
2968
+ const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
2969
+ const incomingUserMessage = {
2970
+ id: args.message.id,
2971
+ role: "user",
2972
+ text: normalizedUserText,
2973
+ createdAtMs: args.message.metadata.dateSent.getTime(),
2974
+ author: {
2975
+ userId: args.message.author.userId,
2976
+ userName: args.message.author.userName,
2977
+ fullName: args.message.author.fullName,
2978
+ isBot: typeof args.message.author.isBot === "boolean" ? args.message.author.isBot : void 0
2979
+ },
2980
+ meta: {
2981
+ explicitMention: args.explicitMention,
2982
+ slackTs: args.message.id,
2983
+ imagesHydrated: !messageHasPotentialImageAttachment
2984
+ }
2985
+ };
2986
+ const userMessageId = upsertConversationMessage(conversation, incomingUserMessage);
2987
+ if (messageHasPotentialImageAttachment || !conversation.vision.backfillCompletedAtMs) {
2988
+ await hydrateConversationVisionContext(conversation, {
2989
+ threadId: args.context.threadId,
2990
+ channelId: args.context.channelId,
2991
+ requesterId: args.context.requesterId,
2992
+ runId: args.context.runId,
2993
+ threadTs: getThreadTs(args.context.threadId)
2994
+ });
2995
+ }
2996
+ await compactConversationIfNeeded(conversation, {
2997
+ threadId: args.context.threadId,
2998
+ channelId: args.context.channelId,
2999
+ requesterId: args.context.requesterId,
3000
+ runId: args.context.runId
3001
+ });
3002
+ const conversationContext = buildConversationContext(conversation);
3003
+ const routingContext = buildConversationContext(conversation, {
3004
+ excludeMessageId: userMessageId
3005
+ });
3006
+ setSpanAttributes({
3007
+ "app.backfill_source": conversation.backfill.source ?? "none",
3008
+ "app.context_tokens_estimated": conversation.stats.estimatedContextTokens
3009
+ });
3010
+ return {
3011
+ artifacts,
3012
+ configuration,
3013
+ channelConfiguration,
3014
+ conversation,
3015
+ sandboxId: existingSandboxId,
3016
+ conversationContext,
3017
+ routingContext,
3018
+ userMessageId
3019
+ };
3020
+ }
3021
+
3022
+ // src/chat/bot.ts
3023
+ var createdBot = new Chat2({
3024
+ userName: botConfig.userName,
3025
+ adapters: {
3026
+ slack: (() => {
3027
+ const signingSecret = getSlackSigningSecret();
3028
+ const botToken = getSlackBotToken();
3029
+ const clientId = getSlackClientId();
3030
+ const clientSecret = getSlackClientSecret();
3031
+ if (!signingSecret) {
3032
+ throw new Error("SLACK_SIGNING_SECRET is required");
3033
+ }
3034
+ return createSlackAdapter({
3035
+ signingSecret,
3036
+ ...botToken ? { botToken } : {},
3037
+ ...clientId ? { clientId } : {},
3038
+ ...clientSecret ? { clientSecret } : {}
3039
+ });
3040
+ })()
3041
+ },
3042
+ state: getStateAdapter()
3043
+ });
3044
+ var registerSingleton = createdBot.registerSingleton;
3045
+ if (typeof registerSingleton === "function") {
3046
+ registerSingleton.call(createdBot);
3047
+ }
3048
+ var bot = createdBot;
3049
+ function getSlackAdapter() {
3050
+ return bot.getAdapter("slack");
3051
+ }
3052
+ var replyToThread = createReplyToThread({
3053
+ getSlackAdapter,
3054
+ prepareTurnState
3055
+ });
3056
+ var appSlackRuntime = createAppSlackRuntime({
3057
+ assistantUserName: botConfig.userName,
3058
+ modelId: botConfig.modelId,
3059
+ now: () => Date.now(),
3060
+ getThreadId,
3061
+ getChannelId,
3062
+ getRunId,
3063
+ stripLeadingBotMention,
3064
+ withSpan,
3065
+ logWarn,
3066
+ logException,
3067
+ prepareTurnState,
3068
+ persistPreparedState: async ({ thread, preparedState }) => {
3069
+ await persistThreadState(thread, {
3070
+ conversation: preparedState.conversation
3071
+ });
3072
+ },
3073
+ getPreparedConversationContext: (preparedState) => preparedState.routingContext ?? preparedState.conversationContext,
3074
+ shouldReplyInSubscribedThread,
3075
+ onSubscribedMessageSkipped: async ({ thread, preparedState, decision, completedAtMs }) => {
3076
+ markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
3077
+ replied: false,
3078
+ skippedReason: decision.reason
3079
+ });
3080
+ preparedState.conversation.processing.activeTurnId = void 0;
3081
+ preparedState.conversation.processing.lastCompletedAtMs = completedAtMs;
3082
+ updateConversationStats(preparedState.conversation);
3083
+ await persistThreadState(thread, {
3084
+ conversation: preparedState.conversation
3085
+ });
3086
+ },
3087
+ replyToThread,
3088
+ initializeAssistantThread: async ({ threadId, channelId, threadTs, sourceChannelId }) => {
3089
+ await initializeAssistantThread({
3090
+ threadId,
3091
+ channelId,
3092
+ threadTs,
3093
+ sourceChannelId,
3094
+ getSlackAdapter
3095
+ });
3096
+ }
3097
+ });
3098
+ registerBotHandlers({
3099
+ bot,
3100
+ appSlackRuntime
3101
+ });
3102
+ export {
3103
+ appSlackRuntime,
3104
+ bot,
3105
+ createNormalizingStream,
3106
+ resetBotDepsForTests,
3107
+ setBotDepsForTests
3108
+ };