@sentry/junior 0.203.0 → 0.204.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 (174) hide show
  1. package/dist/agent-CMyTD4DD.js +17509 -0
  2. package/dist/agent-hooks-Bbcp9e33.js +4040 -0
  3. package/dist/agent-runner-DNb1A49M.js +678 -0
  4. package/dist/api/schema.d.ts +2728 -2639
  5. package/dist/api/schema.js +4 -169
  6. package/dist/api.d.ts +23 -17
  7. package/dist/api.js +3881 -5326
  8. package/dist/app-B1TOqBI8.d.ts +246 -0
  9. package/dist/app.d.ts +4 -520
  10. package/dist/app.js +9522 -11875
  11. package/dist/auth-pause-state-DosEalIG.js +168 -0
  12. package/dist/catalog-runtime-BReos-D4.js +7 -0
  13. package/dist/checkpoint-CNrPyRJN.js +2450 -0
  14. package/dist/cli/chat.d.ts +20 -0
  15. package/dist/cli/chat.js +229 -336
  16. package/dist/cli/check.d.ts +8 -0
  17. package/dist/cli/check.js +513 -806
  18. package/dist/cli/env.d.ts +13 -0
  19. package/dist/cli/env.js +63 -12
  20. package/dist/cli/init.d.ts +3 -0
  21. package/dist/cli/init.js +90 -159
  22. package/dist/cli/main.d.ts +6 -0
  23. package/dist/cli/main.js +43 -64
  24. package/dist/cli/plugins.d.ts +13 -0
  25. package/dist/cli/plugins.js +148 -227
  26. package/dist/cli/run.d.ts +16 -0
  27. package/dist/cli/run.js +52 -8
  28. package/dist/cli/snapshot-create.d.ts +4 -0
  29. package/dist/cli/snapshot-create.js +71 -125
  30. package/dist/cli/upgrade.d.ts +15 -0
  31. package/dist/cli/upgrade.js +232 -335
  32. package/dist/config-nGCW1OTG.js +941 -0
  33. package/dist/context-B45_WqVX.js +298 -0
  34. package/dist/conversation-privacy-B4voFRSm.js +182 -0
  35. package/dist/conversation-work-C6y3T5Hx.js +51 -0
  36. package/dist/credential-sync-kXr1hvO1.js +2660 -0
  37. package/dist/db-BeyXKJOI.d.ts +5 -0
  38. package/dist/destinations-BeBiJeEM.js +29 -0
  39. package/dist/execution-stats-DWfSihcH.js +373 -0
  40. package/dist/executor-DcMSlI5c.js +721 -0
  41. package/dist/experimental-CIXf7Uye.js +46 -0
  42. package/dist/history-ClE4toxe.d.ts +32 -0
  43. package/dist/history-CqTuSAdI.js +441 -0
  44. package/dist/instrumentation.d.ts +3 -3
  45. package/dist/instrumentation.js +130 -176
  46. package/dist/job-queue-Drn_LXlM.js +63 -0
  47. package/dist/logging-BzAk1S_s.js +1210 -0
  48. package/dist/model-profile-CZmVoGdX.js +49 -0
  49. package/dist/nitro.d.ts +31 -36
  50. package/dist/nitro.js +275 -456
  51. package/dist/oauth-callback-server-JiftokvA.js +130 -0
  52. package/dist/oauth-relay-CdCnu80W.js +62 -0
  53. package/dist/package-discovery-7a9YqLhE.js +455 -0
  54. package/dist/pending-auth-CWmSdZVg.js +185 -0
  55. package/dist/plugin-module-BKGOXca2.js +81 -0
  56. package/dist/plugins-BI9A1vKA.d.ts +95 -0
  57. package/dist/plugins-egKWjgue.js +99 -0
  58. package/dist/private-trace-result-BLvBjy_e.js +16 -0
  59. package/dist/profile-CrT5nXbw.js +861 -0
  60. package/dist/registry-DH1xUo2N.js +1419 -0
  61. package/dist/reporting-schema-DosTN__Q.js +105 -0
  62. package/dist/resolve-CZHnfxaS.js +546 -0
  63. package/dist/rolldown-runtime-B4FMCO8f.js +28 -0
  64. package/dist/runner-CZV2sC46.js +392 -0
  65. package/dist/sandbox-egress-signals-bHZfmhbd.js +46 -0
  66. package/dist/schema-Rn8-XEaN.js +1140 -0
  67. package/dist/sentry-CD3SdJl-.js +26 -0
  68. package/dist/skills-CIcDQSgM.js +316 -0
  69. package/dist/spawn-DdkUFRmq.js +29 -0
  70. package/dist/stats-DTUbAb88.js +2634 -0
  71. package/dist/store-C5qxF2ac.d.ts +208 -0
  72. package/dist/task-queue-CJIR4RF7.js +136 -0
  73. package/dist/task-runner-D-O3A-Cp.js +639 -0
  74. package/dist/thread-state-CRc9jTJS.js +431 -0
  75. package/dist/turn-execution-BkinIdJF.js +47 -0
  76. package/dist/turn-lifecycle-DQV19VTW.js +54 -0
  77. package/dist/usage-D3EbSO7w.js +296 -0
  78. package/dist/usage-schema-CKaDkH_j.js +20 -0
  79. package/dist/validation-C2CR-OX3.js +40 -0
  80. package/dist/vercel-queue-DIkVcfK4.js +167 -0
  81. package/dist/vercel.d.ts +6 -6
  82. package/dist/vercel.js +14 -6
  83. package/dist/version-CnXZ614E.d.ts +5 -0
  84. package/dist/version-DxcWdoKa.js +8 -0
  85. package/dist/version.d.ts +2 -4
  86. package/dist/version.js +2 -6
  87. package/dist/web-authorization-RMBxAR_t.js +76 -0
  88. package/dist/web-input-3EPfpf55.js +494 -0
  89. package/dist/work-fGofI7Op.js +509 -0
  90. package/dist/worker-CsOLq-mR.js +625 -0
  91. package/package.json +8 -8
  92. package/dist/agent-hooks-GB2VPTNA.js +0 -51
  93. package/dist/catalog-runtime-UBLIWD3O.js +0 -14
  94. package/dist/chunk-2LYHWMR4.js +0 -46
  95. package/dist/chunk-352OWAZH.js +0 -320
  96. package/dist/chunk-3Y47VMHV.js +0 -6
  97. package/dist/chunk-3YJSKOWF.js +0 -164
  98. package/dist/chunk-4KSSKTQ5.js +0 -229
  99. package/dist/chunk-4UO6FK4G.js +0 -64
  100. package/dist/chunk-5AHX53NQ.js +0 -717
  101. package/dist/chunk-5J3E5FWA.js +0 -700
  102. package/dist/chunk-5PDAG6O3.js +0 -206
  103. package/dist/chunk-6Y5B5PEX.js +0 -80
  104. package/dist/chunk-7GWQ5S5R.js +0 -2086
  105. package/dist/chunk-7H2VZTWN.js +0 -103
  106. package/dist/chunk-7NL7T6VU.js +0 -406
  107. package/dist/chunk-A5L27LE4.js +0 -387
  108. package/dist/chunk-ADIAWL34.js +0 -81
  109. package/dist/chunk-ASTEMBKJ.js +0 -519
  110. package/dist/chunk-CEMMLV5L.js +0 -98
  111. package/dist/chunk-CM5HNV2W.js +0 -131
  112. package/dist/chunk-CRTRBBRI.js +0 -105
  113. package/dist/chunk-DA2KGZZI.js +0 -13
  114. package/dist/chunk-DIB4GX3G.js +0 -8
  115. package/dist/chunk-DKIJ2RP5.js +0 -68
  116. package/dist/chunk-DWPN6SCH.js +0 -172
  117. package/dist/chunk-EDKUFGPL.js +0 -64
  118. package/dist/chunk-ENY6SKIU.js +0 -1228
  119. package/dist/chunk-F4MGCZOX.js +0 -2943
  120. package/dist/chunk-FA6POGLH.js +0 -70
  121. package/dist/chunk-FSUR7GTG.js +0 -20475
  122. package/dist/chunk-GKEOHJL5.js +0 -1856
  123. package/dist/chunk-GMCB6W5Y.js +0 -333
  124. package/dist/chunk-IE2VPG7O.js +0 -1135
  125. package/dist/chunk-IZ5B5IHQ.js +0 -3656
  126. package/dist/chunk-JAL3CH4G.js +0 -27
  127. package/dist/chunk-KIIEUXD3.js +0 -1764
  128. package/dist/chunk-KXU4NIL7.js +0 -692
  129. package/dist/chunk-MTM5IFCY.js +0 -1190
  130. package/dist/chunk-N7TMSOZH.js +0 -235
  131. package/dist/chunk-NCEBK6PH.js +0 -36
  132. package/dist/chunk-NHJU2ZAL.js +0 -56
  133. package/dist/chunk-P6KTRNVC.js +0 -321
  134. package/dist/chunk-PDO5BLNM.js +0 -22
  135. package/dist/chunk-PDRNLLFT.js +0 -3295
  136. package/dist/chunk-PEQXYSSE.js +0 -45
  137. package/dist/chunk-PKEX2AQX.js +0 -198
  138. package/dist/chunk-PSJCGIGO.js +0 -1163
  139. package/dist/chunk-PZUSUL55.js +0 -506
  140. package/dist/chunk-R45M4TUS.js +0 -37
  141. package/dist/chunk-RC745GGZ.js +0 -122
  142. package/dist/chunk-SP3QKZXJ.js +0 -820
  143. package/dist/chunk-TXKUKMVO.js +0 -417
  144. package/dist/chunk-UE4GPWSD.js +0 -931
  145. package/dist/chunk-UIE3R5XU.js +0 -17
  146. package/dist/chunk-UXPG6ZIN.js +0 -84
  147. package/dist/chunk-VFUK3X5B.js +0 -50
  148. package/dist/chunk-VNMJWREK.js +0 -787
  149. package/dist/chunk-VZ7CSOX2.js +0 -706
  150. package/dist/chunk-WCNQJKFT.js +0 -10
  151. package/dist/chunk-WDZR3QQM.js +0 -135
  152. package/dist/chunk-WLYVJCHV.js +0 -1433
  153. package/dist/chunk-WS7NAFTM.js +0 -1520
  154. package/dist/chunk-X6GDDWQH.js +0 -60
  155. package/dist/chunk-XPDEYNQI.js +0 -102
  156. package/dist/chunk-XRW44VGM.js +0 -58
  157. package/dist/chunk-Y2CM7HXH.js +0 -111
  158. package/dist/chunk-Y7X25LFY.js +0 -15
  159. package/dist/conversation-work-VIOQ5ZF3.js +0 -54
  160. package/dist/db-NGDJV7YH.js +0 -26
  161. package/dist/discovery-DWVDM7UK.js +0 -44
  162. package/dist/experimental-P5FZM4CU.js +0 -12
  163. package/dist/oauth-callback-server-YFY7YEGK.js +0 -243
  164. package/dist/oauth-relay-5XPVRPYB.js +0 -8
  165. package/dist/plugins-YTYJMMJF.js +0 -14
  166. package/dist/runner-ORTNSB2Q.js +0 -522
  167. package/dist/sandbox-egress-signals-FIL7EGPR.js +0 -73
  168. package/dist/sentry-MB3NL3IR.js +0 -42
  169. package/dist/skills-H376WQOH.js +0 -27
  170. package/dist/spawn-T43WHLOS.js +0 -42
  171. package/dist/task-runner-LBZWF32V.js +0 -36
  172. package/dist/validation-REJLRKI6.js +0 -17
  173. package/dist/work-3KJ3RE76.js +0 -51
  174. package/dist/worker-QFEBU72E.js +0 -24
