@sentry/junior 0.203.0 → 0.205.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 (174) hide show
  1. package/dist/agent-D4nu8MQV.js +17521 -0
  2. package/dist/agent-hooks-DXHpTJ4m.js +4042 -0
  3. package/dist/agent-runner-C04colea.js +678 -0
  4. package/dist/api/schema.d.ts +2736 -2639
  5. package/dist/api/schema.js +4 -169
  6. package/dist/api.d.ts +23 -17
  7. package/dist/api.js +3881 -5326
  8. package/dist/app-Cb776lL8.d.ts +247 -0
  9. package/dist/app.d.ts +4 -520
  10. package/dist/app.js +9524 -11875
  11. package/dist/auth-pause-state-C2P3guS1.js +168 -0
  12. package/dist/catalog-runtime-CXw3asJD.js +7 -0
  13. package/dist/checkpoint-CrViJ8Ps.js +2450 -0
  14. package/dist/cli/chat.d.ts +20 -0
  15. package/dist/cli/chat.js +229 -336
  16. package/dist/cli/check.d.ts +8 -0
  17. package/dist/cli/check.js +513 -806
  18. package/dist/cli/env.d.ts +13 -0
  19. package/dist/cli/env.js +63 -12
  20. package/dist/cli/init.d.ts +3 -0
  21. package/dist/cli/init.js +90 -159
  22. package/dist/cli/main.d.ts +6 -0
  23. package/dist/cli/main.js +43 -64
  24. package/dist/cli/plugins.d.ts +13 -0
  25. package/dist/cli/plugins.js +148 -227
  26. package/dist/cli/run.d.ts +16 -0
  27. package/dist/cli/run.js +52 -8
  28. package/dist/cli/snapshot-create.d.ts +4 -0
  29. package/dist/cli/snapshot-create.js +71 -125
  30. package/dist/cli/upgrade.d.ts +15 -0
  31. package/dist/cli/upgrade.js +232 -335
  32. package/dist/config-BOtJQ7Pc.js +941 -0
  33. package/dist/context-B45_WqVX.js +298 -0
  34. package/dist/conversation-privacy-BZRA6Pes.js +182 -0
  35. package/dist/conversation-work-C6y3T5Hx.js +51 -0
  36. package/dist/credential-sync-Bep34IB_.js +2661 -0
  37. package/dist/db-BeyXKJOI.d.ts +5 -0
  38. package/dist/destinations-BeBiJeEM.js +29 -0
  39. package/dist/execution-stats-B6lgWxCK.js +373 -0
  40. package/dist/executor-BlUhFL7_.js +721 -0
  41. package/dist/experimental-CIXf7Uye.js +46 -0
  42. package/dist/history-ClE4toxe.d.ts +32 -0
  43. package/dist/history-CqTuSAdI.js +441 -0
  44. package/dist/instrumentation.d.ts +3 -3
  45. package/dist/instrumentation.js +130 -176
  46. package/dist/job-queue-DLMqIK74.js +63 -0
  47. package/dist/logging-BMJzdDlP.js +1210 -0
  48. package/dist/model-profile-CZmVoGdX.js +49 -0
  49. package/dist/nitro.d.ts +31 -36
  50. package/dist/nitro.js +275 -456
  51. package/dist/oauth-callback-server-WwG5_Zgm.js +130 -0
  52. package/dist/oauth-relay-CdCnu80W.js +62 -0
  53. package/dist/package-discovery-7a9YqLhE.js +455 -0
  54. package/dist/pending-auth-CUbXq06G.js +185 -0
  55. package/dist/plugin-module-BKGOXca2.js +81 -0
  56. package/dist/plugins-BI9A1vKA.d.ts +95 -0
  57. package/dist/plugins-egKWjgue.js +99 -0
  58. package/dist/private-trace-result-BLvBjy_e.js +16 -0
  59. package/dist/profile-T0tXE4zu.js +861 -0
  60. package/dist/registry-BA0__8ai.js +1419 -0
  61. package/dist/reporting-schema-DosTN__Q.js +105 -0
  62. package/dist/resolve-bvD1ORdv.js +546 -0
  63. package/dist/rolldown-runtime-B4FMCO8f.js +28 -0
  64. package/dist/runner-BXYxfIUL.js +392 -0
  65. package/dist/sandbox-egress-signals-ngEPjSEW.js +46 -0
  66. package/dist/schema-Rn8-XEaN.js +1140 -0
  67. package/dist/sentry-CD3SdJl-.js +26 -0
  68. package/dist/skills-DJOlYdBs.js +316 -0
  69. package/dist/spawn-h0R1bTEo.js +29 -0
  70. package/dist/stats-3YZn-Swi.js +2648 -0
  71. package/dist/store-C5qxF2ac.d.ts +208 -0
  72. package/dist/task-queue-CoXwd2eq.js +136 -0
  73. package/dist/task-runner-B2_mJZj0.js +639 -0
  74. package/dist/thread-state-CqZ1m-es.js +431 -0
  75. package/dist/turn-execution-fI6PZ0iI.js +47 -0
  76. package/dist/turn-lifecycle-CcnwrOcp.js +54 -0
  77. package/dist/usage-B5SfMb3H.js +296 -0
  78. package/dist/usage-schema-CKaDkH_j.js +20 -0
  79. package/dist/validation-DfWcvhuS.js +40 -0
  80. package/dist/vercel-queue-DIkVcfK4.js +167 -0
  81. package/dist/vercel.d.ts +6 -6
  82. package/dist/vercel.js +14 -6
  83. package/dist/version-CnXZ614E.d.ts +5 -0
  84. package/dist/version-oD-oPrbO.js +8 -0
  85. package/dist/version.d.ts +2 -4
  86. package/dist/version.js +2 -6
  87. package/dist/web-authorization-Crbzlmzo.js +76 -0
  88. package/dist/web-input-BOuq3oKK.js +494 -0
  89. package/dist/work-B91LQKHr.js +509 -0
  90. package/dist/worker-BcRRTe6k.js +625 -0
  91. package/package.json +9 -9
  92. package/dist/agent-hooks-GB2VPTNA.js +0 -51
  93. package/dist/catalog-runtime-UBLIWD3O.js +0 -14
  94. package/dist/chunk-2LYHWMR4.js +0 -46
  95. package/dist/chunk-352OWAZH.js +0 -320
  96. package/dist/chunk-3Y47VMHV.js +0 -6
  97. package/dist/chunk-3YJSKOWF.js +0 -164
  98. package/dist/chunk-4KSSKTQ5.js +0 -229
  99. package/dist/chunk-4UO6FK4G.js +0 -64
  100. package/dist/chunk-5AHX53NQ.js +0 -717
  101. package/dist/chunk-5J3E5FWA.js +0 -700
  102. package/dist/chunk-5PDAG6O3.js +0 -206
  103. package/dist/chunk-6Y5B5PEX.js +0 -80
  104. package/dist/chunk-7GWQ5S5R.js +0 -2086
  105. package/dist/chunk-7H2VZTWN.js +0 -103
  106. package/dist/chunk-7NL7T6VU.js +0 -406
  107. package/dist/chunk-A5L27LE4.js +0 -387
  108. package/dist/chunk-ADIAWL34.js +0 -81
  109. package/dist/chunk-ASTEMBKJ.js +0 -519
  110. package/dist/chunk-CEMMLV5L.js +0 -98
  111. package/dist/chunk-CM5HNV2W.js +0 -131
  112. package/dist/chunk-CRTRBBRI.js +0 -105
  113. package/dist/chunk-DA2KGZZI.js +0 -13
  114. package/dist/chunk-DIB4GX3G.js +0 -8
  115. package/dist/chunk-DKIJ2RP5.js +0 -68
  116. package/dist/chunk-DWPN6SCH.js +0 -172
  117. package/dist/chunk-EDKUFGPL.js +0 -64
  118. package/dist/chunk-ENY6SKIU.js +0 -1228
  119. package/dist/chunk-F4MGCZOX.js +0 -2943
  120. package/dist/chunk-FA6POGLH.js +0 -70
  121. package/dist/chunk-FSUR7GTG.js +0 -20475
  122. package/dist/chunk-GKEOHJL5.js +0 -1856
  123. package/dist/chunk-GMCB6W5Y.js +0 -333
  124. package/dist/chunk-IE2VPG7O.js +0 -1135
  125. package/dist/chunk-IZ5B5IHQ.js +0 -3656
  126. package/dist/chunk-JAL3CH4G.js +0 -27
  127. package/dist/chunk-KIIEUXD3.js +0 -1764
  128. package/dist/chunk-KXU4NIL7.js +0 -692
  129. package/dist/chunk-MTM5IFCY.js +0 -1190
  130. package/dist/chunk-N7TMSOZH.js +0 -235
  131. package/dist/chunk-NCEBK6PH.js +0 -36
  132. package/dist/chunk-NHJU2ZAL.js +0 -56
  133. package/dist/chunk-P6KTRNVC.js +0 -321
  134. package/dist/chunk-PDO5BLNM.js +0 -22
  135. package/dist/chunk-PDRNLLFT.js +0 -3295
  136. package/dist/chunk-PEQXYSSE.js +0 -45
  137. package/dist/chunk-PKEX2AQX.js +0 -198
  138. package/dist/chunk-PSJCGIGO.js +0 -1163
  139. package/dist/chunk-PZUSUL55.js +0 -506
  140. package/dist/chunk-R45M4TUS.js +0 -37
  141. package/dist/chunk-RC745GGZ.js +0 -122
  142. package/dist/chunk-SP3QKZXJ.js +0 -820
  143. package/dist/chunk-TXKUKMVO.js +0 -417
  144. package/dist/chunk-UE4GPWSD.js +0 -931
  145. package/dist/chunk-UIE3R5XU.js +0 -17
  146. package/dist/chunk-UXPG6ZIN.js +0 -84
  147. package/dist/chunk-VFUK3X5B.js +0 -50
  148. package/dist/chunk-VNMJWREK.js +0 -787
  149. package/dist/chunk-VZ7CSOX2.js +0 -706
  150. package/dist/chunk-WCNQJKFT.js +0 -10
  151. package/dist/chunk-WDZR3QQM.js +0 -135
  152. package/dist/chunk-WLYVJCHV.js +0 -1433
  153. package/dist/chunk-WS7NAFTM.js +0 -1520
  154. package/dist/chunk-X6GDDWQH.js +0 -60
  155. package/dist/chunk-XPDEYNQI.js +0 -102
  156. package/dist/chunk-XRW44VGM.js +0 -58
  157. package/dist/chunk-Y2CM7HXH.js +0 -111
  158. package/dist/chunk-Y7X25LFY.js +0 -15
  159. package/dist/conversation-work-VIOQ5ZF3.js +0 -54
  160. package/dist/db-NGDJV7YH.js +0 -26
  161. package/dist/discovery-DWVDM7UK.js +0 -44
  162. package/dist/experimental-P5FZM4CU.js +0 -12
  163. package/dist/oauth-callback-server-YFY7YEGK.js +0 -243
  164. package/dist/oauth-relay-5XPVRPYB.js +0 -8
  165. package/dist/plugins-YTYJMMJF.js +0 -14
  166. package/dist/runner-ORTNSB2Q.js +0 -522
  167. package/dist/sandbox-egress-signals-FIL7EGPR.js +0 -73
  168. package/dist/sentry-MB3NL3IR.js +0 -42
  169. package/dist/skills-H376WQOH.js +0 -27
  170. package/dist/spawn-T43WHLOS.js +0 -42
  171. package/dist/task-runner-LBZWF32V.js +0 -36
  172. package/dist/validation-REJLRKI6.js +0 -17
  173. package/dist/work-3KJ3RE76.js +0 -51
  174. package/dist/worker-QFEBU72E.js +0 -24
