@sentry/junior 0.178.0 → 0.179.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 (67) hide show
  1. package/dist/{agent-hooks-PBT4NNBV.js → agent-hooks-32IDCLMW.js} +12 -12
  2. package/dist/api.js +229 -196
  3. package/dist/app.js +570 -195
  4. package/dist/{catalog-runtime-M7QGKPFZ.js → catalog-runtime-FBVA6WAQ.js} +5 -5
  5. package/dist/chat/scheduled-tasks/personal.d.ts +5 -8
  6. package/dist/chat/scheduled-tasks/runs.d.ts +49 -0
  7. package/dist/chat/scheduled-tasks/schedule-intent.d.ts +1 -1
  8. package/dist/chat/scheduled-tasks/tasks.d.ts +27 -0
  9. package/dist/chat/scheduled-tasks/tool-support.d.ts +0 -4
  10. package/dist/chat/scheduled-tasks/tools/create.d.ts +1 -1
  11. package/dist/chat/scheduled-tasks/tools/update.d.ts +1 -1
  12. package/dist/chat/scheduled-tasks/types.d.ts +127 -78
  13. package/dist/{chunk-JVXZ3WDH.js → chunk-2HIPYNQI.js} +6 -6
  14. package/dist/{chunk-BHHWCNXI.js → chunk-3AFVR5UC.js} +5 -5
  15. package/dist/{chunk-XWUJWHFD.js → chunk-3I7XE3PA.js} +6 -6
  16. package/dist/{chunk-H6ZXBFGX.js → chunk-3TM3FYLQ.js} +1 -1
  17. package/dist/{chunk-6OYVH66J.js → chunk-4GWXAILB.js} +1 -1
  18. package/dist/{chunk-BUGO42R6.js → chunk-5IRROPA5.js} +4 -4
  19. package/dist/{chunk-534BO42N.js → chunk-B6ECUSVX.js} +3 -3
  20. package/dist/{chunk-G3CQEUHI.js → chunk-CJLBLELR.js} +5 -5
  21. package/dist/{chunk-22T3HB44.js → chunk-FAS6AIMF.js} +1 -1
  22. package/dist/{chunk-BFLMRTPD.js → chunk-FIDMHAO6.js} +6 -6
  23. package/dist/{chunk-AVRWMQ4U.js → chunk-FTGD2FBP.js} +1 -1
  24. package/dist/{chunk-UMG7M32G.js → chunk-G2CAC4CV.js} +17 -17
  25. package/dist/{chunk-3ZZEPLLB.js → chunk-GENV6KF2.js} +2 -2
  26. package/dist/{chunk-RE4XWXBV.js → chunk-GPLAT6H7.js} +6 -6
  27. package/dist/{chunk-UGWBJAGN.js → chunk-HQNE5APD.js} +1 -1
  28. package/dist/{chunk-AQHSXA3J.js → chunk-JMJNFEDC.js} +3 -3
  29. package/dist/{chunk-PRDKO3QB.js → chunk-JT4WMD3C.js} +12 -12
  30. package/dist/{chunk-4K6VNP3S.js → chunk-KJLHIHFF.js} +9 -9
  31. package/dist/{chunk-QCCLDJHB.js → chunk-Q5B3WTGG.js} +8 -8
  32. package/dist/{chunk-RGXOLPWO.js → chunk-R5NWWYIS.js} +1 -1
  33. package/dist/{chunk-ZCHHVTMF.js → chunk-R6U7N4FS.js} +1 -1
  34. package/dist/{chunk-OGRD5PFM.js → chunk-RTRBYSF3.js} +1 -1
  35. package/dist/{chunk-FQ6BMP5P.js → chunk-RZRYOLCE.js} +396 -74
  36. package/dist/{chunk-D3UJTX4U.js → chunk-SLGGOASN.js} +1 -1
  37. package/dist/{chunk-IQRPDKPJ.js → chunk-TIEKUH4Q.js} +1 -1
  38. package/dist/{chunk-LAOP4EPL.js → chunk-U4TKB6F6.js} +4 -4
  39. package/dist/{chunk-AGJ4OQ2V.js → chunk-UT6X7XN7.js} +1 -1
  40. package/dist/{chunk-XCNS3CXE.js → chunk-VP4OVRDD.js} +3 -3
  41. package/dist/{chunk-KEQAA42D.js → chunk-XSPSCWLH.js} +1 -1
  42. package/dist/chunk-XZY6G5YZ.js +1088 -0
  43. package/dist/{chunk-ABLODWPO.js → chunk-YFNAMRGE.js} +1 -1
  44. package/dist/{chunk-U3UDPGZG.js → chunk-ZQMG3CL5.js} +2 -2
  45. package/dist/cli/chat.js +31 -31
  46. package/dist/cli/check.js +6 -6
  47. package/dist/cli/plugins.js +13 -13
  48. package/dist/cli/snapshot-create.js +10 -10
  49. package/dist/cli/upgrade.js +7 -7
  50. package/dist/db/schema/scheduled-tasks.d.ts +29 -5
  51. package/dist/db/schema.d.ts +28 -4
  52. package/dist/{db-QVYOWATH.js → db-EJAV7JL6.js} +7 -7
  53. package/dist/instrumentation.js +4 -4
  54. package/dist/nitro.js +2 -2
  55. package/dist/{oauth-callback-server-PLPAFXO6.js → oauth-callback-server-GAVSUBTQ.js} +23 -23
  56. package/dist/{runner-AGNEIOZS.js → runner-34JL64JF.js} +15 -15
  57. package/dist/{skills-U5KQ66EM.js → skills-U545HALM.js} +6 -6
  58. package/dist/spawn-I2S4IDUQ.js +37 -0
  59. package/dist/{validation-QE4U45KS.js → validation-EVXVGA44.js} +6 -6
  60. package/dist/version.js +1 -1
  61. package/dist/{work-ATAXAHJJ.js → work-QOAUPDBU.js} +18 -18
  62. package/dist/worker-5GX2IKLZ.js +24 -0
  63. package/package.json +5 -5
  64. package/dist/chat/scheduled-tasks/store.d.ts +0 -78
  65. package/dist/chunk-RGV6KHDM.js +0 -1944
  66. package/dist/spawn-ZRYYZP2B.js +0 -37
  67. package/dist/worker-LIL5AOER.js +0 -24
