@sentry/junior 0.221.1 → 0.222.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/{agent-CY49A4pN.js → agent-7kKtDe-6.js} +47 -27
  2. package/dist/{agent-hooks-BnZBgDPe.js → agent-hooks-DkriF84n.js} +9 -8
  3. package/dist/{agent-runner-BC8iSAfE.js → agent-runner-D0ImXS7U.js} +6 -5
  4. package/dist/api/schema.d.ts +51 -2
  5. package/dist/api/schema.js +4 -4
  6. package/dist/api.js +18 -515
  7. package/dist/{app-BYD2Nyji.d.ts → app-C0zbsnr7.d.ts} +1 -1
  8. package/dist/app.d.ts +1 -1
  9. package/dist/app.js +153 -32
  10. package/dist/{auth-pause-state-CJwPs1cT.js → auth-pause-state-3OT67TA8.js} +52 -7
  11. package/dist/{briefs-BSOhy23c.js → briefs-CNH1ztn5.js} +4 -4
  12. package/dist/{catalog-runtime-0oMD2jas.js → catalog-runtime-XH_LFA_7.js} +1 -1
  13. package/dist/{checkpoint-Bl47qAIY.js → checkpoint-CRod8es5.js} +8 -7
  14. package/dist/cli/chat.js +13 -13
  15. package/dist/cli/check.js +3 -3
  16. package/dist/cli/main.js +1 -1
  17. package/dist/cli/plugins.js +3 -3
  18. package/dist/cli/snapshot-create.js +3 -3
  19. package/dist/cli/upgrade.js +3 -3
  20. package/dist/{client-BrThxuRI.js → client-CcSboWx_.js} +2 -2
  21. package/dist/{config-CEuC6CFj.js → config-CGYv-YPE.js} +2 -2
  22. package/dist/{conversation-BSRGzJmY.js → conversation-BBDt88di.js} +46 -3
  23. package/dist/{conversation-privacy-aHn1OwXL.js → conversation-privacy-BGjdDVWD.js} +1 -1
  24. package/dist/{credential-sync-Dht95TJQ.js → credential-sync-BfzGNruT.js} +125 -30
  25. package/dist/{execution-stats-D9m2kmmg.js → execution-stats-D97ePWuM.js} +3 -3
  26. package/dist/{executor-3IasVOqK.js → executor-BLBZ-UB8.js} +1 -1
  27. package/dist/{history-ClE4toxe.d.ts → history-CaV8Ww7M.d.ts} +16 -1
  28. package/dist/instrumentation.js +2 -2
  29. package/dist/{job-queue-36liupTq.js → job-queue-_8gxwDzt.js} +1 -1
  30. package/dist/{logging-gR24QCo_.js → logging-qmsMdU0y.js} +1 -1
  31. package/dist/nitro.d.ts +1 -1
  32. package/dist/nitro.js +3 -3
  33. package/dist/{oauth-callback-server-BvNIWVA3.js → oauth-callback-server-Mjfm46A5.js} +2 -2
  34. package/dist/{pending-auth-Cl4BsopG.js → pending-auth-_1Ry-_Bw.js} +2 -2
  35. package/dist/{profile-VmUpQuOm.js → profile-DbG8AlNd.js} +4 -4
  36. package/dist/{prompt-BkaHwBW0.js → prompt-B8rCQI7K.js} +1 -1
  37. package/dist/{registry-BATk1X_v.js → registry-DGyQCsiy.js} +1 -1
  38. package/dist/reporting-window-DbUqng0P.js +229 -0
  39. package/dist/{resolve-Cf79l1H8.js → resolve-D5SqDbCA.js} +2 -2
  40. package/dist/{runner-DNN4EPj_.js → runner-BRAFjJhe.js} +21 -10
  41. package/dist/{sandbox-egress-signals-C-HUJW1G.js → sandbox-egress-signals-CdI1aVHT.js} +1 -1
  42. package/dist/{schema-YPJXVcOW.js → schema-CTOODa3L.js} +5 -128
  43. package/dist/{short-title-Cy3D8vg1.js → short-title-CB2fxEyx.js} +2 -2
  44. package/dist/{skills-DMQkR1Yu.js → skills-BoS3b8cg.js} +2 -2
  45. package/dist/{spawn-BHk7RpYw.js → spawn-BXCU1udr.js} +2 -2
  46. package/dist/{stats-8Bzht8zO.js → stats-qUXQZq-w.js} +52 -13
  47. package/dist/{task-queue-Cm4FhC0n.js → task-queue-Di_q8922.js} +3 -3
  48. package/dist/{task-runner-dQwOhCYs.js → task-runner-4rCppQMo.js} +8 -8
  49. package/dist/{thread-state-D0Ktlch_.js → thread-state-2Paza8DV.js} +5 -5
  50. package/dist/{turn-execution-BfLZfabf.js → turn-execution--F7a9kEU.js} +3 -3
  51. package/dist/{turn-lifecycle-D75pWL8e.js → turn-lifecycle-f-y0Irpi.js} +1 -1
  52. package/dist/{validation-D5fdtuzG.js → validation-Be9lSORU.js} +1 -1
  53. package/dist/{version-DErORHQC.js → version-BydrOZMQ.js} +1 -1
  54. package/dist/version.js +1 -1
  55. package/dist/{web-authorization-IHMPbDay.js → web-authorization-BdfOkTuG.js} +1 -1
  56. package/dist/web-input-DegYUyKU.js +899 -0
  57. package/dist/{work-CaaSjlNk.js → work-Citqpnfc.js} +9 -9
  58. package/dist/{worker-Eom9CfBl.js → worker-Bn_HFyvM.js} +5 -5
  59. package/migrations/0044_event_automation_channel_destinations.sql +36 -0
  60. package/migrations/meta/0044_snapshot.json +3473 -0
  61. package/migrations/meta/_journal.json +7 -0
  62. package/package.json +4 -4
  63. package/dist/reporting-schema-DosTN__Q.js +0 -105
  64. package/dist/web-input-DskRJhrK.js +0 -392
