@sentry/junior 0.188.0 → 0.189.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 (109) hide show
  1. package/dist/{agent-hooks-5RQEVLQD.js → agent-hooks-6QRYH6PX.js} +11 -11
  2. package/dist/api/acp/conversations.d.ts +2 -2
  3. package/dist/api/conversations/list.d.ts +7 -0
  4. package/dist/api/people/shared.d.ts +54 -0
  5. package/dist/api.js +41 -52
  6. package/dist/app.d.ts +1 -1
  7. package/dist/app.js +1003 -346
  8. package/dist/{catalog-runtime-L4A6BJ3U.js → catalog-runtime-IETJLJFG.js} +4 -4
  9. package/dist/chat/agent/resume.d.ts +0 -2
  10. package/dist/chat/agent/types.d.ts +8 -9
  11. package/dist/chat/agent-dispatch/work.d.ts +1 -10
  12. package/dist/chat/agent-invocations/spawn.d.ts +1 -1
  13. package/dist/chat/agent-invocations/types.d.ts +296 -96
  14. package/dist/chat/app/conversation-work.d.ts +16 -8
  15. package/dist/chat/conversation-privacy.d.ts +7 -7
  16. package/dist/chat/conversations/destination.d.ts +12 -0
  17. package/dist/chat/conversations/projection.d.ts +2 -2
  18. package/dist/chat/conversations/sql/location.d.ts +13 -4
  19. package/dist/chat/{api-turns → conversations}/stop.d.ts +2 -2
  20. package/dist/chat/conversations/store.d.ts +4 -4
  21. package/dist/chat/conversations/structured-events.d.ts +1 -1
  22. package/dist/chat/{api-turns/authorization.d.ts → conversations/web-authorization.d.ts} +1 -0
  23. package/dist/chat/conversations/web-input.d.ts +98 -0
  24. package/dist/chat/conversations/web-mailbox.d.ts +12 -0
  25. package/dist/chat/event-tasks/tool-support.d.ts +0 -6
  26. package/dist/chat/providers/slack/runtime.d.ts +1 -0
  27. package/dist/chat/providers/slack/system-turn.d.ts +8 -0
  28. package/dist/chat/resource-events/actor.d.ts +13 -12
  29. package/dist/chat/resource-events/notification.d.ts +1 -1
  30. package/dist/chat/services/guardian-action-policy.d.ts +1 -1
  31. package/dist/chat/services/turn-session-routing.d.ts +2 -2
  32. package/dist/chat/task-execution/assistant-message.d.ts +32 -0
  33. package/dist/chat/task-execution/conversation-turn.d.ts +8 -0
  34. package/dist/chat/{api-turns → task-execution}/mailbox-input.d.ts +1 -2
  35. package/dist/chat/task-execution/mailbox-turn.d.ts +42 -0
  36. package/dist/chat/task-execution/paused-turn.d.ts +1 -1
  37. package/dist/chat/task-execution/slack-work.d.ts +0 -2
  38. package/dist/{chunk-AW77HH54.js → chunk-4RHJ3CXR.js} +14 -295
  39. package/dist/{chunk-ZX7RSBQ6.js → chunk-5CXCGDIL.js} +1 -1
  40. package/dist/{chunk-QVH6FW3T.js → chunk-5VWAY27R.js} +7 -3
  41. package/dist/{chunk-XJIPMHKI.js → chunk-6AUR6PWR.js} +3 -3
  42. package/dist/{chunk-QDHJTKPE.js → chunk-7R5JRPCY.js} +8 -6
  43. package/dist/{chunk-6JZGXLL2.js → chunk-AC7MDOJW.js} +5 -7
  44. package/dist/{chunk-Q7O74WOO.js → chunk-BYBSI5TI.js} +2 -2
  45. package/dist/{chunk-VFZAUQ5V.js → chunk-CMWIMFM6.js} +4 -182
  46. package/dist/{chunk-IUUQWIG2.js → chunk-CUYFCI4G.js} +59 -11
  47. package/dist/chunk-DA2KGZZI.js +13 -0
  48. package/dist/{chunk-FIUCHYT6.js → chunk-DYXVAO2E.js} +1 -1
  49. package/dist/{chunk-NZLZ4KYZ.js → chunk-E3PDZPGV.js} +1 -1
  50. package/dist/{chunk-Z5SKZE5B.js → chunk-GP32KD2J.js} +4 -12
  51. package/dist/{chunk-XI4PVY2T.js → chunk-KN32OWL2.js} +5 -5
  52. package/dist/chunk-LHKZKAZ4.js +285 -0
  53. package/dist/{chunk-6YGFBU3A.js → chunk-MFSET7Q7.js} +44 -334
  54. package/dist/{chunk-QCGNHSMA.js → chunk-MI6ZBDTS.js} +6 -6
  55. package/dist/{chunk-CIG6WI7Q.js → chunk-MRXMFB4L.js} +5 -5
  56. package/dist/{chunk-CVBDVIF4.js → chunk-MVPLEPSI.js} +1 -1
  57. package/dist/{chunk-32XSSFZV.js → chunk-NY2F7BZA.js} +1 -1
  58. package/dist/chunk-O35GZCKL.js +311 -0
  59. package/dist/{chunk-HCIVBCP5.js → chunk-OEIITG5F.js} +1 -1
  60. package/dist/{chunk-UVUXFRYN.js → chunk-PCPSX4ES.js} +209 -7
  61. package/dist/{chunk-QIMN3FXL.js → chunk-PY3EIEAF.js} +1 -1
  62. package/dist/{chunk-RMB7CERI.js → chunk-QNIOBNOY.js} +282 -121
  63. package/dist/{chunk-WGZJ5SU4.js → chunk-S3SGZUOI.js} +1 -1
  64. package/dist/chunk-SR6OMO5C.js +703 -0
  65. package/dist/{chunk-CDSLOYOR.js → chunk-UCWO5MKP.js} +6 -6
  66. package/dist/{chunk-PSHMPRIU.js → chunk-VKQWU4PO.js} +2 -2
  67. package/dist/{chunk-X55FOVZO.js → chunk-WDHPN2GH.js} +3 -3
  68. package/dist/{chunk-WW2Y7JIA.js → chunk-WMUAKAIF.js} +43 -44
  69. package/dist/{chunk-MQDIIFZM.js → chunk-WN6VLHOU.js} +12 -5
  70. package/dist/{chunk-W42ENMJW.js → chunk-WWYUYKKV.js} +1 -1
  71. package/dist/{chunk-ERZMACYA.js → chunk-WYDF6PGK.js} +3 -3
  72. package/dist/{chunk-SC67CH3C.js → chunk-YNZPOFF3.js} +14 -17
  73. package/dist/{chunk-3HEZWDS5.js → chunk-YYSEH2WC.js} +2 -2
  74. package/dist/{chunk-W2DDJCPS.js → chunk-YYZNESC3.js} +8 -8
  75. package/dist/{chunk-CZRWU4ZM.js → chunk-ZUPDSJUE.js} +2 -5
  76. package/dist/{chunk-QIOUTXTZ.js → chunk-ZV6PFUA4.js} +1 -1
  77. package/dist/cli/chat.js +34 -33
  78. package/dist/cli/check.js +5 -5
  79. package/dist/cli/plugins.js +12 -12
  80. package/dist/cli/snapshot-create.js +9 -9
  81. package/dist/cli/upgrade.js +6 -6
  82. package/dist/db/schema/agent-invocations.d.ts +0 -36
  83. package/dist/db/schema/conversations.d.ts +62 -0
  84. package/dist/db/schema.d.ts +31 -36
  85. package/dist/{db-YPRZAA2T.js → db-LPJKR2EO.js} +6 -6
  86. package/dist/instrumentation.js +3 -3
  87. package/dist/nitro.js +6 -6
  88. package/dist/{oauth-callback-server-DBBIR3XG.js → oauth-callback-server-DMXF4TTQ.js} +30 -28
  89. package/dist/{runner-FKVFFPLO.js → runner-L5DPVYCN.js} +25 -21
  90. package/dist/{skills-4RBAZEMV.js → skills-NXG3LBCK.js} +5 -5
  91. package/dist/spawn-VRUIGKWW.js +42 -0
  92. package/dist/task-runner-7YTBSPXW.js +36 -0
  93. package/dist/{validation-E6Z5LHZM.js → validation-CH36LTOS.js} +5 -5
  94. package/dist/version.js +1 -1
  95. package/dist/{work-OFTXWOK4.js → work-HBC5Y5VZ.js} +22 -21
  96. package/dist/{worker-OMWNOKTJ.js → worker-5RDZHLXH.js} +10 -10
  97. package/migrations/0037_conversation_location.sql +23 -0
  98. package/migrations/meta/0037_snapshot.json +3192 -0
  99. package/migrations/meta/_journal.json +7 -0
  100. package/package.json +5 -5
  101. package/dist/chat/api-turns/assistant-message.d.ts +0 -12
  102. package/dist/chat/api-turns/cancellation.d.ts +0 -24
  103. package/dist/chat/api-turns/routing.d.ts +0 -51
  104. package/dist/chat/api-turns/work.d.ts +0 -99
  105. package/dist/chat/task-execution/slack-resource-event.d.ts +0 -55
  106. package/dist/chunk-6FGN7ZEG.js +0 -221
  107. package/dist/chunk-LODJKINF.js +0 -1502
  108. package/dist/spawn-CO7QFCNJ.js +0 -41
  109. package/dist/task-runner-DUGZGI7V.js +0 -34
