@sentry/junior 0.265.0 → 0.267.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 (76) hide show
  1. package/dist/{actor-tlxzqaRe.js → actor-DZSokpwj.js} +8 -1
  2. package/dist/{adapter-cUkPH3Om.js → adapter-BybMEezj.js} +2 -2
  3. package/dist/{admin-Cf0rDmWV.js → admin-DOs1_EiW.js} +1 -1
  4. package/dist/{agent-runner-C6MwHJEs.js → agent-runner-DgNATN4D.js} +874 -228
  5. package/dist/api/schema.d.ts +202 -23
  6. package/dist/api/schema.js +6 -5
  7. package/dist/api.d.ts +6 -1
  8. package/dist/api.js +130 -1205
  9. package/dist/{app-Csbi9dW7.d.ts → app-DAB3fzyp.d.ts} +21 -3
  10. package/dist/app.d.ts +2 -2
  11. package/dist/app.js +1611 -353
  12. package/dist/{assistant-reply-UPA8vKlC.js → assistant-reply-CT7VcXQe.js} +1 -1
  13. package/dist/{automation-2RbKzpMT.js → automation-CmeFBSEh.js} +42 -14
  14. package/dist/{briefs-DRjx8kQ9.js → briefs-CWKjka7v.js} +5 -5
  15. package/dist/{catalog-runtime-D3qbJlYz.js → catalog-runtime-cMlAI5LY.js} +1 -1
  16. package/dist/cli/chat.js +15 -15
  17. package/dist/cli/check.js +3 -3
  18. package/dist/cli/init.js +0 -1
  19. package/dist/cli/main.js +3 -3
  20. package/dist/cli/plugins.js +4 -4
  21. package/dist/cli/snapshot-create.js +4 -4
  22. package/dist/cli/upgrade.d.ts +1 -1
  23. package/dist/cli/upgrade.js +3 -3
  24. package/dist/{client-DX34VNtH.js → client-BjgbMawj.js} +2 -2
  25. package/dist/{config-BayQ9hHH.js → config-CdB6yJj5.js} +32 -10
  26. package/dist/{connections-mpN5Lsnj.js → connections-RE_65jzK.js} +13 -12
  27. package/dist/{conversation-B0baEwoA.js → conversation-DSo310Ps.js} +14 -3
  28. package/dist/{conversation-privacy-frjQCJa9.js → conversation-privacy-BzgJC5k0.js} +1 -1
  29. package/dist/{credential-sync-CZmyDNdR.js → credential-sync-DhEGEpgq.js} +15 -15
  30. package/dist/{credentialed-C6pKlB1p.js → credentialed-CIGwoLU2.js} +2913 -1026
  31. package/dist/{db-BnRqQ7G5.d.ts → db-Bg-zC3r5.d.ts} +1 -1
  32. package/dist/{executor-CAr7xQr2.js → executor-VYrx_K4w.js} +7 -4
  33. package/dist/{history-AY8rs-FR.js → history-C84mlAwO.js} +2 -1
  34. package/dist/{images-B2yA3HWE.js → images-CecjXAAz.js} +3 -3
  35. package/dist/instrumentation.js +6 -2
  36. package/dist/{job-queue-Cjo6Yg_b.js → job-queue-C0BZSa7r.js} +128 -3
  37. package/dist/{web-input-Y6aXvmfa.js → list-1XQP-o6K.js} +1374 -71
  38. package/dist/{logging-D2QBriUo.js → logging-DuZ1A4ab.js} +11 -3
  39. package/dist/{message-C9ymnpBS.js → message-NKSbbnA6.js} +4 -4
  40. package/dist/nitro.d.ts +1 -1
  41. package/dist/nitro.js +3 -3
  42. package/dist/{oauth-callback-server-BtMf3TPZ.js → oauth-callback-server-qAHLPtml.js} +2 -2
  43. package/dist/{prompt-CvaDdSX3.js → prompt-BW9jFsqZ.js} +1 -1
  44. package/dist/{registry-COTeZ2cV.js → registry-FQutO926.js} +2 -2
  45. package/dist/{resolve-g9PKz9pH.js → resolve-Bk4yeuHy.js} +3 -3
  46. package/dist/{result-COnC42GR.js → result-B9e9ATOU.js} +9 -9
  47. package/dist/{runner-BnTNS7x_.js → runner-C3S2ObH-.js} +11 -11
  48. package/dist/{sandbox-egress-signals-C_IIdLFk.js → sandbox-egress-signals-DqpGzzMB.js} +1 -1
  49. package/dist/{schema-D5Amlzkp.js → schema-X7Fj4fv1.js} +3 -2
  50. package/dist/{service-account-CwvToidT.js → service-account-BQasl-qq.js} +3 -3
  51. package/dist/{service-account-YdYftwV2.js → service-account-Do30HUNv.js} +3 -3
  52. package/dist/{session-stop-CpCUy1ZJ.js → session-stop-DkOjt46_.js} +45 -128
  53. package/dist/{skills-Bb2KaJ3m.js → skills-CQHrwfOa.js} +2 -2
  54. package/dist/{spawn-mbTcrLb3.js → spawn-BHuqN14f.js} +2 -2
  55. package/dist/{store-CqRnaZSq.js → store-6EetVP-m.js} +3 -3
  56. package/dist/{store-CG0VJyye.d.ts → store-B8CABbmR.d.ts} +10 -2
  57. package/dist/store-DvNZIsCr.js +295 -0
  58. package/dist/{task-queue-8XjDIORF.js → task-queue-B0DLgd2P.js} +10 -5
  59. package/dist/{task-runner-6mLmG6g_.js → task-runner-B-PD_JOj.js} +26 -14
  60. package/dist/{tasks-DGbrur6x.js → tasks-0ilmuCux.js} +1440 -46
  61. package/dist/{thread-state-D45lz-oC.js → thread-state-DRmdJ2QN.js} +5 -5
  62. package/dist/{turn-failure-response-CaEPLSBn.js → turn-failure-response-G11cs2gE.js} +9 -9
  63. package/dist/{turn-lifecycle-C35IyRGp.js → turn-lifecycle-OzLIwvi1.js} +2 -2
  64. package/dist/{turn-session-routing-BKx6rfVd.js → turn-session-routing-Dyut9l7Y.js} +1 -1
  65. package/dist/user-input-request-BDFBdF8r.js +116 -0
  66. package/dist/{validation-C0X3CX51.js → validation-CxEmt1id.js} +1 -1
  67. package/dist/{version-BPHgJwnm.js → version-Bapedn4A.js} +1 -1
  68. package/dist/version.js +1 -1
  69. package/dist/{viewer-B2wXNspU.js → viewer-DrN3oC2X.js} +22 -11
  70. package/dist/{work-DMbqK0kT.js → work-DUQoNcqP.js} +52 -51
  71. package/dist/{worker-B-rDJ3XN.js → worker-D1l4Ugk0.js} +30 -19
  72. package/migrations/0064_any_resource_event_triggers.sql +2 -0
  73. package/migrations/meta/0064_snapshot.json +3505 -0
  74. package/migrations/meta/_journal.json +7 -0
  75. package/package.json +20 -5
  76. package/dist/store-DdT74Lvq.js +0 -1424
