@sentry/junior 0.114.0 → 0.115.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.
Files changed (88) hide show
  1. package/dist/{agent-hooks-WNBUY4OB.js → agent-hooks-4EUUXP2F.js} +12 -11
  2. package/dist/api/conversations/detail.d.ts +2 -0
  3. package/dist/api/conversations/event-list.d.ts +2 -0
  4. package/dist/api/health.d.ts +7 -0
  5. package/dist/api/plugin-reports.d.ts +62 -0
  6. package/dist/api/plugins.d.ts +9 -0
  7. package/dist/api/runtime.d.ts +24 -0
  8. package/dist/api/schema/conversation.d.ts +8 -0
  9. package/dist/api/schema.d.ts +2 -2
  10. package/dist/api/schema.js +6 -6
  11. package/dist/api/skills.d.ts +6 -0
  12. package/dist/api.js +123 -99
  13. package/dist/app.js +1104 -5006
  14. package/dist/catalog-runtime-Y32UEQLB.js +18 -0
  15. package/dist/chat/agent/request.d.ts +6 -0
  16. package/dist/chat/agent/sandbox.d.ts +2 -0
  17. package/dist/chat/agent/tools.d.ts +2 -0
  18. package/dist/chat/config.d.ts +2 -0
  19. package/dist/chat/conversations/history.d.ts +6 -0
  20. package/dist/chat/local/credential-sync.d.ts +5 -0
  21. package/dist/chat/local/oauth-relay.d.ts +4 -0
  22. package/dist/chat/mcp/oauth.d.ts +1 -0
  23. package/dist/chat/oauth-authorization.d.ts +17 -0
  24. package/dist/chat/oauth-flow.d.ts +9 -0
  25. package/dist/chat/sandbox/egress/schemas.d.ts +46 -0
  26. package/dist/chat/sandbox/egress/signals.d.ts +9 -0
  27. package/dist/chat/sandbox/sandbox.d.ts +2 -0
  28. package/dist/chat/sentry-payload-filter.d.ts +4 -4
  29. package/dist/chat/services/mcp-auth-orchestration.d.ts +5 -3
  30. package/dist/chat/services/plugin-auth-orchestration.d.ts +2 -0
  31. package/dist/chat/slack/message-control.d.ts +2 -0
  32. package/dist/chat/slack/oauth-authorization-message.d.ts +3 -0
  33. package/dist/chat/task-execution/slack-work.d.ts +2 -0
  34. package/dist/chat/tool-support/zod-tool.d.ts +1 -1
  35. package/dist/chat/tools/definition.d.ts +5 -0
  36. package/dist/{chunk-TLGM5X2M.js → chunk-2QG4MZQ7.js} +2 -2
  37. package/dist/{chunk-WV4UJBAS.js → chunk-43RYMWJ2.js} +523 -2373
  38. package/dist/chunk-4CODA7P5.js +43 -0
  39. package/dist/{chunk-YU6PO42Q.js → chunk-67CVNDJ7.js} +1 -1
  40. package/dist/{chunk-OSKEC4XU.js → chunk-6FKLPVF3.js} +1 -1
  41. package/dist/{chunk-2ZTTXTSZ.js → chunk-6OPTF4BQ.js} +10 -4
  42. package/dist/{chunk-W6RDUAO3.js → chunk-6V762AB4.js} +1 -1
  43. package/dist/{chunk-ABPSDO6M.js → chunk-AGLQYELI.js} +77 -8
  44. package/dist/{chunk-6QWPVTMO.js → chunk-CEYOL62C.js} +9 -7
  45. package/dist/{chunk-RD7EJI7H.js → chunk-CWEZEPD4.js} +3 -3
  46. package/dist/chunk-DIB4GX3G.js +8 -0
  47. package/dist/{chunk-K2W4CK7K.js → chunk-EW5MLKT5.js} +10 -75
  48. package/dist/{chunk-42WRQLJ6.js → chunk-F3B2YJ76.js} +0 -14
  49. package/dist/chunk-FA6POGLH.js +70 -0
  50. package/dist/{chunk-PM3PX42K.js → chunk-FHC4JUSF.js} +1 -1
  51. package/dist/{chunk-6Y5L4BOM.js → chunk-FXOYURO4.js} +5 -3
  52. package/dist/{chunk-S4QDH23A.js → chunk-G36V2YGB.js} +83 -81
  53. package/dist/{chunk-O4GT7LYB.js → chunk-G5SX4UPR.js} +3 -43
  54. package/dist/chunk-HDSDCEPK.js +80 -0
  55. package/dist/{chunk-FIGAF6KL.js → chunk-HWCQRGLL.js} +5 -3
  56. package/dist/{chunk-C4MZ3GP5.js → chunk-HZF5P5UN.js} +1 -1
  57. package/dist/chunk-LYA2AB5G.js +1914 -0
  58. package/dist/{chunk-4FUEG3QY.js → chunk-NVUQSFWX.js} +2 -2
  59. package/dist/chunk-OB42YVAE.js +16 -0
  60. package/dist/{chunk-XLFBQJUO.js → chunk-PVZHG2XZ.js} +4 -4
  61. package/dist/{chunk-VNNBSKCO.js → chunk-RAVKCKXA.js} +7 -7
  62. package/dist/{chunk-ZGXH3REA.js → chunk-SYJT7AKB.js} +12 -2
  63. package/dist/{chunk-5CZ7RETL.js → chunk-TVE25LPW.js} +1 -1
  64. package/dist/chunk-W3U3YQYI.js +71 -0
  65. package/dist/chunk-YCLO7D7M.js +4267 -0
  66. package/dist/cli/chat.js +72 -40
  67. package/dist/cli/check.js +10 -8
  68. package/dist/cli/init.js +1 -0
  69. package/dist/cli/plugins.js +16 -14
  70. package/dist/cli/snapshot-warmup.js +13 -11
  71. package/dist/cli/upgrade.js +10 -8
  72. package/dist/{db-4VLP7KBF.js → db-KBSJHAZU.js} +8 -7
  73. package/dist/handlers/sandbox-egress-signals.d.ts +4 -0
  74. package/dist/instrumentation.js +72 -4
  75. package/dist/nitro.js +2 -2
  76. package/dist/oauth-callback-server-EYRMJTCS.js +231 -0
  77. package/dist/oauth-relay-3ZU72UGL.js +9 -0
  78. package/dist/reporting-schema.d.ts +12 -4
  79. package/dist/reporting.d.ts +5 -5
  80. package/dist/{runner-IPXLWQOF.js → runner-RQGZ5Z2S.js} +139 -72
  81. package/dist/sandbox-egress-signals-CDFAP67O.js +75 -0
  82. package/dist/{skills-QCNZISNF.js → skills-JJQIE3BT.js} +10 -8
  83. package/dist/validation-MZ47PZP5.js +21 -0
  84. package/dist/version.js +1 -1
  85. package/package.json +5 -5
  86. package/dist/catalog-runtime-6O4M6JGA.js +0 -16
  87. package/dist/chat/oauth-authorization-message.d.ts +0 -6
  88. package/dist/validation-I2BW3UHD.js +0 -19