@@ -1,3295 +0,0 @@
1
- import {
2
- relayLocalOAuthCallback
3
- } from "./chunk-FA6POGLH.js";
4
- import {
5
- AgentRunError,
6
- executeTurn
7
- } from "./chunk-XRW44VGM.js";
8
- import {
9
- buildDeliveredTurnStatePatch,
10
- finalizeFailedTurnReplyWithEvent,
11
- getInterruptionMarker,
12
- getTurnUserMessage,
13
- getTurnUserMessageId,
14
- getTurnUserReplyAttachmentContext,
15
- getTurnUserSlackMessageTs,
16
- persistAuthPauseTurnState,
17
- requireTurnFailureEventId
18
- } from "./chunk-4KSSKTQ5.js";
19
- import {
20
- resolveTurnSessionRouting,
21
- scheduleSessionCompletedPluginTasks
22
- } from "./chunk-SP3QKZXJ.js";
23
- import {
24
- getTurnLifecycle
25
- } from "./chunk-EDKUFGPL.js";
26
- import {
27
- coerceThreadConversationState,
28
- configValueSchema,
29
- getLocationConfigurationService,
30
- getPersistedSandboxState,
31
- getPersistedThreadState,
32
- persistThreadStateById
33
- } from "./chunk-ASTEMBKJ.js";
34
- import {
35
- buildSentryConversationUrl,
36
- deleteWebAuthorization
37
- } from "./chunk-CRTRBBRI.js";
38
- import {
39
- createUserTokenStore,
40
- deleteMcpAuthSession,
41
- finalizeMcpAuthorization,
42
- getMcpAuthSession,
43
- getMcpProviderErrorAttributes,
44
- getMcpStoredOAuthCredentials,
45
- lookupSlackUserProfile,
46
- putMcpStoredOAuthCredentials,
47
- resolvePluginOAuthAccount
48
- } from "./chunk-PSJCGIGO.js";
49
- import {
50
- RetryableDeliveryError,
51
- addReactionToMessage,
52
- escapeSlackMrkdwnText,
53
- formatProviderLabel,
54
- formatSlackLink,
55
- normalizeSlackReplyMarkdown,
56
- normalizeSlackStatusText,
57
- parseOAuthStatePayload,
58
- postSlackMessage,
59
- removeReactionFromMessage,
60
- resolveBaseUrl,
61
- truncateStatusText
62
- } from "./chunk-UE4GPWSD.js";
63
- import {
64
- CooperativeTurnYieldError,
65
- clearPendingAuth,
66
- getAssistantReplyText,
67
- getConversationPendingAuth,
68
- isCooperativeTurnYieldError,
69
- isPendingAuthLatestRequest,
70
- markTurnFailed
71
- } from "./chunk-5PDAG6O3.js";
72
- import {
73
- abandonTurnRecord,
74
- buildConversationContext,
75
- buildTurnErrorResponse,
76
- commitAcceptedReply,
77
- failTurnRecord,
78
- getTurnRecord,
79
- hydrateConversationMessages,
80
- loadProjection,
81
- loadTurnCheckpoint,
82
- markConversationMessage,
83
- persistWithRetry,
84
- recordAuthenticationLinked,
85
- recordAuthorizationCompleted,
86
- recordDeliveredAssistantMessage,
87
- recordTurnSummary,
88
- saveTurnCheckpoint,
89
- turnHasReply
90
- } from "./chunk-F4MGCZOX.js";
91
- import {
92
- parseSlackThreadId,
93
- readSlackRawMessageContext,
94
- resolveSlackChannelIdFromMessage,
95
- resolveSlackChannelIdFromThreadId
96
- } from "./chunk-XPDEYNQI.js";
97
- import {
98
- ensureConversationWake,
99
- requestConversationWork
100
- } from "./chunk-KIIEUXD3.js";
101
- import {
102
- getTurnRequestDeadline
103
- } from "./chunk-NCEBK6PH.js";
104
- import {
105
- getVercelConversationWorkQueue
106
- } from "./chunk-DKIJ2RP5.js";
107
- import {
108
- SlackActionError,
109
- getDashboardConversationLink,
110
- getHeaderString,
111
- getPluginSlackConversationLink,
112
- getSlackClient,
113
- isDmChannel,
114
- normalizeSlackConversationId
115
- } from "./chunk-IZ5B5IHQ.js";
116
- import {
117
- getConversationStore,
118
- getSqlExecutor,
119
- requireSlackDestination,
120
- upsertIdentity,
121
- upsertLinkedIdentity
122
- } from "./chunk-GKEOHJL5.js";
123
- import {
124
- acquireActiveLock,
125
- getStateAdapter
126
- } from "./chunk-P6KTRNVC.js";
127
- import {
128
- botConfig,
129
- escapeXml,
130
- getChatConfig,
131
- getRuntimeMetadata,
132
- normalizeSlackEmojiName
133
- } from "./chunk-ENY6SKIU.js";
134
- import {
135
- defaultModelId
136
- } from "./chunk-NHJU2ZAL.js";
137
- import {
138
- discoverSkills
139
- } from "./chunk-A5L27LE4.js";
140
- import {
141
- pluginCatalogRuntime
142
- } from "./chunk-WCNQJKFT.js";
143
- import {
144
- buildOAuthTokenRequest,
145
- hasRequiredOAuthScope,
146
- parseOAuthTokenResponse
147
- } from "./chunk-7GWQ5S5R.js";
148
- import {
149
- createSlackResumeActor,
150
- isUserActor,
151
- parseSlackMessageTs,
152
- parseSlackUserId
153
- } from "./chunk-N7TMSOZH.js";
154
- import {
155
- homeDir
156
- } from "./chunk-TXKUKMVO.js";
157
- import {
158
- logException,
159
- logInfo,
160
- logWarn,
161
- runBestEffort,
162
- setTags,
163
- toOptionalString,
164
- withLogContext
165
- } from "./chunk-WS7NAFTM.js";
166
-
167
- // src/chat/slack/assistant-thread/status-render.ts
168
- var DEFAULT_STATUS_CONTEXTS = {
169
- thinking: "\u2026",
170
- searching: "sources",
171
- reading: "task",
172
- reviewing: "results",
173
- drafting: "reply",
174
- running: "tasks"
175
- };
176
- function formatAssistantStatusText(verb, context) {
177
- const normalizedVerb = normalizeSlackStatusText(verb).trim().toLowerCase();
178
- const normalizedContext = normalizeSlackStatusText(context ?? "") || DEFAULT_STATUS_CONTEXTS[normalizedVerb] || "";
179
- if (!normalizedVerb) {
180
- return truncateStatusText(normalizedContext || "Working");
181
- }
182
- const displayVerb = `${normalizedVerb[0]?.toUpperCase() ?? ""}${normalizedVerb.slice(1)}`;
183
- return truncateStatusText(
184
- normalizedContext ? `${displayVerb} ${normalizedContext}` : displayVerb
185
- );
186
- }
187
- function makeAssistantStatus(verb, context) {
188
- return {
189
- text: formatAssistantStatusText(verb, context)
190
- };
191
- }
192
- function renderAssistantStatus(args) {
193
- const visible = truncateStatusText(
194
- normalizeSlackStatusText(args.status.text)
195
- );
196
- return {
197
- key: visible,
198
- visible
199
- };
200
- }
201
- function selectAssistantLoadingMessages(args) {
202
- const random = args.random ?? Math.random;
203
- const normalized = Array.from(
204
- new Set(
205
- args.messages.map((message) => truncateStatusText(normalizeSlackStatusText(message))).filter((message) => message.length > 0)
206
- )
207
- );
208
- if (normalized.length === 0) {
209
- return void 0;
210
- }
211
- const shuffled = [...normalized];
212
- for (let index = shuffled.length - 1; index > 0; index -= 1) {
213
- const otherIndex = Math.floor(random() * (index + 1));
214
- [shuffled[index], shuffled[otherIndex]] = [
215
- shuffled[otherIndex],
216
- shuffled[index]
217
- ];
218
- }
219
- return shuffled.slice(0, 10);
220
- }
221
-
222
- // src/chat/slack/assistant-thread/status-scheduler.ts
223
- var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
224
- var STATUS_MIN_VISIBLE_MS = 1200;
225
- var STATUS_ROTATION_INTERVAL_MS = 3e4;
226
- function createAssistantStatusScheduler(args) {
227
- const now = args.now ?? (() => Date.now());
228
- const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
229
- const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
230
- const random = args.random ?? Math.random;
231
- const loadingMessages = selectAssistantLoadingMessages({
232
- messages: args.loadingMessages ?? [],
233
- random
234
- });
235
- const defaultStatus = makeAssistantStatus("thinking");
236
- let active = false;
237
- let currentKey = "";
238
- let currentVisibleStatus = "";
239
- let currentLoadingMessages;
240
- let lastStatusAt = 0;
241
- let pendingStatus = null;
242
- let pendingKey = "";
243
- let pendingTimer = null;
244
- let rotationTimer = null;
245
- let inflightStatusUpdate = Promise.resolve();
246
- const enqueueStatusUpdate = (task) => {
247
- const request = inflightStatusUpdate.catch(() => void 0).then(async () => {
248
- await task();
249
- });
250
- inflightStatusUpdate = request.catch(() => void 0);
251
- return request;
252
- };
253
- const scheduleRotation = () => {
254
- if (rotationTimer) {
255
- clearTimer(rotationTimer);
256
- rotationTimer = null;
257
- }
258
- if (!active || !currentVisibleStatus) {
259
- return;
260
- }
261
- rotationTimer = setTimer(() => {
262
- rotationTimer = null;
263
- if (!active || !currentVisibleStatus) {
264
- return;
265
- }
266
- void postStatus(currentVisibleStatus, currentLoadingMessages);
267
- }, STATUS_ROTATION_INTERVAL_MS);
268
- };
269
- const getLoadingMessagesForVisibleStatus = (visible) => visible ? [visible] : void 0;
270
- const getInitialStatusText = () => {
271
- if (loadingMessages?.length) {
272
- return loadingMessages[0];
273
- }
274
- return defaultStatus.text;
275
- };
276
- const haveSameLoadingMessages = (left, right) => {
277
- if (left === right) {
278
- return true;
279
- }
280
- if (!left || !right || left.length !== right.length) {
281
- return false;
282
- }
283
- return left.every((message, index) => message === right[index]);
284
- };
285
- const postStatus = async (text, nextLoadingMessages) => {
286
- if (!text && !currentVisibleStatus) {
287
- return;
288
- }
289
- currentVisibleStatus = text;
290
- currentLoadingMessages = nextLoadingMessages;
291
- lastStatusAt = now();
292
- scheduleRotation();
293
- await enqueueStatusUpdate(async () => {
294
- await args.sendStatus(text, nextLoadingMessages);
295
- });
296
- };
297
- const postRenderedStatus = async (status) => {
298
- const presentation = renderAssistantStatus({
299
- status
300
- });
301
- const nextLoadingMessages = getLoadingMessagesForVisibleStatus(
302
- presentation.visible
303
- );
304
- currentKey = presentation.key;
305
- await postStatus(presentation.visible, nextLoadingMessages);
306
- };
307
- const clearPending = () => {
308
- if (pendingTimer) {
309
- clearTimer(pendingTimer);
310
- pendingTimer = null;
311
- }
312
- pendingStatus = null;
313
- pendingKey = "";
314
- };
315
- const flushPending = async () => {
316
- if (!active || !pendingStatus) {
317
- clearPending();
318
- return;
319
- }
320
- const next = pendingStatus;
321
- clearPending();
322
- const nextPresentation = renderAssistantStatus({
323
- status: next
324
- });
325
- if (nextPresentation.key !== currentKey) {
326
- await postRenderedStatus(next);
327
- }
328
- };
329
- return {
330
- async clear() {
331
- active = false;
332
- clearPending();
333
- if (rotationTimer) {
334
- clearTimer(rotationTimer);
335
- rotationTimer = null;
336
- }
337
- currentKey = "";
338
- await postStatus("");
339
- },
340
- update(status) {
341
- if (!active) {
342
- active = true;
343
- clearPending();
344
- if (status) {
345
- void postRenderedStatus(status);
346
- return;
347
- }
348
- currentKey = "initial";
349
- void postStatus(getInitialStatusText(), loadingMessages);
350
- return;
351
- }
352
- if (!status) {
353
- clearPending();
354
- currentKey = "initial";
355
- void postStatus(getInitialStatusText(), loadingMessages);
356
- return;
357
- }
358
- const presentation = renderAssistantStatus({
359
- status
360
- });
361
- if (!presentation.visible) {
362
- return;
363
- }
364
- if (presentation.key === currentKey || presentation.key === pendingKey) {
365
- return;
366
- }
367
- if (presentation.visible === currentVisibleStatus) {
368
- clearPending();
369
- currentKey = presentation.key;
370
- const nextLoadingMessages = getLoadingMessagesForVisibleStatus(
371
- presentation.visible
372
- );
373
- if (!haveSameLoadingMessages(currentLoadingMessages, nextLoadingMessages)) {
374
- void postStatus(presentation.visible, nextLoadingMessages);
375
- }
376
- return;
377
- }
378
- const elapsed = now() - lastStatusAt;
379
- const waitMs = Math.max(
380
- STATUS_UPDATE_DEBOUNCE_MS - elapsed,
381
- STATUS_MIN_VISIBLE_MS - elapsed,
382
- 0
383
- );
384
- if (waitMs <= 0) {
385
- clearPending();
386
- void postRenderedStatus(status);
387
- return;
388
- }
389
- pendingStatus = status;
390
- pendingKey = presentation.key;
391
- if (pendingTimer) {
392
- return;
393
- }
394
- pendingTimer = setTimer(
395
- () => {
396
- pendingTimer = null;
397
- void flushPending();
398
- },
399
- Math.max(1, waitMs)
400
- );
401
- }
402
- };
403
- }
404
-
405
- // src/chat/slack/assistant-thread/status-send.ts
406
- var SLACK_ASSISTANT_ACTIVE_STATUS = "is working on your request...";
407
- function createSlackAdapterStatusSender(args) {
408
- const adapter = args.getSlackAdapter();
409
- const boundToken = getSlackAdapterRequestToken(adapter);
410
- return async (text, loadingMessages) => {
411
- const channelId = args.channelId;
412
- const threadTs = args.threadTs;
413
- if (!channelId || !threadTs) {
414
- return;
415
- }
416
- const normalizedChannelId = normalizeSlackConversationId(channelId);
417
- if (!normalizedChannelId) {
418
- return;
419
- }
420
- const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
421
- try {
422
- await runWithBoundSlackToken(
423
- adapter,
424
- boundToken,
425
- () => adapter.setAssistantStatus(
426
- normalizedChannelId,
427
- threadTs,
428
- text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
429
- nextLoadingMessages
430
- )
431
- );
432
- } catch (error) {
433
- logAssistantStatusFailure({
434
- status: text,
435
- error,
436
- channelId,
437
- normalizedChannelId,
438
- threadTs
439
- });
440
- }
441
- };
442
- }
443
- function createSlackWebApiStatusSender(args) {
444
- const getClient = args.getSlackClient ?? getSlackClient;
445
- return async (text, loadingMessages) => {
446
- const channelId = args.channelId;
447
- const threadTs = args.threadTs;
448
- if (!channelId || !threadTs) {
449
- return;
450
- }
451
- const normalizedChannelId = normalizeSlackConversationId(channelId);
452
- if (!normalizedChannelId) {
453
- return;
454
- }
455
- const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
456
- try {
457
- await getClient().assistant.threads.setStatus({
458
- channel_id: normalizedChannelId,
459
- thread_ts: threadTs,
460
- status: text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
461
- ...nextLoadingMessages ? { loading_messages: nextLoadingMessages } : void 0
462
- });
463
- } catch (error) {
464
- logAssistantStatusFailure({
465
- status: text,
466
- error,
467
- channelId,
468
- normalizedChannelId,
469
- threadTs
470
- });
471
- }
472
- };
473
- }
474
- function getSlackAdapterRequestToken(adapter) {
475
- const token = adapter.requestContext?.getStore()?.token;
476
- if (typeof token !== "string") {
477
- return void 0;
478
- }
479
- const trimmed = token.trim();
480
- return trimmed || void 0;
481
- }
482
- async function runWithBoundSlackToken(adapter, token, task) {
483
- if (!token) {
484
- return await task();
485
- }
486
- return await adapter.withBotToken(token, task);
487
- }
488
- function logAssistantStatusFailure(args) {
489
- logWarn("assistant.status.update.failed", {
490
- "app.slack.status_text": args.status || "(clear)",
491
- "app.slack.channel_id_raw": args.channelId,
492
- "app.slack.channel_id": args.normalizedChannelId,
493
- "app.slack.thread_ts": args.threadTs,
494
- "exception.message": args.error instanceof Error ? args.error.message : String(args.error)
495
- });
496
- }
497
-
498
- // src/chat/slack/assistant-thread/status.ts
499
- function createSlackAdapterAssistantStatusSession(args) {
500
- return createAssistantStatusScheduler({
501
- sendStatus: createSlackAdapterStatusSender({
502
- channelId: args.channelId,
503
- threadTs: args.threadTs,
504
- getSlackAdapter: args.getSlackAdapter
505
- }),
506
- loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
507
- now: args.now,
508
- setTimer: args.setTimer,
509
- clearTimer: args.clearTimer,
510
- random: args.random
511
- });
512
- }
513
- function createSlackWebApiAssistantStatusSession(args) {
514
- return createAssistantStatusScheduler({
515
- sendStatus: createSlackWebApiStatusSender({
516
- channelId: args.channelId,
517
- threadTs: args.threadTs,
518
- getSlackClient: args.getSlackClient
519
- }),
520
- loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
521
- now: args.now,
522
- setTimer: args.setTimer,
523
- clearTimer: args.clearTimer,
524
- random: args.random
525
- });
526
- }
527
-
528
- // src/chat/slack/footer.ts
529
- function formatReplyAttribution(attribution) {
530
- return attribution.detail ? `${attribution.label} \xB7 ${attribution.detail}` : attribution.label;
531
- }
532
- function buildSlackReplyFooter(args) {
533
- const items = [];
534
- const conversationId = args.conversationId?.trim();
535
- if (conversationId) {
536
- const idItem = {
537
- label: "ID",
538
- value: conversationId
539
- };
540
- const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? getDashboardConversationLink(conversationId) ?? buildSentryConversationUrl(conversationId);
541
- if (conversationUrl) {
542
- idItem.url = conversationUrl;
543
- }
544
- items.push(idItem);
545
- }
546
- return items.length > 0 || args.replyAttribution ? {
547
- ...args.replyAttribution ? { attribution: args.replyAttribution } : void 0,
548
- items
549
- } : void 0;
550
- }
551
- function buildSlackReplyBlocks(text, footer) {
552
- if (!text.trim()) {
553
- return void 0;
554
- }
555
- const blocks = [
556
- {
557
- type: "markdown",
558
- text
559
- }
560
- ];
561
- if (footer && (footer.attribution || footer.items.length > 0)) {
562
- const attributionElements = footer.attribution ? [
563
- {
564
- type: "plain_text",
565
- text: formatReplyAttribution(footer.attribution)
566
- }
567
- ] : [];
568
- blocks.push({
569
- type: "context",
570
- elements: [
571
- ...attributionElements,
572
- ...footer.items.map((item) => ({
573
- type: "mrkdwn",
574
- text: item.url ? `*${escapeSlackMrkdwnText(item.label)}:* ${formatSlackLink(item.url, item.value)}` : `*${escapeSlackMrkdwnText(item.label)}:* ${escapeSlackMrkdwnText(item.value)}`
575
- }))
576
- ]
577
- });
578
- }
579
- return blocks;
580
- }
581
-
582
- // src/chat/slack/output.ts
583
- var MAX_INLINE_CHARS = 2200;
584
- var MAX_INLINE_LINES = 45;
585
- var CONTINUED_MARKER = "\n\n[Continued below]";
586
- function countSlackLines(text) {
587
- if (!text) {
588
- return 0;
589
- }
590
- return text.split("\n").length;
591
- }
592
- function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
593
- return text.length <= maxChars && countSlackLines(text) <= maxLines;
594
- }
595
- function findSplitIndex(text, maxChars) {
596
- if (text.length <= maxChars) {
597
- return text.length;
598
- }
599
- const bounded = text.slice(0, maxChars);
600
- const newlineIndex = bounded.lastIndexOf("\n");
601
- if (newlineIndex > 0) {
602
- return newlineIndex;
603
- }
604
- const spaceIndex = bounded.lastIndexOf(" ");
605
- if (spaceIndex > 0) {
606
- return spaceIndex;
607
- }
608
- return maxChars;
609
- }
610
- function splitByLineBudget(text, maxLines) {
611
- if (maxLines <= 0) {
612
- return "";
613
- }
614
- const lines = text.split("\n");
615
- if (lines.length <= maxLines) {
616
- return text;
617
- }
618
- return lines.slice(0, maxLines).join("\n");
619
- }
620
- function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
621
- return {
622
- maxChars: Math.max(1, maxChars - suffix.length),
623
- maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
624
- };
625
- }
626
- function forceSplitBudget(text, budget) {
627
- const lineCount = countSlackLines(text);
628
- return {
629
- maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
630
- maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
631
- };
632
- }
633
- function getFenceContinuation(text) {
634
- let open;
635
- for (const line of text.split("\n")) {
636
- const trimmed = line.trimStart();
637
- const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
638
- if (!openerMatch) {
639
- continue;
640
- }
641
- if (!open) {
642
- open = {
643
- fence: openerMatch[1],
644
- openerLine: trimmed
645
- };
646
- continue;
647
- }
648
- if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
649
- open = void 0;
650
- }
651
- }
652
- if (!open) {
653
- return null;
654
- }
655
- return {
656
- closeSuffix: text.endsWith("\n") ? open.fence : `
657
- ${open.fence}`,
658
- reopenPrefix: `${open.openerLine}
659
- `
660
- };
661
- }
662
- function appendSlackSuffix(text, marker) {
663
- const carryover = getFenceContinuation(text);
664
- return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
665
- }
666
- function stripTrailingContinuationMarker(text) {
667
- return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
668
- }
669
- function takeSlackContinuationChunk(text, budget) {
670
- let { prefix, rest } = takeSlackInlinePrefix(text, budget);
671
- if (!rest) {
672
- ({ prefix, rest } = takeSlackInlinePrefix(
673
- text,
674
- forceSplitBudget(text, budget)
675
- ));
676
- }
677
- let carryover = rest ? getFenceContinuation(prefix) : null;
678
- if (!carryover) {
679
- return { prefix, rest };
680
- }
681
- const carryoverBudget = reserveInlineBudgetForSuffix(
682
- `${carryover.closeSuffix}${CONTINUED_MARKER}`
683
- );
684
- ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
685
- if (!rest) {
686
- ({ prefix, rest } = takeSlackInlinePrefix(
687
- text,
688
- forceSplitBudget(text, carryoverBudget)
689
- ));
690
- }
691
- carryover = rest ? getFenceContinuation(prefix) : null;
692
- if (!carryover) {
693
- return { prefix, rest };
694
- }
695
- return {
696
- prefix,
697
- rest: `${carryover.reopenPrefix}${rest}`
698
- };
699
- }
700
- function takeSlackContinuationPrefix(text, options) {
701
- const budget = {
702
- maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
703
- maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
704
- };
705
- const { prefix, rest } = (() => {
706
- if (options?.forceSplit) {
707
- return takeSlackContinuationChunk(text, budget);
708
- }
709
- const initial = takeSlackInlinePrefix(text, budget);
710
- return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
711
- })();
712
- return {
713
- prefix,
714
- renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
715
- rest
716
- };
717
- }
718
- function takeSlackInlinePrefix(text, options) {
719
- const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
720
- const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
721
- const normalized = text.replace(/\r\n?/g, "\n");
722
- if (!normalized) {
723
- return { prefix: "", rest: "" };
724
- }
725
- if (fitsInlineBudget(normalized, maxChars, maxLines)) {
726
- return { prefix: normalized, rest: "" };
727
- }
728
- const lineBounded = splitByLineBudget(normalized, maxLines);
729
- const cutIndex = findSplitIndex(lineBounded, maxChars);
730
- const prefix = lineBounded.slice(0, cutIndex).trimEnd();
731
- if (prefix) {
732
- return {
733
- prefix,
734
- rest: normalized.slice(prefix.length).trimStart()
735
- };
736
- }
737
- const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
738
- return {
739
- prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
740
- rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
741
- };
742
- }
743
- function splitSlackReplyText(text, options) {
744
- const normalized = normalizeSlackReplyMarkdown(text);
745
- if (!normalized) {
746
- return [];
747
- }
748
- const chunks = [];
749
- const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
750
- let remaining = normalized;
751
- while (remaining) {
752
- const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
753
- if (fitsFinalChunk) {
754
- chunks.push(
755
- options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
756
- );
757
- break;
758
- }
759
- const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
760
- ...continuationBudget,
761
- forceSplit: true
762
- });
763
- chunks.push(renderedPrefix);
764
- remaining = rest;
765
- }
766
- if (chunks.length === 2) {
767
- chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
768
- }
769
- return chunks;
770
- }
771
- function getSlackContinuationBudget() {
772
- return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
773
- }
774
- function buildSlackOutputMessage(text) {
775
- const normalized = normalizeSlackReplyMarkdown(text);
776
- if (!normalized) {
777
- throw new Error(
778
- `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
779
- );
780
- }
781
- return {
782
- markdown: normalized
783
- };
784
- }
785
-
786
- // src/chat/slack/reply.ts
787
- async function sendSlackReply(args) {
788
- const chunks = splitSlackReplyText(args.text);
789
- const footer = buildSlackReplyFooter({
790
- conversationId: args.conversationId,
791
- replyAttribution: args.replyAttribution
792
- });
793
- const messageTs = [];
794
- let threadTs = args.threadTs;
795
- for (const [index, text] of chunks.entries()) {
796
- const isFinalChunk = index === chunks.length - 1;
797
- const blocks = buildSlackReplyBlocks(
798
- text,
799
- isFinalChunk ? footer : void 0
800
- );
801
- const fallbackText = isFinalChunk && args.replyAttribution ? `${text}
802
-
803
- ${escapeSlackMrkdwnText(formatReplyAttribution(args.replyAttribution))}` : text;
804
- const response = await postSlackMessage({
805
- channelId: args.channelId,
806
- threadTs,
807
- text: fallbackText,
808
- ...blocks ? { blocks } : void 0
809
- });
810
- if (response.ts) {
811
- messageTs.push(response.ts);
812
- threadTs ??= response.ts;
813
- }
814
- }
815
- return messageTs;
816
- }
817
-
818
- // src/chat/slack/errors.ts
819
- function getSlackApiErrorCode(error) {
820
- if (!error || typeof error !== "object") {
821
- return void 0;
822
- }
823
- const candidate = error;
824
- if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
825
- return candidate.data.error;
826
- }
827
- if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
828
- return candidate.code;
829
- }
830
- return void 0;
831
- }
832
- function getSlackErrorObservabilityAttributes(error) {
833
- if (!error || typeof error !== "object") {
834
- return {};
835
- }
836
- const candidate = error;
837
- const attributes = {};
838
- if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
839
- attributes["app.slack.error_code"] = candidate.code;
840
- }
841
- if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
842
- attributes["app.slack.api_error"] = candidate.data.error;
843
- }
844
- const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
845
- if (requestId) {
846
- attributes["app.slack.request_id"] = requestId;
847
- }
848
- if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) {
849
- attributes["http.response.status_code"] = candidate.statusCode;
850
- }
851
- return attributes;
852
- }
853
- var TRANSIENT_SLACK_API_ERRORS = /* @__PURE__ */ new Set([
854
- "fatal_error",
855
- "internal_error",
856
- // chat.postMessage documents both spellings.
857
- "rate_limited",
858
- "ratelimited",
859
- "request_timeout",
860
- "service_unavailable"
861
- ]);
862
- function isRetryableSlackPostError(error) {
863
- if (!error || typeof error !== "object") {
864
- return true;
865
- }
866
- const candidate = error;
867
- if (typeof candidate.statusCode === "number") {
868
- if (candidate.statusCode === 429 || candidate.statusCode >= 500) {
869
- return true;
870
- }
871
- if (candidate.statusCode >= 400) {
872
- return false;
873
- }
874
- }
875
- const apiError = error instanceof SlackActionError ? error.apiError : typeof candidate.data?.error === "string" ? candidate.data.error : void 0;
876
- if (apiError) {
877
- return TRANSIENT_SLACK_API_ERRORS.has(apiError);
878
- }
879
- if (error instanceof SlackActionError) {
880
- return error.code === "rate_limited" || error.code === "internal_error";
881
- }
882
- return true;
883
- }
884
- function isSlackTitlePermissionError(error) {
885
- const code = getSlackApiErrorCode(error);
886
- return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
887
- }
888
-
889
- // src/chat/runtime/thread-context.ts
890
- function escapeRegExp(value) {
891
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
892
- }
893
- function readOptionalStringField(value, key) {
894
- if (!(key in value)) {
895
- return void 0;
896
- }
897
- return toOptionalString(value[key]);
898
- }
899
- function stripLeadingBotMention(text, options = {}) {
900
- if (!text.trim()) return text;
901
- let next = text;
902
- if (options.stripLeadingSlackMentionToken) {
903
- if (options.botUserId) {
904
- const botUserId = escapeRegExp(options.botUserId);
905
- const mentionByBotUserIdRe = new RegExp(
906
- `^\\s*(?:<@${botUserId}(?:\\|[^>]+)?>|@${botUserId})[\\s,:-]*`,
907
- "i"
908
- );
909
- next = next.replace(mentionByBotUserIdRe, "").trim();
910
- } else {
911
- next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
912
- }
913
- }
914
- const mentionByNameRe = new RegExp(
915
- `^\\s*@${escapeRegExp(botConfig.userName)}\\b[\\s,:-]*`,
916
- "i"
917
- );
918
- next = next.replace(mentionByNameRe, "").trim();
919
- const mentionByLabeledEntityRe = new RegExp(
920
- `^\\s*<@[^>|]+\\|${escapeRegExp(botConfig.userName)}>[\\s,:-]*`,
921
- "i"
922
- );
923
- next = next.replace(mentionByLabeledEntityRe, "").trim();
924
- return next;
925
- }
926
- function getThreadId(thread, _message) {
927
- return toOptionalString(thread.id);
928
- }
929
- function getRunId(thread, message) {
930
- return readOptionalStringField(thread, "runId") ?? readOptionalStringField(message, "runId");
931
- }
932
- function getChannelId(thread, message) {
933
- return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
934
- }
935
- function getThreadTs(threadId) {
936
- return parseSlackThreadId(threadId)?.threadTs;
937
- }
938
- function getAssistantThreadContext(message) {
939
- const rawContext = readSlackRawMessageContext(message);
940
- const channelId = rawContext?.channelId;
941
- if (channelId) {
942
- const threadTs = isDmChannel(channelId) ? rawContext.threadTs : rawContext.threadTs ?? rawContext.messageTs;
943
- if (threadTs) {
944
- return { channelId, threadTs };
945
- }
946
- }
947
- const parsedThreadId = parseSlackThreadId(
948
- readOptionalStringField(message, "threadId")
949
- );
950
- if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
951
- return void 0;
952
- }
953
- return parsedThreadId;
954
- }
955
- function getMessageTs(message) {
956
- const directTs = readOptionalStringField(message, "ts");
957
- const parsedDirectTs = parseSlackMessageTs(directTs);
958
- if (parsedDirectTs) {
959
- return parsedDirectTs;
960
- }
961
- const rawContext = readSlackRawMessageContext(message);
962
- if (!rawContext) {
963
- return void 0;
964
- }
965
- const candidates = [rawContext.messageTs, rawContext.nestedMessageTs];
966
- for (const candidate of candidates) {
967
- const ts = parseSlackMessageTs(candidate);
968
- if (ts) {
969
- return ts;
970
- }
971
- }
972
- return void 0;
973
- }
974
-
975
- // src/chat/providers/slack/processing-reaction.ts
976
- var noProcessingReaction = {
977
- complete: async () => void 0,
978
- keep: () => void 0,
979
- stop: async () => void 0
980
- };
981
- function isProcessingReactionEmoji(value) {
982
- return typeof value === "string" && normalizeSlackEmojiName(value) === getChatConfig().slack.processingReactionEmoji;
983
- }
984
- function shouldKeepProcessingReactionForToolInvocation(input) {
985
- return input.toolName === "addReaction" && isProcessingReactionEmoji(input.params.emoji);
986
- }
987
- async function startProcessingReaction(args) {
988
- if (args.message.author.isMe) {
989
- return noProcessingReaction;
990
- }
991
- const channelId = getChannelId(args.thread, args.message);
992
- const messageTs = getMessageTs(args.message);
993
- if (!channelId || !messageTs) {
994
- return noProcessingReaction;
995
- }
996
- return startProcessingReactionForMessage({
997
- channelId,
998
- timestamp: messageTs
999
- });
1000
- }
1001
- async function startProcessingReactionForMessage(args) {
1002
- try {
1003
- await addReactionToMessage({
1004
- channelId: args.channelId,
1005
- timestamp: args.timestamp,
1006
- emoji: getChatConfig().slack.processingReactionEmoji
1007
- });
1008
- } catch (error) {
1009
- logException(error, "slack.processing.reaction_add.failed", {
1010
- "app.slack.action": "reactions.add",
1011
- "messaging.message.id": args.timestamp,
1012
- ...getSlackErrorObservabilityAttributes(error)
1013
- });
1014
- return noProcessingReaction;
1015
- }
1016
- let shouldRemove = true;
1017
- const removeProcessingReaction = async () => {
1018
- if (!shouldRemove) {
1019
- return false;
1020
- }
1021
- try {
1022
- await removeReactionFromMessage({
1023
- channelId: args.channelId,
1024
- timestamp: args.timestamp,
1025
- emoji: getChatConfig().slack.processingReactionEmoji
1026
- });
1027
- return true;
1028
- } catch (error) {
1029
- logException(error, "slack.processing.reaction_remove.failed", {
1030
- "app.slack.action": "reactions.remove",
1031
- "messaging.message.id": args.timestamp,
1032
- ...getSlackErrorObservabilityAttributes(error)
1033
- });
1034
- return false;
1035
- }
1036
- };
1037
- return {
1038
- complete: async () => {
1039
- const shouldAddCompleted = shouldRemove;
1040
- await removeProcessingReaction();
1041
- if (!shouldAddCompleted) {
1042
- return;
1043
- }
1044
- try {
1045
- await addReactionToMessage({
1046
- channelId: args.channelId,
1047
- timestamp: args.timestamp,
1048
- emoji: getChatConfig().slack.completedReactionEmoji
1049
- });
1050
- } catch (error) {
1051
- logException(error, "slack.processing.reaction_complete.failed", {
1052
- "app.slack.action": "reactions.add",
1053
- "messaging.message.id": args.timestamp,
1054
- ...getSlackErrorObservabilityAttributes(error)
1055
- });
1056
- }
1057
- },
1058
- keep: () => {
1059
- shouldRemove = false;
1060
- },
1061
- stop: async () => {
1062
- await removeProcessingReaction();
1063
- }
1064
- };
1065
- }
1066
-
1067
- // src/chat/services/auth-pause-response.ts
1068
- var MAX_AUTH_REQUEST_LENGTH = 200;
1069
- function formatAuthRequest(requestText) {
1070
- const normalized = requestText.replace(/\s+/g, " ").trim();
1071
- if (!normalized) {
1072
- return void 0;
1073
- }
1074
- const bounded = normalized.length > MAX_AUTH_REQUEST_LENGTH ? `${normalized.slice(0, MAX_AUTH_REQUEST_LENGTH - 1).trimEnd()}\u2026` : normalized;
1075
- return bounded.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1076
- }
1077
- function buildAuthPauseResponse(slackUserId, providerDisplayName, requestText) {
1078
- const mention = slackUserId ? `<@${slackUserId}> ` : "";
1079
- const request = requestText ? formatAuthRequest(requestText) : void 0;
1080
- if (!request) {
1081
- return `${mention}I'll need you to authorize ${providerDisplayName}. I sent you a link.`;
1082
- }
1083
- return `${mention}I need access to ${providerDisplayName} to continue.
1084
-
1085
- *Why:* ${request}
1086
-
1087
- I sent you a link.`;
1088
- }
1089
-
1090
- // src/chat/providers/slack/resume.ts
1091
- function resolveReplyTimeoutMs() {
1092
- const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
1093
- if (!raw) {
1094
- return void 0;
1095
- }
1096
- const parsed = Number.parseInt(raw, 10);
1097
- return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
1098
- }
1099
- async function postSlackMessageBestEffort(channelId, threadTs, text, conversationId, replyAttribution) {
1100
- try {
1101
- if (conversationId) {
1102
- await sendSlackReply({
1103
- channelId,
1104
- conversationId,
1105
- replyAttribution,
1106
- text,
1107
- threadTs
1108
- });
1109
- return;
1110
- }
1111
- await postSlackMessage({ channelId, threadTs, text });
1112
- } catch {
1113
- }
1114
- }
1115
- function createReadOnlyConfigService(rawValues) {
1116
- const values = Object.fromEntries(
1117
- Object.entries(rawValues).map(([key, value]) => [
1118
- key,
1119
- configValueSchema.parse(value)
1120
- ])
1121
- );
1122
- const entries = Object.entries(values).map(([key, value]) => ({
1123
- key,
1124
- value,
1125
- scope: "location",
1126
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1127
- }));
1128
- return {
1129
- get: async (key) => entries.find((entry) => entry.key === key),
1130
- set: async () => {
1131
- throw new Error("Configuration is read-only for a resumed Run");
1132
- },
1133
- unset: async () => false,
1134
- list: async ({ prefix } = {}) => entries.filter((entry) => !prefix || entry.key.startsWith(prefix)),
1135
- resolve: async (key) => values[key],
1136
- resolveValues: async ({ keys, prefix } = {}) => {
1137
- const filtered = {};
1138
- for (const [key, value] of Object.entries(values)) {
1139
- if (prefix && !key.startsWith(prefix)) continue;
1140
- if (keys && !keys.includes(key)) continue;
1141
- filtered[key] = value;
1142
- }
1143
- return filtered;
1144
- }
1145
- };
1146
- }
1147
- var ResumeTurnBusyError = class extends Error {
1148
- constructor(lockKey) {
1149
- super(`Another worker is already resuming Slack Turn "${lockKey}"`);
1150
- this.name = "ResumeTurnBusyError";
1151
- }
1152
- };
1153
- function getDefaultLockKey(channelId, threadTs) {
1154
- return threadTs ? `slack:${channelId}:${threadTs}` : `slack:${channelId}`;
1155
- }
1156
- function getResumeLogContext(args, lockKey) {
1157
- const actor = args.run?.actor;
1158
- return {
1159
- conversationId: args.conversationId,
1160
- messageConversationId: lockKey,
1161
- userId: isUserActor(actor) ? actor.userId : void 0,
1162
- userName: isUserActor(actor) ? actor.userName : void 0,
1163
- destinationName: args.channelId,
1164
- assistantUserName: botConfig.userName,
1165
- modelId: defaultModelId(botConfig)
1166
- };
1167
- }
1168
- async function postResumeFailureReply(args) {
1169
- await postSlackMessage({
1170
- channelId: args.channelId,
1171
- threadTs: args.threadTs,
1172
- text: buildTurnErrorResponse(args.error, args.eventId)
1173
- });
1174
- }
1175
- async function handleResumeFailure(args) {
1176
- const turnLifecycle = getTurnLifecycle();
1177
- const capturedEventId = logException(args.error, args.eventName);
1178
- const eventId = requireTurnFailureEventId(capturedEventId, args.eventName);
1179
- let failureStatePersistError;
1180
- try {
1181
- await args.resume.onFailure?.(args.error);
1182
- } catch (persistError) {
1183
- const persistEventId = logException(
1184
- persistError,
1185
- "slack.resume.failure_state_persist.failed",
1186
- { "app.error.original_event_id": eventId }
1187
- );
1188
- try {
1189
- await turnLifecycle.fail({
1190
- conversationId: args.resume.conversationId,
1191
- turnId: args.resume.turnId,
1192
- createdAtMs: Date.now(),
1193
- failureCode: "persistence_failed",
1194
- ...persistEventId ? { eventId: persistEventId } : void 0
1195
- });
1196
- } catch (lifecycleError) {
1197
- logException(
1198
- lifecycleError,
1199
- "slack.resume.failure_lifecycle_persist.failed",
1200
- { "app.error.original_event_id": eventId }
1201
- );
1202
- }
1203
- failureStatePersistError = persistError;
1204
- }
1205
- try {
1206
- await postResumeFailureReply({
1207
- channelId: args.resume.channelId,
1208
- threadTs: args.resume.threadTs,
1209
- eventId,
1210
- error: args.error
1211
- });
1212
- } catch (deliveryError) {
1213
- const deliveryEventId = logException(
1214
- deliveryError,
1215
- "slack.resume.failure_delivery.failed",
1216
- { "app.error.original_event_id": eventId }
1217
- );
1218
- try {
1219
- await turnLifecycle.fail({
1220
- conversationId: args.resume.conversationId,
1221
- turnId: args.resume.turnId,
1222
- createdAtMs: Date.now(),
1223
- failureCode: "delivery_failed",
1224
- ...deliveryEventId ? { eventId: deliveryEventId } : void 0
1225
- });
1226
- } catch (lifecycleError) {
1227
- logException(
1228
- lifecycleError,
1229
- "slack.resume.failure_lifecycle_persist.failed",
1230
- { "app.error.original_event_id": eventId }
1231
- );
1232
- }
1233
- throw deliveryError;
1234
- }
1235
- if (failureStatePersistError) {
1236
- throw failureStatePersistError;
1237
- }
1238
- await turnLifecycle.fail({
1239
- conversationId: args.resume.conversationId,
1240
- turnId: args.resume.turnId,
1241
- createdAtMs: Date.now(),
1242
- failureCode: args.failureCode,
1243
- eventId
1244
- });
1245
- }
1246
- function buildResumedRun(args, statusSession, delivery) {
1247
- const savedRun = args.run;
1248
- if (!savedRun) {
1249
- throw new TypeError("Slack resume requires a saved Run");
1250
- }
1251
- if (!savedRun.source) {
1252
- throw new TypeError("Slack resume requires a Run Source");
1253
- }
1254
- const source = savedRun.source;
1255
- if (savedRun.destination.platform !== "slack") {
1256
- throw new TypeError("Slack resume requires a Slack Destination");
1257
- }
1258
- const requestDeadline = getTurnRequestDeadline();
1259
- const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
1260
- const persistedLocationConfiguration = savedRun.environment?.locationConfiguration ?? (savedRun.environment?.configuration ? createReadOnlyConfigService(savedRun.environment.configuration) : void 0);
1261
- const priorOnEvent = savedRun.onEvent;
1262
- return {
1263
- ...savedRun,
1264
- conversationId: args.conversationId,
1265
- turnId: args.turnId,
1266
- instruction: {
1267
- ...savedRun.instruction ?? { text: args.messageText },
1268
- text: args.messageText
1269
- },
1270
- source: source.kind === "slack" ? {
1271
- ...source,
1272
- channelId: args.channelId,
1273
- ...args.threadTs ? { threadTs: args.threadTs } : void 0
1274
- } : source,
1275
- deadlineAtMs: savedRun.deadlineAtMs ?? requestDeadline?.deadlineAtMs,
1276
- environment: {
1277
- ...savedRun.environment,
1278
- locationConfiguration: persistedLocationConfiguration
1279
- },
1280
- onEvent: async (event) => {
1281
- if (event.type === "status") {
1282
- statusSession.update({ text: event.text });
1283
- }
1284
- await priorOnEvent?.(event);
1285
- },
1286
- delivery,
1287
- durability: {
1288
- ...savedRun.durability,
1289
- onSandboxRefChanged: async (sandboxRef) => {
1290
- await persistThreadStateById(threadId, {
1291
- sandboxRef
1292
- });
1293
- await savedRun.durability?.onSandboxRefChanged?.(sandboxRef);
1294
- }
1295
- }
1296
- };
1297
- }
1298
- async function resumeSlackTurn(args) {
1299
- const lockKey = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
1300
- return withLogContext(
1301
- getResumeLogContext(args, lockKey),
1302
- () => resumeSlackTurnInContext(args)
1303
- );
1304
- }
1305
- async function resumeSlackTurnInContext(args) {
1306
- const stateAdapter = getStateAdapter();
1307
- await stateAdapter.connect();
1308
- const lockKey = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
1309
- const lock = args.ownsConversationLease ? void 0 : await acquireActiveLock(stateAdapter, lockKey);
1310
- if (!args.ownsConversationLease && !lock) {
1311
- throw new ResumeTurnBusyError(lockKey);
1312
- }
1313
- const status = createSlackWebApiAssistantStatusSession({
1314
- channelId: args.channelId,
1315
- threadTs: args.threadTs
1316
- });
1317
- let processingReaction;
1318
- let deferredAuthInfo;
1319
- let deferredPauseHandler;
1320
- let deferredFailureHandler;
1321
- let runResultHandled = false;
1322
- let assistantMessageDelivered = false;
1323
- let shouldScheduleCompletedPluginTasks = false;
1324
- let postDeliveryCommitError;
1325
- const turnLifecycle = getTurnLifecycle();
1326
- let failureCode = "agent_run_failed";
1327
- let runArgs = args;
1328
- try {
1329
- const preparedArgs = await args.beforeStart?.();
1330
- if (preparedArgs === false) {
1331
- return false;
1332
- }
1333
- if (preparedArgs) {
1334
- runArgs = { ...args, ...preparedArgs };
1335
- }
1336
- setTags(getResumeLogContext(runArgs, lockKey));
1337
- const savedRun = runArgs.run;
1338
- if (!savedRun) {
1339
- throw new Error("Resumed Turn requires a saved Run");
1340
- }
1341
- const credentialContext = savedRun.credentialContext;
1342
- if (!credentialContext) {
1343
- throw new Error("Resumed Run requires a credential context");
1344
- }
1345
- const routingActor = savedRun.actor;
1346
- let resumeActor;
1347
- if ("type" in credentialContext.actor) {
1348
- if (!isUserActor(routingActor) || credentialContext.actor.userId !== routingActor.userId) {
1349
- throw new Error("Resumed Run credential actor must match its Actor");
1350
- }
1351
- resumeActor = routingActor;
1352
- } else {
1353
- if (routingActor && (routingActor.platform !== "system" || routingActor.name !== credentialContext.actor.name)) {
1354
- throw new Error(
1355
- "Resumed Run system credential actor must match its Actor"
1356
- );
1357
- }
1358
- resumeActor = credentialContext.actor;
1359
- }
1360
- if (runArgs.messageTs) {
1361
- processingReaction = await startProcessingReactionForMessage({
1362
- channelId: runArgs.channelId,
1363
- timestamp: runArgs.messageTs
1364
- });
1365
- }
1366
- if (runArgs.initialText) {
1367
- await postSlackMessageBestEffort(
1368
- runArgs.channelId,
1369
- runArgs.threadTs,
1370
- runArgs.initialText
1371
- );
1372
- }
1373
- status.update(runArgs.initialStatus);
1374
- const conversationId = runArgs.conversationId;
1375
- const visibility = (await getConversationStore().get({ conversationId }))?.visibility;
1376
- const visibleConversationId = lockKey;
1377
- const sessionId = runArgs.turnId;
1378
- let deliveryConversation;
1379
- let acceptedDeliveryId;
1380
- const getDeliveryConversation = async () => {
1381
- if (deliveryConversation) {
1382
- const userMessageId2 = getTurnUserMessage(
1383
- deliveryConversation,
1384
- sessionId
1385
- )?.id;
1386
- return {
1387
- conversation: deliveryConversation,
1388
- sessionId,
1389
- ...userMessageId2 ? { userMessageId: userMessageId2 } : void 0
1390
- };
1391
- }
1392
- const persistedState = await getPersistedThreadState(
1393
- visibleConversationId
1394
- );
1395
- const conversation = coerceThreadConversationState(persistedState);
1396
- await hydrateConversationMessages({
1397
- conversation,
1398
- conversationId
1399
- });
1400
- deliveryConversation = conversation;
1401
- const userMessageId = getTurnUserMessage(conversation, sessionId)?.id;
1402
- return {
1403
- conversation,
1404
- sessionId,
1405
- ...userMessageId ? { userMessageId } : void 0
1406
- };
1407
- };
1408
- const deliverAssistantMessage = async (reply) => {
1409
- const message = typeof reply === "string" ? void 0 : reply;
1410
- const text = typeof reply === "string" ? reply : getAssistantReplyText(reply);
1411
- if (!text?.trim()) {
1412
- return;
1413
- }
1414
- failureCode = "delivery_failed";
1415
- const deliveryState2 = await getDeliveryConversation();
1416
- let slackMessageTs = [];
1417
- try {
1418
- slackMessageTs = await sendSlackReply({
1419
- channelId: runArgs.channelId,
1420
- conversationId: runArgs.conversationId,
1421
- replyAttribution: runArgs.run?.dispatch?.replyAttribution,
1422
- text,
1423
- threadTs: runArgs.threadTs
1424
- });
1425
- } catch (error) {
1426
- if (isRetryableSlackPostError(error)) {
1427
- throw new RetryableDeliveryError(error);
1428
- }
1429
- throw error;
1430
- }
1431
- const messageTs = slackMessageTs.at(-1);
1432
- assistantMessageDelivered = true;
1433
- acceptedDeliveryId = messageTs;
1434
- const recordedMessageId = recordDeliveredAssistantMessage({
1435
- conversation: deliveryState2.conversation,
1436
- sessionId: deliveryState2.sessionId,
1437
- source: "slack",
1438
- text,
1439
- userMessageId: deliveryState2.userMessageId
1440
- });
1441
- if (messageTs) {
1442
- markConversationMessage(deliveryState2.conversation, recordedMessageId, {
1443
- slackTs: messageTs
1444
- });
1445
- }
1446
- try {
1447
- const destination = runArgs.run?.destination;
1448
- const providerConversationIds = runArgs.threadTs ? [runArgs.threadTs] : slackMessageTs;
1449
- await persistWithRetry(
1450
- () => commitAcceptedReply({
1451
- ...message ? { agentMessage: message } : void 0,
1452
- conversation: deliveryState2.conversation,
1453
- conversationMessageId: recordedMessageId,
1454
- conversationId,
1455
- ...runArgs.run?.dispatch && destination?.platform === "slack" && providerConversationIds.length > 0 ? {
1456
- providerConversationBindings: providerConversationIds.map(
1457
- (providerConversationId) => ({
1458
- provider: "slack",
1459
- providerDestinationId: destination.channelId,
1460
- providerTenantId: destination.teamId,
1461
- providerConversationId
1462
- })
1463
- )
1464
- } : void 0
1465
- })
1466
- );
1467
- } catch (error) {
1468
- logException(
1469
- new Error("Accepted assistant message persistence failed"),
1470
- "slack.resume.assistant_message_post_delivery_persist.failed",
1471
- { "error.type": error instanceof Error ? error.name : typeof error }
1472
- );
1473
- }
1474
- const dispatchId = savedRun.dispatch?.id;
1475
- if (messageTs && dispatchId) {
1476
- try {
1477
- await persistWithRetry(
1478
- () => recordTurnSummary({
1479
- conversationId: runArgs.conversationId,
1480
- destination: savedRun.destination,
1481
- destinationVisibility: visibility,
1482
- dispatchId,
1483
- resultMessageId: messageTs,
1484
- turnId: runArgs.turnId,
1485
- sliceId: runArgs.sliceId ?? 1,
1486
- source: savedRun.source,
1487
- state: "running",
1488
- surface: savedRun.surface ?? "slack"
1489
- })
1490
- );
1491
- } catch (error) {
1492
- logException(error, "agent.turn.delivery_receipt_persist.failed");
1493
- }
1494
- }
1495
- failureCode = "agent_run_failed";
1496
- };
1497
- const deliveryState = await getDeliveryConversation();
1498
- assistantMessageDelivered = turnHasReply(
1499
- deliveryState.conversation,
1500
- sessionId
1501
- );
1502
- const run = buildResumedRun(runArgs, status, deliverAssistantMessage);
1503
- if (runArgs.inputMessageIds?.length) {
1504
- await turnLifecycle.start({
1505
- conversationId: runArgs.conversationId,
1506
- turnId: runArgs.turnId,
1507
- createdAtMs: Date.now(),
1508
- inputMessageIds: [...new Set(runArgs.inputMessageIds)],
1509
- surface: "slack"
1510
- });
1511
- }
1512
- const replyTimeoutMs = resolveReplyTimeoutMs();
1513
- const outcome = await runArgs.executeTurn(
1514
- run,
1515
- async (result) => {
1516
- const finalized = finalizeFailedTurnReplyWithEvent({
1517
- reply: result,
1518
- logException
1519
- });
1520
- const reply = finalized.reply;
1521
- const dispatchErrorMessage = run.dispatch && reply.diagnostics.outcome !== "success" ? reply.diagnostics.errorMessage ?? `Agent turn ended with ${reply.diagnostics.outcome}.` : void 0;
1522
- if (reply.diagnostics.outcome !== "success") {
1523
- await deliverAssistantMessage(reply.text);
1524
- }
1525
- runResultHandled = true;
1526
- await status.clear();
1527
- failureCode = "persistence_failed";
1528
- if (reply.piMessages?.length) {
1529
- await saveTurnCheckpoint({
1530
- mode: "completed",
1531
- conversationId: runArgs.conversationId,
1532
- turnId: runArgs.turnId,
1533
- messages: reply.piMessages,
1534
- durationMs: reply.diagnostics.durationMs,
1535
- usage: reply.diagnostics.usage,
1536
- destination: run.destination,
1537
- destinationVisibility: visibility,
1538
- dispatchId: run.dispatch?.id,
1539
- dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
1540
- ...dispatchErrorMessage ? { errorMessage: dispatchErrorMessage } : void 0,
1541
- ...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : void 0,
1542
- source: run.source,
1543
- actor: resumeActor,
1544
- surface: run.surface ?? "slack",
1545
- sliceId: runArgs.sliceId
1546
- });
1547
- } else if (run.dispatch?.id) {
1548
- await recordTurnSummary({
1549
- conversationId: runArgs.conversationId,
1550
- destination: run.destination,
1551
- destinationVisibility: visibility,
1552
- dispatchId: run.dispatch?.id,
1553
- dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
1554
- ...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : void 0,
1555
- turnId: runArgs.turnId,
1556
- sliceId: runArgs.sliceId ?? 1,
1557
- source: run.source,
1558
- state: reply.diagnostics.outcome === "success" ? "completed" : "failed",
1559
- surface: run.surface ?? "slack"
1560
- });
1561
- }
1562
- await runArgs.commitResult?.(reply);
1563
- if (reply.diagnostics.outcome === "success") {
1564
- shouldScheduleCompletedPluginTasks = true;
1565
- return {
1566
- outcome: assistantMessageDelivered ? "success" : "no_reply"
1567
- };
1568
- }
1569
- return {
1570
- outcome: "failed",
1571
- failureCode: "model_execution_failed",
1572
- ...finalized.eventId ? { eventId: finalized.eventId } : void 0,
1573
- ...finalized.failureReason ? { failureReason: finalized.failureReason } : void 0
1574
- };
1575
- },
1576
- replyTimeoutMs
1577
- );
1578
- if (outcome.status !== "completed") {
1579
- await status.clear();
1580
- const onAuthPause = runArgs.onAuthPause;
1581
- const onSuspend = runArgs.onSuspend;
1582
- if (outcome.status === "awaiting_auth" && onAuthPause) {
1583
- deferredAuthInfo = {
1584
- providerDisplayName: outcome.providerDisplayName,
1585
- actorId: isUserActor(resumeActor) ? resumeActor.userId : void 0,
1586
- ...outcome.requestText ? { requestText: outcome.requestText } : void 0
1587
- };
1588
- deferredPauseHandler = async () => {
1589
- await onAuthPause({
1590
- providerDisplayName: outcome.providerDisplayName
1591
- });
1592
- };
1593
- } else if (outcome.status === "suspended") {
1594
- if (runArgs.ownsConversationLease && (outcome.reason === "timeout" || savedRun.durability?.shouldYield?.())) {
1595
- throw new CooperativeTurnYieldError();
1596
- }
1597
- if (onSuspend) {
1598
- deferredPauseHandler = async () => {
1599
- await onSuspend(outcome.resumeVersion);
1600
- };
1601
- } else {
1602
- deferredFailureHandler = async () => {
1603
- await handleResumeFailure({
1604
- error: new Error("Resumed Run ended suspended without onSuspend"),
1605
- eventName: "slack.resume.turn.failed",
1606
- failureCode: "agent_run_failed",
1607
- resume: runArgs
1608
- });
1609
- };
1610
- }
1611
- } else {
1612
- deferredFailureHandler = async () => {
1613
- await handleResumeFailure({
1614
- error: new Error(
1615
- `Resumed Run ended ${outcome.status} without its required callback`
1616
- ),
1617
- eventName: "slack.resume.turn.failed",
1618
- failureCode: "agent_run_failed",
1619
- resume: runArgs
1620
- });
1621
- };
1622
- }
1623
- }
1624
- if (shouldScheduleCompletedPluginTasks) {
1625
- try {
1626
- const params = {
1627
- conversationId: runArgs.conversationId,
1628
- sessionId: runArgs.turnId
1629
- };
1630
- await (runArgs.scheduleSessionCompletedPluginTasks ?? scheduleSessionCompletedPluginTasks)(params);
1631
- } catch (scheduleError) {
1632
- logException(
1633
- scheduleError,
1634
- "plugin.session.completed_task_schedule.failed"
1635
- );
1636
- }
1637
- }
1638
- } catch (error) {
1639
- await status.clear();
1640
- const runError = error instanceof AgentRunError ? error.cause : error;
1641
- if (isCooperativeTurnYieldError(runError)) {
1642
- if (lock) {
1643
- await stateAdapter.releaseLock(lock);
1644
- }
1645
- await processingReaction?.stop();
1646
- throw runError;
1647
- }
1648
- if (runResultHandled) {
1649
- postDeliveryCommitError = error;
1650
- try {
1651
- await runArgs.onPostDeliveryCommitFailure?.(error);
1652
- } catch (terminalizeError) {
1653
- logException(
1654
- terminalizeError,
1655
- "slack.resume.post_delivery_terminalize.failed"
1656
- );
1657
- }
1658
- } else {
1659
- deferredFailureHandler = async () => {
1660
- await handleResumeFailure({
1661
- error: runError,
1662
- eventName: "slack.resume.turn.failed",
1663
- failureCode,
1664
- resume: runArgs
1665
- });
1666
- };
1667
- }
1668
- } finally {
1669
- if (runResultHandled) {
1670
- await processingReaction?.complete();
1671
- } else {
1672
- await processingReaction?.stop();
1673
- }
1674
- if (lock) {
1675
- await stateAdapter.releaseLock(lock);
1676
- }
1677
- }
1678
- if (postDeliveryCommitError) {
1679
- const eventId = logException(
1680
- postDeliveryCommitError,
1681
- "slack.resume.success_handler.failed"
1682
- );
1683
- await turnLifecycle.fail({
1684
- conversationId: runArgs.conversationId,
1685
- turnId: runArgs.turnId,
1686
- createdAtMs: Date.now(),
1687
- failureCode: "persistence_failed",
1688
- ...eventId ? { eventId } : void 0
1689
- });
1690
- throw postDeliveryCommitError;
1691
- }
1692
- if (deferredPauseHandler) {
1693
- try {
1694
- await deferredPauseHandler();
1695
- if (deferredAuthInfo) {
1696
- await postSlackMessageBestEffort(
1697
- runArgs.channelId,
1698
- runArgs.threadTs,
1699
- buildAuthPauseResponse(
1700
- deferredAuthInfo.actorId,
1701
- deferredAuthInfo.providerDisplayName,
1702
- deferredAuthInfo.requestText
1703
- ),
1704
- runArgs.conversationId,
1705
- runArgs.run?.dispatch?.replyAttribution
1706
- );
1707
- }
1708
- return true;
1709
- } catch (pauseError) {
1710
- await handleResumeFailure({
1711
- error: pauseError,
1712
- eventName: "slack.resume.pause_handler.failed",
1713
- failureCode: "persistence_failed",
1714
- resume: runArgs
1715
- });
1716
- return true;
1717
- }
1718
- }
1719
- if (deferredFailureHandler) {
1720
- await deferredFailureHandler();
1721
- }
1722
- return true;
1723
- }
1724
-
1725
- // src/chat/task-execution/turn-wake.ts
1726
- async function getPausedTurnRequest(args) {
1727
- const checkpoint = await loadTurnCheckpoint({
1728
- conversationId: args.conversationId,
1729
- turnId: args.turnId
1730
- });
1731
- const turn = checkpoint.record;
1732
- if (!turn || turn.state !== "paused" || turn.resumeReason !== "timeout" && turn.resumeReason !== "yield" && turn.resumeReason !== "retry" || turn.resumeReason === "timeout" && turn.sliceId < 2) {
1733
- return void 0;
1734
- }
1735
- let routing;
1736
- try {
1737
- routing = await resolveTurnSessionRouting({
1738
- conversationId: args.conversationId,
1739
- conversationStore: args.conversationStore
1740
- });
1741
- } catch (error) {
1742
- await failTurnRecord({
1743
- conversationId: args.conversationId,
1744
- expectedVersion: turn.version,
1745
- turnId: args.turnId,
1746
- errorMessage: error instanceof Error ? error.message : String(error)
1747
- });
1748
- return void 0;
1749
- }
1750
- return {
1751
- conversationId: args.conversationId,
1752
- destination: routing.destination,
1753
- turnId: args.turnId,
1754
- expectedVersion: turn.version
1755
- };
1756
- }
1757
- async function wakePausedTurn(request, options = {}) {
1758
- const nowMs = options.nowMs ?? Date.now();
1759
- await requestConversationWork({
1760
- conversationId: request.conversationId,
1761
- destination: request.destination,
1762
- nowMs,
1763
- state: options.state
1764
- });
1765
- const queue = options.queue ?? getVercelConversationWorkQueue();
1766
- await ensureConversationWake({
1767
- conversationId: request.conversationId,
1768
- idempotencyKey: [
1769
- "agent-continue",
1770
- request.conversationId,
1771
- request.turnId,
1772
- request.expectedVersion,
1773
- nowMs
1774
- ].join(":"),
1775
- nowMs,
1776
- queue,
1777
- state: options.state
1778
- });
1779
- }
1780
- function createPausedTurns(options = {}) {
1781
- return {
1782
- get: (args) => getPausedTurnRequest({
1783
- ...args,
1784
- conversationStore: options.conversationStore
1785
- }),
1786
- wake: (request) => wakePausedTurn(request, options)
1787
- };
1788
- }
1789
-
1790
- // src/handlers/oauth-html.ts
1791
- function htmlCallbackResponse(title, message, status, options = {}) {
1792
- const footerMessage = options.footerMessage ?? `You can close this tab and return to ${botConfig.userName}.`;
1793
- const html = `<!DOCTYPE html>
1794
- <html>
1795
- <head><title>${title}</title></head>
1796
- <body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
1797
- <div style="text-align: center; max-width: 480px;">
1798
- <h1>${title}</h1>
1799
- <p>${message}</p>
1800
- <p style="margin-top: 2rem; color: #666; font-size: 0.9em;">${footerMessage}</p>
1801
- </div>
1802
- </body>
1803
- </html>`;
1804
- return new Response(html, {
1805
- status,
1806
- headers: {
1807
- "Cache-Control": "no-store",
1808
- "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'",
1809
- "Content-Type": "text/html; charset=utf-8",
1810
- "Referrer-Policy": "no-referrer",
1811
- "X-Content-Type-Options": "nosniff"
1812
- }
1813
- });
1814
- }
1815
-
1816
- // src/handlers/mcp-oauth-callback.ts
1817
- function callbackPages(botName) {
1818
- return {
1819
- missing_state: {
1820
- title: "Authorization failed",
1821
- message: "Missing state parameter.",
1822
- status: 400
1823
- },
1824
- provider_error: {
1825
- title: "Authorization failed",
1826
- message: "The provider returned an authorization error.",
1827
- status: 400
1828
- },
1829
- missing_code: {
1830
- title: "Authorization failed",
1831
- message: "Missing code parameter.",
1832
- status: 400
1833
- },
1834
- expired: {
1835
- title: "Authorization expired",
1836
- message: `This authorization link is no longer active. Return to ${botName} and retry the original request.`,
1837
- status: 400
1838
- },
1839
- success: {
1840
- title: "Authorization complete",
1841
- message: `Your MCP access is connected. ${botName} will continue the paused request.`,
1842
- status: 200
1843
- },
1844
- failure: {
1845
- title: "Authorization failed",
1846
- message: `${botName} could not finish the authorization callback. Return to ${botName} and retry the original request.`,
1847
- status: 500
1848
- }
1849
- };
1850
- }
1851
- var McpOAuthAttemptExpiredError = class extends Error {
1852
- constructor() {
1853
- super("MCP OAuth authorization attempt is no longer current");
1854
- this.name = "McpOAuthAttemptExpiredError";
1855
- }
1856
- };
1857
- function mcpAuthorizationId(args) {
1858
- return `${args.sessionId}:mcp:${args.provider}`;
1859
- }
1860
- function htmlResponse(kind, options) {
1861
- const botName = botConfig.userName;
1862
- const pages = callbackPages(botName);
1863
- const page = kind === "success" && options?.local ? {
1864
- ...pages.success,
1865
- message: `Your MCP access is connected. ${botName} will continue the paused request in the local client.`
1866
- } : pages[kind];
1867
- const footerMessage = kind === "success" && !options?.local ? `You can close this tab and return to ${botName}.` : void 0;
1868
- return htmlCallbackResponse(page.title, page.message, page.status, {
1869
- footerMessage
1870
- });
1871
- }
1872
- async function persistCompletedReplyState(channelId, threadTs, sessionId, reply) {
1873
- const threadId = `slack:${channelId}:${threadTs}`;
1874
- const currentState = await getPersistedThreadState(threadId);
1875
- const conversation = coerceThreadConversationState(currentState);
1876
- await hydrateConversationMessages({ conversation, conversationId: threadId });
1877
- const userMessage = getTurnUserMessage(conversation, sessionId);
1878
- const statePatch = buildDeliveredTurnStatePatch({
1879
- conversation,
1880
- reply,
1881
- sessionId,
1882
- userMessageId: userMessage?.id
1883
- });
1884
- await persistThreadStateById(threadId, {
1885
- ...statePatch
1886
- });
1887
- }
1888
- async function failSessionRecordBestEffort(args) {
1889
- try {
1890
- await failTurnRecord({
1891
- conversationId: args.conversationId,
1892
- turnId: args.turnId,
1893
- errorMessage: args.errorMessage,
1894
- expectedVersion: args.expectedVersion
1895
- });
1896
- } catch (error) {
1897
- logException(
1898
- error,
1899
- "mcp.oauth_callback.session_record.failure_persistence.failed",
1900
- {
1901
- "app.ai.conversation_id": args.conversationId,
1902
- "app.ai.session_id": args.turnId
1903
- }
1904
- );
1905
- }
1906
- }
1907
- async function persistFailedReplyState(channelId, threadTs, sessionId, expectedVersion) {
1908
- const threadId = `slack:${channelId}:${threadTs}`;
1909
- const currentState = await getPersistedThreadState(threadId);
1910
- const conversation = coerceThreadConversationState(currentState);
1911
- await hydrateConversationMessages({ conversation, conversationId: threadId });
1912
- clearPendingAuth(conversation, sessionId);
1913
- markTurnFailed({
1914
- conversation,
1915
- nowMs: Date.now(),
1916
- sessionId,
1917
- userMessageId: getTurnUserMessageId(conversation, sessionId),
1918
- markConversationMessage
1919
- });
1920
- await failSessionRecordBestEffort({
1921
- conversationId: threadId,
1922
- turnId: sessionId,
1923
- errorMessage: "OAuth-resumed MCP turn failed",
1924
- expectedVersion
1925
- });
1926
- await persistThreadStateById(threadId, {
1927
- conversation
1928
- });
1929
- }
1930
- async function resumeAuthorizedMcpTurn(args) {
1931
- const { authSession, agentRunner, provider } = args;
1932
- if (!authSession.channelId || !authSession.destination || !authSession.threadTs) {
1933
- return;
1934
- }
1935
- const destination = requireSlackDestination(
1936
- authSession.destination,
1937
- "MCP OAuth resume"
1938
- );
1939
- const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
1940
- const currentState = await getPersistedThreadState(threadId);
1941
- const conversation = coerceThreadConversationState(currentState);
1942
- await hydrateConversationMessages({ conversation, conversationId: threadId });
1943
- const pendingAuth = getConversationPendingAuth({
1944
- conversation,
1945
- kind: "mcp",
1946
- provider,
1947
- actorId: authSession.userId
1948
- });
1949
- if (pendingAuth?.authSessionId !== authSession.authSessionId) {
1950
- return;
1951
- }
1952
- const resolvedSessionId = pendingAuth.sessionId;
1953
- const userMessage = getTurnUserMessage(conversation, resolvedSessionId);
1954
- if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
1955
- clearPendingAuth(conversation, pendingAuth.sessionId);
1956
- await persistThreadStateById(threadId, { conversation });
1957
- await abandonTurnRecord({
1958
- conversationId: authSession.conversationId,
1959
- turnId: pendingAuth.sessionId,
1960
- errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
1961
- });
1962
- return;
1963
- }
1964
- if (!userMessage) {
1965
- return;
1966
- }
1967
- await resumeSlackTurn({
1968
- messageText: userMessage.text,
1969
- conversationId: authSession.conversationId,
1970
- turnId: resolvedSessionId,
1971
- channelId: authSession.channelId,
1972
- threadTs: authSession.threadTs,
1973
- messageTs: getTurnUserSlackMessageTs(userMessage),
1974
- lockKey: threadId,
1975
- initialText: "",
1976
- executeTurn: async (run, saveResult, timeoutMs) => await executeTurn(agentRunner, run, saveResult, timeoutMs),
1977
- beforeStart: async () => {
1978
- const lockedState = await getPersistedThreadState(threadId);
1979
- const lockedConversation = coerceThreadConversationState(lockedState);
1980
- await hydrateConversationMessages({
1981
- conversation: lockedConversation,
1982
- conversationId: threadId
1983
- });
1984
- const lockedPendingAuth = getConversationPendingAuth({
1985
- conversation: lockedConversation,
1986
- kind: "mcp",
1987
- provider,
1988
- actorId: authSession.userId
1989
- });
1990
- if (lockedPendingAuth?.authSessionId !== authSession.authSessionId) {
1991
- return false;
1992
- }
1993
- const lockedSessionId = lockedPendingAuth.sessionId;
1994
- if (lockedSessionId !== resolvedSessionId) {
1995
- return false;
1996
- }
1997
- if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
1998
- clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
1999
- await persistThreadStateById(threadId, {
2000
- conversation: lockedConversation
2001
- });
2002
- await abandonTurnRecord({
2003
- conversationId: authSession.conversationId,
2004
- turnId: lockedPendingAuth.sessionId,
2005
- errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
2006
- });
2007
- return false;
2008
- }
2009
- const lockedUserMessage = getTurnUserMessage(
2010
- lockedConversation,
2011
- lockedSessionId
2012
- );
2013
- if (!lockedUserMessage) {
2014
- return false;
2015
- }
2016
- const lockedSessionRecord = await getTurnRecord(
2017
- authSession.conversationId,
2018
- lockedSessionId
2019
- );
2020
- if (!lockedSessionRecord || lockedSessionRecord.state !== "paused" || lockedSessionRecord.resumeReason !== "auth") {
2021
- return false;
2022
- }
2023
- const lockedConversationContext = buildConversationContext(
2024
- lockedConversation,
2025
- {
2026
- excludeMessageId: lockedUserMessage.id
2027
- }
2028
- );
2029
- const lockedLocationConfiguration = getLocationConfigurationService(destination);
2030
- let actor;
2031
- try {
2032
- actor = createSlackResumeActor({
2033
- actor: lockedSessionRecord.actor,
2034
- teamId: destination.teamId,
2035
- userId: authSession.userId
2036
- });
2037
- } catch {
2038
- await failTurnRecord({
2039
- conversationId: authSession.conversationId,
2040
- expectedVersion: lockedSessionRecord.version,
2041
- turnId: lockedSessionId,
2042
- errorMessage: "Unable to rebuild Slack actor for OAuth resume"
2043
- });
2044
- return false;
2045
- }
2046
- let routing;
2047
- try {
2048
- routing = await resolveTurnSessionRouting({
2049
- conversationId: authSession.conversationId
2050
- });
2051
- } catch (error) {
2052
- await failTurnRecord({
2053
- conversationId: authSession.conversationId,
2054
- expectedVersion: lockedSessionRecord.version,
2055
- turnId: lockedSessionId,
2056
- errorMessage: error instanceof Error ? error.message : String(error)
2057
- });
2058
- return false;
2059
- }
2060
- await recordAuthorizationCompleted({
2061
- conversationId: authSession.conversationId,
2062
- kind: "mcp",
2063
- provider,
2064
- actorId: authSession.userId,
2065
- authorizationId: mcpAuthorizationId({
2066
- provider,
2067
- sessionId: lockedSessionId
2068
- })
2069
- });
2070
- const lockedMessageTs = getTurnUserSlackMessageTs(lockedUserMessage);
2071
- return {
2072
- messageText: lockedUserMessage.text,
2073
- sliceId: lockedSessionRecord.sliceId,
2074
- messageTs: lockedMessageTs,
2075
- inputMessageIds: [lockedUserMessage.id],
2076
- run: {
2077
- instruction: {
2078
- text: lockedUserMessage.text,
2079
- context: lockedConversationContext,
2080
- ...getTurnUserReplyAttachmentContext(lockedUserMessage)
2081
- },
2082
- // Pi history is SQL-authoritative: the resumed run reads its
2083
- // session record first and falls back to the step projection.
2084
- history: await loadProjection({ conversationId: threadId }),
2085
- credentialContext: {
2086
- actor: { type: "user", userId: actor.userId }
2087
- },
2088
- actor,
2089
- destination,
2090
- ...routing.location ? { location: routing.location } : void 0,
2091
- source: routing.source,
2092
- toolChannelId: authSession.toolChannelId ?? authSession.channelId,
2093
- environment: {
2094
- configuration: authSession.configuration,
2095
- locationConfiguration: lockedLocationConfiguration
2096
- },
2097
- state: {
2098
- pendingAuth: lockedPendingAuth,
2099
- sandboxRef: getPersistedSandboxState(lockedState)
2100
- },
2101
- durability: {
2102
- recordPendingAuth: async (nextPendingAuth) => {
2103
- lockedConversation.processing.pendingAuth = nextPendingAuth;
2104
- await persistThreadStateById(threadId, {
2105
- conversation: lockedConversation
2106
- });
2107
- }
2108
- }
2109
- },
2110
- commitResult: async (reply) => {
2111
- await persistCompletedReplyState(
2112
- authSession.channelId,
2113
- authSession.threadTs,
2114
- lockedSessionId,
2115
- reply
2116
- );
2117
- },
2118
- onPostDeliveryCommitFailure: async () => {
2119
- await failTurnRecord({
2120
- conversationId: authSession.conversationId,
2121
- expectedVersion: lockedSessionRecord.version,
2122
- turnId: lockedSessionId,
2123
- errorMessage: "OAuth-resumed MCP reply was delivered but completion state did not persist"
2124
- });
2125
- },
2126
- onFailure: async () => {
2127
- try {
2128
- await persistFailedReplyState(
2129
- authSession.channelId,
2130
- authSession.threadTs,
2131
- lockedSessionId,
2132
- lockedSessionRecord.version
2133
- );
2134
- } catch (persistError) {
2135
- logException(
2136
- persistError,
2137
- "mcp.oauth_callback.resume_failure_persist.failed",
2138
- { "app.credential.provider": provider }
2139
- );
2140
- }
2141
- },
2142
- onAuthPause: async () => {
2143
- await persistAuthPauseTurnState({
2144
- sessionId: lockedSessionId,
2145
- threadStateId: threadId
2146
- });
2147
- logWarn("mcp.oauth_callback.resume.reparked_for_auth", {
2148
- "app.credential.provider": provider
2149
- });
2150
- },
2151
- onSuspend: async (resumeVersion) => {
2152
- await wakePausedTurn({
2153
- conversationId: authSession.conversationId,
2154
- destination,
2155
- turnId: lockedSessionId,
2156
- expectedVersion: resumeVersion
2157
- });
2158
- }
2159
- };
2160
- }
2161
- });
2162
- }
2163
- async function isCurrentMcpAuthorizationAttempt(authSession, provider) {
2164
- const conversationId = mcpConversationId(authSession);
2165
- if (!conversationId) {
2166
- return false;
2167
- }
2168
- const currentState = await getPersistedThreadState(conversationId);
2169
- const conversation = coerceThreadConversationState(currentState);
2170
- const pendingAuth = getConversationPendingAuth({
2171
- conversation,
2172
- kind: "mcp",
2173
- provider,
2174
- actorId: authSession.userId
2175
- });
2176
- return pendingAuth?.authSessionId === authSession.authSessionId && pendingAuth.sessionId === authSession.sessionId;
2177
- }
2178
- function mcpConversationId(authSession) {
2179
- if (authSession.destination?.platform === "local" || authSession.source?.kind === "web") {
2180
- return authSession.conversationId;
2181
- }
2182
- if (authSession.channelId && authSession.threadTs) {
2183
- return `slack:${authSession.channelId}:${authSession.threadTs}`;
2184
- }
2185
- return void 0;
2186
- }
2187
- async function runCurrentMcpCredentialMutation(authSession, provider, mutation) {
2188
- const conversationId = mcpConversationId(authSession);
2189
- if (!conversationId) {
2190
- throw new McpOAuthAttemptExpiredError();
2191
- }
2192
- const stateAdapter = getStateAdapter();
2193
- await stateAdapter.connect();
2194
- const lock = await acquireActiveLock(stateAdapter, conversationId);
2195
- if (!lock) {
2196
- throw new Error(
2197
- `Could not acquire MCP OAuth callback lock for ${conversationId}`
2198
- );
2199
- }
2200
- try {
2201
- if (!await isCurrentMcpAuthorizationAttempt(authSession, provider)) {
2202
- throw new McpOAuthAttemptExpiredError();
2203
- }
2204
- return await mutation();
2205
- } finally {
2206
- await stateAdapter.releaseLock(lock);
2207
- }
2208
- }
2209
- async function GET(request, provider, waitUntil, options) {
2210
- const localRelay = await relayLocalOAuthCallback(request);
2211
- if (localRelay) {
2212
- return localRelay;
2213
- }
2214
- const url = new URL(request.url);
2215
- const state = url.searchParams.get("state")?.trim();
2216
- const code = url.searchParams.get("code")?.trim();
2217
- const error = url.searchParams.get("error")?.trim();
2218
- if (!state) {
2219
- return htmlResponse("missing_state");
2220
- }
2221
- if (error) {
2222
- try {
2223
- const pendingSession = await getMcpAuthSession(state);
2224
- if (pendingSession && pendingSession.provider === provider) {
2225
- try {
2226
- await runCurrentMcpCredentialMutation(
2227
- pendingSession,
2228
- provider,
2229
- async () => {
2230
- const credentials = await getMcpStoredOAuthCredentials(
2231
- pendingSession.userId,
2232
- provider
2233
- );
2234
- if (!credentials?.clientInformation && !credentials?.discoveryState) {
2235
- return;
2236
- }
2237
- const nextCredentials = credentials.tokens ? { tokens: credentials.tokens } : {};
2238
- await putMcpStoredOAuthCredentials(
2239
- pendingSession.userId,
2240
- provider,
2241
- nextCredentials
2242
- );
2243
- }
2244
- );
2245
- } catch (cleanupError) {
2246
- if (!(cleanupError instanceof McpOAuthAttemptExpiredError)) {
2247
- logException(
2248
- cleanupError,
2249
- "mcp.oauth_callback.provider_error_cleanup.failed",
2250
- { "app.credential.provider": provider }
2251
- );
2252
- }
2253
- }
2254
- await deleteMcpAuthSession(pendingSession.authSessionId);
2255
- }
2256
- } catch (cleanupError) {
2257
- logException(cleanupError, "mcp.oauth_callback.provider_error_cleanup.failed", {
2258
- "app.credential.provider": provider
2259
- });
2260
- }
2261
- return htmlResponse("provider_error");
2262
- }
2263
- if (!code) {
2264
- return htmlResponse("missing_code");
2265
- }
2266
- try {
2267
- const pendingSession = await getMcpAuthSession(state);
2268
- if (!pendingSession || pendingSession.provider !== provider || !await isCurrentMcpAuthorizationAttempt(pendingSession, provider)) {
2269
- if (pendingSession) {
2270
- await deleteMcpAuthSession(pendingSession.authSessionId);
2271
- }
2272
- return htmlResponse("expired");
2273
- }
2274
- const authSession = await finalizeMcpAuthorization(
2275
- provider,
2276
- state,
2277
- code,
2278
- async (mutation) => await runCurrentMcpCredentialMutation(
2279
- pendingSession,
2280
- provider,
2281
- mutation
2282
- )
2283
- );
2284
- let providerLabel;
2285
- try {
2286
- providerLabel = formatProviderLabel(provider);
2287
- } catch {
2288
- providerLabel = void 0;
2289
- }
2290
- try {
2291
- await recordAuthenticationLinked({
2292
- conversationId: authSession.conversationId,
2293
- provider,
2294
- actorId: authSession.userId,
2295
- authorizationId: mcpAuthorizationId({
2296
- provider,
2297
- sessionId: authSession.sessionId
2298
- }),
2299
- turnId: authSession.sessionId,
2300
- ...providerLabel ? { providerLabel } : void 0
2301
- });
2302
- } catch (error2) {
2303
- logException(error2, "mcp.oauth_callback.authentication_event.failed", {
2304
- "app.credential.provider": provider
2305
- });
2306
- }
2307
- try {
2308
- await deleteMcpAuthSession(authSession.authSessionId);
2309
- } catch (cleanupError) {
2310
- logException(cleanupError, "mcp.oauth_callback.session_cleanup.failed", {
2311
- "app.credential.provider": provider
2312
- });
2313
- }
2314
- if (authSession.source?.kind === "web" && authSession.destination) {
2315
- waitUntil(async () => {
2316
- const turn = await getTurnRecord(
2317
- authSession.conversationId,
2318
- authSession.sessionId
2319
- );
2320
- await deleteWebAuthorization({
2321
- actorId: authSession.userId,
2322
- conversationId: authSession.conversationId
2323
- });
2324
- if (!turn || turn.state !== "paused" || turn.resumeReason !== "auth") {
2325
- return;
2326
- }
2327
- await recordAuthorizationCompleted({
2328
- conversationId: authSession.conversationId,
2329
- kind: "mcp",
2330
- provider,
2331
- actorId: authSession.userId,
2332
- authorizationId: mcpAuthorizationId({
2333
- provider,
2334
- sessionId: authSession.sessionId
2335
- })
2336
- });
2337
- await wakePausedTurn(
2338
- {
2339
- conversationId: authSession.conversationId,
2340
- destination: authSession.destination,
2341
- turnId: authSession.sessionId,
2342
- expectedVersion: turn.version
2343
- },
2344
- options.conversationWorkQueue ? { queue: options.conversationWorkQueue } : void 0
2345
- );
2346
- });
2347
- } else if (authSession.destination?.platform !== "local") {
2348
- waitUntil(
2349
- () => resumeAuthorizedMcpTurn({
2350
- authSession,
2351
- agentRunner: options.agentRunner,
2352
- provider
2353
- })
2354
- );
2355
- }
2356
- return htmlResponse("success", {
2357
- // Web roots keep a local destination for conversation-only delivery.
2358
- // Only the CLI path should get the local-client success copy.
2359
- local: authSession.destination?.platform === "local" && authSession.source?.kind !== "web"
2360
- });
2361
- } catch (callbackError) {
2362
- if (callbackError instanceof McpOAuthAttemptExpiredError) {
2363
- await deleteMcpAuthSession(state);
2364
- return htmlResponse("expired");
2365
- }
2366
- logException(callbackError, "mcp.oauth_callback.failed", {
2367
- "app.credential.provider": provider,
2368
- ...getMcpProviderErrorAttributes(callbackError)
2369
- });
2370
- return htmlResponse("failure");
2371
- }
2372
- }
2373
-
2374
- // src/chat/slack/app-home.ts
2375
- import fs from "fs";
2376
- import path from "path";
2377
- var DEFAULT_DESCRIPTION_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
2378
- var MAX_HOME_SKILLS = 6;
2379
- var MAX_SECTION_TEXT_CHARS = 3e3;
2380
- var HIDDEN_HOME_SKILLS = /* @__PURE__ */ new Set(["jr-rpc"]);
2381
- function clampSectionText(text) {
2382
- if (text.length <= MAX_SECTION_TEXT_CHARS) {
2383
- return text;
2384
- }
2385
- return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
2386
- }
2387
- function loadDescriptionText() {
2388
- const descriptionPath = path.join(homeDir(), "DESCRIPTION.md");
2389
- try {
2390
- const raw = fs.readFileSync(descriptionPath, "utf8").trim();
2391
- if (raw.length > 0) {
2392
- return clampSectionText(raw);
2393
- }
2394
- } catch {
2395
- }
2396
- return DEFAULT_DESCRIPTION_TEXT;
2397
- }
2398
- async function buildSkillsSummaryText() {
2399
- const skills = (await discoverSkills()).filter(
2400
- (skill) => !HIDDEN_HOME_SKILLS.has(skill.name)
2401
- );
2402
- if (skills.length === 0) {
2403
- return "No skills installed.";
2404
- }
2405
- const visible = skills.slice(0, MAX_HOME_SKILLS);
2406
- const lines = visible.map(
2407
- (skill) => `\u2022 *${skill.name}* \u2014 ${skill.description}`
2408
- );
2409
- if (skills.length > visible.length) {
2410
- lines.push(`\u2022 \u2026and ${skills.length - visible.length} more`);
2411
- }
2412
- return lines.join("\n");
2413
- }
2414
- function accountLabel(account) {
2415
- const label = account.label ?? account.id;
2416
- return account.url ? `<${account.url}|${label}>` : label;
2417
- }
2418
- function connectedAccountText(plugin, account) {
2419
- return account ? `*${plugin.manifest.name}*
2420
- Connected as ${accountLabel(account)}` : `*${plugin.manifest.name}*
2421
- ${plugin.manifest.description}`;
2422
- }
2423
- async function connectedOAuthTokens(userId, plugin, userTokenStore) {
2424
- if (plugin.manifest.oauth || plugin.manifest.credentials) {
2425
- const stored = await userTokenStore.get(userId, plugin.manifest.name);
2426
- return stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope) ? stored : void 0;
2427
- }
2428
- return void 0;
2429
- }
2430
- async function hasConnectedMcpAccount(userId, plugin) {
2431
- if (!plugin.manifest.mcp) {
2432
- return false;
2433
- }
2434
- const credentials = await getMcpStoredOAuthCredentials(
2435
- userId,
2436
- plugin.manifest.name
2437
- );
2438
- return Boolean(
2439
- credentials?.tokens || credentials?.clientInformation || credentials?.discoveryState
2440
- );
2441
- }
2442
- async function buildHomeView(userId, userTokenStore) {
2443
- const runtimeMetadata = getRuntimeMetadata();
2444
- const descriptionText = loadDescriptionText();
2445
- const skillsSummaryText = await buildSkillsSummaryText();
2446
- const providers = pluginCatalogRuntime.getProviders();
2447
- const connectedSections = [];
2448
- for (const plugin of providers) {
2449
- const tokens = await connectedOAuthTokens(userId, plugin, userTokenStore);
2450
- if (!tokens && !await hasConnectedMcpAccount(userId, plugin)) continue;
2451
- connectedSections.push({
2452
- type: "section",
2453
- text: {
2454
- type: "mrkdwn",
2455
- text: connectedAccountText(plugin, tokens?.account)
2456
- },
2457
- accessory: {
2458
- type: "button",
2459
- text: { type: "plain_text", text: "Unlink" },
2460
- action_id: "app_home_disconnect",
2461
- value: plugin.manifest.name,
2462
- style: "danger"
2463
- }
2464
- });
2465
- }
2466
- const accountBlocks = connectedSections.length > 0 ? connectedSections : [
2467
- {
2468
- type: "section",
2469
- text: {
2470
- type: "mrkdwn",
2471
- text: "No connected accounts"
2472
- }
2473
- }
2474
- ];
2475
- return {
2476
- type: "home",
2477
- blocks: [
2478
- {
2479
- type: "header",
2480
- text: {
2481
- type: "plain_text",
2482
- text: "Junior"
2483
- }
2484
- },
2485
- {
2486
- type: "section",
2487
- text: {
2488
- type: "mrkdwn",
2489
- text: descriptionText
2490
- }
2491
- },
2492
- { type: "divider" },
2493
- {
2494
- type: "header",
2495
- text: {
2496
- type: "plain_text",
2497
- text: "What I can help with"
2498
- }
2499
- },
2500
- {
2501
- type: "section",
2502
- text: {
2503
- type: "mrkdwn",
2504
- text: skillsSummaryText
2505
- }
2506
- },
2507
- { type: "divider" },
2508
- {
2509
- type: "header",
2510
- text: {
2511
- type: "plain_text",
2512
- text: "Connected accounts"
2513
- }
2514
- },
2515
- ...accountBlocks,
2516
- {
2517
- type: "context",
2518
- elements: [
2519
- {
2520
- type: "mrkdwn",
2521
- text: `*junior version:* \`${runtimeMetadata.version ?? "unknown"}\``
2522
- }
2523
- ]
2524
- }
2525
- ]
2526
- };
2527
- }
2528
- async function publishAppHomeView(slackClient, userId, userTokenStore) {
2529
- const view = await buildHomeView(userId, userTokenStore);
2530
- await slackClient.views.publish({ user_id: userId, view });
2531
- }
2532
-
2533
- // src/handlers/oauth-callback.ts
2534
- function htmlErrorResponse(title, message, status) {
2535
- return htmlCallbackResponse(escapeXml(title), escapeXml(message), status);
2536
- }
2537
- function oauthTokenErrorAttributes(provider, endpoint, status) {
2538
- return {
2539
- "app.credential.provider": provider,
2540
- "app.oauth.error.phase": "token_exchange",
2541
- "server.address": new URL(endpoint).hostname,
2542
- ...status !== void 0 ? { "http.response.status_code": status } : void 0
2543
- };
2544
- }
2545
- async function persistCompletedOAuthReplyState(args) {
2546
- const currentState = await getPersistedThreadState(args.conversationId);
2547
- const conversation = coerceThreadConversationState(currentState);
2548
- await hydrateConversationMessages({
2549
- conversation,
2550
- conversationId: args.conversationId
2551
- });
2552
- const userMessage = getTurnUserMessage(conversation, args.sessionId);
2553
- const statePatch = buildDeliveredTurnStatePatch({
2554
- conversation,
2555
- reply: args.reply,
2556
- sessionId: args.sessionId,
2557
- userMessageId: userMessage?.id
2558
- });
2559
- await persistThreadStateById(args.conversationId, {
2560
- ...statePatch
2561
- });
2562
- }
2563
- function pluginAuthorizationId(args) {
2564
- return `${args.sessionId}:plugin:${args.provider}`;
2565
- }
2566
- async function failSessionRecordBestEffort2(args) {
2567
- try {
2568
- await failTurnRecord({
2569
- conversationId: args.conversationId,
2570
- expectedVersion: args.expectedVersion,
2571
- turnId: args.turnId,
2572
- errorMessage: args.errorMessage
2573
- });
2574
- } catch (error) {
2575
- logException(
2576
- error,
2577
- "oauth.callback.session_record.failure_persistence.failed",
2578
- {
2579
- "app.ai.conversation_id": args.conversationId,
2580
- "app.ai.session_id": args.turnId
2581
- }
2582
- );
2583
- }
2584
- }
2585
- async function persistFailedOAuthReplyState(args) {
2586
- const currentState = await getPersistedThreadState(args.conversationId);
2587
- const conversation = coerceThreadConversationState(currentState);
2588
- await hydrateConversationMessages({
2589
- conversation,
2590
- conversationId: args.conversationId
2591
- });
2592
- clearPendingAuth(conversation, args.sessionId);
2593
- markTurnFailed({
2594
- conversation,
2595
- nowMs: Date.now(),
2596
- sessionId: args.sessionId,
2597
- userMessageId: getTurnUserMessage(conversation, args.sessionId)?.id,
2598
- markConversationMessage
2599
- });
2600
- await failSessionRecordBestEffort2({
2601
- conversationId: args.conversationId,
2602
- expectedVersion: args.expectedVersion,
2603
- turnId: args.sessionId,
2604
- errorMessage: "OAuth-resumed turn failed"
2605
- });
2606
- await persistThreadStateById(args.conversationId, {
2607
- conversation
2608
- });
2609
- }
2610
- async function resumeOAuthSessionRecordTurn(stored, options) {
2611
- if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.destination || !stored.source || !stored.threadTs) {
2612
- return;
2613
- }
2614
- const destination = requireSlackDestination(
2615
- stored.destination,
2616
- "OAuth resume"
2617
- );
2618
- const currentState = await getPersistedThreadState(
2619
- stored.resumeConversationId
2620
- );
2621
- const conversation = coerceThreadConversationState(currentState);
2622
- await hydrateConversationMessages({
2623
- conversation,
2624
- conversationId: stored.resumeConversationId
2625
- });
2626
- const pendingAuth = getConversationPendingAuth({
2627
- conversation,
2628
- kind: "plugin",
2629
- provider: stored.provider,
2630
- actorId: stored.userId,
2631
- ...stored.scope ? { scope: stored.scope } : void 0
2632
- });
2633
- const resolvedSessionId = pendingAuth?.sessionId ?? stored.resumeSessionId;
2634
- const userMessage = resolvedSessionId ? getTurnUserMessage(conversation, resolvedSessionId) : void 0;
2635
- if (pendingAuth) {
2636
- if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
2637
- clearPendingAuth(conversation, pendingAuth.sessionId);
2638
- await persistThreadStateById(stored.resumeConversationId, {
2639
- conversation
2640
- });
2641
- await abandonTurnRecord({
2642
- conversationId: stored.resumeConversationId,
2643
- turnId: pendingAuth.sessionId,
2644
- errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
2645
- });
2646
- return;
2647
- }
2648
- }
2649
- const sessionRecord = await getTurnRecord(
2650
- stored.resumeConversationId,
2651
- resolvedSessionId
2652
- );
2653
- if (!sessionRecord) {
2654
- return;
2655
- }
2656
- if (sessionRecord.state === "completed" || sessionRecord.state === "failed" || sessionRecord.state === "abandoned") {
2657
- return;
2658
- }
2659
- if (sessionRecord.state !== "paused" || sessionRecord.resumeReason !== "auth") {
2660
- return;
2661
- }
2662
- if (!userMessage?.author?.userId) {
2663
- return;
2664
- }
2665
- if (!pendingAuth && conversation.processing.activeTurnId !== stored.resumeSessionId) {
2666
- return;
2667
- }
2668
- await resumeSlackTurn({
2669
- messageText: userMessage.text,
2670
- conversationId: stored.resumeConversationId,
2671
- turnId: resolvedSessionId,
2672
- channelId: stored.channelId,
2673
- threadTs: stored.threadTs,
2674
- messageTs: getTurnUserSlackMessageTs(userMessage),
2675
- lockKey: stored.resumeConversationId,
2676
- initialText: "",
2677
- executeTurn: async (run, saveResult, timeoutMs) => await executeTurn(options.agentRunner, run, saveResult, timeoutMs),
2678
- beforeStart: async () => {
2679
- const lockedState = await getPersistedThreadState(
2680
- stored.resumeConversationId
2681
- );
2682
- const lockedConversation = coerceThreadConversationState(lockedState);
2683
- await hydrateConversationMessages({
2684
- conversation: lockedConversation,
2685
- conversationId: stored.resumeConversationId
2686
- });
2687
- const lockedPendingAuth = getConversationPendingAuth({
2688
- conversation: lockedConversation,
2689
- kind: "plugin",
2690
- provider: stored.provider,
2691
- actorId: stored.userId,
2692
- ...stored.scope ? { scope: stored.scope } : void 0
2693
- });
2694
- const lockedSessionId = lockedPendingAuth?.sessionId ?? stored.resumeSessionId;
2695
- if (lockedSessionId !== resolvedSessionId) {
2696
- return false;
2697
- }
2698
- const lockedSessionRecord = await getTurnRecord(
2699
- stored.resumeConversationId,
2700
- lockedSessionId
2701
- );
2702
- if (!lockedSessionRecord || lockedSessionRecord.state !== "paused" || lockedSessionRecord.resumeReason !== "auth") {
2703
- return false;
2704
- }
2705
- if (lockedPendingAuth) {
2706
- if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
2707
- clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
2708
- await persistThreadStateById(stored.resumeConversationId, {
2709
- conversation: lockedConversation
2710
- });
2711
- await abandonTurnRecord({
2712
- conversationId: stored.resumeConversationId,
2713
- turnId: lockedPendingAuth.sessionId,
2714
- errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
2715
- });
2716
- return false;
2717
- }
2718
- } else if (lockedConversation.processing.activeTurnId !== stored.resumeSessionId) {
2719
- return false;
2720
- }
2721
- const lockedUserMessage = getTurnUserMessage(
2722
- lockedConversation,
2723
- lockedSessionId
2724
- );
2725
- if (!lockedUserMessage?.author?.userId) {
2726
- return false;
2727
- }
2728
- const lockedConversationContext = buildConversationContext(
2729
- lockedConversation,
2730
- {
2731
- excludeMessageId: lockedUserMessage.id
2732
- }
2733
- );
2734
- const lockedLocationConfiguration = getLocationConfigurationService(destination);
2735
- let actor;
2736
- try {
2737
- actor = createSlackResumeActor({
2738
- actor: lockedSessionRecord.actor,
2739
- teamId: destination.teamId,
2740
- userId: lockedUserMessage.author.userId
2741
- });
2742
- } catch {
2743
- await failTurnRecord({
2744
- conversationId: stored.resumeConversationId,
2745
- expectedVersion: lockedSessionRecord.version,
2746
- turnId: lockedSessionId,
2747
- errorMessage: "Unable to rebuild Slack actor for OAuth resume"
2748
- });
2749
- return false;
2750
- }
2751
- let routing;
2752
- try {
2753
- routing = await resolveTurnSessionRouting({
2754
- conversationId: stored.resumeConversationId
2755
- });
2756
- } catch (error) {
2757
- await failTurnRecord({
2758
- conversationId: stored.resumeConversationId,
2759
- expectedVersion: lockedSessionRecord.version,
2760
- turnId: lockedSessionId,
2761
- errorMessage: error instanceof Error ? error.message : String(error)
2762
- });
2763
- return false;
2764
- }
2765
- await recordAuthorizationCompleted({
2766
- conversationId: stored.resumeConversationId,
2767
- kind: "plugin",
2768
- provider: stored.provider,
2769
- actorId: stored.userId,
2770
- authorizationId: pluginAuthorizationId({
2771
- provider: stored.provider,
2772
- sessionId: lockedSessionId
2773
- })
2774
- });
2775
- const lockedMessageTs = getTurnUserSlackMessageTs(lockedUserMessage);
2776
- return {
2777
- messageText: lockedUserMessage.text,
2778
- sliceId: lockedSessionRecord.sliceId,
2779
- messageTs: lockedMessageTs,
2780
- inputMessageIds: [lockedUserMessage.id],
2781
- run: {
2782
- instruction: {
2783
- text: lockedUserMessage.text,
2784
- context: lockedConversationContext,
2785
- ...getTurnUserReplyAttachmentContext(lockedUserMessage)
2786
- },
2787
- // Pi history is SQL-authoritative: the resumed run reads its
2788
- // session record first and falls back to the step projection.
2789
- history: await loadProjection({
2790
- conversationId: stored.resumeConversationId
2791
- }),
2792
- credentialContext: {
2793
- actor: {
2794
- type: "user",
2795
- userId: actor.userId
2796
- }
2797
- },
2798
- actor,
2799
- destination,
2800
- ...routing.location ? { location: routing.location } : void 0,
2801
- source: routing.source,
2802
- toolChannelId: stored.channelId,
2803
- environment: {
2804
- locationConfiguration: lockedLocationConfiguration
2805
- },
2806
- state: {
2807
- pendingAuth: lockedPendingAuth,
2808
- sandboxRef: getPersistedSandboxState(lockedState)
2809
- },
2810
- durability: {
2811
- recordPendingAuth: async (nextPendingAuth) => {
2812
- lockedConversation.processing.pendingAuth = nextPendingAuth;
2813
- await persistThreadStateById(stored.resumeConversationId, {
2814
- conversation: lockedConversation
2815
- });
2816
- }
2817
- }
2818
- },
2819
- commitResult: async (reply) => {
2820
- logInfo("oauth.callback.resume.completed", {
2821
- "app.credential.provider": stored.provider,
2822
- "app.ai.outcome": reply.diagnostics.outcome,
2823
- "app.ai.tool_calls": reply.diagnostics.toolCalls.length
2824
- });
2825
- await persistCompletedOAuthReplyState({
2826
- conversationId: stored.resumeConversationId,
2827
- sessionId: lockedSessionId,
2828
- reply
2829
- });
2830
- },
2831
- onPostDeliveryCommitFailure: async () => {
2832
- await failTurnRecord({
2833
- conversationId: stored.resumeConversationId,
2834
- expectedVersion: lockedSessionRecord.version,
2835
- turnId: lockedSessionId,
2836
- errorMessage: "OAuth-resumed reply was delivered but completion state did not persist"
2837
- });
2838
- },
2839
- onFailure: async () => {
2840
- await persistFailedOAuthReplyState({
2841
- conversationId: stored.resumeConversationId,
2842
- expectedVersion: lockedSessionRecord.version,
2843
- sessionId: lockedSessionId
2844
- });
2845
- },
2846
- onAuthPause: async () => {
2847
- await persistAuthPauseTurnState({
2848
- sessionId: lockedSessionId,
2849
- threadStateId: stored.resumeConversationId
2850
- });
2851
- },
2852
- onSuspend: async (resumeVersion) => {
2853
- await wakePausedTurn({
2854
- conversationId: stored.resumeConversationId,
2855
- destination,
2856
- turnId: lockedSessionId,
2857
- expectedVersion: resumeVersion
2858
- });
2859
- }
2860
- };
2861
- }
2862
- });
2863
- }
2864
- async function GET2(request, provider, waitUntil, options) {
2865
- const localRelay = await relayLocalOAuthCallback(request);
2866
- if (localRelay) {
2867
- return localRelay;
2868
- }
2869
- const providerConfig = pluginCatalogRuntime.getOAuthConfig(provider);
2870
- if (!providerConfig) {
2871
- return htmlErrorResponse(
2872
- "Unknown provider",
2873
- "The OAuth provider in this link is not recognized.",
2874
- 404
2875
- );
2876
- }
2877
- const providerLabel = formatProviderLabel(provider);
2878
- const url = new URL(request.url);
2879
- const errorParam = url.searchParams.get("error");
2880
- const code = url.searchParams.get("code");
2881
- const state = url.searchParams.get("state");
2882
- if (errorParam) {
2883
- if (state) {
2884
- const cleanupAdapter = getStateAdapter();
2885
- await cleanupAdapter.delete(`oauth-state:${state}`);
2886
- }
2887
- if (errorParam === "access_denied") {
2888
- return htmlErrorResponse(
2889
- "Authorization declined",
2890
- `You declined the ${providerLabel} authorization request. Return to Junior and connect your ${providerLabel} account again if you change your mind.`,
2891
- 400
2892
- );
2893
- }
2894
- return htmlErrorResponse(
2895
- "Authorization failed",
2896
- `${providerLabel} returned an error: ${errorParam}. Return to Junior and try again.`,
2897
- 400
2898
- );
2899
- }
2900
- if (!code || !state) {
2901
- return htmlErrorResponse(
2902
- "Invalid request",
2903
- "This authorization link is missing required parameters.",
2904
- 400
2905
- );
2906
- }
2907
- const stateAdapter = getStateAdapter();
2908
- const stateKey = `oauth-state:${state}`;
2909
- const stored = parseOAuthStatePayload(await stateAdapter.get(stateKey));
2910
- if (!stored) {
2911
- return htmlErrorResponse(
2912
- "Link expired",
2913
- `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.`,
2914
- 400
2915
- );
2916
- }
2917
- if (stored.provider !== provider) {
2918
- return htmlErrorResponse(
2919
- "Provider mismatch",
2920
- "This authorization link does not match the expected provider.",
2921
- 400
2922
- );
2923
- }
2924
- await stateAdapter.delete(stateKey);
2925
- const clientId = process.env[providerConfig.clientIdEnv]?.trim();
2926
- const clientSecret = process.env[providerConfig.clientSecretEnv]?.trim();
2927
- if (!clientId || !clientSecret) {
2928
- return htmlErrorResponse(
2929
- "Configuration error",
2930
- "OAuth client credentials are not configured on the server.",
2931
- 500
2932
- );
2933
- }
2934
- const baseUrl = resolveBaseUrl();
2935
- if (!baseUrl) {
2936
- return htmlErrorResponse(
2937
- "Configuration error",
2938
- "The server cannot determine its base URL.",
2939
- 500
2940
- );
2941
- }
2942
- const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
2943
- const requestedScope = stored.scope ?? providerConfig.scope;
2944
- let tokenResponse;
2945
- try {
2946
- const tokenRequest = buildOAuthTokenRequest({
2947
- clientId,
2948
- clientSecret,
2949
- payload: {
2950
- grant_type: "authorization_code",
2951
- code,
2952
- redirect_uri: redirectUri
2953
- },
2954
- tokenAuthMethod: providerConfig.tokenAuthMethod,
2955
- tokenExtraHeaders: providerConfig.tokenExtraHeaders
2956
- });
2957
- tokenResponse = await fetch(providerConfig.tokenEndpoint, {
2958
- method: "POST",
2959
- headers: tokenRequest.headers,
2960
- body: tokenRequest.body
2961
- });
2962
- } catch {
2963
- logWarn(
2964
- "oauth.token_exchange.failed",
2965
- oauthTokenErrorAttributes(provider, providerConfig.tokenEndpoint)
2966
- );
2967
- return htmlErrorResponse(
2968
- "Connection failed",
2969
- "Failed to exchange the authorization code. Please try again.",
2970
- 500
2971
- );
2972
- }
2973
- if (!tokenResponse.ok) {
2974
- logWarn(
2975
- "oauth.token_exchange.failed",
2976
- oauthTokenErrorAttributes(
2977
- provider,
2978
- providerConfig.tokenEndpoint,
2979
- tokenResponse.status
2980
- )
2981
- );
2982
- return htmlErrorResponse(
2983
- "Connection failed",
2984
- "The token exchange with the provider failed. Please try again.",
2985
- 500
2986
- );
2987
- }
2988
- let parsedTokenResponse;
2989
- try {
2990
- const tokenData = await tokenResponse.json();
2991
- parsedTokenResponse = parseOAuthTokenResponse(tokenData, requestedScope, {
2992
- treatEmptyScopeAsUnreported: providerConfig.treatEmptyScopeAsUnreported
2993
- });
2994
- } catch {
2995
- logWarn(
2996
- "oauth.token_exchange.failed",
2997
- oauthTokenErrorAttributes(
2998
- provider,
2999
- providerConfig.tokenEndpoint,
3000
- tokenResponse.status
3001
- )
3002
- );
3003
- return htmlErrorResponse(
3004
- "Connection failed",
3005
- "The provider returned an incomplete token response. Please try again.",
3006
- 500
3007
- );
3008
- }
3009
- if (!hasRequiredOAuthScope(parsedTokenResponse.scope, requestedScope)) {
3010
- return htmlErrorResponse(
3011
- "Connection failed",
3012
- `The ${providerLabel} authorization did not grant the access Junior requires. Return to Junior and connect your ${providerLabel} account again.`,
3013
- 400
3014
- );
3015
- }
3016
- const userTokenStore = createUserTokenStore();
3017
- let account;
3018
- try {
3019
- account = await resolvePluginOAuthAccount({
3020
- provider,
3021
- tokens: parsedTokenResponse
3022
- });
3023
- } catch {
3024
- return htmlErrorResponse(
3025
- "Connection failed",
3026
- `Junior could not verify the connected ${providerLabel} account. Please try again.`,
3027
- 500
3028
- );
3029
- }
3030
- await userTokenStore.set(stored.userId, provider, {
3031
- ...parsedTokenResponse,
3032
- ...account ? { account } : void 0
3033
- });
3034
- const slackActor = stored.actor?.platform === "slack" ? stored.actor : void 0;
3035
- if (account && slackActor) {
3036
- await runBestEffort(
3037
- async () => {
3038
- const slackUserId = parseSlackUserId(slackActor.userId);
3039
- if (!slackUserId) {
3040
- throw new Error("OAuth Slack actor user id is invalid");
3041
- }
3042
- const profile = await lookupSlackUserProfile(slackUserId);
3043
- const slackIdentity = await upsertIdentity(getSqlExecutor(), {
3044
- kind: "user",
3045
- provider: "slack",
3046
- providerTenantId: slackActor.teamId,
3047
- providerSubjectId: profile.id,
3048
- displayName: profile.real_name || profile.display_name,
3049
- handle: profile.name,
3050
- ...profile.email ? { email: profile.email, emailVerified: true } : void 0
3051
- });
3052
- if (!slackIdentity.userId) {
3053
- throw new Error("OAuth Slack identity is not linked to a user");
3054
- }
3055
- await upsertLinkedIdentity(getSqlExecutor(), slackIdentity.userId, {
3056
- kind: "user",
3057
- provider,
3058
- providerSubjectId: account.id,
3059
- displayName: account.displayName,
3060
- handle: account.handle ?? account.label
3061
- });
3062
- },
3063
- "oauth.callback.identity_link.failed",
3064
- { "app.credential.provider": provider }
3065
- );
3066
- }
3067
- const resumeConversationId = stored.resumeConversationId;
3068
- if (resumeConversationId) {
3069
- await runBestEffort(
3070
- async () => {
3071
- await recordAuthenticationLinked({
3072
- conversationId: resumeConversationId,
3073
- provider,
3074
- actorId: stored.userId,
3075
- providerLabel,
3076
- ...account?.label ? { accountLabel: account.label } : void 0,
3077
- ...stored.resumeSessionId ? {
3078
- authorizationId: pluginAuthorizationId({
3079
- provider,
3080
- sessionId: stored.resumeSessionId
3081
- }),
3082
- turnId: stored.resumeSessionId
3083
- } : void 0
3084
- });
3085
- },
3086
- "oauth.callback.authentication_event.failed",
3087
- { "app.credential.provider": provider }
3088
- );
3089
- }
3090
- if (stored.destination?.platform !== "local" && stored.source?.kind !== "web") {
3091
- waitUntil(async () => {
3092
- try {
3093
- await publishAppHomeView(
3094
- getSlackClient(),
3095
- stored.userId,
3096
- userTokenStore
3097
- );
3098
- } catch {
3099
- }
3100
- });
3101
- }
3102
- const resumesWebTurn = Boolean(
3103
- stored.source?.kind === "web" && stored.destination && stored.resumeConversationId && stored.resumeSessionId
3104
- );
3105
- const resumesAgentTurn = Boolean(
3106
- stored.destination?.platform !== "local" && stored.resumeConversationId && stored.resumeSessionId
3107
- );
3108
- if (resumesWebTurn) {
3109
- waitUntil(async () => {
3110
- const turn = await getTurnRecord(
3111
- stored.resumeConversationId,
3112
- stored.resumeSessionId
3113
- );
3114
- await deleteWebAuthorization({
3115
- actorId: stored.userId,
3116
- conversationId: stored.resumeConversationId
3117
- });
3118
- if (!turn || turn.state !== "paused" || turn.resumeReason !== "auth") {
3119
- return;
3120
- }
3121
- await recordAuthorizationCompleted({
3122
- conversationId: stored.resumeConversationId,
3123
- kind: "plugin",
3124
- provider: stored.provider,
3125
- actorId: stored.userId,
3126
- authorizationId: pluginAuthorizationId({
3127
- provider: stored.provider,
3128
- sessionId: stored.resumeSessionId
3129
- })
3130
- });
3131
- await wakePausedTurn(
3132
- {
3133
- conversationId: stored.resumeConversationId,
3134
- destination: stored.destination,
3135
- turnId: stored.resumeSessionId,
3136
- expectedVersion: turn.version
3137
- },
3138
- options.conversationWorkQueue ? { queue: options.conversationWorkQueue } : void 0
3139
- );
3140
- });
3141
- } else if (resumesAgentTurn) {
3142
- waitUntil(
3143
- () => withLogContext(
3144
- { conversationId: stored.resumeConversationId },
3145
- async () => {
3146
- try {
3147
- await resumeOAuthSessionRecordTurn(stored, options);
3148
- } catch (error) {
3149
- if (error instanceof ResumeTurnBusyError) {
3150
- logWarn("oauth.callback.resume.busy", {
3151
- "app.credential.provider": stored.provider,
3152
- ...stored.resumeSessionId && {
3153
- "app.ai.session_id": stored.resumeSessionId
3154
- }
3155
- });
3156
- return;
3157
- }
3158
- throw error;
3159
- }
3160
- }
3161
- )
3162
- );
3163
- } else if (stored.channelId && stored.threadTs) {
3164
- const { channelId, threadTs } = stored;
3165
- waitUntil(
3166
- () => postSlackMessage({
3167
- channelId,
3168
- threadTs,
3169
- text: `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
3170
- })
3171
- );
3172
- }
3173
- const botName = botConfig.userName;
3174
- const statusMessage = stored.destination?.platform === "local" && !resumesWebTurn ? `Your request is continuing in the local ${botName} client.` : resumesWebTurn ? `Your request is continuing in ${botName}.` : resumesAgentTurn ? "Your request is being processed in Slack." : `Your ${providerLabel} account is connected.`;
3175
- const footerMessage = stored.destination?.platform === "local" || resumesWebTurn ? `You can close this tab and return to ${botName}.` : "You can close this tab and return to Slack.";
3176
- return htmlCallbackResponse(
3177
- escapeXml(`${providerLabel} account connected`),
3178
- escapeXml(statusMessage),
3179
- 200,
3180
- { footerMessage: escapeXml(footerMessage) }
3181
- );
3182
- }
3183
-
3184
- // src/chat/local/credential-sync.ts
3185
- import { createHmac, createSecretKey, timingSafeEqual } from "crypto";
3186
- import {
3187
- pluginStoredTokensSchema
3188
- } from "@sentry/junior-plugin-api";
3189
- import { z } from "zod";
3190
- var LOCAL_CREDENTIAL_SYNC_CONTEXT = "junior.local-credential-sync.v1";
3191
- var LOCAL_CREDENTIAL_SYNC_MAX_AGE_MS = 6e4;
3192
- var localCredentialSyncSchema = z.object({
3193
- createdAtMs: z.number().finite(),
3194
- provider: z.string().regex(/^[a-z][a-z0-9-]*$/),
3195
- tokens: pluginStoredTokensSchema
3196
- }).strict();
3197
- function syncSecret() {
3198
- const secret = process.env.JUNIOR_SECRET?.trim();
3199
- if (!secret) {
3200
- throw new Error("Local credential sync requires JUNIOR_SECRET");
3201
- }
3202
- return secret;
3203
- }
3204
- function signature(body) {
3205
- const signingKey = createSecretKey(Buffer.from(syncSecret(), "utf8"));
3206
- return createHmac("sha256", signingKey).update(`${LOCAL_CREDENTIAL_SYNC_CONTEXT}:${body}`).digest("base64url");
3207
- }
3208
- function signaturesMatch(expected, actual) {
3209
- const expectedBuffer = Buffer.from(expected);
3210
- const actualBuffer = Buffer.from(actual);
3211
- return expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer);
3212
- }
3213
- async function syncLocalOAuthCredential(provider, tokens) {
3214
- const body = JSON.stringify({
3215
- createdAtMs: Date.now(),
3216
- provider,
3217
- tokens
3218
- });
3219
- const port = process.env.PORT?.trim() || "3000";
3220
- const response = await fetch(
3221
- `http://127.0.0.1:${port}/api/internal/local-oauth-credentials`,
3222
- {
3223
- method: "POST",
3224
- headers: {
3225
- "content-type": "application/json",
3226
- "x-junior-local-credential-signature": signature(body)
3227
- },
3228
- body
3229
- }
3230
- );
3231
- if (!response.ok) {
3232
- throw new Error(
3233
- `Could not sync local OAuth credential: HTTP ${response.status}`
3234
- );
3235
- }
3236
- }
3237
- async function receiveLocalOAuthCredential(request) {
3238
- const hostname = new URL(request.url).hostname;
3239
- if (process.env.NODE_ENV !== "development" || hostname !== "127.0.0.1" && hostname !== "localhost") {
3240
- return new Response(null, { status: 404 });
3241
- }
3242
- const body = await request.text();
3243
- const actualSignature = request.headers.get(
3244
- "x-junior-local-credential-signature"
3245
- );
3246
- if (!actualSignature || !signaturesMatch(signature(body), actualSignature)) {
3247
- return Response.json({ error: "Unauthorized" }, { status: 401 });
3248
- }
3249
- let value;
3250
- try {
3251
- value = JSON.parse(body);
3252
- } catch {
3253
- return Response.json({ error: "Invalid request" }, { status: 400 });
3254
- }
3255
- const payload = localCredentialSyncSchema.safeParse(value);
3256
- if (!payload.success || Math.abs(Date.now() - payload.data.createdAtMs) > LOCAL_CREDENTIAL_SYNC_MAX_AGE_MS) {
3257
- return Response.json({ error: "Invalid request" }, { status: 400 });
3258
- }
3259
- await createUserTokenStore().set(
3260
- "local-cli",
3261
- payload.data.provider,
3262
- payload.data.tokens
3263
- );
3264
- return new Response(null, { status: 204 });
3265
- }
3266
-
3267
- export {
3268
- createSlackAdapterAssistantStatusSession,
3269
- splitSlackReplyText,
3270
- buildSlackOutputMessage,
3271
- sendSlackReply,
3272
- getSlackApiErrorCode,
3273
- getSlackErrorObservabilityAttributes,
3274
- isRetryableSlackPostError,
3275
- isSlackTitlePermissionError,
3276
- stripLeadingBotMention,
3277
- getThreadId,
3278
- getRunId,
3279
- getChannelId,
3280
- getThreadTs,
3281
- getAssistantThreadContext,
3282
- getMessageTs,
3283
- shouldKeepProcessingReactionForToolInvocation,
3284
- startProcessingReaction,
3285
- buildAuthPauseResponse,
3286
- resumeSlackTurn,
3287
- getPausedTurnRequest,
3288
- wakePausedTurn,
3289
- createPausedTurns,
3290
- GET,
3291
- publishAppHomeView,
3292
- GET2,
3293
- syncLocalOAuthCredential,
3294
- receiveLocalOAuthCredential
3295
- };