@sentry/junior 0.162.0 → 0.163.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 (90) hide show
  1. package/README.md +2 -0
  2. package/dist/{agent-hooks-TL2SGC5W.js → agent-hooks-6IX5CV7G.js} +10 -10
  3. package/dist/api/acp/route.d.ts +13 -0
  4. package/dist/api.js +204 -403
  5. package/dist/app.d.ts +2 -2
  6. package/dist/app.js +480 -38
  7. package/dist/{catalog-runtime-IGSMIADJ.js → catalog-runtime-NEPZNTOM.js} +5 -5
  8. package/dist/chat/agent/sandbox.d.ts +4 -0
  9. package/dist/chat/api-turns/cancellation.d.ts +27 -0
  10. package/dist/chat/api-turns/routing.d.ts +31 -0
  11. package/dist/chat/api-turns/work.d.ts +12 -29
  12. package/dist/chat/app/conversation-work.d.ts +6 -1
  13. package/dist/chat/app/production.d.ts +2 -2
  14. package/dist/chat/conversations/history.d.ts +3 -0
  15. package/dist/chat/conversations/turn-lifecycle.d.ts +1 -1
  16. package/dist/chat/experimental.d.ts +1 -1
  17. package/dist/chat/plugins/agent-hooks.d.ts +4 -0
  18. package/dist/chat/repository-instructions.d.ts +1 -1
  19. package/dist/chat/sandbox/paths.d.ts +1 -0
  20. package/dist/chat/sandbox/prepare-workspace.d.ts +11 -0
  21. package/dist/chat/sandbox/ref.d.ts +1 -0
  22. package/dist/chat/sandbox/sandbox.d.ts +4 -0
  23. package/dist/chat/sandbox/session.d.ts +4 -0
  24. package/dist/chat/sandbox/snapshot/profile.d.ts +4 -3
  25. package/dist/chat/sandbox/snapshot/resolve.d.ts +4 -0
  26. package/dist/chat/sandbox/workspace.d.ts +4 -0
  27. package/dist/chat/tools/types.d.ts +5 -0
  28. package/dist/chat/workspaces/checkout-path.d.ts +4 -0
  29. package/dist/chat/workspaces/store.d.ts +8 -0
  30. package/dist/chat/workspaces/tools.d.ts +4 -0
  31. package/dist/chat/workspaces/types.d.ts +13 -0
  32. package/dist/{chunk-BPZQWAZJ.js → chunk-2FBPN2Y4.js} +4 -1
  33. package/dist/{chunk-H2KW7AZC.js → chunk-2HBJQEBE.js} +1 -1
  34. package/dist/{chunk-O6P7DZ6O.js → chunk-3OZNFCPR.js} +6 -6
  35. package/dist/{chunk-7DTPFFQX.js → chunk-3T7RJVDJ.js} +1 -1
  36. package/dist/{chunk-FEWT5KPP.js → chunk-4P2XNVTO.js} +2 -2
  37. package/dist/{chunk-CZ6EHOU5.js → chunk-ATWHPSMF.js} +40 -2
  38. package/dist/{chunk-MMBMDSRK.js → chunk-BLZUFHM7.js} +1 -1
  39. package/dist/chunk-BVREIWBD.js +131 -0
  40. package/dist/{chunk-OPNJRQDP.js → chunk-EKGJVUVS.js} +9 -6
  41. package/dist/{chunk-4WYGQ47U.js → chunk-FEIHINKR.js} +5 -5
  42. package/dist/{chunk-JJ3VXFDI.js → chunk-GCLHNRMU.js} +1 -1
  43. package/dist/{chunk-WHAJ3QVY.js → chunk-ICJ3KCA2.js} +1 -1
  44. package/dist/{chunk-5NEBOHRG.js → chunk-IIT23D3M.js} +4 -4
  45. package/dist/{chunk-EHQLTK3X.js → chunk-IX4XI42N.js} +376 -128
  46. package/dist/{chunk-DDQ43S5Q.js → chunk-JI5EPI3O.js} +4 -4
  47. package/dist/{chunk-C3KUKA27.js → chunk-KD3ROG6P.js} +17 -17
  48. package/dist/{chunk-JRVURR3H.js → chunk-MWVQ35U6.js} +543 -164
  49. package/dist/{chunk-V7QL4QMW.js → chunk-MXFL7LNZ.js} +12 -7
  50. package/dist/{chunk-RDV4BQRZ.js → chunk-NL53P7PS.js} +1 -1
  51. package/dist/{chunk-EBEHJPFF.js → chunk-O4IK4O7C.js} +1 -1
  52. package/dist/{chunk-KQSO4DJ4.js → chunk-O5ZZL7IR.js} +9 -9
  53. package/dist/{chunk-FESUWAYU.js → chunk-RK7KPI6G.js} +1 -1
  54. package/dist/{chunk-OBFRMQBY.js → chunk-RTPJRLOP.js} +9 -9
  55. package/dist/{chunk-HW4BW3PC.js → chunk-SR6WRBJW.js} +4 -4
  56. package/dist/{chunk-6BJFIWEV.js → chunk-THS4CKN4.js} +46 -87
  57. package/dist/{chunk-DZC3F2UI.js → chunk-TPDNZCZU.js} +1 -1
  58. package/dist/{chunk-Y75ZDQB5.js → chunk-TPT6EF7K.js} +1 -1
  59. package/dist/{chunk-5IDFPCON.js → chunk-VRS6WDK5.js} +1 -1
  60. package/dist/{chunk-AJ7J2HWY.js → chunk-WPGYWDJR.js} +6 -6
  61. package/dist/{chunk-K7IV6UV3.js → chunk-XY2IVIAY.js} +5 -5
  62. package/dist/{chunk-MNCEGXEH.js → chunk-YAUQXZVT.js} +2 -2
  63. package/dist/{chunk-THXMSSFZ.js → chunk-ZDKTZG3G.js} +1 -1
  64. package/dist/{chunk-QHCIZLIX.js → chunk-ZF6IC27P.js} +87 -10
  65. package/dist/cli/chat.js +32 -32
  66. package/dist/cli/check.js +6 -6
  67. package/dist/cli/plugins.js +13 -13
  68. package/dist/cli/snapshot-create.js +10 -10
  69. package/dist/cli/upgrade.js +7 -7
  70. package/dist/db/schema/workspaces.d.ts +169 -0
  71. package/dist/db/schema.d.ts +169 -1
  72. package/dist/{db-66XZ4HHQ.js → db-6VBMHVW2.js} +7 -7
  73. package/dist/{experimental-HH6OM6UH.js → experimental-XVDOSRBS.js} +1 -1
  74. package/dist/instrumentation.js +4 -4
  75. package/dist/nitro.js +2 -2
  76. package/dist/{oauth-callback-server-FMLKDUUF.js → oauth-callback-server-5NCCUIM2.js} +24 -24
  77. package/dist/{runner-YZONUMNY.js → runner-6ONEB6TQ.js} +13 -13
  78. package/dist/{skills-3UDJJXSO.js → skills-Y5R36UH4.js} +6 -6
  79. package/dist/spawn-RFMBQ6B3.js +37 -0
  80. package/dist/{validation-3HTDBONC.js → validation-HLHEQDEK.js} +6 -6
  81. package/dist/version.js +1 -1
  82. package/dist/{work-O4YTIS54.js → work-VIEFYTFG.js} +18 -18
  83. package/dist/worker-MRSKPKQZ.js +24 -0
  84. package/migrations/0029_dizzy_barracuda.sql +19 -0
  85. package/migrations/meta/0029_snapshot.json +2709 -0
  86. package/migrations/meta/_journal.json +8 -1
  87. package/package.json +8 -5
  88. package/dist/chunk-EM225ITJ.js +0 -56
  89. package/dist/spawn-TBPVA5S2.js +0 -37
  90. package/dist/worker-WHURUEYX.js +0 -24
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createWebAuthorization,
3
3
  deleteWebAuthorization