@@ -1,1502 +0,0 @@
1
- import {
2
- createWebAuthorization,
3
- deleteWebAuthorization
4
- } from "./chunk-PSHMPRIU.js";
5
- import {
6
- AgentRunError,
7
- executeTurn
8
- } from "./chunk-ERZMACYA.js";
9
- import {
10
- buildDeliveredTurnStatePatch,
11
- completeAuthPauseTurn,
12
- finalizeFailedTurnReplyWithEvent,
13
- getTurnUserMessage
14
- } from "./chunk-QDHJTKPE.js";
15
- import {
16
- processPluginTask,
17
- scheduleSessionCompletedPluginTasks
18
- } from "./chunk-AW77HH54.js";
19
- import {
20
- ConversationTurnLifecycleService
21
- } from "./chunk-W42ENMJW.js";
22
- import {
23
- coerceThreadConversationState,
24
- getPersistedSandboxState,
25
- getPersistedThreadState,
26
- persistThreadStateById
27
- } from "./chunk-CIG6WI7Q.js";
28
- import {
29
- healthReportSchema
30
- } from "./chunk-UTL3MHPC.js";
31
- import {
32
- RESOURCE_EVENT_MESSAGE_AUTHOR,
33
- RESOURCE_EVENT_SYSTEM_ACTOR,
34
- isResourceEventConversationMessage,
35
- isResourceEventMailboxMetadata
36
- } from "./chunk-VFZAUQ5V.js";
37
- import {
38
- parseSlackThreadId
39
- } from "./chunk-CVBDVIF4.js";
40
- import {
41
- appendAndEnqueueExclusiveInboundMessage,
42
- appendAndEnqueueInboundMessage
43
- } from "./chunk-MQDIIFZM.js";
44
- import {
45
- TurnInputCommitLostError,
46
- buildDeterministicTurnId,
47
- clearPendingAuth,
48
- getAssistantReplyText,
49
- getConversationTurnBoundaryError,
50
- isTurnInputCommitLostError,
51
- markTurnClosed,
52
- markTurnFailed,
53
- startActiveTurn
54
- } from "./chunk-Z5SKZE5B.js";
55
- import {
56
- abandonTurnRecord,
57
- buildConversationContext,
58
- commitAcceptedReply,
59
- getTurnRecord,
60
- hydrateConversationMessages,
61
- listTurnSummaries,
62
- loadProjection,
63
- markConversationMessage,
64
- normalizeConversationText,
65
- persistConversationMessages,
66
- persistWithRetry,
67
- recordDeliveredAssistantMessage,
68
- saveTurnCheckpoint,
69
- upsertConversationMessage
70
- } from "./chunk-6YGFBU3A.js";
71
- import {
72
- normalizeSlackConversationId
73
- } from "./chunk-QCGNHSMA.js";
74
- import {
75
- getConversationEventStore,
76
- getConversationStore,
77
- getDb
78
- } from "./chunk-IUUQWIG2.js";
79
- import {
80
- juniorConversationParticipants,
81
- juniorConversations,
82
- juniorIdentities,
83
- juniorUsers
84
- } from "./chunk-QVH6FW3T.js";
85
- import {
86
- juniorDestinations
87
- } from "./chunk-VFUK3X5B.js";
88
- import {
89
- getSlackBotToken
90
- } from "./chunk-Q7O74WOO.js";
91
- import {
92
- canExposeConversationPayload
93
- } from "./chunk-FIUCHYT6.js";
94
- import {
95
- credentialContextForActor
96
- } from "./chunk-CEMMLV5L.js";
97
- import {
98
- createActor
99
- } from "./chunk-N7TMSOZH.js";
100
- import {
101
- logException,
102
- logWarn,
103
- setTags,
104
- withLogContext
105
- } from "./chunk-QIMN3FXL.js";
106
-
107
- // src/handlers/health.ts
108
- function readHealthReport() {
109
- return healthReportSchema.parse({
110
- status: "ok",
111
- service: "junior",
112
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
113
- });
114
- }
115
- function GET() {
116
- return Response.json(readHealthReport());
117
- }
118
-
119
- // src/chat/attachments/vercel.ts
120
- import { del, get, put } from "@vercel/blob";
121
- function createVercelAttachmentStorage() {
122
- return {
123
- provider: "vercel-blob",
124
- async put(input) {
125
- await put(input.key, input.body, {
126
- access: "private",
127
- addRandomSuffix: false,
128
- allowOverwrite: true,
129
- contentType: input.contentType
130
- });
131
- },
132
- async get(key) {
133
- const result = await get(key, { access: "private" });
134
- return result?.statusCode === 200 ? result.stream : null;
135
- },
136
- async delete(keys) {
137
- if (keys.length === 0) return;
138
- await del(keys);
139
- }
140
- };
141
- }
142
-
143
- // src/chat/api-turns/routing.ts
144
- import { z } from "zod";
145
- import {
146
- createLocalSource,
147
- createWebSource
148
- } from "@sentry/junior-plugin-api";
149
- var apiTurnMailboxMetadataSchema = z.object({
150
- authorEmail: z.string().email(),
151
- authorFullName: z.string().min(1).optional(),
152
- authorUserId: z.string().min(1),
153
- authorUserName: z.string().min(1).optional(),
154
- kind: z.literal("api_turn"),
155
- messageId: z.string().min(1)
156
- }).strict();
157
- function turnIdentityFromConversationMessage(message) {
158
- if (isResourceEventConversationMessage(message)) {
159
- return {
160
- actor: RESOURCE_EVENT_SYSTEM_ACTOR,
161
- author: message.author ?? RESOURCE_EVENT_MESSAGE_AUTHOR,
162
- source: "resource_event"
163
- };
164
- }
165
- const actor = createActor(message.author, {
166
- platform: "web",
167
- userId: message.author?.userId
168
- });
169
- if (actor?.platform !== "web") {
170
- return void 0;
171
- }
172
- return {
173
- actor,
174
- author: message.author ?? { userId: actor.userId },
175
- source: "web"
176
- };
177
- }
178
- function sourceFromTurnInput(args) {
179
- switch (args.source) {
180
- case "resource_event":
181
- return createLocalSource(args.conversationId);
182
- case "web":
183
- return createWebSource(
184
- args.conversationId,
185
- args.visibility === "private" ? "private" : "public"
186
- );
187
- }
188
- }
189
- async function getActiveApiTurnId(conversationId) {
190
- const summaries = await listTurnSummaries(conversationId);
191
- const active = summaries.filter(
192
- (summary) => summary.surface === "api" && !summary.dispatchId && (summary.state === "paused" || summary.state === "running")
193
- );
194
- if (active.length > 1) {
195
- throw new Error(
196
- `Conversation ${conversationId} has multiple active web turns`
197
- );
198
- }
199
- const turnId = active[0]?.turnId;
200
- if (!turnId) return void 0;
201
- const record = await getTurnRecord(conversationId, turnId);
202
- return record && record.surface === "api" && !record.dispatchId && (record.state === "paused" || record.state === "running") ? turnId : void 0;
203
- }
204
- async function getAuthPausedApiTurnId(conversationId) {
205
- const turnId = await getActiveApiTurnId(conversationId);
206
- if (!turnId) return void 0;
207
- const record = await getTurnRecord(conversationId, turnId);
208
- return record?.state === "paused" && record.resumeReason === "auth" ? turnId : void 0;
209
- }
210
- function parseApiTurnMessages(messages) {
211
- if (messages.length === 0) {
212
- return [];
213
- }
214
- const parsed = messages.map((message) => ({
215
- message,
216
- metadata: apiTurnMailboxMetadataSchema.safeParse(message.input.metadata)
217
- }));
218
- if (parsed.every((entry) => !entry.metadata.success)) {
219
- return [];
220
- }
221
- if (parsed.some((entry) => !entry.metadata.success)) {
222
- throw new Error("A Turn cannot combine different kinds of input");
223
- }
224
- return parsed.map((entry) => {
225
- if (!entry.metadata.success) {
226
- throw new Error("User message has invalid metadata");
227
- }
228
- const metadata = entry.metadata.data;
229
- const actor = {
230
- platform: "web",
231
- userId: metadata.authorUserId,
232
- email: metadata.authorEmail.trim().toLowerCase(),
233
- ...metadata.authorFullName ? { fullName: metadata.authorFullName } : void 0,
234
- ...metadata.authorUserName ? { userName: metadata.authorUserName } : void 0
235
- };
236
- return {
237
- actor,
238
- author: {
239
- email: actor.email,
240
- ...actor.fullName ? { fullName: actor.fullName } : void 0,
241
- userId: actor.userId,
242
- ...actor.userName ? { userName: actor.userName } : void 0
243
- },
244
- message: entry.message,
245
- source: "web"
246
- };
247
- });
248
- }
249
- function parseResourceEventMessages(messages) {
250
- if (messages.length === 0) {
251
- return [];
252
- }
253
- if (!messages.every(
254
- (message) => message.source === "resource_event" && isResourceEventMailboxMetadata(message.input.metadata)
255
- )) {
256
- return [];
257
- }
258
- return messages.map((message) => {
259
- if (!isResourceEventMailboxMetadata(message.input.metadata)) {
260
- throw new Error("Resource event has invalid metadata");
261
- }
262
- return {
263
- actor: RESOURCE_EVENT_SYSTEM_ACTOR,
264
- author: RESOURCE_EVENT_MESSAGE_AUTHOR,
265
- message,
266
- source: "resource_event"
267
- };
268
- });
269
- }
270
- async function resolveApiTurnWork(context) {
271
- const batch = parseApiTurnMessages(context.attempt.messages);
272
- if (batch.length > 0) {
273
- return { kind: "mailbox", batch };
274
- }
275
- if (context.destination?.platform === "local") {
276
- const resourceBatch = parseResourceEventMessages(context.attempt.messages);
277
- if (resourceBatch.length > 0) {
278
- return { kind: "mailbox", batch: resourceBatch };
279
- }
280
- }
281
- if (context.attempt.messages.length > 0) {
282
- return void 0;
283
- }
284
- const turnId = await getActiveApiTurnId(context.conversationId);
285
- return turnId ? { kind: "resume", turnId } : void 0;
286
- }
287
- async function isApiTurnWork(context) {
288
- return await resolveApiTurnWork(context) !== void 0;
289
- }
290
-
291
- // src/chat/api-turns/work.ts
292
- import { createHash } from "crypto";
293
- import {
294
- localDestinationSchema
295
- } from "@sentry/junior-plugin-api";
296
-
297
- // src/chat/api-turns/assistant-message.ts
298
- async function commitAssistantMessage(args) {
299
- const conversationMessageId = recordDeliveredAssistantMessage({
300
- conversation: args.conversation,
301
- sessionId: args.sessionId,
302
- source: args.source,
303
- text: args.text,
304
- userMessageId: args.userMessageId
305
- });
306
- try {
307
- await persistWithRetry(
308
- () => commitAcceptedReply({
309
- ...args.agentMessage ? { agentMessage: args.agentMessage } : void 0,
310
- conversation: args.conversation,
311
- conversationMessageId,
312
- conversationId: args.conversationId
313
- })
314
- );
315
- } catch (error) {
316
- logException(
317
- new Error("Assistant message persistence failed"),
318
- "conversation.assistant.message_persist.failed",
319
- {
320
- "error.type": error instanceof Error ? error.name : typeof error
321
- }
322
- );
323
- }
324
- }
325
-
326
- // src/chat/api-turns/cancellation.ts
327
- function createApiTurnCancellation() {
328
- const active = /* @__PURE__ */ new Map();
329
- return {
330
- begin(conversationId) {
331
- if (active.has(conversationId)) {
332
- return void 0;
333
- }
334
- const controller = new AbortController();
335
- active.set(conversationId, {
336
- connected: true,
337
- controller,
338
- parked: false
339
- });
340
- return controller.signal;
341
- },
342
- cancel(conversationId) {
343
- const entry = active.get(conversationId);
344
- if (!entry) {
345
- return false;
346
- }
347
- entry.controller.abort(new DOMException("Turn cancelled", "AbortError"));
348
- return true;
349
- },
350
- disconnect(conversationId, signal) {
351
- const entry = active.get(conversationId);
352
- if (entry?.controller.signal !== signal) {
353
- return;
354
- }
355
- entry.connected = false;
356
- if (entry.parked) {
357
- active.delete(conversationId);
358
- }
359
- },
360
- finish(conversationId, signal) {
361
- const entry = active.get(conversationId);
362
- if (entry?.controller.signal === signal) {
363
- active.delete(conversationId);
364
- }
365
- },
366
- park(conversationId, signal) {
367
- const entry = active.get(conversationId);
368
- if (entry?.controller.signal !== signal) {
369
- return;
370
- }
371
- entry.parked = true;
372
- if (!entry.connected) {
373
- active.delete(conversationId);
374
- }
375
- },
376
- signal(conversationId) {
377
- return active.get(conversationId)?.controller.signal;
378
- }
379
- };
380
- }
381
- async function completeCancelledApiTurn(args) {
382
- try {
383
- const pendingAuthorization = args.conversation.processing.pendingAuth?.sessionId === args.turnId ? args.conversation.processing.pendingAuth : void 0;
384
- await abandonTurnRecord({
385
- conversationId: args.conversationId,
386
- turnId: args.turnId,
387
- errorMessage: "Turn cancelled"
388
- });
389
- clearPendingAuth(args.conversation, args.turnId);
390
- markConversationMessage(args.conversation, args.userMessageId, {
391
- replied: false,
392
- skippedReason: "turn cancelled"
393
- });
394
- markTurnClosed({
395
- conversation: args.conversation,
396
- nowMs: Date.now(),
397
- sessionId: args.turnId
398
- });
399
- if (pendingAuthorization) {
400
- await deleteWebAuthorization({
401
- actorId: pendingAuthorization.actorId,
402
- conversationId: args.conversationId
403
- });
404
- }
405
- await persistThreadStateById(args.conversationId, {
406
- conversation: args.conversation,
407
- sandboxRef: args.sandboxRef
408
- });
409
- await new ConversationTurnLifecycleService(
410
- getConversationEventStore()
411
- ).complete({
412
- conversationId: args.conversationId,
413
- createdAtMs: Date.now(),
414
- outcome: "cancelled",
415
- turnId: args.turnId
416
- });
417
- } finally {
418
- if (args.signal) {
419
- args.cancellation?.finish(args.conversationId, args.signal);
420
- }
421
- }
422
- await args.acknowledge();
423
- }
424
-
425
- // src/chat/api-turns/mailbox-input.ts
426
- function joinMailboxText(messages) {
427
- return messages.map((message) => message.input.text.trim()).filter(Boolean).join("\n\n");
428
- }
429
-
430
- // src/chat/api-turns/work.ts
431
- function normalizeEmail(email) {
432
- return email.trim().toLowerCase();
433
- }
434
- function stableHex(...parts) {
435
- return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
436
- }
437
- function createApiConversationId(args) {
438
- return `local:web:${stableHex(
439
- normalizeEmail(args.actorEmail),
440
- args.idempotencyKey
441
- )}`;
442
- }
443
- function apiConversationMessageId(args) {
444
- return `api-msg:${stableHex(args.conversationId, args.idempotencyKey)}`;
445
- }
446
- function apiTurnIdForMessage(messageId) {
447
- return buildDeterministicTurnId(messageId);
448
- }
449
- function requireLocalDestination(conversationId) {
450
- const parsed = localDestinationSchema.safeParse({
451
- platform: "local",
452
- conversationId
453
- });
454
- if (!parsed.success) {
455
- throw new Error(`Invalid local conversation id: ${conversationId}`);
456
- }
457
- return parsed.data;
458
- }
459
- function resolveApiTurnDestination(args) {
460
- if (args.existing) {
461
- return args.existing;
462
- }
463
- return requireLocalDestination(args.conversationId);
464
- }
465
- function storedActorFromApi(actor) {
466
- return {
467
- ...actor.email ? { email: normalizeEmail(actor.email) } : void 0,
468
- ...actor.fullName ? { fullName: actor.fullName } : void 0
469
- };
470
- }
471
- function webActorFromEmail(email, profile) {
472
- const normalized = normalizeEmail(email);
473
- return {
474
- platform: "web",
475
- userId: `dashboard:${stableHex(normalized)}`,
476
- email: normalized,
477
- ...profile?.fullName ? { fullName: profile.fullName } : void 0,
478
- ...profile?.userName ? { userName: profile.userName } : void 0
479
- };
480
- }
481
- function buildApiTurnInboundMessage(args) {
482
- const text = args.message.trim();
483
- if (!text) {
484
- throw new Error("API conversation message must not be empty");
485
- }
486
- if (!args.actor.email) {
487
- throw new Error("API conversation actor requires a verified email");
488
- }
489
- const destination = resolveApiTurnDestination({
490
- conversationId: args.conversationId,
491
- existing: args.destination
492
- });
493
- const nowMs = args.nowMs ?? Date.now();
494
- return {
495
- conversationId: args.conversationId,
496
- createdAtMs: args.createdAtMs ?? nowMs,
497
- delivery: "defer",
498
- destination,
499
- inboundMessageId: args.messageId,
500
- input: {
501
- authorId: args.actor.userId,
502
- text,
503
- metadata: {
504
- authorEmail: normalizeEmail(args.actor.email),
505
- ...args.actor.fullName && { authorFullName: args.actor.fullName },
506
- authorUserId: args.actor.userId,
507
- ...args.actor.userName && { authorUserName: args.actor.userName },
508
- kind: "api_turn",
509
- messageId: args.messageId
510
- }
511
- },
512
- receivedAtMs: nowMs,
513
- publishExternally: false,
514
- source: "web"
515
- };
516
- }
517
- async function recordApiConversationActivity(args) {
518
- const store = args.conversationStore ?? getConversationStore();
519
- const existing = await store.get({ conversationId: args.conversationId });
520
- const destination = resolveApiTurnDestination({
521
- conversationId: args.conversationId,
522
- existing: existing?.destination
523
- });
524
- const isNewRoot = !existing;
525
- const visibility = args.rootVisibility === "private" ? "private" : "public";
526
- const source = isNewRoot ? sourceFromTurnInput({
527
- conversationId: args.conversationId,
528
- source: "web",
529
- visibility
530
- }) : void 0;
531
- await store.recordActivity({
532
- conversationId: args.conversationId,
533
- destination,
534
- nowMs: args.nowMs,
535
- actor: storedActorFromApi(args.actor),
536
- // Do not rewrite a Slack root's origin source when a dashboard participant
537
- // continues it. Mailbox entries still carry source "web" per turn.
538
- ...isNewRoot ? { source: "web" } : void 0,
539
- ...source ? { sessionSource: source } : void 0,
540
- ...isNewRoot ? { visibility } : void 0
541
- });
542
- return destination;
543
- }
544
- async function createAndEnqueueApiConversation(input, options) {
545
- if (!input.actor.email) {
546
- throw new Error("API conversation actor requires a verified email");
547
- }
548
- const conversationId = createApiConversationId({
549
- actorEmail: input.actor.email,
550
- idempotencyKey: input.idempotencyKey
551
- });
552
- return await appendAndEnqueueApiConversationMessage(
553
- {
554
- actor: input.actor,
555
- conversationId,
556
- idempotencyKey: input.idempotencyKey,
557
- message: input.message,
558
- rootVisibility: input.visibility === "private" ? "private" : "public"
559
- },
560
- options
561
- );
562
- }
563
- async function appendAndEnqueueApiConversationMessage(input, options) {
564
- const text = input.message.trim();
565
- if (!text) {
566
- throw new Error("API conversation message must not be empty");
567
- }
568
- if (!input.actor.email) {
569
- throw new Error("API conversation actor requires a verified email");
570
- }
571
- const nowMs = options.nowMs ?? Date.now();
572
- const messageId = apiConversationMessageId({
573
- conversationId: input.conversationId,
574
- idempotencyKey: input.idempotencyKey
575
- });
576
- const destination = await recordApiConversationActivity({
577
- actor: input.actor,
578
- conversationId: input.conversationId,
579
- conversationStore: options.conversationStore,
580
- nowMs,
581
- ...input.rootVisibility && { rootVisibility: input.rootVisibility }
582
- });
583
- const enqueue = options.exclusive ? appendAndEnqueueExclusiveInboundMessage : appendAndEnqueueInboundMessage;
584
- const result = await enqueue({
585
- message: buildApiTurnInboundMessage({
586
- actor: input.actor,
587
- conversationId: input.conversationId,
588
- destination,
589
- message: text,
590
- messageId,
591
- nowMs
592
- }),
593
- conversationStore: options.conversationStore,
594
- nowMs,
595
- queue: options.queue,
596
- state: options.state
597
- });
598
- const status = result.status === "appended" ? "accepted" : result.status;
599
- return {
600
- conversationId: input.conversationId,
601
- messageId,
602
- status
603
- };
604
- }
605
- function captureApiBoundaryFailure(args) {
606
- const eventId = logException(args.error, `api.turn.${args.failureCode}`, {
607
- conversationId: args.conversationId,
608
- ...args.runId ? { runId: args.runId } : void 0,
609
- turnId: args.turnId
610
- });
611
- return typeof eventId === "string" ? eventId : void 0;
612
- }
613
- function hasLostTurnInputCommit(error) {
614
- const cause = getConversationTurnBoundaryError(error)?.cause ?? error;
615
- return isTurnInputCommitLostError(error) || isTurnInputCommitLostError(cause);
616
- }
617
- function createApiTurnWorker(agentRunner, cancellation) {
618
- return async (context) => {
619
- const resolved = await resolveApiTurnWork(context);
620
- if (!resolved) {
621
- throw new Error(
622
- `Unsupported input for Conversation ${context.conversationId}`
623
- );
624
- }
625
- if (context.publishExternally) {
626
- throw new Error(
627
- `publishExternally must be false for Conversation ${context.conversationId}`
628
- );
629
- }
630
- const lifecycle = new ConversationTurnLifecycleService(
631
- getConversationEventStore()
632
- );
633
- const isResume = resolved.kind === "resume";
634
- let turnIdentity;
635
- let text = "";
636
- let turnId = "";
637
- let userMessageId = "";
638
- let startedAtMs = Date.now();
639
- let inputMessageIds = [];
640
- const storedConversation = await getConversationStore().get({
641
- conversationId: context.conversationId
642
- });
643
- const mailboxDestination = resolved.kind === "mailbox" ? resolved.batch[0]?.message.destination : void 0;
644
- const destination = resolveApiTurnDestination({
645
- conversationId: context.conversationId,
646
- existing: context.destination ?? mailboxDestination ?? storedConversation?.destination
647
- });
648
- if (resolved.kind === "mailbox") {
649
- const first = resolved.batch[0];
650
- text = joinMailboxText(resolved.batch.map((entry) => entry.message));
651
- startedAtMs = first.message.createdAtMs;
652
- userMessageId = first.message.inboundMessageId;
653
- turnId = apiTurnIdForMessage(userMessageId);
654
- inputMessageIds = resolved.batch.map(
655
- (entry) => entry.message.inboundMessageId
656
- );
657
- turnIdentity = first;
658
- } else {
659
- turnId = resolved.turnId;
660
- }
661
- const persisted = await getPersistedThreadState(context.conversationId);
662
- const conversation = coerceThreadConversationState(persisted);
663
- await hydrateConversationMessages({
664
- conversation,
665
- conversationId: context.conversationId
666
- });
667
- if (isResume) {
668
- const userMessage = getTurnUserMessage(conversation, turnId);
669
- if (!userMessage) {
670
- throw new Error(
671
- `Unable to locate the persisted user message for Turn "${turnId}"`
672
- );
673
- }
674
- turnIdentity = turnIdentityFromConversationMessage(userMessage);
675
- userMessageId = userMessage.id;
676
- text = userMessage.text;
677
- startedAtMs = userMessage.createdAtMs;
678
- inputMessageIds = [userMessageId];
679
- }
680
- if (!turnIdentity) {
681
- throw new Error(
682
- `Resumed Turn is missing an Actor for Conversation ${context.conversationId}`
683
- );
684
- }
685
- const { actor, author } = turnIdentity;
686
- const source = sourceFromTurnInput({
687
- conversationId: context.conversationId,
688
- source: turnIdentity.source,
689
- visibility: storedConversation?.visibility
690
- });
691
- const webActor = actor.platform === "web" ? actor : void 0;
692
- return await withLogContext(
693
- {
694
- conversationId: context.conversationId,
695
- platform: actor.platform,
696
- ...webActor ? {
697
- userId: webActor.userId,
698
- ...webActor.userName ? { userName: webActor.userName } : void 0
699
- } : void 0
700
- },
701
- async () => {
702
- let acknowledged = isResume || context.attempt.messages.length === 0;
703
- const acknowledge = async () => {
704
- if (acknowledged) {
705
- return;
706
- }
707
- try {
708
- await context.attempt.ack();
709
- } catch {
710
- throw new TurnInputCommitLostError(
711
- `Conversation work lease lost before Conversation API message acknowledgement for ${context.conversationId}`
712
- );
713
- }
714
- acknowledged = true;
715
- };
716
- let sandboxRef = getPersistedSandboxState(persisted);
717
- const initialSandboxRef = sandboxRef;
718
- const appSignal = cancellation?.signal(context.conversationId);
719
- const stopSignal = context.stopSignal?.();
720
- const cancellationSignal = appSignal && stopSignal ? AbortSignal.any([appSignal, stopSignal]) : appSignal ?? stopSignal;
721
- const finishCancellation = () => {
722
- if (appSignal) {
723
- cancellation?.finish(context.conversationId, appSignal);
724
- }
725
- };
726
- const completeCancelledTurn = async () => {
727
- try {
728
- await completeCancelledApiTurn({
729
- acknowledge,
730
- cancellation,
731
- conversation,
732
- conversationId: context.conversationId,
733
- sandboxRef,
734
- signal: appSignal,
735
- turnId,
736
- userMessageId
737
- });
738
- } catch (error) {
739
- if (hasLostTurnInputCommit(error)) {
740
- return { status: "lost_lease" };
741
- }
742
- throw error;
743
- }
744
- return { status: "completed" };
745
- };
746
- if (isResume) {
747
- if (cancellationSignal?.aborted) {
748
- return await completeCancelledTurn();
749
- }
750
- } else {
751
- upsertConversationMessage(conversation, {
752
- id: userMessageId,
753
- role: "user",
754
- text: normalizeConversationText(text),
755
- createdAtMs: startedAtMs,
756
- author,
757
- meta: {
758
- explicitMention: true,
759
- replied: false,
760
- ...turnIdentity.source === "web" ? { source: "web" } : void 0
761
- }
762
- });
763
- await persistConversationMessages({
764
- conversation,
765
- conversationId: context.conversationId
766
- });
767
- await lifecycle.start({
768
- conversationId: context.conversationId,
769
- createdAtMs: Date.now(),
770
- inputMessageIds,
771
- surface: "api",
772
- turnId
773
- });
774
- if (cancellationSignal?.aborted) {
775
- return await completeCancelledTurn();
776
- }
777
- startActiveTurn({
778
- conversation,
779
- nextTurnId: turnId
780
- });
781
- }
782
- let currentRunId;
783
- let assistantMessageDelivered = false;
784
- let failureCode = "persistence_failed";
785
- let modelFailureEventId;
786
- let modelFailureCaptureAttempted = false;
787
- let completedSuccessfully = false;
788
- const deliverAssistantMessage = async (value) => {
789
- const agentMessage = typeof value === "string" ? void 0 : value;
790
- const replyText = typeof value === "string" ? value : getAssistantReplyText(value);
791
- if (!replyText?.trim()) {
792
- return;
793
- }
794
- failureCode = "delivery_failed";
795
- assistantMessageDelivered = true;
796
- await commitAssistantMessage({
797
- ...agentMessage ? { agentMessage } : void 0,
798
- conversation,
799
- conversationId: context.conversationId,
800
- sessionId: turnId,
801
- ...turnIdentity.source === "web" ? { source: "web" } : void 0,
802
- text: replyText,
803
- userMessageId
804
- });
805
- failureCode = "agent_run_failed";
806
- };
807
- try {
808
- if (cancellationSignal?.aborted) {
809
- return await completeCancelledTurn();
810
- }
811
- if (!isResume) {
812
- const pendingAuthorizationActorId = conversation.processing.pendingAuth?.actorId;
813
- const authParked = (await listTurnSummaries(context.conversationId)).filter(
814
- (summary) => summary.surface === "api" && !summary.dispatchId && summary.state === "paused" && summary.resumeReason === "auth"
815
- );
816
- for (const parked of authParked) {
817
- if (cancellationSignal?.aborted) {
818
- return await completeCancelledTurn();
819
- }
820
- await abandonTurnRecord({
821
- conversationId: context.conversationId,
822
- turnId: parked.turnId,
823
- errorMessage: "Auth-paused Turn superseded by new input"
824
- });
825
- markTurnClosed({
826
- conversation,
827
- nowMs: Date.now(),
828
- sessionId: parked.turnId
829
- });
830
- }
831
- if (authParked.length > 0 && pendingAuthorizationActorId) {
832
- await deleteWebAuthorization({
833
- actorId: pendingAuthorizationActorId,
834
- conversationId: context.conversationId
835
- });
836
- }
837
- }
838
- await persistThreadStateById(context.conversationId, {
839
- conversation
840
- });
841
- if (cancellationSignal?.aborted) {
842
- return await completeCancelledTurn();
843
- }
844
- const piMessages = await loadProjection({
845
- conversationId: context.conversationId
846
- });
847
- failureCode = "agent_run_failed";
848
- currentRunId = `api-run:${stableHex(turnId, String(startedAtMs))}`;
849
- setTags({ runId: currentRunId });
850
- const outcome = await executeTurn(
851
- agentRunner,
852
- {
853
- conversationId: context.conversationId,
854
- turnId,
855
- runId: currentRunId,
856
- instruction: {
857
- text,
858
- context: buildConversationContext(conversation, {
859
- excludeMessageId: userMessageId
860
- })
861
- },
862
- history: piMessages,
863
- actor,
864
- credentialContext: credentialContextForActor(actor),
865
- destination,
866
- publishExternally: false,
867
- source: storedConversation?.location ? { ...source, location: storedConversation.location } : source,
868
- surface: "api",
869
- ...cancellationSignal ? { signal: cancellationSignal } : void 0,
870
- ...webActor ? {
871
- authorization: createWebAuthorization({
872
- actorId: webActor.userId,
873
- conversationId: context.conversationId
874
- })
875
- } : { disabledFeatures: ["interactive-auth"] },
876
- state: {
877
- pendingAuth: conversation.processing.pendingAuth,
878
- sandboxRef
879
- },
880
- delivery: { send: deliverAssistantMessage },
881
- durability: {
882
- onInputCommitted: acknowledge,
883
- shouldYield: context.shouldYield,
884
- onSandboxRefChanged: async (nextSandboxRef) => {
885
- sandboxRef = nextSandboxRef;
886
- await persistThreadStateById(context.conversationId, {
887
- conversation,
888
- sandboxRef
889
- });
890
- },
891
- recordPendingAuth: async (pendingAuth) => {
892
- conversation.processing.pendingAuth = pendingAuth;
893
- await persistThreadStateById(context.conversationId, {
894
- conversation,
895
- sandboxRef
896
- });
897
- }
898
- }
899
- },
900
- async (result) => {
901
- if (cancellationSignal?.aborted) {
902
- throw cancellationSignal.reason ?? new DOMException("Turn cancelled", "AbortError");
903
- }
904
- finishCancellation();
905
- modelFailureCaptureAttempted = result.diagnostics.outcome !== "success";
906
- const finalized = finalizeFailedTurnReplyWithEvent({
907
- reply: result,
908
- logException
909
- });
910
- const modelFailureReason = finalized.failureReason;
911
- const reply = finalized.reply;
912
- modelFailureEventId = finalized.eventId;
913
- if (reply.diagnostics.outcome !== "success") {
914
- await deliverAssistantMessage(reply.text);
915
- }
916
- const completedState = buildDeliveredTurnStatePatch({
917
- conversation,
918
- reply,
919
- sessionId: turnId,
920
- userMessageId
921
- });
922
- await persistThreadStateById(context.conversationId, {
923
- conversation: completedState.conversation,
924
- sandboxRef: reply.sandboxRef ?? sandboxRef
925
- });
926
- if (reply.piMessages?.length) {
927
- const latest = await getTurnRecord(
928
- context.conversationId,
929
- turnId
930
- );
931
- await saveTurnCheckpoint({
932
- mode: "completed",
933
- conversationId: context.conversationId,
934
- turnId,
935
- sliceId: latest?.sliceId ?? 1,
936
- messages: reply.piMessages,
937
- durationMs: reply.diagnostics.durationMs,
938
- usage: reply.diagnostics.usage,
939
- destination,
940
- publishExternally: false,
941
- source,
942
- actor,
943
- surface: "api"
944
- });
945
- }
946
- completedSuccessfully = reply.diagnostics.outcome === "success";
947
- return completedSuccessfully ? {
948
- outcome: assistantMessageDelivered ? "success" : "no_reply"
949
- } : {
950
- ...modelFailureEventId ? { eventId: modelFailureEventId } : void 0,
951
- failureCode: "model_execution_failed",
952
- ...modelFailureReason ? { failureReason: modelFailureReason } : void 0,
953
- outcome: "failed"
954
- };
955
- }
956
- );
957
- if (outcome.status !== "completed" && cancellationSignal?.aborted) {
958
- return await completeCancelledTurn();
959
- }
960
- if (outcome.status === "suspended") {
961
- return { status: "yielded" };
962
- }
963
- if (outcome.status === "awaiting_auth") {
964
- completeAuthPauseTurn({
965
- conversation,
966
- sessionId: turnId
967
- });
968
- await persistThreadStateById(context.conversationId, {
969
- conversation,
970
- sandboxRef
971
- });
972
- if (appSignal) {
973
- cancellation?.park(context.conversationId, appSignal);
974
- }
975
- await acknowledge();
976
- return { status: "paused" };
977
- }
978
- if (completedSuccessfully) {
979
- try {
980
- await scheduleSessionCompletedPluginTasks(
981
- {
982
- conversationId: context.conversationId,
983
- sessionId: turnId
984
- },
985
- {
986
- send: async (message) => {
987
- try {
988
- await processPluginTask(message);
989
- } catch (error) {
990
- logException(
991
- error,
992
- "api.plugin.session_completion_task.failed",
993
- {
994
- conversationId: context.conversationId,
995
- pluginName: message.plugin,
996
- taskName: message.name,
997
- turnId
998
- }
999
- );
1000
- }
1001
- }
1002
- }
1003
- );
1004
- } catch (error) {
1005
- logException(error, "api.plugin.session_completion_task.failed", {
1006
- conversationId: context.conversationId,
1007
- turnId
1008
- });
1009
- }
1010
- }
1011
- await acknowledge();
1012
- return { status: "completed" };
1013
- } catch (error) {
1014
- const failure = error instanceof AgentRunError ? error.cause : error;
1015
- if (hasLostTurnInputCommit(failure)) {
1016
- return { status: "lost_lease" };
1017
- }
1018
- if (cancellationSignal?.aborted) {
1019
- return await completeCancelledTurn();
1020
- }
1021
- if (!context.attempt.isFinalAttempt) {
1022
- throw failure;
1023
- }
1024
- const failureEventId = modelFailureCaptureAttempted && failureCode === "agent_run_failed" ? modelFailureEventId : captureApiBoundaryFailure({
1025
- conversationId: context.conversationId,
1026
- error: failure,
1027
- failureCode,
1028
- runId: currentRunId,
1029
- turnId
1030
- });
1031
- try {
1032
- markTurnFailed({
1033
- conversation,
1034
- nowMs: Date.now(),
1035
- sessionId: turnId,
1036
- userMessageId,
1037
- markConversationMessage
1038
- });
1039
- await persistThreadStateById(context.conversationId, {
1040
- conversation,
1041
- sandboxRef: initialSandboxRef ?? null
1042
- });
1043
- } catch (persistenceError) {
1044
- captureApiBoundaryFailure({
1045
- conversationId: context.conversationId,
1046
- error: persistenceError,
1047
- failureCode: "persistence_failed",
1048
- runId: currentRunId,
1049
- turnId
1050
- });
1051
- }
1052
- await lifecycle.fail({
1053
- conversationId: context.conversationId,
1054
- createdAtMs: Date.now(),
1055
- ...failureEventId ? { eventId: failureEventId } : void 0,
1056
- failureCode,
1057
- turnId
1058
- });
1059
- finishCancellation();
1060
- await acknowledge();
1061
- return { status: "completed" };
1062
- }
1063
- }
1064
- );
1065
- };
1066
- }
1067
- function routeApiTurnWork(options) {
1068
- return async (context) => {
1069
- return await isApiTurnWork(context) ? await options.apiTurnWorker(context) : await options.fallbackWorker(context);
1070
- };
1071
- }
1072
-
1073
- // src/api/conversations/access.ts
1074
- import { eq as eq2, inArray as inArray2 } from "drizzle-orm";
1075
- import { alias } from "drizzle-orm/pg-core";
1076
-
1077
- // src/api/conversations/membership.ts
1078
- import {
1079
- and,
1080
- eq,
1081
- exists,
1082
- inArray,
1083
- isNotNull,
1084
- notExists,
1085
- or
1086
- } from "drizzle-orm";
1087
- function conversationHasParticipantUser(userId) {
1088
- const participant = getDb().select({
1089
- rootConversationId: juniorConversationParticipants.rootConversationId
1090
- }).from(juniorConversationParticipants).where(
1091
- and(
1092
- eq(
1093
- juniorConversationParticipants.rootConversationId,
1094
- juniorConversations.conversationId
1095
- ),
1096
- eq(juniorConversationParticipants.userId, userId)
1097
- )
1098
- );
1099
- return exists(participant);
1100
- }
1101
- function conversationHasParticipantEmail(email) {
1102
- const participant = getDb().select({
1103
- rootConversationId: juniorConversationParticipants.rootConversationId
1104
- }).from(juniorConversationParticipants).innerJoin(
1105
- juniorUsers,
1106
- eq(juniorUsers.id, juniorConversationParticipants.userId)
1107
- ).where(
1108
- and(
1109
- eq(
1110
- juniorConversationParticipants.rootConversationId,
1111
- juniorConversations.conversationId
1112
- ),
1113
- eq(juniorUsers.primaryEmailNormalized, email)
1114
- )
1115
- );
1116
- return exists(participant);
1117
- }
1118
- function conversationArchivedForUser(userId) {
1119
- const archive = getDb().select({
1120
- rootConversationId: juniorConversationParticipants.rootConversationId
1121
- }).from(juniorConversationParticipants).where(
1122
- and(
1123
- eq(
1124
- juniorConversationParticipants.rootConversationId,
1125
- juniorConversations.conversationId
1126
- ),
1127
- eq(juniorConversationParticipants.userId, userId),
1128
- isNotNull(juniorConversationParticipants.archivedAt)
1129
- )
1130
- );
1131
- return exists(archive);
1132
- }
1133
- function conversationNotArchivedForUser(userId) {
1134
- const archive = getDb().select({
1135
- rootConversationId: juniorConversationParticipants.rootConversationId
1136
- }).from(juniorConversationParticipants).where(
1137
- and(
1138
- eq(
1139
- juniorConversationParticipants.rootConversationId,
1140
- juniorConversations.conversationId
1141
- ),
1142
- eq(juniorConversationParticipants.userId, userId),
1143
- isNotNull(juniorConversationParticipants.archivedAt)
1144
- )
1145
- );
1146
- return notExists(archive);
1147
- }
1148
- function conversationArchivedForEmail(email) {
1149
- const archive = getDb().select({
1150
- rootConversationId: juniorConversationParticipants.rootConversationId
1151
- }).from(juniorConversationParticipants).innerJoin(
1152
- juniorUsers,
1153
- eq(juniorUsers.id, juniorConversationParticipants.userId)
1154
- ).where(
1155
- and(
1156
- eq(
1157
- juniorConversationParticipants.rootConversationId,
1158
- juniorConversations.conversationId
1159
- ),
1160
- eq(juniorUsers.primaryEmailNormalized, email),
1161
- isNotNull(juniorConversationParticipants.archivedAt)
1162
- )
1163
- );
1164
- return exists(archive);
1165
- }
1166
- function conversationNotArchivedForEmail(email) {
1167
- const archive = getDb().select({
1168
- rootConversationId: juniorConversationParticipants.rootConversationId
1169
- }).from(juniorConversationParticipants).innerJoin(
1170
- juniorUsers,
1171
- eq(juniorUsers.id, juniorConversationParticipants.userId)
1172
- ).where(
1173
- and(
1174
- eq(
1175
- juniorConversationParticipants.rootConversationId,
1176
- juniorConversations.conversationId
1177
- ),
1178
- eq(juniorUsers.primaryEmailNormalized, email),
1179
- isNotNull(juniorConversationParticipants.archivedAt)
1180
- )
1181
- );
1182
- return notExists(archive);
1183
- }
1184
- function viewerConversationMembership(args) {
1185
- if (args.actorMatch && args.participantMatch) {
1186
- return or(args.actorMatch, args.participantMatch);
1187
- }
1188
- return args.actorMatch ?? args.participantMatch;
1189
- }
1190
- async function conversationArchiveTimes(db, conversationIds, subject) {
1191
- if (!subject || conversationIds.length === 0) return /* @__PURE__ */ new Map();
1192
- const query = db.select({
1193
- archivedAt: juniorConversationParticipants.archivedAt,
1194
- rootConversationId: juniorConversationParticipants.rootConversationId
1195
- }).from(juniorConversationParticipants);
1196
- const rows = "userId" in subject ? await query.where(
1197
- and(
1198
- eq(juniorConversationParticipants.userId, subject.userId),
1199
- inArray(juniorConversationParticipants.rootConversationId, [
1200
- ...conversationIds
1201
- ]),
1202
- isNotNull(juniorConversationParticipants.archivedAt)
1203
- )
1204
- ) : await query.innerJoin(
1205
- juniorUsers,
1206
- eq(juniorUsers.id, juniorConversationParticipants.userId)
1207
- ).where(
1208
- and(
1209
- eq(juniorUsers.primaryEmailNormalized, subject.email),
1210
- inArray(juniorConversationParticipants.rootConversationId, [
1211
- ...conversationIds
1212
- ]),
1213
- isNotNull(juniorConversationParticipants.archivedAt)
1214
- )
1215
- );
1216
- return new Map(
1217
- rows.flatMap(
1218
- (row) => row.archivedAt ? [[row.rootConversationId, row.archivedAt.getTime()]] : []
1219
- )
1220
- );
1221
- }
1222
- async function conversationIdsWithParticipantUser(db, conversationIds, userId) {
1223
- if (conversationIds.length === 0) {
1224
- return /* @__PURE__ */ new Set();
1225
- }
1226
- const rows = await db.select({
1227
- rootConversationId: juniorConversationParticipants.rootConversationId
1228
- }).from(juniorConversationParticipants).where(
1229
- and(
1230
- eq(juniorConversationParticipants.userId, userId),
1231
- inArray(juniorConversationParticipants.rootConversationId, [
1232
- ...conversationIds
1233
- ])
1234
- )
1235
- );
1236
- return new Set(rows.map((row) => row.rootConversationId));
1237
- }
1238
-
1239
- // src/api/conversations/access.ts
1240
- var rootConversation = alias(juniorConversations, "access_root_conversation");
1241
- var rootDestination = alias(juniorDestinations, "access_root_destination");
1242
- var rootIdentity = alias(juniorIdentities, "access_root_identity");
1243
- async function readConversationAccessFromSql(db, conversationIds, viewer) {
1244
- if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
1245
- const normalizedViewerEmail = viewer?.email.trim().toLowerCase() || void 0;
1246
- const rows = await db.select({
1247
- conversationId: juniorConversations.conversationId,
1248
- parentConversationId: juniorConversations.parentConversationId,
1249
- storedRootConversationId: juniorConversations.rootConversationId,
1250
- rootConversationId: rootConversation.conversationId,
1251
- rootParentConversationId: rootConversation.parentConversationId,
1252
- rootRootConversationId: rootConversation.rootConversationId,
1253
- visibility: rootDestination.visibility,
1254
- rootEmailNormalized: rootIdentity.emailNormalized,
1255
- rootEmailVerified: rootIdentity.emailVerified,
1256
- rootUserId: rootIdentity.userId
1257
- }).from(juniorConversations).leftJoin(
1258
- rootConversation,
1259
- eq2(
1260
- rootConversation.conversationId,
1261
- juniorConversations.rootConversationId
1262
- )
1263
- ).leftJoin(
1264
- rootDestination,
1265
- eq2(rootDestination.id, rootConversation.destinationId)
1266
- ).leftJoin(
1267
- rootIdentity,
1268
- eq2(rootIdentity.id, rootConversation.actorIdentityId)
1269
- ).where(inArray2(juniorConversations.conversationId, [...conversationIds]));
1270
- const membershipConversationIds = /* @__PURE__ */ new Set();
1271
- for (const row of rows) {
1272
- membershipConversationIds.add(row.conversationId);
1273
- if (row.storedRootConversationId) {
1274
- membershipConversationIds.add(row.storedRootConversationId);
1275
- }
1276
- }
1277
- const participantRootIds = viewer !== void 0 ? await conversationIdsWithParticipantUser(
1278
- db,
1279
- [...membershipConversationIds],
1280
- viewer.id
1281
- ) : /* @__PURE__ */ new Set();
1282
- return new Map(
1283
- rows.map((row) => {
1284
- const hasValidRoot = row.rootConversationId !== null && row.rootConversationId === row.storedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && (row.parentConversationId !== null || row.storedRootConversationId === row.conversationId);
1285
- const validRootConversationId = hasValidRoot ? row.rootConversationId ?? void 0 : void 0;
1286
- const visibility = validRootConversationId === void 0 ? null : row.visibility;
1287
- const isRootActor = validRootConversationId !== void 0 && viewer !== void 0 && (row.rootUserId === viewer.id || normalizedViewerEmail !== void 0 && row.rootEmailVerified === true && row.rootEmailNormalized === normalizedViewerEmail);
1288
- const isMaterializedParticipant = participantRootIds.has(row.conversationId) || validRootConversationId !== void 0 && participantRootIds.has(validRootConversationId);
1289
- const isParticipant = isRootActor || isMaterializedParticipant;
1290
- const canViewPrivateContent = isParticipant || validRootConversationId !== void 0 && canExposeConversationPayload({
1291
- visibility: visibility ?? void 0
1292
- });
1293
- return [
1294
- row.conversationId,
1295
- {
1296
- canViewPrivateContent,
1297
- isParticipant,
1298
- visibility
1299
- }
1300
- ];
1301
- })
1302
- );
1303
- }
1304
-
1305
- // src/chat/slack/user.ts
1306
- import { z as z2 } from "zod";
1307
- var slackUserInfoSchema = z2.object({
1308
- ok: z2.literal(true),
1309
- user: z2.object({
1310
- name: z2.string().nullish(),
1311
- profile: z2.object({
1312
- email: z2.string().nullish(),
1313
- real_name: z2.string().nullish()
1314
- }).optional()
1315
- })
1316
- });
1317
- var USER_CACHE_TTL_MS = 5 * 60 * 1e3;
1318
- var userCache = /* @__PURE__ */ new Map();
1319
- function userCacheKey(teamId, userId) {
1320
- return `${teamId}:${userId}`;
1321
- }
1322
- function readFromCache(teamId, userId) {
1323
- const hit = userCache.get(userCacheKey(teamId, userId));
1324
- if (!hit) return null;
1325
- if (hit.expiresAt < Date.now()) {
1326
- userCache.delete(userCacheKey(teamId, userId));
1327
- return null;
1328
- }
1329
- return hit.value;
1330
- }
1331
- function writeToCache(teamId, userId, value) {
1332
- userCache.set(userCacheKey(teamId, userId), {
1333
- value,
1334
- expiresAt: Date.now() + USER_CACHE_TTL_MS
1335
- });
1336
- }
1337
- async function lookupSlackUser(teamId, userId) {
1338
- if (!teamId || !userId) {
1339
- return null;
1340
- }
1341
- const cached = readFromCache(teamId, userId);
1342
- if (cached) {
1343
- return cached;
1344
- }
1345
- const token = getSlackBotToken();
1346
- if (!token) {
1347
- return null;
1348
- }
1349
- try {
1350
- const response = await fetch(
1351
- `https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`,
1352
- {
1353
- headers: {
1354
- authorization: `Bearer ${token}`
1355
- }
1356
- }
1357
- );
1358
- if (!response.ok) {
1359
- logWarn("slack.user.lookup.failed", {
1360
- "enduser.id": userId,
1361
- "app.slack.team_id": teamId,
1362
- "http.response.status_code": response.status
1363
- });
1364
- return null;
1365
- }
1366
- const parsed = slackUserInfoSchema.safeParse(await response.json());
1367
- if (!parsed.success) {
1368
- return null;
1369
- }
1370
- const payload = parsed.data;
1371
- const userName = payload.user.name?.trim() || void 0;
1372
- const fullName = payload.user.profile?.real_name?.trim() || void 0;
1373
- const result = {
1374
- userName,
1375
- fullName,
1376
- email: payload.user.profile?.email?.trim() || void 0
1377
- };
1378
- writeToCache(teamId, userId, result);
1379
- return result;
1380
- } catch (error) {
1381
- logWarn("slack.user.lookup.failed", {
1382
- "enduser.id": userId,
1383
- "app.slack.team_id": teamId,
1384
- "exception.message": error instanceof Error ? error.message : String(error)
1385
- });
1386
- return null;
1387
- }
1388
- }
1389
-
1390
- // src/chat/slack/conversation-context.ts
1391
- function normalizeConversationName(type, channelName) {
1392
- const trimmed = channelName?.trim();
1393
- if (!trimmed) return void 0;
1394
- if (type === "public_channel" || type === "private_channel" || type === "private_channel_or_group_dm") {
1395
- return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
1396
- }
1397
- return trimmed;
1398
- }
1399
- function typeFromSlackChannelType(channelType) {
1400
- if (channelType === "channel") return "public_channel";
1401
- if (channelType === "group") return "private_channel";
1402
- if (channelType === "mpim") return "group_dm";
1403
- if (channelType === "im") return "direct_message";
1404
- return void 0;
1405
- }
1406
- function typeFromChannelId(channelId, channelName) {
1407
- const normalized = normalizeSlackConversationId(channelId);
1408
- if (!normalized) return void 0;
1409
- if (normalized.startsWith("C")) return "public_channel";
1410
- if (normalized.startsWith("D")) return "direct_message";
1411
- if (normalized.startsWith("G")) {
1412
- return channelName?.trim().startsWith("mpdm-") ? "group_dm" : "private_channel_or_group_dm";
1413
- }
1414
- return void 0;
1415
- }
1416
- function toSlackEventChannelType(channelType) {
1417
- if (channelType === "channel" || channelType === "group" || channelType === "mpim" || channelType === "im") {
1418
- return channelType;
1419
- }
1420
- return void 0;
1421
- }
1422
- function conversationVisibilityFromSlackChannelType(channelType) {
1423
- if (!channelType) return void 0;
1424
- return channelType === "channel" ? "public" : "private";
1425
- }
1426
- function resolveSlackChannelTypeFromMessage(message) {
1427
- const raw = message.raw;
1428
- if (!raw || typeof raw !== "object") {
1429
- return void 0;
1430
- }
1431
- const channelType = raw.channel_type;
1432
- return typeof channelType === "string" ? toSlackEventChannelType(channelType.trim()) : void 0;
1433
- }
1434
- function resolveSlackConversationContext(input) {
1435
- const type = typeFromSlackChannelType(input.channelType) ?? typeFromChannelId(input.channelId, input.channelName);
1436
- if (!type) return void 0;
1437
- const name = normalizeConversationName(type, input.channelName);
1438
- const visibility = conversationVisibilityFromSlackChannelType(
1439
- input.channelType
1440
- );
1441
- return {
1442
- type,
1443
- ...name ? { name } : void 0,
1444
- ...visibility ? { visibility } : void 0
1445
- };
1446
- }
1447
- function resolveSlackConversationContextFromThreadId(input) {
1448
- const slackThread = parseSlackThreadId(input.threadId);
1449
- const context = resolveSlackConversationContext({
1450
- channelId: slackThread?.channelId,
1451
- channelName: input.channelName
1452
- });
1453
- if (!context || !input.visibility) {
1454
- return context;
1455
- }
1456
- return {
1457
- ...context,
1458
- type: input.visibility === "private" && context.type === "public_channel" ? "private_channel" : context.type,
1459
- visibility: input.visibility
1460
- };
1461
- }
1462
- function formatSlackConversationTypeLabel(type) {
1463
- if (type === "public_channel") return "Public Channel";
1464
- if (type === "private_channel") return "Private Channel";
1465
- if (type === "group_dm") return "Group DM";
1466
- if (type === "direct_message") return "Direct Message";
1467
- return "Private Channel or Group DM";
1468
- }
1469
- function formatSlackConversationRedactedLabel(context) {
1470
- if (!context) return void 0;
1471
- return formatSlackConversationTypeLabel(context.type);
1472
- }
1473
-
1474
- export {
1475
- readHealthReport,
1476
- GET,
1477
- createVercelAttachmentStorage,
1478
- lookupSlackUser,
1479
- conversationVisibilityFromSlackChannelType,
1480
- resolveSlackChannelTypeFromMessage,
1481
- resolveSlackConversationContext,
1482
- resolveSlackConversationContextFromThreadId,
1483
- formatSlackConversationRedactedLabel,
1484
- createApiTurnCancellation,
1485
- getAuthPausedApiTurnId,
1486
- apiTurnIdForMessage,
1487
- webActorFromEmail,
1488
- recordApiConversationActivity,
1489
- createAndEnqueueApiConversation,
1490
- appendAndEnqueueApiConversationMessage,
1491
- createApiTurnWorker,
1492
- routeApiTurnWork,
1493
- conversationHasParticipantUser,
1494
- conversationHasParticipantEmail,
1495
- conversationArchivedForUser,
1496
- conversationNotArchivedForUser,
1497
- conversationArchivedForEmail,
1498
- conversationNotArchivedForEmail,
1499
- viewerConversationMembership,
1500
- conversationArchiveTimes,
1501
- readConversationAccessFromSql
1502
- };