@@ -0,0 +1,4267 @@
1
+ import {
2
+ relayLocalOAuthCallback
3
+ } from "./chunk-FA6POGLH.js";
4
+ import {
5
+ ConversationTurnLifecycleService,
6
+ buildDeliveredTurnStatePatch,
7
+ finalizeFailedTurnReplyWithEvent,
8
+ getTurnUserMessage,
9
+ getTurnUserMessageId,
10
+ getTurnUserReplyAttachmentContext,
11
+ getTurnUserSlackMessageTs,
12
+ persistAuthPauseTurnState,
13
+ requireTurnFailureEventId,
14
+ scheduleSessionCompletedPluginTasks
15
+ } from "./chunk-AGLQYELI.js";
16
+ import {
17
+ buildSentryConversationUrl
18
+ } from "./chunk-4CODA7P5.js";
19
+ import {
20
+ getVercelConversationWorkQueue
21
+ } from "./chunk-MIAKQNYX.js";
22
+ import {
23
+ RetryableDeliveryError,
24
+ addReactionToMessage,
25
+ createUserTokenStore,
26
+ deleteMcpAuthSession,
27
+ escapeSlackMrkdwnText,
28
+ finalizeMcpAuthorization,
29
+ formatProviderLabel,
30
+ formatSlackLink,
31
+ getMcpAuthSession,
32
+ getMcpStoredOAuthCredentials,
33
+ normalizeSlackStatusText,
34
+ parseOAuthStatePayload,
35
+ postSlackMessage,
36
+ removeReactionFromMessage,
37
+ resolveBaseUrl,
38
+ resolvePluginOAuthAccount,
39
+ splitSlackReplyText,
40
+ truncateStatusText
41
+ } from "./chunk-LYA2AB5G.js";
42
+ import {
43
+ JUNIOR_THREAD_STATE_TTL_MS,
44
+ TurnSliceLimitExceededError,
45
+ abandonAgentTurnSessionRecord,
46
+ buildConversationContext,
47
+ buildTurnLimitResponse,
48
+ clearPendingAuth,
49
+ coerceThreadArtifactsState,
50
+ coerceThreadConversationState,
51
+ failAgentTurnSessionRecord,
52
+ getAgentTurnSessionRecord,
53
+ getChannelConfigurationServiceById,
54
+ getConversationPendingAuth,
55
+ getPersistedSandboxState,
56
+ getPersistedThreadState,
57
+ hydrateConversationMessages,
58
+ isPendingAuthLatestRequest,
59
+ loadProjection,
60
+ markConversationMessage,
61
+ markTurnFailed,
62
+ persistCompletedSessionRecord,
63
+ persistConversationMessages,
64
+ persistThreadStateById,
65
+ persistWithRetry,
66
+ recordAuthorizationCompleted,
67
+ recordDeliveredAssistantMessage,
68
+ turnHasReply,
69
+ updateConversationStats
70
+ } from "./chunk-CEYOL62C.js";
71
+ import {
72
+ getDashboardConversationLink,
73
+ getPluginSlackConversationLink
74
+ } from "./chunk-RAVKCKXA.js";
75
+ import {
76
+ acquireActiveLock,
77
+ getDefaultRedisStateAdapterFor,
78
+ getStateAdapter
79
+ } from "./chunk-67CVNDJ7.js";
80
+ import {
81
+ SlackActionError,
82
+ getHeaderString,
83
+ getSlackClient,
84
+ isDmChannel,
85
+ normalizeSlackConversationId
86
+ } from "./chunk-CWEZEPD4.js";
87
+ import {
88
+ getConversationEventStore,
89
+ getConversationStore,
90
+ parseDestination,
91
+ requireSlackDestination,
92
+ sameDestination
93
+ } from "./chunk-HWCQRGLL.js";
94
+ import {
95
+ FUNCTION_TIMEOUT_BUFFER_SECONDS,
96
+ botConfig,
97
+ escapeXml,
98
+ getChatConfig,
99
+ getRuntimeMetadata,
100
+ normalizeSlackEmojiName,
101
+ standardModelId
102
+ } from "./chunk-SYJT7AKB.js";
103
+ import {
104
+ parseSlackThreadId,
105
+ readSlackRawMessageContext,
106
+ resolveSlackChannelIdFromMessage,
107
+ resolveSlackChannelIdFromThreadId
108
+ } from "./chunk-FXOYURO4.js";
109
+ import {
110
+ discoverSkills
111
+ } from "./chunk-2QG4MZQ7.js";
112
+ import {
113
+ pluginCatalogRuntime
114
+ } from "./chunk-6FKLPVF3.js";
115
+ import {
116
+ buildOAuthTokenRequest,
117
+ hasRequiredOAuthScope,
118
+ parseOAuthTokenResponse
119
+ } from "./chunk-EW5MLKT5.js";
120
+ import {
121
+ createSlackResumeActor,
122
+ isUserActor,
123
+ parseStoredSlackActor
124
+ } from "./chunk-HZF5P5UN.js";
125
+ import {
126
+ buildTurnFailureResponse,
127
+ logException,
128
+ logInfo,
129
+ logWarn
130
+ } from "./chunk-NVUQSFWX.js";
131
+ import {
132
+ isRecord,
133
+ toOptionalNumber,
134
+ toOptionalString
135
+ } from "./chunk-OB42YVAE.js";
136
+ import {
137
+ parseSlackMessageTs
138
+ } from "./chunk-F3B2YJ76.js";
139
+ import {
140
+ homeDir
141
+ } from "./chunk-3V7OFJZC.js";
142
+
143
+ // src/chat/slack/assistant-thread/status-render.ts
144
+ var DEFAULT_STATUS_CONTEXTS = {
145
+ thinking: "\u2026",
146
+ searching: "sources",
147
+ reading: "task",
148
+ reviewing: "results",
149
+ drafting: "reply",
150
+ running: "tasks"
151
+ };
152
+ function formatAssistantStatusText(verb, context) {
153
+ const normalizedVerb = normalizeSlackStatusText(verb).trim().toLowerCase();
154
+ const normalizedContext = normalizeSlackStatusText(context ?? "") || DEFAULT_STATUS_CONTEXTS[normalizedVerb] || "";
155
+ if (!normalizedVerb) {
156
+ return truncateStatusText(normalizedContext || "Working");
157
+ }
158
+ const displayVerb = `${normalizedVerb[0]?.toUpperCase() ?? ""}${normalizedVerb.slice(1)}`;
159
+ return truncateStatusText(
160
+ normalizedContext ? `${displayVerb} ${normalizedContext}` : displayVerb
161
+ );
162
+ }
163
+ function makeAssistantStatus(verb, context) {
164
+ return {
165
+ text: formatAssistantStatusText(verb, context)
166
+ };
167
+ }
168
+ function renderAssistantStatus(args) {
169
+ const visible = truncateStatusText(
170
+ normalizeSlackStatusText(args.status.text)
171
+ );
172
+ return {
173
+ key: visible,
174
+ visible
175
+ };
176
+ }
177
+ function selectAssistantLoadingMessages(args) {
178
+ const random = args.random ?? Math.random;
179
+ const normalized = Array.from(
180
+ new Set(
181
+ args.messages.map((message) => truncateStatusText(normalizeSlackStatusText(message))).filter((message) => message.length > 0)
182
+ )
183
+ );
184
+ if (normalized.length === 0) {
185
+ return void 0;
186
+ }
187
+ const shuffled = [...normalized];
188
+ for (let index = shuffled.length - 1; index > 0; index -= 1) {
189
+ const otherIndex = Math.floor(random() * (index + 1));
190
+ [shuffled[index], shuffled[otherIndex]] = [
191
+ shuffled[otherIndex],
192
+ shuffled[index]
193
+ ];
194
+ }
195
+ return shuffled.slice(0, 10);
196
+ }
197
+
198
+ // src/chat/slack/assistant-thread/status-scheduler.ts
199
+ var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
200
+ var STATUS_MIN_VISIBLE_MS = 1200;
201
+ var STATUS_ROTATION_INTERVAL_MS = 3e4;
202
+ function createAssistantStatusScheduler(args) {
203
+ const now3 = args.now ?? (() => Date.now());
204
+ const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
205
+ const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
206
+ const random = args.random ?? Math.random;
207
+ const loadingMessages = selectAssistantLoadingMessages({
208
+ messages: args.loadingMessages ?? [],
209
+ random
210
+ });
211
+ const defaultStatus = makeAssistantStatus("thinking");
212
+ let active = false;
213
+ let currentKey = "";
214
+ let currentVisibleStatus = "";
215
+ let currentLoadingMessages;
216
+ let lastStatusAt = 0;
217
+ let pendingStatus = null;
218
+ let pendingKey = "";
219
+ let pendingTimer = null;
220
+ let rotationTimer = null;
221
+ let inflightStatusUpdate = Promise.resolve();
222
+ const enqueueStatusUpdate = (task) => {
223
+ const request = inflightStatusUpdate.catch(() => void 0).then(async () => {
224
+ await task();
225
+ });
226
+ inflightStatusUpdate = request.catch(() => void 0);
227
+ return request;
228
+ };
229
+ const scheduleRotation = () => {
230
+ if (rotationTimer) {
231
+ clearTimer(rotationTimer);
232
+ rotationTimer = null;
233
+ }
234
+ if (!active || !currentVisibleStatus) {
235
+ return;
236
+ }
237
+ rotationTimer = setTimer(() => {
238
+ rotationTimer = null;
239
+ if (!active || !currentVisibleStatus) {
240
+ return;
241
+ }
242
+ void postStatus(currentVisibleStatus, currentLoadingMessages);
243
+ }, STATUS_ROTATION_INTERVAL_MS);
244
+ };
245
+ const getLoadingMessagesForVisibleStatus = (visible) => visible ? [visible] : void 0;
246
+ const getInitialStatusText = () => {
247
+ if (loadingMessages?.length) {
248
+ return loadingMessages[0];
249
+ }
250
+ return defaultStatus.text;
251
+ };
252
+ const haveSameLoadingMessages = (left, right) => {
253
+ if (left === right) {
254
+ return true;
255
+ }
256
+ if (!left || !right || left.length !== right.length) {
257
+ return false;
258
+ }
259
+ return left.every((message, index) => message === right[index]);
260
+ };
261
+ const postStatus = async (text, nextLoadingMessages) => {
262
+ if (!text && !currentVisibleStatus) {
263
+ return;
264
+ }
265
+ currentVisibleStatus = text;
266
+ currentLoadingMessages = nextLoadingMessages;
267
+ lastStatusAt = now3();
268
+ scheduleRotation();
269
+ await enqueueStatusUpdate(async () => {
270
+ await args.sendStatus(text, nextLoadingMessages);
271
+ });
272
+ };
273
+ const postRenderedStatus = async (status) => {
274
+ const presentation = renderAssistantStatus({
275
+ status
276
+ });
277
+ const nextLoadingMessages = getLoadingMessagesForVisibleStatus(
278
+ presentation.visible
279
+ );
280
+ currentKey = presentation.key;
281
+ await postStatus(presentation.visible, nextLoadingMessages);
282
+ };
283
+ const clearPending = () => {
284
+ if (pendingTimer) {
285
+ clearTimer(pendingTimer);
286
+ pendingTimer = null;
287
+ }
288
+ pendingStatus = null;
289
+ pendingKey = "";
290
+ };
291
+ const flushPending = async () => {
292
+ if (!active || !pendingStatus) {
293
+ clearPending();
294
+ return;
295
+ }
296
+ const next = pendingStatus;
297
+ clearPending();
298
+ const nextPresentation = renderAssistantStatus({
299
+ status: next
300
+ });
301
+ if (nextPresentation.key !== currentKey) {
302
+ await postRenderedStatus(next);
303
+ }
304
+ };
305
+ return {
306
+ async clear() {
307
+ active = false;
308
+ clearPending();
309
+ if (rotationTimer) {
310
+ clearTimer(rotationTimer);
311
+ rotationTimer = null;
312
+ }
313
+ currentKey = "";
314
+ await postStatus("");
315
+ },
316
+ update(status) {
317
+ if (!active) {
318
+ active = true;
319
+ clearPending();
320
+ if (status) {
321
+ void postRenderedStatus(status);
322
+ return;
323
+ }
324
+ currentKey = "initial";
325
+ void postStatus(getInitialStatusText(), loadingMessages);
326
+ return;
327
+ }
328
+ if (!status) {
329
+ clearPending();
330
+ currentKey = "initial";
331
+ void postStatus(getInitialStatusText(), loadingMessages);
332
+ return;
333
+ }
334
+ const presentation = renderAssistantStatus({
335
+ status
336
+ });
337
+ if (!presentation.visible) {
338
+ return;
339
+ }
340
+ if (presentation.key === currentKey || presentation.key === pendingKey) {
341
+ return;
342
+ }
343
+ if (presentation.visible === currentVisibleStatus) {
344
+ clearPending();
345
+ currentKey = presentation.key;
346
+ const nextLoadingMessages = getLoadingMessagesForVisibleStatus(
347
+ presentation.visible
348
+ );
349
+ if (!haveSameLoadingMessages(currentLoadingMessages, nextLoadingMessages)) {
350
+ void postStatus(presentation.visible, nextLoadingMessages);
351
+ }
352
+ return;
353
+ }
354
+ const elapsed = now3() - lastStatusAt;
355
+ const waitMs = Math.max(
356
+ STATUS_UPDATE_DEBOUNCE_MS - elapsed,
357
+ STATUS_MIN_VISIBLE_MS - elapsed,
358
+ 0
359
+ );
360
+ if (waitMs <= 0) {
361
+ clearPending();
362
+ void postRenderedStatus(status);
363
+ return;
364
+ }
365
+ pendingStatus = status;
366
+ pendingKey = presentation.key;
367
+ if (pendingTimer) {
368
+ return;
369
+ }
370
+ pendingTimer = setTimer(
371
+ () => {
372
+ pendingTimer = null;
373
+ void flushPending();
374
+ },
375
+ Math.max(1, waitMs)
376
+ );
377
+ }
378
+ };
379
+ }
380
+
381
+ // src/chat/slack/assistant-thread/status-send.ts
382
+ var SLACK_ASSISTANT_ACTIVE_STATUS = "is working on your request...";
383
+ function createSlackAdapterStatusSender(args) {
384
+ const adapter = args.getSlackAdapter();
385
+ const boundToken = getSlackAdapterRequestToken(adapter);
386
+ return async (text, loadingMessages) => {
387
+ const channelId = args.channelId;
388
+ const threadTs = args.threadTs;
389
+ if (!channelId || !threadTs) {
390
+ return;
391
+ }
392
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
393
+ if (!normalizedChannelId) {
394
+ return;
395
+ }
396
+ const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
397
+ try {
398
+ await runWithBoundSlackToken(
399
+ adapter,
400
+ boundToken,
401
+ () => adapter.setAssistantStatus(
402
+ normalizedChannelId,
403
+ threadTs,
404
+ text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
405
+ nextLoadingMessages
406
+ )
407
+ );
408
+ } catch (error) {
409
+ logAssistantStatusFailure({
410
+ status: text,
411
+ error,
412
+ channelId,
413
+ normalizedChannelId,
414
+ threadTs
415
+ });
416
+ }
417
+ };
418
+ }
419
+ function createSlackWebApiStatusSender(args) {
420
+ const getClient = args.getSlackClient ?? getSlackClient;
421
+ return async (text, loadingMessages) => {
422
+ const channelId = args.channelId;
423
+ const threadTs = args.threadTs;
424
+ if (!channelId || !threadTs) {
425
+ return;
426
+ }
427
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
428
+ if (!normalizedChannelId) {
429
+ return;
430
+ }
431
+ const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
432
+ try {
433
+ await getClient().assistant.threads.setStatus({
434
+ channel_id: normalizedChannelId,
435
+ thread_ts: threadTs,
436
+ status: text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
437
+ ...nextLoadingMessages ? { loading_messages: nextLoadingMessages } : {}
438
+ });
439
+ } catch (error) {
440
+ logAssistantStatusFailure({
441
+ status: text,
442
+ error,
443
+ channelId,
444
+ normalizedChannelId,
445
+ threadTs
446
+ });
447
+ }
448
+ };
449
+ }
450
+ function getSlackAdapterRequestToken(adapter) {
451
+ const token = adapter.requestContext?.getStore()?.token;
452
+ if (typeof token !== "string") {
453
+ return void 0;
454
+ }
455
+ const trimmed = token.trim();
456
+ return trimmed || void 0;
457
+ }
458
+ async function runWithBoundSlackToken(adapter, token, task) {
459
+ if (!token) {
460
+ return await task();
461
+ }
462
+ return await adapter.withBotToken(token, task);
463
+ }
464
+ function logAssistantStatusFailure(args) {
465
+ logWarn(
466
+ "assistant_status_update_failed",
467
+ {},
468
+ {
469
+ "app.slack.status_text": args.status || "(clear)",
470
+ "app.slack.channel_id_raw": args.channelId,
471
+ "app.slack.channel_id": args.normalizedChannelId,
472
+ "app.slack.thread_ts": args.threadTs,
473
+ "exception.message": args.error instanceof Error ? args.error.message : String(args.error)
474
+ },
475
+ `Failed to update assistant status channel=${args.normalizedChannelId} raw=${args.channelId} thread=${args.threadTs}`
476
+ );
477
+ }
478
+
479
+ // src/chat/slack/assistant-thread/status.ts
480
+ function createSlackAdapterAssistantStatusSession(args) {
481
+ return createAssistantStatusScheduler({
482
+ sendStatus: createSlackAdapterStatusSender({
483
+ channelId: args.channelId,
484
+ threadTs: args.threadTs,
485
+ getSlackAdapter: args.getSlackAdapter
486
+ }),
487
+ loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
488
+ now: args.now,
489
+ setTimer: args.setTimer,
490
+ clearTimer: args.clearTimer,
491
+ random: args.random
492
+ });
493
+ }
494
+ function createSlackWebApiAssistantStatusSession(args) {
495
+ return createAssistantStatusScheduler({
496
+ sendStatus: createSlackWebApiStatusSender({
497
+ channelId: args.channelId,
498
+ threadTs: args.threadTs,
499
+ getSlackClient: args.getSlackClient
500
+ }),
501
+ loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
502
+ now: args.now,
503
+ setTimer: args.setTimer,
504
+ clearTimer: args.clearTimer,
505
+ random: args.random
506
+ });
507
+ }
508
+
509
+ // src/chat/slack/footer.ts
510
+ function buildSlackReplyFooter(args) {
511
+ const items = [];
512
+ const conversationId = args.conversationId?.trim();
513
+ if (conversationId) {
514
+ const idItem = {
515
+ label: "ID",
516
+ value: conversationId
517
+ };
518
+ const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? getDashboardConversationLink(conversationId) ?? buildSentryConversationUrl(conversationId);
519
+ if (conversationUrl) {
520
+ idItem.url = conversationUrl;
521
+ }
522
+ items.push(idItem);
523
+ }
524
+ return items.length > 0 ? { items } : void 0;
525
+ }
526
+ function buildSlackReplyBlocks(text, footer) {
527
+ if (!text.trim()) {
528
+ return void 0;
529
+ }
530
+ const blocks = [
531
+ {
532
+ type: "markdown",
533
+ text
534
+ }
535
+ ];
536
+ if (footer?.items.length) {
537
+ blocks.push({
538
+ type: "context",
539
+ elements: footer.items.map((item) => ({
540
+ type: "mrkdwn",
541
+ text: item.url ? `*${escapeSlackMrkdwnText(item.label)}:* ${formatSlackLink(item.url, item.value)}` : `*${escapeSlackMrkdwnText(item.label)}:* ${escapeSlackMrkdwnText(item.value)}`
542
+ }))
543
+ });
544
+ }
545
+ return blocks;
546
+ }
547
+
548
+ // src/chat/slack/reply.ts
549
+ async function sendSlackReply(args) {
550
+ const chunks = splitSlackReplyText(args.text);
551
+ const footer = buildSlackReplyFooter({
552
+ conversationId: args.conversationId
553
+ });
554
+ let lastMessageTs;
555
+ for (const [index, text] of chunks.entries()) {
556
+ const blocks = buildSlackReplyBlocks(
557
+ text,
558
+ index === chunks.length - 1 ? footer : void 0
559
+ );
560
+ const response = await postSlackMessage({
561
+ channelId: args.channelId,
562
+ threadTs: args.threadTs,
563
+ text,
564
+ ...blocks ? { blocks } : {}
565
+ });
566
+ lastMessageTs = response.ts;
567
+ }
568
+ return lastMessageTs;
569
+ }
570
+
571
+ // src/chat/slack/errors.ts
572
+ function getSlackApiErrorCode(error) {
573
+ if (!error || typeof error !== "object") {
574
+ return void 0;
575
+ }
576
+ const candidate = error;
577
+ if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
578
+ return candidate.data.error;
579
+ }
580
+ if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
581
+ return candidate.code;
582
+ }
583
+ return void 0;
584
+ }
585
+ function getSlackErrorObservabilityAttributes(error) {
586
+ if (!error || typeof error !== "object") {
587
+ return {};
588
+ }
589
+ const candidate = error;
590
+ const attributes = {};
591
+ if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
592
+ attributes["app.slack.error_code"] = candidate.code;
593
+ }
594
+ if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
595
+ attributes["app.slack.api_error"] = candidate.data.error;
596
+ }
597
+ const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
598
+ if (requestId) {
599
+ attributes["app.slack.request_id"] = requestId;
600
+ }
601
+ if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) {
602
+ attributes["http.response.status_code"] = candidate.statusCode;
603
+ }
604
+ return attributes;
605
+ }
606
+ var TRANSIENT_SLACK_API_ERRORS = /* @__PURE__ */ new Set([
607
+ "fatal_error",
608
+ "internal_error",
609
+ // chat.postMessage documents both spellings.
610
+ "rate_limited",
611
+ "ratelimited",
612
+ "request_timeout",
613
+ "service_unavailable"
614
+ ]);
615
+ function isRetryableSlackPostError(error) {
616
+ if (!error || typeof error !== "object") {
617
+ return true;
618
+ }
619
+ const candidate = error;
620
+ if (typeof candidate.statusCode === "number") {
621
+ if (candidate.statusCode === 429 || candidate.statusCode >= 500) {
622
+ return true;
623
+ }
624
+ if (candidate.statusCode >= 400) {
625
+ return false;
626
+ }
627
+ }
628
+ const apiError = error instanceof SlackActionError ? error.apiError : typeof candidate.data?.error === "string" ? candidate.data.error : void 0;
629
+ if (apiError) {
630
+ return TRANSIENT_SLACK_API_ERRORS.has(apiError);
631
+ }
632
+ if (error instanceof SlackActionError) {
633
+ return error.code === "rate_limited" || error.code === "internal_error";
634
+ }
635
+ return true;
636
+ }
637
+ function isSlackTitlePermissionError(error) {
638
+ const code = getSlackApiErrorCode(error);
639
+ return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
640
+ }
641
+
642
+ // src/chat/runtime/thread-context.ts
643
+ function escapeRegExp(value) {
644
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
645
+ }
646
+ function readObjectField(value, key) {
647
+ return value[key];
648
+ }
649
+ function readOptionalStringField(value, key) {
650
+ return toOptionalString(readObjectField(value, key));
651
+ }
652
+ function stripLeadingBotMention(text, options = {}) {
653
+ if (!text.trim()) return text;
654
+ let next = text;
655
+ if (options.stripLeadingSlackMentionToken) {
656
+ if (options.botUserId) {
657
+ const botUserId = escapeRegExp(options.botUserId);
658
+ const mentionByBotUserIdRe = new RegExp(
659
+ `^\\s*(?:<@${botUserId}(?:\\|[^>]+)?>|@${botUserId})[\\s,:-]*`,
660
+ "i"
661
+ );
662
+ next = next.replace(mentionByBotUserIdRe, "").trim();
663
+ } else {
664
+ next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
665
+ }
666
+ }
667
+ const mentionByNameRe = new RegExp(
668
+ `^\\s*@${escapeRegExp(botConfig.userName)}\\b[\\s,:-]*`,
669
+ "i"
670
+ );
671
+ next = next.replace(mentionByNameRe, "").trim();
672
+ const mentionByLabeledEntityRe = new RegExp(
673
+ `^\\s*<@[^>|]+\\|${escapeRegExp(botConfig.userName)}>[\\s,:-]*`,
674
+ "i"
675
+ );
676
+ next = next.replace(mentionByLabeledEntityRe, "").trim();
677
+ return next;
678
+ }
679
+ function getThreadId(thread, _message) {
680
+ return toOptionalString(thread.id);
681
+ }
682
+ function getRunId(thread, message) {
683
+ return readOptionalStringField(thread, "runId") ?? readOptionalStringField(message, "runId");
684
+ }
685
+ function getChannelId(thread, message) {
686
+ return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
687
+ }
688
+ function getThreadTs(threadId) {
689
+ return parseSlackThreadId(threadId)?.threadTs;
690
+ }
691
+ function getAssistantThreadContext(message) {
692
+ const rawContext = readSlackRawMessageContext(message);
693
+ const channelId = rawContext?.channelId;
694
+ if (channelId) {
695
+ const threadTs = isDmChannel(channelId) ? rawContext.threadTs : rawContext.threadTs ?? rawContext.messageTs;
696
+ if (threadTs) {
697
+ return { channelId, threadTs };
698
+ }
699
+ }
700
+ const parsedThreadId = parseSlackThreadId(
701
+ readOptionalStringField(message, "threadId")
702
+ );
703
+ if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
704
+ return void 0;
705
+ }
706
+ return parsedThreadId;
707
+ }
708
+ function getMessageTs(message) {
709
+ const directTs = readOptionalStringField(message, "ts");
710
+ const parsedDirectTs = parseSlackMessageTs(directTs);
711
+ if (parsedDirectTs) {
712
+ return parsedDirectTs;
713
+ }
714
+ const rawContext = readSlackRawMessageContext(message);
715
+ if (!rawContext) {
716
+ return void 0;
717
+ }
718
+ const candidates = [rawContext.messageTs, rawContext.nestedMessageTs];
719
+ for (const candidate of candidates) {
720
+ const ts = parseSlackMessageTs(candidate);
721
+ if (ts) {
722
+ return ts;
723
+ }
724
+ }
725
+ return void 0;
726
+ }
727
+
728
+ // src/chat/runtime/processing-reaction.ts
729
+ var noProcessingReaction = {
730
+ complete: async () => void 0,
731
+ keep: () => void 0,
732
+ stop: async () => void 0
733
+ };
734
+ function isProcessingReactionEmoji(value) {
735
+ return typeof value === "string" && normalizeSlackEmojiName(value) === getChatConfig().slack.processingReactionEmoji;
736
+ }
737
+ function shouldKeepProcessingReactionForToolInvocation(input) {
738
+ return input.toolName === "addReaction" && isProcessingReactionEmoji(input.params.emoji);
739
+ }
740
+ async function startSlackProcessingReaction(args) {
741
+ if (args.message.author.isMe) {
742
+ return noProcessingReaction;
743
+ }
744
+ const channelId = getChannelId(args.thread, args.message);
745
+ const messageTs = getMessageTs(args.message);
746
+ if (!channelId || !messageTs) {
747
+ return noProcessingReaction;
748
+ }
749
+ return startSlackProcessingReactionForMessage({
750
+ channelId,
751
+ timestamp: messageTs,
752
+ logException: args.logException,
753
+ logContext: args.logContext
754
+ });
755
+ }
756
+ async function startSlackProcessingReactionForMessage(args) {
757
+ try {
758
+ await addReactionToMessage({
759
+ channelId: args.channelId,
760
+ timestamp: args.timestamp,
761
+ emoji: getChatConfig().slack.processingReactionEmoji
762
+ });
763
+ } catch (error) {
764
+ args.logException(
765
+ error,
766
+ "slack_processing_reaction_add_failed",
767
+ args.logContext,
768
+ {
769
+ "app.slack.action": "reactions.add",
770
+ "messaging.message.id": args.timestamp,
771
+ ...getSlackErrorObservabilityAttributes(error)
772
+ },
773
+ "Failed to add Slack processing reaction"
774
+ );
775
+ return noProcessingReaction;
776
+ }
777
+ let shouldRemove = true;
778
+ const removeProcessingReaction = async () => {
779
+ if (!shouldRemove) {
780
+ return false;
781
+ }
782
+ try {
783
+ await removeReactionFromMessage({
784
+ channelId: args.channelId,
785
+ timestamp: args.timestamp,
786
+ emoji: getChatConfig().slack.processingReactionEmoji
787
+ });
788
+ return true;
789
+ } catch (error) {
790
+ args.logException(
791
+ error,
792
+ "slack_processing_reaction_remove_failed",
793
+ args.logContext,
794
+ {
795
+ "app.slack.action": "reactions.remove",
796
+ "messaging.message.id": args.timestamp,
797
+ ...getSlackErrorObservabilityAttributes(error)
798
+ },
799
+ "Failed to remove Slack processing reaction"
800
+ );
801
+ return false;
802
+ }
803
+ };
804
+ return {
805
+ complete: async () => {
806
+ const shouldAddCompleted = shouldRemove;
807
+ await removeProcessingReaction();
808
+ if (!shouldAddCompleted) {
809
+ return;
810
+ }
811
+ try {
812
+ await addReactionToMessage({
813
+ channelId: args.channelId,
814
+ timestamp: args.timestamp,
815
+ emoji: getChatConfig().slack.completedReactionEmoji
816
+ });
817
+ } catch (error) {
818
+ args.logException(
819
+ error,
820
+ "slack_processing_reaction_complete_failed",
821
+ args.logContext,
822
+ {
823
+ "app.slack.action": "reactions.add",
824
+ "messaging.message.id": args.timestamp,
825
+ ...getSlackErrorObservabilityAttributes(error)
826
+ },
827
+ "Failed to add Slack completed reaction"
828
+ );
829
+ }
830
+ },
831
+ keep: () => {
832
+ shouldRemove = false;
833
+ },
834
+ stop: async () => {
835
+ await removeProcessingReaction();
836
+ }
837
+ };
838
+ }
839
+
840
+ // src/chat/services/auth-pause-response.ts
841
+ var MAX_AUTH_REQUEST_LENGTH = 200;
842
+ function formatAuthRequest(requestText) {
843
+ const normalized = requestText.replace(/\s+/g, " ").trim();
844
+ if (!normalized) {
845
+ return void 0;
846
+ }
847
+ const bounded = normalized.length > MAX_AUTH_REQUEST_LENGTH ? `${normalized.slice(0, MAX_AUTH_REQUEST_LENGTH - 1).trimEnd()}\u2026` : normalized;
848
+ return bounded.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
849
+ }
850
+ function buildAuthPauseResponse(slackUserId, providerDisplayName, requestText) {
851
+ const mention = slackUserId ? `<@${slackUserId}> ` : "";
852
+ const request = requestText ? formatAuthRequest(requestText) : void 0;
853
+ if (!request) {
854
+ return `${mention}I'll need you to authorize ${providerDisplayName}. I sent you a link.`;
855
+ }
856
+ return `${mention}I need access to ${providerDisplayName} to continue.
857
+
858
+ *Why:* ${request}
859
+
860
+ I sent you a link.`;
861
+ }
862
+
863
+ // src/chat/runtime/request-deadline.ts
864
+ import { AsyncLocalStorage } from "async_hooks";
865
+ var requestDeadlineStorage = new AsyncLocalStorage();
866
+ function createTurnRequestDeadline(startedAtMs) {
867
+ const requestBudgetMs = Math.max(
868
+ 1,
869
+ getChatConfig().functionMaxDurationSeconds * 1e3 - FUNCTION_TIMEOUT_BUFFER_SECONDS * 1e3
870
+ );
871
+ return {
872
+ startedAtMs,
873
+ deadlineAtMs: startedAtMs + requestBudgetMs
874
+ };
875
+ }
876
+ function getTurnRequestDeadline() {
877
+ return requestDeadlineStorage.getStore();
878
+ }
879
+ function runWithTurnRequestDeadline(callback, startedAtMs = Date.now()) {
880
+ const existingDeadline = requestDeadlineStorage.getStore();
881
+ if (existingDeadline) {
882
+ return callback();
883
+ }
884
+ return requestDeadlineStorage.run(
885
+ createTurnRequestDeadline(startedAtMs),
886
+ callback
887
+ );
888
+ }
889
+
890
+ // src/chat/runtime/slack-resume.ts
891
+ function resolveReplyTimeoutMs(explicitTimeoutMs) {
892
+ if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
893
+ return explicitTimeoutMs;
894
+ }
895
+ const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
896
+ if (!raw) {
897
+ return void 0;
898
+ }
899
+ const parsed = Number.parseInt(raw, 10);
900
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
901
+ }
902
+ async function postSlackMessageBestEffort(channelId, threadTs, text, conversationId) {
903
+ try {
904
+ if (conversationId) {
905
+ await sendSlackReply({
906
+ channelId,
907
+ conversationId,
908
+ text,
909
+ threadTs
910
+ });
911
+ return;
912
+ }
913
+ await postSlackMessage({ channelId, threadTs, text });
914
+ } catch {
915
+ }
916
+ }
917
+ function createReadOnlyConfigService(values) {
918
+ const entries = Object.entries(values).map(([key, value]) => ({
919
+ key,
920
+ value,
921
+ scope: "conversation",
922
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
923
+ }));
924
+ return {
925
+ get: async (key) => entries.find((entry) => entry.key === key),
926
+ set: async () => {
927
+ throw new Error("Read-only configuration in resumed context");
928
+ },
929
+ unset: async () => false,
930
+ list: async ({ prefix } = {}) => entries.filter((entry) => !prefix || entry.key.startsWith(prefix)),
931
+ resolve: async (key) => values[key],
932
+ resolveValues: async ({ keys, prefix } = {}) => {
933
+ const filtered = {};
934
+ for (const [key, value] of Object.entries(values)) {
935
+ if (prefix && !key.startsWith(prefix)) continue;
936
+ if (keys && !keys.includes(key)) continue;
937
+ filtered[key] = value;
938
+ }
939
+ return filtered;
940
+ }
941
+ };
942
+ }
943
+ var ResumeTurnBusyError = class extends Error {
944
+ constructor(lockKey) {
945
+ super(`A turn already owns resume lock "${lockKey}"`);
946
+ this.name = "ResumeTurnBusyError";
947
+ }
948
+ };
949
+ function getDefaultLockKey(channelId, threadTs) {
950
+ return `slack:${channelId}:${threadTs}`;
951
+ }
952
+ function getResumeLogContext(args, lockKey) {
953
+ const routing = args.replyContext?.routing;
954
+ const actor = routing?.actor;
955
+ return {
956
+ conversationId: args.conversationId,
957
+ messageConversationId: lockKey,
958
+ userId: isUserActor(actor) ? actor.userId : void 0,
959
+ userName: isUserActor(actor) ? actor.userName : void 0,
960
+ destinationName: args.channelId,
961
+ assistantUserName: botConfig.userName,
962
+ modelId: standardModelId(botConfig)
963
+ };
964
+ }
965
+ async function postResumeFailureReply(args) {
966
+ await postSlackMessage({
967
+ channelId: args.channelId,
968
+ threadTs: args.threadTs,
969
+ text: args.error instanceof TurnSliceLimitExceededError ? buildTurnLimitResponse(args.eventId) : buildTurnFailureResponse(args.eventId)
970
+ });
971
+ }
972
+ async function handleResumeFailure(args) {
973
+ const logContext = getResumeLogContext(args.resumeArgs, args.lockKey);
974
+ const capturedEventId = logException(
975
+ args.error,
976
+ args.eventName,
977
+ logContext,
978
+ {},
979
+ args.body
980
+ );
981
+ const eventId = requireTurnFailureEventId(capturedEventId, args.eventName);
982
+ let failureStatePersistError;
983
+ try {
984
+ await args.resumeArgs.onFailure?.(args.error);
985
+ } catch (persistError) {
986
+ const persistEventId = logException(
987
+ persistError,
988
+ "slack_resume_failure_state_persist_failed",
989
+ logContext,
990
+ { "app.error.original_event_id": eventId },
991
+ "Failed to persist resumed turn failure state"
992
+ );
993
+ try {
994
+ await args.turnLifecycle.fail({
995
+ conversationId: args.resumeArgs.conversationId,
996
+ turnId: args.resumeArgs.turnId,
997
+ createdAtMs: Date.now(),
998
+ failureCode: "persistence_failed",
999
+ ...persistEventId ? { eventId: persistEventId } : {}
1000
+ });
1001
+ } catch (lifecycleError) {
1002
+ logException(
1003
+ lifecycleError,
1004
+ "slack_resume_failure_lifecycle_persist_failed",
1005
+ logContext,
1006
+ { "app.error.original_event_id": eventId },
1007
+ "Failed to record resumed turn persistence failure"
1008
+ );
1009
+ }
1010
+ failureStatePersistError = persistError;
1011
+ }
1012
+ try {
1013
+ await postResumeFailureReply({
1014
+ channelId: args.resumeArgs.channelId,
1015
+ threadTs: args.resumeArgs.threadTs,
1016
+ eventId,
1017
+ error: args.error
1018
+ });
1019
+ } catch (deliveryError) {
1020
+ const deliveryEventId = logException(
1021
+ deliveryError,
1022
+ "slack_resume_failure_delivery_failed",
1023
+ logContext,
1024
+ { "app.error.original_event_id": eventId },
1025
+ "Failed to deliver resumed turn failure state"
1026
+ );
1027
+ try {
1028
+ await args.turnLifecycle.fail({
1029
+ conversationId: args.resumeArgs.conversationId,
1030
+ turnId: args.resumeArgs.turnId,
1031
+ createdAtMs: Date.now(),
1032
+ failureCode: "delivery_failed",
1033
+ ...deliveryEventId ? { eventId: deliveryEventId } : {}
1034
+ });
1035
+ } catch (lifecycleError) {
1036
+ logException(
1037
+ lifecycleError,
1038
+ "slack_resume_failure_lifecycle_persist_failed",
1039
+ logContext,
1040
+ { "app.error.original_event_id": eventId },
1041
+ "Failed to record resumed turn delivery failure"
1042
+ );
1043
+ }
1044
+ throw deliveryError;
1045
+ }
1046
+ if (failureStatePersistError) {
1047
+ throw failureStatePersistError;
1048
+ }
1049
+ await args.turnLifecycle.fail({
1050
+ conversationId: args.resumeArgs.conversationId,
1051
+ turnId: args.resumeArgs.turnId,
1052
+ createdAtMs: Date.now(),
1053
+ failureCode: args.failureCode,
1054
+ eventId
1055
+ });
1056
+ }
1057
+ function createResumeReplyContext(args, statusSession, delivery) {
1058
+ const replyContext = args.replyContext;
1059
+ if (!replyContext) {
1060
+ throw new TypeError("Slack resume requires a reply context");
1061
+ }
1062
+ if (!replyContext.routing.source) {
1063
+ throw new TypeError("Slack resume requires a reply context source");
1064
+ }
1065
+ const source = replyContext.routing.source;
1066
+ if (source.platform !== "slack") {
1067
+ throw new TypeError("Slack resume requires a Slack source");
1068
+ }
1069
+ if (replyContext.routing.destination.platform !== "slack") {
1070
+ throw new TypeError("Slack resume requires a Slack destination");
1071
+ }
1072
+ const requestDeadline = getTurnRequestDeadline();
1073
+ const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
1074
+ const persistedChannelConfiguration = replyContext.policy?.channelConfiguration ?? (replyContext.policy?.configuration ? createReadOnlyConfigService(replyContext.policy.configuration) : void 0);
1075
+ return {
1076
+ conversationId: args.conversationId,
1077
+ turnId: args.turnId,
1078
+ input: {
1079
+ ...replyContext.input ?? {},
1080
+ messageText: args.messageText
1081
+ },
1082
+ routing: {
1083
+ ...replyContext.routing,
1084
+ source: {
1085
+ ...source,
1086
+ channelId: args.channelId,
1087
+ threadTs: args.threadTs
1088
+ }
1089
+ },
1090
+ policy: {
1091
+ ...replyContext.policy,
1092
+ turnDeadlineAtMs: replyContext.policy?.turnDeadlineAtMs ?? requestDeadline?.deadlineAtMs,
1093
+ channelConfiguration: persistedChannelConfiguration
1094
+ },
1095
+ state: replyContext.state,
1096
+ observers: {
1097
+ ...replyContext.observers,
1098
+ onStatus: async (nextStatus) => {
1099
+ statusSession.update(nextStatus);
1100
+ await replyContext.observers?.onStatus?.(nextStatus);
1101
+ }
1102
+ },
1103
+ delivery,
1104
+ durability: {
1105
+ ...replyContext.durability,
1106
+ onSandboxRefChanged: async (sandboxRef) => {
1107
+ await persistThreadStateById(threadId, {
1108
+ sandboxRef
1109
+ });
1110
+ await replyContext.durability?.onSandboxRefChanged?.(sandboxRef);
1111
+ },
1112
+ onArtifactStateUpdated: async (artifacts) => {
1113
+ await persistThreadStateById(threadId, { artifacts });
1114
+ await replyContext.durability?.onArtifactStateUpdated?.(artifacts);
1115
+ }
1116
+ }
1117
+ };
1118
+ }
1119
+ async function resumeSlackTurn(args) {
1120
+ const stateAdapter = getStateAdapter();
1121
+ await stateAdapter.connect();
1122
+ const lockKey = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
1123
+ const lock = await acquireActiveLock(stateAdapter, lockKey);
1124
+ if (!lock) {
1125
+ throw new ResumeTurnBusyError(lockKey);
1126
+ }
1127
+ const status = createSlackWebApiAssistantStatusSession({
1128
+ channelId: args.channelId,
1129
+ threadTs: args.threadTs
1130
+ });
1131
+ let processingReaction;
1132
+ let deferredAuthInfo;
1133
+ let deferredPauseHandler;
1134
+ let deferredFailureHandler;
1135
+ let runResultHandled = false;
1136
+ let assistantMessageDelivered = false;
1137
+ let postDeliveryCommitError;
1138
+ const turnLifecycle = args.turnLifecycle ?? new ConversationTurnLifecycleService(getConversationEventStore());
1139
+ let failureCode = "agent_run_failed";
1140
+ let runArgs = args;
1141
+ try {
1142
+ const preparedArgs = await args.beforeStart?.();
1143
+ if (preparedArgs === false) {
1144
+ return false;
1145
+ }
1146
+ if (preparedArgs) {
1147
+ runArgs = { ...args, ...preparedArgs };
1148
+ }
1149
+ const activeReplyContext = runArgs.replyContext;
1150
+ if (!activeReplyContext) {
1151
+ throw new Error("Resumed turn requires replyContext");
1152
+ }
1153
+ const credentialContext = activeReplyContext.routing.credentialContext;
1154
+ if (!credentialContext) {
1155
+ throw new Error("Resumed turn requires replyContext.credentialContext");
1156
+ }
1157
+ const routingActor = activeReplyContext.routing.actor;
1158
+ let resumeActor;
1159
+ if ("type" in credentialContext.actor) {
1160
+ if (!isUserActor(routingActor) || credentialContext.actor.userId !== routingActor.userId) {
1161
+ throw new Error(
1162
+ "Resumed turn credential actor must match replyContext.routing.actor.userId"
1163
+ );
1164
+ }
1165
+ resumeActor = routingActor;
1166
+ } else {
1167
+ if (routingActor && (routingActor.platform !== "system" || routingActor.name !== credentialContext.actor.name)) {
1168
+ throw new Error(
1169
+ "Resumed turn system credential actor must match replyContext.routing.actor"
1170
+ );
1171
+ }
1172
+ resumeActor = credentialContext.actor;
1173
+ }
1174
+ if (runArgs.messageTs) {
1175
+ processingReaction = await startSlackProcessingReactionForMessage({
1176
+ channelId: runArgs.channelId,
1177
+ timestamp: runArgs.messageTs,
1178
+ logException,
1179
+ logContext: { ...getResumeLogContext(runArgs, lockKey) }
1180
+ });
1181
+ }
1182
+ if (runArgs.initialText) {
1183
+ await postSlackMessageBestEffort(
1184
+ runArgs.channelId,
1185
+ runArgs.threadTs,
1186
+ runArgs.initialText
1187
+ );
1188
+ }
1189
+ status.update(runArgs.initialStatus);
1190
+ const conversationId = runArgs.conversationId;
1191
+ const visibleConversationId = lockKey;
1192
+ const sessionId = runArgs.turnId;
1193
+ let deliveryConversation;
1194
+ const getDeliveryConversation = async () => {
1195
+ if (deliveryConversation) {
1196
+ const userMessageId2 = getTurnUserMessage(
1197
+ deliveryConversation,
1198
+ sessionId
1199
+ )?.id;
1200
+ return {
1201
+ conversation: deliveryConversation,
1202
+ sessionId,
1203
+ ...userMessageId2 ? { userMessageId: userMessageId2 } : {}
1204
+ };
1205
+ }
1206
+ const persistedState = await getPersistedThreadState(
1207
+ visibleConversationId
1208
+ );
1209
+ const conversation = coerceThreadConversationState(persistedState);
1210
+ await hydrateConversationMessages({
1211
+ conversation,
1212
+ conversationId
1213
+ });
1214
+ deliveryConversation = conversation;
1215
+ const userMessageId = getTurnUserMessage(conversation, sessionId)?.id;
1216
+ return {
1217
+ conversation,
1218
+ sessionId,
1219
+ ...userMessageId ? { userMessageId } : {}
1220
+ };
1221
+ };
1222
+ const deliverAssistantMessage = async (assistantMessage) => {
1223
+ if (!assistantMessage.text.trim()) {
1224
+ return;
1225
+ }
1226
+ failureCode = "delivery_failed";
1227
+ const deliveryState2 = await getDeliveryConversation();
1228
+ let messageTs;
1229
+ try {
1230
+ messageTs = await sendSlackReply({
1231
+ channelId: runArgs.channelId,
1232
+ conversationId: runArgs.conversationId,
1233
+ text: assistantMessage.text,
1234
+ threadTs: runArgs.threadTs
1235
+ });
1236
+ } catch (error) {
1237
+ if (isRetryableSlackPostError(error)) {
1238
+ throw new RetryableDeliveryError(error);
1239
+ }
1240
+ throw error;
1241
+ }
1242
+ assistantMessageDelivered = true;
1243
+ const recordedMessageId = recordDeliveredAssistantMessage({
1244
+ conversation: deliveryState2.conversation,
1245
+ sessionId: deliveryState2.sessionId,
1246
+ text: assistantMessage.text,
1247
+ userMessageId: deliveryState2.userMessageId
1248
+ });
1249
+ if (messageTs) {
1250
+ markConversationMessage(deliveryState2.conversation, recordedMessageId, {
1251
+ slackTs: messageTs
1252
+ });
1253
+ }
1254
+ try {
1255
+ await persistWithRetry(
1256
+ () => persistConversationMessages({
1257
+ conversation: deliveryState2.conversation,
1258
+ conversationId
1259
+ })
1260
+ );
1261
+ } catch (error) {
1262
+ logException(
1263
+ new Error("Accepted assistant message persistence failed"),
1264
+ "slack_resume_assistant_message_post_delivery_persist_failed",
1265
+ getResumeLogContext(runArgs, lockKey),
1266
+ { "error.type": error instanceof Error ? error.name : typeof error },
1267
+ "Failed to persist an accepted resumed assistant message"
1268
+ );
1269
+ }
1270
+ failureCode = "agent_run_failed";
1271
+ };
1272
+ const deliveryState = await getDeliveryConversation();
1273
+ assistantMessageDelivered = turnHasReply(
1274
+ deliveryState.conversation,
1275
+ sessionId
1276
+ );
1277
+ const replyContext = createResumeReplyContext(runArgs, status, {
1278
+ onAssistantMessage: deliverAssistantMessage
1279
+ });
1280
+ if (runArgs.inputMessageIds?.length) {
1281
+ await turnLifecycle.start({
1282
+ conversationId: runArgs.conversationId,
1283
+ turnId: runArgs.turnId,
1284
+ createdAtMs: Date.now(),
1285
+ inputMessageIds: [...new Set(runArgs.inputMessageIds)],
1286
+ surface: "slack"
1287
+ });
1288
+ }
1289
+ const replyPromise = runArgs.agentRunner.run(replyContext);
1290
+ const replyTimeoutMs = resolveReplyTimeoutMs(runArgs.replyTimeoutMs);
1291
+ const outcome = typeof replyTimeoutMs === "number" ? await Promise.race([
1292
+ replyPromise,
1293
+ new Promise(
1294
+ (_, reject) => setTimeout(
1295
+ () => reject(
1296
+ new Error(
1297
+ `executeAgentRun timed out after ${replyTimeoutMs}ms`
1298
+ )
1299
+ ),
1300
+ replyTimeoutMs
1301
+ )
1302
+ )
1303
+ ]) : await replyPromise;
1304
+ if (outcome.status !== "completed") {
1305
+ await status.clear();
1306
+ const onAuthPause = runArgs.onAuthPause;
1307
+ const onSuspend = runArgs.onSuspend;
1308
+ if (outcome.status === "awaiting_auth" && onAuthPause) {
1309
+ deferredAuthInfo = {
1310
+ providerDisplayName: outcome.providerDisplayName,
1311
+ actorId: isUserActor(resumeActor) ? resumeActor.userId : void 0,
1312
+ ...outcome.requestText ? { requestText: outcome.requestText } : {}
1313
+ };
1314
+ deferredPauseHandler = async () => {
1315
+ await onAuthPause({
1316
+ providerDisplayName: outcome.providerDisplayName
1317
+ });
1318
+ };
1319
+ } else if (outcome.status === "suspended" && onSuspend) {
1320
+ deferredPauseHandler = async () => {
1321
+ await onSuspend(outcome.resumeVersion);
1322
+ };
1323
+ } else {
1324
+ deferredFailureHandler = async () => {
1325
+ await handleResumeFailure({
1326
+ body: "Failed to resume Slack turn",
1327
+ error: new Error(
1328
+ `Resumed run ended ${outcome.status} without a pause handler`
1329
+ ),
1330
+ eventName: "slack_resume_turn_failed",
1331
+ failureCode: "agent_run_failed",
1332
+ turnLifecycle,
1333
+ lockKey,
1334
+ resumeArgs: runArgs
1335
+ });
1336
+ };
1337
+ }
1338
+ } else {
1339
+ const finalized = finalizeFailedTurnReplyWithEvent({
1340
+ reply: outcome.result,
1341
+ logException,
1342
+ context: getResumeLogContext(runArgs, lockKey)
1343
+ });
1344
+ const reply = finalized.reply;
1345
+ if (reply.diagnostics.outcome !== "success") {
1346
+ await deliverAssistantMessage({ text: reply.text });
1347
+ }
1348
+ runResultHandled = true;
1349
+ await status.clear();
1350
+ failureCode = "persistence_failed";
1351
+ if (reply.piMessages?.length) {
1352
+ await persistCompletedSessionRecord({
1353
+ conversationId: runArgs.conversationId,
1354
+ sessionId: runArgs.turnId,
1355
+ allMessages: reply.piMessages,
1356
+ modelId: reply.diagnostics.modelId,
1357
+ currentDurationMs: reply.diagnostics.durationMs,
1358
+ currentUsage: reply.diagnostics.usage,
1359
+ destination: replyContext.routing.destination,
1360
+ source: replyContext.routing.source,
1361
+ actor: resumeActor,
1362
+ surface: "slack"
1363
+ });
1364
+ }
1365
+ await runArgs.commitResult?.(reply);
1366
+ if (reply.diagnostics.outcome === "success") {
1367
+ await turnLifecycle.complete({
1368
+ conversationId: runArgs.conversationId,
1369
+ turnId: runArgs.turnId,
1370
+ createdAtMs: Date.now(),
1371
+ outcome: assistantMessageDelivered ? "success" : "no_reply"
1372
+ });
1373
+ } else {
1374
+ await turnLifecycle.fail({
1375
+ conversationId: runArgs.conversationId,
1376
+ turnId: runArgs.turnId,
1377
+ createdAtMs: Date.now(),
1378
+ failureCode: "model_execution_failed",
1379
+ ...finalized.eventId ? { eventId: finalized.eventId } : {}
1380
+ });
1381
+ }
1382
+ if (reply.diagnostics.outcome === "success") {
1383
+ try {
1384
+ const params = {
1385
+ conversationId: runArgs.conversationId,
1386
+ sessionId: runArgs.turnId
1387
+ };
1388
+ if (runArgs.scheduleSessionCompletedPluginTasks) {
1389
+ await runArgs.scheduleSessionCompletedPluginTasks(params);
1390
+ } else {
1391
+ await scheduleSessionCompletedPluginTasks(params);
1392
+ }
1393
+ } catch (scheduleError) {
1394
+ logException(
1395
+ scheduleError,
1396
+ "plugin_session_completed_task_schedule_failed",
1397
+ getResumeLogContext(runArgs, lockKey),
1398
+ {},
1399
+ "Plugin session.completed task scheduling failed"
1400
+ );
1401
+ }
1402
+ }
1403
+ }
1404
+ } catch (error) {
1405
+ await status.clear();
1406
+ if (runResultHandled) {
1407
+ postDeliveryCommitError = error;
1408
+ try {
1409
+ await runArgs.onPostDeliveryCommitFailure?.(error);
1410
+ } catch (terminalizeError) {
1411
+ logException(
1412
+ terminalizeError,
1413
+ "slack_resume_post_delivery_terminalize_failed",
1414
+ getResumeLogContext(runArgs, lockKey),
1415
+ {},
1416
+ "Failed to terminalize resumed turn after post-delivery commit failure"
1417
+ );
1418
+ }
1419
+ } else {
1420
+ deferredFailureHandler = async () => {
1421
+ await handleResumeFailure({
1422
+ body: "Failed to resume Slack turn",
1423
+ error,
1424
+ eventName: "slack_resume_turn_failed",
1425
+ failureCode,
1426
+ turnLifecycle,
1427
+ lockKey,
1428
+ resumeArgs: runArgs
1429
+ });
1430
+ };
1431
+ }
1432
+ } finally {
1433
+ if (runResultHandled) {
1434
+ await processingReaction?.complete();
1435
+ } else {
1436
+ await processingReaction?.stop();
1437
+ }
1438
+ await stateAdapter.releaseLock(lock);
1439
+ }
1440
+ if (postDeliveryCommitError) {
1441
+ const eventId = logException(
1442
+ postDeliveryCommitError,
1443
+ "slack_resume_success_handler_failed",
1444
+ getResumeLogContext(runArgs, lockKey),
1445
+ {},
1446
+ "Failed to persist resumed turn state after assistant output handling"
1447
+ );
1448
+ await turnLifecycle.fail({
1449
+ conversationId: runArgs.conversationId,
1450
+ turnId: runArgs.turnId,
1451
+ createdAtMs: Date.now(),
1452
+ failureCode: "persistence_failed",
1453
+ ...eventId ? { eventId } : {}
1454
+ });
1455
+ throw postDeliveryCommitError;
1456
+ }
1457
+ if (deferredPauseHandler) {
1458
+ try {
1459
+ await deferredPauseHandler();
1460
+ if (deferredAuthInfo) {
1461
+ await postSlackMessageBestEffort(
1462
+ runArgs.channelId,
1463
+ runArgs.threadTs,
1464
+ buildAuthPauseResponse(
1465
+ deferredAuthInfo.actorId,
1466
+ deferredAuthInfo.providerDisplayName,
1467
+ deferredAuthInfo.requestText
1468
+ ),
1469
+ runArgs.conversationId
1470
+ );
1471
+ }
1472
+ return true;
1473
+ } catch (pauseError) {
1474
+ await handleResumeFailure({
1475
+ body: "Failed to handle resumed turn pause",
1476
+ error: pauseError,
1477
+ eventName: "slack_resume_pause_handler_failed",
1478
+ failureCode: "persistence_failed",
1479
+ turnLifecycle,
1480
+ lockKey,
1481
+ resumeArgs: runArgs
1482
+ });
1483
+ return true;
1484
+ }
1485
+ }
1486
+ if (deferredFailureHandler) {
1487
+ await deferredFailureHandler();
1488
+ }
1489
+ return true;
1490
+ }
1491
+
1492
+ // src/chat/task-execution/state.ts
1493
+ import { randomUUID } from "crypto";
1494
+ import { destinationSchema } from "@sentry/junior-plugin-api";
1495
+ import { z } from "zod";
1496
+ var CONVERSATION_PREFIX = "junior:conversation";
1497
+ var CONVERSATION_SCHEMA_VERSION = 2;
1498
+ var CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH = 1e4;
1499
+ var CONVERSATION_INDEX_LOCK_TTL_MS = 1e4;
1500
+ var CONVERSATION_INDEX_LOCK_WAIT_MS = 2e3;
1501
+ var CONVERSATION_INDEX_LOCK_RETRY_MS = 25;
1502
+ var CONVERSATION_MUTATION_LOCK_TTL_MS = 1e4;
1503
+ var CONVERSATION_MUTATION_WAIT_MS = 1e4;
1504
+ var CONVERSATION_MUTATION_RETRY_MS = 25;
1505
+ var InvalidConversationRecordError = class extends Error {
1506
+ constructor(conversationId) {
1507
+ super(`Conversation record is invalid for ${conversationId}`);
1508
+ this.name = "InvalidConversationRecordError";
1509
+ }
1510
+ };
1511
+ function isInvalidConversationRecordError(error) {
1512
+ return error instanceof InvalidConversationRecordError;
1513
+ }
1514
+ var ConversationMutationFencedError = class extends Error {
1515
+ constructor(conversationId) {
1516
+ super(
1517
+ `Conversation mutation lock was lost before write for ${conversationId}`
1518
+ );
1519
+ this.name = "ConversationMutationFencedError";
1520
+ }
1521
+ };
1522
+ var CONVERSATION_BY_ACTIVITY_INDEX_KEY = `${CONVERSATION_PREFIX}:by-activity`;
1523
+ var CONVERSATION_ACTIVE_INDEX_KEY = `${CONVERSATION_PREFIX}:active`;
1524
+ var CONVERSATION_WORK_LEASE_TTL_MS = 9e4;
1525
+ var CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15e3;
1526
+ var CONVERSATION_WORK_STALE_ENQUEUE_MS = 6e4;
1527
+ var CONVERSATION_WORK_MAX_DELIVERY_ATTEMPTS = 5;
1528
+ var inboundMessageSourceSchema = z.enum([
1529
+ "api",
1530
+ "internal",
1531
+ "local",
1532
+ "plugin",
1533
+ "resource_event",
1534
+ "scheduler",
1535
+ "slack"
1536
+ ]);
1537
+ var agentInputSchema = z.object({
1538
+ attachments: z.array(z.unknown()).optional(),
1539
+ authorId: z.string().min(1).optional(),
1540
+ metadata: z.record(z.string(), z.unknown()).optional(),
1541
+ text: z.string()
1542
+ }).strict().refine((input) => input.text.trim() || input.attachments?.length, {
1543
+ message: "agent input requires text or attachments"
1544
+ });
1545
+ var inboundMessageDeliverySchema = z.enum(["defer", "interrupt"]);
1546
+ var inboundMessageSchema = z.object({
1547
+ attemptCount: z.number().finite().optional(),
1548
+ conversationId: z.string().refine((value) => value.trim().length > 0),
1549
+ createdAtMs: z.number().finite(),
1550
+ delivery: inboundMessageDeliverySchema,
1551
+ destination: destinationSchema,
1552
+ inboundMessageId: z.string().refine((value) => value.trim().length > 0),
1553
+ injectedAtMs: z.number().finite().optional(),
1554
+ input: agentInputSchema,
1555
+ receivedAtMs: z.number().finite(),
1556
+ source: inboundMessageSourceSchema
1557
+ }).strict();
1558
+ function conversationKey(conversationId) {
1559
+ return `${CONVERSATION_PREFIX}:${conversationId}`;
1560
+ }
1561
+ function indexLockKey(indexKey) {
1562
+ return `${indexKey}:lock`;
1563
+ }
1564
+ function mutationLockKey(conversationId) {
1565
+ return `${CONVERSATION_PREFIX}:mutation:${conversationId}`;
1566
+ }
1567
+ function now() {
1568
+ return Date.now();
1569
+ }
1570
+ function compareMessages(left, right) {
1571
+ return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
1572
+ }
1573
+ function inputHasAttachments(input) {
1574
+ return Array.isArray(input.attachments) && input.attachments.length > 0;
1575
+ }
1576
+ function upgradedPendingMessage(stored, duplicate) {
1577
+ const promotesInterrupt = stored.delivery === "defer" && duplicate.delivery === "interrupt";
1578
+ const delivery = promotesInterrupt ? "interrupt" : stored.delivery;
1579
+ const input = promotesInterrupt ? {
1580
+ ...duplicate.input,
1581
+ ...inputHasAttachments(duplicate.input) || !inputHasAttachments(stored.input) ? {} : { attachments: stored.input.attachments }
1582
+ } : inputHasAttachments(duplicate.input) && !inputHasAttachments(stored.input) ? duplicate.input : stored.input;
1583
+ if (input === stored.input && delivery === stored.delivery) {
1584
+ return stored;
1585
+ }
1586
+ return { ...stored, delivery, input };
1587
+ }
1588
+ function compareIndexDescending(left, right) {
1589
+ return right.score - left.score || right.conversationId.localeCompare(left.conversationId);
1590
+ }
1591
+ function compareIndexAscending(left, right) {
1592
+ return left.score - right.score || left.conversationId.localeCompare(right.conversationId);
1593
+ }
1594
+ function uniqueStrings(values) {
1595
+ return [...new Set(values)];
1596
+ }
1597
+ function normalizeSource(value) {
1598
+ const parsed = inboundMessageSourceSchema.safeParse(value);
1599
+ return parsed.success ? parsed.data : void 0;
1600
+ }
1601
+ function normalizeExecutionStatus(value) {
1602
+ if (value === "awaiting_resume" || value === "failed" || value === "idle" || value === "pending" || value === "running") {
1603
+ return value;
1604
+ }
1605
+ return void 0;
1606
+ }
1607
+ function normalizeMessage(value, schemaVersion) {
1608
+ const candidate = schemaVersion === 1 && isRecord(value) && value.delivery === void 0 ? {
1609
+ ...value,
1610
+ delivery: "defer"
1611
+ } : value;
1612
+ const parsed = inboundMessageSchema.safeParse(candidate);
1613
+ return parsed.success ? parsed.data : void 0;
1614
+ }
1615
+ function isFinalAttempt(message) {
1616
+ return (message.attemptCount ?? 0) >= CONVERSATION_WORK_MAX_DELIVERY_ATTEMPTS - 1;
1617
+ }
1618
+ function normalizeActor(value) {
1619
+ return parseStoredSlackActor(value);
1620
+ }
1621
+ function normalizeLease(value) {
1622
+ if (!isRecord(value)) {
1623
+ return void 0;
1624
+ }
1625
+ const token = toOptionalString(value.token);
1626
+ const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
1627
+ const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
1628
+ const expiresAtMs = toOptionalNumber(value.expiresAtMs);
1629
+ if (!token || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof expiresAtMs !== "number") {
1630
+ return void 0;
1631
+ }
1632
+ return {
1633
+ token,
1634
+ acquiredAtMs,
1635
+ lastCheckInAtMs,
1636
+ expiresAtMs
1637
+ };
1638
+ }
1639
+ function normalizeExecution(conversationId, value, schemaVersion) {
1640
+ if (!isRecord(value)) {
1641
+ return void 0;
1642
+ }
1643
+ const status = normalizeExecutionStatus(value.status);
1644
+ if (!status) {
1645
+ return void 0;
1646
+ }
1647
+ const pendingMessages2 = [];
1648
+ if (Array.isArray(value.pendingMessages)) {
1649
+ for (const rawMessage of value.pendingMessages) {
1650
+ const message = normalizeMessage(rawMessage, schemaVersion);
1651
+ if (!message || message.conversationId !== conversationId) {
1652
+ return void 0;
1653
+ }
1654
+ if (message.injectedAtMs !== void 0) {
1655
+ continue;
1656
+ }
1657
+ pendingMessages2.push(message);
1658
+ }
1659
+ pendingMessages2.sort(compareMessages);
1660
+ }
1661
+ const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(
1662
+ value.inboundMessageIds.map((id) => typeof id === "string" ? id : void 0).filter((id) => Boolean(id))
1663
+ ) : [];
1664
+ const lease = normalizeLease(value.lease);
1665
+ const normalizedStatus = status === "idle" && lease ? "running" : status === "idle" && pendingMessages2.length > 0 ? "pending" : status;
1666
+ return {
1667
+ status: normalizedStatus,
1668
+ inboundMessageIds: uniqueStrings([
1669
+ ...inboundMessageIds,
1670
+ ...pendingMessages2.map((message) => message.inboundMessageId)
1671
+ ]),
1672
+ pendingCount: pendingMessages2.length,
1673
+ pendingMessages: pendingMessages2,
1674
+ lease,
1675
+ lastCheckpointAtMs: toOptionalNumber(value.lastCheckpointAtMs),
1676
+ lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
1677
+ runId: toOptionalString(value.runId),
1678
+ updatedAtMs: toOptionalNumber(value.updatedAtMs)
1679
+ };
1680
+ }
1681
+ function normalizeConversation(conversationId, value) {
1682
+ if (!isRecord(value) || value.schemaVersion !== 1 && value.schemaVersion !== CONVERSATION_SCHEMA_VERSION) {
1683
+ return void 0;
1684
+ }
1685
+ const schemaVersion = value.schemaVersion;
1686
+ const storedConversationId = toOptionalString(value.conversationId);
1687
+ const createdAtMs = toOptionalNumber(value.createdAtMs);
1688
+ const lastActivityAtMs = toOptionalNumber(value.lastActivityAtMs);
1689
+ const updatedAtMs = toOptionalNumber(value.updatedAtMs);
1690
+ const execution = normalizeExecution(
1691
+ conversationId,
1692
+ value.execution,
1693
+ schemaVersion
1694
+ );
1695
+ const destination = value.destination === void 0 ? void 0 : parseDestination(value.destination);
1696
+ if (storedConversationId !== conversationId || typeof createdAtMs !== "number" || typeof lastActivityAtMs !== "number" || typeof updatedAtMs !== "number" || !execution || value.destination !== void 0 && !destination) {
1697
+ return void 0;
1698
+ }
1699
+ if (execution.pendingMessages.length > 0 && (!destination || execution.pendingMessages.some(
1700
+ (message) => !sameDestination(message.destination, destination)
1701
+ ))) {
1702
+ return void 0;
1703
+ }
1704
+ return {
1705
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1706
+ conversationId,
1707
+ createdAtMs,
1708
+ lastActivityAtMs,
1709
+ updatedAtMs,
1710
+ execution,
1711
+ ...destination ? { destination } : {},
1712
+ ...toOptionalString(value.title) ? { title: toOptionalString(value.title) } : {},
1713
+ ...toOptionalString(value.channelName) ? { channelName: toOptionalString(value.channelName) } : {},
1714
+ ...normalizeActor(value.actor) ? { actor: normalizeActor(value.actor) } : {},
1715
+ ...normalizeSource(value.source) ? { source: normalizeSource(value.source) } : {}
1716
+ };
1717
+ }
1718
+ function emptyConversation(args) {
1719
+ return {
1720
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1721
+ conversationId: args.conversationId,
1722
+ createdAtMs: args.nowMs,
1723
+ lastActivityAtMs: args.nowMs,
1724
+ updatedAtMs: args.nowMs,
1725
+ ...args.destination ? { destination: args.destination } : {},
1726
+ ...args.source ? { source: args.source } : {},
1727
+ execution: {
1728
+ status: "idle",
1729
+ inboundMessageIds: [],
1730
+ pendingCount: 0,
1731
+ pendingMessages: [],
1732
+ updatedAtMs: args.nowMs
1733
+ }
1734
+ };
1735
+ }
1736
+ function isLeaseActive(lease, nowMs) {
1737
+ return Boolean(lease && lease.expiresAtMs > nowMs);
1738
+ }
1739
+ function pendingMessages(conversation) {
1740
+ return [...conversation.execution.pendingMessages].sort(compareMessages);
1741
+ }
1742
+ function isRunnableStatus(status) {
1743
+ return status !== "failed" && status !== "idle";
1744
+ }
1745
+ function hasRunnableWork(conversation) {
1746
+ return isRunnableStatus(conversation.execution.status) || pendingMessages(conversation).length > 0;
1747
+ }
1748
+ function executionWithPendingMessages(execution, pending) {
1749
+ const pendingMessages2 = [...pending].sort(compareMessages);
1750
+ const status = execution.status === "idle" && execution.lease ? "running" : execution.status === "idle" && pendingMessages2.length > 0 ? "pending" : execution.status;
1751
+ return {
1752
+ ...execution,
1753
+ status,
1754
+ inboundMessageIds: uniqueStrings([
1755
+ ...execution.inboundMessageIds,
1756
+ ...pendingMessages2.map((message) => message.inboundMessageId)
1757
+ ]),
1758
+ pendingMessages: pendingMessages2,
1759
+ pendingCount: pendingMessages2.length
1760
+ };
1761
+ }
1762
+ function withExecutionUpdate(conversation, execution, nowMs) {
1763
+ return {
1764
+ ...conversation,
1765
+ updatedAtMs: nowMs,
1766
+ execution: {
1767
+ ...executionWithPendingMessages(execution, execution.pendingMessages),
1768
+ updatedAtMs: nowMs
1769
+ }
1770
+ };
1771
+ }
1772
+ async function getConnectedState(stateAdapter) {
1773
+ const state = stateAdapter ?? getStateAdapter();
1774
+ await state.connect();
1775
+ return state;
1776
+ }
1777
+ async function sleep(ms) {
1778
+ await new Promise((resolve) => {
1779
+ const timer = setTimeout(resolve, ms);
1780
+ timer.unref?.();
1781
+ });
1782
+ }
1783
+ async function withIndexLock(state, indexKey, callback) {
1784
+ const startedAtMs = now();
1785
+ let lock;
1786
+ while (true) {
1787
+ lock = await state.acquireLock(
1788
+ indexLockKey(indexKey),
1789
+ CONVERSATION_INDEX_LOCK_TTL_MS
1790
+ );
1791
+ if (lock) {
1792
+ break;
1793
+ }
1794
+ if (now() - startedAtMs >= CONVERSATION_INDEX_LOCK_WAIT_MS) {
1795
+ throw new Error(
1796
+ `Could not acquire conversation index lock for ${indexKey}`
1797
+ );
1798
+ }
1799
+ await sleep(CONVERSATION_INDEX_LOCK_RETRY_MS);
1800
+ }
1801
+ try {
1802
+ return await callback();
1803
+ } finally {
1804
+ await state.releaseLock(lock);
1805
+ }
1806
+ }
1807
+ function normalizeIndexEntry(value) {
1808
+ if (!isRecord(value)) {
1809
+ return void 0;
1810
+ }
1811
+ const conversationId = toOptionalString(value.conversationId);
1812
+ const score = toOptionalNumber(value.score);
1813
+ if (!conversationId || typeof score !== "number") {
1814
+ return void 0;
1815
+ }
1816
+ return { conversationId, score };
1817
+ }
1818
+ function uniqueIndexEntries(value) {
1819
+ if (!Array.isArray(value)) {
1820
+ return [];
1821
+ }
1822
+ const entries = /* @__PURE__ */ new Map();
1823
+ for (const item of value) {
1824
+ const entry = normalizeIndexEntry(item);
1825
+ if (!entry) {
1826
+ continue;
1827
+ }
1828
+ const existing = entries.get(entry.conversationId);
1829
+ if (!existing || entry.score > existing.score) {
1830
+ entries.set(entry.conversationId, entry);
1831
+ }
1832
+ }
1833
+ return [...entries.values()];
1834
+ }
1835
+ function retainedIndexEntries(indexKey, entries) {
1836
+ if (indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
1837
+ return entries.sort(compareIndexDescending).slice(0, CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH);
1838
+ }
1839
+ if (indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
1840
+ return entries.sort(compareIndexAscending);
1841
+ }
1842
+ throw new Error(`Unknown conversation index ${indexKey}`);
1843
+ }
1844
+ function redisIndexKey(indexKey) {
1845
+ const prefix = getChatConfig().state.keyPrefix;
1846
+ return [...prefix ? [prefix] : [], indexKey].join(":");
1847
+ }
1848
+ function parseRedisIndexEntries(values) {
1849
+ if (!Array.isArray(values)) {
1850
+ return [];
1851
+ }
1852
+ const entries = [];
1853
+ for (let index = 0; index < values.length; index += 2) {
1854
+ const conversationId = toOptionalString(values[index]);
1855
+ const score = typeof values[index + 1] === "number" ? values[index + 1] : Number(values[index + 1]);
1856
+ if (!conversationId || !Number.isFinite(score)) {
1857
+ continue;
1858
+ }
1859
+ entries.push({ conversationId, score });
1860
+ }
1861
+ return entries;
1862
+ }
1863
+ function redisConversationIndexStore(client) {
1864
+ const upsertBoundedActivityScript = `
1865
+ redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
1866
+ redis.call("PEXPIRE", KEYS[1], ARGV[3])
1867
+ local extra = redis.call("ZCARD", KEYS[1]) - tonumber(ARGV[4])
1868
+ if extra > 0 then
1869
+ redis.call("ZREMRANGEBYRANK", KEYS[1], 0, extra - 1)
1870
+ end
1871
+ return 1
1872
+ `;
1873
+ return {
1874
+ async list(args) {
1875
+ const key = redisIndexKey(args.indexKey);
1876
+ const limit = args.limit;
1877
+ const offset = Math.max(0, args.offset ?? 0);
1878
+ if (limit === 0) {
1879
+ return [];
1880
+ }
1881
+ const values = args.scoreMax !== void 0 ? await client.sendCommand([
1882
+ "ZRANGEBYSCORE",
1883
+ key,
1884
+ "-inf",
1885
+ String(args.scoreMax),
1886
+ "WITHSCORES",
1887
+ ...limit !== void 0 || offset > 0 ? ["LIMIT", String(offset), String(limit ?? 1e9)] : []
1888
+ ]) : await client.sendCommand([
1889
+ args.order === "asc" ? "ZRANGE" : "ZREVRANGE",
1890
+ key,
1891
+ String(offset),
1892
+ String(
1893
+ limit === void 0 ? -1 : offset + Math.max(0, limit - 1)
1894
+ ),
1895
+ "WITHSCORES"
1896
+ ]);
1897
+ return parseRedisIndexEntries(values);
1898
+ },
1899
+ async remove(args) {
1900
+ await client.sendCommand([
1901
+ "ZREM",
1902
+ redisIndexKey(args.indexKey),
1903
+ args.conversationId
1904
+ ]);
1905
+ },
1906
+ async upsert(args) {
1907
+ const key = redisIndexKey(args.indexKey);
1908
+ if (args.indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
1909
+ await client.sendCommand([
1910
+ "EVAL",
1911
+ upsertBoundedActivityScript,
1912
+ "1",
1913
+ key,
1914
+ String(args.score),
1915
+ args.conversationId,
1916
+ String(JUNIOR_THREAD_STATE_TTL_MS),
1917
+ String(CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH)
1918
+ ]);
1919
+ return;
1920
+ }
1921
+ if (args.indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
1922
+ await client.sendCommand([
1923
+ "ZADD",
1924
+ key,
1925
+ String(args.score),
1926
+ args.conversationId
1927
+ ]);
1928
+ await client.sendCommand([
1929
+ "PEXPIRE",
1930
+ key,
1931
+ String(JUNIOR_THREAD_STATE_TTL_MS)
1932
+ ]);
1933
+ return;
1934
+ }
1935
+ throw new Error(`Unknown conversation index ${args.indexKey}`);
1936
+ }
1937
+ };
1938
+ }
1939
+ function emulatedConversationIndexStore(state) {
1940
+ const readIndex = async (indexKey) => uniqueIndexEntries(await state.get(indexKey));
1941
+ const writeIndex = async (indexKey, entries) => {
1942
+ await state.set(indexKey, entries, JUNIOR_THREAD_STATE_TTL_MS);
1943
+ };
1944
+ return {
1945
+ async list(args) {
1946
+ const entries = (await readIndex(args.indexKey)).filter(
1947
+ (entry) => args.scoreMax === void 0 ? true : entry.score <= args.scoreMax
1948
+ ).sort(
1949
+ args.order === "asc" ? compareIndexAscending : compareIndexDescending
1950
+ );
1951
+ const offset = Math.max(0, args.offset ?? 0);
1952
+ return entries.slice(
1953
+ offset,
1954
+ args.limit === void 0 ? entries.length : offset + args.limit
1955
+ );
1956
+ },
1957
+ async remove(args) {
1958
+ await withIndexLock(state, args.indexKey, async () => {
1959
+ const entries = await readIndex(args.indexKey);
1960
+ const next = entries.filter(
1961
+ (entry) => entry.conversationId !== args.conversationId
1962
+ );
1963
+ if (next.length === entries.length) {
1964
+ return;
1965
+ }
1966
+ await writeIndex(args.indexKey, next);
1967
+ });
1968
+ },
1969
+ async upsert(args) {
1970
+ await withIndexLock(state, args.indexKey, async () => {
1971
+ const entries = await readIndex(args.indexKey);
1972
+ const withoutCurrent = entries.filter(
1973
+ (entry) => entry.conversationId !== args.conversationId
1974
+ );
1975
+ const next = retainedIndexEntries(args.indexKey, [
1976
+ ...withoutCurrent,
1977
+ { conversationId: args.conversationId, score: args.score }
1978
+ ]);
1979
+ await writeIndex(args.indexKey, next);
1980
+ });
1981
+ }
1982
+ };
1983
+ }
1984
+ async function getConversationIndexStore(state) {
1985
+ const redisStateAdapter = await getDefaultRedisStateAdapterFor(state);
1986
+ if (redisStateAdapter) {
1987
+ return redisConversationIndexStore(redisStateAdapter.getClient());
1988
+ }
1989
+ return emulatedConversationIndexStore(state);
1990
+ }
1991
+ async function upsertIndexEntry(args) {
1992
+ const index = await getConversationIndexStore(args.state);
1993
+ await index.upsert({
1994
+ conversationId: args.conversationId,
1995
+ indexKey: args.indexKey,
1996
+ score: args.score
1997
+ });
1998
+ }
1999
+ async function removeIndexEntry(args) {
2000
+ const index = await getConversationIndexStore(args.state);
2001
+ await index.remove({
2002
+ conversationId: args.conversationId,
2003
+ indexKey: args.indexKey
2004
+ });
2005
+ }
2006
+ async function acquireMutationLock(state, conversationId) {
2007
+ const startedAtMs = now();
2008
+ while (true) {
2009
+ const lock = await state.acquireLock(
2010
+ mutationLockKey(conversationId),
2011
+ CONVERSATION_MUTATION_LOCK_TTL_MS
2012
+ );
2013
+ if (lock) {
2014
+ return lock;
2015
+ }
2016
+ if (now() - startedAtMs >= CONVERSATION_MUTATION_WAIT_MS) {
2017
+ throw new Error(
2018
+ `Could not acquire conversation mutation lock for ${conversationId}`
2019
+ );
2020
+ }
2021
+ await sleep(CONVERSATION_MUTATION_RETRY_MS);
2022
+ }
2023
+ }
2024
+ async function withConversationMutation(args, callback) {
2025
+ const state = await getConnectedState(args.state);
2026
+ const lock = await acquireMutationLock(state, args.conversationId);
2027
+ try {
2028
+ return await callback(state, lock);
2029
+ } finally {
2030
+ await state.releaseLock(lock);
2031
+ }
2032
+ }
2033
+ async function readConversation(state, conversationId) {
2034
+ const raw = await state.get(conversationKey(conversationId));
2035
+ if (raw == null) {
2036
+ return void 0;
2037
+ }
2038
+ const conversation = normalizeConversation(conversationId, raw);
2039
+ if (!conversation) {
2040
+ throw new InvalidConversationRecordError(conversationId);
2041
+ }
2042
+ return conversation;
2043
+ }
2044
+ async function writeConversation(state, lock, conversation) {
2045
+ const execution = executionWithPendingMessages(
2046
+ conversation.execution,
2047
+ conversation.execution.pendingMessages
2048
+ );
2049
+ const next = {
2050
+ ...conversation,
2051
+ execution
2052
+ };
2053
+ const fenced = await state.extendLock(
2054
+ lock,
2055
+ CONVERSATION_MUTATION_LOCK_TTL_MS
2056
+ );
2057
+ if (!fenced) {
2058
+ throw new ConversationMutationFencedError(next.conversationId);
2059
+ }
2060
+ await state.set(
2061
+ conversationKey(next.conversationId),
2062
+ next,
2063
+ JUNIOR_THREAD_STATE_TTL_MS
2064
+ );
2065
+ await upsertIndexEntry({
2066
+ state,
2067
+ indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
2068
+ conversationId: next.conversationId,
2069
+ score: next.lastActivityAtMs
2070
+ });
2071
+ if (!hasRunnableWork(next)) {
2072
+ await removeIndexEntry({
2073
+ state,
2074
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
2075
+ conversationId: next.conversationId
2076
+ });
2077
+ return;
2078
+ }
2079
+ await upsertIndexEntry({
2080
+ state,
2081
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
2082
+ conversationId: next.conversationId,
2083
+ score: next.execution.updatedAtMs ?? next.updatedAtMs
2084
+ });
2085
+ }
2086
+ function assertSameConversationDestination(args) {
2087
+ if (!args.current || sameDestination(args.current, args.next)) {
2088
+ return;
2089
+ }
2090
+ throw new Error(
2091
+ `Conversation destination changed for ${args.conversationId}`
2092
+ );
2093
+ }
2094
+ function conversationWorkState(conversation) {
2095
+ const lease = conversation.execution.lease;
2096
+ return {
2097
+ ...conversation,
2098
+ lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
2099
+ ...lease ? {
2100
+ lease: {
2101
+ acquiredAtMs: lease.acquiredAtMs,
2102
+ lastCheckInAtMs: lease.lastCheckInAtMs,
2103
+ leaseExpiresAtMs: lease.expiresAtMs,
2104
+ leaseToken: lease.token
2105
+ }
2106
+ } : {},
2107
+ messages: pendingMessages(conversation),
2108
+ needsRun: hasRunnableWork(conversation)
2109
+ };
2110
+ }
2111
+ async function getConversation(args) {
2112
+ const state = await getConnectedState(args.state);
2113
+ return await readConversation(state, args.conversationId);
2114
+ }
2115
+ async function getConversationWorkState(args) {
2116
+ const conversation = await getConversation(args);
2117
+ return conversation ? conversationWorkState(conversation) : void 0;
2118
+ }
2119
+ async function appendInboundMessage(args) {
2120
+ const nowMs = args.nowMs ?? now();
2121
+ return await withConversationMutation(
2122
+ { conversationId: args.message.conversationId, state: args.state },
2123
+ async (state, lock) => {
2124
+ const current = await readConversation(state, args.message.conversationId) ?? emptyConversation({
2125
+ conversationId: args.message.conversationId,
2126
+ destination: args.message.destination,
2127
+ nowMs,
2128
+ source: args.message.source
2129
+ });
2130
+ assertSameConversationDestination({
2131
+ conversationId: args.message.conversationId,
2132
+ current: current.destination,
2133
+ next: args.message.destination
2134
+ });
2135
+ const existingPending = current.execution.pendingMessages.some(
2136
+ (message) => message.inboundMessageId === args.message.inboundMessageId
2137
+ );
2138
+ const existing = current.execution.inboundMessageIds.includes(
2139
+ args.message.inboundMessageId
2140
+ );
2141
+ if (existing) {
2142
+ if (!existingPending) {
2143
+ return { status: "duplicate" };
2144
+ }
2145
+ const nextStatus = current.execution.status === "idle" ? "pending" : current.execution.status;
2146
+ await writeConversation(
2147
+ state,
2148
+ lock,
2149
+ withExecutionUpdate(
2150
+ current,
2151
+ {
2152
+ ...current.execution,
2153
+ status: nextStatus,
2154
+ pendingMessages: current.execution.pendingMessages.map(
2155
+ (message) => message.inboundMessageId === args.message.inboundMessageId ? upgradedPendingMessage(message, args.message) : message
2156
+ )
2157
+ },
2158
+ nowMs
2159
+ )
2160
+ );
2161
+ return { status: "duplicate" };
2162
+ }
2163
+ const status = current.execution.lease && current.execution.status === "running" ? "running" : current.execution.lease ? "awaiting_resume" : "pending";
2164
+ const next = {
2165
+ ...current,
2166
+ destination: current.destination ?? args.message.destination,
2167
+ source: current.source ?? args.message.source,
2168
+ lastActivityAtMs: nowMs
2169
+ };
2170
+ await writeConversation(
2171
+ state,
2172
+ lock,
2173
+ withExecutionUpdate(
2174
+ next,
2175
+ {
2176
+ ...current.execution,
2177
+ status,
2178
+ inboundMessageIds: [
2179
+ ...current.execution.inboundMessageIds,
2180
+ args.message.inboundMessageId
2181
+ ],
2182
+ pendingMessages: [
2183
+ ...current.execution.pendingMessages,
2184
+ args.message
2185
+ ].sort(compareMessages)
2186
+ },
2187
+ nowMs
2188
+ )
2189
+ );
2190
+ return { status: "appended" };
2191
+ }
2192
+ );
2193
+ }
2194
+ async function requestConversationWork(args) {
2195
+ const nowMs = args.nowMs ?? now();
2196
+ return await withConversationMutation(args, async (state, lock) => {
2197
+ const existing = await readConversation(state, args.conversationId);
2198
+ if (existing) {
2199
+ assertSameConversationDestination({
2200
+ conversationId: args.conversationId,
2201
+ current: existing.destination,
2202
+ next: args.destination
2203
+ });
2204
+ }
2205
+ const current = existing ?? emptyConversation({
2206
+ conversationId: args.conversationId,
2207
+ destination: args.destination,
2208
+ nowMs
2209
+ });
2210
+ const status = current.execution.lease ? "awaiting_resume" : "pending";
2211
+ await writeConversation(
2212
+ state,
2213
+ lock,
2214
+ withExecutionUpdate(
2215
+ {
2216
+ ...current,
2217
+ destination: current.destination ?? args.destination
2218
+ },
2219
+ {
2220
+ ...current.execution,
2221
+ status
2222
+ },
2223
+ nowMs
2224
+ )
2225
+ );
2226
+ return { status: existing === void 0 ? "created" : "updated" };
2227
+ });
2228
+ }
2229
+ async function markConversationWorkEnqueued(args) {
2230
+ const nowMs = args.nowMs ?? now();
2231
+ await withConversationMutation(args, async (state, lock) => {
2232
+ const current = await readConversation(state, args.conversationId);
2233
+ if (!current) {
2234
+ return;
2235
+ }
2236
+ await writeConversation(
2237
+ state,
2238
+ lock,
2239
+ withExecutionUpdate(
2240
+ current,
2241
+ {
2242
+ ...current.execution,
2243
+ lastEnqueuedAtMs: nowMs
2244
+ },
2245
+ nowMs
2246
+ )
2247
+ );
2248
+ });
2249
+ }
2250
+ async function clearConsumedConversationWake(args) {
2251
+ const nowMs = args.nowMs ?? now();
2252
+ return await withConversationMutation(args, async (state, lock) => {
2253
+ const current = await readConversation(state, args.conversationId);
2254
+ if (!current || hasRunnableWork(current) || current.execution.lastEnqueuedAtMs === void 0) {
2255
+ return false;
2256
+ }
2257
+ await writeConversation(
2258
+ state,
2259
+ lock,
2260
+ withExecutionUpdate(
2261
+ current,
2262
+ {
2263
+ ...current.execution,
2264
+ lastEnqueuedAtMs: void 0
2265
+ },
2266
+ nowMs
2267
+ )
2268
+ );
2269
+ return true;
2270
+ });
2271
+ }
2272
+ async function startConversationWork(args) {
2273
+ const nowMs = args.nowMs ?? now();
2274
+ return await withConversationMutation(args, async (state, lock) => {
2275
+ const current = await readConversation(state, args.conversationId);
2276
+ if (!current) {
2277
+ return { status: "no_work" };
2278
+ }
2279
+ if (isLeaseActive(current.execution.lease, nowMs)) {
2280
+ return {
2281
+ status: "active",
2282
+ leaseExpiresAtMs: current.execution.lease.expiresAtMs
2283
+ };
2284
+ }
2285
+ if (!hasRunnableWork(current)) {
2286
+ return { status: "no_work" };
2287
+ }
2288
+ const lease = {
2289
+ token: randomUUID(),
2290
+ acquiredAtMs: nowMs,
2291
+ lastCheckInAtMs: nowMs,
2292
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
2293
+ };
2294
+ await writeConversation(
2295
+ state,
2296
+ lock,
2297
+ withExecutionUpdate(
2298
+ current,
2299
+ {
2300
+ ...current.execution,
2301
+ lease,
2302
+ status: current.execution.status === "awaiting_resume" ? "awaiting_resume" : "running",
2303
+ runId: current.execution.runId ?? randomUUID(),
2304
+ lastEnqueuedAtMs: void 0
2305
+ },
2306
+ nowMs
2307
+ )
2308
+ );
2309
+ return {
2310
+ status: "acquired",
2311
+ leaseToken: lease.token,
2312
+ leaseExpiresAtMs: lease.expiresAtMs
2313
+ };
2314
+ });
2315
+ }
2316
+ async function checkInConversationWork(args) {
2317
+ const nowMs = args.nowMs ?? now();
2318
+ return await withConversationMutation(args, async (state, lock) => {
2319
+ const current = await readConversation(state, args.conversationId);
2320
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2321
+ return false;
2322
+ }
2323
+ await writeConversation(
2324
+ state,
2325
+ lock,
2326
+ withExecutionUpdate(
2327
+ current,
2328
+ {
2329
+ ...current.execution,
2330
+ lease: {
2331
+ ...current.execution.lease,
2332
+ lastCheckInAtMs: nowMs,
2333
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
2334
+ }
2335
+ },
2336
+ nowMs
2337
+ )
2338
+ );
2339
+ return true;
2340
+ });
2341
+ }
2342
+ async function drainConversationMailbox(args) {
2343
+ const nowMs = args.nowMs ?? now();
2344
+ const pending = await withConversationMutation(args, async (state) => {
2345
+ const current = await readConversation(state, args.conversationId);
2346
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2347
+ throw new Error(
2348
+ `Conversation lease is not held for ${args.conversationId}`
2349
+ );
2350
+ }
2351
+ return pendingMessages(current);
2352
+ });
2353
+ if (pending.length === 0) {
2354
+ return { changed: false, messages: [] };
2355
+ }
2356
+ const result = await args.handle(pending);
2357
+ const pendingIds = new Set(
2358
+ pending.map((message) => message.inboundMessageId)
2359
+ );
2360
+ const acknowledgedIds = new Set(
2361
+ result ?? pending.map((message) => message.inboundMessageId)
2362
+ );
2363
+ for (const inboundMessageId of acknowledgedIds) {
2364
+ if (!pendingIds.has(inboundMessageId)) {
2365
+ throw new Error(
2366
+ `Conversation mailbox drain result is not pending for ${args.conversationId}`
2367
+ );
2368
+ }
2369
+ }
2370
+ if (acknowledgedIds.size === 0) {
2371
+ return { changed: false, messages: [] };
2372
+ }
2373
+ await withConversationMutation(args, async (state, lock) => {
2374
+ const current = await readConversation(state, args.conversationId);
2375
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2376
+ throw new Error(
2377
+ `Conversation lease is not held for ${args.conversationId}`
2378
+ );
2379
+ }
2380
+ const pendingMessages2 = current.execution.pendingMessages.filter(
2381
+ (message) => !acknowledgedIds.has(message.inboundMessageId)
2382
+ );
2383
+ await writeConversation(
2384
+ state,
2385
+ lock,
2386
+ withExecutionUpdate(
2387
+ current,
2388
+ {
2389
+ ...current.execution,
2390
+ status: current.execution.status === "pending" && pendingMessages2.length === 0 ? "running" : current.execution.status,
2391
+ pendingMessages: pendingMessages2
2392
+ },
2393
+ nowMs
2394
+ )
2395
+ );
2396
+ });
2397
+ return {
2398
+ changed: true,
2399
+ messages: pending.filter(
2400
+ (message) => acknowledgedIds.has(message.inboundMessageId)
2401
+ )
2402
+ };
2403
+ }
2404
+ async function ackMessages(args) {
2405
+ const nowMs = args.nowMs ?? now();
2406
+ const inboundMessageIds = new Set(args.inboundMessageIds);
2407
+ return await withConversationMutation(args, async (state, lock) => {
2408
+ const current = await readConversation(state, args.conversationId);
2409
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2410
+ return false;
2411
+ }
2412
+ if (inboundMessageIds.size === 0) {
2413
+ return true;
2414
+ }
2415
+ const pendingMessages2 = current.execution.pendingMessages.filter(
2416
+ (message) => !inboundMessageIds.has(message.inboundMessageId)
2417
+ );
2418
+ if (pendingMessages2.length === current.execution.pendingMessages.length) {
2419
+ return true;
2420
+ }
2421
+ await writeConversation(
2422
+ state,
2423
+ lock,
2424
+ withExecutionUpdate(
2425
+ current,
2426
+ {
2427
+ ...current.execution,
2428
+ pendingMessages: pendingMessages2
2429
+ },
2430
+ nowMs
2431
+ )
2432
+ );
2433
+ return true;
2434
+ });
2435
+ }
2436
+ async function requestConversationContinuation(args) {
2437
+ const nowMs = args.nowMs ?? now();
2438
+ return await withConversationMutation(args, async (state, lock) => {
2439
+ const current = await readConversation(state, args.conversationId);
2440
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2441
+ return false;
2442
+ }
2443
+ assertSameConversationDestination({
2444
+ conversationId: args.conversationId,
2445
+ current: current.destination,
2446
+ next: args.destination
2447
+ });
2448
+ await writeConversation(
2449
+ state,
2450
+ lock,
2451
+ withExecutionUpdate(
2452
+ current,
2453
+ {
2454
+ ...current.execution,
2455
+ status: "awaiting_resume"
2456
+ },
2457
+ nowMs
2458
+ )
2459
+ );
2460
+ return true;
2461
+ });
2462
+ }
2463
+ async function beginConversationResume(args) {
2464
+ const nowMs = args.nowMs ?? now();
2465
+ return await withConversationMutation(args, async (state, lock) => {
2466
+ const current = await readConversation(state, args.conversationId);
2467
+ if (!current || current.execution.lease?.token !== args.leaseToken || current.execution.status !== "awaiting_resume") {
2468
+ return false;
2469
+ }
2470
+ await writeConversation(
2471
+ state,
2472
+ lock,
2473
+ withExecutionUpdate(
2474
+ current,
2475
+ {
2476
+ ...current.execution,
2477
+ status: "running"
2478
+ },
2479
+ nowMs
2480
+ )
2481
+ );
2482
+ return true;
2483
+ });
2484
+ }
2485
+ async function releaseConversationWork(args) {
2486
+ const nowMs = args.nowMs ?? now();
2487
+ return await withConversationMutation(args, async (state, lock) => {
2488
+ const current = await readConversation(state, args.conversationId);
2489
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2490
+ return false;
2491
+ }
2492
+ await writeConversation(
2493
+ state,
2494
+ lock,
2495
+ withExecutionUpdate(
2496
+ current,
2497
+ {
2498
+ ...current.execution,
2499
+ lease: void 0,
2500
+ status: current.execution.status === "running" ? "pending" : current.execution.status
2501
+ },
2502
+ nowMs
2503
+ )
2504
+ );
2505
+ return true;
2506
+ });
2507
+ }
2508
+ async function completeConversationWork(args) {
2509
+ const nowMs = args.nowMs ?? now();
2510
+ return await withConversationMutation(args, async (state, lock) => {
2511
+ const current = await readConversation(state, args.conversationId);
2512
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2513
+ return "lost_lease";
2514
+ }
2515
+ const hasPending = pendingMessages(current).length > 0;
2516
+ const needsRun = current.execution.status === "awaiting_resume";
2517
+ const runnable = needsRun || hasPending;
2518
+ await writeConversation(
2519
+ state,
2520
+ lock,
2521
+ withExecutionUpdate(
2522
+ current,
2523
+ {
2524
+ ...current.execution,
2525
+ lease: void 0,
2526
+ status: needsRun ? "awaiting_resume" : hasPending ? "pending" : "idle",
2527
+ runId: runnable ? current.execution.runId : void 0
2528
+ },
2529
+ nowMs
2530
+ )
2531
+ );
2532
+ return runnable ? "pending" : "completed";
2533
+ });
2534
+ }
2535
+ async function recordAttemptFailure(args) {
2536
+ const nowMs = args.nowMs ?? now();
2537
+ return await withConversationMutation(args, async (state, lock) => {
2538
+ const current = await readConversation(state, args.conversationId);
2539
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2540
+ return {
2541
+ status: "lost_lease",
2542
+ pendingCount: 0,
2543
+ deadLetteredMessages: []
2544
+ };
2545
+ }
2546
+ const pendingIds = new Set(
2547
+ current.execution.pendingMessages.map(
2548
+ (message) => message.inboundMessageId
2549
+ )
2550
+ );
2551
+ if (args.inboundMessageIds.length === 0 || args.inboundMessageIds.some((id) => !pendingIds.has(id))) {
2552
+ return {
2553
+ status: "skipped",
2554
+ pendingCount: current.execution.pendingMessages.length,
2555
+ deadLetteredMessages: []
2556
+ };
2557
+ }
2558
+ const attemptedIds = new Set(args.inboundMessageIds);
2559
+ const deadLetteredMessages = [];
2560
+ const pendingMessages2 = [];
2561
+ for (const message of current.execution.pendingMessages) {
2562
+ if (!attemptedIds.has(message.inboundMessageId)) {
2563
+ pendingMessages2.push(message);
2564
+ continue;
2565
+ }
2566
+ const attempted = {
2567
+ ...message,
2568
+ attemptCount: (message.attemptCount ?? 0) + 1
2569
+ };
2570
+ if (attempted.attemptCount >= CONVERSATION_WORK_MAX_DELIVERY_ATTEMPTS) {
2571
+ deadLetteredMessages.push(attempted);
2572
+ continue;
2573
+ }
2574
+ pendingMessages2.push(attempted);
2575
+ }
2576
+ await writeConversation(
2577
+ state,
2578
+ lock,
2579
+ withExecutionUpdate(
2580
+ current,
2581
+ {
2582
+ ...current.execution,
2583
+ pendingMessages: pendingMessages2
2584
+ },
2585
+ nowMs
2586
+ )
2587
+ );
2588
+ return {
2589
+ status: "recorded",
2590
+ pendingCount: pendingMessages2.length,
2591
+ deadLetteredMessages
2592
+ };
2593
+ });
2594
+ }
2595
+ async function deadLetterAttempt(args) {
2596
+ const nowMs = args.nowMs ?? now();
2597
+ return await withConversationMutation(args, async (state, lock) => {
2598
+ const current = await readConversation(state, args.conversationId);
2599
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2600
+ return "lost_lease";
2601
+ }
2602
+ const runnable = pendingMessages(current).length > 0;
2603
+ await writeConversation(
2604
+ state,
2605
+ lock,
2606
+ withExecutionUpdate(
2607
+ current,
2608
+ {
2609
+ ...current.execution,
2610
+ lease: void 0,
2611
+ status: runnable ? "pending" : "failed",
2612
+ runId: runnable ? current.execution.runId : void 0
2613
+ },
2614
+ nowMs
2615
+ )
2616
+ );
2617
+ return runnable ? "pending" : "failed";
2618
+ });
2619
+ }
2620
+ async function clearExpiredConversationLease(args) {
2621
+ const nowMs = args.nowMs ?? now();
2622
+ return await withConversationMutation(args, async (state, lock) => {
2623
+ const current = await readConversation(state, args.conversationId);
2624
+ if (!current?.execution.lease || current.execution.lease.expiresAtMs > nowMs) {
2625
+ return false;
2626
+ }
2627
+ await writeConversation(
2628
+ state,
2629
+ lock,
2630
+ withExecutionUpdate(
2631
+ current,
2632
+ {
2633
+ ...current.execution,
2634
+ lease: void 0,
2635
+ status: "awaiting_resume"
2636
+ },
2637
+ nowMs
2638
+ )
2639
+ );
2640
+ return true;
2641
+ });
2642
+ }
2643
+ async function removeActiveConversation(args) {
2644
+ const state = await getConnectedState(args.state);
2645
+ await removeIndexEntry({
2646
+ state,
2647
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
2648
+ conversationId: args.conversationId
2649
+ });
2650
+ }
2651
+ async function listActiveConversationIds(args = {}) {
2652
+ const state = await getConnectedState(args.state);
2653
+ const index = await getConversationIndexStore(state);
2654
+ const entries = await index.list({
2655
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
2656
+ limit: args.limit,
2657
+ order: "asc",
2658
+ scoreMax: args.staleBeforeMs
2659
+ });
2660
+ return entries.map((entry) => entry.conversationId);
2661
+ }
2662
+
2663
+ // src/chat/task-execution/store.ts
2664
+ function metadataStore(options) {
2665
+ return options.conversationStore ?? getConversationStore();
2666
+ }
2667
+ function duplicateInboundNudgeIdempotencyKey(message, nowMs) {
2668
+ return `duplicate:${message.conversationId}:${message.inboundMessageId}:${nowMs}`;
2669
+ }
2670
+ function hasRecentEnqueueMarker(conversation, nowMs) {
2671
+ const lastEnqueuedAtMs = conversation.execution.lastEnqueuedAtMs;
2672
+ return typeof lastEnqueuedAtMs === "number" && lastEnqueuedAtMs + CONVERSATION_WORK_STALE_ENQUEUE_MS > nowMs;
2673
+ }
2674
+ function now2() {
2675
+ return Date.now();
2676
+ }
2677
+ async function recordExecutionMetadata(args) {
2678
+ const conversation = await getConversation({
2679
+ conversationId: args.conversationId,
2680
+ state: args.state
2681
+ });
2682
+ if (!conversation) {
2683
+ return;
2684
+ }
2685
+ await metadataStore(args).recordExecution({
2686
+ channelName: conversation.channelName,
2687
+ conversationId: conversation.conversationId,
2688
+ createdAtMs: conversation.createdAtMs,
2689
+ destination: conversation.destination,
2690
+ execution: {
2691
+ lastCheckpointAtMs: conversation.execution.lastCheckpointAtMs,
2692
+ lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
2693
+ runId: conversation.execution.runId,
2694
+ status: conversation.execution.status,
2695
+ updatedAtMs: conversation.execution.updatedAtMs
2696
+ },
2697
+ lastActivityAtMs: conversation.lastActivityAtMs,
2698
+ metrics: null,
2699
+ actor: conversation.actor,
2700
+ source: conversation.source,
2701
+ title: conversation.title,
2702
+ updatedAtMs: conversation.updatedAtMs
2703
+ });
2704
+ }
2705
+ async function getConversationWorkState2(args) {
2706
+ return await getConversationWorkState(args);
2707
+ }
2708
+ function countPendingConversationMessages(conversation) {
2709
+ return conversation.execution.pendingMessages.length;
2710
+ }
2711
+ function hasRunnableConversationWork(conversation) {
2712
+ return conversation.execution.status !== "idle" || countPendingConversationMessages(conversation) > 0;
2713
+ }
2714
+ async function ensureConversationWake(args) {
2715
+ const nowMs = args.nowMs ?? now2();
2716
+ const conversation = await getConversation({
2717
+ conversationId: args.conversationId,
2718
+ state: args.state
2719
+ });
2720
+ if (!conversation || !hasRunnableConversationWork(conversation)) {
2721
+ return { status: "no_work" };
2722
+ }
2723
+ if (!conversation.destination) {
2724
+ return { status: "no_work" };
2725
+ }
2726
+ if (args.replaceExistingWake !== true && conversation.execution.lease && conversation.execution.lease.expiresAtMs > nowMs) {
2727
+ return { status: "lease_active" };
2728
+ }
2729
+ if (args.replaceExistingWake !== true && hasRecentEnqueueMarker(conversation, nowMs)) {
2730
+ return { status: "already_enqueued" };
2731
+ }
2732
+ const queueResult = await args.queue.send(
2733
+ {
2734
+ conversationId: args.conversationId
2735
+ },
2736
+ {
2737
+ delayMs: args.delayMs,
2738
+ idempotencyKey: args.idempotencyKey
2739
+ }
2740
+ );
2741
+ await markConversationWorkEnqueued2({
2742
+ conversationId: args.conversationId,
2743
+ conversationStore: args.conversationStore,
2744
+ nowMs,
2745
+ state: args.state
2746
+ });
2747
+ return { status: "enqueued", queueMessageId: queueResult?.messageId };
2748
+ }
2749
+ async function appendAndEnqueueInboundMessage(args) {
2750
+ const nowMs = args.nowMs ?? now2();
2751
+ const appendResult = await appendInboundMessage({
2752
+ message: args.message,
2753
+ nowMs,
2754
+ state: args.state
2755
+ });
2756
+ let idempotencyKey = args.message.inboundMessageId;
2757
+ if (appendResult.status === "duplicate") {
2758
+ const conversation = await getConversation({
2759
+ conversationId: args.message.conversationId,
2760
+ state: args.state
2761
+ });
2762
+ if (!conversation) {
2763
+ return appendResult;
2764
+ }
2765
+ if (hasRecentEnqueueMarker(conversation, nowMs)) {
2766
+ await recordExecutionMetadata({
2767
+ conversationId: args.message.conversationId,
2768
+ conversationStore: args.conversationStore,
2769
+ state: args.state
2770
+ });
2771
+ return appendResult;
2772
+ }
2773
+ const duplicateStillPending = conversation.execution.pendingMessages.some(
2774
+ (message) => message.inboundMessageId === args.message.inboundMessageId
2775
+ );
2776
+ if (!duplicateStillPending) {
2777
+ await recordExecutionMetadata({
2778
+ conversationId: args.message.conversationId,
2779
+ conversationStore: args.conversationStore,
2780
+ state: args.state
2781
+ });
2782
+ return appendResult;
2783
+ }
2784
+ idempotencyKey = duplicateInboundNudgeIdempotencyKey(args.message, nowMs);
2785
+ }
2786
+ const wake = await ensureConversationWake({
2787
+ conversationId: args.message.conversationId,
2788
+ conversationStore: args.conversationStore,
2789
+ idempotencyKey,
2790
+ nowMs,
2791
+ queue: args.queue,
2792
+ state: args.state
2793
+ });
2794
+ if (wake.status !== "enqueued") {
2795
+ await recordExecutionMetadata({
2796
+ conversationId: args.message.conversationId,
2797
+ conversationStore: args.conversationStore,
2798
+ state: args.state
2799
+ });
2800
+ }
2801
+ return {
2802
+ ...appendResult,
2803
+ ...wake.status === "enqueued" ? { queueMessageId: wake.queueMessageId } : {}
2804
+ };
2805
+ }
2806
+ async function clearConsumedConversationWake2(args) {
2807
+ const result = await clearConsumedConversationWake(args);
2808
+ if (result) {
2809
+ await recordExecutionMetadata(args);
2810
+ }
2811
+ return result;
2812
+ }
2813
+ async function requestConversationWork2(args) {
2814
+ const result = await requestConversationWork(args);
2815
+ await recordExecutionMetadata({
2816
+ conversationId: args.conversationId,
2817
+ conversationStore: args.conversationStore,
2818
+ state: args.state
2819
+ });
2820
+ return result;
2821
+ }
2822
+ async function markConversationWorkEnqueued2(args) {
2823
+ await markConversationWorkEnqueued(args);
2824
+ await recordExecutionMetadata(args);
2825
+ }
2826
+ async function startConversationWork2(args) {
2827
+ const result = await startConversationWork(args);
2828
+ await recordExecutionMetadata(args);
2829
+ return result;
2830
+ }
2831
+ async function checkInConversationWork2(args) {
2832
+ const result = await checkInConversationWork(args);
2833
+ if (result) {
2834
+ await recordExecutionMetadata(args);
2835
+ }
2836
+ return result;
2837
+ }
2838
+ async function drainConversationMailbox2(args) {
2839
+ const result = await drainConversationMailbox(args);
2840
+ if (result.changed) {
2841
+ await recordExecutionMetadata(args);
2842
+ }
2843
+ return result.messages;
2844
+ }
2845
+ async function ackMessages2(args) {
2846
+ const result = await ackMessages(args);
2847
+ await recordExecutionMetadata(args);
2848
+ return result;
2849
+ }
2850
+ async function requestConversationContinuation2(args) {
2851
+ const result = await requestConversationContinuation(args);
2852
+ await recordExecutionMetadata(args);
2853
+ return result;
2854
+ }
2855
+ async function beginConversationResume2(args) {
2856
+ const result = await beginConversationResume(args);
2857
+ if (result) {
2858
+ await recordExecutionMetadata(args);
2859
+ }
2860
+ return result;
2861
+ }
2862
+ async function releaseConversationWork2(args) {
2863
+ const result = await releaseConversationWork(args);
2864
+ await recordExecutionMetadata(args);
2865
+ return result;
2866
+ }
2867
+ async function completeConversationWork2(args) {
2868
+ const result = await completeConversationWork(args);
2869
+ await recordExecutionMetadata(args);
2870
+ return result;
2871
+ }
2872
+ async function recordAttemptFailure2(args) {
2873
+ const result = await recordAttemptFailure(args);
2874
+ if (result.status === "recorded") {
2875
+ await recordExecutionMetadata(args);
2876
+ }
2877
+ return result;
2878
+ }
2879
+ async function deadLetterAttempt2(args) {
2880
+ const result = await deadLetterAttempt(args);
2881
+ await recordExecutionMetadata(args);
2882
+ return result;
2883
+ }
2884
+ async function clearExpiredConversationLease2(args) {
2885
+ const result = await clearExpiredConversationLease(args);
2886
+ await recordExecutionMetadata(args);
2887
+ return result;
2888
+ }
2889
+ async function removeActiveConversation2(args) {
2890
+ return await removeActiveConversation(args);
2891
+ }
2892
+ async function listActiveConversationIds2(args = {}) {
2893
+ return await listActiveConversationIds(args);
2894
+ }
2895
+
2896
+ // src/chat/services/agent-continue.ts
2897
+ async function getAwaitingAgentContinueRequest(args) {
2898
+ const sessionRecord = await getAgentTurnSessionRecord(
2899
+ args.conversationId,
2900
+ args.sessionId
2901
+ );
2902
+ if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" && sessionRecord.resumeReason !== "yield" && sessionRecord.resumeReason !== "retry" || sessionRecord.resumeReason === "timeout" && sessionRecord.sliceId < 2) {
2903
+ return void 0;
2904
+ }
2905
+ if (!sessionRecord.destination) {
2906
+ return void 0;
2907
+ }
2908
+ return {
2909
+ conversationId: args.conversationId,
2910
+ destination: sessionRecord.destination,
2911
+ sessionId: args.sessionId,
2912
+ expectedVersion: sessionRecord.version
2913
+ };
2914
+ }
2915
+ async function scheduleAgentContinue(request, options = {}) {
2916
+ const nowMs = options.nowMs ?? Date.now();
2917
+ await requestConversationWork2({
2918
+ conversationId: request.conversationId,
2919
+ destination: request.destination,
2920
+ nowMs,
2921
+ state: options.state
2922
+ });
2923
+ const queue = options.queue ?? getVercelConversationWorkQueue();
2924
+ await ensureConversationWake({
2925
+ conversationId: request.conversationId,
2926
+ idempotencyKey: [
2927
+ "agent-continue",
2928
+ request.conversationId,
2929
+ request.sessionId,
2930
+ request.expectedVersion,
2931
+ nowMs
2932
+ ].join(":"),
2933
+ nowMs,
2934
+ queue,
2935
+ state: options.state
2936
+ });
2937
+ }
2938
+
2939
+ // src/handlers/oauth-html.ts
2940
+ function htmlCallbackResponse(title, message, status) {
2941
+ const html = `<!DOCTYPE html>
2942
+ <html>
2943
+ <head><title>${title}</title></head>
2944
+ <body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
2945
+ <div style="text-align: center; max-width: 480px;">
2946
+ <h1>${title}</h1>
2947
+ <p>${message}</p>
2948
+ <p style="margin-top: 2rem; color: #666; font-size: 0.9em;">You can close this tab and return to Junior.</p>
2949
+ </div>
2950
+ </body>
2951
+ </html>`;
2952
+ return new Response(html, {
2953
+ status,
2954
+ headers: { "Content-Type": "text/html; charset=utf-8" }
2955
+ });
2956
+ }
2957
+
2958
+ // src/handlers/mcp-oauth-callback.ts
2959
+ var CALLBACK_PAGES = {
2960
+ missing_state: {
2961
+ title: "Authorization failed",
2962
+ message: "Missing state parameter.",
2963
+ status: 400
2964
+ },
2965
+ provider_error: {
2966
+ title: "Authorization failed",
2967
+ message: "The provider returned an authorization error.",
2968
+ status: 400
2969
+ },
2970
+ missing_code: {
2971
+ title: "Authorization failed",
2972
+ message: "Missing code parameter.",
2973
+ status: 400
2974
+ },
2975
+ expired: {
2976
+ title: "Authorization expired",
2977
+ message: "This authorization link is no longer active. Return to Junior and retry the original request.",
2978
+ status: 400
2979
+ },
2980
+ success: {
2981
+ title: "Authorization complete",
2982
+ message: "Your MCP access is connected. Junior will continue the paused request in Slack.",
2983
+ status: 200
2984
+ },
2985
+ failure: {
2986
+ title: "Authorization failed",
2987
+ message: "Junior could not finish the authorization callback. Return to Junior and retry the original request.",
2988
+ status: 500
2989
+ }
2990
+ };
2991
+ var McpOAuthAttemptExpiredError = class extends Error {
2992
+ constructor() {
2993
+ super("MCP OAuth authorization attempt is no longer current");
2994
+ this.name = "McpOAuthAttemptExpiredError";
2995
+ }
2996
+ };
2997
+ function mcpAuthorizationId(args) {
2998
+ return `${args.sessionId}:mcp:${args.provider}`;
2999
+ }
3000
+ function htmlResponse(kind, options) {
3001
+ const page = kind === "success" && options?.local ? {
3002
+ ...CALLBACK_PAGES.success,
3003
+ message: "Your MCP access is connected. Junior will continue the paused request in the local client."
3004
+ } : CALLBACK_PAGES[kind];
3005
+ return htmlCallbackResponse(page.title, page.message, page.status);
3006
+ }
3007
+ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply) {
3008
+ const threadId = `slack:${channelId}:${threadTs}`;
3009
+ const currentState = await getPersistedThreadState(threadId);
3010
+ const conversation = coerceThreadConversationState(currentState);
3011
+ await hydrateConversationMessages({ conversation, conversationId: threadId });
3012
+ const artifacts = coerceThreadArtifactsState(currentState);
3013
+ const userMessage = getTurnUserMessage(conversation, sessionId);
3014
+ const statePatch = buildDeliveredTurnStatePatch({
3015
+ artifacts,
3016
+ conversation,
3017
+ reply,
3018
+ sessionId,
3019
+ userMessageId: userMessage?.id
3020
+ });
3021
+ await persistThreadStateById(threadId, {
3022
+ ...statePatch
3023
+ });
3024
+ }
3025
+ async function failSessionRecordBestEffort(args) {
3026
+ try {
3027
+ await failAgentTurnSessionRecord({
3028
+ conversationId: args.conversationId,
3029
+ sessionId: args.sessionId,
3030
+ errorMessage: args.errorMessage,
3031
+ expectedVersion: args.expectedVersion
3032
+ });
3033
+ } catch (error) {
3034
+ logException(
3035
+ error,
3036
+ "mcp_oauth_callback_session_record_fail_persist_failed",
3037
+ {},
3038
+ {
3039
+ "app.ai.conversation_id": args.conversationId,
3040
+ "app.ai.session_id": args.sessionId
3041
+ },
3042
+ "Failed to mark MCP OAuth-resumed turn session record failed"
3043
+ );
3044
+ }
3045
+ }
3046
+ async function persistFailedReplyState(channelId, threadTs, sessionId, expectedVersion) {
3047
+ const threadId = `slack:${channelId}:${threadTs}`;
3048
+ const currentState = await getPersistedThreadState(threadId);
3049
+ const conversation = coerceThreadConversationState(currentState);
3050
+ await hydrateConversationMessages({ conversation, conversationId: threadId });
3051
+ clearPendingAuth(conversation, sessionId);
3052
+ markTurnFailed({
3053
+ conversation,
3054
+ nowMs: Date.now(),
3055
+ sessionId,
3056
+ userMessageId: getTurnUserMessageId(conversation, sessionId),
3057
+ markConversationMessage,
3058
+ updateConversationStats
3059
+ });
3060
+ await failSessionRecordBestEffort({
3061
+ conversationId: threadId,
3062
+ sessionId,
3063
+ errorMessage: "OAuth-resumed MCP turn failed",
3064
+ expectedVersion
3065
+ });
3066
+ await persistThreadStateById(threadId, {
3067
+ conversation
3068
+ });
3069
+ }
3070
+ async function resumeAuthorizedMcpTurn(args) {
3071
+ const { authSession, agentRunner, provider } = args;
3072
+ if (!authSession.channelId || !authSession.destination || !authSession.threadTs) {
3073
+ return;
3074
+ }
3075
+ const destination = requireSlackDestination(
3076
+ authSession.destination,
3077
+ "MCP OAuth resume"
3078
+ );
3079
+ const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
3080
+ const currentState = await getPersistedThreadState(threadId);
3081
+ const conversation = coerceThreadConversationState(currentState);
3082
+ await hydrateConversationMessages({ conversation, conversationId: threadId });
3083
+ const pendingAuth = getConversationPendingAuth({
3084
+ conversation,
3085
+ kind: "mcp",
3086
+ provider,
3087
+ actorId: authSession.userId
3088
+ });
3089
+ if (pendingAuth?.authSessionId !== authSession.authSessionId) {
3090
+ return;
3091
+ }
3092
+ const resolvedSessionId = pendingAuth.sessionId;
3093
+ const userMessage = getTurnUserMessage(conversation, resolvedSessionId);
3094
+ if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
3095
+ clearPendingAuth(conversation, pendingAuth.sessionId);
3096
+ await persistThreadStateById(threadId, { conversation });
3097
+ await abandonAgentTurnSessionRecord({
3098
+ conversationId: authSession.conversationId,
3099
+ sessionId: pendingAuth.sessionId,
3100
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
3101
+ });
3102
+ return;
3103
+ }
3104
+ if (!userMessage) {
3105
+ return;
3106
+ }
3107
+ await resumeSlackTurn({
3108
+ messageText: userMessage.text,
3109
+ conversationId: authSession.conversationId,
3110
+ turnId: resolvedSessionId,
3111
+ channelId: authSession.channelId,
3112
+ threadTs: authSession.threadTs,
3113
+ messageTs: getTurnUserSlackMessageTs(userMessage),
3114
+ lockKey: threadId,
3115
+ initialText: "",
3116
+ agentRunner,
3117
+ beforeStart: async () => {
3118
+ const lockedState = await getPersistedThreadState(threadId);
3119
+ const lockedConversation = coerceThreadConversationState(lockedState);
3120
+ await hydrateConversationMessages({
3121
+ conversation: lockedConversation,
3122
+ conversationId: threadId
3123
+ });
3124
+ const lockedArtifacts = coerceThreadArtifactsState(lockedState);
3125
+ const lockedPendingAuth = getConversationPendingAuth({
3126
+ conversation: lockedConversation,
3127
+ kind: "mcp",
3128
+ provider,
3129
+ actorId: authSession.userId
3130
+ });
3131
+ if (lockedPendingAuth?.authSessionId !== authSession.authSessionId) {
3132
+ return false;
3133
+ }
3134
+ const lockedSessionId = lockedPendingAuth.sessionId;
3135
+ if (lockedSessionId !== resolvedSessionId) {
3136
+ return false;
3137
+ }
3138
+ if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
3139
+ clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
3140
+ await persistThreadStateById(threadId, {
3141
+ conversation: lockedConversation
3142
+ });
3143
+ await abandonAgentTurnSessionRecord({
3144
+ conversationId: authSession.conversationId,
3145
+ sessionId: lockedPendingAuth.sessionId,
3146
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
3147
+ });
3148
+ return false;
3149
+ }
3150
+ const lockedUserMessage = getTurnUserMessage(
3151
+ lockedConversation,
3152
+ lockedSessionId
3153
+ );
3154
+ if (!lockedUserMessage) {
3155
+ return false;
3156
+ }
3157
+ const lockedSessionRecord = await getAgentTurnSessionRecord(
3158
+ authSession.conversationId,
3159
+ lockedSessionId
3160
+ );
3161
+ if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
3162
+ return false;
3163
+ }
3164
+ const lockedConversationContext = buildConversationContext(
3165
+ lockedConversation,
3166
+ {
3167
+ excludeMessageId: lockedUserMessage.id
3168
+ }
3169
+ );
3170
+ const lockedChannelConfiguration = getChannelConfigurationServiceById(
3171
+ authSession.channelId
3172
+ );
3173
+ let actor;
3174
+ try {
3175
+ actor = createSlackResumeActor({
3176
+ actor: isUserActor(lockedSessionRecord.actor) ? lockedSessionRecord.actor : void 0,
3177
+ teamId: destination.teamId,
3178
+ userId: authSession.userId
3179
+ });
3180
+ } catch {
3181
+ await failAgentTurnSessionRecord({
3182
+ conversationId: authSession.conversationId,
3183
+ expectedVersion: lockedSessionRecord.version,
3184
+ sessionId: lockedSessionId,
3185
+ errorMessage: "Stored Slack actor identity did not match OAuth actor"
3186
+ });
3187
+ return false;
3188
+ }
3189
+ if (!lockedSessionRecord.source) {
3190
+ await failAgentTurnSessionRecord({
3191
+ conversationId: authSession.conversationId,
3192
+ expectedVersion: lockedSessionRecord.version,
3193
+ sessionId: lockedSessionId,
3194
+ errorMessage: "Stored Slack source missing for MCP OAuth resume"
3195
+ });
3196
+ return false;
3197
+ }
3198
+ await recordAuthorizationCompleted({
3199
+ conversationId: authSession.conversationId,
3200
+ kind: "mcp",
3201
+ provider,
3202
+ actorId: authSession.userId,
3203
+ authorizationId: mcpAuthorizationId({
3204
+ provider,
3205
+ sessionId: lockedSessionId
3206
+ })
3207
+ });
3208
+ const lockedMessageTs = getTurnUserSlackMessageTs(lockedUserMessage);
3209
+ return {
3210
+ messageText: lockedUserMessage.text,
3211
+ messageTs: lockedMessageTs,
3212
+ inputMessageIds: [lockedUserMessage.id],
3213
+ replyContext: {
3214
+ input: {
3215
+ conversationContext: lockedConversationContext,
3216
+ // Pi history is SQL-authoritative: the resumed run reads its
3217
+ // session record first and falls back to the step projection.
3218
+ piMessages: await loadProjection({ conversationId: threadId }),
3219
+ ...getTurnUserReplyAttachmentContext(lockedUserMessage)
3220
+ },
3221
+ routing: {
3222
+ credentialContext: {
3223
+ actor: { type: "user", userId: actor.userId }
3224
+ },
3225
+ actor,
3226
+ destination,
3227
+ source: lockedSessionRecord.source,
3228
+ toolChannelId: authSession.toolChannelId ?? lockedArtifacts.assistantContextChannelId ?? authSession.channelId
3229
+ },
3230
+ policy: {
3231
+ configuration: authSession.configuration,
3232
+ channelConfiguration: lockedChannelConfiguration
3233
+ },
3234
+ state: {
3235
+ artifactState: lockedArtifacts,
3236
+ pendingAuth: lockedPendingAuth,
3237
+ sandboxRef: getPersistedSandboxState(lockedState)
3238
+ },
3239
+ durability: {
3240
+ recordPendingAuth: async (nextPendingAuth) => {
3241
+ lockedConversation.processing.pendingAuth = nextPendingAuth;
3242
+ await persistThreadStateById(threadId, {
3243
+ conversation: lockedConversation
3244
+ });
3245
+ }
3246
+ }
3247
+ },
3248
+ commitResult: async (reply) => {
3249
+ await persistCompletedReplyState(
3250
+ authSession.channelId,
3251
+ authSession.threadTs,
3252
+ lockedSessionId,
3253
+ reply
3254
+ );
3255
+ },
3256
+ onPostDeliveryCommitFailure: async () => {
3257
+ await failAgentTurnSessionRecord({
3258
+ conversationId: authSession.conversationId,
3259
+ expectedVersion: lockedSessionRecord.version,
3260
+ sessionId: lockedSessionId,
3261
+ errorMessage: "OAuth-resumed MCP reply was delivered but completion state did not persist"
3262
+ });
3263
+ },
3264
+ onFailure: async () => {
3265
+ try {
3266
+ await persistFailedReplyState(
3267
+ authSession.channelId,
3268
+ authSession.threadTs,
3269
+ lockedSessionId,
3270
+ lockedSessionRecord.version
3271
+ );
3272
+ } catch (persistError) {
3273
+ logException(
3274
+ persistError,
3275
+ "mcp_oauth_callback_resume_failure_persist_failed",
3276
+ {},
3277
+ { "app.credential.provider": provider },
3278
+ "Failed to persist failed MCP resume state"
3279
+ );
3280
+ }
3281
+ },
3282
+ onAuthPause: async () => {
3283
+ await persistAuthPauseTurnState({
3284
+ sessionId: lockedSessionId,
3285
+ threadStateId: threadId
3286
+ });
3287
+ logWarn(
3288
+ "mcp_oauth_callback_resume_reparked_for_auth",
3289
+ {},
3290
+ { "app.credential.provider": provider },
3291
+ "Resumed MCP turn requested another authorization flow"
3292
+ );
3293
+ },
3294
+ onSuspend: async (resumeVersion) => {
3295
+ await scheduleAgentContinue({
3296
+ conversationId: authSession.conversationId,
3297
+ destination,
3298
+ sessionId: lockedSessionId,
3299
+ expectedVersion: resumeVersion
3300
+ });
3301
+ }
3302
+ };
3303
+ }
3304
+ });
3305
+ }
3306
+ async function isCurrentMcpAuthorizationAttempt(authSession, provider) {
3307
+ const conversationId = mcpConversationId(authSession);
3308
+ if (!conversationId) {
3309
+ return false;
3310
+ }
3311
+ const currentState = await getPersistedThreadState(conversationId);
3312
+ const conversation = coerceThreadConversationState(currentState);
3313
+ const pendingAuth = getConversationPendingAuth({
3314
+ conversation,
3315
+ kind: "mcp",
3316
+ provider,
3317
+ actorId: authSession.userId
3318
+ });
3319
+ return pendingAuth?.authSessionId === authSession.authSessionId && pendingAuth.sessionId === authSession.sessionId;
3320
+ }
3321
+ function mcpConversationId(authSession) {
3322
+ if (authSession.destination?.platform === "local") {
3323
+ return authSession.conversationId;
3324
+ }
3325
+ if (authSession.channelId && authSession.threadTs) {
3326
+ return `slack:${authSession.channelId}:${authSession.threadTs}`;
3327
+ }
3328
+ return void 0;
3329
+ }
3330
+ async function runCurrentMcpCredentialMutation(authSession, provider, mutation) {
3331
+ const conversationId = mcpConversationId(authSession);
3332
+ if (!conversationId) {
3333
+ throw new McpOAuthAttemptExpiredError();
3334
+ }
3335
+ const stateAdapter = getStateAdapter();
3336
+ await stateAdapter.connect();
3337
+ const lock = await acquireActiveLock(stateAdapter, conversationId);
3338
+ if (!lock) {
3339
+ throw new Error(
3340
+ `Could not acquire MCP OAuth callback lock for ${conversationId}`
3341
+ );
3342
+ }
3343
+ try {
3344
+ if (!await isCurrentMcpAuthorizationAttempt(authSession, provider)) {
3345
+ throw new McpOAuthAttemptExpiredError();
3346
+ }
3347
+ return await mutation();
3348
+ } finally {
3349
+ await stateAdapter.releaseLock(lock);
3350
+ }
3351
+ }
3352
+ async function GET(request, provider, waitUntil, options) {
3353
+ const localRelay = await relayLocalOAuthCallback(request);
3354
+ if (localRelay) {
3355
+ return localRelay;
3356
+ }
3357
+ const url = new URL(request.url);
3358
+ const state = url.searchParams.get("state")?.trim();
3359
+ const code = url.searchParams.get("code")?.trim();
3360
+ const error = url.searchParams.get("error")?.trim();
3361
+ if (!state) {
3362
+ return htmlResponse("missing_state");
3363
+ }
3364
+ if (error) {
3365
+ return htmlResponse("provider_error");
3366
+ }
3367
+ if (!code) {
3368
+ return htmlResponse("missing_code");
3369
+ }
3370
+ try {
3371
+ const pendingSession = await getMcpAuthSession(state);
3372
+ if (!pendingSession || pendingSession.provider !== provider || !await isCurrentMcpAuthorizationAttempt(pendingSession, provider)) {
3373
+ if (pendingSession) {
3374
+ await deleteMcpAuthSession(pendingSession.authSessionId);
3375
+ }
3376
+ return htmlResponse("expired");
3377
+ }
3378
+ const authSession = await finalizeMcpAuthorization(
3379
+ provider,
3380
+ state,
3381
+ code,
3382
+ async (mutation) => await runCurrentMcpCredentialMutation(
3383
+ pendingSession,
3384
+ provider,
3385
+ mutation
3386
+ )
3387
+ );
3388
+ try {
3389
+ await deleteMcpAuthSession(authSession.authSessionId);
3390
+ } catch (cleanupError) {
3391
+ logException(
3392
+ cleanupError,
3393
+ "mcp_oauth_callback_session_cleanup_failed",
3394
+ {},
3395
+ { "app.credential.provider": provider },
3396
+ "Failed to delete completed MCP auth session"
3397
+ );
3398
+ }
3399
+ if (authSession.destination?.platform !== "local") {
3400
+ waitUntil(
3401
+ () => resumeAuthorizedMcpTurn({
3402
+ authSession,
3403
+ agentRunner: options.agentRunner,
3404
+ provider
3405
+ })
3406
+ );
3407
+ }
3408
+ return htmlResponse("success", {
3409
+ local: authSession.destination?.platform === "local"
3410
+ });
3411
+ } catch (callbackError) {
3412
+ if (callbackError instanceof McpOAuthAttemptExpiredError) {
3413
+ await deleteMcpAuthSession(state);
3414
+ return htmlResponse("expired");
3415
+ }
3416
+ logException(
3417
+ callbackError,
3418
+ "mcp_oauth_callback_failed",
3419
+ {},
3420
+ { "app.credential.provider": provider },
3421
+ "Failed to process MCP OAuth callback"
3422
+ );
3423
+ return htmlResponse("failure");
3424
+ }
3425
+ }
3426
+
3427
+ // src/chat/slack/app-home.ts
3428
+ import fs from "fs";
3429
+ import path from "path";
3430
+ var DEFAULT_DESCRIPTION_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
3431
+ var MAX_HOME_SKILLS = 6;
3432
+ var MAX_SECTION_TEXT_CHARS = 3e3;
3433
+ var HIDDEN_HOME_SKILLS = /* @__PURE__ */ new Set(["jr-rpc"]);
3434
+ function clampSectionText(text) {
3435
+ if (text.length <= MAX_SECTION_TEXT_CHARS) {
3436
+ return text;
3437
+ }
3438
+ return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
3439
+ }
3440
+ function loadDescriptionText() {
3441
+ const descriptionPath = path.join(homeDir(), "DESCRIPTION.md");
3442
+ try {
3443
+ const raw = fs.readFileSync(descriptionPath, "utf8").trim();
3444
+ if (raw.length > 0) {
3445
+ return clampSectionText(raw);
3446
+ }
3447
+ } catch {
3448
+ }
3449
+ return DEFAULT_DESCRIPTION_TEXT;
3450
+ }
3451
+ async function buildSkillsSummaryText() {
3452
+ const skills = (await discoverSkills()).filter(
3453
+ (skill) => !HIDDEN_HOME_SKILLS.has(skill.name)
3454
+ );
3455
+ if (skills.length === 0) {
3456
+ return "No skills installed.";
3457
+ }
3458
+ const visible = skills.slice(0, MAX_HOME_SKILLS);
3459
+ const lines = visible.map(
3460
+ (skill) => `\u2022 *${skill.name}* \u2014 ${skill.description}`
3461
+ );
3462
+ if (skills.length > visible.length) {
3463
+ lines.push(`\u2022 \u2026and ${skills.length - visible.length} more`);
3464
+ }
3465
+ return lines.join("\n");
3466
+ }
3467
+ function accountLabel(account) {
3468
+ const label = account.label ?? account.id;
3469
+ return account.url ? `<${account.url}|${label}>` : label;
3470
+ }
3471
+ function connectedAccountText(plugin, account) {
3472
+ return account ? `*${plugin.manifest.name}*
3473
+ Connected as ${accountLabel(account)}` : `*${plugin.manifest.name}*
3474
+ ${plugin.manifest.description}`;
3475
+ }
3476
+ async function connectedOAuthTokens(userId, plugin, userTokenStore) {
3477
+ if (plugin.manifest.oauth || plugin.manifest.credentials) {
3478
+ const stored = await userTokenStore.get(userId, plugin.manifest.name);
3479
+ return stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope) ? stored : void 0;
3480
+ }
3481
+ return void 0;
3482
+ }
3483
+ async function hasConnectedMcpAccount(userId, plugin) {
3484
+ if (plugin.manifest.mcp) {
3485
+ return Boolean(
3486
+ (await getMcpStoredOAuthCredentials(userId, plugin.manifest.name))?.tokens
3487
+ );
3488
+ }
3489
+ return false;
3490
+ }
3491
+ async function buildHomeView(userId, userTokenStore) {
3492
+ const runtimeMetadata = getRuntimeMetadata();
3493
+ const descriptionText = loadDescriptionText();
3494
+ const skillsSummaryText = await buildSkillsSummaryText();
3495
+ const providers = pluginCatalogRuntime.getProviders();
3496
+ const connectedSections = [];
3497
+ for (const plugin of providers) {
3498
+ const tokens = await connectedOAuthTokens(userId, plugin, userTokenStore);
3499
+ if (!tokens && !await hasConnectedMcpAccount(userId, plugin)) continue;
3500
+ connectedSections.push({
3501
+ type: "section",
3502
+ text: {
3503
+ type: "mrkdwn",
3504
+ text: connectedAccountText(plugin, tokens?.account)
3505
+ },
3506
+ accessory: {
3507
+ type: "button",
3508
+ text: { type: "plain_text", text: "Unlink" },
3509
+ action_id: "app_home_disconnect",
3510
+ value: plugin.manifest.name,
3511
+ style: "danger"
3512
+ }
3513
+ });
3514
+ }
3515
+ const accountBlocks = connectedSections.length > 0 ? connectedSections : [
3516
+ {
3517
+ type: "section",
3518
+ text: {
3519
+ type: "mrkdwn",
3520
+ text: "No connected accounts"
3521
+ }
3522
+ }
3523
+ ];
3524
+ return {
3525
+ type: "home",
3526
+ blocks: [
3527
+ {
3528
+ type: "header",
3529
+ text: {
3530
+ type: "plain_text",
3531
+ text: "Junior"
3532
+ }
3533
+ },
3534
+ {
3535
+ type: "section",
3536
+ text: {
3537
+ type: "mrkdwn",
3538
+ text: descriptionText
3539
+ }
3540
+ },
3541
+ { type: "divider" },
3542
+ {
3543
+ type: "header",
3544
+ text: {
3545
+ type: "plain_text",
3546
+ text: "What I can help with"
3547
+ }
3548
+ },
3549
+ {
3550
+ type: "section",
3551
+ text: {
3552
+ type: "mrkdwn",
3553
+ text: skillsSummaryText
3554
+ }
3555
+ },
3556
+ { type: "divider" },
3557
+ {
3558
+ type: "header",
3559
+ text: {
3560
+ type: "plain_text",
3561
+ text: "Connected accounts"
3562
+ }
3563
+ },
3564
+ ...accountBlocks,
3565
+ {
3566
+ type: "context",
3567
+ elements: [
3568
+ {
3569
+ type: "mrkdwn",
3570
+ text: `*junior version:* \`${runtimeMetadata.version ?? "unknown"}\``
3571
+ }
3572
+ ]
3573
+ }
3574
+ ]
3575
+ };
3576
+ }
3577
+ async function publishAppHomeView(slackClient, userId, userTokenStore) {
3578
+ const view = await buildHomeView(userId, userTokenStore);
3579
+ await slackClient.views.publish({ user_id: userId, view });
3580
+ }
3581
+
3582
+ // src/handlers/oauth-callback.ts
3583
+ function htmlErrorResponse(title, message, status) {
3584
+ return htmlCallbackResponse(escapeXml(title), escapeXml(message), status);
3585
+ }
3586
+ async function persistCompletedOAuthReplyState(args) {
3587
+ const currentState = await getPersistedThreadState(args.conversationId);
3588
+ const conversation = coerceThreadConversationState(currentState);
3589
+ await hydrateConversationMessages({
3590
+ conversation,
3591
+ conversationId: args.conversationId
3592
+ });
3593
+ const artifacts = coerceThreadArtifactsState(currentState);
3594
+ const userMessage = getTurnUserMessage(conversation, args.sessionId);
3595
+ const statePatch = buildDeliveredTurnStatePatch({
3596
+ artifacts,
3597
+ conversation,
3598
+ reply: args.reply,
3599
+ sessionId: args.sessionId,
3600
+ userMessageId: userMessage?.id
3601
+ });
3602
+ await persistThreadStateById(args.conversationId, {
3603
+ ...statePatch
3604
+ });
3605
+ }
3606
+ function pluginAuthorizationId(args) {
3607
+ return `${args.sessionId}:plugin:${args.provider}`;
3608
+ }
3609
+ async function failSessionRecordBestEffort2(args) {
3610
+ try {
3611
+ await failAgentTurnSessionRecord({
3612
+ conversationId: args.conversationId,
3613
+ expectedVersion: args.expectedVersion,
3614
+ sessionId: args.sessionId,
3615
+ errorMessage: args.errorMessage
3616
+ });
3617
+ } catch (error) {
3618
+ logException(
3619
+ error,
3620
+ "oauth_callback_session_record_fail_persist_failed",
3621
+ {},
3622
+ {
3623
+ "app.ai.conversation_id": args.conversationId,
3624
+ "app.ai.session_id": args.sessionId
3625
+ },
3626
+ "Failed to mark OAuth-resumed turn session record failed"
3627
+ );
3628
+ }
3629
+ }
3630
+ async function persistFailedOAuthReplyState(args) {
3631
+ const currentState = await getPersistedThreadState(args.conversationId);
3632
+ const conversation = coerceThreadConversationState(currentState);
3633
+ await hydrateConversationMessages({
3634
+ conversation,
3635
+ conversationId: args.conversationId
3636
+ });
3637
+ clearPendingAuth(conversation, args.sessionId);
3638
+ markTurnFailed({
3639
+ conversation,
3640
+ nowMs: Date.now(),
3641
+ sessionId: args.sessionId,
3642
+ userMessageId: getTurnUserMessage(conversation, args.sessionId)?.id,
3643
+ markConversationMessage,
3644
+ updateConversationStats
3645
+ });
3646
+ await failSessionRecordBestEffort2({
3647
+ conversationId: args.conversationId,
3648
+ expectedVersion: args.expectedVersion,
3649
+ sessionId: args.sessionId,
3650
+ errorMessage: "OAuth-resumed turn failed"
3651
+ });
3652
+ await persistThreadStateById(args.conversationId, {
3653
+ conversation
3654
+ });
3655
+ }
3656
+ async function resumeOAuthSessionRecordTurn(stored, options) {
3657
+ if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.destination || !stored.source || !stored.threadTs) {
3658
+ return;
3659
+ }
3660
+ const destination = requireSlackDestination(
3661
+ stored.destination,
3662
+ "OAuth resume"
3663
+ );
3664
+ const currentState = await getPersistedThreadState(
3665
+ stored.resumeConversationId
3666
+ );
3667
+ const conversation = coerceThreadConversationState(currentState);
3668
+ await hydrateConversationMessages({
3669
+ conversation,
3670
+ conversationId: stored.resumeConversationId
3671
+ });
3672
+ const pendingAuth = getConversationPendingAuth({
3673
+ conversation,
3674
+ kind: "plugin",
3675
+ provider: stored.provider,
3676
+ actorId: stored.userId,
3677
+ ...stored.scope ? { scope: stored.scope } : {}
3678
+ });
3679
+ const resolvedSessionId = pendingAuth?.sessionId ?? stored.resumeSessionId;
3680
+ const userMessage = resolvedSessionId ? getTurnUserMessage(conversation, resolvedSessionId) : void 0;
3681
+ if (pendingAuth) {
3682
+ if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
3683
+ clearPendingAuth(conversation, pendingAuth.sessionId);
3684
+ await persistThreadStateById(stored.resumeConversationId, {
3685
+ conversation
3686
+ });
3687
+ await abandonAgentTurnSessionRecord({
3688
+ conversationId: stored.resumeConversationId,
3689
+ sessionId: pendingAuth.sessionId,
3690
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
3691
+ });
3692
+ return;
3693
+ }
3694
+ }
3695
+ const sessionRecord = await getAgentTurnSessionRecord(
3696
+ stored.resumeConversationId,
3697
+ resolvedSessionId
3698
+ );
3699
+ if (!sessionRecord) {
3700
+ return;
3701
+ }
3702
+ if (sessionRecord.state === "completed" || sessionRecord.state === "failed" || sessionRecord.state === "abandoned") {
3703
+ return;
3704
+ }
3705
+ if (sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "auth") {
3706
+ return;
3707
+ }
3708
+ if (!userMessage?.author?.userId) {
3709
+ return;
3710
+ }
3711
+ if (!pendingAuth && conversation.processing.activeTurnId !== stored.resumeSessionId) {
3712
+ return;
3713
+ }
3714
+ await resumeSlackTurn({
3715
+ messageText: userMessage.text,
3716
+ conversationId: stored.resumeConversationId,
3717
+ turnId: resolvedSessionId,
3718
+ channelId: stored.channelId,
3719
+ threadTs: stored.threadTs,
3720
+ messageTs: getTurnUserSlackMessageTs(userMessage),
3721
+ lockKey: stored.resumeConversationId,
3722
+ initialText: "",
3723
+ agentRunner: options.agentRunner,
3724
+ beforeStart: async () => {
3725
+ const lockedState = await getPersistedThreadState(
3726
+ stored.resumeConversationId
3727
+ );
3728
+ const lockedConversation = coerceThreadConversationState(lockedState);
3729
+ await hydrateConversationMessages({
3730
+ conversation: lockedConversation,
3731
+ conversationId: stored.resumeConversationId
3732
+ });
3733
+ const lockedArtifacts = coerceThreadArtifactsState(lockedState);
3734
+ const lockedPendingAuth = getConversationPendingAuth({
3735
+ conversation: lockedConversation,
3736
+ kind: "plugin",
3737
+ provider: stored.provider,
3738
+ actorId: stored.userId,
3739
+ ...stored.scope ? { scope: stored.scope } : {}
3740
+ });
3741
+ const lockedSessionId = lockedPendingAuth?.sessionId ?? stored.resumeSessionId;
3742
+ if (lockedSessionId !== resolvedSessionId) {
3743
+ return false;
3744
+ }
3745
+ const lockedSessionRecord = await getAgentTurnSessionRecord(
3746
+ stored.resumeConversationId,
3747
+ lockedSessionId
3748
+ );
3749
+ if (!lockedSessionRecord || lockedSessionRecord.state !== "awaiting_resume" || lockedSessionRecord.resumeReason !== "auth") {
3750
+ return false;
3751
+ }
3752
+ if (lockedPendingAuth) {
3753
+ if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
3754
+ clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
3755
+ await persistThreadStateById(stored.resumeConversationId, {
3756
+ conversation: lockedConversation
3757
+ });
3758
+ await abandonAgentTurnSessionRecord({
3759
+ conversationId: stored.resumeConversationId,
3760
+ sessionId: lockedPendingAuth.sessionId,
3761
+ errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
3762
+ });
3763
+ return false;
3764
+ }
3765
+ } else if (lockedConversation.processing.activeTurnId !== stored.resumeSessionId) {
3766
+ return false;
3767
+ }
3768
+ const lockedUserMessage = getTurnUserMessage(
3769
+ lockedConversation,
3770
+ lockedSessionId
3771
+ );
3772
+ if (!lockedUserMessage?.author?.userId) {
3773
+ return false;
3774
+ }
3775
+ const lockedConversationContext = buildConversationContext(
3776
+ lockedConversation,
3777
+ {
3778
+ excludeMessageId: lockedUserMessage.id
3779
+ }
3780
+ );
3781
+ const lockedChannelConfiguration = getChannelConfigurationServiceById(
3782
+ stored.channelId
3783
+ );
3784
+ let actor;
3785
+ try {
3786
+ actor = createSlackResumeActor({
3787
+ actor: isUserActor(lockedSessionRecord.actor) ? lockedSessionRecord.actor : void 0,
3788
+ teamId: destination.teamId,
3789
+ userId: lockedUserMessage.author.userId
3790
+ });
3791
+ } catch {
3792
+ await failAgentTurnSessionRecord({
3793
+ conversationId: stored.resumeConversationId,
3794
+ expectedVersion: lockedSessionRecord.version,
3795
+ sessionId: lockedSessionId,
3796
+ errorMessage: "Stored Slack actor identity did not match OAuth actor"
3797
+ });
3798
+ return false;
3799
+ }
3800
+ if (!lockedSessionRecord.source) {
3801
+ await failAgentTurnSessionRecord({
3802
+ conversationId: stored.resumeConversationId,
3803
+ expectedVersion: lockedSessionRecord.version,
3804
+ sessionId: lockedSessionId,
3805
+ errorMessage: "Stored Slack source missing for OAuth resume"
3806
+ });
3807
+ return false;
3808
+ }
3809
+ await recordAuthorizationCompleted({
3810
+ conversationId: stored.resumeConversationId,
3811
+ kind: "plugin",
3812
+ provider: stored.provider,
3813
+ actorId: stored.userId,
3814
+ authorizationId: pluginAuthorizationId({
3815
+ provider: stored.provider,
3816
+ sessionId: lockedSessionId
3817
+ })
3818
+ });
3819
+ const lockedMessageTs = getTurnUserSlackMessageTs(lockedUserMessage);
3820
+ return {
3821
+ messageText: lockedUserMessage.text,
3822
+ messageTs: lockedMessageTs,
3823
+ inputMessageIds: [lockedUserMessage.id],
3824
+ replyContext: {
3825
+ input: {
3826
+ conversationContext: lockedConversationContext,
3827
+ // Pi history is SQL-authoritative: the resumed run reads its
3828
+ // session record first and falls back to the step projection.
3829
+ piMessages: await loadProjection({
3830
+ conversationId: stored.resumeConversationId
3831
+ }),
3832
+ ...getTurnUserReplyAttachmentContext(lockedUserMessage)
3833
+ },
3834
+ routing: {
3835
+ credentialContext: {
3836
+ actor: {
3837
+ type: "user",
3838
+ userId: actor.userId
3839
+ }
3840
+ },
3841
+ actor,
3842
+ destination,
3843
+ source: lockedSessionRecord.source,
3844
+ toolChannelId: lockedArtifacts.assistantContextChannelId ?? stored.channelId
3845
+ },
3846
+ policy: {
3847
+ channelConfiguration: lockedChannelConfiguration
3848
+ },
3849
+ state: {
3850
+ artifactState: lockedArtifacts,
3851
+ pendingAuth: lockedPendingAuth,
3852
+ sandboxRef: getPersistedSandboxState(lockedState)
3853
+ },
3854
+ durability: {
3855
+ recordPendingAuth: async (nextPendingAuth) => {
3856
+ lockedConversation.processing.pendingAuth = nextPendingAuth;
3857
+ await persistThreadStateById(stored.resumeConversationId, {
3858
+ conversation: lockedConversation
3859
+ });
3860
+ }
3861
+ }
3862
+ },
3863
+ commitResult: async (reply) => {
3864
+ logInfo(
3865
+ "oauth_callback_resume_complete",
3866
+ {},
3867
+ {
3868
+ "app.credential.provider": stored.provider,
3869
+ "app.ai.outcome": reply.diagnostics.outcome,
3870
+ "app.ai.tool_calls": reply.diagnostics.toolCalls.length
3871
+ },
3872
+ "OAuth callback auto-resumed session record finished replying"
3873
+ );
3874
+ await persistCompletedOAuthReplyState({
3875
+ conversationId: stored.resumeConversationId,
3876
+ sessionId: lockedSessionId,
3877
+ reply
3878
+ });
3879
+ },
3880
+ onPostDeliveryCommitFailure: async () => {
3881
+ await failAgentTurnSessionRecord({
3882
+ conversationId: stored.resumeConversationId,
3883
+ expectedVersion: lockedSessionRecord.version,
3884
+ sessionId: lockedSessionId,
3885
+ errorMessage: "OAuth-resumed reply was delivered but completion state did not persist"
3886
+ });
3887
+ },
3888
+ onFailure: async () => {
3889
+ await persistFailedOAuthReplyState({
3890
+ conversationId: stored.resumeConversationId,
3891
+ expectedVersion: lockedSessionRecord.version,
3892
+ sessionId: lockedSessionId
3893
+ });
3894
+ },
3895
+ onAuthPause: async () => {
3896
+ await persistAuthPauseTurnState({
3897
+ sessionId: lockedSessionId,
3898
+ threadStateId: stored.resumeConversationId
3899
+ });
3900
+ },
3901
+ onSuspend: async (resumeVersion) => {
3902
+ await scheduleAgentContinue({
3903
+ conversationId: stored.resumeConversationId,
3904
+ destination,
3905
+ sessionId: lockedSessionId,
3906
+ expectedVersion: resumeVersion
3907
+ });
3908
+ }
3909
+ };
3910
+ }
3911
+ });
3912
+ }
3913
+ async function GET2(request, provider, waitUntil, options) {
3914
+ const localRelay = await relayLocalOAuthCallback(request);
3915
+ if (localRelay) {
3916
+ return localRelay;
3917
+ }
3918
+ const providerConfig = pluginCatalogRuntime.getOAuthConfig(provider);
3919
+ if (!providerConfig) {
3920
+ return htmlErrorResponse(
3921
+ "Unknown provider",
3922
+ "The OAuth provider in this link is not recognized.",
3923
+ 404
3924
+ );
3925
+ }
3926
+ const providerLabel = formatProviderLabel(provider);
3927
+ const url = new URL(request.url);
3928
+ const errorParam = url.searchParams.get("error");
3929
+ const code = url.searchParams.get("code");
3930
+ const state = url.searchParams.get("state");
3931
+ if (errorParam) {
3932
+ if (state) {
3933
+ const cleanupAdapter = getStateAdapter();
3934
+ await cleanupAdapter.delete(`oauth-state:${state}`);
3935
+ }
3936
+ if (errorParam === "access_denied") {
3937
+ return htmlErrorResponse(
3938
+ "Authorization declined",
3939
+ `You declined the ${providerLabel} authorization request. Return to Junior and connect your ${providerLabel} account again if you change your mind.`,
3940
+ 400
3941
+ );
3942
+ }
3943
+ return htmlErrorResponse(
3944
+ "Authorization failed",
3945
+ `${providerLabel} returned an error: ${errorParam}. Return to Junior and try again.`,
3946
+ 400
3947
+ );
3948
+ }
3949
+ if (!code || !state) {
3950
+ return htmlErrorResponse(
3951
+ "Invalid request",
3952
+ "This authorization link is missing required parameters.",
3953
+ 400
3954
+ );
3955
+ }
3956
+ const stateAdapter = getStateAdapter();
3957
+ const stateKey = `oauth-state:${state}`;
3958
+ const stored = parseOAuthStatePayload(await stateAdapter.get(stateKey));
3959
+ if (!stored) {
3960
+ return htmlErrorResponse(
3961
+ "Link expired",
3962
+ `This authorization link has expired (links are valid for 10 minutes). Return to Junior and connect your ${providerLabel} account again to get a new link.`,
3963
+ 400
3964
+ );
3965
+ }
3966
+ if (stored.provider !== provider) {
3967
+ return htmlErrorResponse(
3968
+ "Provider mismatch",
3969
+ "This authorization link does not match the expected provider.",
3970
+ 400
3971
+ );
3972
+ }
3973
+ await stateAdapter.delete(stateKey);
3974
+ const clientId = process.env[providerConfig.clientIdEnv]?.trim();
3975
+ const clientSecret = process.env[providerConfig.clientSecretEnv]?.trim();
3976
+ if (!clientId || !clientSecret) {
3977
+ return htmlErrorResponse(
3978
+ "Configuration error",
3979
+ "OAuth client credentials are not configured on the server.",
3980
+ 500
3981
+ );
3982
+ }
3983
+ const baseUrl = resolveBaseUrl();
3984
+ if (!baseUrl) {
3985
+ return htmlErrorResponse(
3986
+ "Configuration error",
3987
+ "The server cannot determine its base URL.",
3988
+ 500
3989
+ );
3990
+ }
3991
+ const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
3992
+ const requestedScope = stored.scope ?? providerConfig.scope;
3993
+ let tokenResponse;
3994
+ try {
3995
+ const tokenRequest = buildOAuthTokenRequest({
3996
+ clientId,
3997
+ clientSecret,
3998
+ payload: {
3999
+ grant_type: "authorization_code",
4000
+ code,
4001
+ redirect_uri: redirectUri
4002
+ },
4003
+ tokenAuthMethod: providerConfig.tokenAuthMethod,
4004
+ tokenExtraHeaders: providerConfig.tokenExtraHeaders
4005
+ });
4006
+ tokenResponse = await fetch(providerConfig.tokenEndpoint, {
4007
+ method: "POST",
4008
+ headers: tokenRequest.headers,
4009
+ body: tokenRequest.body
4010
+ });
4011
+ } catch {
4012
+ return htmlErrorResponse(
4013
+ "Connection failed",
4014
+ "Failed to exchange the authorization code. Please try again.",
4015
+ 500
4016
+ );
4017
+ }
4018
+ if (!tokenResponse.ok) {
4019
+ return htmlErrorResponse(
4020
+ "Connection failed",
4021
+ "The token exchange with the provider failed. Please try again.",
4022
+ 500
4023
+ );
4024
+ }
4025
+ let parsedTokenResponse;
4026
+ try {
4027
+ const tokenData = await tokenResponse.json();
4028
+ parsedTokenResponse = parseOAuthTokenResponse(tokenData, requestedScope, {
4029
+ treatEmptyScopeAsUnreported: providerConfig.treatEmptyScopeAsUnreported
4030
+ });
4031
+ } catch {
4032
+ return htmlErrorResponse(
4033
+ "Connection failed",
4034
+ "The provider returned an incomplete token response. Please try again.",
4035
+ 500
4036
+ );
4037
+ }
4038
+ if (!hasRequiredOAuthScope(parsedTokenResponse.scope, requestedScope)) {
4039
+ return htmlErrorResponse(
4040
+ "Connection failed",
4041
+ `The ${providerLabel} authorization did not grant the access Junior requires. Return to Junior and connect your ${providerLabel} account again.`,
4042
+ 400
4043
+ );
4044
+ }
4045
+ const userTokenStore = createUserTokenStore();
4046
+ let account;
4047
+ try {
4048
+ account = await resolvePluginOAuthAccount({
4049
+ provider,
4050
+ tokens: parsedTokenResponse
4051
+ });
4052
+ } catch {
4053
+ return htmlErrorResponse(
4054
+ "Connection failed",
4055
+ `Junior could not verify the connected ${providerLabel} account. Please try again.`,
4056
+ 500
4057
+ );
4058
+ }
4059
+ await userTokenStore.set(stored.userId, provider, {
4060
+ ...parsedTokenResponse,
4061
+ ...account ? { account } : {}
4062
+ });
4063
+ if (stored.destination?.platform !== "local") {
4064
+ waitUntil(async () => {
4065
+ try {
4066
+ await publishAppHomeView(
4067
+ getSlackClient(),
4068
+ stored.userId,
4069
+ userTokenStore
4070
+ );
4071
+ } catch {
4072
+ }
4073
+ });
4074
+ }
4075
+ const resumesAgentTurn = Boolean(
4076
+ stored.destination?.platform !== "local" && stored.resumeConversationId && stored.resumeSessionId
4077
+ );
4078
+ if (resumesAgentTurn) {
4079
+ waitUntil(async () => {
4080
+ try {
4081
+ await resumeOAuthSessionRecordTurn(stored, options);
4082
+ } catch (error) {
4083
+ if (error instanceof ResumeTurnBusyError) {
4084
+ logWarn(
4085
+ "oauth_callback_resume_busy",
4086
+ {
4087
+ ...stored.resumeConversationId && {
4088
+ conversationId: stored.resumeConversationId
4089
+ },
4090
+ ...stored.userId && { actorId: stored.userId },
4091
+ ...stored.channelId && { channelId: stored.channelId }
4092
+ },
4093
+ {
4094
+ "app.credential.provider": stored.provider,
4095
+ ...stored.resumeSessionId && {
4096
+ "app.ai.session_id": stored.resumeSessionId
4097
+ }
4098
+ },
4099
+ "OAuth callback resume was busy; user must send another message to continue"
4100
+ );
4101
+ return;
4102
+ }
4103
+ throw error;
4104
+ }
4105
+ });
4106
+ } else if (stored.channelId && stored.threadTs) {
4107
+ const { channelId, threadTs } = stored;
4108
+ waitUntil(
4109
+ () => postSlackMessage({
4110
+ channelId,
4111
+ threadTs,
4112
+ text: `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
4113
+ })
4114
+ );
4115
+ }
4116
+ const statusMessage = stored.destination?.platform === "local" ? "Your request is continuing in the local Junior client." : resumesAgentTurn ? "Your request is being processed in Slack." : "You can close this tab and return to Slack.";
4117
+ const html = `<!DOCTYPE html>
4118
+ <html>
4119
+ <head><title>${providerLabel} Connected</title></head>
4120
+ <body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
4121
+ <div style="text-align: center;">
4122
+ <h1>${providerLabel} account connected</h1>
4123
+ <p>${statusMessage}</p>
4124
+ </div>
4125
+ </body>
4126
+ </html>`;
4127
+ return new Response(html, {
4128
+ status: 200,
4129
+ headers: { "Content-Type": "text/html; charset=utf-8" }
4130
+ });
4131
+ }
4132
+
4133
+ // src/chat/local/credential-sync.ts
4134
+ import { createHmac, createSecretKey, timingSafeEqual } from "crypto";
4135
+ import {
4136
+ pluginStoredTokensSchema
4137
+ } from "@sentry/junior-plugin-api";
4138
+ import { z as z2 } from "zod";
4139
+ var LOCAL_CREDENTIAL_SYNC_CONTEXT = "junior.local-credential-sync.v1";
4140
+ var LOCAL_CREDENTIAL_SYNC_MAX_AGE_MS = 6e4;
4141
+ var localCredentialSyncSchema = z2.object({
4142
+ createdAtMs: z2.number().finite(),
4143
+ provider: z2.string().regex(/^[a-z][a-z0-9-]*$/),
4144
+ tokens: pluginStoredTokensSchema
4145
+ }).strict();
4146
+ function syncSecret() {
4147
+ const secret = process.env.JUNIOR_SECRET?.trim();
4148
+ if (!secret) {
4149
+ throw new Error("Local credential sync requires JUNIOR_SECRET");
4150
+ }
4151
+ return secret;
4152
+ }
4153
+ function signature(body) {
4154
+ const signingKey = createSecretKey(Buffer.from(syncSecret(), "utf8"));
4155
+ return createHmac("sha256", signingKey).update(`${LOCAL_CREDENTIAL_SYNC_CONTEXT}:${body}`).digest("base64url");
4156
+ }
4157
+ function signaturesMatch(expected, actual) {
4158
+ const expectedBuffer = Buffer.from(expected);
4159
+ const actualBuffer = Buffer.from(actual);
4160
+ return expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer);
4161
+ }
4162
+ async function syncLocalOAuthCredential(provider, tokens) {
4163
+ const body = JSON.stringify({
4164
+ createdAtMs: Date.now(),
4165
+ provider,
4166
+ tokens
4167
+ });
4168
+ const port = process.env.PORT?.trim() || "3000";
4169
+ const response = await fetch(
4170
+ `http://127.0.0.1:${port}/api/internal/local-oauth-credentials`,
4171
+ {
4172
+ method: "POST",
4173
+ headers: {
4174
+ "content-type": "application/json",
4175
+ "x-junior-local-credential-signature": signature(body)
4176
+ },
4177
+ body
4178
+ }
4179
+ );
4180
+ if (!response.ok) {
4181
+ throw new Error(
4182
+ `Could not sync local OAuth credential: HTTP ${response.status}`
4183
+ );
4184
+ }
4185
+ }
4186
+ async function receiveLocalOAuthCredential(request) {
4187
+ const hostname = new URL(request.url).hostname;
4188
+ if (process.env.NODE_ENV !== "development" || hostname !== "127.0.0.1" && hostname !== "localhost") {
4189
+ return new Response(null, { status: 404 });
4190
+ }
4191
+ const body = await request.text();
4192
+ const actualSignature = request.headers.get(
4193
+ "x-junior-local-credential-signature"
4194
+ );
4195
+ if (!actualSignature || !signaturesMatch(signature(body), actualSignature)) {
4196
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
4197
+ }
4198
+ let value;
4199
+ try {
4200
+ value = JSON.parse(body);
4201
+ } catch {
4202
+ return Response.json({ error: "Invalid request" }, { status: 400 });
4203
+ }
4204
+ const payload = localCredentialSyncSchema.safeParse(value);
4205
+ if (!payload.success || Math.abs(Date.now() - payload.data.createdAtMs) > LOCAL_CREDENTIAL_SYNC_MAX_AGE_MS) {
4206
+ return Response.json({ error: "Invalid request" }, { status: 400 });
4207
+ }
4208
+ await createUserTokenStore().set(
4209
+ "local-cli",
4210
+ payload.data.provider,
4211
+ payload.data.tokens
4212
+ );
4213
+ return new Response(null, { status: 204 });
4214
+ }
4215
+
4216
+ export {
4217
+ sendSlackReply,
4218
+ getSlackApiErrorCode,
4219
+ getSlackErrorObservabilityAttributes,
4220
+ isRetryableSlackPostError,
4221
+ isSlackTitlePermissionError,
4222
+ isInvalidConversationRecordError,
4223
+ CONVERSATION_WORK_CHECK_IN_INTERVAL_MS,
4224
+ CONVERSATION_WORK_STALE_ENQUEUE_MS,
4225
+ isFinalAttempt,
4226
+ getConversationWorkState2 as getConversationWorkState,
4227
+ countPendingConversationMessages,
4228
+ hasRunnableConversationWork,
4229
+ ensureConversationWake,
4230
+ appendAndEnqueueInboundMessage,
4231
+ clearConsumedConversationWake2 as clearConsumedConversationWake,
4232
+ startConversationWork2 as startConversationWork,
4233
+ checkInConversationWork2 as checkInConversationWork,
4234
+ drainConversationMailbox2 as drainConversationMailbox,
4235
+ ackMessages2 as ackMessages,
4236
+ requestConversationContinuation2 as requestConversationContinuation,
4237
+ beginConversationResume2 as beginConversationResume,
4238
+ releaseConversationWork2 as releaseConversationWork,
4239
+ completeConversationWork2 as completeConversationWork,
4240
+ recordAttemptFailure2 as recordAttemptFailure,
4241
+ deadLetterAttempt2 as deadLetterAttempt,
4242
+ clearExpiredConversationLease2 as clearExpiredConversationLease,
4243
+ removeActiveConversation2 as removeActiveConversation,
4244
+ listActiveConversationIds2 as listActiveConversationIds,
4245
+ createSlackAdapterAssistantStatusSession,
4246
+ stripLeadingBotMention,
4247
+ getThreadId,
4248
+ getRunId,
4249
+ getChannelId,
4250
+ getThreadTs,
4251
+ getAssistantThreadContext,
4252
+ getMessageTs,
4253
+ shouldKeepProcessingReactionForToolInvocation,
4254
+ startSlackProcessingReaction,
4255
+ buildAuthPauseResponse,
4256
+ getTurnRequestDeadline,
4257
+ runWithTurnRequestDeadline,
4258
+ ResumeTurnBusyError,
4259
+ resumeSlackTurn,
4260
+ getAwaitingAgentContinueRequest,
4261
+ scheduleAgentContinue,
4262
+ GET,
4263
+ publishAppHomeView,
4264
+ GET2,
4265
+ syncLocalOAuthCredential,
4266
+ receiveLocalOAuthCredential
4267
+ };