4
- } from "./chunk-RDV4BQRZ.js";
4
+ } from "./chunk-NL53P7PS.js";
5
5
  import {
6
6
  appendAndEnqueueInboundMessage
7
- } from "./chunk-5NEBOHRG.js";
7
+ } from "./chunk-IIT23D3M.js";
8
8
  import {
9
9
  buildDeliveredTurnStatePatch,
10
10
  completeAuthPauseTurn,
@@ -12,25 +12,26 @@ import {
12
12
  getTurnUserMessage,
13
13
  processPluginTask,
14
14
  scheduleSessionCompletedPluginTasks
15
- } from "./chunk-KQSO4DJ4.js";
15
+ } from "./chunk-O5ZZL7IR.js";
16
16
  import {
17
17
  ConversationTurnLifecycleService,
18
18
  coerceThreadConversationState,
19
19
  getPersistedSandboxState,
20
20
  getPersistedThreadState,
21
21
  persistThreadStateById
22
- } from "./chunk-OPNJRQDP.js";
22
+ } from "./chunk-EKGJVUVS.js";
23
23
  import {
24
24
  healthReportSchema
25
25
  } from "./chunk-UTL3MHPC.js";
26
26
  import {
27
27
  parseSlackThreadId
28
- } from "./chunk-WHAJ3QVY.js";
28
+ } from "./chunk-ICJ3KCA2.js";
29
29
  import {
30
30
  TurnInputCommitLostError,
31
31
  abandonTurnRecord,
32
32
  buildConversationContext,
33
33
  buildDeterministicTurnId,
34
+ clearPendingAuth,
34
35
  getAssistantReplyText,
35
36
  getConversationTurnBoundaryError,
36
37
  getTurnRecord,
@@ -48,17 +49,31 @@ import {
48
49
  saveTurnCheckpoint,
49
50
  startActiveTurn,
50
51
  upsertConversationMessage
51
- } from "./chunk-V7QL4QMW.js";
52
+ } from "./chunk-MXFL7LNZ.js";
52
53
  import {
53
54
  normalizeSlackConversationId
54
- } from "./chunk-QHCIZLIX.js";
55
+ } from "./chunk-ZF6IC27P.js";
55
56
  import {
56
57
  getConversationEventStore,
57
- getConversationStore
58
- } from "./chunk-DDQ43S5Q.js";
58
+ getConversationStore,
59
+ getDb
60
+ } from "./chunk-JI5EPI3O.js";
61
+ import {
62
+ juniorApiTokens,
63
+ juniorConversationParticipants,
64
+ juniorConversations,
65
+ juniorIdentities,
66
+ juniorUsers
67
+ } from "./chunk-ATWHPSMF.js";
59
68
  import {
60
69
  getSlackBotToken
61
- } from "./chunk-MNCEGXEH.js";
70
+ } from "./chunk-YAUQXZVT.js";
71
+ import {
72
+ juniorDestinations
73
+ } from "./chunk-VFUK3X5B.js";
74
+ import {
75
+ canExposeConversationPayload
76
+ } from "./chunk-3T7RJVDJ.js";
62
77
  import {
63
78
  credentialContextForActor
64
79
  } from "./chunk-BDUENQD3.js";
@@ -67,7 +82,7 @@ import {
67
82
  logWarn,
68
83
  setTags,
69
84
  withLogContext
70
- } from "./chunk-Y75ZDQB5.js";
85
+ } from "./chunk-TPT6EF7K.js";
71
86
 