@@ -1,1424 +0,0 @@
1
- import { i as getChatConfig } from "./config-BayQ9hHH.js";
2
- import { B as toOptionalNumber, V as toOptionalString, z as isRecord } from "./logging-D2QBriUo.js";
3
- import { s as parseStoredSlackActor } from "./actor-tlxzqaRe.js";
4
- import { V as sameDestination, d as getConversationStore, z as parseDestination } from "./viewer-B2wXNspU.js";
5
- import { ln as JUNIOR_THREAD_STATE_TTL_MS } from "./credentialed-C6pKlB1p.js";
6
- import { n as getDefaultRedisStateAdapterFor, r as getStateAdapter } from "./adapter-cUkPH3Om.js";
7
- import "./job-queue-Cjo6Yg_b.js";
8
- import { z } from "zod";
9
- import { randomUUID } from "node:crypto";
10
- import { destinationSchema } from "@sentry/junior-plugin-api";
11
- // src/chat/task-execution/state.ts
12
- /**
13
- * Expiring conversation mailbox and execution index store.
14
- *
15
- * The conversation record owns pending inbound work, execution status, and the
16
- * lease. `conversation:active` feeds heartbeat recovery and contains every
17
- * non-idle conversation until the lease/status path makes it idle.
18
- * `conversation:by-activity` supports legacy SQL import only; SQL owns durable
19
- * conversation metadata in configured runtime.
20
- */
21
- const CONVERSATION_PREFIX = "junior:conversation:v2";
22
- const CONVERSATION_SCHEMA_VERSION = 2;
23
- const CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH = 1e4;
24
- const CONVERSATION_INDEX_LOCK_TTL_MS = 1e4;
25
- const CONVERSATION_INDEX_LOCK_WAIT_MS = 2e3;
26
- const CONVERSATION_INDEX_LOCK_RETRY_MS = 25;
27
- const CONVERSATION_MUTATION_LOCK_TTL_MS = 1e4;
28
- const CONVERSATION_MUTATION_WAIT_MS = 1e4;
29
- const CONVERSATION_MUTATION_RETRY_MS = 25;
30
- var InvalidConversationRecordError = class extends Error {
31
- constructor(conversationId) {
32
- super(`Conversation record is invalid for ${conversationId}`);
33
- this.name = "InvalidConversationRecordError";
34
- }
35
- };
36
- /** Return whether an error means a stored conversation record failed validation. */
37
- function isInvalidConversationRecordError(error) {
38
- return error instanceof InvalidConversationRecordError;
39
- }
40
- var ConversationMutationFencedError = class extends Error {
41
- constructor(conversationId) {
42
- super(`Conversation mutation lock was lost before write for ${conversationId}`);
43
- this.name = "ConversationMutationFencedError";
44
- }
45
- };
46
- const CONVERSATION_BY_ACTIVITY_INDEX_KEY = `${CONVERSATION_PREFIX}:by-activity`;
47
- const CONVERSATION_ACTIVE_INDEX_KEY = `${CONVERSATION_PREFIX}:active`;
48
- const CONVERSATION_WORK_LEASE_TTL_MS = 9e4;
49
- const CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15e3;
50
- const CONVERSATION_WORK_STALE_ENQUEUE_MS = 6e4;
51
- const inboundMessageSourceSchema = z.enum([
52
- "api",
53
- "internal",
54
- "local",
55
- "plugin",
56
- "event",
57
- "scheduler",
58
- "slack",
59
- "web"
60
- ]);
61
- const agentInputSchema = z.object({
62
- attachments: z.array(z.unknown()).optional(),
63
- authorId: z.string().min(1).optional(),
64
- metadata: z.record(z.string(), z.unknown()).optional(),
65
- text: z.string()
66
- }).strict().refine((input) => input.text.trim() || input.attachments?.length, { message: "agent input requires text or attachments" });
67
- /** Durable delivery modes for pending inbound mailbox work. */
68
- const inboundMessageDeliverySchema = z.enum(["defer", "interrupt"]);
69
- /** Sentry trace propagation headers captured when an inbound message arrives. */
70
- const inboundMessageTraceContextSchema = z.object({
71
- baggage: z.string().optional(),
72
- sentryTrace: z.string().min(1)
73
- }).optional().catch(void 0);
74
- /** Redis mailbox shape kept for deployed workers that still require this field. */
75
- const storedInboundMessageSchema = z.object({
76
- attemptCount: z.number().finite().optional(),
77
- conversationId: z.string().refine((value) => value.trim().length > 0),
78
- createdAtMs: z.number().finite(),
79
- delivery: inboundMessageDeliverySchema,
80
- destination: destinationSchema.optional(),
81
- inboundMessageId: z.string().refine((value) => value.trim().length > 0),
82
- injectedAtMs: z.number().finite().optional(),
83
- input: agentInputSchema,
84
- receivedAtMs: z.number().finite(),
85
- source: inboundMessageSourceSchema,
86
- traceContext: inboundMessageTraceContextSchema
87
- }).strict().extend({ publishExternally: z.boolean() });
88
- function conversationKey(conversationId) {
89
- return `${CONVERSATION_PREFIX}:${conversationId}`;
90
- }
91
- function conversationStopKey(conversationId) {
92
- return `${CONVERSATION_PREFIX}:stop:${conversationId}`;
93
- }
94
- function indexLockKey(indexKey) {
95
- return `${indexKey}:lock`;
96
- }
97
- function mutationLockKey(conversationId) {
98
- return `${CONVERSATION_PREFIX}:mutation:${conversationId}`;
99
- }
100
- function now$1() {
101
- return Date.now();
102
- }
103
- function compareMessages(left, right) {
104
- return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
105
- }
106
- function inputHasAttachments(input) {
107
- return Array.isArray(input.attachments) && input.attachments.length > 0;
108
- }
109
- /**
110
- * Upgrade a still-pending payload when a duplicate source event carries file
111
- * attachments the stored copy lacks (Slack `app_mention`/`message` twins),
112
- * keeping the stored ordering timestamps and delivery attempts.
113
- */
114
- function upgradedPendingMessage(stored, duplicate) {
115
- const promotesInterrupt = stored.delivery === "defer" && duplicate.delivery === "interrupt";
116
- const delivery = promotesInterrupt ? "interrupt" : stored.delivery;
117
- const input = promotesInterrupt ? {
118
- ...duplicate.input,
119
- ...inputHasAttachments(duplicate.input) || !inputHasAttachments(stored.input) ? void 0 : { attachments: stored.input.attachments }
120
- } : inputHasAttachments(duplicate.input) && !inputHasAttachments(stored.input) ? duplicate.input : stored.input;
121
- if (input === stored.input && delivery === stored.delivery) return stored;
122
- return {
123
- ...stored,
124
- delivery,
125
- input
126
- };
127
- }
128
- function compareIndexDescending(left, right) {
129
- return right.score - left.score || right.conversationId.localeCompare(left.conversationId);
130
- }
131
- function compareIndexAscending(left, right) {
132
- return left.score - right.score || left.conversationId.localeCompare(right.conversationId);
133
- }
134
- function uniqueStrings(values) {
135
- return [...new Set(values)];
136
- }
137
- function normalizeSource(value) {
138
- const parsed = inboundMessageSourceSchema.safeParse(value);
139
- return parsed.success ? parsed.data : void 0;
140
- }
141
- function normalizeExecutionStatus(value) {
142
- if (value === "paused") return value;
143
- if (value === "failed" || value === "idle" || value === "pending" || value === "running") return value;
144
- }
145
- function normalizeMessage(value) {
146
- const parsed = storedInboundMessageSchema.safeParse(value);
147
- if (!parsed.success) return;
148
- const { publishExternally: _publishExternally, traceContext, ...message } = parsed.data;
149
- return traceContext ? {
150
- ...message,
151
- traceContext
152
- } : message;
153
- }
154
- function storeMessage(message, publishExternally) {
155
- return {
156
- ...message,
157
- publishExternally
158
- };
159
- }
160
- /** Whether this is the final attempt before an unacked message is dead-lettered. */
161
- function isFinalAttempt(message) {
162
- return (message.attemptCount ?? 0) >= 4;
163
- }
164
- function normalizeActor(value) {
165
- return parseStoredSlackActor(value);
166
- }
167
- function normalizeLease(value) {
168
- if (!isRecord(value)) return;
169
- const token = toOptionalString(value.token);
170
- const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
171
- const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
172
- const expiresAtMs = toOptionalNumber(value.expiresAtMs);
173
- if (!token || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof expiresAtMs !== "number") return;
174
- return {
175
- token,
176
- acquiredAtMs,
177
- lastCheckInAtMs,
178
- expiresAtMs
179
- };
180
- }
181
- function normalizeStop(value) {
182
- if (!isRecord(value)) return;
183
- const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(value.inboundMessageIds.filter((id) => typeof id === "string")) : [];
184
- const runId = toOptionalString(value.runId);
185
- return runId ? {
186
- inboundMessageIds,
187
- runId
188
- } : void 0;
189
- }
190
- /** Decode execution state and repair idle records that still own work. */
191
- function normalizeExecution(conversationId, value) {
192
- if (!isRecord(value)) return;
193
- const status = normalizeExecutionStatus(value.status);
194
- if (!status) return;
195
- const pendingMessages = [];
196
- if (Array.isArray(value.pendingMessages)) {
197
- for (const rawMessage of value.pendingMessages) {
198
- const message = normalizeMessage(rawMessage);
199
- if (!message || message.conversationId !== conversationId) return;
200
- if (message.injectedAtMs !== void 0) continue;
201
- pendingMessages.push(message);
202
- }
203
- pendingMessages.sort(compareMessages);
204
- }
205
- const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(value.inboundMessageIds.map((id) => typeof id === "string" ? id : void 0).filter((id) => Boolean(id))) : [];
206
- const lease = normalizeLease(value.lease);
207
- const runId = toOptionalString(value.runId);
208
- return {
209
- status: status === "idle" && lease ? "running" : status === "idle" && pendingMessages.length > 0 ? "pending" : status,
210
- inboundMessageIds: uniqueStrings([...inboundMessageIds, ...pendingMessages.map((message) => message.inboundMessageId)]),
211
- pendingCount: pendingMessages.length,
212
- pendingMessages,
213
- lease,
214
- lastCheckpointAtMs: toOptionalNumber(value.lastCheckpointAtMs),
215
- lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
216
- lastProgressAtMs: toOptionalNumber(value.lastProgressAtMs),
217
- retryCount: toOptionalNumber(value.retryCount),
218
- runId,
219
- updatedAtMs: toOptionalNumber(value.updatedAtMs)
220
- };
221
- }
222
- /** Decode current conversation records. */
223
- function normalizeConversation(conversationId, value) {
224
- if (!isRecord(value) || value.schemaVersion !== CONVERSATION_SCHEMA_VERSION) return;
225
- const storedConversationId = toOptionalString(value.conversationId);
226
- const createdAtMs = toOptionalNumber(value.createdAtMs);
227
- const lastActivityAtMs = toOptionalNumber(value.lastActivityAtMs);
228
- const updatedAtMs = toOptionalNumber(value.updatedAtMs);
229
- const execution = normalizeExecution(conversationId, value.execution);
230
- const destination = value.destination === void 0 ? void 0 : parseDestination(value.destination);
231
- if (storedConversationId !== conversationId || typeof createdAtMs !== "number" || typeof lastActivityAtMs !== "number" || typeof updatedAtMs !== "number" || !execution || value.destination !== void 0 && !destination) return;
232
- if (execution.pendingMessages.length > 0 && execution.pendingMessages.some((message) => {
233
- if (!message.destination) return false;
234
- return !destination || !sameDestination(message.destination, destination);
235
- })) return;
236
- return {
237
- schemaVersion: CONVERSATION_SCHEMA_VERSION,
238
- conversationId,
239
- createdAtMs,
240
- lastActivityAtMs,
241
- updatedAtMs,
242
- execution,
243
- ...destination ? { destination } : void 0,
244
- ...toOptionalString(value.title) ? { title: toOptionalString(value.title) } : void 0,
245
- ...toOptionalString(value.channelName) ? { channelName: toOptionalString(value.channelName) } : void 0,
246
- ...normalizeActor(value.actor) ? { actor: normalizeActor(value.actor) } : void 0,
247
- ...normalizeSource(value.source) ? { source: normalizeSource(value.source) } : void 0
248
- };
249
- }
250
- function emptyConversation(args) {
251
- return {
252
- schemaVersion: CONVERSATION_SCHEMA_VERSION,
253
- conversationId: args.conversationId,
254
- createdAtMs: args.nowMs,
255
- lastActivityAtMs: args.nowMs,
256
- updatedAtMs: args.nowMs,
257
- ...args.destination ? { destination: args.destination } : void 0,
258
- ...args.source ? { source: args.source } : void 0,
259
- execution: {
260
- status: "idle",
261
- inboundMessageIds: [],
262
- pendingCount: 0,
263
- pendingMessages: [],
264
- updatedAtMs: args.nowMs
265
- }
266
- };
267
- }
268
- function isLeaseActive(lease, nowMs) {
269
- return Boolean(lease && lease.expiresAtMs > nowMs);
270
- }
271
- function pendingMessages(conversation) {
272
- return [...conversation.execution.pendingMessages].sort(compareMessages);
273
- }
274
- function isRunnableStatus(status) {
275
- return status !== "failed" && status !== "idle";
276
- }
277
- function hasRunnableWork(conversation) {
278
- return isRunnableStatus(conversation.execution.status) || pendingMessages(conversation).length > 0;
279
- }
280
- function executionWithPendingMessages(execution, pending) {
281
- const pendingMessages = [...pending].sort(compareMessages);
282
- const status = execution.status === "idle" && execution.lease ? "running" : execution.status === "idle" && pendingMessages.length > 0 ? "pending" : execution.status;
283
- return {
284
- ...execution,
285
- status,
286
- inboundMessageIds: uniqueStrings([...execution.inboundMessageIds, ...pendingMessages.map((message) => message.inboundMessageId)]),
287
- pendingMessages,
288
- pendingCount: pendingMessages.length
289
- };
290
- }
291
- function withExecutionUpdate(conversation, execution, nowMs) {
292
- return {
293
- ...conversation,
294
- updatedAtMs: nowMs,
295
- execution: {
296
- ...executionWithPendingMessages(execution, execution.pendingMessages),
297
- updatedAtMs: nowMs
298
- }
299
- };
300
- }
301
- async function getConnectedState(stateAdapter) {
302
- const state = stateAdapter ?? getStateAdapter();
303
- await state.connect();
304
- return state;
305
- }
306
- async function sleep(ms) {
307
- await new Promise((resolve) => {
308
- setTimeout(resolve, ms).unref?.();
309
- });
310
- }
311
- async function withIndexLock(state, indexKey, callback) {
312
- const startedAtMs = now$1();
313
- let lock;
314
- while (true) {
315
- lock = await state.acquireLock(indexLockKey(indexKey), CONVERSATION_INDEX_LOCK_TTL_MS);
316
- if (lock) break;
317
- if (now$1() - startedAtMs >= CONVERSATION_INDEX_LOCK_WAIT_MS) throw new Error(`Could not acquire conversation index lock for ${indexKey}`);
318
- await sleep(CONVERSATION_INDEX_LOCK_RETRY_MS);
319
- }
320
- try {
321
- return await callback();
322
- } finally {
323
- await state.releaseLock(lock);
324
- }
325
- }
326
- function normalizeIndexEntry(value) {
327
- if (!isRecord(value)) return;
328
- const conversationId = toOptionalString(value.conversationId);
329
- const score = toOptionalNumber(value.score);
330
- if (!conversationId || typeof score !== "number") return;
331
- return {
332
- conversationId,
333
- score
334
- };
335
- }
336
- function uniqueIndexEntries(value) {
337
- if (!Array.isArray(value)) return [];
338
- const entries = /* @__PURE__ */ new Map();
339
- for (const item of value) {
340
- const entry = normalizeIndexEntry(item);
341
- if (!entry) continue;
342
- const existing = entries.get(entry.conversationId);
343
- if (!existing || entry.score > existing.score) entries.set(entry.conversationId, entry);
344
- }
345
- return [...entries.values()];
346
- }
347
- function retainedIndexEntries(indexKey, entries) {
348
- if (indexKey === "junior:conversation:v2:by-activity") return entries.sort(compareIndexDescending).slice(0, CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH);
349
- if (indexKey === "junior:conversation:v2:active") return entries.sort(compareIndexAscending);
350
- throw new Error(`Unknown conversation index ${indexKey}`);
351
- }
352
- function redisIndexKey(indexKey) {
353
- const prefix = getChatConfig().state.keyPrefix;
354
- return [...prefix ? [prefix] : [], indexKey].join(":");
355
- }
356
- function parseRedisIndexEntries(values) {
357
- if (!Array.isArray(values)) return [];
358
- const entries = [];
359
- for (let index = 0; index < values.length; index += 2) {
360
- const conversationId = toOptionalString(values[index]);
361
- const score = typeof values[index + 1] === "number" ? values[index + 1] : Number(values[index + 1]);
362
- if (!conversationId || !Number.isFinite(score)) continue;
363
- entries.push({
364
- conversationId,
365
- score
366
- });
367
- }
368
- return entries;
369
- }
370
- function redisConversationIndexStore(client) {
371
- const upsertBoundedActivityScript = `
372
- redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
373
- redis.call("PEXPIRE", KEYS[1], ARGV[3])
374
- local extra = redis.call("ZCARD", KEYS[1]) - tonumber(ARGV[4])
375
- if extra > 0 then
376
- redis.call("ZREMRANGEBYRANK", KEYS[1], 0, extra - 1)
377
- end
378
- return 1
379
- `;
380
- return {
381
- async list(args) {
382
- const key = redisIndexKey(args.indexKey);
383
- const limit = args.limit;
384
- const offset = Math.max(0, args.offset ?? 0);
385
- if (limit === 0) return [];
386
- return parseRedisIndexEntries(args.scoreMax !== void 0 ? await client.sendCommand([
387
- "ZRANGEBYSCORE",
388
- key,
389
- "-inf",
390
- String(args.scoreMax),
391
- "WITHSCORES",
392
- ...limit !== void 0 || offset > 0 ? [
393
- "LIMIT",
394
- String(offset),
395
- String(limit ?? 1e9)
396
- ] : []
397
- ]) : await client.sendCommand([
398
- args.order === "asc" ? "ZRANGE" : "ZREVRANGE",
399
- key,
400
- String(offset),
401
- String(limit === void 0 ? -1 : offset + Math.max(0, limit - 1)),
402
- "WITHSCORES"
403
- ]));
404
- },
405
- async remove(args) {
406
- await client.sendCommand([
407
- "ZREM",
408
- redisIndexKey(args.indexKey),
409
- args.conversationId
410
- ]);
411
- },
412
- async upsert(args) {
413
- const key = redisIndexKey(args.indexKey);
414
- if (args.indexKey === "junior:conversation:v2:by-activity") {
415
- await client.sendCommand([
416
- "EVAL",
417
- upsertBoundedActivityScript,
418
- "1",
419
- key,
420
- String(args.score),
421
- args.conversationId,
422
- String(JUNIOR_THREAD_STATE_TTL_MS),
423
- String(CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH)
424
- ]);
425
- return;
426
- }
427
- if (args.indexKey === "junior:conversation:v2:active") {
428
- await client.sendCommand([
429
- "ZADD",
430
- key,
431
- String(args.score),
432
- args.conversationId
433
- ]);
434
- await client.sendCommand([
435
- "PEXPIRE",
436
- key,
437
- String(JUNIOR_THREAD_STATE_TTL_MS)
438
- ]);
439
- return;
440
- }
441
- throw new Error(`Unknown conversation index ${args.indexKey}`);
442
- }
443
- };
444
- }
445
- function emulatedConversationIndexStore(state) {
446
- const readIndex = async (indexKey) => uniqueIndexEntries(await state.get(indexKey));
447
- const writeIndex = async (indexKey, entries) => {
448
- await state.set(indexKey, entries, JUNIOR_THREAD_STATE_TTL_MS);
449
- };
450
- return {
451
- async list(args) {
452
- const entries = (await readIndex(args.indexKey)).filter((entry) => args.scoreMax === void 0 ? true : entry.score <= args.scoreMax).sort(args.order === "asc" ? compareIndexAscending : compareIndexDescending);
453
- const offset = Math.max(0, args.offset ?? 0);
454
- return entries.slice(offset, args.limit === void 0 ? entries.length : offset + args.limit);
455
- },
456
- async remove(args) {
457
- await withIndexLock(state, args.indexKey, async () => {
458
- const entries = await readIndex(args.indexKey);
459
- const next = entries.filter((entry) => entry.conversationId !== args.conversationId);
460
- if (next.length === entries.length) return;
461
- await writeIndex(args.indexKey, next);
462
- });
463
- },
464
- async upsert(args) {
465
- await withIndexLock(state, args.indexKey, async () => {
466
- const withoutCurrent = (await readIndex(args.indexKey)).filter((entry) => entry.conversationId !== args.conversationId);
467
- const next = retainedIndexEntries(args.indexKey, [...withoutCurrent, {
468
- conversationId: args.conversationId,
469
- score: args.score
470
- }]);
471
- await writeIndex(args.indexKey, next);
472
- });
473
- }
474
- };
475
- }
476
- async function getConversationIndexStore(state) {
477
- const redisStateAdapter = await getDefaultRedisStateAdapterFor(state);
478
- if (redisStateAdapter) return redisConversationIndexStore(redisStateAdapter.getClient());
479
- return emulatedConversationIndexStore(state);
480
- }
481
- async function upsertIndexEntry(args) {
482
- await (await getConversationIndexStore(args.state)).upsert({
483
- conversationId: args.conversationId,
484
- indexKey: args.indexKey,
485
- score: args.score
486
- });
487
- }
488
- async function removeIndexEntry(args) {
489
- await (await getConversationIndexStore(args.state)).remove({
490
- conversationId: args.conversationId,
491
- indexKey: args.indexKey
492
- });
493
- }
494
- async function acquireMutationLock(state, conversationId) {
495
- const startedAtMs = now$1();
496
- while (true) {
497
- const lock = await state.acquireLock(mutationLockKey(conversationId), CONVERSATION_MUTATION_LOCK_TTL_MS);
498
- if (lock) return lock;
499
- if (now$1() - startedAtMs >= CONVERSATION_MUTATION_WAIT_MS) throw new Error(`Could not acquire conversation mutation lock for ${conversationId}`);
500
- await sleep(CONVERSATION_MUTATION_RETRY_MS);
501
- }
502
- }
503
- async function withConversationMutation(args, callback) {
504
- const state = await getConnectedState(args.state);
505
- const lock = await acquireMutationLock(state, args.conversationId);
506
- try {
507
- return await callback(state, lock);
508
- } finally {
509
- await state.releaseLock(lock);
510
- }
511
- }
512
- async function readConversation(state, conversationId) {
513
- const raw = await state.get(conversationKey(conversationId));
514
- if (raw == null) return;
515
- const conversation = normalizeConversation(conversationId, raw);
516
- if (!conversation) throw new InvalidConversationRecordError(conversationId);
517
- return conversation;
518
- }
519
- /**
520
- * Persist a conversation and refresh its reporting and active-recovery indexes.
521
- *
522
- * Conversation writes are lock-serialized blind whole-record writes, so the
523
- * mutation lock is re-validated (and extended) immediately before the write:
524
- * a stalled mutator that lost its lock must fail loudly instead of clobbering
525
- * a newer record with a stale copy.
526
- */
527
- async function writeConversation(state, lock, conversation) {
528
- const execution = executionWithPendingMessages(conversation.execution, conversation.execution.pendingMessages);
529
- const next = {
530
- ...conversation,
531
- execution
532
- };
533
- await fenceConversationMutation(state, lock, next.conversationId);
534
- const hasSlackDestination = next.destination?.platform === "slack";
535
- const stored = {
536
- ...next,
537
- execution: {
538
- ...next.execution,
539
- pendingMessages: next.execution.pendingMessages.map((message) => storeMessage(message, hasSlackDestination && (message.source === "slack" || message.source === "event" || message.source === "plugin" || message.source === "scheduler")))
540
- }
541
- };
542
- await state.set(conversationKey(next.conversationId), stored, JUNIOR_THREAD_STATE_TTL_MS);
543
- await upsertIndexEntry({
544
- state,
545
- indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
546
- conversationId: next.conversationId,
547
- score: next.lastActivityAtMs
548
- });
549
- if (!hasRunnableWork(next)) {
550
- await removeIndexEntry({
551
- state,
552
- indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
553
- conversationId: next.conversationId
554
- });
555
- return;
556
- }
557
- await upsertIndexEntry({
558
- state,
559
- indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
560
- conversationId: next.conversationId,
561
- score: next.execution.updatedAtMs ?? next.updatedAtMs
562
- });
563
- }
564
- function assertSameOptionalConversationDestination(args) {
565
- if (!args.next || !args.current) return;
566
- if (sameDestination(args.current, args.next)) return;
567
- throw new Error(`Conversation destination changed for ${args.conversationId}`);
568
- }
569
- function conversationWorkState(conversation) {
570
- const lease = conversation.execution.lease;
571
- return {
572
- ...conversation,
573
- lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
574
- ...lease ? { lease: {
575
- acquiredAtMs: lease.acquiredAtMs,
576
- lastCheckInAtMs: lease.lastCheckInAtMs,
577
- leaseExpiresAtMs: lease.expiresAtMs,
578
- leaseToken: lease.token
579
- } } : void 0,
580
- messages: pendingMessages(conversation),
581
- needsRun: hasRunnableWork(conversation)
582
- };
583
- }
584
- /** Return a persisted conversation record, if one exists. */
585
- async function getConversation$1(args) {
586
- return await readConversation(await getConnectedState(args.state), args.conversationId);
587
- }
588
- /** Return a persisted conversation work record, if one exists. */
589
- async function getConversationWorkState$1(args) {
590
- const conversation = await getConversation$1(args);
591
- return conversation ? conversationWorkState(conversation) : void 0;
592
- }
593
- /** Return whether a conversation has pending or resumable execution work. */
594
- function hasRunnableConversationWork$1(conversation) {
595
- return hasRunnableWork(conversation);
596
- }
597
- async function appendInboundMessageWithAdmission(args) {
598
- const nowMs = args.nowMs ?? now$1();
599
- return await withConversationMutation({
600
- conversationId: args.message.conversationId,
601
- state: args.state
602
- }, async (state, lock) => {
603
- const existing = await readConversation(state, args.message.conversationId);
604
- const current = existing ?? emptyConversation({
605
- conversationId: args.message.conversationId,
606
- destination: args.message.destination,
607
- nowMs,
608
- source: args.message.source
609
- });
610
- if (existing) assertSameOptionalConversationDestination({
611
- conversationId: args.message.conversationId,
612
- current: current.destination,
613
- next: args.message.destination
614
- });
615
- const existingPending = current.execution.pendingMessages.some((message) => message.inboundMessageId === args.message.inboundMessageId);
616
- if (current.execution.inboundMessageIds.includes(args.message.inboundMessageId)) {
617
- if (!existingPending) return { status: "duplicate" };
618
- const nextStatus = current.execution.status === "idle" ? "pending" : current.execution.status;
619
- await writeConversation(state, lock, withExecutionUpdate(current, {
620
- ...current.execution,
621
- status: nextStatus,
622
- pendingMessages: current.execution.pendingMessages.map((message) => message.inboundMessageId === args.message.inboundMessageId ? upgradedPendingMessage(message, args.message) : message)
623
- }, nowMs));
624
- return { status: "duplicate" };
625
- }
626
- if (args.exclusive && hasRunnableWork(current)) return { status: "active" };
627
- let status;
628
- if (current.execution.lease) status = current.execution.status === "running" ? "running" : "paused";
629
- else if (current.execution.status === "paused" && args.message.delivery === "defer") status = "paused";
630
- else status = "pending";
631
- await writeConversation(state, lock, withExecutionUpdate({
632
- ...current,
633
- destination: current.destination ?? args.message.destination,
634
- source: current.source ?? args.message.source,
635
- lastActivityAtMs: nowMs
636
- }, {
637
- ...current.execution,
638
- status,
639
- inboundMessageIds: [...current.execution.inboundMessageIds, args.message.inboundMessageId],
640
- pendingMessages: [...current.execution.pendingMessages, args.message].sort(compareMessages)
641
- }, nowMs));
642
- return { status: "appended" };
643
- });
644
- }
645
- /** Persist one inbound message idempotently in its conversation mailbox. */
646
- async function appendInboundMessage(args) {
647
- const result = await appendInboundMessageWithAdmission({
648
- ...args,
649
- exclusive: false
650
- });
651
- if (result.status === "active") throw new Error("Non-exclusive mailbox append returned active");
652
- return result;
653
- }
654
- /** Append only when the Conversation has no other runnable work. */
655
- async function appendExclusiveInboundMessage(args) {
656
- return await appendInboundMessageWithAdmission({
657
- ...args,
658
- exclusive: true
659
- });
660
- }
661
- /** Mark a conversation runnable when there is no new mailbox message. */
662
- async function requestConversationWork$1(args) {
663
- const nowMs = args.nowMs ?? now$1();
664
- return await withConversationMutation(args, async (state, lock) => {
665
- const existing = await readConversation(state, args.conversationId);
666
- if (existing) assertSameOptionalConversationDestination({
667
- conversationId: args.conversationId,
668
- current: existing.destination,
669
- next: args.destination
670
- });
671
- const current = existing ?? emptyConversation({
672
- conversationId: args.conversationId,
673
- destination: args.destination,
674
- nowMs
675
- });
676
- const status = current.execution.lease ? "paused" : "pending";
677
- await writeConversation(state, lock, withExecutionUpdate({
678
- ...current,
679
- destination: current.destination ?? args.destination
680
- }, {
681
- ...current.execution,
682
- status
683
- }, nowMs));
684
- return { status: existing === void 0 ? "created" : "updated" };
685
- });
686
- }
687
- /** Record that a wake-up nudge was accepted for the conversation. */
688
- async function markConversationWorkEnqueued$1(args) {
689
- const nowMs = args.nowMs ?? now$1();
690
- await withConversationMutation(args, async (state, lock) => {
691
- const current = await readConversation(state, args.conversationId);
692
- if (!current) return;
693
- await writeConversation(state, lock, withExecutionUpdate(current, {
694
- ...current.execution,
695
- lastEnqueuedAtMs: nowMs
696
- }, nowMs));
697
- });
698
- }
699
- /** Clear a wake marker after its queue delivery finds no runnable work. */
700
- async function clearConsumedConversationWake$1(args) {
701
- const nowMs = args.nowMs ?? now$1();
702
- return await withConversationMutation(args, async (state, lock) => {
703
- const current = await readConversation(state, args.conversationId);
704
- if (!current || hasRunnableWork(current) || current.execution.lastEnqueuedAtMs === void 0) return false;
705
- await writeConversation(state, lock, withExecutionUpdate(current, {
706
- ...current.execution,
707
- lastEnqueuedAtMs: void 0
708
- }, nowMs));
709
- return true;
710
- });
711
- }
712
- /** Try to acquire the durable execution lease for one conversation. */
713
- async function startConversationWork$1(args) {
714
- const nowMs = args.nowMs ?? now$1();
715
- return await withConversationMutation(args, async (state, lock) => {
716
- const current = await readConversation(state, args.conversationId);
717
- if (!current) return { status: "no_work" };
718
- if (isLeaseActive(current.execution.lease, nowMs)) return {
719
- status: "active",
720
- leaseExpiresAtMs: current.execution.lease.expiresAtMs
721
- };
722
- if (!hasRunnableWork(current)) return { status: "no_work" };
723
- const lease = {
724
- token: randomUUID(),
725
- acquiredAtMs: nowMs,
726
- lastCheckInAtMs: nowMs,
727
- expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
728
- };
729
- const startsNewRun = current.execution.runId === void 0;
730
- await writeConversation(state, lock, withExecutionUpdate(current, {
731
- ...current.execution,
732
- lease,
733
- status: current.execution.status === "paused" ? "paused" : "running",
734
- runId: current.execution.runId ?? randomUUID(),
735
- lastEnqueuedAtMs: void 0,
736
- retryCount: startsNewRun ? 0 : current.execution.retryCount
737
- }, nowMs));
738
- return {
739
- status: "acquired",
740
- leaseToken: lease.token,
741
- leaseExpiresAtMs: lease.expiresAtMs
742
- };
743
- });
744
- }
745
- /** Extend the durable execution lease when the worker checks in. */
746
- async function checkInConversationWork$1(args) {
747
- const nowMs = args.nowMs ?? now$1();
748
- return await withConversationMutation(args, async (state, lock) => {
749
- const current = await readConversation(state, args.conversationId);
750
- if (!current || current.execution.lease?.token !== args.leaseToken) return false;
751
- await writeConversation(state, lock, withExecutionUpdate(current, {
752
- ...current.execution,
753
- lease: {
754
- ...current.execution.lease,
755
- lastCheckInAtMs: nowMs,
756
- expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
757
- }
758
- }, nowMs));
759
- return true;
760
- });
761
- }
762
- /**
763
- * Resolve pending mailbox entries after the caller accepts responsibility.
764
- *
765
- * Returning ids acknowledges only that subset; returning nothing acknowledges
766
- * every pending entry passed to the handler.
767
- */
768
- async function drainConversationMailbox$1(args) {
769
- const nowMs = args.nowMs ?? now$1();
770
- const pending = await withConversationMutation(args, async (state) => {
771
- const current = await readConversation(state, args.conversationId);
772
- if (!current || current.execution.lease?.token !== args.leaseToken) throw new Error(`Conversation lease is not held for ${args.conversationId}`);
773
- return pendingMessages(current);
774
- });
775
- if (pending.length === 0) return {
776
- changed: false,
777
- messages: []
778
- };
779
- const result = await args.handle(pending);
780
- const pendingIds = new Set(pending.map((message) => message.inboundMessageId));
781
- const acknowledgedIds = new Set(result ?? pending.map((message) => message.inboundMessageId));
782
- for (const inboundMessageId of acknowledgedIds) if (!pendingIds.has(inboundMessageId)) throw new Error(`Conversation mailbox drain result is not pending for ${args.conversationId}`);
783
- if (acknowledgedIds.size === 0) return {
784
- changed: false,
785
- messages: []
786
- };
787
- await withConversationMutation(args, async (state, lock) => {
788
- const current = await readConversation(state, args.conversationId);
789
- if (!current || current.execution.lease?.token !== args.leaseToken) throw new Error(`Conversation lease is not held for ${args.conversationId}`);
790
- const pendingMessages = current.execution.pendingMessages.filter((message) => !acknowledgedIds.has(message.inboundMessageId));
791
- await writeConversation(state, lock, withExecutionUpdate(current, {
792
- ...current.execution,
793
- status: current.execution.status === "pending" && pendingMessages.length === 0 ? "running" : current.execution.status,
794
- lastProgressAtMs: nowMs,
795
- retryCount: 0,
796
- pendingMessages
797
- }, nowMs));
798
- });
799
- return {
800
- changed: true,
801
- messages: pending.filter((message) => acknowledgedIds.has(message.inboundMessageId))
802
- };
803
- }
804
- /** Acknowledge leased mailbox entries after the handler accepts responsibility. */
805
- async function ackMessages$1(args) {
806
- const nowMs = args.nowMs ?? now$1();
807
- const inboundMessageIds = new Set(args.inboundMessageIds);
808
- return await withConversationMutation(args, async (state, lock) => {
809
- const current = await readConversation(state, args.conversationId);
810
- if (!current || current.execution.lease?.token !== args.leaseToken) return false;
811
- if (inboundMessageIds.size === 0) return true;
812
- const pendingMessages = current.execution.pendingMessages.filter((message) => !inboundMessageIds.has(message.inboundMessageId));
813
- if (pendingMessages.length === current.execution.pendingMessages.length) return true;
814
- await writeConversation(state, lock, withExecutionUpdate(current, {
815
- ...current.execution,
816
- lastProgressAtMs: nowMs,
817
- retryCount: 0,
818
- pendingMessages
819
- }, nowMs));
820
- return true;
821
- });
822
- }
823
- function isHumanFacingMessage(message) {
824
- return message.source === "web" || message.source === "slack";
825
- }
826
- async function readConversationStop(state, conversationId) {
827
- return normalizeStop(await state.get(conversationStopKey(conversationId)));
828
- }
829
- async function fenceConversationMutation(state, lock, conversationId) {
830
- if (!await state.extendLock(lock, CONVERSATION_MUTATION_LOCK_TTL_MS)) throw new ConversationMutationFencedError(conversationId);
831
- }
832
- /** Return whether one Conversation run has a durable stop request. */
833
- async function hasConversationStop(args) {
834
- return (await readConversationStop(await getConnectedState(args.state), args.conversationId))?.runId === args.runId;
835
- }
836
- /** Persist a stop request for the current run without process affinity. */
837
- async function stopConversationWork$1(args) {
838
- const nowMs = args.nowMs ?? now$1();
839
- return await withConversationMutation(args, async (state, lock) => {
840
- const current = await readConversation(state, args.conversationId);
841
- if (!current || !hasRunnableWork(current)) return { status: "no_work" };
842
- const runId = current.execution.runId ?? randomUUID();
843
- const pendingMessages = current.execution.pendingMessages.filter(isHumanFacingMessage);
844
- const inboundMessageIds = pendingMessages.map((message) => message.inboundMessageId);
845
- if (current.execution.runId === void 0) await writeConversation(state, lock, withExecutionUpdate(current, {
846
- ...current.execution,
847
- runId
848
- }, nowMs));
849
- await fenceConversationMutation(state, lock, args.conversationId);
850
- await state.set(conversationStopKey(args.conversationId), {
851
- inboundMessageIds,
852
- runId
853
- }, JUNIOR_THREAD_STATE_TTL_MS);
854
- return {
855
- runId,
856
- status: "requested",
857
- pendingMessages
858
- };
859
- });
860
- }
861
- /** Clear one observed stop request and discard older human-facing mailbox work. */
862
- async function completeConversationStop$1(args) {
863
- const nowMs = args.nowMs ?? now$1();
864
- return await withConversationMutation(args, async (state, lock) => {
865
- const current = await readConversation(state, args.conversationId);
866
- if (!current || current.execution.lease?.token !== args.leaseToken) return {
867
- status: "lost_lease",
868
- removedInboundMessageIds: []
869
- };
870
- const stop = await readConversationStop(state, args.conversationId);
871
- if (!stop || stop.runId !== args.runId) return {
872
- status: "none",
873
- removedInboundMessageIds: []
874
- };
875
- const stoppedInboundMessageIds = new Set(stop.inboundMessageIds);
876
- const removedInboundMessageIds = [];
877
- const pendingMessages = current.execution.pendingMessages.filter((message) => {
878
- const shouldStop = stoppedInboundMessageIds.has(message.inboundMessageId);
879
- if (shouldStop) removedInboundMessageIds.push(message.inboundMessageId);
880
- return !shouldStop;
881
- });
882
- await writeConversation(state, lock, withExecutionUpdate(current, {
883
- ...current.execution,
884
- lastEnqueuedAtMs: pendingMessages.length === 0 ? void 0 : current.execution.lastEnqueuedAtMs,
885
- pendingMessages
886
- }, nowMs));
887
- await fenceConversationMutation(state, lock, args.conversationId);
888
- await state.delete(conversationStopKey(args.conversationId));
889
- return {
890
- status: "cleared",
891
- removedInboundMessageIds
892
- };
893
- });
894
- }
895
- /** Cancel human-facing pending mailbox rows without requiring a worker lease. */
896
- async function cancelHumanFacingPendingMessages$1(args) {
897
- const nowMs = args.nowMs ?? now$1();
898
- const requestedIds = args.inboundMessageIds === void 0 ? void 0 : new Set(args.inboundMessageIds);
899
- return await withConversationMutation(args, async (state, lock) => {
900
- const current = await readConversation(state, args.conversationId);
901
- if (!current) return {
902
- cancelledInboundMessageIds: [],
903
- cancelledMessages: []
904
- };
905
- const cancelledInboundMessageIds = [];
906
- const cancelledMessages = [];
907
- const pendingMessages = [];
908
- for (const message of current.execution.pendingMessages) {
909
- const isRequested = requestedIds === void 0 || requestedIds.has(message.inboundMessageId);
910
- const isInSnapshot = args.receivedBeforeMs === void 0 || message.receivedAtMs <= args.receivedBeforeMs;
911
- if (isHumanFacingMessage(message) && isRequested && isInSnapshot) {
912
- cancelledInboundMessageIds.push(message.inboundMessageId);
913
- cancelledMessages.push(message);
914
- continue;
915
- }
916
- pendingMessages.push(message);
917
- }
918
- if (cancelledInboundMessageIds.length === 0) return {
919
- cancelledInboundMessageIds,
920
- cancelledMessages
921
- };
922
- const becomesIdle = current.execution.status === "pending" && pendingMessages.length === 0;
923
- await writeConversation(state, lock, withExecutionUpdate(current, {
924
- ...current.execution,
925
- lastEnqueuedAtMs: pendingMessages.length === 0 ? void 0 : current.execution.lastEnqueuedAtMs,
926
- pendingMessages,
927
- retryCount: becomesIdle ? 0 : current.execution.retryCount,
928
- runId: becomesIdle ? void 0 : current.execution.runId,
929
- status: becomesIdle ? "idle" : current.execution.status
930
- }, nowMs));
931
- return {
932
- cancelledInboundMessageIds,
933
- cancelledMessages
934
- };
935
- });
936
- }
937
- /** Mark the leased conversation as needing another queue-delivered slice. */
938
- async function requestAnotherSlice$1(args) {
939
- const nowMs = args.nowMs ?? now$1();
940
- return await withConversationMutation(args, async (state, lock) => {
941
- const current = await readConversation(state, args.conversationId);
942
- if (!current || current.execution.lease?.token !== args.leaseToken) return false;
943
- assertSameOptionalConversationDestination({
944
- conversationId: args.conversationId,
945
- current: current.destination,
946
- next: args.destination
947
- });
948
- await writeConversation(state, lock, withExecutionUpdate(current, {
949
- ...current.execution,
950
- status: "paused"
951
- }, nowMs));
952
- return true;
953
- });
954
- }
955
- /** Begin a requested turn resume under the worker's existing lease. */
956
- async function beginConversationResume$1(args) {
957
- const nowMs = args.nowMs ?? now$1();
958
- return await withConversationMutation(args, async (state, lock) => {
959
- const current = await readConversation(state, args.conversationId);
960
- if (!current || current.execution.lease?.token !== args.leaseToken || current.execution.status !== "paused") return false;
961
- await writeConversation(state, lock, withExecutionUpdate(current, {
962
- ...current.execution,
963
- status: "running"
964
- }, nowMs));
965
- return true;
966
- });
967
- }
968
- /** Release the durable execution lease without changing completion state. */
969
- async function releaseConversationWork$1(args) {
970
- const nowMs = args.nowMs ?? now$1();
971
- return await withConversationMutation(args, async (state, lock) => {
972
- const current = await readConversation(state, args.conversationId);
973
- if (!current || current.execution.lease?.token !== args.leaseToken) return false;
974
- await writeConversation(state, lock, withExecutionUpdate(current, {
975
- ...current.execution,
976
- lease: void 0,
977
- status: current.execution.status === "running" ? "pending" : current.execution.status
978
- }, nowMs));
979
- return true;
980
- });
981
- }
982
- /** Count one failed execution attempt and stop the conversation when retries are exhausted. */
983
- async function recordConversationRetry$1(args) {
984
- const nowMs = args.nowMs ?? now$1();
985
- return await withConversationMutation(args, async (state, lock) => {
986
- const current = await readConversation(state, args.conversationId);
987
- if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
988
- const count = (current.execution.retryCount ?? 0) + 1;
989
- const stopped = count >= 5;
990
- await writeConversation(state, lock, withExecutionUpdate(current, {
991
- ...current.execution,
992
- lastEnqueuedAtMs: void 0,
993
- lease: stopped ? void 0 : current.execution.lease,
994
- retryCount: count,
995
- pendingMessages: stopped ? [] : current.execution.pendingMessages,
996
- runId: stopped ? void 0 : current.execution.runId,
997
- status: stopped ? "failed" : "paused"
998
- }, nowMs));
999
- return stopped ? "stopped" : "recorded";
1000
- });
1001
- }
1002
- /** Finish a leased conversation and report whether runnable work remains. */
1003
- async function completeConversationWork$1(args) {
1004
- const nowMs = args.nowMs ?? now$1();
1005
- return await withConversationMutation(args, async (state, lock) => {
1006
- const current = await readConversation(state, args.conversationId);
1007
- if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
1008
- const hasPending = pendingMessages(current).length > 0;
1009
- const stopped = args.resumeIfStopped === true && (await readConversationStop(state, args.conversationId))?.runId === current.execution.runId;
1010
- const needsRun = current.execution.status === "paused" || stopped;
1011
- const runnable = needsRun || hasPending;
1012
- await writeConversation(state, lock, withExecutionUpdate(current, {
1013
- ...current.execution,
1014
- lease: void 0,
1015
- status: needsRun ? "paused" : hasPending ? "pending" : "idle",
1016
- lastProgressAtMs: args.madeProgress === false ? current.execution.lastProgressAtMs : nowMs,
1017
- retryCount: runnable || args.madeProgress === false ? current.execution.retryCount : 0,
1018
- runId: runnable ? current.execution.runId : void 0
1019
- }, nowMs));
1020
- return runnable ? "pending" : "completed";
1021
- });
1022
- }
1023
- /**
1024
- * Record one failed execution attempt for pending messages, dead-lettering
1025
- * the conversation when its retry budget or the legacy message delivery limit
1026
- * is exhausted.
1027
- *
1028
- * Retries are counted only when the run made no durable progress: if any
1029
- * attempted message left the mailbox, the remaining pending entries may be
1030
- * deliberate deferrals and are left untouched. Consumed message ids stay in
1031
- * `inboundMessageIds` so source retries remain duplicates.
1032
- */
1033
- async function recordAttemptFailure$1(args) {
1034
- const nowMs = args.nowMs ?? now$1();
1035
- return await withConversationMutation(args, async (state, lock) => {
1036
- const current = await readConversation(state, args.conversationId);
1037
- if (!current || current.execution.lease?.token !== args.leaseToken) return {
1038
- status: "lost_lease",
1039
- pendingCount: 0,
1040
- deadLetteredMessages: [],
1041
- retryCount: 0
1042
- };
1043
- const pendingIds = new Set(current.execution.pendingMessages.map((message) => message.inboundMessageId));
1044
- if (args.inboundMessageIds.length === 0 || args.inboundMessageIds.some((id) => !pendingIds.has(id))) return {
1045
- status: "skipped",
1046
- pendingCount: current.execution.pendingMessages.length,
1047
- deadLetteredMessages: [],
1048
- retryCount: current.execution.retryCount ?? 0
1049
- };
1050
- const retryCount = (current.execution.retryCount ?? 0) + 1;
1051
- const retriesExhausted = retryCount >= 5;
1052
- const attemptedIds = new Set(args.inboundMessageIds);
1053
- const deadLetteredMessages = [];
1054
- const pendingMessages = [];
1055
- for (const message of current.execution.pendingMessages) {
1056
- if (!attemptedIds.has(message.inboundMessageId)) {
1057
- if (retriesExhausted) deadLetteredMessages.push(message);
1058
- else pendingMessages.push(message);
1059
- continue;
1060
- }
1061
- const attempted = {
1062
- ...message,
1063
- attemptCount: (message.attemptCount ?? 0) + 1
1064
- };
1065
- if (retriesExhausted || attempted.attemptCount >= 5) {
1066
- deadLetteredMessages.push(attempted);
1067
- continue;
1068
- }
1069
- pendingMessages.push(attempted);
1070
- }
1071
- await writeConversation(state, lock, withExecutionUpdate(current, {
1072
- ...current.execution,
1073
- retryCount,
1074
- pendingMessages
1075
- }, nowMs));
1076
- return {
1077
- status: "recorded",
1078
- pendingCount: pendingMessages.length,
1079
- deadLetteredMessages,
1080
- retryCount
1081
- };
1082
- });
1083
- }
1084
- /** Record a terminal failure completion for a leased conversation. */
1085
- async function deadLetterAttempt$1(args) {
1086
- const nowMs = args.nowMs ?? now$1();
1087
- return await withConversationMutation(args, async (state, lock) => {
1088
- const current = await readConversation(state, args.conversationId);
1089
- if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
1090
- const runnable = pendingMessages(current).length > 0;
1091
- await writeConversation(state, lock, withExecutionUpdate(current, {
1092
- ...current.execution,
1093
- lease: void 0,
1094
- status: runnable ? "pending" : "failed",
1095
- runId: runnable ? current.execution.runId : void 0
1096
- }, nowMs));
1097
- return runnable ? "pending" : "failed";
1098
- });
1099
- }
1100
- /** Recover an expired lease, consuming the retry budget under the mutation lock. */
1101
- async function clearExpiredConversationLease$1(args) {
1102
- const nowMs = args.nowMs ?? now$1();
1103
- return await withConversationMutation(args, async (state, lock) => {
1104
- const current = await readConversation(state, args.conversationId);
1105
- if (!current?.execution.lease || current.execution.lease.expiresAtMs > nowMs) return "not_expired";
1106
- const retryCount = (current.execution.retryCount ?? 0) + 1;
1107
- const stopped = retryCount >= 5;
1108
- await writeConversation(state, lock, withExecutionUpdate(current, {
1109
- ...current.execution,
1110
- lastEnqueuedAtMs: void 0,
1111
- lease: void 0,
1112
- retryCount,
1113
- pendingMessages: stopped ? [] : current.execution.pendingMessages,
1114
- runId: stopped ? void 0 : current.execution.runId,
1115
- status: stopped ? "failed" : "paused"
1116
- }, nowMs));
1117
- return stopped ? "stopped" : "requeued";
1118
- });
1119
- }
1120
- /** Remove one conversation from the active index after it is missing or idle. */
1121
- async function removeActiveConversation$1(args) {
1122
- await removeIndexEntry({
1123
- state: await getConnectedState(args.state),
1124
- indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1125
- conversationId: args.conversationId
1126
- });
1127
- }
1128
- /** List active conversation ids by oldest execution update first. */
1129
- async function listActiveConversationIds$1(args = {}) {
1130
- return (await (await getConversationIndexStore(await getConnectedState(args.state))).list({
1131
- indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1132
- limit: args.limit,
1133
- order: "asc",
1134
- scoreMax: args.staleBeforeMs
1135
- })).map((entry) => entry.conversationId);
1136
- }
1137
- // src/chat/task-execution/store.ts
1138
- function metadataStore(options) {
1139
- return options.conversationStore ?? getConversationStore();
1140
- }
1141
- function duplicateInboundNudgeIdempotencyKey(message, nowMs) {
1142
- return `duplicate:${message.conversationId}:${message.inboundMessageId}:${nowMs}`;
1143
- }
1144
- function hasRecentEnqueueMarker(conversation, nowMs) {
1145
- const lastEnqueuedAtMs = conversation.execution.lastEnqueuedAtMs;
1146
- return typeof lastEnqueuedAtMs === "number" && lastEnqueuedAtMs + 6e4 > nowMs;
1147
- }
1148
- function now() {
1149
- return Date.now();
1150
- }
1151
- async function recordExecutionMetadata(args) {
1152
- const conversation = await getConversation$1({
1153
- conversationId: args.conversationId,
1154
- state: args.state
1155
- });
1156
- if (!conversation) return;
1157
- await metadataStore(args).recordExecution({
1158
- channelName: conversation.channelName,
1159
- conversationId: conversation.conversationId,
1160
- createdAtMs: conversation.createdAtMs,
1161
- destination: conversation.destination,
1162
- execution: {
1163
- lastCheckpointAtMs: conversation.execution.lastCheckpointAtMs,
1164
- lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
1165
- runId: conversation.execution.runId,
1166
- status: conversation.execution.status,
1167
- updatedAtMs: conversation.execution.updatedAtMs
1168
- },
1169
- lastActivityAtMs: conversation.lastActivityAtMs,
1170
- metrics: null,
1171
- actor: conversation.actor,
1172
- source: conversation.source,
1173
- title: conversation.title,
1174
- updatedAtMs: conversation.updatedAtMs
1175
- });
1176
- }
1177
- /** Return a persisted conversation record, if one exists. */
1178
- async function getConversation(args) {
1179
- return await getConversation$1(args);
1180
- }
1181
- /** Return a persisted conversation work record, if one exists. */
1182
- async function getConversationWorkState(args) {
1183
- return await getConversationWorkState$1(args);
1184
- }
1185
- /** Count one failed conversation execution and persist its terminal retry state. */
1186
- async function recordConversationRetry(args) {
1187
- const result = await recordConversationRetry$1(args);
1188
- await recordExecutionMetadata(args);
1189
- return result;
1190
- }
1191
- /** Count mailbox messages that have not yet reached the conversation event log. */
1192
- function countPendingConversationMessages(conversation) {
1193
- return conversation.execution.pendingMessages.length;
1194
- }
1195
- /** Return whether a conversation has pending or resumable execution work. */
1196
- function hasRunnableConversationWork(conversation) {
1197
- return conversation.execution.status !== "idle" || countPendingConversationMessages(conversation) > 0;
1198
- }
1199
- /**
1200
- * Ensure runnable conversation work has one accepted queue wake-up nudge.
1201
- *
1202
- * Ordinary wakes coalesce on a recent accepted marker. A caller can ignore the
1203
- * marker without replacing an active lease. Replacement is only for consumed
1204
- * or known-stale deliveries where another queue nudge must exist.
1205
- */
1206
- async function ensureConversationWake(args) {
1207
- const nowMs = args.nowMs ?? now();
1208
- const conversation = await getConversation$1({
1209
- conversationId: args.conversationId,
1210
- state: args.state
1211
- });
1212
- if (!conversation || !hasRunnableConversationWork(conversation)) return { status: "no_work" };
1213
- if (args.replaceExistingWake !== true && conversation.execution.lease && conversation.execution.lease.expiresAtMs > nowMs) return { status: "lease_active" };
1214
- if (args.ignoreEnqueueMarker !== true && args.replaceExistingWake !== true && hasRecentEnqueueMarker(conversation, nowMs)) return { status: "already_enqueued" };
1215
- const queueResult = await args.queue.send({
1216
- schemaVersion: 2,
1217
- conversationId: args.conversationId
1218
- }, {
1219
- delayMs: args.delayMs,
1220
- idempotencyKey: args.idempotencyKey
1221
- });
1222
- await markConversationWorkEnqueued({
1223
- conversationId: args.conversationId,
1224
- conversationStore: args.conversationStore,
1225
- nowMs,
1226
- state: args.state
1227
- });
1228
- return {
1229
- status: "enqueued",
1230
- queueMessageId: queueResult?.messageId
1231
- };
1232
- }
1233
- async function enqueueAfterAppend(args) {
1234
- const nowMs = args.nowMs ?? now();
1235
- const appendResult = args.appendResult;
1236
- if (appendResult.status === "active") return appendResult;
1237
- let idempotencyKey = args.message.inboundMessageId;
1238
- if (appendResult.status === "duplicate") {
1239
- const conversation = await getConversation$1({
1240
- conversationId: args.message.conversationId,
1241
- state: args.state
1242
- });
1243
- if (!conversation) return appendResult;
1244
- if (hasRecentEnqueueMarker(conversation, nowMs)) {
1245
- await recordExecutionMetadata({
1246
- conversationId: args.message.conversationId,
1247
- conversationStore: args.conversationStore,
1248
- state: args.state
1249
- });
1250
- return appendResult;
1251
- }
1252
- if (!conversation.execution.pendingMessages.some((message) => message.inboundMessageId === args.message.inboundMessageId)) {
1253
- await recordExecutionMetadata({
1254
- conversationId: args.message.conversationId,
1255
- conversationStore: args.conversationStore,
1256
- state: args.state
1257
- });
1258
- return appendResult;
1259
- }
1260
- idempotencyKey = duplicateInboundNudgeIdempotencyKey(args.message, nowMs);
1261
- }
1262
- const wake = await ensureConversationWake({
1263
- conversationId: args.message.conversationId,
1264
- conversationStore: args.conversationStore,
1265
- delayMs: args.queueDelayMs,
1266
- idempotencyKey,
1267
- ignoreEnqueueMarker: args.message.source === "event" ? void 0 : true,
1268
- nowMs,
1269
- queue: args.queue,
1270
- replaceExistingWake: args.replaceExistingWake,
1271
- state: args.state
1272
- });
1273
- if (wake.status !== "enqueued") await recordExecutionMetadata({
1274
- conversationId: args.message.conversationId,
1275
- conversationStore: args.conversationStore,
1276
- state: args.state
1277
- });
1278
- return {
1279
- ...appendResult,
1280
- ...wake.status === "enqueued" ? { queueMessageId: wake.queueMessageId } : void 0
1281
- };
1282
- }
1283
- /** Persist inbound work and ensure a worker wake-up. */
1284
- async function appendAndEnqueueInboundMessage(args) {
1285
- const nowMs = args.nowMs ?? now();
1286
- const result = await enqueueAfterAppend({
1287
- ...args,
1288
- appendResult: await appendInboundMessage({
1289
- message: args.message,
1290
- nowMs,
1291
- state: args.state
1292
- }),
1293
- nowMs
1294
- });
1295
- if (result.status === "active") throw new Error("Non-exclusive mailbox enqueue returned active");
1296
- return result;
1297
- }
1298
- /** Persist exclusive inbound work and wake its worker when admitted. */
1299
- async function appendAndEnqueueExclusiveInboundMessage(args) {
1300
- const nowMs = args.nowMs ?? now();
1301
- return await enqueueAfterAppend({
1302
- ...args,
1303
- appendResult: await appendExclusiveInboundMessage({
1304
- message: args.message,
1305
- nowMs,
1306
- state: args.state
1307
- }),
1308
- nowMs
1309
- });
1310
- }
1311
- /** Persist a stop request for the current Conversation run. */
1312
- async function stopConversationWork(args) {
1313
- const result = await stopConversationWork$1(args);
1314
- if (result.status === "requested") await recordExecutionMetadata(args);
1315
- return result;
1316
- }
1317
- /** Finish an observed stop request under the current worker lease. */
1318
- async function completeConversationStop(args) {
1319
- const result = await completeConversationStop$1(args);
1320
- if (result.status === "cleared") await recordExecutionMetadata(args);
1321
- return result;
1322
- }
1323
- /** Clear an accepted wake marker after its delivery finds no runnable work. */
1324
- async function clearConsumedConversationWake(args) {
1325
- const result = await clearConsumedConversationWake$1(args);
1326
- if (result) await recordExecutionMetadata(args);
1327
- return result;
1328
- }
1329
- /** Mark a conversation runnable when there is no new mailbox message. */
1330
- async function requestConversationWork(args) {
1331
- const result = await requestConversationWork$1(args);
1332
- await recordExecutionMetadata({
1333
- conversationId: args.conversationId,
1334
- conversationStore: args.conversationStore,
1335
- state: args.state
1336
- });
1337
- return result;
1338
- }
1339
- /** Record that a wake-up nudge was accepted for the conversation. */
1340
- async function markConversationWorkEnqueued(args) {
1341
- await markConversationWorkEnqueued$1(args);
1342
- await recordExecutionMetadata(args);
1343
- }
1344
- /** Try to acquire the durable execution lease for one conversation. */
1345
- async function startConversationWork(args) {
1346
- const result = await startConversationWork$1(args);
1347
- await recordExecutionMetadata(args);
1348
- return result;
1349
- }
1350
- /** Extend the durable execution lease when the worker checks in. */
1351
- async function checkInConversationWork(args) {
1352
- const result = await checkInConversationWork$1(args);
1353
- if (result) await recordExecutionMetadata(args);
1354
- return result;
1355
- }
1356
- /** Drain pending mailbox entries after the caller accepts responsibility. */
1357
- async function drainConversationMailbox(args) {
1358
- const result = await drainConversationMailbox$1(args);
1359
- if (result.changed) await recordExecutionMetadata(args);
1360
- return result.messages;
1361
- }
1362
- /** Acknowledge leased mailbox entries after the handler accepts responsibility. */
1363
- async function ackMessages(args) {
1364
- const result = await ackMessages$1(args);
1365
- await recordExecutionMetadata(args);
1366
- return result;
1367
- }
1368
- /** Cancel human-facing pending mailbox rows without requiring a worker lease. */
1369
- async function cancelHumanFacingPendingMessages(args) {
1370
- const result = await cancelHumanFacingPendingMessages$1(args);
1371
- if (result.cancelledInboundMessageIds.length > 0) await recordExecutionMetadata(args);
1372
- return result;
1373
- }
1374
- /** Mark the leased conversation as needing another queue-delivered slice. */
1375
- async function requestAnotherSlice(args) {
1376
- const result = await requestAnotherSlice$1(args);
1377
- await recordExecutionMetadata(args);
1378
- return result;
1379
- }
1380
- /** Begin a requested turn resume without releasing the current lease. */
1381
- async function beginConversationResume(args) {
1382
- const result = await beginConversationResume$1(args);
1383
- if (result) await recordExecutionMetadata(args);
1384
- return result;
1385
- }
1386
- /** Release the durable execution lease without changing completion state. */
1387
- async function releaseConversationWork(args) {
1388
- const result = await releaseConversationWork$1(args);
1389
- await recordExecutionMetadata(args);
1390
- return result;
1391
- }
1392
- /** Finish a leased conversation and report whether runnable work remains. */
1393
- async function completeConversationWork(args) {
1394
- const result = await completeConversationWork$1(args);
1395
- await recordExecutionMetadata(args);
1396
- return result;
1397
- }
1398
- /** Record one failed delivery attempt and dead-letter messages at their limit. */
1399
- async function recordAttemptFailure(args) {
1400
- const result = await recordAttemptFailure$1(args);
1401
- if (result.status === "recorded") await recordExecutionMetadata(args);
1402
- return result;
1403
- }
1404
- /** Record a terminal failure completion for a leased conversation. */
1405
- async function deadLetterAttempt(args) {
1406
- const result = await deadLetterAttempt$1(args);
1407
- await recordExecutionMetadata(args);
1408
- return result;
1409
- }
1410
- /** Recover an expired lease and persist the resulting retry state. */
1411
- async function clearExpiredConversationLease(args) {
1412
- const result = await clearExpiredConversationLease$1(args);
1413
- await recordExecutionMetadata(args);
1414
- return result;
1415
- }
1416
- /** Remove one conversation from the active index after it is missing or idle. */
1417
- async function removeActiveConversation(args) {
1418
- return await removeActiveConversation$1(args);
1419
- }
1420
- /** List active conversation ids by oldest execution update first. */
1421
- async function listActiveConversationIds(args = {}) {
1422
- return await listActiveConversationIds$1(args);
1423
- }
1424
- export { hasRunnableConversationWork$1 as A, requestAnotherSlice as C, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS as D, stopConversationWork as E, isInvalidConversationRecordError as M, CONVERSATION_WORK_STALE_ENQUEUE_MS as O, removeActiveConversation as S, startConversationWork as T, hasRunnableConversationWork as _, cancelHumanFacingPendingMessages as a, recordConversationRetry as b, clearExpiredConversationLease as c, countPendingConversationMessages as d, deadLetterAttempt as f, getConversationWorkState as g, getConversation as h, beginConversationResume as i, isFinalAttempt as j, hasConversationStop as k, completeConversationStop as l, ensureConversationWake as m, appendAndEnqueueExclusiveInboundMessage as n, checkInConversationWork as o, drainConversationMailbox as p, appendAndEnqueueInboundMessage as r, clearConsumedConversationWake as s, ackMessages as t, completeConversationWork as u, listActiveConversationIds as v, requestConversationWork as w, releaseConversationWork as x, recordAttemptFailure as y };