@sentry/junior 0.177.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 (102) hide show
  1. package/dist/{agent-hooks-IYHCN7CO.js → agent-hooks-32IDCLMW.js} +12 -12
  2. package/dist/api/conversations/archive.d.ts +5 -2
  3. package/dist/api/conversations/list.d.ts +0 -1
  4. package/dist/api/conversations/membership.d.ts +14 -0
  5. package/dist/api/conversations/projection.d.ts +2 -1
  6. package/dist/api/locations/detail.d.ts +1 -1
  7. package/dist/api/people/shared.d.ts +2 -2
  8. package/dist/api/schema/conversation.d.ts +4 -4
  9. package/dist/api/schema/location.d.ts +1 -1
  10. package/dist/api/schema/person.d.ts +1 -1
  11. package/dist/api/schema.js +1 -1
  12. package/dist/api.js +399 -287
  13. package/dist/app.js +573 -198
  14. package/dist/{catalog-runtime-2TD4CXSB.js → catalog-runtime-FBVA6WAQ.js} +5 -5
  15. package/dist/chat/agent/sandbox.d.ts +2 -0
  16. package/dist/chat/conversations/sql/participants.d.ts +1 -0
  17. package/dist/chat/conversations/store.d.ts +0 -1
  18. package/dist/chat/sandbox/errors.d.ts +4 -0
  19. package/dist/chat/sandbox/noninteractive-command.d.ts +1 -0
  20. package/dist/chat/sandbox/prepare-workspace.d.ts +27 -3
  21. package/dist/chat/sandbox/sandbox.d.ts +2 -0
  22. package/dist/chat/sandbox/session.d.ts +2 -0
  23. package/dist/chat/sandbox/snapshot/builder-sandbox.d.ts +5 -0
  24. package/dist/chat/sandbox/snapshot/install.d.ts +2 -2
  25. package/dist/chat/sandbox/snapshot/span.d.ts +3 -0
  26. package/dist/chat/sandbox/snapshot/store.d.ts +20 -17
  27. package/dist/chat/sandbox/snapshot/waiting-error.d.ts +15 -0
  28. package/dist/chat/sandbox/snapshot/workspace.d.ts +17 -0
  29. package/dist/chat/scheduled-tasks/personal.d.ts +5 -8
  30. package/dist/chat/scheduled-tasks/runs.d.ts +49 -0
  31. package/dist/chat/scheduled-tasks/schedule-intent.d.ts +1 -1
  32. package/dist/chat/scheduled-tasks/tasks.d.ts +27 -0
  33. package/dist/chat/scheduled-tasks/tool-support.d.ts +0 -4
  34. package/dist/chat/scheduled-tasks/tools/create.d.ts +1 -1
  35. package/dist/chat/scheduled-tasks/tools/update.d.ts +1 -1
  36. package/dist/chat/scheduled-tasks/types.d.ts +127 -78
  37. package/dist/chat/services/workspace-snapshot-wait.d.ts +27 -0
  38. package/dist/chat/workspaces/types.d.ts +1 -0
  39. package/dist/{chunk-CAWBLVBY.js → chunk-2HIPYNQI.js} +6 -6
  40. package/dist/{chunk-ZCYABD7N.js → chunk-3AFVR5UC.js} +5 -5
  41. package/dist/{chunk-PA7QDS7P.js → chunk-3I7XE3PA.js} +204 -100
  42. package/dist/{chunk-VGDOEKO2.js → chunk-3TM3FYLQ.js} +1 -1
  43. package/dist/{chunk-6NZRKJWU.js → chunk-4GWXAILB.js} +1 -1
  44. package/dist/{chunk-LFZODSAY.js → chunk-5IRROPA5.js} +4 -4
  45. package/dist/{chunk-N4YEVOEC.js → chunk-B6ECUSVX.js} +45 -276
  46. package/dist/{chunk-4TWDRU64.js → chunk-CJLBLELR.js} +5 -5
  47. package/dist/{chunk-3FWXQZJJ.js → chunk-FAS6AIMF.js} +1 -1
  48. package/dist/{chunk-I7PKGMKI.js → chunk-FIDMHAO6.js} +6 -6
  49. package/dist/{chunk-4SVAFAI2.js → chunk-FTGD2FBP.js} +1 -1
  50. package/dist/{chunk-LVL5NG5G.js → chunk-G2CAC4CV.js} +19 -19
  51. package/dist/{chunk-DLTP6TUJ.js → chunk-GENV6KF2.js} +2 -2
  52. package/dist/{chunk-FVVYHML7.js → chunk-GPLAT6H7.js} +6 -6
  53. package/dist/{chunk-7CVYXUOJ.js → chunk-HQNE5APD.js} +1 -1
  54. package/dist/{chunk-Q362TECJ.js → chunk-JMJNFEDC.js} +331 -2
  55. package/dist/{chunk-AFOFVEXP.js → chunk-JT4WMD3C.js} +124 -13
  56. package/dist/{chunk-7ZFESKJ2.js → chunk-KJLHIHFF.js} +9 -9
  57. package/dist/{chunk-QLLOX7JY.js → chunk-Q5B3WTGG.js} +8 -8
  58. package/dist/{chunk-DGGIKWYQ.js → chunk-R5NWWYIS.js} +1 -1
  59. package/dist/{chunk-WCQUKX74.js → chunk-R6U7N4FS.js} +14 -5
  60. package/dist/{chunk-WYO2HXY5.js → chunk-RTRBYSF3.js} +1 -1
  61. package/dist/{chunk-2IMQLZHS.js → chunk-RZRYOLCE.js} +1144 -136
  62. package/dist/{chunk-7B6NW4P7.js → chunk-SLGGOASN.js} +1 -1
  63. package/dist/{chunk-SNPQGTHJ.js → chunk-TIEKUH4Q.js} +1 -1
  64. package/dist/{chunk-ELEAIRTG.js → chunk-TKB25A3L.js} +2 -2
  65. package/dist/{chunk-UB5YB2ED.js → chunk-U4TKB6F6.js} +6 -6
  66. package/dist/{chunk-ORMJY4RG.js → chunk-UT6X7XN7.js} +1 -1
  67. package/dist/{chunk-LPMLAJYR.js → chunk-VP4OVRDD.js} +11 -17
  68. package/dist/{chunk-7PZDAXIK.js → chunk-XSPSCWLH.js} +1 -1
  69. package/dist/chunk-XZY6G5YZ.js +1088 -0
  70. package/dist/{chunk-J2EWL6GJ.js → chunk-YFNAMRGE.js} +1 -1
  71. package/dist/{chunk-6KGMMVHF.js → chunk-ZQMG3CL5.js} +2 -2
  72. package/dist/cli/chat.js +31 -30
  73. package/dist/cli/check.js +6 -6
  74. package/dist/cli/plugins.js +13 -13
  75. package/dist/cli/snapshot-create.js +10 -10
  76. package/dist/cli/upgrade.js +7 -7
  77. package/dist/db/schema/conversation-participants.d.ts +17 -0
  78. package/dist/db/schema/conversations.d.ts +629 -1
  79. package/dist/db/schema/scheduled-tasks.d.ts +29 -5
  80. package/dist/db/schema/snapshots.d.ts +1 -1
  81. package/dist/db/schema.d.ts +46 -5
  82. package/dist/{db-A3CB76US.js → db-EJAV7JL6.js} +7 -7
  83. package/dist/instrumentation.js +4 -4
  84. package/dist/nitro.js +2 -2
  85. package/dist/{oauth-callback-server-64QMI3IB.js → oauth-callback-server-GAVSUBTQ.js} +23 -23
  86. package/dist/{runner-EIAE2BGD.js → runner-34JL64JF.js} +15 -15
  87. package/dist/{skills-ZADWFKDG.js → skills-U545HALM.js} +6 -6
  88. package/dist/spawn-I2S4IDUQ.js +37 -0
  89. package/dist/{validation-4I6AF4CV.js → validation-EVXVGA44.js} +6 -6
  90. package/dist/version.js +1 -1
  91. package/dist/{work-BS6BZ66U.js → work-QOAUPDBU.js} +18 -18
  92. package/dist/worker-5GX2IKLZ.js +24 -0
  93. package/migrations/0033_workspace_snapshot_active_build.sql +1 -0
  94. package/migrations/0034_strong_randall.sql +31 -0
  95. package/migrations/meta/0033_snapshot.json +2914 -0
  96. package/migrations/meta/0034_snapshot.json +3038 -0
  97. package/migrations/meta/_journal.json +15 -1
  98. package/package.json +4 -4
  99. package/dist/chat/scheduled-tasks/store.d.ts +0 -78
  100. package/dist/chunk-4TRHPYTL.js +0 -1944
  101. package/dist/spawn-DVUOUFFC.js +0 -37
  102. package/dist/worker-RS5PWTCE.js +0 -24