@@ -0,0 +1,1088 @@
1
+ import {
2
+ ToolInputError,
3
+ canRouteResourceEvents,
4
+ getPlugins
5
+ } from "./chunk-3I7XE3PA.js";
6
+ import {
7
+ getDb
8
+ } from "./chunk-VP4OVRDD.js";
9
+ import {
10
+ juniorAttachments,
11
+ juniorConversations,
12
+ juniorEventTasks,
13
+ juniorSchedulerRuns,
14
+ juniorSchedulerTasks,
15
+ juniorStats
16
+ } from "./chunk-R6U7N4FS.js";
17
+ import {
18
+ juniorDestinations
19
+ } from "./chunk-VFUK3X5B.js";
20
+
21
+ // src/chat/resource-events/catalog.ts
22
+ import {
23
+ normalizeResourceEventIdentifier
24
+ } from "@sentry/junior-plugin-api";
25
+ import { z } from "zod";
26
+ function enumSchema(values, unavailableMessage) {
27
+ if (values.length === 0) {
28
+ return z.string().refine(() => false, unavailableMessage);
29
+ }
30
+ return z.enum(values);
31
+ }
32
+ function eventNamespaceSchema(catalog) {
33
+ return enumSchema(
34
+ Object.keys(catalog).sort(),
35
+ "No resource event namespaces are enabled."
36
+ );
37
+ }
38
+ function registeredEventTypeSchema(catalog) {
39
+ return enumSchema(
40
+ [
41
+ ...new Set(
42
+ Object.values(catalog).flatMap(
43
+ (registration) => registration.resourceTypes.flatMap(
44
+ (resourceType) => resourceType.supportedEvents
45
+ )
46
+ )
47
+ )
48
+ ].sort(),
49
+ "No resource event types are enabled."
50
+ );
51
+ }
52
+ function registeredResourceTypeSchema(catalog) {
53
+ return enumSchema(
54
+ [
55
+ ...new Set(
56
+ Object.values(catalog).flatMap(
57
+ (registration) => registration.resourceTypes.map((resourceType) => resourceType.type)
58
+ )
59
+ )
60
+ ].sort(),
61
+ "No resource types are enabled."
62
+ );
63
+ }
64
+ function pluginSupportsEvent(catalog, namespace, resourceType, eventType) {
65
+ return catalog[namespace]?.resourceTypes.find((candidate) => candidate.type === resourceType)?.supportedEvents.includes(eventType) ?? false;
66
+ }
67
+ function resourceEventGuidance(catalog, namespace, resourceType, eventType) {
68
+ return catalog[namespace]?.resourceTypes.find(
69
+ (candidate) => candidate.type === resourceType
70
+ )?.guidance?.[eventType];
71
+ }
72
+ function normalizeEventIdentifier(catalog, namespace, identifier) {
73
+ return normalizeResourceEventIdentifier(catalog[namespace], identifier);
74
+ }
75
+
76
+ // src/chat/resource-events/runtime-catalog.ts
77
+ function getResourceEventCatalog() {
78
+ return Object.fromEntries(
79
+ canRouteResourceEvents() ? getPlugins().flatMap((plugin) => {
80
+ const registration = plugin.resourceEvents;
81
+ if (!registration || registration.isEnabled?.() === false) {
82
+ return [];
83
+ }
84
+ return [[plugin.manifest.name, registration]];
85
+ }) : []
86
+ );
87
+ }
88
+
89
+ // src/chat/event-tasks/store.ts
90
+ import { and, asc, desc, eq, inArray, or, sql } from "drizzle-orm";
91
+
92
+ // src/chat/event-tasks/types.ts
93
+ import {
94
+ destinationVisibilitySchema,
95
+ resourceEventTypeSchema,
96
+ slackActorSchema,
97
+ slackDestinationSchema
98
+ } from "@sentry/junior-plugin-api";
99
+ import { z as z2 } from "zod";
100
+ var EVENT_TASK_IDENTIFIER_MAX_LENGTH = 300;
101
+ var eventTaskPrincipalSchema = z2.object({
102
+ slackUserId: slackActorSchema.shape.userId,
103
+ fullName: z2.string().optional(),
104
+ userName: z2.string().optional()
105
+ }).strict();
106
+ var eventTaskTriggerSchema = z2.object({
107
+ events: z2.array(resourceEventTypeSchema).min(1),
108
+ label: z2.string().min(1),
109
+ namespace: z2.string().min(1),
110
+ identifier: z2.string().min(1).max(EVENT_TASK_IDENTIFIER_MAX_LENGTH),
111
+ resourceType: z2.string().min(1)
112
+ }).strict();
113
+ var eventTaskSchema = z2.object({
114
+ id: z2.string().min(1),
115
+ createdAtMs: z2.number().finite(),
116
+ createdBy: eventTaskPrincipalSchema,
117
+ credentialMode: z2.enum(["system", "creator"]),
118
+ destination: slackDestinationSchema,
119
+ destinationVisibility: destinationVisibilitySchema,
120
+ task: z2.object({ text: z2.string().min(1) }).strict(),
121
+ trigger: eventTaskTriggerSchema
122
+ }).strict();
123
+
124
+ // src/chat/event-tasks/store.ts
125
+ function eventTaskJsonPayload(task) {
126
+ const { title: _title, ...payload } = task;
127
+ return payload;
128
+ }
129
+ function parseTask(row) {
130
+ const raw = row.task && typeof row.task === "object" ? { ...row.task } : row.task;
131
+ if (raw && typeof raw === "object" && "title" in raw) {
132
+ delete raw.title;
133
+ }
134
+ const payload = eventTaskSchema.parse(raw);
135
+ const title = row.title?.trim();
136
+ return title ? { ...payload, title } : payload;
137
+ }
138
+ async function getEventTask(db, id) {
139
+ const rows = await db.select({ task: juniorEventTasks.task, title: juniorEventTasks.title }).from(juniorEventTasks).where(eq(juniorEventTasks.id, id)).limit(1);
140
+ return rows[0] ? parseTask(rows[0]) : void 0;
141
+ }
142
+ async function createEventTask(db, task) {
143
+ const parsed = eventTaskSchema.parse(eventTaskJsonPayload(task));
144
+ const title = task.title?.trim() || null;
145
+ await db.insert(juniorEventTasks).values({
146
+ id: parsed.id,
147
+ teamId: parsed.destination.teamId,
148
+ namespace: parsed.trigger.namespace,
149
+ identifier: parsed.trigger.identifier,
150
+ createdAtMs: parsed.createdAtMs,
151
+ title,
152
+ task: parsed
153
+ }).onConflictDoNothing();
154
+ return await getEventTask(db, parsed.id) ?? { ...parsed, ...title ? { title } : {} };
155
+ }
156
+ async function saveEventTask(db, task) {
157
+ const parsed = eventTaskSchema.parse(eventTaskJsonPayload(task));
158
+ const title = task.title?.trim() || null;
159
+ const rows = await db.update(juniorEventTasks).set({
160
+ namespace: parsed.trigger.namespace,
161
+ identifier: parsed.trigger.identifier,
162
+ title,
163
+ task: parsed
164
+ }).where(eq(juniorEventTasks.id, parsed.id)).returning({ task: juniorEventTasks.task, title: juniorEventTasks.title });
165
+ return rows[0] ? parseTask(rows[0]) : void 0;
166
+ }
167
+ async function deleteEventTask(db, id) {
168
+ const rows = await db.delete(juniorEventTasks).where(eq(juniorEventTasks.id, id)).returning({ task: juniorEventTasks.task, title: juniorEventTasks.title });
169
+ return rows[0] ? parseTask(rows[0]) : void 0;
170
+ }
171
+ async function listEventTasksForTeam(db, teamId) {
172
+ const rows = await db.select({ task: juniorEventTasks.task, title: juniorEventTasks.title }).from(juniorEventTasks).where(eq(juniorEventTasks.teamId, teamId)).orderBy(asc(juniorEventTasks.createdAtMs), asc(juniorEventTasks.id));
173
+ return rows.map(parseTask);
174
+ }
175
+ function viewerSlackIdentities(user) {
176
+ return user.identities.filter(
177
+ (identity) => identity.provider === "slack" && Boolean(identity.providerTenantId) && Boolean(identity.providerSubjectId)
178
+ );
179
+ }
180
+ function eventTaskBelongsToUser(task, user) {
181
+ return viewerSlackIdentities(user).some(
182
+ (identity) => identity.providerTenantId === task.destination.teamId && identity.providerSubjectId === task.createdBy.slackUserId
183
+ );
184
+ }
185
+ async function listEventTasksCreatedBy(db, user, limit) {
186
+ const identities = viewerSlackIdentities(user);
187
+ const ownership = or(
188
+ ...identities.map(
189
+ (identity) => and(
190
+ eq(juniorEventTasks.teamId, identity.providerTenantId),
191
+ sql`${juniorEventTasks.task}->'createdBy'->>'slackUserId' = ${identity.providerSubjectId}`
192
+ )
193
+ )
194
+ );
195
+ if (!ownership) return [];
196
+ const rows = await db.select({ task: juniorEventTasks.task, title: juniorEventTasks.title }).from(juniorEventTasks).where(ownership).orderBy(desc(juniorEventTasks.createdAtMs), desc(juniorEventTasks.id)).limit(limit);
197
+ return rows.map(parseTask);
198
+ }
199
+ async function listPublicEventTasksForTeams(db, teamIds, limit) {
200
+ if (teamIds.length === 0) return [];
201
+ const rows = await db.select({ task: juniorEventTasks.task, title: juniorEventTasks.title }).from(juniorEventTasks).innerJoin(
202
+ juniorDestinations,
203
+ and(
204
+ eq(juniorDestinations.provider, "slack"),
205
+ eq(juniorDestinations.providerTenantId, juniorEventTasks.teamId),
206
+ sql`${juniorDestinations.providerDestinationId} = ${juniorEventTasks.task}->'destination'->>'channelId'`
207
+ )
208
+ ).where(
209
+ and(
210
+ inArray(juniorEventTasks.teamId, teamIds),
211
+ eq(juniorDestinations.visibility, "public")
212
+ )
213
+ ).orderBy(desc(juniorEventTasks.createdAtMs), desc(juniorEventTasks.id)).limit(limit);
214
+ return rows.map(parseTask);
215
+ }
216
+ async function findMatchingEventTasks(db, event, teamId) {
217
+ const rows = await db.select({ task: juniorEventTasks.task, title: juniorEventTasks.title }).from(juniorEventTasks).where(
218
+ and(
219
+ eq(juniorEventTasks.teamId, teamId),
220
+ eq(juniorEventTasks.namespace, event.namespace),
221
+ eq(juniorEventTasks.identifier, event.identifier)
222
+ )
223
+ ).orderBy(asc(juniorEventTasks.createdAtMs), asc(juniorEventTasks.id));
224
+ return rows.map(parseTask).filter((task) => task.trigger.events.includes(event.eventType));
225
+ }
226
+
227
+ // src/stats.ts
228
+ import { and as and2, asc as asc2, gte, lte, sql as sql2 } from "drizzle-orm";
229
+ function utcDate(nowMs) {
230
+ return new Date(nowMs).toISOString().slice(0, 10);
231
+ }
232
+ async function incrementStat(key, options = {}) {
233
+ const date = utcDate(options.nowMs ?? Date.now());
234
+ await getDb().insert(juniorStats).values({ ...key, date, count: 1 }).onConflictDoUpdate({
235
+ target: [
236
+ juniorStats.date,
237
+ juniorStats.namespace,
238
+ juniorStats.metric,
239
+ juniorStats.name
240
+ ],
241
+ set: { count: sql2`${juniorStats.count} + 1` }
242
+ });
243
+ }
244
+ async function readStats(start, end) {
245
+ return await getDb().select().from(juniorStats).where(and2(gte(juniorStats.date, start), lte(juniorStats.date, end))).orderBy(
246
+ asc2(juniorStats.date),
247
+ asc2(juniorStats.namespace),
248
+ asc2(juniorStats.metric),
249
+ asc2(juniorStats.name)
250
+ );
251
+ }
252
+
253
+ // src/chat/tool-support/structured-result.ts
254
+ import { z as z3 } from "zod";
255
+ var juniorToolContinuationSchema = z3.object({
256
+ arguments: z3.record(z3.string(), z3.unknown()),
257
+ reason: z3.string().min(1).optional()
258
+ }).strict();
259
+ var juniorToolBoundContinuationSchema = juniorToolContinuationSchema.extend({
260
+ tool_name: z3.string().min(1)
261
+ });
262
+ var juniorToolOutputSchema = z3.object({
263
+ target: z3.string().min(1).optional(),
264
+ truncated: z3.boolean().optional(),
265
+ /** True when this attempt did not finish before its time budget. */
266
+ timed_out: z3.boolean().optional(),
267
+ continuation: juniorToolContinuationSchema.optional()
268
+ }).passthrough();
269
+ function sortJsonValue(value) {
270
+ if (Array.isArray(value)) {
271
+ return value.map(sortJsonValue);
272
+ }
273
+ if (!value || typeof value !== "object") {
274
+ return value;
275
+ }
276
+ return Object.fromEntries(
277
+ Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, sortJsonValue(item)])
278
+ );
279
+ }
280
+ function isRecord(value) {
281
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
282
+ }
283
+ function injectContinuationToolName(details, toolName) {
284
+ if (!isRecord(details) || !isRecord(details.continuation)) {
285
+ return details;
286
+ }
287
+ return {
288
+ ...details,
289
+ continuation: juniorToolBoundContinuationSchema.parse({
290
+ ...details.continuation,
291
+ tool_name: toolName
292
+ })
293
+ };
294
+ }
295
+ function makeStructuredToolOutput(details, options = {}) {
296
+ return {
297
+ content: [
298
+ {
299
+ type: "text",
300
+ text: JSON.stringify(sortJsonValue(details))
301
+ },
302
+ ...options.content ?? []
303
+ ],
304
+ details
305
+ };
306
+ }
307
+
308
+ // src/chat/event-tasks/tool-support.ts
309
+ import { z as z4 } from "zod";
310
+ var compactEventTaskResultSchema = z4.object({
311
+ id: z4.string().min(1),
312
+ task: z4.string().min(1),
313
+ namespace: z4.string().min(1),
314
+ identifier: z4.string().min(1),
315
+ resourceType: z4.string().min(1),
316
+ label: z4.string().min(1),
317
+ events: z4.array(z4.string().min(1)).min(1),
318
+ credentialMode: z4.enum(["system", "creator"]),
319
+ createdBy: eventTaskPrincipalSchema,
320
+ triggerAvailable: z4.boolean()
321
+ }).strict();
322
+ var eventTaskToolResultSchema = juniorToolOutputSchema.extend({
323
+ task: compactEventTaskResultSchema
324
+ }).strict();
325
+ var eventTaskListToolResultSchema = juniorToolOutputSchema.extend({
326
+ tasks: z4.array(compactEventTaskResultSchema),
327
+ truncated: z4.boolean()
328
+ }).strict();
329
+ function registeredEventTaskTriggerSchema(catalog) {
330
+ return z4.object({
331
+ namespace: eventNamespaceSchema(catalog),
332
+ identifier: z4.string().trim().min(1).max(EVENT_TASK_IDENTIFIER_MAX_LENGTH),
333
+ resourceType: registeredResourceTypeSchema(catalog),
334
+ label: z4.string().trim().min(1).max(500),
335
+ events: z4.array(registeredEventTypeSchema(catalog)).min(1)
336
+ }).strict();
337
+ }
338
+ function requireSupportedEventTaskTrigger(catalog, trigger) {
339
+ for (const eventType of trigger.events) {
340
+ if (!pluginSupportsEvent(
341
+ catalog,
342
+ trigger.namespace,
343
+ trigger.resourceType,
344
+ eventType
345
+ )) {
346
+ throw new ToolInputError(
347
+ `Resource type "${trigger.namespace}:${trigger.resourceType}" does not support event "${eventType}".`
348
+ );
349
+ }
350
+ }
351
+ }
352
+ function requireEventTaskSlackContext(context) {
353
+ if (context.source.platform !== "slack" || context.destination.platform !== "slack" || context.actor?.platform !== "slack" || context.actor.teamId !== context.source.teamId) {
354
+ throw new ToolInputError(
355
+ "Event tasks require an active Slack channel or DM and actor."
356
+ );
357
+ }
358
+ return {
359
+ actor: context.actor,
360
+ destination: {
361
+ platform: "slack",
362
+ teamId: context.source.teamId,
363
+ channelId: context.source.channelId
364
+ },
365
+ source: context.source
366
+ };
367
+ }
368
+ function eventTaskMatchesDestination(task, destination) {
369
+ return task.destination.teamId === destination.teamId && task.destination.channelId === destination.channelId;
370
+ }
371
+ async function writableEventTask(context, id) {
372
+ const { destination } = requireEventTaskSlackContext(context);
373
+ const task = await getEventTask(getDb(), id);
374
+ if (!task || !eventTaskMatchesDestination(task, destination)) {
375
+ throw new ToolInputError(
376
+ "Event task was not found in this Slack channel or DM."
377
+ );
378
+ }
379
+ return task;
380
+ }
381
+ function eventTaskTriggerAvailable(task, catalog) {
382
+ const registration = catalog[task.trigger.namespace];
383
+ return Boolean(
384
+ registration && task.trigger.events.every(
385
+ (eventType) => registration.resourceTypes.some(
386
+ (resourceType) => resourceType.supportedEvents.includes(eventType)
387
+ )
388
+ )
389
+ );
390
+ }
391
+ function compactEventTask(task, catalog) {
392
+ return compactEventTaskResultSchema.parse({
393
+ id: task.id,
394
+ task: task.task.text,
395
+ namespace: task.trigger.namespace,
396
+ identifier: task.trigger.identifier,
397
+ resourceType: task.trigger.resourceType,
398
+ label: task.trigger.label,
399
+ events: task.trigger.events,
400
+ credentialMode: task.credentialMode,
401
+ createdBy: task.createdBy,
402
+ triggerAvailable: eventTaskTriggerAvailable(task, catalog)
403
+ });
404
+ }
405
+ function eventTaskSuccess(task, catalog) {
406
+ return {
407
+ task: compactEventTask(task, catalog)
408
+ };
409
+ }
410
+
411
+ // src/chat/scheduled-tasks/tasks.ts
412
+ import { and as and3, asc as asc3, desc as desc2, eq as eq2, inArray as inArray2, notInArray, sql as sql3 } from "drizzle-orm";
413
+ import { z as z6 } from "zod";
414
+
415
+ // src/chat/scheduled-tasks/types.ts
416
+ import {
417
+ slackActorSchema as slackActorSchema2,
418
+ slackDestinationSchema as slackDestinationSchema2
419
+ } from "@sentry/junior-plugin-api";
420
+ import { z as z5 } from "zod";
421
+ var scheduledTaskStatusSchema = z5.enum([
422
+ "active",
423
+ "blocked",
424
+ "completed",
425
+ "deleted"
426
+ ]);
427
+ var scheduledTaskCredentialModeSchema = z5.enum(["system", "creator"]);
428
+ var scheduledRunStatusSchema = z5.enum([
429
+ "pending",
430
+ "running",
431
+ "completed",
432
+ "failed",
433
+ "blocked",
434
+ "skipped"
435
+ ]);
436
+ var scheduledTaskPrincipalSchema = z5.object({
437
+ slackUserId: slackActorSchema2.shape.userId,
438
+ fullName: z5.string().optional(),
439
+ userName: z5.string().optional()
440
+ }).strict();
441
+ var scheduledTaskRecurrenceSchema = z5.object({
442
+ dayOfMonth: z5.number().optional(),
443
+ frequency: z5.enum(["daily", "weekly", "monthly", "yearly"]),
444
+ interval: z5.number(),
445
+ month: z5.number().optional(),
446
+ startDate: z5.string(),
447
+ time: z5.object({
448
+ hour: z5.number(),
449
+ minute: z5.number()
450
+ }).strict(),
451
+ weekdays: z5.array(z5.number()).optional()
452
+ }).strict();
453
+ var scheduledTaskScheduleSchema = z5.object({
454
+ description: z5.string(),
455
+ kind: z5.enum(["one_off", "recurring"]),
456
+ recurrence: scheduledTaskRecurrenceSchema.optional(),
457
+ timezone: z5.string()
458
+ }).strict();
459
+ var scheduledTaskExecutionActorSchema = z5.object({
460
+ platform: z5.literal("system"),
461
+ name: z5.string()
462
+ }).strict();
463
+ var scheduledTaskSchema = z5.object({
464
+ id: z5.string(),
465
+ conversationAccess: z5.object({
466
+ audience: z5.enum(["direct", "group", "channel"]),
467
+ visibility: z5.enum(["private", "public"])
468
+ }).strict(),
469
+ createdAtMs: z5.number(),
470
+ createdBy: scheduledTaskPrincipalSchema,
471
+ /** Authoritative provider identity that created the task. */
472
+ creatorIdentityId: z5.string(),
473
+ /** Selects system credentials or task-bound creator credential delegation. */
474
+ credentialMode: scheduledTaskCredentialModeSchema,
475
+ destination: slackDestinationSchema2,
476
+ executionActor: scheduledTaskExecutionActorSchema.optional(),
477
+ lastRunAtMs: z5.number().optional(),
478
+ nextRunAtMs: z5.number().optional(),
479
+ originalRequest: z5.string().optional(),
480
+ runNowAtMs: z5.number().optional(),
481
+ schedule: scheduledTaskScheduleSchema,
482
+ status: scheduledTaskStatusSchema,
483
+ statusReason: z5.string().optional(),
484
+ task: z5.object({ text: z5.string() }).strict(),
485
+ /** SQL-backed short display title generated from the task instruction. */
486
+ title: z5.string().optional(),
487
+ updatedAtMs: z5.number()
488
+ }).strict();
489
+ var scheduledRunSchema = z5.object({
490
+ id: z5.string(),
491
+ attempt: z5.number(),
492
+ claimedAtMs: z5.number(),
493
+ completedAtMs: z5.number().optional(),
494
+ dispatchId: z5.string().optional(),
495
+ errorMessage: z5.string().optional(),
496
+ resultMessageTs: z5.string().optional(),
497
+ scheduledForMs: z5.number(),
498
+ startedAtMs: z5.number().optional(),
499
+ status: scheduledRunStatusSchema,
500
+ taskId: z5.string()
501
+ }).strict();
502
+ var SCHEDULED_TASK_SYSTEM_ACTOR = Object.freeze({
503
+ platform: "system",
504
+ name: "scheduled-task"
505
+ });
506
+
507
+ // src/chat/scheduled-tasks/tasks.ts
508
+ var SCHEDULER_KEY_PREFIX = "junior:scheduler";
509
+ var retainedScheduledTaskSchema = scheduledTaskSchema.omit({ creatorIdentityId: true, title: true }).extend({
510
+ // TODO(v0.131.0): Remove paused decoding and SQL list filtering after
511
+ // v0.129.x workers can no longer overlap upgrades.
512
+ status: z6.enum(["active", "blocked", "completed", "deleted", "paused"]),
513
+ version: z6.number().optional()
514
+ }).strict();
515
+ function parseScheduledTaskRow(row) {
516
+ const record = z6.record(z6.string(), z6.unknown()).safeParse(row.record);
517
+ if (!record.success) return void 0;
518
+ const {
519
+ creatorIdentityId: legacyCreatorIdentityId,
520
+ title: legacyTitle,
521
+ ...retained
522
+ } = record.data;
523
+ const parsed = retainedScheduledTaskSchema.safeParse(retained);
524
+ if (!parsed.success) return void 0;
525
+ const { status, version: _version, ...task } = parsed.data;
526
+ const fallbackIdentity = row.creatorIdentityId === null ? z6.string().safeParse(legacyCreatorIdentityId) : void 0;
527
+ const creatorIdentityId = row.creatorIdentityId ?? (fallbackIdentity?.success ? fallbackIdentity.data : void 0);
528
+ if (creatorIdentityId === void 0) return void 0;
529
+ const fallbackTitle = row.title === null ? z6.string().safeParse(legacyTitle) : void 0;
530
+ const titleSource = row.title !== null ? row.title : fallbackTitle?.success ? fallbackTitle.data : void 0;
531
+ const title = titleSource?.trim() || void 0;
532
+ if (status === "paused") {
533
+ const {
534
+ nextRunAtMs: _nextRunAtMs,
535
+ runNowAtMs: _runNowAtMs,
536
+ ...retained2
537
+ } = task;
538
+ return {
539
+ ...retained2,
540
+ creatorIdentityId,
541
+ status: "deleted",
542
+ ...title ? { title } : {}
543
+ };
544
+ }
545
+ return {
546
+ ...task,
547
+ creatorIdentityId,
548
+ status,
549
+ ...title ? { title } : {}
550
+ };
551
+ }
552
+ function isListedScheduledTask(task) {
553
+ return task !== void 0 && task.status !== "deleted";
554
+ }
555
+ function scheduledRunId(taskId, scheduledForMs) {
556
+ return `${taskId}:${scheduledForMs}`;
557
+ }
558
+ function withScheduledTaskLock(db, taskId, callback) {
559
+ return db.transaction(async (tx) => {
560
+ await tx.execute(
561
+ sql3`SELECT pg_advisory_xact_lock(hashtext(${`${SCHEDULER_KEY_PREFIX}:task:${taskId}:lock`}))`
562
+ );
563
+ return await callback(tx);
564
+ });
565
+ }
566
+ function requireStoredTask(task) {
567
+ const parsed = scheduledTaskSchema.safeParse(task);
568
+ if (!parsed.success) {
569
+ throw new Error("Scheduled task routing context is invalid.");
570
+ }
571
+ const { title, ...current } = parsed.data;
572
+ const normalizedTitle = title?.trim() || void 0;
573
+ return { ...current, ...normalizedTitle ? { title: normalizedTitle } : {} };
574
+ }
575
+ function scheduledTaskJsonRecord(task) {
576
+ const { title: _title, ...record } = task;
577
+ return record;
578
+ }
579
+ async function upsertScheduledTask(db, task) {
580
+ const title = task.title?.trim() || null;
581
+ await db.insert(juniorSchedulerTasks).values({
582
+ createdAtMs: task.createdAtMs,
583
+ creatorIdentityId: task.creatorIdentityId,
584
+ // TODO(v0.128.0): Remove after v0.127.x runtimes no longer read this column.
585
+ creatorSlackUserId: task.createdBy.slackUserId,
586
+ id: task.id,
587
+ nextRunAtMs: task.nextRunAtMs,
588
+ record: scheduledTaskJsonRecord(task),
589
+ runNowAtMs: task.runNowAtMs,
590
+ status: task.status,
591
+ teamId: task.destination.teamId,
592
+ title
593
+ }).onConflictDoUpdate({
594
+ target: juniorSchedulerTasks.id,
595
+ set: {
596
+ createdAtMs: sql3`excluded.created_at_ms`,
597
+ creatorIdentityId: sql3`excluded.creator_identity_id`,
598
+ creatorSlackUserId: sql3`excluded.creator_slack_user_id`,
599
+ nextRunAtMs: sql3`excluded.next_run_at_ms`,
600
+ record: sql3`excluded.record`,
601
+ runNowAtMs: sql3`excluded.run_now_at_ms`,
602
+ status: sql3`excluded.status`,
603
+ teamId: sql3`excluded.team_id`,
604
+ title: sql3`excluded.title`
605
+ }
606
+ });
607
+ }
608
+ async function readScheduledTask(db, taskId) {
609
+ const rows = await db.select({
610
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
611
+ record: juniorSchedulerTasks.record,
612
+ title: juniorSchedulerTasks.title
613
+ }).from(juniorSchedulerTasks).where(eq2(juniorSchedulerTasks.id, taskId)).limit(1);
614
+ return rows[0] ? parseScheduledTaskRow(rows[0]) : void 0;
615
+ }
616
+ async function readListedScheduledTasks(db, teamId) {
617
+ const rows = await db.select({
618
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
619
+ record: juniorSchedulerTasks.record,
620
+ title: juniorSchedulerTasks.title
621
+ }).from(juniorSchedulerTasks).where(
622
+ and3(
623
+ notInArray(juniorSchedulerTasks.status, ["deleted", "paused"]),
624
+ teamId === void 0 ? void 0 : eq2(juniorSchedulerTasks.teamId, teamId)
625
+ )
626
+ ).orderBy(
627
+ asc3(juniorSchedulerTasks.createdAtMs),
628
+ asc3(juniorSchedulerTasks.id)
629
+ );
630
+ return rows.map(parseScheduledTaskRow).filter(isListedScheduledTask);
631
+ }
632
+ async function listScheduledTasks(db) {
633
+ return await readListedScheduledTasks(db);
634
+ }
635
+ async function listScheduledTasksForTeam(db, teamId) {
636
+ return await readListedScheduledTasks(db, teamId);
637
+ }
638
+ async function writeScheduledTask(db, task, current) {
639
+ if (current?.status === "blocked" && task.status === "active" && typeof task.nextRunAtMs === "number" && Number.isFinite(task.nextRunAtMs)) {
640
+ await db.delete(juniorSchedulerRuns).where(
641
+ and3(
642
+ eq2(juniorSchedulerRuns.id, scheduledRunId(task.id, task.nextRunAtMs)),
643
+ eq2(juniorSchedulerRuns.status, "blocked")
644
+ )
645
+ );
646
+ }
647
+ await upsertScheduledTask(db, task);
648
+ }
649
+ async function createScheduledTask(db, task) {
650
+ const next = requireStoredTask(task);
651
+ return await withScheduledTaskLock(db, task.id, async (tx) => {
652
+ const current = await readScheduledTask(tx, task.id);
653
+ if (current) return current;
654
+ await writeScheduledTask(tx, next, void 0);
655
+ return next;
656
+ });
657
+ }
658
+ async function saveScheduledTask(db, task) {
659
+ const next = requireStoredTask(task);
660
+ await withScheduledTaskLock(db, task.id, async (tx) => {
661
+ const current = await readScheduledTask(tx, task.id);
662
+ await writeScheduledTask(tx, next, current);
663
+ });
664
+ return next;
665
+ }
666
+ async function saveScheduledTaskInLock(db, task, current) {
667
+ await writeScheduledTask(db, requireStoredTask(task), current);
668
+ }
669
+ async function listPublicScheduledTasksForTeams(db, teamIds, limit) {
670
+ if (teamIds.length === 0) return [];
671
+ const rows = await db.select({
672
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
673
+ record: juniorSchedulerTasks.record,
674
+ title: juniorSchedulerTasks.title
675
+ }).from(juniorSchedulerTasks).innerJoin(
676
+ juniorDestinations,
677
+ and3(
678
+ eq2(juniorDestinations.provider, "slack"),
679
+ eq2(juniorDestinations.providerTenantId, juniorSchedulerTasks.teamId),
680
+ sql3`${juniorDestinations.providerDestinationId} = ${juniorSchedulerTasks.record}->'destination'->>'channelId'`
681
+ )
682
+ ).where(
683
+ and3(
684
+ inArray2(juniorSchedulerTasks.teamId, teamIds),
685
+ notInArray(juniorSchedulerTasks.status, [
686
+ "completed",
687
+ "deleted",
688
+ "paused"
689
+ ]),
690
+ eq2(juniorDestinations.visibility, "public")
691
+ )
692
+ ).orderBy(
693
+ desc2(juniorSchedulerTasks.createdAtMs),
694
+ desc2(juniorSchedulerTasks.id)
695
+ ).limit(limit);
696
+ return rows.map(parseScheduledTaskRow).filter(isListedScheduledTask);
697
+ }
698
+
699
+ // src/chat/attachments/store.ts
700
+ import { createHash, randomUUID } from "crypto";
701
+ import { and as and4, asc as asc4, eq as eq3, inArray as inArray3, isNotNull, isNull, or as or2 } from "drizzle-orm";
702
+ var ATTACHMENT_GC_BATCH_LIMIT = 200;
703
+ function attachmentDigest(file) {
704
+ return createHash("sha256").update(file.data).digest("hex");
705
+ }
706
+ function attachmentId(args) {
707
+ return createHash("sha256").update(
708
+ [
709
+ args.conversationId,
710
+ args.provider,
711
+ args.sha256,
712
+ args.filename,
713
+ args.contentType
714
+ ].join("\0")
715
+ ).digest("hex");
716
+ }
717
+ function attachmentKey(args) {
718
+ return `conversations/${args.conversationId}/attachments/${args.attachmentId}/${args.writeId}/${args.filename}`;
719
+ }
720
+ async function bestEffortDelete(storage, key) {
721
+ try {
722
+ await storage.delete([key]);
723
+ } catch {
724
+ }
725
+ }
726
+ async function isConversationPurged(db, conversationId) {
727
+ const [row] = await db.db().select({
728
+ transcriptPurgedAt: juniorConversations.transcriptPurgedAt
729
+ }).from(juniorConversations).where(eq3(juniorConversations.conversationId, conversationId));
730
+ return row?.transcriptPurgedAt != null;
731
+ }
732
+ function purgedConversationError(conversationId) {
733
+ return new Error(
734
+ `Cannot store attachment for purged conversation ${conversationId}`
735
+ );
736
+ }
737
+ async function assertConversationNotPurged(db, conversationId) {
738
+ if (await isConversationPurged(db, conversationId)) {
739
+ throw purgedConversationError(conversationId);
740
+ }
741
+ }
742
+ async function rejectPurgedAttachmentWrite(args) {
743
+ await args.db.db().update(juniorAttachments).set({ deleteRequestedAt: args.now }).where(eq3(juniorAttachments.id, args.id));
744
+ await bestEffortDelete(args.storage, args.storageKey);
745
+ throw purgedConversationError(args.conversationId);
746
+ }
747
+ async function reuseLiveAttachment(args) {
748
+ await bestEffortDelete(args.storage, args.storageKey);
749
+ await assertConversationNotPurged(args.db, args.conversationId);
750
+ return { id: args.id };
751
+ }
752
+ async function storeAttachment(args) {
753
+ const now = new Date(args.nowMs ?? Date.now());
754
+ const sha256 = attachmentDigest(args.file);
755
+ const id = attachmentId({
756
+ conversationId: args.conversationId,
757
+ contentType: args.file.mimeType,
758
+ filename: args.file.filename,
759
+ provider: args.storage.provider,
760
+ sha256
761
+ });
762
+ const [existing] = await args.db.db().select({
763
+ contentType: juniorAttachments.contentType,
764
+ conversationId: juniorAttachments.conversationId,
765
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
766
+ filename: juniorAttachments.filename,
767
+ id: juniorAttachments.id,
768
+ provider: juniorAttachments.provider,
769
+ sha256: juniorAttachments.sha256,
770
+ storageKey: juniorAttachments.storageKey
771
+ }).from(juniorAttachments).where(eq3(juniorAttachments.id, id));
772
+ if (existing) {
773
+ if (existing.contentType !== args.file.mimeType || existing.conversationId !== args.conversationId || existing.filename !== args.file.filename || existing.provider !== args.storage.provider || existing.sha256 !== sha256) {
774
+ throw new Error(`Attachment write conflicts with ${existing.id}`);
775
+ }
776
+ if (existing.deleteRequestedAt === null) {
777
+ await assertConversationNotPurged(args.db, args.conversationId);
778
+ return { id: existing.id };
779
+ }
780
+ }
781
+ await assertConversationNotPurged(args.db, args.conversationId);
782
+ const storageKey = attachmentKey({
783
+ attachmentId: id,
784
+ conversationId: args.conversationId,
785
+ filename: args.file.filename,
786
+ writeId: randomUUID()
787
+ });
788
+ await args.storage.put({
789
+ body: args.file.data,
790
+ contentType: args.file.mimeType,
791
+ key: storageKey
792
+ });
793
+ if (existing?.deleteRequestedAt != null) {
794
+ if (await isConversationPurged(args.db, args.conversationId)) {
795
+ await bestEffortDelete(args.storage, storageKey);
796
+ throw purgedConversationError(args.conversationId);
797
+ }
798
+ const oldStorageKey = existing.storageKey;
799
+ const revived2 = await args.db.db().update(juniorAttachments).set({
800
+ deleteRequestedAt: null,
801
+ storageKey
802
+ }).where(
803
+ and4(
804
+ eq3(juniorAttachments.id, id),
805
+ isNotNull(juniorAttachments.deleteRequestedAt)
806
+ )
807
+ ).returning({ id: juniorAttachments.id });
808
+ if (revived2.length > 0) {
809
+ if (await isConversationPurged(args.db, args.conversationId)) {
810
+ await rejectPurgedAttachmentWrite({
811
+ conversationId: args.conversationId,
812
+ db: args.db,
813
+ id,
814
+ now,
815
+ storage: args.storage,
816
+ storageKey
817
+ });
818
+ }
819
+ if (oldStorageKey !== storageKey) {
820
+ await bestEffortDelete(args.storage, oldStorageKey);
821
+ }
822
+ return { id };
823
+ }
824
+ const [afterRace2] = await args.db.db().select({
825
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
826
+ id: juniorAttachments.id,
827
+ storageKey: juniorAttachments.storageKey
828
+ }).from(juniorAttachments).where(eq3(juniorAttachments.id, id));
829
+ if (afterRace2?.deleteRequestedAt === null) {
830
+ return reuseLiveAttachment({
831
+ conversationId: args.conversationId,
832
+ db: args.db,
833
+ id: afterRace2.id,
834
+ storage: args.storage,
835
+ storageKey
836
+ });
837
+ }
838
+ }
839
+ let inserted = [];
840
+ try {
841
+ inserted = await args.db.db().insert(juniorAttachments).values({
842
+ id,
843
+ conversationId: args.conversationId,
844
+ provider: args.storage.provider,
845
+ storageKey,
846
+ filename: args.file.filename,
847
+ contentType: args.file.mimeType,
848
+ bytes: args.file.bytes,
849
+ sha256,
850
+ createdAt: now
851
+ }).onConflictDoNothing({ target: juniorAttachments.id }).returning({ id: juniorAttachments.id });
852
+ } catch (error) {
853
+ await bestEffortDelete(args.storage, storageKey);
854
+ throw error;
855
+ }
856
+ if (inserted.length > 0) {
857
+ if (await isConversationPurged(args.db, args.conversationId)) {
858
+ await rejectPurgedAttachmentWrite({
859
+ conversationId: args.conversationId,
860
+ db: args.db,
861
+ id,
862
+ now,
863
+ storage: args.storage,
864
+ storageKey
865
+ });
866
+ }
867
+ return { id };
868
+ }
869
+ const [row] = await args.db.db().select({
870
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
871
+ id: juniorAttachments.id,
872
+ storageKey: juniorAttachments.storageKey
873
+ }).from(juniorAttachments).where(eq3(juniorAttachments.id, id));
874
+ if (!row) {
875
+ await bestEffortDelete(args.storage, storageKey);
876
+ throw new Error(`Attachment row missing after write for ${id}`);
877
+ }
878
+ if (row.deleteRequestedAt === null) {
879
+ return reuseLiveAttachment({
880
+ conversationId: args.conversationId,
881
+ db: args.db,
882
+ id: row.id,
883
+ storage: args.storage,
884
+ storageKey
885
+ });
886
+ }
887
+ if (await isConversationPurged(args.db, args.conversationId)) {
888
+ await bestEffortDelete(args.storage, storageKey);
889
+ throw purgedConversationError(args.conversationId);
890
+ }
891
+ const revived = await args.db.db().update(juniorAttachments).set({
892
+ deleteRequestedAt: null,
893
+ storageKey
894
+ }).where(
895
+ and4(
896
+ eq3(juniorAttachments.id, id),
897
+ isNotNull(juniorAttachments.deleteRequestedAt)
898
+ )
899
+ ).returning({ id: juniorAttachments.id });
900
+ if (revived.length > 0) {
901
+ if (await isConversationPurged(args.db, args.conversationId)) {
902
+ await rejectPurgedAttachmentWrite({
903
+ conversationId: args.conversationId,
904
+ db: args.db,
905
+ id,
906
+ now,
907
+ storage: args.storage,
908
+ storageKey
909
+ });
910
+ }
911
+ if (row.storageKey !== storageKey) {
912
+ await bestEffortDelete(args.storage, row.storageKey);
913
+ }
914
+ return { id };
915
+ }
916
+ const [afterRace] = await args.db.db().select({
917
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
918
+ id: juniorAttachments.id
919
+ }).from(juniorAttachments).where(eq3(juniorAttachments.id, id));
920
+ if (afterRace?.deleteRequestedAt === null) {
921
+ return reuseLiveAttachment({
922
+ conversationId: args.conversationId,
923
+ db: args.db,
924
+ id: afterRace.id,
925
+ storage: args.storage,
926
+ storageKey
927
+ });
928
+ }
929
+ await bestEffortDelete(args.storage, storageKey);
930
+ throw new Error(`Attachment ${id} disappeared while reviving`);
931
+ }
932
+ async function readLiveAttachment(args) {
933
+ const [row] = await args.db.db().select({
934
+ bytes: juniorAttachments.bytes,
935
+ contentType: juniorAttachments.contentType,
936
+ conversationId: juniorAttachments.conversationId,
937
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
938
+ filename: juniorAttachments.filename,
939
+ id: juniorAttachments.id,
940
+ provider: juniorAttachments.provider,
941
+ storageKey: juniorAttachments.storageKey
942
+ }).from(juniorAttachments).where(
943
+ and4(
944
+ eq3(juniorAttachments.id, args.attachmentId),
945
+ eq3(juniorAttachments.conversationId, args.conversationId),
946
+ isNull(juniorAttachments.deleteRequestedAt)
947
+ )
948
+ );
949
+ if (!row) return null;
950
+ if (await isConversationPurged(args.db, args.conversationId)) {
951
+ return null;
952
+ }
953
+ return {
954
+ bytes: row.bytes,
955
+ contentType: row.contentType,
956
+ conversationId: row.conversationId,
957
+ filename: row.filename,
958
+ id: row.id,
959
+ provider: row.provider,
960
+ storageKey: row.storageKey
961
+ };
962
+ }
963
+ async function storeAttachments(args) {
964
+ const stored = [];
965
+ for (const file of args.files) {
966
+ stored.push(
967
+ await storeAttachment({
968
+ conversationId: args.conversationId,
969
+ db: args.db,
970
+ file,
971
+ ...args.nowMs !== void 0 ? { nowMs: args.nowMs } : {},
972
+ storage: args.storage
973
+ })
974
+ );
975
+ }
976
+ return stored;
977
+ }
978
+ async function requestAttachmentDeletion(db, conversationIds, nowMs) {
979
+ if (conversationIds.length === 0) return;
980
+ await db.db().update(juniorAttachments).set({ deleteRequestedAt: new Date(nowMs) }).where(
981
+ and4(
982
+ inArray3(juniorAttachments.conversationId, conversationIds),
983
+ isNull(juniorAttachments.deleteRequestedAt)
984
+ )
985
+ );
986
+ }
987
+ async function collectAttachmentGarbage(args) {
988
+ void args.nowMs;
989
+ const rows = await args.db.db().select({
990
+ id: juniorAttachments.id,
991
+ storageKey: juniorAttachments.storageKey
992
+ }).from(juniorAttachments).innerJoin(
993
+ juniorConversations,
994
+ eq3(
995
+ juniorAttachments.conversationId,
996
+ juniorConversations.conversationId
997
+ )
998
+ ).where(
999
+ and4(
1000
+ eq3(juniorAttachments.provider, args.storage.provider),
1001
+ or2(
1002
+ isNotNull(juniorAttachments.deleteRequestedAt),
1003
+ isNotNull(juniorConversations.transcriptPurgedAt)
1004
+ )
1005
+ )
1006
+ ).orderBy(asc4(juniorAttachments.createdAt), asc4(juniorAttachments.id)).limit(args.limit ?? ATTACHMENT_GC_BATCH_LIMIT);
1007
+ if (rows.length === 0) return { deleted: 0 };
1008
+ await args.storage.delete(rows.map((row) => row.storageKey));
1009
+ const stillEligible = await args.db.db().select({ id: juniorAttachments.id }).from(juniorAttachments).innerJoin(
1010
+ juniorConversations,
1011
+ eq3(
1012
+ juniorAttachments.conversationId,
1013
+ juniorConversations.conversationId
1014
+ )
1015
+ ).where(
1016
+ and4(
1017
+ inArray3(
1018
+ juniorAttachments.id,
1019
+ rows.map((row) => row.id)
1020
+ ),
1021
+ or2(
1022
+ isNotNull(juniorAttachments.deleteRequestedAt),
1023
+ isNotNull(juniorConversations.transcriptPurgedAt)
1024
+ )
1025
+ )
1026
+ );
1027
+ if (stillEligible.length === 0) return { deleted: 0 };
1028
+ const removed = await args.db.db().delete(juniorAttachments).where(
1029
+ inArray3(
1030
+ juniorAttachments.id,
1031
+ stillEligible.map((row) => row.id)
1032
+ )
1033
+ ).returning({
1034
+ id: juniorAttachments.id
1035
+ });
1036
+ return { deleted: removed.length };
1037
+ }
1038
+
1039
+ export {
1040
+ juniorToolOutputSchema,
1041
+ injectContinuationToolName,
1042
+ makeStructuredToolOutput,
1043
+ eventNamespaceSchema,
1044
+ registeredEventTypeSchema,
1045
+ registeredResourceTypeSchema,
1046
+ pluginSupportsEvent,
1047
+ resourceEventGuidance,
1048
+ normalizeEventIdentifier,
1049
+ getResourceEventCatalog,
1050
+ getEventTask,
1051
+ createEventTask,
1052
+ saveEventTask,
1053
+ deleteEventTask,
1054
+ listEventTasksForTeam,
1055
+ eventTaskBelongsToUser,
1056
+ listEventTasksCreatedBy,
1057
+ listPublicEventTasksForTeams,
1058
+ findMatchingEventTasks,
1059
+ eventTaskToolResultSchema,
1060
+ eventTaskListToolResultSchema,
1061
+ registeredEventTaskTriggerSchema,
1062
+ requireSupportedEventTaskTrigger,
1063
+ requireEventTaskSlackContext,
1064
+ eventTaskMatchesDestination,
1065
+ writableEventTask,
1066
+ eventTaskTriggerAvailable,
1067
+ compactEventTask,
1068
+ eventTaskSuccess,
1069
+ scheduledRunSchema,
1070
+ SCHEDULED_TASK_SYSTEM_ACTOR,
1071
+ parseScheduledTaskRow,
1072
+ isListedScheduledTask,
1073
+ scheduledRunId,
1074
+ withScheduledTaskLock,
1075
+ readScheduledTask,
1076
+ listScheduledTasks,
1077
+ listScheduledTasksForTeam,
1078
+ createScheduledTask,
1079
+ saveScheduledTask,
1080
+ saveScheduledTaskInLock,
1081
+ listPublicScheduledTasksForTeams,
1082
+ readLiveAttachment,
1083
+ storeAttachments,
1084
+ requestAttachmentDeletion,
1085
+ collectAttachmentGarbage,
1086
+ incrementStat,
1087
+ readStats
1088
+ };