72
87
  // src/handlers/health.ts
73
88
  function readHealthReport() {
@@ -105,115 +120,254 @@ function createVercelAttachmentStorage() {
105
120
  };
106
121
  }
107
122
 
108
- // src/chat/slack/user.ts
109
- import { z } from "zod";
110
- var slackUserInfoSchema = z.object({
111
- ok: z.literal(true),
112
- user: z.object({
113
- name: z.string().nullish(),
114
- profile: z.object({
115
- email: z.string().nullish(),
116
- real_name: z.string().nullish()
117
- }).optional()
118
- })
119
- });
120
- var USER_CACHE_TTL_MS = 5 * 60 * 1e3;
121
- var userCache = /* @__PURE__ */ new Map();
122
- function userCacheKey(teamId, userId) {
123
- return `${teamId}:${userId}`;
123
+ // src/personal-tokens/store.ts
124
+ import { createHash, randomBytes, randomUUID } from "crypto";
125
+ import { and, desc, eq, gt, isNull } from "drizzle-orm";
126
+ var TOKEN_PREFIX = "jr_pat_";
127
+ var TOKEN_SUFFIX_LENGTH = 8;
128
+ var DEFAULT_TOKEN_LIFETIME_MS = 90 * 24 * 60 * 60 * 1e3;
129
+ function normalizeEmail(email) {
130
+ return email.trim().toLowerCase();
124
131
  }
125
- function readFromCache(teamId, userId) {
126
- const hit = userCache.get(userCacheKey(teamId, userId));
127
- if (!hit) return null;
128
- if (hit.expiresAt < Date.now()) {
129
- userCache.delete(userCacheKey(teamId, userId));
130
- return null;
132
+ function hashToken(token) {
133
+ return createHash("sha256").update(token, "utf8").digest("hex");
134
+ }
135
+ function metadata(row) {
136
+ return {
137
+ id: row.id,
138
+ name: row.name,
139
+ tokenSuffix: row.tokenSuffix,
140
+ createdAt: row.createdAt.toISOString(),
141
+ expiresAt: row.expiresAt.toISOString(),
142
+ lastUsedAt: row.lastUsedAt?.toISOString() ?? null
143
+ };
144
+ }
145
+ async function createPersonalToken(args) {
146
+ const now = args.now ?? /* @__PURE__ */ new Date();
147
+ const token = `${TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`;
148
+ const row = {
149
+ id: randomUUID(),
150
+ ownerEmailNormalized: normalizeEmail(args.email),
151
+ name: args.name.trim(),
152
+ tokenHash: hashToken(token),
153
+ tokenSuffix: token.slice(-TOKEN_SUFFIX_LENGTH),
154
+ createdAt: now,
155
+ expiresAt: new Date(now.getTime() + DEFAULT_TOKEN_LIFETIME_MS)
156
+ };
157
+ await getDb().insert(juniorApiTokens).values(row);
158
+ return { ...metadata({ ...row, lastUsedAt: null, revokedAt: null }), token };
159
+ }
160
+ async function listPersonalTokens(email, now = /* @__PURE__ */ new Date()) {
161
+ const rows = await getDb().select().from(juniorApiTokens).where(
162
+ and(
163
+ eq(juniorApiTokens.ownerEmailNormalized, normalizeEmail(email)),
164
+ isNull(juniorApiTokens.revokedAt),
165
+ gt(juniorApiTokens.expiresAt, now)
166
+ )
167
+ ).orderBy(desc(juniorApiTokens.createdAt));
168
+ return rows.map(metadata);
169
+ }
170
+ async function revokePersonalToken(args) {
171
+ const rows = await getDb().update(juniorApiTokens).set({ revokedAt: args.now ?? /* @__PURE__ */ new Date() }).where(
172
+ and(
173
+ eq(juniorApiTokens.id, args.id),
174
+ eq(juniorApiTokens.ownerEmailNormalized, normalizeEmail(args.email)),
175
+ isNull(juniorApiTokens.revokedAt)
176
+ )
177
+ ).returning({ id: juniorApiTokens.id });
178
+ return rows.length > 0;
179
+ }
180
+ async function authenticatePersonalToken(token, now = /* @__PURE__ */ new Date()) {
181
+ if (!token.startsWith(TOKEN_PREFIX)) return void 0;
182
+ const rows = await getDb().select({
183
+ id: juniorApiTokens.id,
184
+ ownerEmailNormalized: juniorApiTokens.ownerEmailNormalized
185
+ }).from(juniorApiTokens).where(
186
+ and(
187
+ eq(juniorApiTokens.tokenHash, hashToken(token)),
188
+ isNull(juniorApiTokens.revokedAt),
189
+ gt(juniorApiTokens.expiresAt, now)
190
+ )
191
+ ).limit(1);
192
+ const match = rows[0];
193
+ if (!match) return void 0;
194
+ await getDb().update(juniorApiTokens).set({ lastUsedAt: now }).where(eq(juniorApiTokens.id, match.id));
195
+ return match.ownerEmailNormalized;
196
+ }
197
+
198
+ // src/chat/api-turns/work.ts
199
+ import { createHash as createHash2 } from "crypto";
200
+ import {
201
+ createWebSource,
202
+ localDestinationSchema
203
+ } from "@sentry/junior-plugin-api";
204
+
205
+ // src/chat/api-turns/cancellation.ts
206
+ function createApiTurnCancellation() {
207
+ const active = /* @__PURE__ */ new Map();
208
+ return {
209
+ begin(conversationId) {
210
+ if (active.has(conversationId)) {
211
+ return void 0;
212
+ }
213
+ const controller = new AbortController();
214
+ active.set(conversationId, {
215
+ connected: true,
216
+ controller,
217
+ parked: false
218
+ });
219
+ return controller.signal;
220
+ },
221
+ cancel(conversationId) {
222
+ const entry = active.get(conversationId);
223
+ if (!entry) {
224
+ return false;
225
+ }
226
+ entry.controller.abort(new Error("API Turn cancelled"));
227
+ return true;
228
+ },
229
+ disconnect(conversationId, signal) {
230
+ const entry = active.get(conversationId);
231
+ if (entry?.controller.signal !== signal) {
232
+ return;
233
+ }
234
+ entry.connected = false;
235
+ if (entry.parked) {
236
+ active.delete(conversationId);
237
+ }
238
+ },
239
+ finish(conversationId, signal) {
240
+ const entry = active.get(conversationId);
241
+ if (entry?.controller.signal === signal) {
242
+ active.delete(conversationId);
243
+ }
244
+ },
245
+ park(conversationId, signal) {
246
+ const entry = active.get(conversationId);
247
+ if (entry?.controller.signal !== signal) {
248
+ return;
249
+ }
250
+ entry.parked = true;
251
+ if (!entry.connected) {
252
+ active.delete(conversationId);
253
+ }
254
+ },
255
+ signal(conversationId) {
256
+ return active.get(conversationId)?.controller.signal;
257
+ }
258
+ };
259
+ }
260
+ async function completeCancelledApiTurn(args) {
261
+ try {
262
+ await abandonTurnRecord({
263
+ conversationId: args.conversationId,
264
+ turnId: args.turnId,
265
+ errorMessage: "API Turn cancelled"
266
+ });
267
+ clearPendingAuth(args.conversation, args.turnId);
268
+ markConversationMessage(args.conversation, args.userMessageId, {
269
+ replied: false,
270
+ skippedReason: "turn cancelled"
271
+ });
272
+ markTurnClosed({
273
+ conversation: args.conversation,
274
+ nowMs: Date.now(),
275
+ sessionId: args.turnId
276
+ });
277
+ await deleteWebAuthorization({
278
+ actorId: args.actorId,
279
+ conversationId: args.conversationId
280
+ });
281
+ await persistThreadStateById(args.conversationId, {
282
+ conversation: args.conversation,
283
+ sandboxRef: args.sandboxRef
284
+ });
285
+ await args.lifecycle.complete({
286
+ conversationId: args.conversationId,
287
+ createdAtMs: Date.now(),
288
+ outcome: "cancelled",
289
+ turnId: args.turnId
290
+ });
291
+ } finally {
292
+ if (args.signal) {
293
+ args.cancellation?.finish(args.conversationId, args.signal);
294
+ }
131
295
  }
132
- return hit.value;
296
+ await args.acknowledge();
133
297
  }
134
- function writeToCache(teamId, userId, value) {
135
- userCache.set(userCacheKey(teamId, userId), {
136
- value,
137
- expiresAt: Date.now() + USER_CACHE_TTL_MS
298
+
299
+ // src/chat/api-turns/routing.ts
300
+ import { z } from "zod";
301
+ var apiTurnMailboxMetadataSchema = z.object({
302
+ authorEmail: z.string().email(),
303
+ authorFullName: z.string().min(1).optional(),
304
+ authorUserId: z.string().min(1),
305
+ authorUserName: z.string().min(1).optional(),
306
+ kind: z.literal("api_turn"),
307
+ messageId: z.string().min(1)
308
+ }).strict();
309
+ function parseApiTurnMessages(messages) {
310
+ if (messages.length === 0) {
311
+ return [];
312
+ }
313
+ const parsed = messages.map((message) => ({
314
+ message,
315
+ metadata: apiTurnMailboxMetadataSchema.safeParse(message.input.metadata)
316
+ }));
317
+ if (parsed.every((entry) => !entry.metadata.success)) {
318
+ return [];
319
+ }
320
+ if (parsed.some((entry) => !entry.metadata.success)) {
321
+ throw new Error("Conversation mailbox mixes web turns and other input");
322
+ }
323
+ return parsed.map((entry) => {
324
+ if (!entry.metadata.success) {
325
+ throw new Error("API turn mailbox metadata failed validation");
326
+ }
327
+ return { message: entry.message, metadata: entry.metadata.data };
138
328
  });
139
329
  }
140
- async function lookupSlackUser(teamId, userId) {
141
- if (!teamId || !userId) {
142
- return null;
143
- }
144
- const cached = readFromCache(teamId, userId);
145
- if (cached) {
146
- return cached;
330
+ async function resolveApiTurnWork(context) {
331
+ const batch = parseApiTurnMessages(context.attempt.messages);
332
+ if (batch.length > 0) {
333
+ return { kind: "mailbox", batch };
147
334
  }
148
- const token = getSlackBotToken();
149
- if (!token) {
150
- return null;
335
+ if (context.attempt.messages.length > 0) {
336
+ return void 0;
151
337
  }
152
- try {
153
- const response = await fetch(
154
- `https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`,
155
- {
156
- headers: {
157
- authorization: `Bearer ${token}`
158
- }
159
- }
338
+ const summaries = await listTurnSummaries(context.conversationId);
339
+ const active = summaries.filter(
340
+ (summary) => summary.surface === "api" && !summary.dispatchId && (summary.state === "paused" || summary.state === "running")
341
+ );
342
+ if (active.length > 1) {
343
+ throw new Error(
344
+ `Conversation ${context.conversationId} has multiple active web turns`
160
345
  );
161
- if (!response.ok) {
162
- logWarn("slack.user.lookup.failed", {
163
- "enduser.id": userId,
164
- "app.slack.team_id": teamId,
165
- "http.response.status_code": response.status
166
- });
167
- return null;
168
- }
169
- const parsed = slackUserInfoSchema.safeParse(await response.json());
170
- if (!parsed.success) {
171
- return null;
172
- }
173
- const payload = parsed.data;
174
- const userName = payload.user.name?.trim() || void 0;
175
- const fullName = payload.user.profile?.real_name?.trim() || void 0;
176
- const result = {
177
- userName,
178
- fullName,
179
- email: payload.user.profile?.email?.trim() || void 0
180
- };
181
- writeToCache(teamId, userId, result);
182
- return result;
183
- } catch (error) {
184
- logWarn("slack.user.lookup.failed", {
185
- "enduser.id": userId,
186
- "app.slack.team_id": teamId,
187
- "exception.message": error instanceof Error ? error.message : String(error)
188
- });
189
- return null;
190
346
  }
347
+ const turnId = active[0]?.turnId;
348
+ if (!turnId) {
349
+ return void 0;
350
+ }
351
+ const record = await getTurnRecord(context.conversationId, turnId);
352
+ if (!record || record.surface !== "api" || Boolean(record.dispatchId) || record.state !== "paused" && record.state !== "running") {
353
+ return void 0;
354
+ }
355
+ return { kind: "resume", turnId };
356
+ }
357
+ async function isApiTurnWork(context) {
358
+ return await resolveApiTurnWork(context) !== void 0;
191
359
  }
192
360
 
193
361
  // src/chat/api-turns/work.ts
194
- import { createHash } from "crypto";
195
- import { z as z2 } from "zod";
196
- import {
197
- createWebSource,
198
- localDestinationSchema
199
- } from "@sentry/junior-plugin-api";
200
- var apiTurnMailboxMetadataSchema = z2.object({
201
- authorEmail: z2.string().email(),
202
- authorFullName: z2.string().min(1).optional(),
203
- authorUserId: z2.string().min(1),
204
- authorUserName: z2.string().min(1).optional(),
205
- kind: z2.literal("api_turn"),
206
- messageId: z2.string().min(1)
207
- }).strict();
208
- function normalizeEmail(email) {
362
+ function normalizeEmail2(email) {
209
363
  return email.trim().toLowerCase();
210
364
  }
211
365
  function stableHex(...parts) {
212
- return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
366
+ return createHash2("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
213
367
  }
214
368
  function createApiConversationId(args) {
215
369
  return `local:web:${stableHex(
216
- normalizeEmail(args.actorEmail),
370
+ normalizeEmail2(args.actorEmail),
217
371
  args.idempotencyKey
218
372
  )}`;
219
373
  }
@@ -245,23 +399,23 @@ function webSourceForConversation(args) {
245
399
  args.visibility === "private" ? "private" : "public"
246
400
  );
247
401
  }
248
- function actorFromMetadata(metadata) {
402
+ function actorFromMetadata(metadata2) {
249
403
  return {
250
404
  platform: "web",
251
- userId: metadata.authorUserId,
252
- email: normalizeEmail(metadata.authorEmail),
253
- ...metadata.authorFullName ? { fullName: metadata.authorFullName } : {},
254
- ...metadata.authorUserName ? { userName: metadata.authorUserName } : {}
405
+ userId: metadata2.authorUserId,
406
+ email: normalizeEmail2(metadata2.authorEmail),
407
+ ...metadata2.authorFullName ? { fullName: metadata2.authorFullName } : {},
408
+ ...metadata2.authorUserName ? { userName: metadata2.authorUserName } : {}
255
409
  };
256
410
  }
257
411
  function storedActorFromApi(actor) {
258
412
  return {
259
- ...actor.email ? { email: normalizeEmail(actor.email) } : {},
413
+ ...actor.email ? { email: normalizeEmail2(actor.email) } : {},
260
414
  ...actor.fullName ? { fullName: actor.fullName } : {}
261
415
  };
262
416
  }
263
417
  function webActorFromEmail(email, profile) {
264
- const normalized = normalizeEmail(email);
418
+ const normalized = normalizeEmail2(email);
265
419
  return {
266
420
  platform: "web",
267
421
  userId: `dashboard:${stableHex(normalized)}`,
@@ -303,7 +457,7 @@ function buildApiTurnInboundMessage(args) {
303
457
  authorId: args.actor.userId,
304
458
  text,
305
459
  metadata: {
306
- authorEmail: normalizeEmail(args.actor.email),
460
+ authorEmail: normalizeEmail2(args.actor.email),
307
461
  ...args.actor.fullName ? { authorFullName: args.actor.fullName } : {},
308
462
  authorUserId: args.actor.userId,
309
463
  ...args.actor.userName ? { authorUserName: args.actor.userName } : {},
@@ -401,57 +555,6 @@ async function appendAndEnqueueApiConversationMessage(input, options) {
401
555
  status: result.status === "duplicate" ? "duplicate" : "accepted"
402
556
  };
403
557
  }
404
- function parseApiTurnMessages(messages) {
405
- if (messages.length === 0) {
406
- return [];
407
- }
408
- const parsed = messages.map((message) => ({
409
- message,
410
- metadata: apiTurnMailboxMetadataSchema.safeParse(message.input.metadata)
411
- }));
412
- if (parsed.every((entry) => !entry.metadata.success)) {
413
- return [];
414
- }
415
- if (parsed.some((entry) => !entry.metadata.success)) {
416
- throw new Error("Conversation mailbox mixes web turns and other input");
417
- }
418
- return parsed.map((entry) => {
419
- if (!entry.metadata.success) {
420
- throw new Error("API turn mailbox metadata failed validation");
421
- }
422
- return { message: entry.message, metadata: entry.metadata.data };
423
- });
424
- }
425
- async function resolveApiTurnWork(context) {
426
- const batch = parseApiTurnMessages(context.attempt.messages);
427
- if (batch.length > 0) {
428
- return { kind: "mailbox", batch };
429
- }
430
- if (context.attempt.messages.length > 0) {
431
- return void 0;
432
- }
433
- const summaries = await listTurnSummaries(context.conversationId);
434
- const active = summaries.filter(
435
- (summary) => summary.surface === "api" && !summary.dispatchId && (summary.state === "paused" || summary.state === "running")
436
- );
437
- if (active.length > 1) {
438
- throw new Error(
439
- `Conversation ${context.conversationId} has multiple active web turns`
440
- );
441
- }
442
- const turnId = active[0]?.turnId;
443
- if (!turnId) {
444
- return void 0;
445
- }
446
- const record = await getTurnRecord(context.conversationId, turnId);
447
- if (!record || record.surface !== "api" || Boolean(record.dispatchId) || record.state !== "paused" && record.state !== "running") {
448
- return void 0;
449
- }
450
- return { kind: "resume", turnId };
451
- }
452
- async function isApiTurnWork(context) {
453
- return await resolveApiTurnWork(context) !== void 0;
454
- }
455
558
  function captureApiBoundaryFailure(args) {
456
559
  const eventId = logException(args.error, `api.turn.${args.failureCode}`, {
457
560
  conversationId: args.conversationId,
@@ -460,6 +563,10 @@ function captureApiBoundaryFailure(args) {
460
563
  });
461
564
  return typeof eventId === "string" ? eventId : void 0;
462
565
  }
566
+ function hasLostTurnInputCommit(error) {
567
+ const cause = getConversationTurnBoundaryError(error)?.cause ?? error;
568
+ return isTurnInputCommitLostError(error) || isTurnInputCommitLostError(cause);
569
+ }
463
570
  function createApiTurnWorker(options) {
464
571
  return async (context) => {
465
572
  const resolved = await resolveApiTurnWork(context);
@@ -618,6 +725,39 @@ function createApiTurnWorker(options) {
618
725
  let modelFailureEventId;
619
726
  let modelFailureCaptureAttempted = false;
620
727
  let reply;
728
+ const cancellationSignal = options.cancellation?.signal(
729
+ context.conversationId
730
+ );
731
+ const finishCancellation = () => {
732
+ if (cancellationSignal) {
733
+ options.cancellation?.finish(
734
+ context.conversationId,
735
+ cancellationSignal
736
+ );
737
+ }
738
+ };
739
+ const completeCancelledTurn = async () => {
740
+ try {
741
+ await completeCancelledApiTurn({
742
+ acknowledge,
743
+ actorId: actor.userId,
744
+ cancellation: options.cancellation,
745
+ conversation,
746
+ conversationId: context.conversationId,
747
+ lifecycle,
748
+ sandboxRef,
749
+ signal: cancellationSignal,
750
+ turnId,
751
+ userMessageId
752
+ });
753
+ } catch (error) {
754
+ if (hasLostTurnInputCommit(error)) {
755
+ return { status: "lost_lease" };
756
+ }
757
+ throw error;
758
+ }
759
+ return { status: "completed" };
760
+ };
621
761
  const deliverAssistantMessage = async (value) => {
622
762
  const replyText = typeof value === "string" ? value : getAssistantReplyText(value);
623
763
  if (!replyText?.trim()) {
@@ -654,6 +794,9 @@ function createApiTurnWorker(options) {
654
794
  await persistThreadStateById(context.conversationId, {
655
795
  conversation
656
796
  });
797
+ if (cancellationSignal?.aborted) {
798
+ return await completeCancelledTurn();
799
+ }
657
800
  const piMessages = await loadProjection({
658
801
  conversationId: context.conversationId
659
802
  });
@@ -677,6 +820,7 @@ function createApiTurnWorker(options) {
677
820
  publishExternally: false,
678
821
  source,
679
822
  surface: "api",
823
+ ...cancellationSignal ? { signal: cancellationSignal } : {},
680
824
  authorization: createWebAuthorization({
681
825
  actorId: actor.userId,
682
826
  conversationId: context.conversationId
@@ -705,6 +849,9 @@ function createApiTurnWorker(options) {
705
849
  }
706
850
  }
707
851
  });
852
+ if (cancellationSignal?.aborted) {
853
+ return await completeCancelledTurn();
854
+ }
708
855
  if (outcome.status === "suspended") {
709
856
  return { status: "yielded" };
710
857
  }
@@ -717,9 +864,16 @@ function createApiTurnWorker(options) {
717
864
  conversation,
718
865
  sandboxRef
719
866
  });
867
+ if (cancellationSignal) {
868
+ options.cancellation?.park(
869
+ context.conversationId,
870
+ cancellationSignal
871
+ );
872
+ }
720
873
  await acknowledge();
721
874
  return { status: "completed" };
722
875
  }
876
+ finishCancellation();
723
877
  reply = outcome.result;
724
878
  modelFailureCaptureAttempted = reply.diagnostics.outcome !== "success";
725
879
  const finalized = finalizeFailedTurnReplyWithEvent({
@@ -808,10 +962,12 @@ function createApiTurnWorker(options) {
808
962
  await acknowledge();
809
963
  return { status: "completed" };
810
964
  } catch (error) {
811
- const cause = getConversationTurnBoundaryError(error)?.cause ?? error;
812
- if (isTurnInputCommitLostError(error) || isTurnInputCommitLostError(cause)) {
965
+ if (hasLostTurnInputCommit(error)) {
813
966
  return { status: "lost_lease" };
814
967
  }
968
+ if (cancellationSignal?.aborted) {
969
+ return await completeCancelledTurn();
970
+ }
815
971
  if (!context.attempt.isFinalAttempt) {
816
972
  throw error;
817
973
  }
@@ -850,6 +1006,7 @@ function createApiTurnWorker(options) {
850
1006
  failureCode,
851
1007
  turnId
852
1008
  });
1009
+ finishCancellation();
853
1010
  await acknowledge();
854
1011
  return { status: "completed" };
855
1012
  }
@@ -863,6 +1020,217 @@ function routeApiTurnWork(options) {
863
1020
  };
864
1021
  }
865
1022
 
1023
+ // src/api/conversations/access.ts
1024
+ import { eq as eq3, inArray as inArray2 } from "drizzle-orm";
1025
+ import { alias } from "drizzle-orm/pg-core";
1026
+
1027
+ // src/api/conversations/membership.ts
1028
+ import { and as and2, eq as eq2, exists, inArray, or } from "drizzle-orm";
1029
+ function conversationHasParticipantUser(userId) {
1030
+ const participant = getDb().select({
1031
+ rootConversationId: juniorConversationParticipants.rootConversationId
1032
+ }).from(juniorConversationParticipants).where(
1033
+ and2(
1034
+ eq2(
1035
+ juniorConversationParticipants.rootConversationId,
1036
+ juniorConversations.conversationId
1037
+ ),
1038
+ eq2(juniorConversationParticipants.userId, userId)
1039
+ )
1040
+ );
1041
+ return exists(participant);
1042
+ }
1043
+ function conversationHasParticipantEmail(email) {
1044
+ const participant = getDb().select({
1045
+ rootConversationId: juniorConversationParticipants.rootConversationId
1046
+ }).from(juniorConversationParticipants).innerJoin(
1047
+ juniorUsers,
1048
+ eq2(juniorUsers.id, juniorConversationParticipants.userId)
1049
+ ).where(
1050
+ and2(
1051
+ eq2(
1052
+ juniorConversationParticipants.rootConversationId,
1053
+ juniorConversations.conversationId
1054
+ ),
1055
+ eq2(juniorUsers.primaryEmailNormalized, email)
1056
+ )
1057
+ );
1058
+ return exists(participant);
1059
+ }
1060
+ function viewerConversationMembership(args) {
1061
+ if (args.actorMatch && args.participantMatch) {
1062
+ return or(args.actorMatch, args.participantMatch);
1063
+ }
1064
+ return args.actorMatch ?? args.participantMatch;
1065
+ }
1066
+ async function conversationIdsWithParticipantUser(db, conversationIds, userId) {
1067
+ if (conversationIds.length === 0) {
1068
+ return /* @__PURE__ */ new Set();
1069
+ }
1070
+ const rows = await db.select({
1071
+ rootConversationId: juniorConversationParticipants.rootConversationId
1072
+ }).from(juniorConversationParticipants).where(
1073
+ and2(
1074
+ eq2(juniorConversationParticipants.userId, userId),
1075
+ inArray(juniorConversationParticipants.rootConversationId, [
1076
+ ...conversationIds
1077
+ ])
1078
+ )
1079
+ );
1080
+ return new Set(rows.map((row) => row.rootConversationId));
1081
+ }
1082
+
1083
+ // src/api/conversations/access.ts
1084
+ var rootConversation = alias(juniorConversations, "access_root_conversation");
1085
+ var rootDestination = alias(juniorDestinations, "access_root_destination");
1086
+ var rootIdentity = alias(juniorIdentities, "access_root_identity");
1087
+ async function readConversationAccessFromSql(db, conversationIds, viewer) {
1088
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
1089
+ const normalizedViewerEmail = viewer?.email.trim().toLowerCase() || void 0;
1090
+ const rows = await db.select({
1091
+ conversationId: juniorConversations.conversationId,
1092
+ parentConversationId: juniorConversations.parentConversationId,
1093
+ storedRootConversationId: juniorConversations.rootConversationId,
1094
+ rootConversationId: rootConversation.conversationId,
1095
+ rootParentConversationId: rootConversation.parentConversationId,
1096
+ rootRootConversationId: rootConversation.rootConversationId,
1097
+ visibility: rootDestination.visibility,
1098
+ rootEmailNormalized: rootIdentity.emailNormalized,
1099
+ rootEmailVerified: rootIdentity.emailVerified,
1100
+ rootUserId: rootIdentity.userId
1101
+ }).from(juniorConversations).leftJoin(
1102
+ rootConversation,
1103
+ eq3(
1104
+ rootConversation.conversationId,
1105
+ juniorConversations.rootConversationId
1106
+ )
1107
+ ).leftJoin(
1108
+ rootDestination,
1109
+ eq3(rootDestination.id, rootConversation.destinationId)
1110
+ ).leftJoin(
1111
+ rootIdentity,
1112
+ eq3(rootIdentity.id, rootConversation.actorIdentityId)
1113
+ ).where(inArray2(juniorConversations.conversationId, [...conversationIds]));
1114
+ const membershipConversationIds = /* @__PURE__ */ new Set();
1115
+ for (const row of rows) {
1116
+ membershipConversationIds.add(row.conversationId);
1117
+ if (row.storedRootConversationId) {
1118
+ membershipConversationIds.add(row.storedRootConversationId);
1119
+ }
1120
+ }
1121
+ const participantRootIds = viewer !== void 0 ? await conversationIdsWithParticipantUser(
1122
+ db,
1123
+ [...membershipConversationIds],
1124
+ viewer.id
1125
+ ) : /* @__PURE__ */ new Set();
1126
+ return new Map(
1127
+ rows.map((row) => {
1128
+ const hasValidRoot = row.rootConversationId !== null && row.rootConversationId === row.storedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && (row.parentConversationId !== null || row.storedRootConversationId === row.conversationId);
1129
+ const validRootConversationId = hasValidRoot ? row.rootConversationId ?? void 0 : void 0;
1130
+ const visibility = validRootConversationId === void 0 ? null : row.visibility;
1131
+ const isRootActor = validRootConversationId !== void 0 && viewer !== void 0 && (row.rootUserId === viewer.id || normalizedViewerEmail !== void 0 && row.rootEmailVerified === true && row.rootEmailNormalized === normalizedViewerEmail);
1132
+ const isMaterializedParticipant = participantRootIds.has(row.conversationId) || validRootConversationId !== void 0 && participantRootIds.has(validRootConversationId);
1133
+ const isParticipant = isRootActor || isMaterializedParticipant;
1134
+ const canViewPrivateContent = isParticipant || validRootConversationId !== void 0 && canExposeConversationPayload({
1135
+ visibility: visibility ?? void 0
1136
+ });
1137
+ return [
1138
+ row.conversationId,
1139
+ {
1140
+ canViewPrivateContent,
1141
+ isParticipant,
1142
+ visibility
1143
+ }
1144
+ ];
1145
+ })
1146
+ );
1147
+ }
1148
+
1149
+ // src/chat/slack/user.ts
1150
+ import { z as z2 } from "zod";
1151
+ var slackUserInfoSchema = z2.object({
1152
+ ok: z2.literal(true),
1153
+ user: z2.object({
1154
+ name: z2.string().nullish(),
1155
+ profile: z2.object({
1156
+ email: z2.string().nullish(),
1157
+ real_name: z2.string().nullish()
1158
+ }).optional()
1159
+ })
1160
+ });
1161
+ var USER_CACHE_TTL_MS = 5 * 60 * 1e3;
1162
+ var userCache = /* @__PURE__ */ new Map();
1163
+ function userCacheKey(teamId, userId) {
1164
+ return `${teamId}:${userId}`;
1165
+ }
1166
+ function readFromCache(teamId, userId) {
1167
+ const hit = userCache.get(userCacheKey(teamId, userId));
1168
+ if (!hit) return null;
1169
+ if (hit.expiresAt < Date.now()) {
1170
+ userCache.delete(userCacheKey(teamId, userId));
1171
+ return null;
1172
+ }
1173
+ return hit.value;
1174
+ }
1175
+ function writeToCache(teamId, userId, value) {
1176
+ userCache.set(userCacheKey(teamId, userId), {
1177
+ value,
1178
+ expiresAt: Date.now() + USER_CACHE_TTL_MS
1179
+ });
1180
+ }
1181
+ async function lookupSlackUser(teamId, userId) {
1182
+ if (!teamId || !userId) {
1183
+ return null;
1184
+ }
1185
+ const cached = readFromCache(teamId, userId);
1186
+ if (cached) {
1187
+ return cached;
1188
+ }
1189
+ const token = getSlackBotToken();
1190
+ if (!token) {
1191
+ return null;
1192
+ }
1193
+ try {
1194
+ const response = await fetch(
1195
+ `https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`,
1196
+ {
1197
+ headers: {
1198
+ authorization: `Bearer ${token}`
1199
+ }
1200
+ }
1201
+ );
1202
+ if (!response.ok) {
1203
+ logWarn("slack.user.lookup.failed", {
1204
+ "enduser.id": userId,
1205
+ "app.slack.team_id": teamId,
1206
+ "http.response.status_code": response.status
1207
+ });
1208
+ return null;
1209
+ }
1210
+ const parsed = slackUserInfoSchema.safeParse(await response.json());
1211
+ if (!parsed.success) {
1212
+ return null;
1213
+ }
1214
+ const payload = parsed.data;
1215
+ const userName = payload.user.name?.trim() || void 0;
1216
+ const fullName = payload.user.profile?.real_name?.trim() || void 0;
1217
+ const result = {
1218
+ userName,
1219
+ fullName,
1220
+ email: payload.user.profile?.email?.trim() || void 0
1221
+ };
1222
+ writeToCache(teamId, userId, result);
1223
+ return result;
1224
+ } catch (error) {
1225
+ logWarn("slack.user.lookup.failed", {
1226
+ "enduser.id": userId,
1227
+ "app.slack.team_id": teamId,
1228
+ "exception.message": error instanceof Error ? error.message : String(error)
1229
+ });
1230
+ return null;
1231
+ }
1232
+ }
1233
+
866
1234
  // src/chat/slack/conversation-context.ts
867
1235
  function normalizeConversationName(type, channelName) {
868
1236
  const trimmed = channelName?.trim();
@@ -957,9 +1325,20 @@ export {
957
1325
  resolveSlackConversationContext,
958
1326
  resolveSlackConversationContextFromThreadId,
959
1327
  formatSlackConversationRedactedLabel,
1328
+ createApiTurnCancellation,
1329
+ apiTurnIdForMessage,
960
1330
  webActorFromEmail,
1331
+ recordApiConversationActivity,
961
1332
  createAndEnqueueApiConversation,
962
1333
  appendAndEnqueueApiConversationMessage,
963
1334
  createApiTurnWorker,
964
- routeApiTurnWork
1335
+ routeApiTurnWork,
1336
+ conversationHasParticipantUser,
1337
+ conversationHasParticipantEmail,
1338
+ viewerConversationMembership,
1339
+ readConversationAccessFromSql,
1340
+ createPersonalToken,
1341
+ listPersonalTokens,
1342
+ revokePersonalToken,
1343
+ authenticatePersonalToken
965
1344
  };