@@ -0,0 +1,899 @@
1
+ import { f as logWarn } from "./logging-qmsMdU0y.js";
2
+ import { s as getSlackBotToken } from "./config-CGYv-YPE.js";
3
+ import "./context-Bplcgypl.js";
4
+ import { Gt as getDb, K as normalizeSlackConversationId, Wt as getConversationStore } from "./agent-hooks-DkriF84n.js";
5
+ import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
6
+ import { D as juniorIdentities, O as juniorUsers, o as juniorSchedulerTasks } from "./executor-BLBZ-UB8.js";
7
+ import { t as fallbackShortTitle } from "./short-title-CB2fxEyx.js";
8
+ import { A as eventAutomationTriggerAvailable, F as effectiveTaskOutcomes, G as listDeletedEventAutomationsCreatedBy, H as eventAutomationBelongsToUser, J as listPublicEventAutomationsForTeams, K as listEventAutomationsCreatedBy, V as deleteEventAutomation, W as getEventAutomation, X as getEventCatalog, _ as parseScheduledAutomationRow, c as parseSlackThreadId, ft as appendAndEnqueueExclusiveInboundMessage, m as listPublicScheduledAutomationsForTeams, p as isListedScheduledAutomation, pt as appendAndEnqueueInboundMessage, qt as buildDeterministicTurnId, v as readScheduledAutomation, y as saveScheduledAutomation } from "./stats-qUXQZq-w.js";
9
+ import { p as healthReportSchema, s as sumUtcHoursIntoSixHours } from "./reporting-window-DbUqng0P.js";
10
+ import { a as readAutomationExecutionStatusDays, c as readAutomationExecutions, i as readAutomationExecutionHours, l as readAutomationRuns, n as readAutomationExecutionByConversationId, o as readAutomationExecutionStatusHours, r as readAutomationExecutionDays, s as readAutomationExecutionSummaries, t as emptyAutomationRunWindows } from "./execution-stats-D97ePWuM.js";
11
+ import { z } from "zod";
12
+ import { createHash } from "node:crypto";
13
+ import { and, desc, eq, inArray, lt, notInArray, or, sql } from "drizzle-orm";
14
+ import { createWebSource, localDestinationSchema } from "@sentry/junior-plugin-api";
15
+ import { del, get, put } from "@vercel/blob";
16
+ //#region src/handlers/health.ts
17
+ /** Build Junior's process health value for HTTP and reporting consumers. */
18
+ function readHealthReport() {
19
+ return healthReportSchema.parse({
20
+ status: "ok",
21
+ service: "junior",
22
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
23
+ });
24
+ }
25
+ /**
26
+ * Returns a minimal JSON health response for runtime health checks.
27
+ */
28
+ function GET() {
29
+ return Response.json(readHealthReport());
30
+ }
31
+ //#endregion
32
+ //#region src/chat/attachments/vercel.ts
33
+ /** Store private conversation attachments in Vercel Blob. */
34
+ function createVercelAttachmentStorage() {
35
+ return {
36
+ provider: "vercel-blob",
37
+ async put(input) {
38
+ await put(input.key, input.body, {
39
+ access: "private",
40
+ addRandomSuffix: false,
41
+ allowOverwrite: true,
42
+ contentType: input.contentType
43
+ });
44
+ },
45
+ async get(key) {
46
+ const result = await get(key, {
47
+ access: "private",
48
+ useCache: false
49
+ });
50
+ return result?.statusCode === 200 ? result.stream : null;
51
+ },
52
+ async delete(keys) {
53
+ if (keys.length === 0) return;
54
+ await del(keys);
55
+ }
56
+ };
57
+ }
58
+ //#endregion
59
+ //#region src/chat/conversations/web-mailbox.ts
60
+ /** Parse web mailbox metadata written before Source and Actor were complete. */
61
+ const legacyWebMailboxMetadataSchema = z.object({
62
+ authorEmail: z.string().email(),
63
+ authorFullName: z.string().min(1).optional(),
64
+ authorUserId: z.string().min(1),
65
+ authorUserName: z.string().min(1).optional(),
66
+ kind: z.literal("api_turn"),
67
+ messageId: z.string().min(1)
68
+ }).strict();
69
+ //#endregion
70
+ //#region src/chat/slack/user.ts
71
+ const slackUserInfoSchema = z.object({
72
+ ok: z.literal(true),
73
+ user: z.object({
74
+ name: z.string().nullish(),
75
+ profile: z.object({
76
+ email: z.string().nullish(),
77
+ real_name: z.string().nullish()
78
+ }).optional()
79
+ })
80
+ });
81
+ const USER_CACHE_TTL_MS = 3e5;
82
+ const userCache = /* @__PURE__ */ new Map();
83
+ function userCacheKey(teamId, userId) {
84
+ return `${teamId}:${userId}`;
85
+ }
86
+ function readFromCache(teamId, userId) {
87
+ const hit = userCache.get(userCacheKey(teamId, userId));
88
+ if (!hit) return null;
89
+ if (hit.expiresAt < Date.now()) {
90
+ userCache.delete(userCacheKey(teamId, userId));
91
+ return null;
92
+ }
93
+ return hit.value;
94
+ }
95
+ function writeToCache(teamId, userId, value) {
96
+ userCache.set(userCacheKey(teamId, userId), {
97
+ value,
98
+ expiresAt: Date.now() + USER_CACHE_TTL_MS
99
+ });
100
+ }
101
+ /** Fetch Slack user profile info with in-memory TTL cache to avoid repeated API calls. */
102
+ async function lookupSlackUser(teamId, userId) {
103
+ if (!teamId || !userId) return null;
104
+ const cached = readFromCache(teamId, userId);
105
+ if (cached) return cached;
106
+ const token = getSlackBotToken();
107
+ if (!token) return null;
108
+ try {
109
+ const response = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, { headers: { authorization: `Bearer ${token}` } });
110
+ if (!response.ok) {
111
+ logWarn("slack.user.lookup.failed", {
112
+ "enduser.id": userId,
113
+ "app.slack.team_id": teamId,
114
+ "http.response.status_code": response.status
115
+ });
116
+ return null;
117
+ }
118
+ const parsed = slackUserInfoSchema.safeParse(await response.json());
119
+ if (!parsed.success) return null;
120
+ const payload = parsed.data;
121
+ const result = {
122
+ userName: payload.user.name?.trim() || void 0,
123
+ fullName: payload.user.profile?.real_name?.trim() || void 0,
124
+ email: payload.user.profile?.email?.trim() || void 0
125
+ };
126
+ writeToCache(teamId, userId, result);
127
+ return result;
128
+ } catch (error) {
129
+ logWarn("slack.user.lookup.failed", {
130
+ "enduser.id": userId,
131
+ "app.slack.team_id": teamId,
132
+ "exception.message": error instanceof Error ? error.message : String(error)
133
+ });
134
+ return null;
135
+ }
136
+ }
137
+ //#endregion
138
+ //#region src/chat/scheduled-automations/personal.ts
139
+ const cursorSchema = z.object({
140
+ createdAtMs: z.number().finite(),
141
+ id: z.string().min(1),
142
+ query: z.string().max(200).optional(),
143
+ version: z.literal(1)
144
+ }).strict();
145
+ var InvalidScheduledAutomationCursorError = class extends Error {
146
+ constructor() {
147
+ super("Scheduled automation cursor is invalid.");
148
+ this.name = "InvalidScheduledAutomationCursorError";
149
+ }
150
+ };
151
+ var PersonalScheduledAutomationNotFoundError = class extends Error {
152
+ constructor() {
153
+ super("Scheduled automation was not found.");
154
+ this.name = "PersonalScheduledAutomationNotFoundError";
155
+ }
156
+ };
157
+ function normalizeQuery(query) {
158
+ return query?.trim().toLowerCase() || void 0;
159
+ }
160
+ function decodeCursor(value, query) {
161
+ if (!value) return void 0;
162
+ try {
163
+ const parsed = cursorSchema.parse(JSON.parse(Buffer.from(value, "base64url").toString("utf8")));
164
+ if (parsed.query !== query) throw new InvalidScheduledAutomationCursorError();
165
+ return {
166
+ createdAtMs: parsed.createdAtMs,
167
+ id: parsed.id
168
+ };
169
+ } catch {
170
+ throw new InvalidScheduledAutomationCursorError();
171
+ }
172
+ }
173
+ function encodeCursor(task, query) {
174
+ return Buffer.from(JSON.stringify({
175
+ createdAtMs: task.createdAtMs,
176
+ id: task.id,
177
+ ...query ? { query } : void 0,
178
+ version: 1
179
+ }), "utf8").toString("base64url");
180
+ }
181
+ /** Delete one scheduled automation created by the viewer. */
182
+ async function deleteViewerScheduledAutomation(db, user, id, nowMs = Date.now()) {
183
+ const identityIds = new Set(user.identities.map((identity) => identity.id));
184
+ const task = await readScheduledAutomation(db, id);
185
+ if (!task || task.status === "deleted" || !identityIds.has(task.creatorIdentityId)) throw new PersonalScheduledAutomationNotFoundError();
186
+ await saveScheduledAutomation(db, {
187
+ ...task,
188
+ nextRunAtMs: void 0,
189
+ runNowAtMs: void 0,
190
+ status: "deleted",
191
+ updatedAtMs: nowMs
192
+ });
193
+ }
194
+ /** List scheduled automations created by the viewer with a stable SQL cursor. */
195
+ async function listViewerScheduledAutomations(db, user, input) {
196
+ const identityIds = user.identities.map((identity) => identity.id);
197
+ if (identityIds.length === 0) return { automations: [] };
198
+ const query = normalizeQuery(input.query);
199
+ const cursor = decodeCursor(input.cursor, query);
200
+ const automations = [];
201
+ let before = cursor;
202
+ const fetchLimit = input.limit + 1;
203
+ while (automations.length < fetchLimit) {
204
+ const cursorFilter = before ? or(lt(juniorSchedulerTasks.createdAtMs, before.createdAtMs), and(eq(juniorSchedulerTasks.createdAtMs, before.createdAtMs), lt(juniorSchedulerTasks.id, before.id))) : void 0;
205
+ const search = query ? sql`strpos(lower(coalesce(${juniorSchedulerTasks.title}, ${juniorSchedulerTasks.record}->'task'->>'text')), ${query}) > 0` : void 0;
206
+ const rows = await db.select({
207
+ createdAtMs: juniorSchedulerTasks.createdAtMs,
208
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
209
+ id: juniorSchedulerTasks.id,
210
+ record: juniorSchedulerTasks.record,
211
+ title: juniorSchedulerTasks.title
212
+ }).from(juniorSchedulerTasks).where(and(notInArray(juniorSchedulerTasks.status, ["deleted", "paused"]), inArray(juniorSchedulerTasks.creatorIdentityId, identityIds), cursorFilter, search)).orderBy(desc(juniorSchedulerTasks.createdAtMs), desc(juniorSchedulerTasks.id)).limit(fetchLimit);
213
+ automations.push(...rows.map(parseScheduledAutomationRow).filter(isListedScheduledAutomation));
214
+ if (rows.length < fetchLimit) break;
215
+ const last = rows.at(-1);
216
+ before = {
217
+ createdAtMs: last.createdAtMs,
218
+ id: last.id
219
+ };
220
+ }
221
+ const page = automations.slice(0, input.limit);
222
+ return {
223
+ automations: page,
224
+ ...automations.length > input.limit && page.length > 0 ? { nextCursor: encodeCursor(page.at(-1), query) } : void 0
225
+ };
226
+ }
227
+ //#endregion
228
+ //#region src/chat/automations/read.ts
229
+ const TASK_LIST_LIMIT = 100;
230
+ const TASK_FETCH_LIMIT = 101;
231
+ const TASK_EXECUTION_LIST_LIMIT = 100;
232
+ function creatorLabel(creator) {
233
+ return creator.fullName?.trim() || (creator.userName?.trim() ? `@${creator.userName.trim()}` : "") || creator.slackUserId;
234
+ }
235
+ function creatorKey(teamId, slackUserId) {
236
+ return `${teamId}:${slackUserId}`;
237
+ }
238
+ async function creatorProfileEmails(candidates) {
239
+ const selectors = new Map(candidates.map(({ task }) => {
240
+ const teamId = task.destination.teamId;
241
+ const slackUserId = task.createdBy.slackUserId;
242
+ return [creatorKey(teamId, slackUserId), {
243
+ slackUserId,
244
+ teamId
245
+ }];
246
+ }));
247
+ if (selectors.size === 0) return /* @__PURE__ */ new Map();
248
+ const rows = await getDb().select({
249
+ email: juniorUsers.primaryEmailNormalized,
250
+ slackUserId: juniorIdentities.providerSubjectId,
251
+ teamId: juniorIdentities.providerTenantId
252
+ }).from(juniorIdentities).innerJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.provider, "slack"), eq(juniorIdentities.emailVerified, true), or(...[...selectors.values()].map((selector) => and(eq(juniorIdentities.providerTenantId, selector.teamId), eq(juniorIdentities.providerSubjectId, selector.slackUserId))))));
253
+ return new Map(rows.map((row) => [creatorKey(row.teamId, row.slackUserId), row.email]));
254
+ }
255
+ function destinationKey(destination) {
256
+ return `${destination.teamId}:${destination.channelId}`;
257
+ }
258
+ function displayText(value, fallback) {
259
+ return value.trim() || fallback;
260
+ }
261
+ function taskDisplayTitle(title, instruction, fallback) {
262
+ const stored = title?.trim();
263
+ if (stored) return stored;
264
+ return fallbackShortTitle(instruction, fallback);
265
+ }
266
+ async function destinationDetails(destinations) {
267
+ const selectors = new Map(destinations.map((destination) => [destinationKey(destination), destination]));
268
+ if (selectors.size === 0) return /* @__PURE__ */ new Map();
269
+ const rows = await getDb().select({
270
+ displayName: juniorDestinations.displayName,
271
+ kind: juniorDestinations.kind,
272
+ providerDestinationId: juniorDestinations.providerDestinationId,
273
+ providerTenantId: juniorDestinations.providerTenantId,
274
+ visibility: juniorDestinations.visibility
275
+ }).from(juniorDestinations).where(or(...[...selectors.values()].map((destination) => and(eq(juniorDestinations.provider, "slack"), eq(juniorDestinations.providerTenantId, destination.teamId), eq(juniorDestinations.providerDestinationId, destination.channelId)))));
276
+ return new Map(rows.map((row) => {
277
+ const name = row.displayName?.trim();
278
+ const label = name ? row.kind === "channel" ? `#${name.replace(/^#/, "")}` : name : row.kind === "dm" ? "Direct message" : row.kind === "group" ? "Group message" : `Channel ${row.providerDestinationId}`;
279
+ return [`${row.providerTenantId}:${row.providerDestinationId}`, {
280
+ label,
281
+ visibility: row.visibility === "public" ? "public" : "private"
282
+ }];
283
+ }));
284
+ }
285
+ function executionSummaryFields(stats) {
286
+ return {
287
+ ...stats?.lastConversationId ? { lastConversationId: stats.lastConversationId } : void 0,
288
+ ...stats?.lastExecutedAtMs ? { lastRunAt: new Date(stats.lastExecutedAtMs).toISOString() } : void 0,
289
+ runs: stats?.runs ?? emptyAutomationRunWindows(),
290
+ totalRuns: stats?.totalRuns ?? 0
291
+ };
292
+ }
293
+ function scheduledAutomationSummary(task, ownedByViewer, destination, stats, createdByEmail) {
294
+ if (task.status === "deleted") throw new Error("Deleted scheduled automations cannot enter the Automations view");
295
+ const nextRunAtMs = task.runNowAtMs ?? task.nextRunAtMs;
296
+ const instruction = displayText(task.task.text, "Untitled scheduled automation");
297
+ return {
298
+ createdAt: new Date(task.createdAtMs).toISOString(),
299
+ createdBy: creatorLabel(task.createdBy),
300
+ ...createdByEmail ? { createdByEmail } : void 0,
301
+ destination: {
302
+ channelId: task.destination.channelId,
303
+ label: destination.label,
304
+ teamId: task.destination.teamId,
305
+ visibility: destination.visibility
306
+ },
307
+ id: task.id,
308
+ instruction,
309
+ kind: "scheduled",
310
+ ...executionSummaryFields(stats),
311
+ ...nextRunAtMs !== void 0 ? { nextRunAt: new Date(nextRunAtMs).toISOString() } : void 0,
312
+ ownedByViewer,
313
+ schedule: displayText(task.schedule.description, "Schedule unavailable"),
314
+ status: task.status,
315
+ outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
316
+ title: taskDisplayTitle(task.title, instruction, "Untitled scheduled automation")
317
+ };
318
+ }
319
+ function eventAutomationSummary(task, ownedByViewer, destination, stats, createdByEmail) {
320
+ const instruction = task.task.text;
321
+ return {
322
+ createdAt: new Date(task.createdAtMs).toISOString(),
323
+ createdBy: creatorLabel(task.createdBy),
324
+ ...createdByEmail ? { createdByEmail } : void 0,
325
+ destination: {
326
+ channelId: task.destination.channelId,
327
+ label: destination.label,
328
+ teamId: task.destination.teamId,
329
+ visibility: destination.visibility
330
+ },
331
+ events: task.trigger.events,
332
+ id: task.id,
333
+ instruction,
334
+ kind: "event",
335
+ ...executionSummaryFields(stats),
336
+ ownedByViewer,
337
+ resource: `${task.trigger.label} · ${task.trigger.identifier}`,
338
+ source: task.trigger.namespace,
339
+ outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
340
+ title: taskDisplayTitle(task.title, instruction, "Untitled event automation"),
341
+ triggerAvailable: eventAutomationTriggerAvailable(task, getEventCatalog())
342
+ };
343
+ }
344
+ function viewerTeamIds(user) {
345
+ return [...new Set(user.identities.filter((identity) => identity.provider === "slack").map((identity) => identity.providerTenantId).filter((teamId) => Boolean(teamId)))];
346
+ }
347
+ async function resolveViewerTaskCandidate(user, kind, id) {
348
+ const db = getDb();
349
+ const identityIds = new Set(user.identities.map((identity) => identity.id));
350
+ const teamIds = viewerTeamIds(user);
351
+ if (kind === "scheduled") {
352
+ const task = await readScheduledAutomation(db, id);
353
+ if (!task || task.status === "deleted") return void 0;
354
+ const ownedByViewer = identityIds.has(task.creatorIdentityId);
355
+ const destination = (await destinationDetails([task.destination])).get(destinationKey(task.destination));
356
+ const publicToViewer = teamIds.includes(task.destination.teamId) && destination?.visibility === "public";
357
+ if (!ownedByViewer && !publicToViewer) return void 0;
358
+ return {
359
+ kind: "scheduled",
360
+ ownedByViewer,
361
+ publicToViewer,
362
+ task
363
+ };
364
+ }
365
+ const task = await getEventAutomation(db, id);
366
+ if (!task || task.status === "deleted") return void 0;
367
+ const ownedByViewer = eventAutomationBelongsToUser(task, user);
368
+ const destination = (await destinationDetails([task.destination])).get(destinationKey(task.destination));
369
+ const publicToViewer = teamIds.includes(task.destination.teamId) && destination?.visibility === "public";
370
+ if (!ownedByViewer && !publicToViewer) return void 0;
371
+ return {
372
+ kind: "event",
373
+ ownedByViewer,
374
+ publicToViewer,
375
+ task
376
+ };
377
+ }
378
+ /** Read one current Automation summary with the Automations view access rules. */
379
+ async function readViewerAutomationSummary(user, id) {
380
+ const candidate = await resolveViewerTaskCandidate(user, "scheduled", id) ?? await resolveViewerTaskCandidate(user, "event", id);
381
+ if (!candidate) return void 0;
382
+ return automationSummaryForCandidate(candidate);
383
+ }
384
+ async function automationSummaryForCandidate(candidate) {
385
+ const [destinations, creatorEmails, stats] = await Promise.all([
386
+ destinationDetails([candidate.task.destination]),
387
+ creatorProfileEmails([candidate]),
388
+ readAutomationExecutionSummaries(candidate.kind, "junior")
389
+ ]);
390
+ const destination = destinations.get(destinationKey(candidate.task.destination)) ?? {
391
+ label: `Channel ${candidate.task.destination.channelId}`,
392
+ visibility: "private"
393
+ };
394
+ const createdByEmail = creatorEmails.get(creatorKey(candidate.task.destination.teamId, candidate.task.createdBy.slackUserId));
395
+ if (candidate.kind === "scheduled") return scheduledAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), createdByEmail);
396
+ return eventAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), createdByEmail);
397
+ }
398
+ /** Read viewer-owned and public-workspace automations as one bounded newest-first projection. */
399
+ function emptyAutomationExecutionDay(date) {
400
+ return {
401
+ costUsd: 0,
402
+ date,
403
+ event: 0,
404
+ scheduled: 0
405
+ };
406
+ }
407
+ function emptyAutomationExecutionStatusDay(date) {
408
+ return {
409
+ blocked: 0,
410
+ completed: 0,
411
+ date,
412
+ failed: 0
413
+ };
414
+ }
415
+ function automationExecutionSixHours(hours, nowMs = Date.now()) {
416
+ return sumUtcHoursIntoSixHours({
417
+ empty: emptyAutomationExecutionDay,
418
+ hours,
419
+ nowMs
420
+ });
421
+ }
422
+ function automationExecutionStatusSixHours(hours, nowMs = Date.now()) {
423
+ return sumUtcHoursIntoSixHours({
424
+ empty: emptyAutomationExecutionStatusDay,
425
+ hours,
426
+ nowMs
427
+ });
428
+ }
429
+ /** List one viewer's scheduled and event automations, optionally filtered by title or instruction search. */
430
+ async function readViewerAutomations(user, input = {}) {
431
+ const db = getDb();
432
+ const query = input.q?.trim().toLowerCase() || void 0;
433
+ const identityIds = new Set(user.identities.map((identity) => identity.id));
434
+ const teamIds = viewerTeamIds(user);
435
+ const [scheduledPage, publicScheduled, eventAutomations, publicEventAutomations] = await Promise.all([
436
+ listViewerScheduledAutomations(db, user, {
437
+ limit: TASK_FETCH_LIMIT,
438
+ query
439
+ }),
440
+ listPublicScheduledAutomationsForTeams(db, teamIds, {
441
+ limit: TASK_FETCH_LIMIT,
442
+ query
443
+ }),
444
+ listEventAutomationsCreatedBy(db, user, TASK_FETCH_LIMIT, query),
445
+ listPublicEventAutomationsForTeams(db, teamIds, TASK_FETCH_LIMIT, query)
446
+ ]);
447
+ const candidatesById = /* @__PURE__ */ new Map();
448
+ const publicScheduledIds = new Set(publicScheduled.map((task) => task.id));
449
+ const publicEventAutomationIds = new Set(publicEventAutomations.map((task) => task.id));
450
+ for (const task of [...scheduledPage.automations, ...publicScheduled]) candidatesById.set(`scheduled:${task.id}`, {
451
+ kind: "scheduled",
452
+ ownedByViewer: identityIds.has(task.creatorIdentityId),
453
+ publicToViewer: publicScheduledIds.has(task.id),
454
+ task
455
+ });
456
+ for (const task of [...eventAutomations, ...publicEventAutomations]) candidatesById.set(`event:${task.id}`, {
457
+ kind: "event",
458
+ ownedByViewer: eventAutomationBelongsToUser(task, user),
459
+ publicToViewer: publicEventAutomationIds.has(task.id),
460
+ task
461
+ });
462
+ const candidates = [...candidatesById.values()].sort((left, right) => right.task.createdAtMs - left.task.createdAtMs || right.task.id.localeCompare(left.task.id));
463
+ const ownedCandidates = candidates.filter((candidate) => candidate.ownedByViewer);
464
+ const publicCandidates = candidates.filter((candidate) => candidate.publicToViewer);
465
+ const selectedCandidates = /* @__PURE__ */ new Set([...ownedCandidates.slice(0, TASK_LIST_LIMIT), ...publicCandidates.slice(0, TASK_LIST_LIMIT)]);
466
+ const selected = candidates.filter((candidate) => selectedCandidates.has(candidate));
467
+ const [destinations, creatorEmails] = await Promise.all([destinationDetails(selected.map(({ task }) => task.destination)), creatorProfileEmails(selected)]);
468
+ const visible = selected.filter((candidate) => candidate.ownedByViewer || destinations.get(destinationKey(candidate.task.destination))?.visibility === "public");
469
+ const [executionDays, executionHours, scheduledStats, eventStats] = await Promise.all([
470
+ readAutomationExecutionDays(),
471
+ readAutomationExecutionHours(),
472
+ readAutomationExecutionSummaries("scheduled", "junior"),
473
+ readAutomationExecutionSummaries("event", "junior")
474
+ ]);
475
+ const automations = visible.map((candidate) => {
476
+ const destination = destinations.get(destinationKey(candidate.task.destination)) ?? {
477
+ label: `Channel ${candidate.task.destination.channelId}`,
478
+ visibility: "private"
479
+ };
480
+ const createdByEmail = creatorEmails.get(creatorKey(candidate.task.destination.teamId, candidate.task.createdBy.slackUserId));
481
+ if (candidate.kind === "scheduled") return scheduledAutomationSummary(candidate.task, candidate.ownedByViewer, destination, scheduledStats.get(candidate.task.id), createdByEmail);
482
+ return eventAutomationSummary(candidate.task, candidate.ownedByViewer, destination, eventStats.get(candidate.task.id), createdByEmail);
483
+ });
484
+ return {
485
+ executionDays,
486
+ executionHours,
487
+ executionSixHours: automationExecutionSixHours(executionHours),
488
+ automations,
489
+ truncated: ownedCandidates.length > TASK_LIST_LIMIT || publicCandidates.length > TASK_LIST_LIMIT
490
+ };
491
+ }
492
+ /** Load deleted scheduled automations the viewer still owns, newest first. */
493
+ async function readDeletedOwnedScheduledAutomations(user) {
494
+ const identityIds = user.identities.map((identity) => identity.id);
495
+ if (identityIds.length === 0) return [];
496
+ return (await getDb().select({
497
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
498
+ id: juniorSchedulerTasks.id,
499
+ record: juniorSchedulerTasks.record,
500
+ title: juniorSchedulerTasks.title
501
+ }).from(juniorSchedulerTasks).where(and(eq(juniorSchedulerTasks.status, "deleted"), inArray(juniorSchedulerTasks.creatorIdentityId, identityIds))).orderBy(desc(juniorSchedulerTasks.createdAtMs), desc(juniorSchedulerTasks.id)).limit(TASK_EXECUTION_LIST_LIMIT)).map(parseScheduledAutomationRow).filter((task) => task?.status === "deleted");
502
+ }
503
+ /** Load deleted event automations the viewer still owns, newest first. */
504
+ async function readDeletedOwnedEventAutomations(user) {
505
+ return await listDeletedEventAutomationsCreatedBy(getDb(), user, TASK_EXECUTION_LIST_LIMIT);
506
+ }
507
+ function automationTitleForRun(run, automationTitles) {
508
+ return automationTitles.get(`${run.kind}:${run.automationId}`) ?? run.title?.trim() ?? "Untitled task";
509
+ }
510
+ function addDeletedAutomationRun(args, automationTitles, automations) {
511
+ const key = `${args.kind}:${args.automationId}`;
512
+ if (automationTitles.has(key)) return;
513
+ automationTitles.set(key, args.title);
514
+ automations.push({
515
+ kind: args.kind,
516
+ automationId: args.automationId
517
+ });
518
+ }
519
+ /**
520
+ * Read newest runs across live viewer-visible automations and deleted automations the
521
+ * viewer owns. Deleted task rows stay available so history does not depend on
522
+ * conversation actor membership.
523
+ */
524
+ async function readViewerAutomationRuns(user) {
525
+ const [taskList, deletedScheduled, deletedEvent] = await Promise.all([
526
+ readViewerAutomations(user),
527
+ readDeletedOwnedScheduledAutomations(user),
528
+ readDeletedOwnedEventAutomations(user)
529
+ ]);
530
+ const automationTitles = /* @__PURE__ */ new Map();
531
+ const automations = [];
532
+ for (const task of taskList.automations) {
533
+ automationTitles.set(`${task.kind}:${task.id}`, task.title);
534
+ automations.push({
535
+ kind: task.kind,
536
+ automationId: task.id
537
+ });
538
+ }
539
+ for (const task of deletedScheduled) {
540
+ const instruction = displayText(task.task.text, "Untitled scheduled automation");
541
+ addDeletedAutomationRun({
542
+ kind: "scheduled",
543
+ automationId: task.id,
544
+ title: taskDisplayTitle(task.title, instruction, "Untitled scheduled automation")
545
+ }, automationTitles, automations);
546
+ }
547
+ for (const task of deletedEvent) {
548
+ const instruction = displayText(task.task.text, "Untitled event automation");
549
+ addDeletedAutomationRun({
550
+ kind: "event",
551
+ automationId: task.id,
552
+ title: taskDisplayTitle(task.title, instruction, "Untitled event automation")
553
+ }, automationTitles, automations);
554
+ }
555
+ const runs = await readAutomationRuns({
556
+ limit: 101,
557
+ automations
558
+ });
559
+ return {
560
+ runs: runs.slice(0, TASK_EXECUTION_LIST_LIMIT).map((run) => ({
561
+ ...run,
562
+ automationTitle: automationTitleForRun(run, automationTitles)
563
+ })),
564
+ truncated: runs.length > TASK_EXECUTION_LIST_LIMIT
565
+ };
566
+ }
567
+ var ViewerTaskNotFoundError = class extends Error {
568
+ constructor() {
569
+ super("Task was not found.");
570
+ this.name = "ViewerTaskNotFoundError";
571
+ }
572
+ };
573
+ /** Read one viewer-visible task and its newest terminal executions. */
574
+ async function readViewerAutomationExecutions(user, kind, id) {
575
+ const candidate = await resolveViewerTaskCandidate(user, kind, id);
576
+ if (!candidate) throw new ViewerTaskNotFoundError();
577
+ const [automation, executions, executionDays, executionHours] = await Promise.all([
578
+ automationSummaryForCandidate(candidate),
579
+ readAutomationExecutions({
580
+ kind,
581
+ limit: 101,
582
+ automationId: id
583
+ }),
584
+ readAutomationExecutionStatusDays({
585
+ kind,
586
+ automationId: id
587
+ }),
588
+ readAutomationExecutionStatusHours({
589
+ kind,
590
+ automationId: id
591
+ })
592
+ ]);
593
+ return {
594
+ executionDays,
595
+ executionHours,
596
+ executionSixHours: automationExecutionStatusSixHours(executionHours),
597
+ executions: executions.slice(0, TASK_EXECUTION_LIST_LIMIT),
598
+ automation,
599
+ truncated: executions.length > TASK_EXECUTION_LIST_LIMIT
600
+ };
601
+ }
602
+ /** Resolve the source task for one conversation when a terminal execution links it. */
603
+ async function readConversationSourceTask(args) {
604
+ const execution = await readAutomationExecutionByConversationId({ conversationId: args.conversationId });
605
+ if (!execution) return void 0;
606
+ if (!args.viewer) return { kind: execution.kind };
607
+ const candidate = await resolveViewerTaskCandidate(args.viewer, execution.kind, execution.automationId);
608
+ if (!candidate) return { kind: execution.kind };
609
+ if (candidate.kind === "scheduled") {
610
+ const instruction = displayText(candidate.task.task.text, "Untitled scheduled automation");
611
+ return {
612
+ id: candidate.task.id,
613
+ kind: "scheduled",
614
+ label: instruction,
615
+ title: taskDisplayTitle(candidate.task.title, instruction, "Untitled scheduled automation")
616
+ };
617
+ }
618
+ const instruction = displayText(candidate.task.task.text, "Untitled event automation");
619
+ return {
620
+ id: candidate.task.id,
621
+ kind: "event",
622
+ label: instruction,
623
+ title: taskDisplayTitle(candidate.task.title, instruction, "Untitled event automation")
624
+ };
625
+ }
626
+ /** Delete one viewer-owned scheduled or event automation. */
627
+ async function deleteViewerTask(user, kind, id) {
628
+ if (kind === "scheduled") try {
629
+ await deleteViewerScheduledAutomation(getDb(), user, id);
630
+ return;
631
+ } catch (error) {
632
+ if (error instanceof PersonalScheduledAutomationNotFoundError) throw new ViewerTaskNotFoundError();
633
+ throw error;
634
+ }
635
+ const task = await getEventAutomation(getDb(), id);
636
+ if (!task || task.status === "deleted" || !eventAutomationBelongsToUser(task, user)) throw new ViewerTaskNotFoundError();
637
+ await deleteEventAutomation(getDb(), id);
638
+ }
639
+ //#endregion
640
+ //#region src/chat/slack/conversation-context.ts
641
+ function normalizeConversationName(type, channelName) {
642
+ const trimmed = channelName?.trim();
643
+ if (!trimmed) return void 0;
644
+ if (type === "public_channel" || type === "private_channel" || type === "private_channel_or_group_dm") return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
645
+ return trimmed;
646
+ }
647
+ function typeFromSlackChannelType(channelType) {
648
+ if (channelType === "channel") return "public_channel";
649
+ if (channelType === "group") return "private_channel";
650
+ if (channelType === "mpim") return "group_dm";
651
+ if (channelType === "im") return "direct_message";
652
+ }
653
+ function typeFromChannelId(channelId, channelName) {
654
+ const normalized = normalizeSlackConversationId(channelId);
655
+ if (!normalized) return void 0;
656
+ if (normalized.startsWith("C")) return "public_channel";
657
+ if (normalized.startsWith("D")) return "direct_message";
658
+ if (normalized.startsWith("G")) return channelName?.trim().startsWith("mpdm-") ? "group_dm" : "private_channel_or_group_dm";
659
+ }
660
+ function toSlackEventChannelType(channelType) {
661
+ if (channelType === "channel" || channelType === "group" || channelType === "mpim" || channelType === "im") return channelType;
662
+ }
663
+ /**
664
+ * Map Slack's Events API channel_type to a source-confirmed visibility.
665
+ *
666
+ * Slack's event metadata is the only visibility authority; channel IDs never
667
+ * classify visibility.
668
+ */
669
+ function conversationVisibilityFromSlackChannelType(channelType) {
670
+ if (!channelType) return void 0;
671
+ return channelType === "channel" ? "public" : "private";
672
+ }
673
+ /** Resolve Slack's raw event channel type from a Chat SDK message-like object. */
674
+ function resolveSlackChannelTypeFromMessage(message) {
675
+ const raw = message.raw;
676
+ if (!raw || typeof raw !== "object") return;
677
+ const channelType = raw.channel_type;
678
+ return typeof channelType === "string" ? toSlackEventChannelType(channelType.trim()) : void 0;
679
+ }
680
+ /** Build Slack conversation facts available to runtime consumers. */
681
+ function resolveSlackConversationContext(input) {
682
+ const type = typeFromSlackChannelType(input.channelType) ?? typeFromChannelId(input.channelId, input.channelName);
683
+ if (!type) return void 0;
684
+ const name = normalizeConversationName(type, input.channelName);
685
+ const visibility = conversationVisibilityFromSlackChannelType(input.channelType);
686
+ return {
687
+ type,
688
+ ...name ? { name } : void 0,
689
+ ...visibility ? { visibility } : void 0
690
+ };
691
+ }
692
+ /** Build Slack conversation facts from Junior's persisted Slack thread id. */
693
+ function resolveSlackConversationContextFromThreadId(input) {
694
+ const context = resolveSlackConversationContext({
695
+ channelId: parseSlackThreadId(input.threadId)?.channelId,
696
+ channelName: input.channelName
697
+ });
698
+ if (!context || !input.visibility) return context;
699
+ return {
700
+ ...context,
701
+ type: input.visibility === "private" && context.type === "public_channel" ? "private_channel" : context.type,
702
+ visibility: input.visibility
703
+ };
704
+ }
705
+ /** Render a human label for a privacy-preserving Slack conversation type. */
706
+ function formatSlackConversationTypeLabel(type) {
707
+ if (type === "public_channel") return "Public Channel";
708
+ if (type === "private_channel") return "Private Channel";
709
+ if (type === "group_dm") return "Group DM";
710
+ if (type === "direct_message") return "Direct Message";
711
+ return "Private Channel or Group DM";
712
+ }
713
+ /** Render a Slack conversation label without exposing conversation names. */
714
+ function formatSlackConversationRedactedLabel(context) {
715
+ if (!context) return void 0;
716
+ return formatSlackConversationTypeLabel(context.type);
717
+ }
718
+ //#endregion
719
+ //#region src/chat/conversations/destination.ts
720
+ function requireLocalDestination(conversationId) {
721
+ const parsed = localDestinationSchema.safeParse({
722
+ platform: "local",
723
+ conversationId
724
+ });
725
+ if (!parsed.success) throw new Error(`Invalid local conversation id: ${conversationId}`);
726
+ return parsed.data;
727
+ }
728
+ /**
729
+ * Keep a stored Destination, or create the temporary local Destination.
730
+ *
731
+ * TODO(dcramer): Delete this module after Conversation and AgentRun use
732
+ * Location, and web and event Turns no longer need a local
733
+ * Destination.
734
+ */
735
+ function resolveConversationDestination(args) {
736
+ return args.existing ?? requireLocalDestination(args.conversationId);
737
+ }
738
+ //#endregion
739
+ //#region src/chat/conversations/web-input.ts
740
+ /** Store web input in a Conversation mailbox. */
741
+ function normalizeEmail(email) {
742
+ return email.trim().toLowerCase();
743
+ }
744
+ function stableHex(...parts) {
745
+ return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
746
+ }
747
+ /**
748
+ * Build a durable Conversation id for one web Actor and create key.
749
+ *
750
+ * Retries with the same key must address the same Conversation before the
751
+ * mailbox Message id is derived.
752
+ */
753
+ function createConversationId(args) {
754
+ return `local:web:${stableHex(normalizeEmail(args.actorEmail), args.idempotencyKey)}`;
755
+ }
756
+ /**
757
+ * Build the retry-stable id for one web Message.
758
+ *
759
+ * TODO(dcramer): Replace the `api-msg` prefix after deployed request retries
760
+ * no longer need to derive ids written by the old web input code.
761
+ */
762
+ function webMessageId(args) {
763
+ return `api-msg:${stableHex(args.conversationId, args.idempotencyKey)}`;
764
+ }
765
+ /** Return the stable Turn id for one mailbox Message. */
766
+ function conversationTurnIdForMessage(messageId) {
767
+ return buildDeterministicTurnId(messageId);
768
+ }
769
+ /**
770
+ * Store the dashboard participant in the current Conversation actor field.
771
+ *
772
+ * TODO(dcramer): Remove this StoredSlackActor conversion after Conversation
773
+ * actor storage accepts web Actors directly.
774
+ */
775
+ function storedActorFromWeb(actor) {
776
+ return {
777
+ ...actor.email ? { email: normalizeEmail(actor.email) } : void 0,
778
+ ...actor.fullName ? { fullName: actor.fullName } : void 0
779
+ };
780
+ }
781
+ /** Rebuild the web Actor from durable Conversation identity. */
782
+ function webActorFromEmail(email, profile) {
783
+ const normalized = normalizeEmail(email);
784
+ return {
785
+ platform: "web",
786
+ userId: `dashboard:${stableHex(normalized)}`,
787
+ email: normalized,
788
+ ...profile?.fullName ? { fullName: profile.fullName } : void 0,
789
+ ...profile?.userName ? { userName: profile.userName } : void 0
790
+ };
791
+ }
792
+ /** Build one web mailbox entry. */
793
+ function buildWebInboundMessage(args) {
794
+ const text = args.message.trim();
795
+ if (!text) throw new Error("Web Message must not be empty");
796
+ if (!args.actor.email) throw new Error("Web Actor requires a verified email");
797
+ const destination = resolveConversationDestination({
798
+ conversationId: args.conversationId,
799
+ existing: args.destination
800
+ });
801
+ const nowMs = args.nowMs ?? Date.now();
802
+ return {
803
+ conversationId: args.conversationId,
804
+ createdAtMs: args.createdAtMs ?? nowMs,
805
+ delivery: "defer",
806
+ destination,
807
+ inboundMessageId: args.messageId,
808
+ input: {
809
+ authorId: args.actor.userId,
810
+ text,
811
+ metadata: {
812
+ authorEmail: normalizeEmail(args.actor.email),
813
+ ...args.actor.fullName && { authorFullName: args.actor.fullName },
814
+ authorUserId: args.actor.userId,
815
+ ...args.actor.userName && { authorUserName: args.actor.userName },
816
+ kind: "api_turn",
817
+ messageId: args.messageId
818
+ }
819
+ },
820
+ receivedAtMs: nowMs,
821
+ source: "web"
822
+ };
823
+ }
824
+ /** Record web activity and create a Conversation root when needed. */
825
+ async function recordWebConversationActivity(args) {
826
+ const store = args.conversationStore ?? getConversationStore();
827
+ const existing = await store.get({ conversationId: args.conversationId });
828
+ const destination = resolveConversationDestination({
829
+ conversationId: args.conversationId,
830
+ existing: existing?.destination
831
+ });
832
+ const isNewRoot = !existing;
833
+ const visibility = args.rootVisibility === "private" ? "private" : "public";
834
+ const source = isNewRoot ? createWebSource(args.conversationId, visibility) : void 0;
835
+ await store.recordActivity({
836
+ conversationId: args.conversationId,
837
+ destination,
838
+ nowMs: args.nowMs,
839
+ actor: storedActorFromWeb(args.actor),
840
+ ...isNewRoot ? { source: "web" } : void 0,
841
+ ...source ? { sessionSource: source } : void 0,
842
+ ...isNewRoot ? { visibility } : void 0
843
+ });
844
+ return destination;
845
+ }
846
+ /** Create a Conversation from web input and enqueue its first Message. */
847
+ async function createAndEnqueueConversation(input, options) {
848
+ if (!input.actor.email) throw new Error("Web Actor requires a verified email");
849
+ const conversationId = createConversationId({
850
+ actorEmail: input.actor.email,
851
+ idempotencyKey: input.idempotencyKey
852
+ });
853
+ return await appendAndEnqueueWebMessage({
854
+ actor: input.actor,
855
+ conversationId,
856
+ idempotencyKey: input.idempotencyKey,
857
+ message: input.message,
858
+ rootVisibility: input.visibility === "private" ? "private" : "public"
859
+ }, options);
860
+ }
861
+ async function appendAndEnqueueWebMessage(input, options) {
862
+ const text = input.message.trim();
863
+ if (!text) throw new Error("Web Message must not be empty");
864
+ if (!input.actor.email) throw new Error("Web Actor requires a verified email");
865
+ const nowMs = options.nowMs ?? Date.now();
866
+ const messageId = webMessageId({
867
+ conversationId: input.conversationId,
868
+ idempotencyKey: input.idempotencyKey
869
+ });
870
+ const destination = await recordWebConversationActivity({
871
+ actor: input.actor,
872
+ conversationId: input.conversationId,
873
+ conversationStore: options.conversationStore,
874
+ nowMs,
875
+ ...input.rootVisibility && { rootVisibility: input.rootVisibility }
876
+ });
877
+ const result = await (options.exclusive ? appendAndEnqueueExclusiveInboundMessage : appendAndEnqueueInboundMessage)({
878
+ message: buildWebInboundMessage({
879
+ actor: input.actor,
880
+ conversationId: input.conversationId,
881
+ destination,
882
+ message: text,
883
+ messageId,
884
+ nowMs
885
+ }),
886
+ conversationStore: options.conversationStore,
887
+ nowMs,
888
+ queue: options.queue,
889
+ state: options.state
890
+ });
891
+ const status = result.status === "appended" ? "accepted" : result.status;
892
+ return {
893
+ conversationId: input.conversationId,
894
+ messageId,
895
+ status
896
+ };
897
+ }
898
+ //#endregion
899
+ export { readHealthReport as C, GET as S, readViewerAutomationSummary as _, webActorFromEmail as a, legacyWebMailboxMetadataSchema as b, formatSlackConversationRedactedLabel as c, resolveSlackConversationContextFromThreadId as d, ViewerTaskNotFoundError as f, readViewerAutomationRuns as g, readViewerAutomationExecutions as h, recordWebConversationActivity as i, resolveSlackChannelTypeFromMessage as l, readConversationSourceTask as m, conversationTurnIdForMessage as n, resolveConversationDestination as o, deleteViewerTask as p, createAndEnqueueConversation as r, conversationVisibilityFromSlackChannelType as s, appendAndEnqueueWebMessage as t, resolveSlackConversationContext as u, readViewerAutomations as v, createVercelAttachmentStorage as x, lookupSlackUser as y };