@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.
- package/dist/{agent-hooks-IYHCN7CO.js → agent-hooks-32IDCLMW.js} +12 -12
- package/dist/api/conversations/archive.d.ts +5 -2
- package/dist/api/conversations/list.d.ts +0 -1
- package/dist/api/conversations/membership.d.ts +14 -0
- package/dist/api/conversations/projection.d.ts +2 -1
- package/dist/api/locations/detail.d.ts +1 -1
- package/dist/api/people/shared.d.ts +2 -2
- package/dist/api/schema/conversation.d.ts +4 -4
- package/dist/api/schema/location.d.ts +1 -1
- package/dist/api/schema/person.d.ts +1 -1
- package/dist/api/schema.js +1 -1
- package/dist/api.js +399 -287
- package/dist/app.js +573 -198
- package/dist/{catalog-runtime-2TD4CXSB.js → catalog-runtime-FBVA6WAQ.js} +5 -5
- package/dist/chat/agent/sandbox.d.ts +2 -0
- package/dist/chat/conversations/sql/participants.d.ts +1 -0
- package/dist/chat/conversations/store.d.ts +0 -1
- package/dist/chat/sandbox/errors.d.ts +4 -0
- package/dist/chat/sandbox/noninteractive-command.d.ts +1 -0
- package/dist/chat/sandbox/prepare-workspace.d.ts +27 -3
- package/dist/chat/sandbox/sandbox.d.ts +2 -0
- package/dist/chat/sandbox/session.d.ts +2 -0
- package/dist/chat/sandbox/snapshot/builder-sandbox.d.ts +5 -0
- package/dist/chat/sandbox/snapshot/install.d.ts +2 -2
- package/dist/chat/sandbox/snapshot/span.d.ts +3 -0
- package/dist/chat/sandbox/snapshot/store.d.ts +20 -17
- package/dist/chat/sandbox/snapshot/waiting-error.d.ts +15 -0
- package/dist/chat/sandbox/snapshot/workspace.d.ts +17 -0
- package/dist/chat/scheduled-tasks/personal.d.ts +5 -8
- package/dist/chat/scheduled-tasks/runs.d.ts +49 -0
- package/dist/chat/scheduled-tasks/schedule-intent.d.ts +1 -1
- package/dist/chat/scheduled-tasks/tasks.d.ts +27 -0
- package/dist/chat/scheduled-tasks/tool-support.d.ts +0 -4
- package/dist/chat/scheduled-tasks/tools/create.d.ts +1 -1
- package/dist/chat/scheduled-tasks/tools/update.d.ts +1 -1
- package/dist/chat/scheduled-tasks/types.d.ts +127 -78
- package/dist/chat/services/workspace-snapshot-wait.d.ts +27 -0
- package/dist/chat/workspaces/types.d.ts +1 -0
- package/dist/{chunk-CAWBLVBY.js → chunk-2HIPYNQI.js} +6 -6
- package/dist/{chunk-ZCYABD7N.js → chunk-3AFVR5UC.js} +5 -5
- package/dist/{chunk-PA7QDS7P.js → chunk-3I7XE3PA.js} +204 -100
- package/dist/{chunk-VGDOEKO2.js → chunk-3TM3FYLQ.js} +1 -1
- package/dist/{chunk-6NZRKJWU.js → chunk-4GWXAILB.js} +1 -1
- package/dist/{chunk-LFZODSAY.js → chunk-5IRROPA5.js} +4 -4
- package/dist/{chunk-N4YEVOEC.js → chunk-B6ECUSVX.js} +45 -276
- package/dist/{chunk-4TWDRU64.js → chunk-CJLBLELR.js} +5 -5
- package/dist/{chunk-3FWXQZJJ.js → chunk-FAS6AIMF.js} +1 -1
- package/dist/{chunk-I7PKGMKI.js → chunk-FIDMHAO6.js} +6 -6
- package/dist/{chunk-4SVAFAI2.js → chunk-FTGD2FBP.js} +1 -1
- package/dist/{chunk-LVL5NG5G.js → chunk-G2CAC4CV.js} +19 -19
- package/dist/{chunk-DLTP6TUJ.js → chunk-GENV6KF2.js} +2 -2
- package/dist/{chunk-FVVYHML7.js → chunk-GPLAT6H7.js} +6 -6
- package/dist/{chunk-7CVYXUOJ.js → chunk-HQNE5APD.js} +1 -1
- package/dist/{chunk-Q362TECJ.js → chunk-JMJNFEDC.js} +331 -2
- package/dist/{chunk-AFOFVEXP.js → chunk-JT4WMD3C.js} +124 -13
- package/dist/{chunk-7ZFESKJ2.js → chunk-KJLHIHFF.js} +9 -9
- package/dist/{chunk-QLLOX7JY.js → chunk-Q5B3WTGG.js} +8 -8
- package/dist/{chunk-DGGIKWYQ.js → chunk-R5NWWYIS.js} +1 -1
- package/dist/{chunk-WCQUKX74.js → chunk-R6U7N4FS.js} +14 -5
- package/dist/{chunk-WYO2HXY5.js → chunk-RTRBYSF3.js} +1 -1
- package/dist/{chunk-2IMQLZHS.js → chunk-RZRYOLCE.js} +1144 -136
- package/dist/{chunk-7B6NW4P7.js → chunk-SLGGOASN.js} +1 -1
- package/dist/{chunk-SNPQGTHJ.js → chunk-TIEKUH4Q.js} +1 -1
- package/dist/{chunk-ELEAIRTG.js → chunk-TKB25A3L.js} +2 -2
- package/dist/{chunk-UB5YB2ED.js → chunk-U4TKB6F6.js} +6 -6
- package/dist/{chunk-ORMJY4RG.js → chunk-UT6X7XN7.js} +1 -1
- package/dist/{chunk-LPMLAJYR.js → chunk-VP4OVRDD.js} +11 -17
- package/dist/{chunk-7PZDAXIK.js → chunk-XSPSCWLH.js} +1 -1
- package/dist/chunk-XZY6G5YZ.js +1088 -0
- package/dist/{chunk-J2EWL6GJ.js → chunk-YFNAMRGE.js} +1 -1
- package/dist/{chunk-6KGMMVHF.js → chunk-ZQMG3CL5.js} +2 -2
- package/dist/cli/chat.js +31 -30
- package/dist/cli/check.js +6 -6
- package/dist/cli/plugins.js +13 -13
- package/dist/cli/snapshot-create.js +10 -10
- package/dist/cli/upgrade.js +7 -7
- package/dist/db/schema/conversation-participants.d.ts +17 -0
- package/dist/db/schema/conversations.d.ts +629 -1
- package/dist/db/schema/scheduled-tasks.d.ts +29 -5
- package/dist/db/schema/snapshots.d.ts +1 -1
- package/dist/db/schema.d.ts +46 -5
- package/dist/{db-A3CB76US.js → db-EJAV7JL6.js} +7 -7
- package/dist/instrumentation.js +4 -4
- package/dist/nitro.js +2 -2
- package/dist/{oauth-callback-server-64QMI3IB.js → oauth-callback-server-GAVSUBTQ.js} +23 -23
- package/dist/{runner-EIAE2BGD.js → runner-34JL64JF.js} +15 -15
- package/dist/{skills-ZADWFKDG.js → skills-U545HALM.js} +6 -6
- package/dist/spawn-I2S4IDUQ.js +37 -0
- package/dist/{validation-4I6AF4CV.js → validation-EVXVGA44.js} +6 -6
- package/dist/version.js +1 -1
- package/dist/{work-BS6BZ66U.js → work-QOAUPDBU.js} +18 -18
- package/dist/worker-5GX2IKLZ.js +24 -0
- package/migrations/0033_workspace_snapshot_active_build.sql +1 -0
- package/migrations/0034_strong_randall.sql +31 -0
- package/migrations/meta/0033_snapshot.json +2914 -0
- package/migrations/meta/0034_snapshot.json +3038 -0
- package/migrations/meta/_journal.json +15 -1
- package/package.json +4 -4
- package/dist/chat/scheduled-tasks/store.d.ts +0 -78
- package/dist/chunk-4TRHPYTL.js +0 -1944
- package/dist/spawn-DVUOUFFC.js +0 -37
- package/dist/worker-RS5PWTCE.js +0 -24
package/dist/chunk-4TRHPYTL.js
DELETED
|
@@ -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
|
-
};
|