@@ -1,1944 +0,0 @@
1
- import {
2
- ToolInputError,
3
- canRouteResourceEvents,
4
- getPlugins
5
- } from "./chunk-PA7QDS7P.js";
6
- import {
7
- getDb
8
- } from "./chunk-LPMLAJYR.js";
9
- import {
10
- juniorAttachments,
11
- juniorConversations,
12
- juniorEventTasks,
13
- juniorSchedulerRuns,
14
- juniorSchedulerTasks,
15
- juniorStats
16
- } from "./chunk-WCQUKX74.js";
17
- import {
18
- juniorDestinations
19
- } from "./chunk-VFUK3X5B.js";
20
- import {
21
- logInfo
22
- } from "./chunk-7CVYXUOJ.js";
23
-
24
- // src/chat/resource-events/catalog.ts
25
- import {
26
- normalizeResourceEventIdentifier
27
- } from "@sentry/junior-plugin-api";
28
- import { z } from "zod";
29
- function enumSchema(values, unavailableMessage) {
30
- if (values.length === 0) {
31
- return z.string().refine(() => false, unavailableMessage);
32
- }
33
- return z.enum(values);
34
- }
35
- function eventNamespaceSchema(catalog) {
36
- return enumSchema(
37
- Object.keys(catalog).sort(),
38
- "No resource event namespaces are enabled."
39
- );
40
- }
41
- function registeredEventTypeSchema(catalog) {
42
- return enumSchema(
43
- [
44
- ...new Set(
45
- Object.values(catalog).flatMap(
46
- (registration) => registration.resourceTypes.flatMap(
47
- (resourceType) => resourceType.supportedEvents
48
- )
49
- )
50
- )
51
- ].sort(),
52
- "No resource event types are enabled."
53
- );
54
- }
55
- function registeredResourceTypeSchema(catalog) {
56
- return enumSchema(
57
- [
58
- ...new Set(
59
- Object.values(catalog).flatMap(
60
- (registration) => registration.resourceTypes.map((resourceType) => resourceType.type)
61
- )
62
- )
63
- ].sort(),
64
- "No resource types are enabled."
65
- );
66
- }
67
- function pluginSupportsEvent(catalog, namespace, resourceType, eventType) {
68
- return catalog[namespace]?.resourceTypes.find((candidate) => candidate.type === resourceType)?.supportedEvents.includes(eventType) ?? false;
69
- }
70
- function resourceEventGuidance(catalog, namespace, resourceType, eventType) {
71
- return catalog[namespace]?.resourceTypes.find(
72
- (candidate) => candidate.type === resourceType
73
- )?.guidance?.[eventType];
74
- }
75
- function normalizeEventIdentifier(catalog, namespace, identifier) {
76
- return normalizeResourceEventIdentifier(catalog[namespace], identifier);
77
- }
78
-
79
- // src/chat/resource-events/runtime-catalog.ts
80
- function getResourceEventCatalog() {
81
- return Object.fromEntries(
82
- canRouteResourceEvents() ? getPlugins().flatMap((plugin) => {
83
- const registration = plugin.resourceEvents;
84
- if (!registration || registration.isEnabled?.() === false) {
85
- return [];
86
- }
87
- return [[plugin.manifest.name, registration]];
88
- }) : []
89
- );
90
- }
91
-
92
- // src/chat/event-tasks/store.ts
93
- import { and, asc, desc, eq, inArray, or, sql } from "drizzle-orm";
94
-
95
- // src/chat/event-tasks/types.ts
96
- import {
97
- destinationVisibilitySchema,
98
- resourceEventTypeSchema,
99
- slackActorSchema,
100
- slackDestinationSchema
101
- } from "@sentry/junior-plugin-api";
102
- import { z as z2 } from "zod";
103
- var EVENT_TASK_IDENTIFIER_MAX_LENGTH = 300;
104
- var eventTaskPrincipalSchema = z2.object({
105
- slackUserId: slackActorSchema.shape.userId,
106
- fullName: z2.string().optional(),
107
- userName: z2.string().optional()
108
- }).strict();
109
- var eventTaskTriggerSchema = z2.object({
110
- events: z2.array(resourceEventTypeSchema).min(1),
111
- label: z2.string().min(1),
112
- namespace: z2.string().min(1),
113
- identifier: z2.string().min(1).max(EVENT_TASK_IDENTIFIER_MAX_LENGTH),
114
- resourceType: z2.string().min(1)
115
- }).strict();
116
- var eventTaskSchema = z2.object({
117
- id: z2.string().min(1),
118
- createdAtMs: z2.number().finite(),
119
- createdBy: eventTaskPrincipalSchema,
120
- credentialMode: z2.enum(["system", "creator"]),
121
- destination: slackDestinationSchema,
122
- destinationVisibility: destinationVisibilitySchema,
123
- task: z2.object({ text: z2.string().min(1) }).strict(),
124
- trigger: eventTaskTriggerSchema
125
- }).strict();
126
-
127
- // src/chat/event-tasks/store.ts
128
- function eventTaskJsonPayload(task) {
129
- const { title: _title, ...payload } = task;
130
- return payload;
131
- }
132
- function parseTask(row) {
133
- const raw = row.task && typeof row.task === "object" ? { ...row.task } : row.task;
134
- if (raw && typeof raw === "object" && "title" in raw) {
135
- delete raw.title;
136
- }
137
- const payload = eventTaskSchema.parse(raw);
138
- const title = row.title?.trim();
139
- return title ? { ...payload, title } : payload;
140
- }
141
- async function getEventTask(db, id) {
142
- const rows = await db.select({ task: juniorEventTasks.task, title: juniorEventTasks.title }).from(juniorEventTasks).where(eq(juniorEventTasks.id, id)).limit(1);
143
- return rows[0] ? parseTask(rows[0]) : void 0;
144
- }
145
- async function createEventTask(db, task) {
146
- const parsed = eventTaskSchema.parse(eventTaskJsonPayload(task));
147
- const title = task.title?.trim() || null;
148
- await db.insert(juniorEventTasks).values({
149
- id: parsed.id,
150
- teamId: parsed.destination.teamId,
151
- namespace: parsed.trigger.namespace,
152
- identifier: parsed.trigger.identifier,
153
- createdAtMs: parsed.createdAtMs,
154
- title,
155
- task: parsed
156
- }).onConflictDoNothing();
157
- return await getEventTask(db, parsed.id) ?? { ...parsed, ...title ? { title } : {} };
158
- }
159
- async function saveEventTask(db, task) {
160
- const parsed = eventTaskSchema.parse(eventTaskJsonPayload(task));
161
- const title = task.title?.trim() || null;
162
- const rows = await db.update(juniorEventTasks).set({
163
- namespace: parsed.trigger.namespace,
164
- identifier: parsed.trigger.identifier,
165
- title,
166
- task: parsed
167
- }).where(eq(juniorEventTasks.id, parsed.id)).returning({ task: juniorEventTasks.task, title: juniorEventTasks.title });
168
- return rows[0] ? parseTask(rows[0]) : void 0;
169
- }
170
- async function deleteEventTask(db, id) {
171
- const rows = await db.delete(juniorEventTasks).where(eq(juniorEventTasks.id, id)).returning({ task: juniorEventTasks.task, title: juniorEventTasks.title });
172
- return rows[0] ? parseTask(rows[0]) : void 0;
173
- }
174
- async function listEventTasksForTeam(db, teamId) {
175
- 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));
176
- return rows.map(parseTask);
177
- }
178
- function viewerSlackIdentities(user) {
179
- return user.identities.filter(
180
- (identity) => identity.provider === "slack" && Boolean(identity.providerTenantId) && Boolean(identity.providerSubjectId)
181
- );
182
- }
183
- function eventTaskBelongsToUser(task, user) {
184
- return viewerSlackIdentities(user).some(
185
- (identity) => identity.providerTenantId === task.destination.teamId && identity.providerSubjectId === task.createdBy.slackUserId
186
- );
187
- }
188
- async function listEventTasksCreatedBy(db, user, limit) {
189
- const identities = viewerSlackIdentities(user);
190
- const ownership = or(
191
- ...identities.map(
192
- (identity) => and(
193
- eq(juniorEventTasks.teamId, identity.providerTenantId),
194
- sql`${juniorEventTasks.task}->'createdBy'->>'slackUserId' = ${identity.providerSubjectId}`
195
- )
196
- )
197
- );
198
- if (!ownership) return [];
199
- const rows = await db.select({ task: juniorEventTasks.task, title: juniorEventTasks.title }).from(juniorEventTasks).where(ownership).orderBy(desc(juniorEventTasks.createdAtMs), desc(juniorEventTasks.id)).limit(limit);
200
- return rows.map(parseTask);
201
- }
202
- async function listPublicEventTasksForTeams(db, teamIds, limit) {
203
- if (teamIds.length === 0) return [];
204
- const rows = await db.select({ task: juniorEventTasks.task, title: juniorEventTasks.title }).from(juniorEventTasks).innerJoin(
205
- juniorDestinations,
206
- and(
207
- eq(juniorDestinations.provider, "slack"),
208
- eq(juniorDestinations.providerTenantId, juniorEventTasks.teamId),
209
- sql`${juniorDestinations.providerDestinationId} = ${juniorEventTasks.task}->'destination'->>'channelId'`
210
- )
211
- ).where(
212
- and(
213
- inArray(juniorEventTasks.teamId, teamIds),
214
- eq(juniorDestinations.visibility, "public")
215
- )
216
- ).orderBy(desc(juniorEventTasks.createdAtMs), desc(juniorEventTasks.id)).limit(limit);
217
- return rows.map(parseTask);
218
- }
219
- async function findMatchingEventTasks(db, event, teamId) {
220
- const rows = await db.select({ task: juniorEventTasks.task, title: juniorEventTasks.title }).from(juniorEventTasks).where(
221
- and(
222
- eq(juniorEventTasks.teamId, teamId),
223
- eq(juniorEventTasks.namespace, event.namespace),
224
- eq(juniorEventTasks.identifier, event.identifier)
225
- )
226
- ).orderBy(asc(juniorEventTasks.createdAtMs), asc(juniorEventTasks.id));
227
- return rows.map(parseTask).filter((task) => task.trigger.events.includes(event.eventType));
228
- }
229
-
230
- // src/stats.ts
231
- import { and as and2, asc as asc2, gte, lte, sql as sql2 } from "drizzle-orm";
232
- function utcDate(nowMs) {
233
- return new Date(nowMs).toISOString().slice(0, 10);
234
- }
235
- async function incrementStat(key, options = {}) {
236
- const date = utcDate(options.nowMs ?? Date.now());
237
- await getDb().insert(juniorStats).values({ ...key, date, count: 1 }).onConflictDoUpdate({
238
- target: [
239
- juniorStats.date,
240
- juniorStats.namespace,
241
- juniorStats.metric,
242
- juniorStats.name
243
- ],
244
- set: { count: sql2`${juniorStats.count} + 1` }
245
- });
246
- }
247
- async function readStats(start, end) {
248
- return await getDb().select().from(juniorStats).where(and2(gte(juniorStats.date, start), lte(juniorStats.date, end))).orderBy(
249
- asc2(juniorStats.date),
250
- asc2(juniorStats.namespace),
251
- asc2(juniorStats.metric),
252
- asc2(juniorStats.name)
253
- );
254
- }
255
-
256
- // src/chat/tool-support/structured-result.ts
257
- import { z as z3 } from "zod";
258
- var juniorToolContinuationSchema = z3.object({
259
- arguments: z3.record(z3.string(), z3.unknown()),
260
- reason: z3.string().min(1).optional()
261
- }).strict();
262
- var juniorToolBoundContinuationSchema = juniorToolContinuationSchema.extend({
263
- tool_name: z3.string().min(1)
264
- });
265
- var juniorToolOutputSchema = z3.object({
266
- target: z3.string().min(1).optional(),
267
- truncated: z3.boolean().optional(),
268
- /** True when this attempt did not finish before its time budget. */
269
- timed_out: z3.boolean().optional(),
270
- continuation: juniorToolContinuationSchema.optional()
271
- }).passthrough();
272
- function sortJsonValue(value) {
273
- if (Array.isArray(value)) {
274
- return value.map(sortJsonValue);
275
- }
276
- if (!value || typeof value !== "object") {
277
- return value;
278
- }
279
- return Object.fromEntries(
280
- Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, sortJsonValue(item)])
281
- );
282
- }
283
- function isRecord(value) {
284
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
285
- }
286
- function injectContinuationToolName(details, toolName) {
287
- if (!isRecord(details) || !isRecord(details.continuation)) {
288
- return details;
289
- }
290
- return {
291
- ...details,
292
- continuation: juniorToolBoundContinuationSchema.parse({
293
- ...details.continuation,
294
- tool_name: toolName
295
- })
296
- };
297
- }
298
- function makeStructuredToolOutput(details, options = {}) {
299
- return {
300
- content: [
301
- {
302
- type: "text",
303
- text: JSON.stringify(sortJsonValue(details))
304
- },
305
- ...options.content ?? []
306
- ],
307
- details
308
- };
309
- }
310
-
311
- // src/chat/event-tasks/tool-support.ts
312
- import { z as z4 } from "zod";
313
- var compactEventTaskResultSchema = z4.object({
314
- id: z4.string().min(1),
315
- task: z4.string().min(1),
316
- namespace: z4.string().min(1),
317
- identifier: z4.string().min(1),
318
- resourceType: z4.string().min(1),
319
- label: z4.string().min(1),
320
- events: z4.array(z4.string().min(1)).min(1),
321
- credentialMode: z4.enum(["system", "creator"]),
322
- createdBy: eventTaskPrincipalSchema,
323
- triggerAvailable: z4.boolean()
324
- }).strict();
325
- var eventTaskToolResultSchema = juniorToolOutputSchema.extend({
326
- task: compactEventTaskResultSchema
327
- }).strict();
328
- var eventTaskListToolResultSchema = juniorToolOutputSchema.extend({
329
- tasks: z4.array(compactEventTaskResultSchema),
330
- truncated: z4.boolean()
331
- }).strict();
332
- function registeredEventTaskTriggerSchema(catalog) {
333
- return z4.object({
334
- namespace: eventNamespaceSchema(catalog),
335
- identifier: z4.string().trim().min(1).max(EVENT_TASK_IDENTIFIER_MAX_LENGTH),
336
- resourceType: registeredResourceTypeSchema(catalog),
337
- label: z4.string().trim().min(1).max(500),
338
- events: z4.array(registeredEventTypeSchema(catalog)).min(1)
339
- }).strict();
340
- }
341
- function requireSupportedEventTaskTrigger(catalog, trigger) {
342
- for (const eventType of trigger.events) {
343
- if (!pluginSupportsEvent(
344
- catalog,
345
- trigger.namespace,
346
- trigger.resourceType,
347
- eventType
348
- )) {
349
- throw new ToolInputError(
350
- `Resource type "${trigger.namespace}:${trigger.resourceType}" does not support event "${eventType}".`
351
- );
352
- }
353
- }
354
- }
355
- function requireEventTaskSlackContext(context) {
356
- if (context.source.platform !== "slack" || context.destination.platform !== "slack" || context.actor?.platform !== "slack" || context.actor.teamId !== context.source.teamId) {
357
- throw new ToolInputError(
358
- "Event tasks require an active Slack channel or DM and actor."
359
- );
360
- }
361
- return {
362
- actor: context.actor,
363
- destination: {
364
- platform: "slack",
365
- teamId: context.source.teamId,
366
- channelId: context.source.channelId
367
- },
368
- source: context.source
369
- };
370
- }
371
- function eventTaskMatchesDestination(task, destination) {
372
- return task.destination.teamId === destination.teamId && task.destination.channelId === destination.channelId;
373
- }
374
- async function writableEventTask(context, id) {
375
- const { destination } = requireEventTaskSlackContext(context);
376
- const task = await getEventTask(getDb(), id);
377
- if (!task || !eventTaskMatchesDestination(task, destination)) {
378
- throw new ToolInputError(
379
- "Event task was not found in this Slack channel or DM."
380
- );
381
- }
382
- return task;
383
- }
384
- function eventTaskTriggerAvailable(task, catalog) {
385
- const registration = catalog[task.trigger.namespace];
386
- return Boolean(
387
- registration && task.trigger.events.every(
388
- (eventType) => registration.resourceTypes.some(
389
- (resourceType) => resourceType.supportedEvents.includes(eventType)
390
- )
391
- )
392
- );
393
- }
394
- function compactEventTask(task, catalog) {
395
- return compactEventTaskResultSchema.parse({
396
- id: task.id,
397
- task: task.task.text,
398
- namespace: task.trigger.namespace,
399
- identifier: task.trigger.identifier,
400
- resourceType: task.trigger.resourceType,
401
- label: task.trigger.label,
402
- events: task.trigger.events,
403
- credentialMode: task.credentialMode,
404
- createdBy: task.createdBy,
405
- triggerAvailable: eventTaskTriggerAvailable(task, catalog)
406
- });
407
- }
408
- function eventTaskSuccess(task, catalog) {
409
- return {
410
- task: compactEventTask(task, catalog)
411
- };
412
- }
413
-
414
- // src/chat/scheduled-tasks/store.ts
415
- import {
416
- destinationSchema,
417
- isSlackDestination,
418
- slackActorSchema as slackActorSchema2
419
- } from "@sentry/junior-plugin-api";
420
- import {
421
- and as and3,
422
- asc as asc3,
423
- desc as desc2,
424
- eq as eq2,
425
- inArray as inArray2,
426
- isNotNull,
427
- lt,
428
- lte as lte2,
429
- notInArray,
430
- or as or2,
431
- sql as sql3
432
- } from "drizzle-orm";
433
- import { z as z6 } from "zod";
434
-
435
- // src/chat/scheduled-tasks/cadence.ts
436
- var FORMATTERS = /* @__PURE__ */ new Map();
437
- function getFormatter(timezone) {
438
- const existing = FORMATTERS.get(timezone);
439
- if (existing) {
440
- return existing;
441
- }
442
- const formatter = new Intl.DateTimeFormat("en-US", {
443
- timeZone: timezone,
444
- hour12: false,
445
- year: "numeric",
446
- month: "2-digit",
447
- day: "2-digit",
448
- hour: "2-digit",
449
- minute: "2-digit",
450
- second: "2-digit"
451
- });
452
- FORMATTERS.set(timezone, formatter);
453
- return formatter;
454
- }
455
- function normalizeHour(hour) {
456
- return hour === 24 ? 0 : hour;
457
- }
458
- function getLocalDateWeekday(date) {
459
- return new Date(Date.UTC(date.year, date.month - 1, date.day)).getUTCDay();
460
- }
461
- function getWeekStart(date) {
462
- return addDays(date, -((getLocalDateWeekday(date) + 6) % 7));
463
- }
464
- function getZonedDateTimeParts(timestampMs, timezone) {
465
- const parts = getFormatter(timezone).formatToParts(new Date(timestampMs));
466
- const values = new Map(parts.map((part) => [part.type, part.value]));
467
- const year = Number(values.get("year"));
468
- const month = Number(values.get("month"));
469
- const day = Number(values.get("day"));
470
- const hour = normalizeHour(Number(values.get("hour")));
471
- const minute = Number(values.get("minute"));
472
- const second = Number(values.get("second"));
473
- return {
474
- year,
475
- month,
476
- day,
477
- hour,
478
- minute,
479
- second,
480
- weekday: getLocalDateWeekday({ year, month, day })
481
- };
482
- }
483
- function getTimeZoneOffsetMs(timestampMs, timezone) {
484
- const parts = getZonedDateTimeParts(timestampMs, timezone);
485
- return Date.UTC(
486
- parts.year,
487
- parts.month - 1,
488
- parts.day,
489
- parts.hour,
490
- parts.minute,
491
- parts.second
492
- ) - timestampMs;
493
- }
494
- function localDateTimeToTimestampMs(args) {
495
- const localAsUtcMs = Date.UTC(
496
- args.date.year,
497
- args.date.month - 1,
498
- args.date.day,
499
- args.time.hour,
500
- args.time.minute,
501
- 0
502
- );
503
- const offsets = /* @__PURE__ */ new Set();
504
- for (const probeDeltaMs of [
505
- -36 * 60 * 60 * 1e3,
506
- -12 * 60 * 60 * 1e3,
507
- 0,
508
- 12 * 60 * 60 * 1e3,
509
- 36 * 60 * 60 * 1e3
510
- ]) {
511
- offsets.add(
512
- getTimeZoneOffsetMs(localAsUtcMs + probeDeltaMs, args.timezone)
513
- );
514
- }
515
- const matches = [...offsets].map((offsetMs) => localAsUtcMs - offsetMs).filter((timestampMs) => {
516
- const parts = getZonedDateTimeParts(timestampMs, args.timezone);
517
- return parts.year === args.date.year && parts.month === args.date.month && parts.day === args.date.day && parts.hour === args.time.hour && parts.minute === args.time.minute;
518
- });
519
- if (matches.length === 0) {
520
- return void 0;
521
- }
522
- return Math.min(...matches);
523
- }
524
- function resolveLocalScheduleAtMs(args) {
525
- const date = parseLocalDate(args.date);
526
- if (!date) {
527
- return void 0;
528
- }
529
- return localDateTimeToTimestampMs({
530
- date,
531
- time: args.time,
532
- timezone: args.timezone
533
- });
534
- }
535
- function daysBetween(left, right) {
536
- return Math.floor(
537
- (Date.UTC(right.year, right.month - 1, right.day) - Date.UTC(left.year, left.month - 1, left.day)) / (24 * 60 * 60 * 1e3)
538
- );
539
- }
540
- function weeklyRecurrenceMatchesDate(date, start, recurrence) {
541
- if (compareDate(date, start) < 0) {
542
- return false;
543
- }
544
- return normalizeWeekdays(recurrence.weekdays).includes(
545
- getLocalDateWeekday(date)
546
- ) && Math.floor(daysBetween(getWeekStart(start), getWeekStart(date)) / 7) % recurrence.interval === 0;
547
- }
548
- function greatestCommonDivisor(left, right) {
549
- let a = Math.abs(left);
550
- let b = Math.abs(right);
551
- while (b !== 0) {
552
- [a, b] = [b, a % b];
553
- }
554
- return a;
555
- }
556
- function findNextRunAtMs(args) {
557
- const start = parseLocalDate(args.recurrence.startDate);
558
- const interval = args.recurrence.interval;
559
- if (!start || !Number.isInteger(interval) || interval <= 0) {
560
- return void 0;
561
- }
562
- const searchFrom = compareDate(args.searchFrom, start) < 0 ? start : args.searchFrom;
563
- if (args.recurrence.frequency === "daily") {
564
- const offsetDays = daysBetween(start, searchFrom);
565
- let candidateDate = addDays(
566
- start,
567
- Math.ceil(offsetDays / interval) * interval
568
- );
569
- const gregorianCycleDays = 146097;
570
- const candidateCount2 = gregorianCycleDays / greatestCommonDivisor(gregorianCycleDays, interval);
571
- for (let attempts = 0; attempts < candidateCount2; attempts += 1) {
572
- const candidate = buildCandidate({
573
- date: candidateDate,
574
- recurrence: args.recurrence,
575
- timezone: args.timezone
576
- });
577
- if (candidate !== void 0 && candidate > args.afterMs) {
578
- return candidate;
579
- }
580
- candidateDate = addDays(candidateDate, interval);
581
- }
582
- return void 0;
583
- }
584
- if (args.recurrence.frequency === "weekly") {
585
- if (normalizeWeekdays(args.recurrence.weekdays).length === 0) {
586
- return void 0;
587
- }
588
- let candidateDate = searchFrom;
589
- const gregorianCycleDays = 146097;
590
- const cadenceDays = interval * 7;
591
- const searchDays = gregorianCycleDays * cadenceDays / greatestCommonDivisor(gregorianCycleDays, cadenceDays);
592
- for (let attempts = 0; attempts < searchDays; attempts += 1) {
593
- if (weeklyRecurrenceMatchesDate(candidateDate, start, args.recurrence)) {
594
- const candidate = buildCandidate({
595
- date: candidateDate,
596
- recurrence: args.recurrence,
597
- timezone: args.timezone
598
- });
599
- if (candidate !== void 0 && candidate > args.afterMs) {
600
- return candidate;
601
- }
602
- }
603
- candidateDate = addDays(candidateDate, 1);
604
- }
605
- return void 0;
606
- }
607
- if (args.recurrence.frequency === "monthly") {
608
- const startMonth = start.year * 12 + start.month - 1;
609
- const searchMonth = searchFrom.year * 12 + searchFrom.month - 1;
610
- let candidateMonth = startMonth + Math.ceil((searchMonth - startMonth) / interval) * interval;
611
- const gregorianCycleMonths = 4800;
612
- const candidateCount2 = gregorianCycleMonths / greatestCommonDivisor(gregorianCycleMonths, interval);
613
- for (let attempts = 0; attempts < candidateCount2; attempts += 1) {
614
- const candidateDate = {
615
- year: Math.floor(candidateMonth / 12),
616
- month: candidateMonth % 12 + 1,
617
- day: args.recurrence.dayOfMonth ?? 0
618
- };
619
- if (compareDate(candidateDate, searchFrom) >= 0) {
620
- const candidate = buildCandidate({
621
- date: candidateDate,
622
- recurrence: args.recurrence,
623
- timezone: args.timezone
624
- });
625
- if (candidate !== void 0 && candidate > args.afterMs) {
626
- return candidate;
627
- }
628
- }
629
- candidateMonth += interval;
630
- }
631
- return void 0;
632
- }
633
- let candidateYear = start.year + Math.ceil((searchFrom.year - start.year) / interval) * interval;
634
- const candidateCount = 400 / greatestCommonDivisor(400, interval);
635
- for (let attempts = 0; attempts < candidateCount; attempts += 1) {
636
- const candidateDate = {
637
- year: candidateYear,
638
- month: args.recurrence.month ?? 0,
639
- day: args.recurrence.dayOfMonth ?? 0
640
- };
641
- if (compareDate(candidateDate, searchFrom) >= 0) {
642
- const candidate = buildCandidate({
643
- date: candidateDate,
644
- recurrence: args.recurrence,
645
- timezone: args.timezone
646
- });
647
- if (candidate !== void 0 && candidate > args.afterMs) {
648
- return candidate;
649
- }
650
- }
651
- candidateYear += interval;
652
- }
653
- return void 0;
654
- }
655
- function getFirstRunAtMs(args) {
656
- return findNextRunAtMs({
657
- ...args,
658
- searchFrom: getLocalDate(args.afterMs, args.timezone)
659
- });
660
- }
661
- function compareDate(left, right) {
662
- return Date.UTC(left.year, left.month - 1, left.day) - Date.UTC(right.year, right.month - 1, right.day);
663
- }
664
- function addDays(date, days) {
665
- const next = new Date(Date.UTC(date.year, date.month - 1, date.day + days));
666
- return {
667
- year: next.getUTCFullYear(),
668
- month: next.getUTCMonth() + 1,
669
- day: next.getUTCDate()
670
- };
671
- }
672
- function daysInMonth(year, month) {
673
- return new Date(Date.UTC(year, month, 0)).getUTCDate();
674
- }
675
- function parseLocalDate(value) {
676
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
677
- if (!match) {
678
- return void 0;
679
- }
680
- const year = Number(match[1]);
681
- const month = Number(match[2]);
682
- const day = Number(match[3]);
683
- if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day) || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {
684
- return void 0;
685
- }
686
- return { year, month, day };
687
- }
688
- function getLocalDate(timestampMs, timezone) {
689
- const parts = getZonedDateTimeParts(timestampMs, timezone);
690
- return { year: parts.year, month: parts.month, day: parts.day };
691
- }
692
- function normalizeWeekdays(values) {
693
- return [
694
- ...new Set((values ?? []).filter((value) => value >= 0 && value <= 6))
695
- ].sort((a, b) => a - b);
696
- }
697
- function buildCandidate(args) {
698
- return localDateTimeToTimestampMs({
699
- date: args.date,
700
- time: args.recurrence.time,
701
- timezone: args.timezone
702
- });
703
- }
704
- function getNextRunAtMs(task, scheduledForMs, afterMs = scheduledForMs) {
705
- if (task.schedule.kind !== "recurring") {
706
- return void 0;
707
- }
708
- const recurrence = task.schedule.recurrence;
709
- if (!recurrence || !Number.isFinite(recurrence.interval) || recurrence.interval <= 0) {
710
- return void 0;
711
- }
712
- const timezone = task.schedule.timezone;
713
- const afterDate = getLocalDate(afterMs, timezone);
714
- const nextScheduledDate = addDays(getLocalDate(scheduledForMs, timezone), 1);
715
- return findNextRunAtMs({
716
- afterMs,
717
- recurrence,
718
- searchFrom: compareDate(afterDate, nextScheduledDate) < 0 ? nextScheduledDate : afterDate,
719
- timezone
720
- });
721
- }
722
-
723
- // src/chat/scheduled-tasks/telemetry.ts
724
- function scheduledTaskAttributes(task, extras = {}) {
725
- return {
726
- "app.task.id": task.id,
727
- "app.task.type": "scheduled",
728
- "app.task.status": task.status,
729
- "app.task.schedule.kind": task.schedule.kind,
730
- "app.task.schedule.timezone": task.schedule.timezone,
731
- "app.task.credential_mode": task.credentialMode,
732
- "app.task.destination.channel_id": task.destination.channelId,
733
- "app.task.destination.team_id": task.destination.teamId,
734
- "app.task.destination.visibility": task.conversationAccess.visibility,
735
- "app.task.destination.audience": task.conversationAccess.audience,
736
- "messaging.destination.name": task.destination.channelId,
737
- ...typeof task.nextRunAtMs === "number" ? { "app.task.next_run_at": new Date(task.nextRunAtMs).toISOString() } : {},
738
- ...extras
739
- };
740
- }
741
- function scheduledTaskRunAttributes(task, run, extras = {}) {
742
- return scheduledTaskAttributes(task, {
743
- "app.task.run.id": run.id,
744
- "app.task.run.status": run.status,
745
- "app.task.run.scheduled_for": new Date(run.scheduledForMs).toISOString(),
746
- ...run.dispatchId ? { "app.dispatch.id": run.dispatchId } : {},
747
- ...run.resultMessageTs ? { "app.task.result_message_ts": run.resultMessageTs } : {},
748
- ...extras
749
- });
750
- }
751
- function logScheduledTaskRunSkipped(task, run, extras = {}) {
752
- logInfo(
753
- "scheduled_task.run.skipped",
754
- scheduledTaskRunAttributes(task, run, extras)
755
- );
756
- }
757
-
758
- // src/chat/scheduled-tasks/types.ts
759
- import { z as z5 } from "zod";
760
- var scheduledTaskCredentialModeSchema = z5.enum(["system", "creator"]);
761
- var SCHEDULED_TASK_SYSTEM_ACTOR = Object.freeze({
762
- platform: "system",
763
- name: "scheduled-task"
764
- });
765
-
766
- // src/chat/scheduled-tasks/store.ts
767
- var SCHEDULER_KEY_PREFIX = "junior:scheduler";
768
- var SCHEDULER_RECORD_TTL_MS = 5 * 365 * 24 * 60 * 60 * 1e3;
769
- var SCHEDULED_RUN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
770
- var CLAIM_TTL_MS = 6 * 60 * 60 * 1e3;
771
- var PENDING_CLAIM_STALE_MS = 6e4;
772
- var MISSED_RUN_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
773
- var SQL_INCOMPLETE_RUN_STATUSES = ["pending", "running"];
774
- var slackDestinationSchema2 = destinationSchema.refine(isSlackDestination);
775
- var taskPrincipalSchema = z6.object({
776
- slackUserId: slackActorSchema2.shape.userId,
777
- fullName: z6.string().optional(),
778
- userName: z6.string().optional()
779
- }).strict();
780
- var recurrenceSchema = z6.object({
781
- dayOfMonth: z6.number().optional(),
782
- frequency: z6.enum(["daily", "weekly", "monthly", "yearly"]),
783
- interval: z6.number(),
784
- month: z6.number().optional(),
785
- startDate: z6.string(),
786
- time: z6.object({
787
- hour: z6.number(),
788
- minute: z6.number()
789
- }).strict(),
790
- weekdays: z6.array(z6.number()).optional()
791
- }).strict();
792
- var taskScheduleSchema = z6.object({
793
- description: z6.string(),
794
- kind: z6.enum(["one_off", "recurring"]),
795
- recurrence: recurrenceSchema.optional(),
796
- timezone: z6.string()
797
- }).strict();
798
- var taskSpecSchema = z6.object({
799
- text: z6.string()
800
- }).strict();
801
- var taskRecordFields = {
802
- id: z6.string(),
803
- conversationAccess: z6.object({
804
- audience: z6.enum(["direct", "group", "channel"]),
805
- visibility: z6.enum(["private", "public"])
806
- }).strict(),
807
- createdAtMs: z6.number(),
808
- createdBy: taskPrincipalSchema,
809
- creatorIdentityId: z6.string(),
810
- destination: slackDestinationSchema2,
811
- executionActor: z6.object({
812
- platform: z6.literal("system"),
813
- name: z6.string()
814
- }).strict().optional(),
815
- lastRunAtMs: z6.number().optional(),
816
- nextRunAtMs: z6.number().optional(),
817
- originalRequest: z6.string().optional(),
818
- runNowAtMs: z6.number().optional(),
819
- schedule: taskScheduleSchema,
820
- // TODO(v0.131.0): Remove paused decoding and SQL list filtering after
821
- // v0.129.x workers can no longer overlap upgrades.
822
- status: z6.enum(["active", "blocked", "completed", "deleted", "paused"]),
823
- statusReason: z6.string().optional(),
824
- task: taskSpecSchema,
825
- updatedAtMs: z6.number(),
826
- version: z6.number().optional()
827
- };
828
- var taskRecordSchema = z6.object({
829
- ...taskRecordFields,
830
- credentialMode: scheduledTaskCredentialModeSchema
831
- }).strict();
832
- var runRecordSchema = z6.object({
833
- id: z6.string(),
834
- attempt: z6.number(),
835
- claimedAtMs: z6.number(),
836
- completedAtMs: z6.number().optional(),
837
- dispatchId: z6.string().optional(),
838
- errorMessage: z6.string().optional(),
839
- idempotencyKey: z6.string().optional(),
840
- resultMessageTs: z6.string().optional(),
841
- scheduledForMs: z6.number(),
842
- startedAtMs: z6.number().optional(),
843
- status: z6.enum([
844
- "pending",
845
- "running",
846
- "completed",
847
- "failed",
848
- "blocked",
849
- "skipped"
850
- ]),
851
- taskId: z6.string(),
852
- taskVersion: z6.number().optional()
853
- }).strict();
854
- function normalizedTaskQuery(query) {
855
- return query?.trim().toLowerCase() || void 0;
856
- }
857
- function taskKey(taskId) {
858
- return `${SCHEDULER_KEY_PREFIX}:task:${taskId}`;
859
- }
860
- function taskLockKey(taskId) {
861
- return `${taskKey(taskId)}:lock`;
862
- }
863
- function runKey(runId) {
864
- return `${SCHEDULER_KEY_PREFIX}:run:${runId}`;
865
- }
866
- function indexLockKey(indexKey) {
867
- return `${indexKey}:lock`;
868
- }
869
- function buildRunId(taskId, scheduledForMs) {
870
- return `${taskId}:${scheduledForMs}`;
871
- }
872
- var schedulerTaskIndexSchema = z6.array(z6.string().min(1));
873
- function statusAfterTerminalOccurrence(args) {
874
- if (args.blocked || args.outcome === "blocked") {
875
- return "blocked";
876
- }
877
- if (args.nextRunAtMs) {
878
- return args.previousStatus ?? "active";
879
- }
880
- if (args.outcome === "completed") {
881
- return "completed";
882
- }
883
- return "deleted";
884
- }
885
- function isStalePendingRun(run, nowMs) {
886
- return run?.status === "pending" && run.claimedAtMs + PENDING_CLAIM_STALE_MS <= nowMs;
887
- }
888
- function getDueRunAtMs(task, nowMs) {
889
- if (typeof task.runNowAtMs === "number" && Number.isFinite(task.runNowAtMs) && task.runNowAtMs <= nowMs) {
890
- return task.runNowAtMs;
891
- }
892
- if (typeof task.nextRunAtMs === "number" && Number.isFinite(task.nextRunAtMs) && task.nextRunAtMs <= nowMs) {
893
- return task.nextRunAtMs;
894
- }
895
- return void 0;
896
- }
897
- function buildScheduledRun(args) {
898
- return {
899
- id: buildRunId(args.task.id, args.scheduledForMs),
900
- attempt: 1,
901
- claimedAtMs: args.claimedAtMs,
902
- scheduledForMs: args.scheduledForMs,
903
- status: "pending",
904
- taskId: args.task.id
905
- };
906
- }
907
- function buildSkippedScheduledRun(args) {
908
- return {
909
- ...buildScheduledRun({
910
- claimedAtMs: args.completedAtMs,
911
- scheduledForMs: args.scheduledForMs,
912
- task: args.task
913
- }),
914
- completedAtMs: args.completedAtMs,
915
- errorMessage: args.errorMessage,
916
- status: "skipped"
917
- };
918
- }
919
- function isMissedRunTooOld(args) {
920
- return args.scheduledForMs + MISSED_RUN_MAX_AGE_MS < args.nowMs;
921
- }
922
- function normalizedText(value) {
923
- return value?.trim().replace(/\s+/g, " ").toLowerCase() ?? "";
924
- }
925
- function taskDedupeFingerprint(task) {
926
- return JSON.stringify({
927
- credentialMode: task.credentialMode,
928
- credentialUserId: task.credentialMode === "creator" ? task.createdBy.slackUserId : null,
929
- destination: task.destination,
930
- schedule: {
931
- kind: task.schedule.kind,
932
- oneOffAtMs: task.schedule.kind === "one_off" ? task.nextRunAtMs : null,
933
- recurrence: task.schedule.recurrence ? {
934
- dayOfMonth: task.schedule.recurrence.dayOfMonth ?? null,
935
- frequency: task.schedule.recurrence.frequency,
936
- interval: task.schedule.recurrence.interval,
937
- month: task.schedule.recurrence.month ?? null,
938
- startDate: task.schedule.recurrence.startDate,
939
- time: task.schedule.recurrence.time,
940
- weekdays: [...task.schedule.recurrence.weekdays ?? []].sort()
941
- } : null,
942
- timezone: task.schedule.timezone
943
- },
944
- task: normalizedText(task.task.text)
945
- });
946
- }
947
- function isEarlierTask(left, right) {
948
- return left.createdAtMs < right.createdAtMs || left.createdAtMs === right.createdAtMs && left.id < right.id;
949
- }
950
- function canFinishRun(run, startedAtMs) {
951
- if (run.status === "pending") {
952
- return startedAtMs === void 0;
953
- }
954
- return run.status === "running" && run.startedAtMs === startedAtMs;
955
- }
956
- function parseStoredTask(value, title) {
957
- const record = parseJsonRecord(value);
958
- if (!record) return void 0;
959
- const { title: recordTitle, ...rest } = record;
960
- const parsed = taskRecordSchema.safeParse(rest);
961
- if (!parsed.success) return void 0;
962
- const storedTitle = title ?? (typeof recordTitle === "string" ? recordTitle : null);
963
- return stripLegacyTaskFields(parsed.data, storedTitle);
964
- }
965
- function stripLegacyTaskFields(task, title) {
966
- const { version: _version, status, ...current } = task;
967
- const storedTitle = title?.trim() || void 0;
968
- if (status === "paused") {
969
- const {
970
- nextRunAtMs: _nextRunAtMs,
971
- runNowAtMs: _runNowAtMs,
972
- ...rest
973
- } = current;
974
- return {
975
- ...rest,
976
- status: "deleted",
977
- ...storedTitle ? { title: storedTitle } : {}
978
- };
979
- }
980
- return {
981
- ...current,
982
- status,
983
- ...storedTitle ? { title: storedTitle } : {}
984
- };
985
- }
986
- function scheduledTaskJsonRecord(task) {
987
- const { title: _title, ...record } = task;
988
- return record;
989
- }
990
- function stripLegacyRunFields(run) {
991
- const {
992
- idempotencyKey: _idempotencyKey,
993
- taskVersion: _taskVersion,
994
- ...current
995
- } = run;
996
- return current;
997
- }
998
- function parseJsonRecord(value) {
999
- if (typeof value === "string") {
1000
- try {
1001
- return JSON.parse(value);
1002
- } catch {
1003
- return void 0;
1004
- }
1005
- }
1006
- if (value && typeof value === "object") {
1007
- return value;
1008
- }
1009
- return void 0;
1010
- }
1011
- function present(value) {
1012
- return value !== void 0;
1013
- }
1014
- function listedScheduledTask(task) {
1015
- return task !== void 0 && task.status !== "deleted";
1016
- }
1017
- function requireStoredTask(task) {
1018
- const parsed = parseStoredTask(task, task.title);
1019
- if (!parsed) {
1020
- throw new Error("Scheduled task routing context is invalid.");
1021
- }
1022
- return parsed;
1023
- }
1024
- function parseSqlTaskRecord(value, creatorIdentityId, title) {
1025
- const record = parseJsonRecord(value);
1026
- const withIdentity = record && creatorIdentityId ? { ...record, creatorIdentityId } : record;
1027
- return parseStoredTask(withIdentity, title);
1028
- }
1029
- function parseSqlTaskRow(row) {
1030
- return parseSqlTaskRecord(row.record, row.creatorIdentityId, row.title);
1031
- }
1032
- function parseSqlRunRow(row) {
1033
- const parsed = runRecordSchema.safeParse(parseJsonRecord(row.record));
1034
- return parsed.success ? stripLegacyRunFields(parsed.data) : void 0;
1035
- }
1036
- async function withSqlLock(db, key, callback) {
1037
- return await db.transaction(async (tx) => {
1038
- await tx.execute(sql3`SELECT pg_advisory_xact_lock(hashtext(${key}))`);
1039
- return await callback(tx);
1040
- });
1041
- }
1042
- async function upsertSqlTask(db, task) {
1043
- const title = task.title?.trim() || null;
1044
- await db.insert(juniorSchedulerTasks).values({
1045
- createdAtMs: task.createdAtMs,
1046
- creatorIdentityId: task.creatorIdentityId,
1047
- // TODO(v0.128.0): Remove after v0.127.x runtimes no longer read this column.
1048
- creatorSlackUserId: task.createdBy.slackUserId,
1049
- id: task.id,
1050
- nextRunAtMs: task.nextRunAtMs,
1051
- record: scheduledTaskJsonRecord(task),
1052
- runNowAtMs: task.runNowAtMs,
1053
- status: task.status,
1054
- teamId: task.destination.teamId,
1055
- title
1056
- }).onConflictDoUpdate({
1057
- target: juniorSchedulerTasks.id,
1058
- set: {
1059
- createdAtMs: sql3`excluded.created_at_ms`,
1060
- creatorIdentityId: sql3`excluded.creator_identity_id`,
1061
- creatorSlackUserId: sql3`excluded.creator_slack_user_id`,
1062
- nextRunAtMs: sql3`excluded.next_run_at_ms`,
1063
- record: sql3`excluded.record`,
1064
- runNowAtMs: sql3`excluded.run_now_at_ms`,
1065
- status: sql3`excluded.status`,
1066
- teamId: sql3`excluded.team_id`,
1067
- title: sql3`excluded.title`
1068
- }
1069
- });
1070
- }
1071
- async function upsertSqlRun(db, run) {
1072
- await db.insert(juniorSchedulerRuns).values({
1073
- id: run.id,
1074
- record: run,
1075
- scheduledForMs: run.scheduledForMs,
1076
- status: run.status,
1077
- taskId: run.taskId
1078
- }).onConflictDoUpdate({
1079
- target: juniorSchedulerRuns.id,
1080
- set: {
1081
- record: sql3`excluded.record`,
1082
- scheduledForMs: sql3`excluded.scheduled_for_ms`,
1083
- status: sql3`excluded.status`,
1084
- taskId: sql3`excluded.task_id`
1085
- }
1086
- });
1087
- }
1088
- async function getTaskFromSql(db, taskId) {
1089
- const rows = await db.select({
1090
- creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
1091
- record: juniorSchedulerTasks.record,
1092
- title: juniorSchedulerTasks.title
1093
- }).from(juniorSchedulerTasks).where(eq2(juniorSchedulerTasks.id, taskId)).limit(1);
1094
- return rows[0] ? parseSqlTaskRow(rows[0]) : void 0;
1095
- }
1096
- async function getRunFromSql(db, runId) {
1097
- const rows = await db.select({ record: juniorSchedulerRuns.record }).from(juniorSchedulerRuns).where(eq2(juniorSchedulerRuns.id, runId)).limit(1);
1098
- return rows[0] ? parseSqlRunRow(rows[0]) : void 0;
1099
- }
1100
- async function listTasksFromSql(db) {
1101
- const rows = await db.select({
1102
- creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
1103
- record: juniorSchedulerTasks.record,
1104
- title: juniorSchedulerTasks.title
1105
- }).from(juniorSchedulerTasks).where(notInArray(juniorSchedulerTasks.status, ["deleted", "paused"])).orderBy(
1106
- asc3(juniorSchedulerTasks.createdAtMs),
1107
- asc3(juniorSchedulerTasks.id)
1108
- );
1109
- return rows.map(parseSqlTaskRow).filter(listedScheduledTask);
1110
- }
1111
- async function listTasksCreatedByFromSql(db, input) {
1112
- if (input.identityIds.length === 0) return [];
1113
- const query = normalizedTaskQuery(input.query);
1114
- const tasks = [];
1115
- let before = input.before;
1116
- while (tasks.length < input.limit) {
1117
- const cursor = before ? or2(
1118
- lt(juniorSchedulerTasks.createdAtMs, before.createdAtMs),
1119
- and3(
1120
- eq2(juniorSchedulerTasks.createdAtMs, before.createdAtMs),
1121
- lt(juniorSchedulerTasks.id, before.id)
1122
- )
1123
- ) : void 0;
1124
- const search = query ? or2(
1125
- sql3`strpos(lower(${juniorSchedulerTasks.title}), ${query}) > 0`,
1126
- sql3`strpos(lower(${juniorSchedulerTasks.record}->'task'->>'text'), ${query}) > 0`,
1127
- sql3`strpos(lower(${juniorSchedulerTasks.record}->'schedule'->>'description'), ${query}) > 0`,
1128
- sql3`strpos(lower(${juniorSchedulerTasks.record}->'schedule'->>'timezone'), ${query}) > 0`,
1129
- sql3`strpos(lower(${juniorSchedulerTasks.status}), ${query}) > 0`
1130
- ) : void 0;
1131
- const rows = await db.select({
1132
- createdAtMs: juniorSchedulerTasks.createdAtMs,
1133
- creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
1134
- id: juniorSchedulerTasks.id,
1135
- record: juniorSchedulerTasks.record,
1136
- title: juniorSchedulerTasks.title
1137
- }).from(juniorSchedulerTasks).where(
1138
- and3(
1139
- notInArray(juniorSchedulerTasks.status, ["deleted", "paused"]),
1140
- inArray2(juniorSchedulerTasks.creatorIdentityId, input.identityIds),
1141
- cursor,
1142
- search
1143
- )
1144
- ).orderBy(
1145
- desc2(juniorSchedulerTasks.createdAtMs),
1146
- desc2(juniorSchedulerTasks.id)
1147
- ).limit(input.limit);
1148
- tasks.push(...rows.map(parseSqlTaskRow).filter(listedScheduledTask));
1149
- if (rows.length < input.limit) {
1150
- break;
1151
- }
1152
- const last = rows.at(-1);
1153
- before = {
1154
- createdAtMs: last.createdAtMs,
1155
- id: last.id
1156
- };
1157
- }
1158
- return tasks.slice(0, input.limit);
1159
- }
1160
- async function listTasksForTeamFromSql(db, teamId) {
1161
- const rows = await db.select({
1162
- creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
1163
- record: juniorSchedulerTasks.record,
1164
- title: juniorSchedulerTasks.title
1165
- }).from(juniorSchedulerTasks).where(
1166
- and3(
1167
- eq2(juniorSchedulerTasks.teamId, teamId),
1168
- notInArray(juniorSchedulerTasks.status, ["deleted", "paused"])
1169
- )
1170
- ).orderBy(
1171
- asc3(juniorSchedulerTasks.createdAtMs),
1172
- asc3(juniorSchedulerTasks.id)
1173
- );
1174
- return rows.map(parseSqlTaskRow).filter(listedScheduledTask);
1175
- }
1176
- async function listPublicScheduledTasksForTeams(db, teamIds, limit) {
1177
- if (teamIds.length === 0) return [];
1178
- const rows = await db.select({
1179
- creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
1180
- record: juniorSchedulerTasks.record,
1181
- title: juniorSchedulerTasks.title
1182
- }).from(juniorSchedulerTasks).innerJoin(
1183
- juniorDestinations,
1184
- and3(
1185
- eq2(juniorDestinations.provider, "slack"),
1186
- eq2(juniorDestinations.providerTenantId, juniorSchedulerTasks.teamId),
1187
- sql3`${juniorDestinations.providerDestinationId} = ${juniorSchedulerTasks.record}->'destination'->>'channelId'`
1188
- )
1189
- ).where(
1190
- and3(
1191
- inArray2(juniorSchedulerTasks.teamId, teamIds),
1192
- notInArray(juniorSchedulerTasks.status, [
1193
- "completed",
1194
- "deleted",
1195
- "paused"
1196
- ]),
1197
- eq2(juniorDestinations.visibility, "public")
1198
- )
1199
- ).orderBy(
1200
- desc2(juniorSchedulerTasks.createdAtMs),
1201
- desc2(juniorSchedulerTasks.id)
1202
- ).limit(limit);
1203
- return rows.map(parseSqlTaskRow).filter(listedScheduledTask);
1204
- }
1205
- async function listIncompleteRunsForTasksFromSql(db, tasks) {
1206
- if (tasks.length === 0) {
1207
- return [];
1208
- }
1209
- const rows = await db.select({ record: juniorSchedulerRuns.record }).from(juniorSchedulerRuns).where(
1210
- and3(
1211
- inArray2(
1212
- juniorSchedulerRuns.taskId,
1213
- tasks.map((task) => task.id)
1214
- ),
1215
- inArray2(juniorSchedulerRuns.status, [...SQL_INCOMPLETE_RUN_STATUSES])
1216
- )
1217
- ).orderBy(
1218
- asc3(juniorSchedulerRuns.scheduledForMs),
1219
- asc3(juniorSchedulerRuns.id)
1220
- );
1221
- return rows.map(parseSqlRunRow).filter(present);
1222
- }
1223
- var SqlSchedulerStore = class {
1224
- constructor(db) {
1225
- this.db = db;
1226
- }
1227
- db;
1228
- async createTask(task) {
1229
- const next = requireStoredTask(task);
1230
- return await withSqlLock(this.db, taskLockKey(task.id), async (db) => {
1231
- const current = await getTaskFromSql(db, task.id);
1232
- if (current) {
1233
- return current;
1234
- }
1235
- await this.saveTaskRecord(db, next, void 0);
1236
- return next;
1237
- });
1238
- }
1239
- async saveTask(task) {
1240
- const next = requireStoredTask(task);
1241
- await withSqlLock(this.db, taskLockKey(task.id), async (db) => {
1242
- const current = await getTaskFromSql(db, task.id);
1243
- await this.saveTaskRecord(db, next, current);
1244
- });
1245
- }
1246
- async saveTaskRecord(db, task, current) {
1247
- if (current?.status === "blocked" && task.status === "active" && typeof task.nextRunAtMs === "number" && Number.isFinite(task.nextRunAtMs)) {
1248
- await db.delete(juniorSchedulerRuns).where(
1249
- and3(
1250
- eq2(juniorSchedulerRuns.id, buildRunId(task.id, task.nextRunAtMs)),
1251
- eq2(juniorSchedulerRuns.status, "blocked")
1252
- )
1253
- );
1254
- }
1255
- await upsertSqlTask(db, task);
1256
- }
1257
- async getTask(taskId) {
1258
- return await getTaskFromSql(this.db, taskId);
1259
- }
1260
- async listTasks() {
1261
- return await listTasksFromSql(this.db);
1262
- }
1263
- async listTasksCreatedBy(input) {
1264
- return await listTasksCreatedByFromSql(this.db, input);
1265
- }
1266
- async listTasksForTeam(teamId) {
1267
- return await listTasksForTeamFromSql(this.db, teamId);
1268
- }
1269
- async claimDueRun(args) {
1270
- const skippedRuns = [];
1271
- const claimed = await withSqlLock(
1272
- this.db,
1273
- "junior:scheduler:claim",
1274
- async (db) => {
1275
- const rows = await db.select({
1276
- creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
1277
- record: juniorSchedulerTasks.record,
1278
- title: juniorSchedulerTasks.title
1279
- }).from(juniorSchedulerTasks).where(
1280
- and3(
1281
- eq2(juniorSchedulerTasks.status, "active"),
1282
- or2(
1283
- and3(
1284
- isNotNull(juniorSchedulerTasks.runNowAtMs),
1285
- lte2(juniorSchedulerTasks.runNowAtMs, args.nowMs)
1286
- ),
1287
- and3(
1288
- isNotNull(juniorSchedulerTasks.nextRunAtMs),
1289
- lte2(juniorSchedulerTasks.nextRunAtMs, args.nowMs)
1290
- )
1291
- )
1292
- )
1293
- ).orderBy(
1294
- asc3(juniorSchedulerTasks.createdAtMs),
1295
- asc3(juniorSchedulerTasks.id)
1296
- );
1297
- for (const row of rows) {
1298
- const task = parseSqlTaskRow(row);
1299
- if (!task) {
1300
- continue;
1301
- }
1302
- const scheduledForMs = getDueRunAtMs(task, args.nowMs);
1303
- if (scheduledForMs === void 0) {
1304
- continue;
1305
- }
1306
- const runId = buildRunId(task.id, scheduledForMs);
1307
- const incompleteRuns = await listIncompleteRunsForTasksFromSql(db, [
1308
- task
1309
- ]);
1310
- const incompleteRun = incompleteRuns.find((run2) => run2.id === runId);
1311
- const blockingRun = incompleteRuns.find(
1312
- (run2) => run2.id !== runId && !isStalePendingRun(run2, args.nowMs)
1313
- );
1314
- if (blockingRun) {
1315
- continue;
1316
- }
1317
- if (incompleteRun) {
1318
- if (!isStalePendingRun(incompleteRun, args.nowMs)) {
1319
- continue;
1320
- }
1321
- const reclaimed = {
1322
- ...incompleteRun,
1323
- attempt: incompleteRun.attempt + 1,
1324
- claimedAtMs: args.nowMs
1325
- };
1326
- await upsertSqlRun(db, reclaimed);
1327
- return reclaimed;
1328
- }
1329
- const existingRun = await getRunFromSql(db, runId);
1330
- if (existingRun) {
1331
- continue;
1332
- }
1333
- if (isMissedRunTooOld({ nowMs: args.nowMs, scheduledForMs })) {
1334
- const skipped = await this.skipMissedRun(db, {
1335
- nowMs: args.nowMs,
1336
- scheduledForMs,
1337
- task
1338
- });
1339
- if (skipped) {
1340
- skippedRuns.push(skipped);
1341
- }
1342
- continue;
1343
- }
1344
- const run = buildScheduledRun({
1345
- claimedAtMs: args.nowMs,
1346
- scheduledForMs,
1347
- task
1348
- });
1349
- await upsertSqlRun(db, run);
1350
- return run;
1351
- }
1352
- return void 0;
1353
- }
1354
- );
1355
- for (const skipped of skippedRuns) {
1356
- logScheduledTaskRunSkipped(skipped.task, skipped.run, {
1357
- "app.task.run.error": skipped.errorMessage
1358
- });
1359
- }
1360
- return claimed;
1361
- }
1362
- async skipMissedRun(db, args) {
1363
- const current = await getTaskFromSql(db, args.task.id);
1364
- if (!current || current.status !== "active" || getDueRunAtMs(current, args.nowMs) !== args.scheduledForMs) {
1365
- return void 0;
1366
- }
1367
- const duplicateOf = await this.findStaleRecoveryCanonicalTask(db, current);
1368
- const errorMessage = duplicateOf ? `Duplicate stale scheduled task was skipped without dispatch. Canonical task: ${duplicateOf.id}.` : "Scheduled occurrence was more than 24 hours late and was skipped without dispatch.";
1369
- const skipped = buildSkippedScheduledRun({
1370
- completedAtMs: args.nowMs,
1371
- errorMessage,
1372
- scheduledForMs: args.scheduledForMs,
1373
- task: current
1374
- });
1375
- await upsertSqlRun(db, skipped);
1376
- const isRunNow = current.runNowAtMs === args.scheduledForMs;
1377
- let nextRunAtMs;
1378
- if (!duplicateOf) {
1379
- nextRunAtMs = isRunNow && current.nextRunAtMs !== args.scheduledForMs ? current.nextRunAtMs : current.schedule.kind === "recurring" ? getNextRunAtMs(current, args.scheduledForMs, args.nowMs) : void 0;
1380
- }
1381
- const nextStatus = statusAfterTerminalOccurrence({ nextRunAtMs });
1382
- await this.saveTaskRecord(
1383
- db,
1384
- {
1385
- ...current,
1386
- nextRunAtMs,
1387
- runNowAtMs: isRunNow ? void 0 : current.runNowAtMs,
1388
- status: nextStatus,
1389
- statusReason: nextStatus === "deleted" ? errorMessage : void 0,
1390
- updatedAtMs: args.nowMs
1391
- },
1392
- current
1393
- );
1394
- return { errorMessage, run: skipped, task: current };
1395
- }
1396
- async findStaleRecoveryCanonicalTask(db, task) {
1397
- const fingerprint = taskDedupeFingerprint(task);
1398
- const tasks = await listTasksForTeamFromSql(db, task.destination.teamId);
1399
- return tasks.filter((candidate) => candidate.id !== task.id).filter(
1400
- (candidate) => candidate.status === "active" && isEarlierTask(candidate, task) && taskDedupeFingerprint(candidate) === fingerprint
1401
- ).sort((a, b) => a.createdAtMs - b.createdAtMs || a.id.localeCompare(b.id)).at(0);
1402
- }
1403
- async getRun(runId) {
1404
- return await getRunFromSql(this.db, runId);
1405
- }
1406
- async listIncompleteRuns() {
1407
- return await listIncompleteRunsForTasksFromSql(
1408
- this.db,
1409
- await this.listTasks()
1410
- );
1411
- }
1412
- async listIncompleteRunsForTasks(tasks) {
1413
- return await listIncompleteRunsForTasksFromSql(this.db, tasks);
1414
- }
1415
- async markRunDispatched(args) {
1416
- return await this.updateRun(
1417
- args.runId,
1418
- (run) => run.status === "pending" && run.claimedAtMs === args.claimedAtMs ? {
1419
- ...run,
1420
- dispatchId: args.dispatchId,
1421
- startedAtMs: args.nowMs,
1422
- status: "running"
1423
- } : void 0
1424
- );
1425
- }
1426
- async markRunCompleted(args) {
1427
- const next = await this.updateRun(
1428
- args.runId,
1429
- (run) => canFinishRun(run, args.startedAtMs) ? {
1430
- ...run,
1431
- completedAtMs: args.completedAtMs,
1432
- resultMessageTs: args.resultMessageTs,
1433
- status: "completed"
1434
- } : void 0
1435
- );
1436
- return next;
1437
- }
1438
- async markRunFailed(args) {
1439
- return await this.updateRun(
1440
- args.runId,
1441
- (run) => canFinishRun(run, args.startedAtMs) ? {
1442
- ...run,
1443
- completedAtMs: args.completedAtMs,
1444
- errorMessage: args.errorMessage,
1445
- status: "failed"
1446
- } : void 0
1447
- );
1448
- }
1449
- async markRunSkipped(args) {
1450
- return await this.updateRun(
1451
- args.runId,
1452
- (run) => run.status === "pending" ? {
1453
- ...run,
1454
- completedAtMs: args.completedAtMs,
1455
- errorMessage: args.errorMessage,
1456
- status: "skipped"
1457
- } : void 0
1458
- );
1459
- }
1460
- async markRunBlocked(args) {
1461
- return await this.updateRun(
1462
- args.runId,
1463
- (run) => canFinishRun(run, args.startedAtMs) ? {
1464
- ...run,
1465
- completedAtMs: args.completedAtMs,
1466
- errorMessage: args.errorMessage,
1467
- status: "blocked"
1468
- } : void 0
1469
- );
1470
- }
1471
- async updateTaskAfterRun(args) {
1472
- await withSqlLock(this.db, taskLockKey(args.run.taskId), async (db) => {
1473
- const current = await getTaskFromSql(db, args.run.taskId);
1474
- if (!current || current.status === "deleted" || current.status === "completed") {
1475
- return;
1476
- }
1477
- const isRunNow = current.runNowAtMs === args.run.scheduledForMs;
1478
- if (isRunNow) {
1479
- let nextRunAtMs2 = current.nextRunAtMs;
1480
- if (args.status !== "blocked" && typeof current.nextRunAtMs === "number" && current.nextRunAtMs <= args.run.scheduledForMs) {
1481
- nextRunAtMs2 = getNextRunAtMs(
1482
- current,
1483
- current.nextRunAtMs,
1484
- args.nowMs
1485
- );
1486
- }
1487
- await this.saveTaskRecord(
1488
- db,
1489
- {
1490
- ...current,
1491
- lastRunAtMs: args.run.scheduledForMs,
1492
- nextRunAtMs: nextRunAtMs2,
1493
- runNowAtMs: void 0,
1494
- status: statusAfterTerminalOccurrence({
1495
- nextRunAtMs: nextRunAtMs2,
1496
- outcome: args.status,
1497
- previousStatus: current.status
1498
- }),
1499
- statusReason: args.status === "blocked" ? args.errorMessage : void 0,
1500
- updatedAtMs: args.nowMs
1501
- },
1502
- current
1503
- );
1504
- return;
1505
- }
1506
- if (current.status !== "active" || current.nextRunAtMs !== args.run.scheduledForMs) {
1507
- await this.saveTaskRecord(
1508
- db,
1509
- {
1510
- ...current,
1511
- lastRunAtMs: args.run.scheduledForMs,
1512
- updatedAtMs: args.nowMs
1513
- },
1514
- current
1515
- );
1516
- return;
1517
- }
1518
- const nextRunAtMs = args.status === "blocked" ? void 0 : getNextRunAtMs(current, args.run.scheduledForMs, args.nowMs);
1519
- await this.saveTaskRecord(
1520
- db,
1521
- {
1522
- ...current,
1523
- lastRunAtMs: args.run.scheduledForMs,
1524
- nextRunAtMs,
1525
- status: statusAfterTerminalOccurrence({
1526
- nextRunAtMs,
1527
- outcome: args.status
1528
- }),
1529
- statusReason: args.status === "blocked" ? args.errorMessage : void 0,
1530
- updatedAtMs: args.nowMs
1531
- },
1532
- current
1533
- );
1534
- });
1535
- }
1536
- async updateRun(runId, update) {
1537
- return await withSqlLock(
1538
- this.db,
1539
- indexLockKey(runKey(runId)),
1540
- async (db) => {
1541
- const current = await getRunFromSql(db, runId);
1542
- if (!current) {
1543
- return void 0;
1544
- }
1545
- const next = update(current);
1546
- if (!next) {
1547
- return void 0;
1548
- }
1549
- await upsertSqlRun(db, next);
1550
- return next;
1551
- }
1552
- );
1553
- }
1554
- };
1555
- function createSchedulerSqlStore(db) {
1556
- return new SqlSchedulerStore(db);
1557
- }
1558
-
1559
- // src/chat/attachments/store.ts
1560
- import { createHash, randomUUID } from "crypto";
1561
- import { and as and4, asc as asc4, eq as eq3, inArray as inArray3, isNotNull as isNotNull2, isNull, or as or3 } from "drizzle-orm";
1562
- var ATTACHMENT_GC_BATCH_LIMIT = 200;
1563
- function attachmentDigest(file) {
1564
- return createHash("sha256").update(file.data).digest("hex");
1565
- }
1566
- function attachmentId(args) {
1567
- return createHash("sha256").update(
1568
- [
1569
- args.conversationId,
1570
- args.provider,
1571
- args.sha256,
1572
- args.filename,
1573
- args.contentType
1574
- ].join("\0")
1575
- ).digest("hex");
1576
- }
1577
- function attachmentKey(args) {
1578
- return `conversations/${args.conversationId}/attachments/${args.attachmentId}/${args.writeId}/${args.filename}`;
1579
- }
1580
- async function bestEffortDelete(storage, key) {
1581
- try {
1582
- await storage.delete([key]);
1583
- } catch {
1584
- }
1585
- }
1586
- async function isConversationPurged(db, conversationId) {
1587
- const [row] = await db.db().select({
1588
- transcriptPurgedAt: juniorConversations.transcriptPurgedAt
1589
- }).from(juniorConversations).where(eq3(juniorConversations.conversationId, conversationId));
1590
- return row?.transcriptPurgedAt != null;
1591
- }
1592
- function purgedConversationError(conversationId) {
1593
- return new Error(
1594
- `Cannot store attachment for purged conversation ${conversationId}`
1595
- );
1596
- }
1597
- async function assertConversationNotPurged(db, conversationId) {
1598
- if (await isConversationPurged(db, conversationId)) {
1599
- throw purgedConversationError(conversationId);
1600
- }
1601
- }
1602
- async function rejectPurgedAttachmentWrite(args) {
1603
- await args.db.db().update(juniorAttachments).set({ deleteRequestedAt: args.now }).where(eq3(juniorAttachments.id, args.id));
1604
- await bestEffortDelete(args.storage, args.storageKey);
1605
- throw purgedConversationError(args.conversationId);
1606
- }
1607
- async function reuseLiveAttachment(args) {
1608
- await bestEffortDelete(args.storage, args.storageKey);
1609
- await assertConversationNotPurged(args.db, args.conversationId);
1610
- return { id: args.id };
1611
- }
1612
- async function storeAttachment(args) {
1613
- const now = new Date(args.nowMs ?? Date.now());
1614
- const sha256 = attachmentDigest(args.file);
1615
- const id = attachmentId({
1616
- conversationId: args.conversationId,
1617
- contentType: args.file.mimeType,
1618
- filename: args.file.filename,
1619
- provider: args.storage.provider,
1620
- sha256
1621
- });
1622
- const [existing] = await args.db.db().select({
1623
- contentType: juniorAttachments.contentType,
1624
- conversationId: juniorAttachments.conversationId,
1625
- deleteRequestedAt: juniorAttachments.deleteRequestedAt,
1626
- filename: juniorAttachments.filename,
1627
- id: juniorAttachments.id,
1628
- provider: juniorAttachments.provider,
1629
- sha256: juniorAttachments.sha256,
1630
- storageKey: juniorAttachments.storageKey
1631
- }).from(juniorAttachments).where(eq3(juniorAttachments.id, id));
1632
- if (existing) {
1633
- if (existing.contentType !== args.file.mimeType || existing.conversationId !== args.conversationId || existing.filename !== args.file.filename || existing.provider !== args.storage.provider || existing.sha256 !== sha256) {
1634
- throw new Error(`Attachment write conflicts with ${existing.id}`);
1635
- }
1636
- if (existing.deleteRequestedAt === null) {
1637
- await assertConversationNotPurged(args.db, args.conversationId);
1638
- return { id: existing.id };
1639
- }
1640
- }
1641
- await assertConversationNotPurged(args.db, args.conversationId);
1642
- const storageKey = attachmentKey({
1643
- attachmentId: id,
1644
- conversationId: args.conversationId,
1645
- filename: args.file.filename,
1646
- writeId: randomUUID()
1647
- });
1648
- await args.storage.put({
1649
- body: args.file.data,
1650
- contentType: args.file.mimeType,
1651
- key: storageKey
1652
- });
1653
- if (existing?.deleteRequestedAt != null) {
1654
- if (await isConversationPurged(args.db, args.conversationId)) {
1655
- await bestEffortDelete(args.storage, storageKey);
1656
- throw purgedConversationError(args.conversationId);
1657
- }
1658
- const oldStorageKey = existing.storageKey;
1659
- const revived2 = await args.db.db().update(juniorAttachments).set({
1660
- deleteRequestedAt: null,
1661
- storageKey
1662
- }).where(
1663
- and4(
1664
- eq3(juniorAttachments.id, id),
1665
- isNotNull2(juniorAttachments.deleteRequestedAt)
1666
- )
1667
- ).returning({ id: juniorAttachments.id });
1668
- if (revived2.length > 0) {
1669
- if (await isConversationPurged(args.db, args.conversationId)) {
1670
- await rejectPurgedAttachmentWrite({
1671
- conversationId: args.conversationId,
1672
- db: args.db,
1673
- id,
1674
- now,
1675
- storage: args.storage,
1676
- storageKey
1677
- });
1678
- }
1679
- if (oldStorageKey !== storageKey) {
1680
- await bestEffortDelete(args.storage, oldStorageKey);
1681
- }
1682
- return { id };
1683
- }
1684
- const [afterRace2] = await args.db.db().select({
1685
- deleteRequestedAt: juniorAttachments.deleteRequestedAt,
1686
- id: juniorAttachments.id,
1687
- storageKey: juniorAttachments.storageKey
1688
- }).from(juniorAttachments).where(eq3(juniorAttachments.id, id));
1689
- if (afterRace2?.deleteRequestedAt === null) {
1690
- return reuseLiveAttachment({
1691
- conversationId: args.conversationId,
1692
- db: args.db,
1693
- id: afterRace2.id,
1694
- storage: args.storage,
1695
- storageKey
1696
- });
1697
- }
1698
- }
1699
- let inserted = [];
1700
- try {
1701
- inserted = await args.db.db().insert(juniorAttachments).values({
1702
- id,
1703
- conversationId: args.conversationId,
1704
- provider: args.storage.provider,
1705
- storageKey,
1706
- filename: args.file.filename,
1707
- contentType: args.file.mimeType,
1708
- bytes: args.file.bytes,
1709
- sha256,
1710
- createdAt: now
1711
- }).onConflictDoNothing({ target: juniorAttachments.id }).returning({ id: juniorAttachments.id });
1712
- } catch (error) {
1713
- await bestEffortDelete(args.storage, storageKey);
1714
- throw error;
1715
- }
1716
- if (inserted.length > 0) {
1717
- if (await isConversationPurged(args.db, args.conversationId)) {
1718
- await rejectPurgedAttachmentWrite({
1719
- conversationId: args.conversationId,
1720
- db: args.db,
1721
- id,
1722
- now,
1723
- storage: args.storage,
1724
- storageKey
1725
- });
1726
- }
1727
- return { id };
1728
- }
1729
- const [row] = await args.db.db().select({
1730
- deleteRequestedAt: juniorAttachments.deleteRequestedAt,
1731
- id: juniorAttachments.id,
1732
- storageKey: juniorAttachments.storageKey
1733
- }).from(juniorAttachments).where(eq3(juniorAttachments.id, id));
1734
- if (!row) {
1735
- await bestEffortDelete(args.storage, storageKey);
1736
- throw new Error(`Attachment row missing after write for ${id}`);
1737
- }
1738
- if (row.deleteRequestedAt === null) {
1739
- return reuseLiveAttachment({
1740
- conversationId: args.conversationId,
1741
- db: args.db,
1742
- id: row.id,
1743
- storage: args.storage,
1744
- storageKey
1745
- });
1746
- }
1747
- if (await isConversationPurged(args.db, args.conversationId)) {
1748
- await bestEffortDelete(args.storage, storageKey);
1749
- throw purgedConversationError(args.conversationId);
1750
- }
1751
- const revived = await args.db.db().update(juniorAttachments).set({
1752
- deleteRequestedAt: null,
1753
- storageKey
1754
- }).where(
1755
- and4(
1756
- eq3(juniorAttachments.id, id),
1757
- isNotNull2(juniorAttachments.deleteRequestedAt)
1758
- )
1759
- ).returning({ id: juniorAttachments.id });
1760
- if (revived.length > 0) {
1761
- if (await isConversationPurged(args.db, args.conversationId)) {
1762
- await rejectPurgedAttachmentWrite({
1763
- conversationId: args.conversationId,
1764
- db: args.db,
1765
- id,
1766
- now,
1767
- storage: args.storage,
1768
- storageKey
1769
- });
1770
- }
1771
- if (row.storageKey !== storageKey) {
1772
- await bestEffortDelete(args.storage, row.storageKey);
1773
- }
1774
- return { id };
1775
- }
1776
- const [afterRace] = await args.db.db().select({
1777
- deleteRequestedAt: juniorAttachments.deleteRequestedAt,
1778
- id: juniorAttachments.id
1779
- }).from(juniorAttachments).where(eq3(juniorAttachments.id, id));
1780
- if (afterRace?.deleteRequestedAt === null) {
1781
- return reuseLiveAttachment({
1782
- conversationId: args.conversationId,
1783
- db: args.db,
1784
- id: afterRace.id,
1785
- storage: args.storage,
1786
- storageKey
1787
- });
1788
- }
1789
- await bestEffortDelete(args.storage, storageKey);
1790
- throw new Error(`Attachment ${id} disappeared while reviving`);
1791
- }
1792
- async function readLiveAttachment(args) {
1793
- const [row] = await args.db.db().select({
1794
- bytes: juniorAttachments.bytes,
1795
- contentType: juniorAttachments.contentType,
1796
- conversationId: juniorAttachments.conversationId,
1797
- deleteRequestedAt: juniorAttachments.deleteRequestedAt,
1798
- filename: juniorAttachments.filename,
1799
- id: juniorAttachments.id,
1800
- provider: juniorAttachments.provider,
1801
- storageKey: juniorAttachments.storageKey
1802
- }).from(juniorAttachments).where(
1803
- and4(
1804
- eq3(juniorAttachments.id, args.attachmentId),
1805
- eq3(juniorAttachments.conversationId, args.conversationId),
1806
- isNull(juniorAttachments.deleteRequestedAt)
1807
- )
1808
- );
1809
- if (!row) return null;
1810
- if (await isConversationPurged(args.db, args.conversationId)) {
1811
- return null;
1812
- }
1813
- return {
1814
- bytes: row.bytes,
1815
- contentType: row.contentType,
1816
- conversationId: row.conversationId,
1817
- filename: row.filename,
1818
- id: row.id,
1819
- provider: row.provider,
1820
- storageKey: row.storageKey
1821
- };
1822
- }
1823
- async function storeAttachments(args) {
1824
- const stored = [];
1825
- for (const file of args.files) {
1826
- stored.push(
1827
- await storeAttachment({
1828
- conversationId: args.conversationId,
1829
- db: args.db,
1830
- file,
1831
- ...args.nowMs !== void 0 ? { nowMs: args.nowMs } : {},
1832
- storage: args.storage
1833
- })
1834
- );
1835
- }
1836
- return stored;
1837
- }
1838
- async function requestAttachmentDeletion(db, conversationIds, nowMs) {
1839
- if (conversationIds.length === 0) return;
1840
- await db.db().update(juniorAttachments).set({ deleteRequestedAt: new Date(nowMs) }).where(
1841
- and4(
1842
- inArray3(juniorAttachments.conversationId, conversationIds),
1843
- isNull(juniorAttachments.deleteRequestedAt)
1844
- )
1845
- );
1846
- }
1847
- async function collectAttachmentGarbage(args) {
1848
- void args.nowMs;
1849
- const rows = await args.db.db().select({
1850
- id: juniorAttachments.id,
1851
- storageKey: juniorAttachments.storageKey
1852
- }).from(juniorAttachments).innerJoin(
1853
- juniorConversations,
1854
- eq3(
1855
- juniorAttachments.conversationId,
1856
- juniorConversations.conversationId
1857
- )
1858
- ).where(
1859
- and4(
1860
- eq3(juniorAttachments.provider, args.storage.provider),
1861
- or3(
1862
- isNotNull2(juniorAttachments.deleteRequestedAt),
1863
- isNotNull2(juniorConversations.transcriptPurgedAt)
1864
- )
1865
- )
1866
- ).orderBy(asc4(juniorAttachments.createdAt), asc4(juniorAttachments.id)).limit(args.limit ?? ATTACHMENT_GC_BATCH_LIMIT);
1867
- if (rows.length === 0) return { deleted: 0 };
1868
- await args.storage.delete(rows.map((row) => row.storageKey));
1869
- const stillEligible = await args.db.db().select({ id: juniorAttachments.id }).from(juniorAttachments).innerJoin(
1870
- juniorConversations,
1871
- eq3(
1872
- juniorAttachments.conversationId,
1873
- juniorConversations.conversationId
1874
- )
1875
- ).where(
1876
- and4(
1877
- inArray3(
1878
- juniorAttachments.id,
1879
- rows.map((row) => row.id)
1880
- ),
1881
- or3(
1882
- isNotNull2(juniorAttachments.deleteRequestedAt),
1883
- isNotNull2(juniorConversations.transcriptPurgedAt)
1884
- )
1885
- )
1886
- );
1887
- if (stillEligible.length === 0) return { deleted: 0 };
1888
- const removed = await args.db.db().delete(juniorAttachments).where(
1889
- inArray3(
1890
- juniorAttachments.id,
1891
- stillEligible.map((row) => row.id)
1892
- )
1893
- ).returning({
1894
- id: juniorAttachments.id
1895
- });
1896
- return { deleted: removed.length };
1897
- }
1898
-
1899
- export {
1900
- juniorToolOutputSchema,
1901
- injectContinuationToolName,
1902
- makeStructuredToolOutput,
1903
- eventNamespaceSchema,
1904
- registeredEventTypeSchema,
1905
- registeredResourceTypeSchema,
1906
- pluginSupportsEvent,
1907
- resourceEventGuidance,
1908
- normalizeEventIdentifier,
1909
- getResourceEventCatalog,
1910
- getEventTask,
1911
- createEventTask,
1912
- saveEventTask,
1913
- deleteEventTask,
1914
- listEventTasksForTeam,
1915
- eventTaskBelongsToUser,
1916
- listEventTasksCreatedBy,
1917
- listPublicEventTasksForTeams,
1918
- findMatchingEventTasks,
1919
- eventTaskToolResultSchema,
1920
- eventTaskListToolResultSchema,
1921
- registeredEventTaskTriggerSchema,
1922
- requireSupportedEventTaskTrigger,
1923
- requireEventTaskSlackContext,
1924
- eventTaskMatchesDestination,
1925
- writableEventTask,
1926
- eventTaskTriggerAvailable,
1927
- compactEventTask,
1928
- eventTaskSuccess,
1929
- getZonedDateTimeParts,
1930
- resolveLocalScheduleAtMs,
1931
- getFirstRunAtMs,
1932
- scheduledTaskAttributes,
1933
- scheduledTaskRunAttributes,
1934
- logScheduledTaskRunSkipped,
1935
- SCHEDULED_TASK_SYSTEM_ACTOR,
1936
- listPublicScheduledTasksForTeams,
1937
- createSchedulerSqlStore,
1938
- readLiveAttachment,
1939
- storeAttachments,
1940
- requestAttachmentDeletion,
1941
- collectAttachmentGarbage,
1942
- incrementStat,
1943
- readStats
1944
- };