@@ -0,0 +1,2634 @@
1
+ import { i as getChatConfig } from "./config-nGCW1OTG.js";
2
+ import { F as isRecord$1, I as toOptionalNumber, L as toOptionalString } from "./logging-BzAk1S_s.js";
3
+ import { _ as parseSlackMessageTs, d as parseStoredSlackActor, h as parseSlackTeamId, p as parseSlackChannelId } from "./context-B45_WqVX.js";
4
+ import { B as getSlackClient, Bt as parseDestination, Et as getDb, G as withSlackRetries, Ht as sameDestination, Tt as getConversationStore, u as getPlugins, ut as JUNIOR_THREAD_STATE_TTL_MS } from "./agent-hooks-Bbcp9e33.js";
5
+ import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
6
+ import { C as juniorConversations, a as juniorSchedulerRuns, c as juniorStats, l as juniorEventTasks, o as juniorSchedulerTasks, x as juniorAttachments } from "./executor-DcMSlI5c.js";
7
+ import { o as ToolInputError } from "./usage-D3EbSO7w.js";
8
+ import { C as getDefaultRedisStateAdapterFor, w as getStateAdapter } from "./profile-CrT5nXbw.js";
9
+ import "./vercel-queue-DIkVcfK4.js";
10
+ import { z } from "zod";
11
+ import { createHash, randomUUID } from "node:crypto";
12
+ import { and, asc, desc, eq, gte, inArray, isNotNull, isNull, lte, ne, notInArray, or, sql } from "drizzle-orm";
13
+ import { actorUserIdSchema, destinationSchema, destinationVisibilitySchema, normalizeResourceEventIdentifier, resourceEventMatchSchema, resourceEventMatches, resourceEventTypeSchema, slackDestinationSchema, taskOutcomeSchema } from "@sentry/junior-plugin-api";
14
+ //#region src/chat/state/turn-id.ts
15
+ /** Build a stable turn identifier from a message ID. */
16
+ function buildDeterministicTurnId(messageId) {
17
+ return `turn_${messageId.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
18
+ }
19
+ /** Build the stable visible assistant-message identity for one logical turn. */
20
+ function buildDeterministicAssistantMessageId(turnId) {
21
+ return `${turnId}:assistant:1`;
22
+ }
23
+ //#endregion
24
+ //#region src/chat/tool-support/structured-result.ts
25
+ const juniorToolContinuationSchema = z.object({
26
+ arguments: z.record(z.string(), z.unknown()),
27
+ reason: z.string().min(1).optional()
28
+ }).strict();
29
+ const juniorToolBoundContinuationSchema = juniorToolContinuationSchema.extend({ tool_name: z.string().min(1) });
30
+ /** Shared optional fields for canonical Junior tool outputs. */
31
+ const juniorToolOutputSchema = z.object({
32
+ target: z.string().min(1).optional(),
33
+ truncated: z.boolean().optional(),
34
+ /** True when this attempt did not finish before its time budget. */
35
+ timed_out: z.boolean().optional(),
36
+ continuation: juniorToolContinuationSchema.optional()
37
+ }).passthrough();
38
+ function sortJsonValue(value) {
39
+ if (Array.isArray(value)) return value.map(sortJsonValue);
40
+ if (!value || typeof value !== "object") return value;
41
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, sortJsonValue(item)]));
42
+ }
43
+ function isRecord(value) {
44
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
45
+ }
46
+ /** Bind continuation metadata to the exposed runtime tool name. */
47
+ function injectContinuationToolName(details, toolName) {
48
+ if (!isRecord(details) || !isRecord(details.continuation)) return details;
49
+ return {
50
+ ...details,
51
+ continuation: juniorToolBoundContinuationSchema.parse({
52
+ ...details.continuation,
53
+ tool_name: toolName
54
+ })
55
+ };
56
+ }
57
+ function makeStructuredToolOutput(details, options = {}) {
58
+ return {
59
+ content: [{
60
+ type: "text",
61
+ text: JSON.stringify(sortJsonValue(details))
62
+ }, ...options.content ?? []],
63
+ details
64
+ };
65
+ }
66
+ //#endregion
67
+ //#region src/chat/task-execution/state.ts
68
+ /**
69
+ * Expiring conversation mailbox and execution index store.
70
+ *
71
+ * The conversation record owns pending inbound work, execution status, and the
72
+ * lease. `conversation:active` feeds heartbeat recovery and contains every
73
+ * non-idle conversation until the lease/status path makes it idle.
74
+ * `conversation:by-activity` supports legacy SQL import only; SQL owns durable
75
+ * conversation metadata in configured runtime.
76
+ */
77
+ const CONVERSATION_PREFIX = "junior:conversation:v2";
78
+ const CONVERSATION_SCHEMA_VERSION = 2;
79
+ const CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH = 1e4;
80
+ const CONVERSATION_INDEX_LOCK_TTL_MS = 1e4;
81
+ const CONVERSATION_INDEX_LOCK_WAIT_MS = 2e3;
82
+ const CONVERSATION_INDEX_LOCK_RETRY_MS = 25;
83
+ const CONVERSATION_MUTATION_LOCK_TTL_MS = 1e4;
84
+ const CONVERSATION_MUTATION_WAIT_MS = 1e4;
85
+ const CONVERSATION_MUTATION_RETRY_MS = 25;
86
+ var InvalidConversationRecordError = class extends Error {
87
+ constructor(conversationId) {
88
+ super(`Conversation record is invalid for ${conversationId}`);
89
+ this.name = "InvalidConversationRecordError";
90
+ }
91
+ };
92
+ /** Return whether an error means a stored conversation record failed validation. */
93
+ function isInvalidConversationRecordError(error) {
94
+ return error instanceof InvalidConversationRecordError;
95
+ }
96
+ var ConversationMutationFencedError = class extends Error {
97
+ constructor(conversationId) {
98
+ super(`Conversation mutation lock was lost before write for ${conversationId}`);
99
+ this.name = "ConversationMutationFencedError";
100
+ }
101
+ };
102
+ const CONVERSATION_BY_ACTIVITY_INDEX_KEY = `${CONVERSATION_PREFIX}:by-activity`;
103
+ const CONVERSATION_ACTIVE_INDEX_KEY = `${CONVERSATION_PREFIX}:active`;
104
+ const CONVERSATION_WORK_LEASE_TTL_MS = 9e4;
105
+ const CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15e3;
106
+ const CONVERSATION_WORK_STALE_ENQUEUE_MS = 6e4;
107
+ const inboundMessageSourceSchema = z.enum([
108
+ "api",
109
+ "internal",
110
+ "local",
111
+ "plugin",
112
+ "resource_event",
113
+ "scheduler",
114
+ "slack",
115
+ "web"
116
+ ]);
117
+ const agentInputSchema = z.object({
118
+ attachments: z.array(z.unknown()).optional(),
119
+ authorId: z.string().min(1).optional(),
120
+ metadata: z.record(z.string(), z.unknown()).optional(),
121
+ text: z.string()
122
+ }).strict().refine((input) => input.text.trim() || input.attachments?.length, { message: "agent input requires text or attachments" });
123
+ /** Durable delivery modes for pending inbound mailbox work. */
124
+ const inboundMessageDeliverySchema = z.enum(["defer", "interrupt"]);
125
+ /** Redis mailbox shape kept for deployed workers that still require this field. */
126
+ const storedInboundMessageSchema = z.object({
127
+ attemptCount: z.number().finite().optional(),
128
+ conversationId: z.string().refine((value) => value.trim().length > 0),
129
+ createdAtMs: z.number().finite(),
130
+ delivery: inboundMessageDeliverySchema,
131
+ destination: destinationSchema.optional(),
132
+ inboundMessageId: z.string().refine((value) => value.trim().length > 0),
133
+ injectedAtMs: z.number().finite().optional(),
134
+ input: agentInputSchema,
135
+ receivedAtMs: z.number().finite(),
136
+ source: inboundMessageSourceSchema
137
+ }).strict().extend({ publishExternally: z.boolean() });
138
+ function conversationKey(conversationId) {
139
+ return `${CONVERSATION_PREFIX}:${conversationId}`;
140
+ }
141
+ function indexLockKey(indexKey) {
142
+ return `${indexKey}:lock`;
143
+ }
144
+ function mutationLockKey(conversationId) {
145
+ return `${CONVERSATION_PREFIX}:mutation:${conversationId}`;
146
+ }
147
+ function now$1() {
148
+ return Date.now();
149
+ }
150
+ function compareMessages(left, right) {
151
+ return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
152
+ }
153
+ function inputHasAttachments(input) {
154
+ return Array.isArray(input.attachments) && input.attachments.length > 0;
155
+ }
156
+ /**
157
+ * Upgrade a still-pending payload when a duplicate source event carries file
158
+ * attachments the stored copy lacks (Slack `app_mention`/`message` twins),
159
+ * keeping the stored ordering timestamps and delivery attempts.
160
+ */
161
+ function upgradedPendingMessage(stored, duplicate) {
162
+ const promotesInterrupt = stored.delivery === "defer" && duplicate.delivery === "interrupt";
163
+ const delivery = promotesInterrupt ? "interrupt" : stored.delivery;
164
+ const input = promotesInterrupt ? {
165
+ ...duplicate.input,
166
+ ...inputHasAttachments(duplicate.input) || !inputHasAttachments(stored.input) ? void 0 : { attachments: stored.input.attachments }
167
+ } : inputHasAttachments(duplicate.input) && !inputHasAttachments(stored.input) ? duplicate.input : stored.input;
168
+ if (input === stored.input && delivery === stored.delivery) return stored;
169
+ return {
170
+ ...stored,
171
+ delivery,
172
+ input
173
+ };
174
+ }
175
+ function compareIndexDescending(left, right) {
176
+ return right.score - left.score || right.conversationId.localeCompare(left.conversationId);
177
+ }
178
+ function compareIndexAscending(left, right) {
179
+ return left.score - right.score || left.conversationId.localeCompare(right.conversationId);
180
+ }
181
+ function uniqueStrings(values) {
182
+ return [...new Set(values)];
183
+ }
184
+ function normalizeSource(value) {
185
+ const parsed = inboundMessageSourceSchema.safeParse(value);
186
+ return parsed.success ? parsed.data : void 0;
187
+ }
188
+ function normalizeExecutionStatus(value) {
189
+ if (value === "paused") return value;
190
+ if (value === "failed" || value === "idle" || value === "pending" || value === "running") return value;
191
+ }
192
+ function normalizeMessage(value) {
193
+ const parsed = storedInboundMessageSchema.safeParse(value);
194
+ if (!parsed.success) return;
195
+ const { publishExternally: _publishExternally, ...message } = parsed.data;
196
+ return message;
197
+ }
198
+ function storeMessage(message, publishExternally) {
199
+ return {
200
+ ...message,
201
+ publishExternally
202
+ };
203
+ }
204
+ /** Whether this is the final attempt before an unacked message is dead-lettered. */
205
+ function isFinalAttempt(message) {
206
+ return (message.attemptCount ?? 0) >= 4;
207
+ }
208
+ function normalizeActor(value) {
209
+ return parseStoredSlackActor(value);
210
+ }
211
+ function normalizeLease(value) {
212
+ if (!isRecord$1(value)) return;
213
+ const token = toOptionalString(value.token);
214
+ const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
215
+ const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
216
+ const expiresAtMs = toOptionalNumber(value.expiresAtMs);
217
+ if (!token || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof expiresAtMs !== "number") return;
218
+ return {
219
+ token,
220
+ acquiredAtMs,
221
+ lastCheckInAtMs,
222
+ expiresAtMs
223
+ };
224
+ }
225
+ function normalizeStop(value) {
226
+ if (!isRecord$1(value)) return;
227
+ const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(value.inboundMessageIds.filter((id) => typeof id === "string")) : [];
228
+ const runId = toOptionalString(value.runId);
229
+ return runId ? {
230
+ inboundMessageIds,
231
+ runId
232
+ } : void 0;
233
+ }
234
+ /** Decode execution state and repair idle records that still own work. */
235
+ function normalizeExecution(conversationId, value) {
236
+ if (!isRecord$1(value)) return;
237
+ const status = normalizeExecutionStatus(value.status);
238
+ if (!status) return;
239
+ const pendingMessages = [];
240
+ if (Array.isArray(value.pendingMessages)) {
241
+ for (const rawMessage of value.pendingMessages) {
242
+ const message = normalizeMessage(rawMessage);
243
+ if (!message || message.conversationId !== conversationId) return;
244
+ if (message.injectedAtMs !== void 0) continue;
245
+ pendingMessages.push(message);
246
+ }
247
+ pendingMessages.sort(compareMessages);
248
+ }
249
+ const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(value.inboundMessageIds.map((id) => typeof id === "string" ? id : void 0).filter((id) => Boolean(id))) : [];
250
+ const lease = normalizeLease(value.lease);
251
+ const runId = toOptionalString(value.runId);
252
+ const normalizedStop = normalizeStop(value.stop);
253
+ const stop = normalizedStop?.runId === runId ? normalizedStop : void 0;
254
+ return {
255
+ status: status === "idle" && lease ? "running" : status === "idle" && pendingMessages.length > 0 ? "pending" : status,
256
+ inboundMessageIds: uniqueStrings([...inboundMessageIds, ...pendingMessages.map((message) => message.inboundMessageId)]),
257
+ pendingCount: pendingMessages.length,
258
+ pendingMessages,
259
+ lease,
260
+ lastCheckpointAtMs: toOptionalNumber(value.lastCheckpointAtMs),
261
+ lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
262
+ lastProgressAtMs: toOptionalNumber(value.lastProgressAtMs),
263
+ retryCount: toOptionalNumber(value.retryCount),
264
+ runId,
265
+ stop,
266
+ updatedAtMs: toOptionalNumber(value.updatedAtMs)
267
+ };
268
+ }
269
+ /** Decode current conversation records. */
270
+ function normalizeConversation(conversationId, value) {
271
+ if (!isRecord$1(value) || value.schemaVersion !== CONVERSATION_SCHEMA_VERSION) return;
272
+ const storedConversationId = toOptionalString(value.conversationId);
273
+ const createdAtMs = toOptionalNumber(value.createdAtMs);
274
+ const lastActivityAtMs = toOptionalNumber(value.lastActivityAtMs);
275
+ const updatedAtMs = toOptionalNumber(value.updatedAtMs);
276
+ const execution = normalizeExecution(conversationId, value.execution);
277
+ const destination = value.destination === void 0 ? void 0 : parseDestination(value.destination);
278
+ if (storedConversationId !== conversationId || typeof createdAtMs !== "number" || typeof lastActivityAtMs !== "number" || typeof updatedAtMs !== "number" || !execution || value.destination !== void 0 && !destination) return;
279
+ if (execution.pendingMessages.length > 0 && execution.pendingMessages.some((message) => {
280
+ if (!message.destination) return false;
281
+ return !destination || !sameDestination(message.destination, destination);
282
+ })) return;
283
+ return {
284
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
285
+ conversationId,
286
+ createdAtMs,
287
+ lastActivityAtMs,
288
+ updatedAtMs,
289
+ execution,
290
+ ...destination ? { destination } : void 0,
291
+ ...toOptionalString(value.title) ? { title: toOptionalString(value.title) } : void 0,
292
+ ...toOptionalString(value.channelName) ? { channelName: toOptionalString(value.channelName) } : void 0,
293
+ ...normalizeActor(value.actor) ? { actor: normalizeActor(value.actor) } : void 0,
294
+ ...normalizeSource(value.source) ? { source: normalizeSource(value.source) } : void 0
295
+ };
296
+ }
297
+ function emptyConversation(args) {
298
+ return {
299
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
300
+ conversationId: args.conversationId,
301
+ createdAtMs: args.nowMs,
302
+ lastActivityAtMs: args.nowMs,
303
+ updatedAtMs: args.nowMs,
304
+ ...args.destination ? { destination: args.destination } : void 0,
305
+ ...args.source ? { source: args.source } : void 0,
306
+ execution: {
307
+ status: "idle",
308
+ inboundMessageIds: [],
309
+ pendingCount: 0,
310
+ pendingMessages: [],
311
+ updatedAtMs: args.nowMs
312
+ }
313
+ };
314
+ }
315
+ function isLeaseActive(lease, nowMs) {
316
+ return Boolean(lease && lease.expiresAtMs > nowMs);
317
+ }
318
+ function pendingMessages(conversation) {
319
+ return [...conversation.execution.pendingMessages].sort(compareMessages);
320
+ }
321
+ function isRunnableStatus(status) {
322
+ return status !== "failed" && status !== "idle";
323
+ }
324
+ function hasRunnableWork(conversation) {
325
+ return isRunnableStatus(conversation.execution.status) || pendingMessages(conversation).length > 0;
326
+ }
327
+ function executionWithPendingMessages(execution, pending) {
328
+ const pendingMessages = [...pending].sort(compareMessages);
329
+ const status = execution.status === "idle" && execution.lease ? "running" : execution.status === "idle" && pendingMessages.length > 0 ? "pending" : execution.status;
330
+ return {
331
+ ...execution,
332
+ status,
333
+ inboundMessageIds: uniqueStrings([...execution.inboundMessageIds, ...pendingMessages.map((message) => message.inboundMessageId)]),
334
+ pendingMessages,
335
+ pendingCount: pendingMessages.length
336
+ };
337
+ }
338
+ function withExecutionUpdate(conversation, execution, nowMs) {
339
+ return {
340
+ ...conversation,
341
+ updatedAtMs: nowMs,
342
+ execution: {
343
+ ...executionWithPendingMessages(execution, execution.pendingMessages),
344
+ updatedAtMs: nowMs
345
+ }
346
+ };
347
+ }
348
+ async function getConnectedState(stateAdapter) {
349
+ const state = stateAdapter ?? getStateAdapter();
350
+ await state.connect();
351
+ return state;
352
+ }
353
+ async function sleep(ms) {
354
+ await new Promise((resolve) => {
355
+ setTimeout(resolve, ms).unref?.();
356
+ });
357
+ }
358
+ async function withIndexLock(state, indexKey, callback) {
359
+ const startedAtMs = now$1();
360
+ let lock;
361
+ while (true) {
362
+ lock = await state.acquireLock(indexLockKey(indexKey), CONVERSATION_INDEX_LOCK_TTL_MS);
363
+ if (lock) break;
364
+ if (now$1() - startedAtMs >= CONVERSATION_INDEX_LOCK_WAIT_MS) throw new Error(`Could not acquire conversation index lock for ${indexKey}`);
365
+ await sleep(CONVERSATION_INDEX_LOCK_RETRY_MS);
366
+ }
367
+ try {
368
+ return await callback();
369
+ } finally {
370
+ await state.releaseLock(lock);
371
+ }
372
+ }
373
+ function normalizeIndexEntry(value) {
374
+ if (!isRecord$1(value)) return;
375
+ const conversationId = toOptionalString(value.conversationId);
376
+ const score = toOptionalNumber(value.score);
377
+ if (!conversationId || typeof score !== "number") return;
378
+ return {
379
+ conversationId,
380
+ score
381
+ };
382
+ }
383
+ function uniqueIndexEntries(value) {
384
+ if (!Array.isArray(value)) return [];
385
+ const entries = /* @__PURE__ */ new Map();
386
+ for (const item of value) {
387
+ const entry = normalizeIndexEntry(item);
388
+ if (!entry) continue;
389
+ const existing = entries.get(entry.conversationId);
390
+ if (!existing || entry.score > existing.score) entries.set(entry.conversationId, entry);
391
+ }
392
+ return [...entries.values()];
393
+ }
394
+ function retainedIndexEntries(indexKey, entries) {
395
+ if (indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) return entries.sort(compareIndexDescending).slice(0, CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH);
396
+ if (indexKey === CONVERSATION_ACTIVE_INDEX_KEY) return entries.sort(compareIndexAscending);
397
+ throw new Error(`Unknown conversation index ${indexKey}`);
398
+ }
399
+ function redisIndexKey(indexKey) {
400
+ const prefix = getChatConfig().state.keyPrefix;
401
+ return [...prefix ? [prefix] : [], indexKey].join(":");
402
+ }
403
+ function parseRedisIndexEntries(values) {
404
+ if (!Array.isArray(values)) return [];
405
+ const entries = [];
406
+ for (let index = 0; index < values.length; index += 2) {
407
+ const conversationId = toOptionalString(values[index]);
408
+ const score = typeof values[index + 1] === "number" ? values[index + 1] : Number(values[index + 1]);
409
+ if (!conversationId || !Number.isFinite(score)) continue;
410
+ entries.push({
411
+ conversationId,
412
+ score
413
+ });
414
+ }
415
+ return entries;
416
+ }
417
+ function redisConversationIndexStore(client) {
418
+ const upsertBoundedActivityScript = `
419
+ redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
420
+ redis.call("PEXPIRE", KEYS[1], ARGV[3])
421
+ local extra = redis.call("ZCARD", KEYS[1]) - tonumber(ARGV[4])
422
+ if extra > 0 then
423
+ redis.call("ZREMRANGEBYRANK", KEYS[1], 0, extra - 1)
424
+ end
425
+ return 1
426
+ `;
427
+ return {
428
+ async list(args) {
429
+ const key = redisIndexKey(args.indexKey);
430
+ const limit = args.limit;
431
+ const offset = Math.max(0, args.offset ?? 0);
432
+ if (limit === 0) return [];
433
+ return parseRedisIndexEntries(args.scoreMax !== void 0 ? await client.sendCommand([
434
+ "ZRANGEBYSCORE",
435
+ key,
436
+ "-inf",
437
+ String(args.scoreMax),
438
+ "WITHSCORES",
439
+ ...limit !== void 0 || offset > 0 ? [
440
+ "LIMIT",
441
+ String(offset),
442
+ String(limit ?? 1e9)
443
+ ] : []
444
+ ]) : await client.sendCommand([
445
+ args.order === "asc" ? "ZRANGE" : "ZREVRANGE",
446
+ key,
447
+ String(offset),
448
+ String(limit === void 0 ? -1 : offset + Math.max(0, limit - 1)),
449
+ "WITHSCORES"
450
+ ]));
451
+ },
452
+ async remove(args) {
453
+ await client.sendCommand([
454
+ "ZREM",
455
+ redisIndexKey(args.indexKey),
456
+ args.conversationId
457
+ ]);
458
+ },
459
+ async upsert(args) {
460
+ const key = redisIndexKey(args.indexKey);
461
+ if (args.indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
462
+ await client.sendCommand([
463
+ "EVAL",
464
+ upsertBoundedActivityScript,
465
+ "1",
466
+ key,
467
+ String(args.score),
468
+ args.conversationId,
469
+ String(JUNIOR_THREAD_STATE_TTL_MS),
470
+ String(CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH)
471
+ ]);
472
+ return;
473
+ }
474
+ if (args.indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
475
+ await client.sendCommand([
476
+ "ZADD",
477
+ key,
478
+ String(args.score),
479
+ args.conversationId
480
+ ]);
481
+ await client.sendCommand([
482
+ "PEXPIRE",
483
+ key,
484
+ String(JUNIOR_THREAD_STATE_TTL_MS)
485
+ ]);
486
+ return;
487
+ }
488
+ throw new Error(`Unknown conversation index ${args.indexKey}`);
489
+ }
490
+ };
491
+ }
492
+ function emulatedConversationIndexStore(state) {
493
+ const readIndex = async (indexKey) => uniqueIndexEntries(await state.get(indexKey));
494
+ const writeIndex = async (indexKey, entries) => {
495
+ await state.set(indexKey, entries, JUNIOR_THREAD_STATE_TTL_MS);
496
+ };
497
+ return {
498
+ async list(args) {
499
+ const entries = (await readIndex(args.indexKey)).filter((entry) => args.scoreMax === void 0 ? true : entry.score <= args.scoreMax).sort(args.order === "asc" ? compareIndexAscending : compareIndexDescending);
500
+ const offset = Math.max(0, args.offset ?? 0);
501
+ return entries.slice(offset, args.limit === void 0 ? entries.length : offset + args.limit);
502
+ },
503
+ async remove(args) {
504
+ await withIndexLock(state, args.indexKey, async () => {
505
+ const entries = await readIndex(args.indexKey);
506
+ const next = entries.filter((entry) => entry.conversationId !== args.conversationId);
507
+ if (next.length === entries.length) return;
508
+ await writeIndex(args.indexKey, next);
509
+ });
510
+ },
511
+ async upsert(args) {
512
+ await withIndexLock(state, args.indexKey, async () => {
513
+ const withoutCurrent = (await readIndex(args.indexKey)).filter((entry) => entry.conversationId !== args.conversationId);
514
+ const next = retainedIndexEntries(args.indexKey, [...withoutCurrent, {
515
+ conversationId: args.conversationId,
516
+ score: args.score
517
+ }]);
518
+ await writeIndex(args.indexKey, next);
519
+ });
520
+ }
521
+ };
522
+ }
523
+ async function getConversationIndexStore(state) {
524
+ const redisStateAdapter = await getDefaultRedisStateAdapterFor(state);
525
+ if (redisStateAdapter) return redisConversationIndexStore(redisStateAdapter.getClient());
526
+ return emulatedConversationIndexStore(state);
527
+ }
528
+ async function upsertIndexEntry(args) {
529
+ await (await getConversationIndexStore(args.state)).upsert({
530
+ conversationId: args.conversationId,
531
+ indexKey: args.indexKey,
532
+ score: args.score
533
+ });
534
+ }
535
+ async function removeIndexEntry(args) {
536
+ await (await getConversationIndexStore(args.state)).remove({
537
+ conversationId: args.conversationId,
538
+ indexKey: args.indexKey
539
+ });
540
+ }
541
+ async function acquireMutationLock(state, conversationId) {
542
+ const startedAtMs = now$1();
543
+ while (true) {
544
+ const lock = await state.acquireLock(mutationLockKey(conversationId), CONVERSATION_MUTATION_LOCK_TTL_MS);
545
+ if (lock) return lock;
546
+ if (now$1() - startedAtMs >= CONVERSATION_MUTATION_WAIT_MS) throw new Error(`Could not acquire conversation mutation lock for ${conversationId}`);
547
+ await sleep(CONVERSATION_MUTATION_RETRY_MS);
548
+ }
549
+ }
550
+ async function withConversationMutation(args, callback) {
551
+ const state = await getConnectedState(args.state);
552
+ const lock = await acquireMutationLock(state, args.conversationId);
553
+ try {
554
+ return await callback(state, lock);
555
+ } finally {
556
+ await state.releaseLock(lock);
557
+ }
558
+ }
559
+ async function readConversation(state, conversationId) {
560
+ const raw = await state.get(conversationKey(conversationId));
561
+ if (raw == null) return;
562
+ const conversation = normalizeConversation(conversationId, raw);
563
+ if (!conversation) throw new InvalidConversationRecordError(conversationId);
564
+ return conversation;
565
+ }
566
+ /**
567
+ * Persist a conversation and refresh its reporting and active-recovery indexes.
568
+ *
569
+ * Conversation writes are lock-serialized blind whole-record writes, so the
570
+ * mutation lock is re-validated (and extended) immediately before the write:
571
+ * a stalled mutator that lost its lock must fail loudly instead of clobbering
572
+ * a newer record with a stale copy.
573
+ */
574
+ async function writeConversation(state, lock, conversation) {
575
+ const execution = executionWithPendingMessages(conversation.execution, conversation.execution.pendingMessages);
576
+ const next = {
577
+ ...conversation,
578
+ execution
579
+ };
580
+ if (!await state.extendLock(lock, CONVERSATION_MUTATION_LOCK_TTL_MS)) throw new ConversationMutationFencedError(next.conversationId);
581
+ const hasSlackDestination = next.destination?.platform === "slack";
582
+ const stored = {
583
+ ...next,
584
+ execution: {
585
+ ...next.execution,
586
+ pendingMessages: next.execution.pendingMessages.map((message) => storeMessage(message, hasSlackDestination && (message.source === "slack" || message.source === "resource_event" || message.source === "plugin" || message.source === "scheduler")))
587
+ }
588
+ };
589
+ await state.set(conversationKey(next.conversationId), stored, JUNIOR_THREAD_STATE_TTL_MS);
590
+ await upsertIndexEntry({
591
+ state,
592
+ indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
593
+ conversationId: next.conversationId,
594
+ score: next.lastActivityAtMs
595
+ });
596
+ if (!hasRunnableWork(next)) {
597
+ await removeIndexEntry({
598
+ state,
599
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
600
+ conversationId: next.conversationId
601
+ });
602
+ return;
603
+ }
604
+ await upsertIndexEntry({
605
+ state,
606
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
607
+ conversationId: next.conversationId,
608
+ score: next.execution.updatedAtMs ?? next.updatedAtMs
609
+ });
610
+ }
611
+ function assertSameOptionalConversationDestination(args) {
612
+ if (!args.next || !args.current) return;
613
+ if (sameDestination(args.current, args.next)) return;
614
+ throw new Error(`Conversation destination changed for ${args.conversationId}`);
615
+ }
616
+ function conversationWorkState(conversation) {
617
+ const lease = conversation.execution.lease;
618
+ return {
619
+ ...conversation,
620
+ lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
621
+ ...lease ? { lease: {
622
+ acquiredAtMs: lease.acquiredAtMs,
623
+ lastCheckInAtMs: lease.lastCheckInAtMs,
624
+ leaseExpiresAtMs: lease.expiresAtMs,
625
+ leaseToken: lease.token
626
+ } } : void 0,
627
+ messages: pendingMessages(conversation),
628
+ needsRun: hasRunnableWork(conversation)
629
+ };
630
+ }
631
+ /** Return a persisted conversation record, if one exists. */
632
+ async function getConversation$1(args) {
633
+ return await readConversation(await getConnectedState(args.state), args.conversationId);
634
+ }
635
+ /** Return a persisted conversation work record, if one exists. */
636
+ async function getConversationWorkState$1(args) {
637
+ const conversation = await getConversation$1(args);
638
+ return conversation ? conversationWorkState(conversation) : void 0;
639
+ }
640
+ /** Return whether a conversation has pending or resumable execution work. */
641
+ function hasRunnableConversationWork$1(conversation) {
642
+ return hasRunnableWork(conversation);
643
+ }
644
+ async function appendInboundMessageWithAdmission(args) {
645
+ const nowMs = args.nowMs ?? now$1();
646
+ return await withConversationMutation({
647
+ conversationId: args.message.conversationId,
648
+ state: args.state
649
+ }, async (state, lock) => {
650
+ const existing = await readConversation(state, args.message.conversationId);
651
+ const current = existing ?? emptyConversation({
652
+ conversationId: args.message.conversationId,
653
+ destination: args.message.destination,
654
+ nowMs,
655
+ source: args.message.source
656
+ });
657
+ if (existing) assertSameOptionalConversationDestination({
658
+ conversationId: args.message.conversationId,
659
+ current: current.destination,
660
+ next: args.message.destination
661
+ });
662
+ const existingPending = current.execution.pendingMessages.some((message) => message.inboundMessageId === args.message.inboundMessageId);
663
+ if (current.execution.inboundMessageIds.includes(args.message.inboundMessageId)) {
664
+ if (!existingPending) return { status: "duplicate" };
665
+ const nextStatus = current.execution.status === "idle" ? "pending" : current.execution.status;
666
+ await writeConversation(state, lock, withExecutionUpdate(current, {
667
+ ...current.execution,
668
+ status: nextStatus,
669
+ pendingMessages: current.execution.pendingMessages.map((message) => message.inboundMessageId === args.message.inboundMessageId ? upgradedPendingMessage(message, args.message) : message)
670
+ }, nowMs));
671
+ return { status: "duplicate" };
672
+ }
673
+ if (args.exclusive && hasRunnableWork(current)) return { status: "active" };
674
+ let status;
675
+ if (current.execution.lease) status = current.execution.status === "running" ? "running" : "paused";
676
+ else if (current.execution.status === "paused" && args.message.delivery === "defer") status = "paused";
677
+ else status = "pending";
678
+ await writeConversation(state, lock, withExecutionUpdate({
679
+ ...current,
680
+ destination: current.destination ?? args.message.destination,
681
+ source: current.source ?? args.message.source,
682
+ lastActivityAtMs: nowMs
683
+ }, {
684
+ ...current.execution,
685
+ status,
686
+ inboundMessageIds: [...current.execution.inboundMessageIds, args.message.inboundMessageId],
687
+ pendingMessages: [...current.execution.pendingMessages, args.message].sort(compareMessages)
688
+ }, nowMs));
689
+ return { status: "appended" };
690
+ });
691
+ }
692
+ /** Persist one inbound message idempotently in its conversation mailbox. */
693
+ async function appendInboundMessage(args) {
694
+ const result = await appendInboundMessageWithAdmission({
695
+ ...args,
696
+ exclusive: false
697
+ });
698
+ if (result.status === "active") throw new Error("Non-exclusive mailbox append returned active");
699
+ return result;
700
+ }
701
+ /** Append only when the Conversation has no other runnable work. */
702
+ async function appendExclusiveInboundMessage(args) {
703
+ return await appendInboundMessageWithAdmission({
704
+ ...args,
705
+ exclusive: true
706
+ });
707
+ }
708
+ /** Mark a conversation runnable when there is no new mailbox message. */
709
+ async function requestConversationWork$1(args) {
710
+ const nowMs = args.nowMs ?? now$1();
711
+ return await withConversationMutation(args, async (state, lock) => {
712
+ const existing = await readConversation(state, args.conversationId);
713
+ if (existing) assertSameOptionalConversationDestination({
714
+ conversationId: args.conversationId,
715
+ current: existing.destination,
716
+ next: args.destination
717
+ });
718
+ const current = existing ?? emptyConversation({
719
+ conversationId: args.conversationId,
720
+ destination: args.destination,
721
+ nowMs
722
+ });
723
+ const status = current.execution.lease ? "paused" : "pending";
724
+ await writeConversation(state, lock, withExecutionUpdate({
725
+ ...current,
726
+ destination: current.destination ?? args.destination
727
+ }, {
728
+ ...current.execution,
729
+ status
730
+ }, nowMs));
731
+ return { status: existing === void 0 ? "created" : "updated" };
732
+ });
733
+ }
734
+ /** Record that a wake-up nudge was accepted for the conversation. */
735
+ async function markConversationWorkEnqueued$1(args) {
736
+ const nowMs = args.nowMs ?? now$1();
737
+ await withConversationMutation(args, async (state, lock) => {
738
+ const current = await readConversation(state, args.conversationId);
739
+ if (!current) return;
740
+ await writeConversation(state, lock, withExecutionUpdate(current, {
741
+ ...current.execution,
742
+ lastEnqueuedAtMs: nowMs
743
+ }, nowMs));
744
+ });
745
+ }
746
+ /** Clear a wake marker after its queue delivery finds no runnable work. */
747
+ async function clearConsumedConversationWake$1(args) {
748
+ const nowMs = args.nowMs ?? now$1();
749
+ return await withConversationMutation(args, async (state, lock) => {
750
+ const current = await readConversation(state, args.conversationId);
751
+ if (!current || hasRunnableWork(current) || current.execution.lastEnqueuedAtMs === void 0) return false;
752
+ await writeConversation(state, lock, withExecutionUpdate(current, {
753
+ ...current.execution,
754
+ lastEnqueuedAtMs: void 0
755
+ }, nowMs));
756
+ return true;
757
+ });
758
+ }
759
+ /** Try to acquire the durable execution lease for one conversation. */
760
+ async function startConversationWork$1(args) {
761
+ const nowMs = args.nowMs ?? now$1();
762
+ return await withConversationMutation(args, async (state, lock) => {
763
+ const current = await readConversation(state, args.conversationId);
764
+ if (!current) return { status: "no_work" };
765
+ if (isLeaseActive(current.execution.lease, nowMs)) return {
766
+ status: "active",
767
+ leaseExpiresAtMs: current.execution.lease.expiresAtMs
768
+ };
769
+ if (!hasRunnableWork(current)) return { status: "no_work" };
770
+ const lease = {
771
+ token: randomUUID(),
772
+ acquiredAtMs: nowMs,
773
+ lastCheckInAtMs: nowMs,
774
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
775
+ };
776
+ const startsNewRun = current.execution.runId === void 0;
777
+ await writeConversation(state, lock, withExecutionUpdate(current, {
778
+ ...current.execution,
779
+ lease,
780
+ status: current.execution.status === "paused" ? "paused" : "running",
781
+ runId: current.execution.runId ?? randomUUID(),
782
+ lastEnqueuedAtMs: void 0,
783
+ retryCount: startsNewRun ? 0 : current.execution.retryCount
784
+ }, nowMs));
785
+ return {
786
+ status: "acquired",
787
+ leaseToken: lease.token,
788
+ leaseExpiresAtMs: lease.expiresAtMs
789
+ };
790
+ });
791
+ }
792
+ /** Extend the durable execution lease when the worker checks in. */
793
+ async function checkInConversationWork$1(args) {
794
+ const nowMs = args.nowMs ?? now$1();
795
+ return await withConversationMutation(args, async (state, lock) => {
796
+ const current = await readConversation(state, args.conversationId);
797
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
798
+ await writeConversation(state, lock, withExecutionUpdate(current, {
799
+ ...current.execution,
800
+ lease: {
801
+ ...current.execution.lease,
802
+ lastCheckInAtMs: nowMs,
803
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
804
+ }
805
+ }, nowMs));
806
+ return true;
807
+ });
808
+ }
809
+ /**
810
+ * Resolve pending mailbox entries after the caller accepts responsibility.
811
+ *
812
+ * Returning ids acknowledges only that subset; returning nothing acknowledges
813
+ * every pending entry passed to the handler.
814
+ */
815
+ async function drainConversationMailbox$1(args) {
816
+ const nowMs = args.nowMs ?? now$1();
817
+ const pending = await withConversationMutation(args, async (state) => {
818
+ const current = await readConversation(state, args.conversationId);
819
+ if (!current || current.execution.lease?.token !== args.leaseToken) throw new Error(`Conversation lease is not held for ${args.conversationId}`);
820
+ return pendingMessages(current);
821
+ });
822
+ if (pending.length === 0) return {
823
+ changed: false,
824
+ messages: []
825
+ };
826
+ const result = await args.handle(pending);
827
+ const pendingIds = new Set(pending.map((message) => message.inboundMessageId));
828
+ const acknowledgedIds = new Set(result ?? pending.map((message) => message.inboundMessageId));
829
+ for (const inboundMessageId of acknowledgedIds) if (!pendingIds.has(inboundMessageId)) throw new Error(`Conversation mailbox drain result is not pending for ${args.conversationId}`);
830
+ if (acknowledgedIds.size === 0) return {
831
+ changed: false,
832
+ messages: []
833
+ };
834
+ await withConversationMutation(args, async (state, lock) => {
835
+ const current = await readConversation(state, args.conversationId);
836
+ if (!current || current.execution.lease?.token !== args.leaseToken) throw new Error(`Conversation lease is not held for ${args.conversationId}`);
837
+ const pendingMessages = current.execution.pendingMessages.filter((message) => !acknowledgedIds.has(message.inboundMessageId));
838
+ await writeConversation(state, lock, withExecutionUpdate(current, {
839
+ ...current.execution,
840
+ status: current.execution.status === "pending" && pendingMessages.length === 0 ? "running" : current.execution.status,
841
+ lastProgressAtMs: nowMs,
842
+ retryCount: 0,
843
+ pendingMessages
844
+ }, nowMs));
845
+ });
846
+ return {
847
+ changed: true,
848
+ messages: pending.filter((message) => acknowledgedIds.has(message.inboundMessageId))
849
+ };
850
+ }
851
+ /** Acknowledge leased mailbox entries after the handler accepts responsibility. */
852
+ async function ackMessages$1(args) {
853
+ const nowMs = args.nowMs ?? now$1();
854
+ const inboundMessageIds = new Set(args.inboundMessageIds);
855
+ return await withConversationMutation(args, async (state, lock) => {
856
+ const current = await readConversation(state, args.conversationId);
857
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
858
+ if (inboundMessageIds.size === 0) return true;
859
+ const pendingMessages = current.execution.pendingMessages.filter((message) => !inboundMessageIds.has(message.inboundMessageId));
860
+ if (pendingMessages.length === current.execution.pendingMessages.length) return true;
861
+ await writeConversation(state, lock, withExecutionUpdate(current, {
862
+ ...current.execution,
863
+ lastProgressAtMs: nowMs,
864
+ retryCount: 0,
865
+ pendingMessages
866
+ }, nowMs));
867
+ return true;
868
+ });
869
+ }
870
+ function isHumanFacingMessage(message) {
871
+ return message.source === "web" || message.source === "slack";
872
+ }
873
+ /** Persist a stop request for the current run without process affinity. */
874
+ async function stopConversationWork$1(args) {
875
+ const nowMs = args.nowMs ?? now$1();
876
+ return await withConversationMutation(args, async (state, lock) => {
877
+ const current = await readConversation(state, args.conversationId);
878
+ if (!current || !hasRunnableWork(current)) return { status: "no_work" };
879
+ const runId = current.execution.runId ?? randomUUID();
880
+ const inboundMessageIds = current.execution.pendingMessages.filter(isHumanFacingMessage).map((message) => message.inboundMessageId);
881
+ await writeConversation(state, lock, withExecutionUpdate(current, {
882
+ ...current.execution,
883
+ runId,
884
+ stop: {
885
+ inboundMessageIds,
886
+ runId
887
+ }
888
+ }, nowMs));
889
+ return {
890
+ runId,
891
+ status: "requested"
892
+ };
893
+ });
894
+ }
895
+ /** Clear one observed stop request and discard older human-facing mailbox work. */
896
+ async function completeConversationStop$1(args) {
897
+ const nowMs = args.nowMs ?? now$1();
898
+ return await withConversationMutation(args, async (state, lock) => {
899
+ const current = await readConversation(state, args.conversationId);
900
+ if (!current || current.execution.lease?.token !== args.leaseToken) return {
901
+ status: "lost_lease",
902
+ removedInboundMessageIds: []
903
+ };
904
+ const stop = current.execution.stop;
905
+ if (!stop || stop.runId !== args.runId) return {
906
+ status: "none",
907
+ removedInboundMessageIds: []
908
+ };
909
+ const stoppedInboundMessageIds = new Set(stop.inboundMessageIds);
910
+ const removedInboundMessageIds = [];
911
+ const pendingMessages = current.execution.pendingMessages.filter((message) => {
912
+ const shouldStop = stoppedInboundMessageIds.has(message.inboundMessageId);
913
+ if (shouldStop) removedInboundMessageIds.push(message.inboundMessageId);
914
+ return !shouldStop;
915
+ });
916
+ await writeConversation(state, lock, withExecutionUpdate(current, {
917
+ ...current.execution,
918
+ lastEnqueuedAtMs: pendingMessages.length === 0 ? void 0 : current.execution.lastEnqueuedAtMs,
919
+ pendingMessages,
920
+ stop: void 0
921
+ }, nowMs));
922
+ return {
923
+ status: "cleared",
924
+ removedInboundMessageIds
925
+ };
926
+ });
927
+ }
928
+ /** Cancel human-facing pending mailbox rows without requiring a worker lease. */
929
+ async function cancelHumanFacingPendingMessages$1(args) {
930
+ const nowMs = args.nowMs ?? now$1();
931
+ const requestedIds = args.inboundMessageIds === void 0 ? void 0 : new Set(args.inboundMessageIds);
932
+ return await withConversationMutation(args, async (state, lock) => {
933
+ const current = await readConversation(state, args.conversationId);
934
+ if (!current) return { cancelledInboundMessageIds: [] };
935
+ const cancelledInboundMessageIds = [];
936
+ const pendingMessages = [];
937
+ for (const message of current.execution.pendingMessages) {
938
+ const isRequested = requestedIds === void 0 || requestedIds.has(message.inboundMessageId);
939
+ const isInSnapshot = args.receivedBeforeMs === void 0 || message.receivedAtMs <= args.receivedBeforeMs;
940
+ if (isHumanFacingMessage(message) && isRequested && isInSnapshot) {
941
+ cancelledInboundMessageIds.push(message.inboundMessageId);
942
+ continue;
943
+ }
944
+ pendingMessages.push(message);
945
+ }
946
+ if (cancelledInboundMessageIds.length === 0) return { cancelledInboundMessageIds };
947
+ const becomesIdle = current.execution.status === "pending" && pendingMessages.length === 0;
948
+ await writeConversation(state, lock, withExecutionUpdate(current, {
949
+ ...current.execution,
950
+ lastEnqueuedAtMs: pendingMessages.length === 0 ? void 0 : current.execution.lastEnqueuedAtMs,
951
+ pendingMessages,
952
+ retryCount: becomesIdle ? 0 : current.execution.retryCount,
953
+ runId: becomesIdle ? void 0 : current.execution.runId,
954
+ status: becomesIdle ? "idle" : current.execution.status,
955
+ stop: becomesIdle ? void 0 : current.execution.stop
956
+ }, nowMs));
957
+ return { cancelledInboundMessageIds };
958
+ });
959
+ }
960
+ /** Mark the leased conversation as needing another queue-delivered slice. */
961
+ async function requestAnotherSlice$1(args) {
962
+ const nowMs = args.nowMs ?? now$1();
963
+ return await withConversationMutation(args, async (state, lock) => {
964
+ const current = await readConversation(state, args.conversationId);
965
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
966
+ assertSameOptionalConversationDestination({
967
+ conversationId: args.conversationId,
968
+ current: current.destination,
969
+ next: args.destination
970
+ });
971
+ await writeConversation(state, lock, withExecutionUpdate(current, {
972
+ ...current.execution,
973
+ status: "paused"
974
+ }, nowMs));
975
+ return true;
976
+ });
977
+ }
978
+ /** Begin a requested turn resume under the worker's existing lease. */
979
+ async function beginConversationResume$1(args) {
980
+ const nowMs = args.nowMs ?? now$1();
981
+ return await withConversationMutation(args, async (state, lock) => {
982
+ const current = await readConversation(state, args.conversationId);
983
+ if (!current || current.execution.lease?.token !== args.leaseToken || current.execution.status !== "paused") return false;
984
+ await writeConversation(state, lock, withExecutionUpdate(current, {
985
+ ...current.execution,
986
+ status: "running"
987
+ }, nowMs));
988
+ return true;
989
+ });
990
+ }
991
+ /** Release the durable execution lease without changing completion state. */
992
+ async function releaseConversationWork$1(args) {
993
+ const nowMs = args.nowMs ?? now$1();
994
+ return await withConversationMutation(args, async (state, lock) => {
995
+ const current = await readConversation(state, args.conversationId);
996
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
997
+ await writeConversation(state, lock, withExecutionUpdate(current, {
998
+ ...current.execution,
999
+ lease: void 0,
1000
+ status: current.execution.status === "running" ? "pending" : current.execution.status
1001
+ }, nowMs));
1002
+ return true;
1003
+ });
1004
+ }
1005
+ /** Count one failed execution attempt and stop the conversation when retries are exhausted. */
1006
+ async function recordConversationRetry$1(args) {
1007
+ const nowMs = args.nowMs ?? now$1();
1008
+ return await withConversationMutation(args, async (state, lock) => {
1009
+ const current = await readConversation(state, args.conversationId);
1010
+ if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
1011
+ const count = (current.execution.retryCount ?? 0) + 1;
1012
+ const stopped = count >= 5;
1013
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1014
+ ...current.execution,
1015
+ lastEnqueuedAtMs: void 0,
1016
+ lease: stopped ? void 0 : current.execution.lease,
1017
+ retryCount: count,
1018
+ pendingMessages: stopped ? [] : current.execution.pendingMessages,
1019
+ runId: stopped ? void 0 : current.execution.runId,
1020
+ status: stopped ? "failed" : "paused",
1021
+ stop: stopped ? void 0 : current.execution.stop
1022
+ }, nowMs));
1023
+ return stopped ? "stopped" : "recorded";
1024
+ });
1025
+ }
1026
+ /** Finish a leased conversation and report whether runnable work remains. */
1027
+ async function completeConversationWork$1(args) {
1028
+ const nowMs = args.nowMs ?? now$1();
1029
+ return await withConversationMutation(args, async (state, lock) => {
1030
+ const current = await readConversation(state, args.conversationId);
1031
+ if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
1032
+ const hasPending = pendingMessages(current).length > 0;
1033
+ const needsRun = current.execution.status === "paused" || args.resumeIfStopped === true && current.execution.stop !== void 0;
1034
+ const runnable = needsRun || hasPending;
1035
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1036
+ ...current.execution,
1037
+ lease: void 0,
1038
+ status: needsRun ? "paused" : hasPending ? "pending" : "idle",
1039
+ lastProgressAtMs: args.madeProgress === false ? current.execution.lastProgressAtMs : nowMs,
1040
+ retryCount: runnable || args.madeProgress === false ? current.execution.retryCount : 0,
1041
+ runId: runnable ? current.execution.runId : void 0,
1042
+ stop: runnable ? current.execution.stop : void 0
1043
+ }, nowMs));
1044
+ return runnable ? "pending" : "completed";
1045
+ });
1046
+ }
1047
+ /**
1048
+ * Record one failed execution attempt for pending messages, dead-lettering
1049
+ * the conversation when its retry budget or the legacy message delivery limit
1050
+ * is exhausted.
1051
+ *
1052
+ * Retries are counted only when the run made no durable progress: if any
1053
+ * attempted message left the mailbox, the remaining pending entries may be
1054
+ * deliberate deferrals and are left untouched. Consumed message ids stay in
1055
+ * `inboundMessageIds` so source retries remain duplicates.
1056
+ */
1057
+ async function recordAttemptFailure$1(args) {
1058
+ const nowMs = args.nowMs ?? now$1();
1059
+ return await withConversationMutation(args, async (state, lock) => {
1060
+ const current = await readConversation(state, args.conversationId);
1061
+ if (!current || current.execution.lease?.token !== args.leaseToken) return {
1062
+ status: "lost_lease",
1063
+ pendingCount: 0,
1064
+ deadLetteredMessages: [],
1065
+ retryCount: 0
1066
+ };
1067
+ const pendingIds = new Set(current.execution.pendingMessages.map((message) => message.inboundMessageId));
1068
+ if (args.inboundMessageIds.length === 0 || args.inboundMessageIds.some((id) => !pendingIds.has(id))) return {
1069
+ status: "skipped",
1070
+ pendingCount: current.execution.pendingMessages.length,
1071
+ deadLetteredMessages: [],
1072
+ retryCount: current.execution.retryCount ?? 0
1073
+ };
1074
+ const retryCount = (current.execution.retryCount ?? 0) + 1;
1075
+ const retriesExhausted = retryCount >= 5;
1076
+ const attemptedIds = new Set(args.inboundMessageIds);
1077
+ const deadLetteredMessages = [];
1078
+ const pendingMessages = [];
1079
+ for (const message of current.execution.pendingMessages) {
1080
+ if (!attemptedIds.has(message.inboundMessageId)) {
1081
+ if (retriesExhausted) deadLetteredMessages.push(message);
1082
+ else pendingMessages.push(message);
1083
+ continue;
1084
+ }
1085
+ const attempted = {
1086
+ ...message,
1087
+ attemptCount: (message.attemptCount ?? 0) + 1
1088
+ };
1089
+ if (retriesExhausted || attempted.attemptCount >= 5) {
1090
+ deadLetteredMessages.push(attempted);
1091
+ continue;
1092
+ }
1093
+ pendingMessages.push(attempted);
1094
+ }
1095
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1096
+ ...current.execution,
1097
+ retryCount,
1098
+ pendingMessages
1099
+ }, nowMs));
1100
+ return {
1101
+ status: "recorded",
1102
+ pendingCount: pendingMessages.length,
1103
+ deadLetteredMessages,
1104
+ retryCount
1105
+ };
1106
+ });
1107
+ }
1108
+ /** Record a terminal failure completion for a leased conversation. */
1109
+ async function deadLetterAttempt$1(args) {
1110
+ const nowMs = args.nowMs ?? now$1();
1111
+ return await withConversationMutation(args, async (state, lock) => {
1112
+ const current = await readConversation(state, args.conversationId);
1113
+ if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
1114
+ const runnable = pendingMessages(current).length > 0;
1115
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1116
+ ...current.execution,
1117
+ lease: void 0,
1118
+ status: runnable ? "pending" : "failed",
1119
+ runId: runnable ? current.execution.runId : void 0,
1120
+ stop: runnable ? current.execution.stop : void 0
1121
+ }, nowMs));
1122
+ return runnable ? "pending" : "failed";
1123
+ });
1124
+ }
1125
+ /** Recover an expired lease, consuming the retry budget under the mutation lock. */
1126
+ async function clearExpiredConversationLease$1(args) {
1127
+ const nowMs = args.nowMs ?? now$1();
1128
+ return await withConversationMutation(args, async (state, lock) => {
1129
+ const current = await readConversation(state, args.conversationId);
1130
+ if (!current?.execution.lease || current.execution.lease.expiresAtMs > nowMs) return "not_expired";
1131
+ const retryCount = (current.execution.retryCount ?? 0) + 1;
1132
+ const stopped = retryCount >= 5;
1133
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1134
+ ...current.execution,
1135
+ lastEnqueuedAtMs: void 0,
1136
+ lease: void 0,
1137
+ retryCount,
1138
+ pendingMessages: stopped ? [] : current.execution.pendingMessages,
1139
+ runId: stopped ? void 0 : current.execution.runId,
1140
+ status: stopped ? "failed" : "paused",
1141
+ stop: stopped ? void 0 : current.execution.stop
1142
+ }, nowMs));
1143
+ return stopped ? "stopped" : "requeued";
1144
+ });
1145
+ }
1146
+ /** Remove one conversation from the active index after it is missing or idle. */
1147
+ async function removeActiveConversation$1(args) {
1148
+ await removeIndexEntry({
1149
+ state: await getConnectedState(args.state),
1150
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1151
+ conversationId: args.conversationId
1152
+ });
1153
+ }
1154
+ /** List active conversation ids by oldest execution update first. */
1155
+ async function listActiveConversationIds$1(args = {}) {
1156
+ return (await (await getConversationIndexStore(await getConnectedState(args.state))).list({
1157
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1158
+ limit: args.limit,
1159
+ order: "asc",
1160
+ scoreMax: args.staleBeforeMs
1161
+ })).map((entry) => entry.conversationId);
1162
+ }
1163
+ //#endregion
1164
+ //#region src/chat/task-execution/store.ts
1165
+ function metadataStore(options) {
1166
+ return options.conversationStore ?? getConversationStore();
1167
+ }
1168
+ function duplicateInboundNudgeIdempotencyKey(message, nowMs) {
1169
+ return `duplicate:${message.conversationId}:${message.inboundMessageId}:${nowMs}`;
1170
+ }
1171
+ function hasRecentEnqueueMarker(conversation, nowMs) {
1172
+ const lastEnqueuedAtMs = conversation.execution.lastEnqueuedAtMs;
1173
+ return typeof lastEnqueuedAtMs === "number" && lastEnqueuedAtMs + 6e4 > nowMs;
1174
+ }
1175
+ function now() {
1176
+ return Date.now();
1177
+ }
1178
+ async function recordExecutionMetadata(args) {
1179
+ const conversation = await getConversation$1({
1180
+ conversationId: args.conversationId,
1181
+ state: args.state
1182
+ });
1183
+ if (!conversation) return;
1184
+ await metadataStore(args).recordExecution({
1185
+ channelName: conversation.channelName,
1186
+ conversationId: conversation.conversationId,
1187
+ createdAtMs: conversation.createdAtMs,
1188
+ destination: conversation.destination,
1189
+ execution: {
1190
+ lastCheckpointAtMs: conversation.execution.lastCheckpointAtMs,
1191
+ lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
1192
+ runId: conversation.execution.runId,
1193
+ status: conversation.execution.status,
1194
+ updatedAtMs: conversation.execution.updatedAtMs
1195
+ },
1196
+ lastActivityAtMs: conversation.lastActivityAtMs,
1197
+ metrics: null,
1198
+ actor: conversation.actor,
1199
+ source: conversation.source,
1200
+ title: conversation.title,
1201
+ updatedAtMs: conversation.updatedAtMs
1202
+ });
1203
+ }
1204
+ /** Return a persisted conversation record, if one exists. */
1205
+ async function getConversation(args) {
1206
+ return await getConversation$1(args);
1207
+ }
1208
+ /** Return a persisted conversation work record, if one exists. */
1209
+ async function getConversationWorkState(args) {
1210
+ return await getConversationWorkState$1(args);
1211
+ }
1212
+ /** Count one failed conversation execution and persist its terminal retry state. */
1213
+ async function recordConversationRetry(args) {
1214
+ const result = await recordConversationRetry$1(args);
1215
+ await recordExecutionMetadata(args);
1216
+ return result;
1217
+ }
1218
+ /** Count mailbox messages that have not yet reached the conversation event log. */
1219
+ function countPendingConversationMessages(conversation) {
1220
+ return conversation.execution.pendingMessages.length;
1221
+ }
1222
+ /** Return whether a conversation has pending or resumable execution work. */
1223
+ function hasRunnableConversationWork(conversation) {
1224
+ return conversation.execution.status !== "idle" || countPendingConversationMessages(conversation) > 0;
1225
+ }
1226
+ /**
1227
+ * Ensure runnable conversation work has one accepted queue wake-up nudge.
1228
+ *
1229
+ * Ordinary wakes coalesce on a recent accepted marker. Replacement is only for
1230
+ * consumed or known-stale deliveries where another queue nudge must exist.
1231
+ */
1232
+ async function ensureConversationWake(args) {
1233
+ const nowMs = args.nowMs ?? now();
1234
+ const conversation = await getConversation$1({
1235
+ conversationId: args.conversationId,
1236
+ state: args.state
1237
+ });
1238
+ if (!conversation || !hasRunnableConversationWork(conversation)) return { status: "no_work" };
1239
+ if (args.replaceExistingWake !== true && conversation.execution.lease && conversation.execution.lease.expiresAtMs > nowMs) return { status: "lease_active" };
1240
+ if (args.replaceExistingWake !== true && hasRecentEnqueueMarker(conversation, nowMs)) return { status: "already_enqueued" };
1241
+ const queueResult = await args.queue.send({
1242
+ schemaVersion: 2,
1243
+ conversationId: args.conversationId
1244
+ }, {
1245
+ delayMs: args.delayMs,
1246
+ idempotencyKey: args.idempotencyKey
1247
+ });
1248
+ await markConversationWorkEnqueued({
1249
+ conversationId: args.conversationId,
1250
+ conversationStore: args.conversationStore,
1251
+ nowMs,
1252
+ state: args.state
1253
+ });
1254
+ return {
1255
+ status: "enqueued",
1256
+ queueMessageId: queueResult?.messageId
1257
+ };
1258
+ }
1259
+ async function enqueueAfterAppend(args) {
1260
+ const nowMs = args.nowMs ?? now();
1261
+ const appendResult = args.appendResult;
1262
+ if (appendResult.status === "active") return appendResult;
1263
+ let idempotencyKey = args.message.inboundMessageId;
1264
+ if (appendResult.status === "duplicate") {
1265
+ const conversation = await getConversation$1({
1266
+ conversationId: args.message.conversationId,
1267
+ state: args.state
1268
+ });
1269
+ if (!conversation) return appendResult;
1270
+ if (hasRecentEnqueueMarker(conversation, nowMs)) {
1271
+ await recordExecutionMetadata({
1272
+ conversationId: args.message.conversationId,
1273
+ conversationStore: args.conversationStore,
1274
+ state: args.state
1275
+ });
1276
+ return appendResult;
1277
+ }
1278
+ if (!conversation.execution.pendingMessages.some((message) => message.inboundMessageId === args.message.inboundMessageId)) {
1279
+ await recordExecutionMetadata({
1280
+ conversationId: args.message.conversationId,
1281
+ conversationStore: args.conversationStore,
1282
+ state: args.state
1283
+ });
1284
+ return appendResult;
1285
+ }
1286
+ idempotencyKey = duplicateInboundNudgeIdempotencyKey(args.message, nowMs);
1287
+ }
1288
+ const wake = await ensureConversationWake({
1289
+ conversationId: args.message.conversationId,
1290
+ conversationStore: args.conversationStore,
1291
+ idempotencyKey,
1292
+ nowMs,
1293
+ queue: args.queue,
1294
+ state: args.state
1295
+ });
1296
+ if (wake.status !== "enqueued") await recordExecutionMetadata({
1297
+ conversationId: args.message.conversationId,
1298
+ conversationStore: args.conversationStore,
1299
+ state: args.state
1300
+ });
1301
+ return {
1302
+ ...appendResult,
1303
+ ...wake.status === "enqueued" ? { queueMessageId: wake.queueMessageId } : void 0
1304
+ };
1305
+ }
1306
+ /** Persist inbound work and ensure a worker wake-up. */
1307
+ async function appendAndEnqueueInboundMessage(args) {
1308
+ const nowMs = args.nowMs ?? now();
1309
+ const result = await enqueueAfterAppend({
1310
+ ...args,
1311
+ appendResult: await appendInboundMessage({
1312
+ message: args.message,
1313
+ nowMs,
1314
+ state: args.state
1315
+ }),
1316
+ nowMs
1317
+ });
1318
+ if (result.status === "active") throw new Error("Non-exclusive mailbox enqueue returned active");
1319
+ return result;
1320
+ }
1321
+ /** Persist exclusive inbound work and wake its worker when admitted. */
1322
+ async function appendAndEnqueueExclusiveInboundMessage(args) {
1323
+ const nowMs = args.nowMs ?? now();
1324
+ return await enqueueAfterAppend({
1325
+ ...args,
1326
+ appendResult: await appendExclusiveInboundMessage({
1327
+ message: args.message,
1328
+ nowMs,
1329
+ state: args.state
1330
+ }),
1331
+ nowMs
1332
+ });
1333
+ }
1334
+ /** Persist a stop request for the current Conversation run. */
1335
+ async function stopConversationWork(args) {
1336
+ const result = await stopConversationWork$1(args);
1337
+ if (result.status === "requested") await recordExecutionMetadata(args);
1338
+ return result;
1339
+ }
1340
+ /** Finish an observed stop request under the current worker lease. */
1341
+ async function completeConversationStop(args) {
1342
+ const result = await completeConversationStop$1(args);
1343
+ if (result.status === "cleared") await recordExecutionMetadata(args);
1344
+ return result;
1345
+ }
1346
+ /** Clear an accepted wake marker after its delivery finds no runnable work. */
1347
+ async function clearConsumedConversationWake(args) {
1348
+ const result = await clearConsumedConversationWake$1(args);
1349
+ if (result) await recordExecutionMetadata(args);
1350
+ return result;
1351
+ }
1352
+ /** Mark a conversation runnable when there is no new mailbox message. */
1353
+ async function requestConversationWork(args) {
1354
+ const result = await requestConversationWork$1(args);
1355
+ await recordExecutionMetadata({
1356
+ conversationId: args.conversationId,
1357
+ conversationStore: args.conversationStore,
1358
+ state: args.state
1359
+ });
1360
+ return result;
1361
+ }
1362
+ /** Record that a wake-up nudge was accepted for the conversation. */
1363
+ async function markConversationWorkEnqueued(args) {
1364
+ await markConversationWorkEnqueued$1(args);
1365
+ await recordExecutionMetadata(args);
1366
+ }
1367
+ /** Try to acquire the durable execution lease for one conversation. */
1368
+ async function startConversationWork(args) {
1369
+ const result = await startConversationWork$1(args);
1370
+ await recordExecutionMetadata(args);
1371
+ return result;
1372
+ }
1373
+ /** Extend the durable execution lease when the worker checks in. */
1374
+ async function checkInConversationWork(args) {
1375
+ const result = await checkInConversationWork$1(args);
1376
+ if (result) await recordExecutionMetadata(args);
1377
+ return result;
1378
+ }
1379
+ /** Drain pending mailbox entries after the caller accepts responsibility. */
1380
+ async function drainConversationMailbox(args) {
1381
+ const result = await drainConversationMailbox$1(args);
1382
+ if (result.changed) await recordExecutionMetadata(args);
1383
+ return result.messages;
1384
+ }
1385
+ /** Acknowledge leased mailbox entries after the handler accepts responsibility. */
1386
+ async function ackMessages(args) {
1387
+ const result = await ackMessages$1(args);
1388
+ await recordExecutionMetadata(args);
1389
+ return result;
1390
+ }
1391
+ /** Cancel human-facing pending mailbox rows without requiring a worker lease. */
1392
+ async function cancelHumanFacingPendingMessages(args) {
1393
+ const result = await cancelHumanFacingPendingMessages$1(args);
1394
+ if (result.cancelledInboundMessageIds.length > 0) await recordExecutionMetadata(args);
1395
+ return result;
1396
+ }
1397
+ /** Mark the leased conversation as needing another queue-delivered slice. */
1398
+ async function requestAnotherSlice(args) {
1399
+ const result = await requestAnotherSlice$1(args);
1400
+ await recordExecutionMetadata(args);
1401
+ return result;
1402
+ }
1403
+ /** Begin a requested turn resume without releasing the current lease. */
1404
+ async function beginConversationResume(args) {
1405
+ const result = await beginConversationResume$1(args);
1406
+ if (result) await recordExecutionMetadata(args);
1407
+ return result;
1408
+ }
1409
+ /** Release the durable execution lease without changing completion state. */
1410
+ async function releaseConversationWork(args) {
1411
+ const result = await releaseConversationWork$1(args);
1412
+ await recordExecutionMetadata(args);
1413
+ return result;
1414
+ }
1415
+ /** Finish a leased conversation and report whether runnable work remains. */
1416
+ async function completeConversationWork(args) {
1417
+ const result = await completeConversationWork$1(args);
1418
+ await recordExecutionMetadata(args);
1419
+ return result;
1420
+ }
1421
+ /** Record one failed delivery attempt and dead-letter messages at their limit. */
1422
+ async function recordAttemptFailure(args) {
1423
+ const result = await recordAttemptFailure$1(args);
1424
+ if (result.status === "recorded") await recordExecutionMetadata(args);
1425
+ return result;
1426
+ }
1427
+ /** Record a terminal failure completion for a leased conversation. */
1428
+ async function deadLetterAttempt(args) {
1429
+ const result = await deadLetterAttempt$1(args);
1430
+ await recordExecutionMetadata(args);
1431
+ return result;
1432
+ }
1433
+ /** Recover an expired lease and persist the resulting retry state. */
1434
+ async function clearExpiredConversationLease(args) {
1435
+ const result = await clearExpiredConversationLease$1(args);
1436
+ await recordExecutionMetadata(args);
1437
+ return result;
1438
+ }
1439
+ /** Remove one conversation from the active index after it is missing or idle. */
1440
+ async function removeActiveConversation(args) {
1441
+ return await removeActiveConversation$1(args);
1442
+ }
1443
+ /** List active conversation ids by oldest execution update first. */
1444
+ async function listActiveConversationIds(args = {}) {
1445
+ return await listActiveConversationIds$1(args);
1446
+ }
1447
+ //#endregion
1448
+ //#region src/chat/resource-events/catalog.ts
1449
+ /** Core-owned resource-event namespace. Not a plugin publisher. */
1450
+ const CORE_RESOURCE_EVENT_NAMESPACE = "junior";
1451
+ /** True when the catalog has any enabled registration. */
1452
+ function hasResourceEventCatalogEntries(catalog) {
1453
+ return Object.keys(catalog).length > 0;
1454
+ }
1455
+ /**
1456
+ * True when at least one plugin namespace is enabled.
1457
+ *
1458
+ * Core Workspace snapshot events stay in the catalog for search and temporary
1459
+ * watches. Durable event tasks still require a plugin publisher.
1460
+ */
1461
+ function hasPluginResourceEventCatalogEntries(catalog) {
1462
+ return Object.keys(pluginResourceEventCatalog(catalog)).length > 0;
1463
+ }
1464
+ /**
1465
+ * Catalog entries that durable event tasks may select.
1466
+ *
1467
+ * Core Workspace snapshot events only wake temporary watches. Snapshot finish
1468
+ * does not run event-task ingest, so those namespaces stay out of create/update
1469
+ * schemas and trigger availability.
1470
+ */
1471
+ function pluginResourceEventCatalog(catalog) {
1472
+ return Object.fromEntries(Object.entries(catalog).filter(([namespace]) => namespace !== CORE_RESOURCE_EVENT_NAMESPACE));
1473
+ }
1474
+ function enumSchema(values, unavailableMessage) {
1475
+ if (values.length === 0) return z.string().refine(() => false, unavailableMessage);
1476
+ return z.enum(values);
1477
+ }
1478
+ /** Build the model-facing namespace enum from enabled resource event plugins. */
1479
+ function eventNamespaceSchema(catalog) {
1480
+ return enumSchema(Object.keys(catalog).sort(), "No resource event namespaces are enabled.");
1481
+ }
1482
+ /** Build the model-facing event enum from enabled plugin registrations. */
1483
+ function registeredEventTypeSchema(catalog) {
1484
+ return enumSchema([...new Set(Object.values(catalog).flatMap((registration) => registration.resourceTypes.flatMap((resourceType) => resourceType.supportedEvents)))].sort(), "No resource event types are enabled.");
1485
+ }
1486
+ /** Build the model-facing resource type enum from enabled plugin registrations. */
1487
+ function registeredResourceTypeSchema(catalog) {
1488
+ return enumSchema([...new Set(Object.values(catalog).flatMap((registration) => registration.resourceTypes.map((resourceType) => resourceType.type)))].sort(), "No resource types are enabled.");
1489
+ }
1490
+ /** Return whether one enabled plugin declared an event type. */
1491
+ function pluginSupportsEvent(catalog, namespace, resourceType, eventType) {
1492
+ return catalog[namespace]?.resourceTypes.find((candidate) => candidate.type === resourceType)?.supportedEvents.includes(eventType) ?? false;
1493
+ }
1494
+ /** Return app guidance for one registered resource type and event type. */
1495
+ function resourceEventGuidance(catalog, namespace, resourceType, eventType) {
1496
+ return catalog[namespace]?.resourceTypes.find((candidate) => candidate.type === resourceType)?.guidance?.[eventType];
1497
+ }
1498
+ /** Return declared match keys for one registered resource type. */
1499
+ function resourceEventMatchFields(catalog, namespace, resourceType) {
1500
+ return catalog[namespace]?.resourceTypes.find((candidate) => candidate.type === resourceType)?.matchFields;
1501
+ }
1502
+ /** Build the match object the tools accept for one resource type. */
1503
+ function registeredResourceEventMatchSchema() {
1504
+ return resourceEventMatchSchema.optional().describe("Optional exact values from the resource type matchFields. A single value must equal the event data. A list means the event value may be any one entry. Omit a key to ignore it. Unmatched events are dropped with no agent turn.");
1505
+ }
1506
+ /** Reject match keys the resource type does not list. */
1507
+ function requireSupportedResourceEventMatch(catalog, input) {
1508
+ const match = input.match;
1509
+ if (!match || Object.keys(match).length === 0) return void 0;
1510
+ const fields = resourceEventMatchFields(catalog, input.namespace, input.resourceType);
1511
+ if (!fields || Object.keys(fields).length === 0) throw new ToolInputError(`Resource type "${input.namespace}:${input.resourceType}" does not support match.`);
1512
+ const normalized = {};
1513
+ for (const [key, value] of Object.entries(match)) {
1514
+ const field = fields[key];
1515
+ if (!field) throw new ToolInputError(`Resource type "${input.namespace}:${input.resourceType}" does not support match key "${key}".`);
1516
+ if (Array.isArray(value)) {
1517
+ if (field.kind === "boolean") throw new ToolInputError(`Match key "${key}" is boolean and cannot use a list.`);
1518
+ for (const entry of value) {
1519
+ if (typeof entry !== field.kind) throw new ToolInputError(`Match key "${key}" expects ${field.kind} values.`);
1520
+ if (field.enum && typeof entry === "string" && !field.enum.includes(entry)) throw new ToolInputError(`Match key "${key}" does not allow value "${entry}".`);
1521
+ }
1522
+ normalized[key] = [...new Set(value)];
1523
+ continue;
1524
+ }
1525
+ if (typeof value !== field.kind) throw new ToolInputError(`Match key "${key}" expects a ${field.kind} value.`);
1526
+ if (field.enum && typeof value === "string" && !field.enum.includes(value)) throw new ToolInputError(`Match key "${key}" does not allow value "${value}".`);
1527
+ normalized[key] = value;
1528
+ }
1529
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
1530
+ }
1531
+ /** Normalize one selector with the convention declared by its plugin. */
1532
+ function normalizeEventIdentifier(catalog, namespace, identifier) {
1533
+ return normalizeResourceEventIdentifier(catalog[namespace], identifier);
1534
+ }
1535
+ //#endregion
1536
+ //#region src/chat/sandbox/snapshot/events.ts
1537
+ const WORKSPACE_SNAPSHOT_NAMESPACE = CORE_RESOURCE_EVENT_NAMESPACE;
1538
+ const WORKSPACE_SNAPSHOT_RESOURCE_TYPE = "workspace_snapshot";
1539
+ const WORKSPACE_SNAPSHOT_READY_EVENT = "workspace_snapshot.ready";
1540
+ const WORKSPACE_SNAPSHOT_FAILED_EVENT = "workspace_snapshot.failed";
1541
+ /**
1542
+ * Core catalog entry for Workspace snapshot ready and failed events.
1543
+ *
1544
+ * Uses the same registration shape as plugins so search, guidance, and tool
1545
+ * schemas treat these events like any other resource type.
1546
+ */
1547
+ function workspaceSnapshotResourceEvents() {
1548
+ return { resourceTypes: [{
1549
+ type: WORKSPACE_SNAPSHOT_RESOURCE_TYPE,
1550
+ supportedEvents: [WORKSPACE_SNAPSHOT_READY_EVENT, WORKSPACE_SNAPSHOT_FAILED_EVENT],
1551
+ suggestedEvents: [],
1552
+ guidance: {
1553
+ [WORKSPACE_SNAPSHOT_READY_EVENT]: "Call switchWorkspace again with the same Workspace name. Do not create another watch when switchWorkspace already returned a subscription.",
1554
+ [WORKSPACE_SNAPSHOT_FAILED_EVENT]: "Report the snapshot failure. Do not keep waiting for this Workspace unless the user asks to retry."
1555
+ }
1556
+ }] };
1557
+ }
1558
+ /** Describe the events that report when a snapshot build ends. */
1559
+ function workspaceSnapshotWatch(input) {
1560
+ return {
1561
+ identifier: input.workspaceId,
1562
+ label: `Workspace ${input.workspaceName} snapshot`,
1563
+ namespace: WORKSPACE_SNAPSHOT_NAMESPACE,
1564
+ supportedEvents: [WORKSPACE_SNAPSHOT_READY_EVENT, WORKSPACE_SNAPSHOT_FAILED_EVENT],
1565
+ suggestedEvents: [],
1566
+ type: WORKSPACE_SNAPSHOT_RESOURCE_TYPE
1567
+ };
1568
+ }
1569
+ /** Report that a Workspace snapshot build is ready or failed. */
1570
+ function workspaceSnapshotFinishedEvent(input) {
1571
+ const occurredAtMs = input.occurredAtMs ?? Date.now();
1572
+ const eventType = input.status === "ready" ? WORKSPACE_SNAPSHOT_READY_EVENT : WORKSPACE_SNAPSHOT_FAILED_EVENT;
1573
+ const trustedSummary = input.status === "ready" ? "Workspace snapshot is ready." : "Workspace snapshot build failed.";
1574
+ const event = {
1575
+ eventKey: `${WORKSPACE_SNAPSHOT_NAMESPACE}:${input.workspaceId}:${input.resultId}:${input.status}`,
1576
+ eventType,
1577
+ identifier: input.workspaceId,
1578
+ namespace: WORKSPACE_SNAPSHOT_NAMESPACE,
1579
+ occurredAtMs,
1580
+ terminal: true,
1581
+ trustedSummary,
1582
+ data: {
1583
+ workspaceId: input.workspaceId,
1584
+ status: input.status
1585
+ }
1586
+ };
1587
+ if (input.status === "failed" && input.error) event.untrustedText = input.error;
1588
+ return event;
1589
+ }
1590
+ //#endregion
1591
+ //#region src/chat/resource-events/runtime-catalog.ts
1592
+ /**
1593
+ * Enabled resource-event registrations for search, guidance, and tool schemas.
1594
+ *
1595
+ * Includes core Workspace snapshot events under the `junior` namespace, plus
1596
+ * every enabled plugin registration. Plugins may not use the `junior` namespace.
1597
+ */
1598
+ function getResourceEventCatalog() {
1599
+ const catalog = { [CORE_RESOURCE_EVENT_NAMESPACE]: workspaceSnapshotResourceEvents() };
1600
+ for (const plugin of getPlugins()) {
1601
+ const registration = plugin.resourceEvents;
1602
+ if (!registration || registration.isEnabled?.() === false) continue;
1603
+ if (plugin.manifest.name === "junior") throw new Error(`Plugin "${CORE_RESOURCE_EVENT_NAMESPACE}" cannot register resource events; that namespace is reserved for core Workspace snapshots`);
1604
+ catalog[plugin.manifest.name] = registration;
1605
+ }
1606
+ return catalog;
1607
+ }
1608
+ /** Validate the persisted Slack creator identity for an event task. */
1609
+ const eventTaskPrincipalSchema = z.object({
1610
+ slackUserId: actorUserIdSchema,
1611
+ fullName: z.string().optional(),
1612
+ userName: z.string().optional()
1613
+ }).strict();
1614
+ /** Validate one persisted event-task selector and its presentation metadata. */
1615
+ const eventTaskTriggerSchema = z.object({
1616
+ events: z.array(resourceEventTypeSchema).min(1),
1617
+ label: z.string().min(1),
1618
+ match: resourceEventMatchSchema.optional(),
1619
+ namespace: z.string().min(1),
1620
+ identifier: z.string().min(1).max(300),
1621
+ resourceType: z.string().min(1)
1622
+ }).strict();
1623
+ /** Validate one persisted event task. */
1624
+ const eventTaskSchema = z.object({
1625
+ id: z.string().min(1),
1626
+ createdAtMs: z.number().finite(),
1627
+ createdBy: eventTaskPrincipalSchema,
1628
+ credentialMode: z.enum(["system", "creator"]),
1629
+ destination: slackDestinationSchema,
1630
+ destinationVisibility: destinationVisibilitySchema,
1631
+ /** Visible effects after successful work. Missing legacy values send a message. */
1632
+ outcomes: z.array(taskOutcomeSchema).max(5).optional(),
1633
+ task: z.object({ text: z.string().min(1) }).strict(),
1634
+ trigger: eventTaskTriggerSchema
1635
+ }).strict();
1636
+ //#endregion
1637
+ //#region src/chat/event-tasks/store.ts
1638
+ /** JSON task payload must not carry SQL-backed columns. */
1639
+ function eventTaskJsonPayload(task) {
1640
+ const { status: _status, title: _title, ...payload } = task;
1641
+ return payload;
1642
+ }
1643
+ function parseTask(row) {
1644
+ const raw = row.task && typeof row.task === "object" ? { ...row.task } : row.task;
1645
+ if (raw && typeof raw === "object" && "title" in raw) delete raw.title;
1646
+ const payload = eventTaskSchema.parse(raw);
1647
+ const title = row.title?.trim();
1648
+ return {
1649
+ ...payload,
1650
+ status: row.status === "deleted" ? "deleted" : "active",
1651
+ ...title ? { title } : void 0
1652
+ };
1653
+ }
1654
+ function activeEventTaskWhere() {
1655
+ return ne(juniorEventTasks.status, "deleted");
1656
+ }
1657
+ /** Read one event task by id, including deleted rows when present. */
1658
+ async function getEventTask(db, id) {
1659
+ const rows = await db.select({
1660
+ status: juniorEventTasks.status,
1661
+ task: juniorEventTasks.task,
1662
+ title: juniorEventTasks.title
1663
+ }).from(juniorEventTasks).where(eq(juniorEventTasks.id, id)).limit(1);
1664
+ return rows[0] ? parseTask(rows[0]) : void 0;
1665
+ }
1666
+ /** Create one retry-stable event task, or revive a deleted row with the new payload. */
1667
+ async function createEventTask(db, task) {
1668
+ const parsed = eventTaskSchema.parse(eventTaskJsonPayload(task));
1669
+ const title = task.title?.trim() || null;
1670
+ await db.insert(juniorEventTasks).values({
1671
+ id: parsed.id,
1672
+ teamId: parsed.destination.teamId,
1673
+ namespace: parsed.trigger.namespace,
1674
+ identifier: parsed.trigger.identifier,
1675
+ createdAtMs: parsed.createdAtMs,
1676
+ status: "active",
1677
+ title,
1678
+ task: parsed
1679
+ }).onConflictDoNothing();
1680
+ const existing = await getEventTask(db, parsed.id);
1681
+ if (!existing) return {
1682
+ ...parsed,
1683
+ status: "active",
1684
+ ...title ? { title } : void 0
1685
+ };
1686
+ if (existing.status !== "deleted") return existing;
1687
+ const rows = await db.update(juniorEventTasks).set({
1688
+ teamId: parsed.destination.teamId,
1689
+ namespace: parsed.trigger.namespace,
1690
+ identifier: parsed.trigger.identifier,
1691
+ status: "active",
1692
+ title,
1693
+ task: parsed
1694
+ }).where(and(eq(juniorEventTasks.id, parsed.id), eq(juniorEventTasks.status, "deleted"))).returning({
1695
+ status: juniorEventTasks.status,
1696
+ task: juniorEventTasks.task,
1697
+ title: juniorEventTasks.title
1698
+ });
1699
+ return rows[0] ? parseTask(rows[0]) : await getEventTask(db, parsed.id) ?? {
1700
+ ...parsed,
1701
+ status: "active",
1702
+ ...title ? { title } : void 0
1703
+ };
1704
+ }
1705
+ /** Replace an existing non-deleted event task. */
1706
+ async function saveEventTask(db, task) {
1707
+ const parsed = eventTaskSchema.parse(eventTaskJsonPayload(task));
1708
+ const title = task.title?.trim() || null;
1709
+ const rows = await db.update(juniorEventTasks).set({
1710
+ namespace: parsed.trigger.namespace,
1711
+ identifier: parsed.trigger.identifier,
1712
+ title,
1713
+ task: parsed
1714
+ }).where(and(eq(juniorEventTasks.id, parsed.id), activeEventTaskWhere())).returning({
1715
+ status: juniorEventTasks.status,
1716
+ task: juniorEventTasks.task,
1717
+ title: juniorEventTasks.title
1718
+ });
1719
+ return rows[0] ? parseTask(rows[0]) : void 0;
1720
+ }
1721
+ /** Mark one existing event task deleted while retaining the row for history. */
1722
+ async function deleteEventTask(db, id) {
1723
+ const rows = await db.update(juniorEventTasks).set({ status: "deleted" }).where(and(eq(juniorEventTasks.id, id), activeEventTaskWhere())).returning({
1724
+ status: juniorEventTasks.status,
1725
+ task: juniorEventTasks.task,
1726
+ title: juniorEventTasks.title
1727
+ });
1728
+ return rows[0] ? parseTask(rows[0]) : void 0;
1729
+ }
1730
+ /** List live event tasks in one Slack workspace. */
1731
+ async function listEventTasksForTeam(db, teamId) {
1732
+ return (await db.select({
1733
+ status: juniorEventTasks.status,
1734
+ task: juniorEventTasks.task,
1735
+ title: juniorEventTasks.title
1736
+ }).from(juniorEventTasks).where(and(eq(juniorEventTasks.teamId, teamId), activeEventTaskWhere())).orderBy(asc(juniorEventTasks.createdAtMs), asc(juniorEventTasks.id))).map(parseTask);
1737
+ }
1738
+ function viewerSlackIdentities(user) {
1739
+ return user.identities.filter((identity) => identity.provider === "slack" && Boolean(identity.providerTenantId) && Boolean(identity.providerSubjectId));
1740
+ }
1741
+ /** Return whether one event task was created by a viewer-linked Slack identity. */
1742
+ function eventTaskBelongsToUser(task, user) {
1743
+ return viewerSlackIdentities(user).some((identity) => identity.providerTenantId === task.destination.teamId && identity.providerSubjectId === task.createdBy.slackUserId);
1744
+ }
1745
+ /** List a bounded newest-first page of live event tasks created by one user. */
1746
+ async function listEventTasksCreatedBy(db, user, limit, query) {
1747
+ const identities = viewerSlackIdentities(user);
1748
+ const ownership = or(...identities.map((identity) => and(eq(juniorEventTasks.teamId, identity.providerTenantId), sql`${juniorEventTasks.task}->'createdBy'->>'slackUserId' = ${identity.providerSubjectId}`)));
1749
+ if (!ownership) return [];
1750
+ return (await db.select({
1751
+ status: juniorEventTasks.status,
1752
+ task: juniorEventTasks.task,
1753
+ title: juniorEventTasks.title
1754
+ }).from(juniorEventTasks).where(and(ownership, activeEventTaskWhere(), query ? sql`strpos(lower(coalesce(${juniorEventTasks.title}, ${juniorEventTasks.task}->'task'->>'text')), ${query}) > 0` : void 0)).orderBy(desc(juniorEventTasks.createdAtMs), desc(juniorEventTasks.id)).limit(limit)).map(parseTask);
1755
+ }
1756
+ /**
1757
+ * List deleted event tasks created by one user, newest first.
1758
+ * Used only to keep historical runs after the live task list drops the row.
1759
+ */
1760
+ async function listDeletedEventTasksCreatedBy(db, user, limit) {
1761
+ const identities = viewerSlackIdentities(user);
1762
+ const ownership = or(...identities.map((identity) => and(eq(juniorEventTasks.teamId, identity.providerTenantId), sql`${juniorEventTasks.task}->'createdBy'->>'slackUserId' = ${identity.providerSubjectId}`)));
1763
+ if (!ownership) return [];
1764
+ return (await db.select({
1765
+ status: juniorEventTasks.status,
1766
+ task: juniorEventTasks.task,
1767
+ title: juniorEventTasks.title
1768
+ }).from(juniorEventTasks).where(and(ownership, eq(juniorEventTasks.status, "deleted"))).orderBy(desc(juniorEventTasks.createdAtMs), desc(juniorEventTasks.id)).limit(limit)).map(parseTask);
1769
+ }
1770
+ /** List live event tasks whose current Slack destination is public. */
1771
+ async function listPublicEventTasksForTeams(db, teamIds, limit, query) {
1772
+ if (teamIds.length === 0) return [];
1773
+ return (await db.select({
1774
+ status: juniorEventTasks.status,
1775
+ task: juniorEventTasks.task,
1776
+ title: juniorEventTasks.title
1777
+ }).from(juniorEventTasks).innerJoin(juniorDestinations, and(eq(juniorDestinations.provider, "slack"), eq(juniorDestinations.providerTenantId, juniorEventTasks.teamId), sql`${juniorDestinations.providerDestinationId} = ${juniorEventTasks.task}->'destination'->>'channelId'`)).where(and(inArray(juniorEventTasks.teamId, teamIds), activeEventTaskWhere(), query ? sql`strpos(lower(coalesce(${juniorEventTasks.title}, ${juniorEventTasks.task}->'task'->>'text')), ${query}) > 0` : void 0, eq(juniorDestinations.visibility, "public"))).orderBy(desc(juniorEventTasks.createdAtMs), desc(juniorEventTasks.id)).limit(limit)).map(parseTask);
1778
+ }
1779
+ /**
1780
+ * Collect match keys from live event tasks for these identifiers and event types.
1781
+ * Does not evaluate match values — only reports keys that filters use.
1782
+ */
1783
+ async function collectEventTaskMatchKeys(db, input) {
1784
+ const eventTypes = new Set(input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean));
1785
+ const identifiers = [...new Set(input.identifiers.map((value) => value.trim()).filter(Boolean))];
1786
+ if (eventTypes.size === 0 || identifiers.length === 0) return [];
1787
+ const rows = await db.select({
1788
+ status: juniorEventTasks.status,
1789
+ task: juniorEventTasks.task,
1790
+ title: juniorEventTasks.title
1791
+ }).from(juniorEventTasks).where(and(eq(juniorEventTasks.teamId, input.teamId), eq(juniorEventTasks.namespace, input.namespace), inArray(juniorEventTasks.identifier, identifiers), activeEventTaskWhere())).orderBy(asc(juniorEventTasks.createdAtMs), asc(juniorEventTasks.id));
1792
+ const keys = /* @__PURE__ */ new Set();
1793
+ for (const row of rows) {
1794
+ const task = parseTask(row);
1795
+ if (!task.trigger.events.some((eventType) => eventTypes.has(eventType))) continue;
1796
+ for (const key of Object.keys(task.trigger.match ?? {})) keys.add(key);
1797
+ }
1798
+ return [...keys].sort();
1799
+ }
1800
+ /** Find every live task matching one normalized resource event. */
1801
+ async function findMatchingEventTasks(db, event, teamId) {
1802
+ return (await db.select({
1803
+ status: juniorEventTasks.status,
1804
+ task: juniorEventTasks.task,
1805
+ title: juniorEventTasks.title
1806
+ }).from(juniorEventTasks).where(and(eq(juniorEventTasks.teamId, teamId), eq(juniorEventTasks.namespace, event.namespace), eq(juniorEventTasks.identifier, event.identifier), activeEventTaskWhere())).orderBy(asc(juniorEventTasks.createdAtMs), asc(juniorEventTasks.id))).map(parseTask).filter((task) => task.trigger.events.includes(event.eventType) && resourceEventMatches(task.trigger.match, event.data));
1807
+ }
1808
+ //#endregion
1809
+ //#region src/chat/task-outcomes.ts
1810
+ const taskMessageDestinationInputSchema = z.union([slackDestinationSchema, z.object({
1811
+ platform: z.literal("slack"),
1812
+ teamId: z.string().min(1),
1813
+ userId: actorUserIdSchema
1814
+ }).strict()]);
1815
+ /** Input accepted by task authoring tools before a user becomes a DM Destination. */
1816
+ const taskOutcomeInputSchema = z.object({
1817
+ action: z.literal("send_message"),
1818
+ destination: taskMessageDestinationInputSchema
1819
+ }).strict();
1820
+ /** Resolve tool input to the Slack Destinations stored on a task. */
1821
+ async function resolveTaskOutcomes(outcomes, currentDestination, creatorSlackUserId) {
1822
+ if (outcomes === void 0) return [{
1823
+ action: "send_message",
1824
+ destination: currentDestination
1825
+ }];
1826
+ const resolved = [];
1827
+ for (const outcome of outcomes) {
1828
+ if (outcome.destination.teamId !== currentDestination.teamId) throw new Error("Message destinations must be in the current Slack workspace.");
1829
+ if ("channelId" in outcome.destination) {
1830
+ if (outcome.destination.channelId !== currentDestination.channelId) throw new Error("Messages can only be sent to the current Slack conversation or the task creator.");
1831
+ resolved.push({
1832
+ action: "send_message",
1833
+ destination: outcome.destination
1834
+ });
1835
+ continue;
1836
+ }
1837
+ const userId = outcome.destination.userId;
1838
+ if (userId !== creatorSlackUserId) throw new Error("Direct messages can only be sent to the task creator.");
1839
+ const channelId = (await withSlackRetries(() => getSlackClient().conversations.open({ users: userId }), 3, { action: "conversations.open" })).channel?.id;
1840
+ if (!channelId) throw new Error("Slack did not return a direct message destination.");
1841
+ resolved.push({
1842
+ action: "send_message",
1843
+ destination: {
1844
+ platform: "slack",
1845
+ teamId: currentDestination.teamId,
1846
+ channelId
1847
+ }
1848
+ });
1849
+ }
1850
+ return resolved;
1851
+ }
1852
+ /** Return stored outcomes, or preserve the legacy message Destination. */
1853
+ function effectiveTaskOutcomes(outcomes, destination) {
1854
+ return outcomes ?? [{
1855
+ action: "send_message",
1856
+ destination
1857
+ }];
1858
+ }
1859
+ //#endregion
1860
+ //#region src/chat/event-tasks/tool-support.ts
1861
+ const compactEventTaskResultSchema = z.object({
1862
+ id: z.string().min(1),
1863
+ task: z.string().min(1),
1864
+ namespace: z.string().min(1),
1865
+ identifier: z.string().min(1),
1866
+ resourceType: z.string().min(1),
1867
+ label: z.string().min(1),
1868
+ events: z.array(z.string().min(1)).min(1),
1869
+ match: z.record(z.string(), z.unknown()).optional(),
1870
+ credentialMode: z.enum(["system", "creator"]),
1871
+ outcomes: z.array(taskOutcomeSchema).max(5),
1872
+ createdBy: eventTaskPrincipalSchema,
1873
+ triggerAvailable: z.boolean()
1874
+ }).strict();
1875
+ /** Validate one successful event-task mutation result. */
1876
+ const eventTaskToolResultSchema = juniorToolOutputSchema.extend({ task: compactEventTaskResultSchema }).strict();
1877
+ /** Validate one successful event-task list result. */
1878
+ const eventTaskListToolResultSchema = juniorToolOutputSchema.extend({
1879
+ tasks: z.array(compactEventTaskResultSchema),
1880
+ truncated: z.boolean()
1881
+ }).strict();
1882
+ /** Build the validated resource-event trigger accepted by event-task tools. */
1883
+ function registeredEventTaskTriggerSchema(catalog) {
1884
+ const plugins = pluginResourceEventCatalog(catalog);
1885
+ return z.object({
1886
+ namespace: eventNamespaceSchema(plugins),
1887
+ identifier: z.string().trim().min(1).max(300),
1888
+ resourceType: registeredResourceTypeSchema(plugins),
1889
+ label: z.string().trim().min(1).max(500),
1890
+ events: z.array(registeredEventTypeSchema(plugins)).min(1),
1891
+ match: registeredResourceEventMatchSchema()
1892
+ }).strict();
1893
+ }
1894
+ /** Reject an event-task trigger that is no longer supported by the catalog. */
1895
+ function requireSupportedEventTaskTrigger(catalog, trigger) {
1896
+ const plugins = pluginResourceEventCatalog(catalog);
1897
+ for (const eventType of trigger.events) if (!pluginSupportsEvent(plugins, trigger.namespace, trigger.resourceType, eventType)) throw new ToolInputError(`Resource type "${trigger.namespace}:${trigger.resourceType}" does not support event "${eventType}".`);
1898
+ return requireSupportedResourceEventMatch(plugins, {
1899
+ match: trigger.match,
1900
+ namespace: trigger.namespace,
1901
+ resourceType: trigger.resourceType
1902
+ });
1903
+ }
1904
+ /** Require the active Slack authority used for event-task management. */
1905
+ function requireEventTaskSlackContext(context) {
1906
+ if (context.source.kind !== "slack" || context.destination.platform !== "slack" || context.actor?.platform !== "slack" || context.actor.teamId !== context.source.teamId) throw new ToolInputError("Event tasks require an active Slack channel or DM and actor.");
1907
+ return {
1908
+ actor: context.actor,
1909
+ destination: {
1910
+ platform: "slack",
1911
+ teamId: context.source.teamId,
1912
+ channelId: context.source.channelId
1913
+ },
1914
+ source: context.source
1915
+ };
1916
+ }
1917
+ /** Return whether an event task belongs to the active Slack destination. */
1918
+ function eventTaskMatchesDestination(task, destination) {
1919
+ return task.destination.teamId === destination.teamId && task.destination.channelId === destination.channelId;
1920
+ }
1921
+ /**
1922
+ * Return whether the active destination may update or delete this task.
1923
+ * Same-destination tasks stay local. Public tasks may be managed by id from
1924
+ * another destination in the same workspace.
1925
+ */
1926
+ function eventTaskIsWritableFrom(task, destination) {
1927
+ if (task.destination.teamId !== destination.teamId) return false;
1928
+ return task.destination.channelId === destination.channelId || task.destinationVisibility === "public";
1929
+ }
1930
+ /** Load one task the active destination may update or delete. */
1931
+ async function writableEventTask(context, id) {
1932
+ const { destination } = requireEventTaskSlackContext(context);
1933
+ const task = await getEventTask(getDb(), id);
1934
+ if (!task || task.status === "deleted" || !eventTaskIsWritableFrom(task, destination)) throw new ToolInputError("Event task was not found.");
1935
+ return task;
1936
+ }
1937
+ function eventTaskTriggerAvailable(task, catalog) {
1938
+ const registration = pluginResourceEventCatalog(catalog)[task.trigger.namespace];
1939
+ return Boolean(registration && task.trigger.events.every((eventType) => registration.resourceTypes.some((resourceType) => resourceType.supportedEvents.includes(eventType))));
1940
+ }
1941
+ /** Project an event task into the bounded tool-result shape. */
1942
+ function compactEventTask(task, catalog) {
1943
+ return compactEventTaskResultSchema.parse({
1944
+ id: task.id,
1945
+ task: task.task.text,
1946
+ namespace: task.trigger.namespace,
1947
+ identifier: task.trigger.identifier,
1948
+ resourceType: task.trigger.resourceType,
1949
+ label: task.trigger.label,
1950
+ events: task.trigger.events,
1951
+ ...task.trigger.match ? { match: task.trigger.match } : void 0,
1952
+ credentialMode: task.credentialMode,
1953
+ outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
1954
+ createdBy: task.createdBy,
1955
+ triggerAvailable: eventTaskTriggerAvailable(task, catalog)
1956
+ });
1957
+ }
1958
+ /** Return the standard successful event-task tool result. */
1959
+ function eventTaskSuccess(task, catalog) {
1960
+ return { task: compactEventTask(task, catalog) };
1961
+ }
1962
+ //#endregion
1963
+ //#region src/chat/scheduled-tasks/types.ts
1964
+ /** Durable scheduled-task domain types owned by Junior core. */
1965
+ const scheduledTaskStatusSchema = z.enum([
1966
+ "active",
1967
+ "blocked",
1968
+ "completed",
1969
+ "deleted"
1970
+ ]);
1971
+ const scheduledTaskCredentialModeSchema = z.enum(["system", "creator"]);
1972
+ const scheduledRunStatusSchema = z.enum([
1973
+ "pending",
1974
+ "running",
1975
+ "completed",
1976
+ "failed",
1977
+ "blocked",
1978
+ "skipped"
1979
+ ]);
1980
+ const scheduledTaskPrincipalSchema = z.object({
1981
+ slackUserId: actorUserIdSchema,
1982
+ fullName: z.string().optional(),
1983
+ userName: z.string().optional()
1984
+ }).strict();
1985
+ const scheduledTaskRecurrenceSchema = z.object({
1986
+ dayOfMonth: z.number().optional(),
1987
+ frequency: z.enum([
1988
+ "daily",
1989
+ "weekly",
1990
+ "monthly",
1991
+ "yearly"
1992
+ ]),
1993
+ interval: z.number(),
1994
+ month: z.number().optional(),
1995
+ startDate: z.string(),
1996
+ time: z.object({
1997
+ hour: z.number(),
1998
+ minute: z.number()
1999
+ }).strict(),
2000
+ weekdays: z.array(z.number()).optional()
2001
+ }).strict();
2002
+ const scheduledTaskScheduleSchema = z.object({
2003
+ description: z.string(),
2004
+ kind: z.enum(["one_off", "recurring"]),
2005
+ recurrence: scheduledTaskRecurrenceSchema.optional(),
2006
+ timezone: z.string()
2007
+ }).strict();
2008
+ const scheduledTaskExecutionActorSchema = z.object({
2009
+ platform: z.literal("system"),
2010
+ name: z.string()
2011
+ }).strict();
2012
+ /** Validate the current scheduled-task domain shape. */
2013
+ const scheduledTaskSchema = z.object({
2014
+ id: z.string(),
2015
+ conversationAccess: z.object({
2016
+ audience: z.enum([
2017
+ "direct",
2018
+ "group",
2019
+ "channel"
2020
+ ]),
2021
+ visibility: z.enum(["private", "public"])
2022
+ }).strict(),
2023
+ createdAtMs: z.number(),
2024
+ createdBy: scheduledTaskPrincipalSchema,
2025
+ /** Authoritative provider identity that created the task. */
2026
+ creatorIdentityId: z.string(),
2027
+ /** Selects system credentials or task-bound creator credential delegation. */
2028
+ credentialMode: scheduledTaskCredentialModeSchema,
2029
+ destination: slackDestinationSchema,
2030
+ executionActor: scheduledTaskExecutionActorSchema.optional(),
2031
+ lastRunAtMs: z.number().optional(),
2032
+ nextRunAtMs: z.number().optional(),
2033
+ originalRequest: z.string().optional(),
2034
+ runNowAtMs: z.number().optional(),
2035
+ schedule: scheduledTaskScheduleSchema,
2036
+ status: scheduledTaskStatusSchema,
2037
+ statusReason: z.string().optional(),
2038
+ /** Visible effects after successful work. Missing legacy values send a message. */
2039
+ outcomes: z.array(taskOutcomeSchema).max(5).optional(),
2040
+ task: z.object({ text: z.string() }).strict(),
2041
+ /** SQL-backed short display title generated from the task instruction. */
2042
+ title: z.string().optional(),
2043
+ updatedAtMs: z.number()
2044
+ }).strict();
2045
+ /** Validate the current scheduled-run domain shape. */
2046
+ const scheduledRunSchema = z.object({
2047
+ id: z.string(),
2048
+ attempt: z.number(),
2049
+ claimedAtMs: z.number(),
2050
+ completedAtMs: z.number().optional(),
2051
+ dispatchId: z.string().optional(),
2052
+ errorMessage: z.string().optional(),
2053
+ resultMessageTs: z.string().optional(),
2054
+ scheduledForMs: z.number(),
2055
+ startedAtMs: z.number().optional(),
2056
+ status: scheduledRunStatusSchema,
2057
+ taskId: z.string()
2058
+ }).strict();
2059
+ const SCHEDULED_TASK_SYSTEM_ACTOR = Object.freeze({
2060
+ platform: "system",
2061
+ name: "scheduled-task"
2062
+ });
2063
+ //#endregion
2064
+ //#region src/chat/scheduled-tasks/tasks.ts
2065
+ const SCHEDULER_KEY_PREFIX = "junior:scheduler";
2066
+ const retainedScheduledTaskSchema = scheduledTaskSchema.omit({
2067
+ creatorIdentityId: true,
2068
+ title: true
2069
+ }).extend({
2070
+ status: z.enum([
2071
+ "active",
2072
+ "blocked",
2073
+ "completed",
2074
+ "deleted",
2075
+ "paused"
2076
+ ]),
2077
+ version: z.number().optional()
2078
+ }).strict();
2079
+ /** Decode a retained scheduler task row and reject invalid routing context. */
2080
+ function parseScheduledTaskRow(row) {
2081
+ const record = z.record(z.string(), z.unknown()).safeParse(row.record);
2082
+ if (!record.success) return void 0;
2083
+ const { creatorIdentityId: legacyCreatorIdentityId, title: legacyTitle, ...retained } = record.data;
2084
+ const parsed = retainedScheduledTaskSchema.safeParse(retained);
2085
+ if (!parsed.success) return void 0;
2086
+ const { status, version: _version, ...task } = parsed.data;
2087
+ const fallbackIdentity = row.creatorIdentityId === null ? z.string().safeParse(legacyCreatorIdentityId) : void 0;
2088
+ const creatorIdentityId = row.creatorIdentityId ?? (fallbackIdentity?.success ? fallbackIdentity.data : void 0);
2089
+ if (creatorIdentityId === void 0) return void 0;
2090
+ const fallbackTitle = row.title === null ? z.string().safeParse(legacyTitle) : void 0;
2091
+ const title = (row.title !== null ? row.title : fallbackTitle?.success ? fallbackTitle.data : void 0)?.trim() || void 0;
2092
+ if (status === "paused") {
2093
+ const { nextRunAtMs: _nextRunAtMs, runNowAtMs: _runNowAtMs, ...retained } = task;
2094
+ return {
2095
+ ...retained,
2096
+ creatorIdentityId,
2097
+ status: "deleted",
2098
+ ...title ? { title } : void 0
2099
+ };
2100
+ }
2101
+ return {
2102
+ ...task,
2103
+ creatorIdentityId,
2104
+ status,
2105
+ ...title ? { title } : void 0
2106
+ };
2107
+ }
2108
+ /** Keep scan paths from returning current or legacy tombstones. */
2109
+ function isListedScheduledTask(task) {
2110
+ return task !== void 0 && task.status !== "deleted";
2111
+ }
2112
+ /** Build the stable run id for one task occurrence. */
2113
+ function scheduledRunId(taskId, scheduledForMs) {
2114
+ return `${taskId}:${scheduledForMs}`;
2115
+ }
2116
+ /** Acquire the deployed advisory lock for one scheduled task. */
2117
+ function withScheduledTaskLock(db, taskId, callback) {
2118
+ return db.transaction(async (tx) => {
2119
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${`${SCHEDULER_KEY_PREFIX}:task:${taskId}:lock`}))`);
2120
+ return await callback(tx);
2121
+ });
2122
+ }
2123
+ function requireStoredTask(task) {
2124
+ const parsed = scheduledTaskSchema.safeParse(task);
2125
+ if (!parsed.success) throw new Error("Scheduled task routing context is invalid.");
2126
+ const { title, ...current } = parsed.data;
2127
+ const normalizedTitle = title?.trim() || void 0;
2128
+ return {
2129
+ ...current,
2130
+ ...normalizedTitle ? { title: normalizedTitle } : void 0
2131
+ };
2132
+ }
2133
+ function scheduledTaskJsonRecord(task) {
2134
+ const { title: _title, ...record } = task;
2135
+ return record;
2136
+ }
2137
+ async function upsertScheduledTask(db, task) {
2138
+ const title = task.title?.trim() || null;
2139
+ await db.insert(juniorSchedulerTasks).values({
2140
+ createdAtMs: task.createdAtMs,
2141
+ creatorIdentityId: task.creatorIdentityId,
2142
+ creatorSlackUserId: task.createdBy.slackUserId,
2143
+ id: task.id,
2144
+ nextRunAtMs: task.nextRunAtMs,
2145
+ record: scheduledTaskJsonRecord(task),
2146
+ runNowAtMs: task.runNowAtMs,
2147
+ status: task.status,
2148
+ teamId: task.destination.teamId,
2149
+ title
2150
+ }).onConflictDoUpdate({
2151
+ target: juniorSchedulerTasks.id,
2152
+ set: {
2153
+ createdAtMs: sql`excluded.created_at_ms`,
2154
+ creatorIdentityId: sql`excluded.creator_identity_id`,
2155
+ creatorSlackUserId: sql`excluded.creator_slack_user_id`,
2156
+ nextRunAtMs: sql`excluded.next_run_at_ms`,
2157
+ record: sql`excluded.record`,
2158
+ runNowAtMs: sql`excluded.run_now_at_ms`,
2159
+ status: sql`excluded.status`,
2160
+ teamId: sql`excluded.team_id`,
2161
+ title: sql`excluded.title`
2162
+ }
2163
+ });
2164
+ }
2165
+ /** Read and decode one scheduled task. */
2166
+ async function readScheduledTask(db, taskId) {
2167
+ const rows = await db.select({
2168
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
2169
+ record: juniorSchedulerTasks.record,
2170
+ title: juniorSchedulerTasks.title
2171
+ }).from(juniorSchedulerTasks).where(eq(juniorSchedulerTasks.id, taskId)).limit(1);
2172
+ return rows[0] ? parseScheduledTaskRow(rows[0]) : void 0;
2173
+ }
2174
+ async function readListedScheduledTasks(db, teamId) {
2175
+ return (await db.select({
2176
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
2177
+ record: juniorSchedulerTasks.record,
2178
+ title: juniorSchedulerTasks.title
2179
+ }).from(juniorSchedulerTasks).where(and(notInArray(juniorSchedulerTasks.status, ["deleted", "paused"]), teamId === void 0 ? void 0 : eq(juniorSchedulerTasks.teamId, teamId))).orderBy(asc(juniorSchedulerTasks.createdAtMs), asc(juniorSchedulerTasks.id))).map(parseScheduledTaskRow).filter(isListedScheduledTask);
2180
+ }
2181
+ /** List decoded scheduled tasks in stable creation order. */
2182
+ async function listScheduledTasks(db) {
2183
+ return await readListedScheduledTasks(db);
2184
+ }
2185
+ /** List decoded scheduled tasks for one team in stable creation order. */
2186
+ async function listScheduledTasksForTeam(db, teamId) {
2187
+ return await readListedScheduledTasks(db, teamId);
2188
+ }
2189
+ async function writeScheduledTask(db, task, current) {
2190
+ if (current?.status === "blocked" && task.status === "active" && typeof task.nextRunAtMs === "number" && Number.isFinite(task.nextRunAtMs)) await db.delete(juniorSchedulerRuns).where(and(eq(juniorSchedulerRuns.id, scheduledRunId(task.id, task.nextRunAtMs)), eq(juniorSchedulerRuns.status, "blocked")));
2191
+ if (task.status === "deleted" && current?.status !== "deleted") await skipPendingRunsForDeletedTask(db, task);
2192
+ await upsertScheduledTask(db, task);
2193
+ }
2194
+ async function skipPendingRunsForDeletedTask(db, task) {
2195
+ const errorMessage = `Scheduled task ${task.id} was deleted before the run started.`;
2196
+ const rows = await db.select({ record: juniorSchedulerRuns.record }).from(juniorSchedulerRuns).where(and(eq(juniorSchedulerRuns.taskId, task.id), eq(juniorSchedulerRuns.status, "pending")));
2197
+ for (const row of rows) {
2198
+ const parsed = scheduledRunSchema.safeParse(row.record);
2199
+ if (!parsed.success || parsed.data.status !== "pending") continue;
2200
+ const next = scheduledRunSchema.parse({
2201
+ ...parsed.data,
2202
+ completedAtMs: task.updatedAtMs,
2203
+ errorMessage,
2204
+ status: "skipped"
2205
+ });
2206
+ await db.update(juniorSchedulerRuns).set({
2207
+ record: next,
2208
+ status: next.status
2209
+ }).where(and(eq(juniorSchedulerRuns.id, next.id), eq(juniorSchedulerRuns.status, "pending")));
2210
+ }
2211
+ }
2212
+ /** Create a scheduled task once under its retry-stable task lock. */
2213
+ async function createScheduledTask(db, task) {
2214
+ const next = requireStoredTask(task);
2215
+ return await withScheduledTaskLock(db, task.id, async (tx) => {
2216
+ const current = await readScheduledTask(tx, task.id);
2217
+ if (current) return current;
2218
+ await writeScheduledTask(tx, next, void 0);
2219
+ return next;
2220
+ });
2221
+ }
2222
+ /** Save a scheduled task and clear its blocked occurrence on reactivation. */
2223
+ async function saveScheduledTask(db, task) {
2224
+ const next = requireStoredTask(task);
2225
+ await withScheduledTaskLock(db, task.id, async (tx) => {
2226
+ const current = await readScheduledTask(tx, task.id);
2227
+ await writeScheduledTask(tx, next, current);
2228
+ });
2229
+ return next;
2230
+ }
2231
+ /** Save a scheduled task inside a task lock already held by the caller. */
2232
+ async function saveScheduledTaskInLock(db, task, current) {
2233
+ await writeScheduledTask(db, requireStoredTask(task), current);
2234
+ }
2235
+ /** List scheduled tasks whose current Slack destination is public. */
2236
+ async function listPublicScheduledTasksForTeams(db, teamIds, input) {
2237
+ if (teamIds.length === 0) return [];
2238
+ return (await db.select({
2239
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
2240
+ record: juniorSchedulerTasks.record,
2241
+ title: juniorSchedulerTasks.title
2242
+ }).from(juniorSchedulerTasks).innerJoin(juniorDestinations, and(eq(juniorDestinations.provider, "slack"), eq(juniorDestinations.providerTenantId, juniorSchedulerTasks.teamId), sql`${juniorDestinations.providerDestinationId} = ${juniorSchedulerTasks.record}->'destination'->>'channelId'`)).where(and(inArray(juniorSchedulerTasks.teamId, teamIds), notInArray(juniorSchedulerTasks.status, [
2243
+ "completed",
2244
+ "deleted",
2245
+ "paused"
2246
+ ]), input.query ? sql`strpos(lower(coalesce(${juniorSchedulerTasks.title}, ${juniorSchedulerTasks.record}->'task'->>'text')), ${input.query}) > 0` : void 0, eq(juniorDestinations.visibility, "public"))).orderBy(desc(juniorSchedulerTasks.createdAtMs), desc(juniorSchedulerTasks.id)).limit(input.limit)).map(parseScheduledTaskRow).filter(isListedScheduledTask);
2247
+ }
2248
+ //#endregion
2249
+ //#region src/chat/slack/context.ts
2250
+ function toTrimmedSlackString(value) {
2251
+ return toOptionalString(value)?.trim() || void 0;
2252
+ }
2253
+ const slackMessageEnvelopeSchema = z.object({ raw: z.unknown().optional() });
2254
+ const rawSlackMessageSchema = z.object({
2255
+ channel: z.unknown().optional(),
2256
+ message: z.unknown().optional(),
2257
+ team: z.unknown().optional(),
2258
+ team_id: z.unknown().optional(),
2259
+ thread_ts: z.unknown().optional(),
2260
+ ts: z.unknown().optional(),
2261
+ user_team: z.unknown().optional()
2262
+ });
2263
+ const nestedRawSlackMessageSchema = z.object({ ts: z.unknown().optional() });
2264
+ /** Project only the Slack raw fields consumed by runtime thread context. */
2265
+ function readSlackRawMessageContext(message) {
2266
+ const envelope = slackMessageEnvelopeSchema.safeParse(message);
2267
+ if (!envelope.success) return;
2268
+ const raw = rawSlackMessageSchema.safeParse(envelope.data.raw);
2269
+ if (!raw.success) return;
2270
+ const nestedMessage = nestedRawSlackMessageSchema.safeParse(raw.data.message);
2271
+ const channelId = parseSlackChannelId(raw.data.channel);
2272
+ const threadTs = parseSlackMessageTs(raw.data.thread_ts);
2273
+ const messageTs = parseSlackMessageTs(raw.data.ts);
2274
+ const nestedMessageTs = nestedMessage.success && parseSlackMessageTs(nestedMessage.data.ts);
2275
+ const teamId = parseSlackTeamId(raw.data.team_id) ?? parseSlackTeamId(raw.data.team);
2276
+ const authorTeamId = parseSlackTeamId(raw.data.user_team);
2277
+ return {
2278
+ ...channelId ? { channelId } : void 0,
2279
+ ...threadTs ? { threadTs } : void 0,
2280
+ ...messageTs ? { messageTs } : void 0,
2281
+ ...nestedMessageTs ? { nestedMessageTs } : void 0,
2282
+ ...teamId ? { teamId } : void 0,
2283
+ ...authorTeamId ? { authorTeamId } : void 0
2284
+ };
2285
+ }
2286
+ /** Extract a channel ID and validated Slack timestamp from `slack:<channel>:<ts>`. */
2287
+ function parseSlackThreadId(threadId) {
2288
+ const normalizedThreadId = toTrimmedSlackString(threadId);
2289
+ if (!normalizedThreadId) return;
2290
+ const parts = normalizedThreadId.split(":");
2291
+ if (parts.length !== 3 || parts[0] !== "slack") return;
2292
+ const channelId = parseSlackChannelId(parts[1]);
2293
+ const threadTs = parseSlackMessageTs(parts[2]);
2294
+ if (!channelId || !threadTs) return;
2295
+ return {
2296
+ channelId,
2297
+ threadTs
2298
+ };
2299
+ }
2300
+ /** Resolve the Slack channel ID from a `slack:<channel>:<ts>` thread identifier. */
2301
+ function resolveSlackChannelIdFromThreadId(threadId) {
2302
+ return parseSlackThreadId(threadId)?.channelId;
2303
+ }
2304
+ /** Best-effort channel ID extraction from a raw Slack message payload. */
2305
+ function resolveSlackChannelIdFromMessage(message) {
2306
+ const messageChannelId = parseSlackChannelId(message.channelId);
2307
+ if (messageChannelId) return messageChannelId;
2308
+ const raw = message.raw;
2309
+ if (raw && typeof raw === "object") {
2310
+ const rawChannel = parseSlackChannelId(raw.channel);
2311
+ if (rawChannel) return rawChannel;
2312
+ }
2313
+ return resolveSlackChannelIdFromThreadId(toTrimmedSlackString(message.threadId));
2314
+ }
2315
+ //#endregion
2316
+ //#region src/chat/attachments/store.ts
2317
+ const DAY_MS = 864e5;
2318
+ const ATTACHMENT_GC_BATCH_LIMIT = 200;
2319
+ /** Time that Junior keeps attachment bytes after the file is stored. */
2320
+ const ATTACHMENT_RETENTION_MS = 30 * DAY_MS;
2321
+ function attachmentDigest(file) {
2322
+ return createHash("sha256").update(file.data).digest("hex");
2323
+ }
2324
+ /** Stable conversation-owned id for one file body and presentation metadata. */
2325
+ function attachmentId(args) {
2326
+ const identity = [
2327
+ args.conversationId,
2328
+ args.storageProvider,
2329
+ args.sha256,
2330
+ args.filename,
2331
+ args.contentType
2332
+ ];
2333
+ if (args.source) identity.push(args.source.provider, args.source.id);
2334
+ return createHash("sha256").update(identity.join("\0")).digest("hex");
2335
+ }
2336
+ /**
2337
+ * Object key for one write attempt.
2338
+ *
2339
+ * Attachment ids are content-stable. Object keys are not: each put gets a fresh
2340
+ * key so a failed-write cleanup or GC delete cannot remove another writer's
2341
+ * durable blob.
2342
+ */
2343
+ function attachmentKey(args) {
2344
+ return `conversations/${args.conversationId}/attachments/${args.attachmentId}/${args.writeId}/${args.filename}`;
2345
+ }
2346
+ async function bestEffortDelete(storage, key) {
2347
+ try {
2348
+ await storage.delete([key]);
2349
+ } catch {}
2350
+ }
2351
+ async function isConversationPurged(db, conversationId) {
2352
+ const [row] = await db.db().select({ transcriptPurgedAt: juniorConversations.transcriptPurgedAt }).from(juniorConversations).where(eq(juniorConversations.conversationId, conversationId));
2353
+ return row?.transcriptPurgedAt != null;
2354
+ }
2355
+ function purgedConversationError(conversationId) {
2356
+ return /* @__PURE__ */ new Error(`Cannot store attachment for purged conversation ${conversationId}`);
2357
+ }
2358
+ async function assertConversationNotPurged(db, conversationId) {
2359
+ if (await isConversationPurged(db, conversationId)) throw purgedConversationError(conversationId);
2360
+ }
2361
+ /** Mark a just-written row for deletion and drop its unused object. */
2362
+ async function rejectPurgedAttachmentWrite(args) {
2363
+ await args.db.db().update(juniorAttachments).set({ deleteRequestedAt: args.now }).where(eq(juniorAttachments.id, args.id));
2364
+ await bestEffortDelete(args.storage, args.storageKey);
2365
+ throw purgedConversationError(args.conversationId);
2366
+ }
2367
+ /**
2368
+ * Return another writer's live row only when the conversation is still active.
2369
+ *
2370
+ * The caller already wrote a unique object for this attempt. Delete that unused
2371
+ * object either way. If the conversation is purged, do not report success for a
2372
+ * row that purge/GC is about to remove.
2373
+ */
2374
+ async function reuseLiveAttachment(args) {
2375
+ await bestEffortDelete(args.storage, args.storageKey);
2376
+ await assertConversationNotPurged(args.db, args.conversationId);
2377
+ return { id: args.id };
2378
+ }
2379
+ /**
2380
+ * Persist one conversation-owned file.
2381
+ *
2382
+ * Identity is content-stable under the conversation. Object storage is written
2383
+ * first under a unique key; the SQL row is created only after that write
2384
+ * succeeds. A row means the attachment is durable. Retries reuse an existing
2385
+ * live row. A purge-marked row is revived by writing a new object and pointing
2386
+ * the row at it, unless the conversation is still purged. If the SQL write
2387
+ * fails after object storage accepts the bytes, this path deletes that unique
2388
+ * object. A process crash between those steps can leave an unreferenced unique
2389
+ * object with no SQL row.
2390
+ */
2391
+ async function storeAttachment(args) {
2392
+ const now = new Date(args.nowMs ?? Date.now());
2393
+ const sha256 = attachmentDigest(args.file);
2394
+ const id = attachmentId({
2395
+ conversationId: args.conversationId,
2396
+ contentType: args.file.mimeType,
2397
+ filename: args.file.filename,
2398
+ source: args.source,
2399
+ storageProvider: args.storage.provider,
2400
+ sha256
2401
+ });
2402
+ const [existing] = await args.db.db().select({
2403
+ contentType: juniorAttachments.contentType,
2404
+ conversationId: juniorAttachments.conversationId,
2405
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
2406
+ filename: juniorAttachments.filename,
2407
+ id: juniorAttachments.id,
2408
+ provider: juniorAttachments.provider,
2409
+ providerId: juniorAttachments.providerId,
2410
+ storageProvider: juniorAttachments.storageProvider,
2411
+ sha256: juniorAttachments.sha256,
2412
+ storageKey: juniorAttachments.storageKey
2413
+ }).from(juniorAttachments).where(eq(juniorAttachments.id, id));
2414
+ if (existing) {
2415
+ if (existing.contentType !== args.file.mimeType || existing.conversationId !== args.conversationId || existing.filename !== args.file.filename || existing.provider !== (args.source?.provider ?? null) || existing.providerId !== (args.source?.id ?? null) || existing.storageProvider !== args.storage.provider || existing.sha256 !== sha256) throw new Error(`Attachment write conflicts with ${existing.id}`);
2416
+ if (existing.deleteRequestedAt === null) {
2417
+ await assertConversationNotPurged(args.db, args.conversationId);
2418
+ return { id: existing.id };
2419
+ }
2420
+ }
2421
+ await assertConversationNotPurged(args.db, args.conversationId);
2422
+ const storageKey = attachmentKey({
2423
+ attachmentId: id,
2424
+ conversationId: args.conversationId,
2425
+ filename: args.file.filename,
2426
+ writeId: randomUUID()
2427
+ });
2428
+ await args.storage.put({
2429
+ body: args.file.data,
2430
+ contentType: args.file.mimeType,
2431
+ key: storageKey
2432
+ });
2433
+ if (existing?.deleteRequestedAt != null) {
2434
+ if (await isConversationPurged(args.db, args.conversationId)) {
2435
+ await bestEffortDelete(args.storage, storageKey);
2436
+ throw purgedConversationError(args.conversationId);
2437
+ }
2438
+ const oldStorageKey = existing.storageKey;
2439
+ if ((await args.db.db().update(juniorAttachments).set({
2440
+ deleteRequestedAt: null,
2441
+ storageKey
2442
+ }).where(and(eq(juniorAttachments.id, id), isNotNull(juniorAttachments.deleteRequestedAt))).returning({ id: juniorAttachments.id })).length > 0) {
2443
+ if (await isConversationPurged(args.db, args.conversationId)) await rejectPurgedAttachmentWrite({
2444
+ conversationId: args.conversationId,
2445
+ db: args.db,
2446
+ id,
2447
+ now,
2448
+ storage: args.storage,
2449
+ storageKey
2450
+ });
2451
+ if (oldStorageKey !== storageKey) await bestEffortDelete(args.storage, oldStorageKey);
2452
+ return { id };
2453
+ }
2454
+ const [afterRace] = await args.db.db().select({
2455
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
2456
+ id: juniorAttachments.id,
2457
+ storageKey: juniorAttachments.storageKey
2458
+ }).from(juniorAttachments).where(eq(juniorAttachments.id, id));
2459
+ if (afterRace?.deleteRequestedAt === null) return reuseLiveAttachment({
2460
+ conversationId: args.conversationId,
2461
+ db: args.db,
2462
+ id: afterRace.id,
2463
+ storage: args.storage,
2464
+ storageKey
2465
+ });
2466
+ }
2467
+ let inserted = [];
2468
+ try {
2469
+ inserted = await args.db.db().insert(juniorAttachments).values({
2470
+ id,
2471
+ conversationId: args.conversationId,
2472
+ storageProvider: args.storage.provider,
2473
+ storageKey,
2474
+ provider: args.source?.provider,
2475
+ providerId: args.source?.id,
2476
+ filename: args.file.filename,
2477
+ contentType: args.file.mimeType,
2478
+ bytes: args.file.bytes,
2479
+ sha256,
2480
+ createdAt: now
2481
+ }).onConflictDoNothing({ target: juniorAttachments.id }).returning({ id: juniorAttachments.id });
2482
+ } catch (error) {
2483
+ await bestEffortDelete(args.storage, storageKey);
2484
+ throw error;
2485
+ }
2486
+ if (inserted.length > 0) {
2487
+ if (await isConversationPurged(args.db, args.conversationId)) await rejectPurgedAttachmentWrite({
2488
+ conversationId: args.conversationId,
2489
+ db: args.db,
2490
+ id,
2491
+ now,
2492
+ storage: args.storage,
2493
+ storageKey
2494
+ });
2495
+ return { id };
2496
+ }
2497
+ const [row] = await args.db.db().select({
2498
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
2499
+ id: juniorAttachments.id,
2500
+ storageKey: juniorAttachments.storageKey
2501
+ }).from(juniorAttachments).where(eq(juniorAttachments.id, id));
2502
+ if (!row) {
2503
+ await bestEffortDelete(args.storage, storageKey);
2504
+ throw new Error(`Attachment row missing after write for ${id}`);
2505
+ }
2506
+ if (row.deleteRequestedAt === null) return reuseLiveAttachment({
2507
+ conversationId: args.conversationId,
2508
+ db: args.db,
2509
+ id: row.id,
2510
+ storage: args.storage,
2511
+ storageKey
2512
+ });
2513
+ if (await isConversationPurged(args.db, args.conversationId)) {
2514
+ await bestEffortDelete(args.storage, storageKey);
2515
+ throw purgedConversationError(args.conversationId);
2516
+ }
2517
+ if ((await args.db.db().update(juniorAttachments).set({
2518
+ deleteRequestedAt: null,
2519
+ storageKey
2520
+ }).where(and(eq(juniorAttachments.id, id), isNotNull(juniorAttachments.deleteRequestedAt))).returning({ id: juniorAttachments.id })).length > 0) {
2521
+ if (await isConversationPurged(args.db, args.conversationId)) await rejectPurgedAttachmentWrite({
2522
+ conversationId: args.conversationId,
2523
+ db: args.db,
2524
+ id,
2525
+ now,
2526
+ storage: args.storage,
2527
+ storageKey
2528
+ });
2529
+ if (row.storageKey !== storageKey) await bestEffortDelete(args.storage, row.storageKey);
2530
+ return { id };
2531
+ }
2532
+ const [afterRace] = await args.db.db().select({
2533
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
2534
+ id: juniorAttachments.id
2535
+ }).from(juniorAttachments).where(eq(juniorAttachments.id, id));
2536
+ if (afterRace?.deleteRequestedAt === null) return reuseLiveAttachment({
2537
+ conversationId: args.conversationId,
2538
+ db: args.db,
2539
+ id: afterRace.id,
2540
+ storage: args.storage,
2541
+ storageKey
2542
+ });
2543
+ await bestEffortDelete(args.storage, storageKey);
2544
+ throw new Error(`Attachment ${id} disappeared while reviving`);
2545
+ }
2546
+ /** Load one live attachment owned by a conversation. */
2547
+ async function readLiveAttachment(args) {
2548
+ const [row] = await args.db.db().select({
2549
+ bytes: juniorAttachments.bytes,
2550
+ contentType: juniorAttachments.contentType,
2551
+ conversationId: juniorAttachments.conversationId,
2552
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
2553
+ filename: juniorAttachments.filename,
2554
+ id: juniorAttachments.id,
2555
+ storageProvider: juniorAttachments.storageProvider,
2556
+ storageKey: juniorAttachments.storageKey
2557
+ }).from(juniorAttachments).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
2558
+ if (!row) return null;
2559
+ if (await isConversationPurged(args.db, args.conversationId)) return null;
2560
+ return {
2561
+ bytes: row.bytes,
2562
+ contentType: row.contentType,
2563
+ conversationId: row.conversationId,
2564
+ filename: row.filename,
2565
+ id: row.id,
2566
+ storageKey: row.storageKey,
2567
+ storageProvider: row.storageProvider
2568
+ };
2569
+ }
2570
+ /** Persist many conversation-owned files. */
2571
+ async function storeAttachments(args) {
2572
+ const stored = [];
2573
+ for (const file of args.files) stored.push(await storeAttachment({
2574
+ conversationId: args.conversationId,
2575
+ db: args.db,
2576
+ file,
2577
+ ...args.nowMs !== void 0 ? { nowMs: args.nowMs } : void 0,
2578
+ storage: args.storage
2579
+ }));
2580
+ return stored;
2581
+ }
2582
+ /** Mark attachments unavailable when their conversation content is purged. */
2583
+ async function requestAttachmentDeletion(db, conversationIds, nowMs) {
2584
+ if (conversationIds.length === 0) return;
2585
+ await db.db().update(juniorAttachments).set({ deleteRequestedAt: new Date(nowMs) }).where(and(inArray(juniorAttachments.conversationId, conversationIds), isNull(juniorAttachments.deleteRequestedAt)));
2586
+ }
2587
+ /**
2588
+ * Delete purged attachments in one bounded batch.
2589
+ *
2590
+ * Eligible rows are 30 days old, explicitly purge-marked, or owned by a
2591
+ * conversation whose transcript is already purged. Delete object keys first,
2592
+ * then remove rows that are still eligible so a failed blob delete remains
2593
+ * retryable.
2594
+ */
2595
+ async function collectAttachmentGarbage(args) {
2596
+ const expiresBefore = /* @__PURE__ */ new Date(args.nowMs - ATTACHMENT_RETENTION_MS);
2597
+ const rows = await args.db.db().select({
2598
+ id: juniorAttachments.id,
2599
+ storageKey: juniorAttachments.storageKey
2600
+ }).from(juniorAttachments).innerJoin(juniorConversations, eq(juniorAttachments.conversationId, juniorConversations.conversationId)).where(and(eq(juniorAttachments.storageProvider, args.storage.provider), or(lte(juniorAttachments.createdAt, expiresBefore), isNotNull(juniorAttachments.deleteRequestedAt), isNotNull(juniorConversations.transcriptPurgedAt)))).orderBy(asc(juniorAttachments.createdAt), asc(juniorAttachments.id)).limit(args.limit ?? ATTACHMENT_GC_BATCH_LIMIT);
2601
+ if (rows.length === 0) return { deleted: 0 };
2602
+ await args.storage.delete(rows.map((row) => row.storageKey));
2603
+ const stillEligible = await args.db.db().select({ id: juniorAttachments.id }).from(juniorAttachments).innerJoin(juniorConversations, eq(juniorAttachments.conversationId, juniorConversations.conversationId)).where(and(inArray(juniorAttachments.id, rows.map((row) => row.id)), or(lte(juniorAttachments.createdAt, expiresBefore), isNotNull(juniorAttachments.deleteRequestedAt), isNotNull(juniorConversations.transcriptPurgedAt))));
2604
+ if (stillEligible.length === 0) return { deleted: 0 };
2605
+ return { deleted: (await args.db.db().delete(juniorAttachments).where(inArray(juniorAttachments.id, stillEligible.map((row) => row.id))).returning({ id: juniorAttachments.id })).length };
2606
+ }
2607
+ //#endregion
2608
+ //#region src/stats.ts
2609
+ function utcDate(nowMs) {
2610
+ return new Date(nowMs).toISOString().slice(0, 10);
2611
+ }
2612
+ /** Increment one daily named counter. */
2613
+ async function incrementStat(key, options = {}) {
2614
+ const date = utcDate(options.nowMs ?? Date.now());
2615
+ await getDb().insert(juniorStats).values({
2616
+ ...key,
2617
+ date,
2618
+ count: 1
2619
+ }).onConflictDoUpdate({
2620
+ target: [
2621
+ juniorStats.date,
2622
+ juniorStats.namespace,
2623
+ juniorStats.metric,
2624
+ juniorStats.name
2625
+ ],
2626
+ set: { count: sql`${juniorStats.count} + 1` }
2627
+ });
2628
+ }
2629
+ /** Read daily counters inside an inclusive UTC date range. */
2630
+ async function readStats(start, end) {
2631
+ return await getDb().select().from(juniorStats).where(and(gte(juniorStats.date, start), lte(juniorStats.date, end))).orderBy(asc(juniorStats.date), asc(juniorStats.namespace), asc(juniorStats.metric), asc(juniorStats.name));
2632
+ }
2633
+ //#endregion
2634
+ export { workspaceSnapshotWatch as $, eventTaskTriggerAvailable as A, releaseConversationWork as At, deleteEventTask as B, isInvalidConversationRecordError as Bt, SCHEDULED_TASK_SYSTEM_ACTOR as C, ensureConversationWake as Ct, eventTaskMatchesDestination as D, listActiveConversationIds as Dt, eventTaskListToolResultSchema as E, hasRunnableConversationWork as Et, effectiveTaskOutcomes as F, stopConversationWork as Ft, listEventTasksCreatedBy as G, buildDeterministicTurnId as Gt, findMatchingEventTasks as H, juniorToolOutputSchema as Ht, resolveTaskOutcomes as I, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS as It, saveEventTask as J, listEventTasksForTeam as K, taskOutcomeInputSchema as L, CONVERSATION_WORK_STALE_ENQUEUE_MS as Lt, requireEventTaskSlackContext as M, requestAnotherSlice as Mt, requireSupportedEventTaskTrigger as N, requestConversationWork as Nt, eventTaskSuccess as O, recordAttemptFailure as Ot, writableEventTask as P, startConversationWork as Pt, workspaceSnapshotFinishedEvent as Q, collectEventTaskMatchKeys as R, hasRunnableConversationWork$1 as Rt, withScheduledTaskLock as S, drainConversationMailbox as St, compactEventTask as T, getConversationWorkState as Tt, getEventTask as U, makeStructuredToolOutput as Ut, eventTaskBelongsToUser as V, injectContinuationToolName as Vt, listDeletedEventTasksCreatedBy as W, buildDeterministicAssistantMessageId as Wt, WORKSPACE_SNAPSHOT_FAILED_EVENT as X, getResourceEventCatalog as Y, WORKSPACE_SNAPSHOT_READY_EVENT as Z, parseScheduledTaskRow as _, clearExpiredConversationLease as _t, requestAttachmentDeletion as a, registeredEventTypeSchema as at, saveScheduledTaskInLock as b, countPendingConversationMessages as bt, parseSlackThreadId as c, requireSupportedResourceEventMatch as ct, resolveSlackChannelIdFromThreadId as d, appendAndEnqueueExclusiveInboundMessage as dt, eventNamespaceSchema as et, createScheduledTask as f, appendAndEnqueueInboundMessage as ft, listScheduledTasksForTeam as g, clearConsumedConversationWake as gt, listScheduledTasks as h, checkInConversationWork as ht, readLiveAttachment as i, pluginSupportsEvent as it, registeredEventTaskTriggerSchema as j, removeActiveConversation as jt, eventTaskToolResultSchema as k, recordConversationRetry as kt, readSlackRawMessageContext as l, resourceEventGuidance as lt, listPublicScheduledTasksForTeams as m, cancelHumanFacingPendingMessages as mt, readStats as n, hasResourceEventCatalogEntries as nt, storeAttachment as o, registeredResourceEventMatchSchema as ot, isListedScheduledTask as p, beginConversationResume as pt, listPublicEventTasksForTeams as q, collectAttachmentGarbage as r, normalizeEventIdentifier as rt, storeAttachments as s, registeredResourceTypeSchema as st, incrementStat as t, hasPluginResourceEventCatalogEntries as tt, resolveSlackChannelIdFromMessage as u, ackMessages as ut, readScheduledTask as v, completeConversationStop as vt, scheduledRunSchema as w, getConversation as wt, scheduledRunId as x, deadLetterAttempt as xt, saveScheduledTask as y, completeConversationWork as yt, createEventTask as z, isFinalAttempt as zt };