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