@sentry/junior 0.71.2 → 0.72.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 (57) hide show
  1. package/bin/junior.mjs +10 -0
  2. package/dist/api-reference.d.ts +2 -0
  3. package/dist/app.d.ts +5 -5
  4. package/dist/app.js +1039 -1971
  5. package/dist/chat/agent-dispatch/heartbeat.d.ts +0 -6
  6. package/dist/chat/mcp/errors.d.ts +3 -0
  7. package/dist/chat/mcp/tool-manager.d.ts +5 -1
  8. package/dist/chat/plugins/agent-hooks.d.ts +3 -2
  9. package/dist/chat/requester.d.ts +60 -0
  10. package/dist/chat/respond.d.ts +2 -6
  11. package/dist/chat/runtime/agent-continue-runner.d.ts +25 -0
  12. package/dist/chat/runtime/reply-executor.d.ts +4 -4
  13. package/dist/chat/runtime/turn.d.ts +2 -2
  14. package/dist/chat/services/agent-continue.d.ts +27 -0
  15. package/dist/chat/services/message-actor-identity.d.ts +12 -4
  16. package/dist/chat/services/turn-session-record.d.ts +10 -7
  17. package/dist/chat/slack/user.d.ts +4 -4
  18. package/dist/chat/state/adapter.d.ts +2 -0
  19. package/dist/chat/state/conversation-details.d.ts +4 -3
  20. package/dist/chat/state/session-log.d.ts +43 -0
  21. package/dist/chat/state/turn-session.d.ts +7 -10
  22. package/dist/chat/task-execution/queue.d.ts +1 -1
  23. package/dist/chat/task-execution/slack-work.d.ts +5 -5
  24. package/dist/chat/task-execution/store.d.ts +83 -48
  25. package/dist/chat/task-execution/worker.d.ts +3 -3
  26. package/dist/chat/tools/definition.d.ts +3 -0
  27. package/dist/chat/tools/execution/tool-error-handler.d.ts +2 -1
  28. package/dist/chat/tools/types.d.ts +2 -5
  29. package/dist/{chunk-R62YWUNO.js → chunk-3FYPXHPL.js} +10 -28
  30. package/dist/chunk-4JXCSGSA.js +212 -0
  31. package/dist/{chunk-GT67ZWZQ.js → chunk-55XEZFGD.js} +5 -3
  32. package/dist/{chunk-BBXYXOJW.js → chunk-6GEYPE6T.js} +18 -523
  33. package/dist/chunk-G3E7SCME.js +28 -0
  34. package/dist/{chunk-UXG6TU2U.js → chunk-GB3AL54K.js} +8 -93
  35. package/dist/chunk-HNMUVGSR.js +1119 -0
  36. package/dist/{chunk-XE2VFQQN.js → chunk-ICKIDP7G.js} +1 -1
  37. package/dist/chunk-KVZL5NZS.js +519 -0
  38. package/dist/chunk-PP7AGSBU.js +185 -0
  39. package/dist/{chunk-B5HKWWQB.js → chunk-VLIO6RQR.js} +8 -6
  40. package/dist/{chunk-HOGQL2H6.js → chunk-VSNA5KAB.js} +177 -101
  41. package/dist/{chunk-76YMBKW7.js → chunk-XC33FJZN.js} +4 -12
  42. package/dist/{chunk-JS4HURDT.js → chunk-ZJQPA67D.js} +25 -25
  43. package/dist/cli/check.js +10 -8
  44. package/dist/cli/run.js +9 -1
  45. package/dist/cli/snapshot-warmup.js +10 -7
  46. package/dist/cli/upgrade.js +599 -0
  47. package/dist/nitro.d.ts +1 -1
  48. package/dist/nitro.js +5 -4
  49. package/dist/plugins.d.ts +1 -1
  50. package/dist/reporting/conversations.d.ts +116 -0
  51. package/dist/reporting.d.ts +24 -129
  52. package/dist/reporting.js +310 -158
  53. package/package.json +3 -3
  54. package/dist/chat/runtime/timeout-resume-runner.d.ts +0 -19
  55. package/dist/chat/services/requester-identity.d.ts +0 -19
  56. package/dist/chat/services/timeout-resume.d.ts +0 -23
  57. package/dist/handlers/turn-resume.d.ts +0 -4
@@ -0,0 +1,1119 @@
1
+ import {
2
+ parseDestination,
3
+ sameDestination
4
+ } from "./chunk-XC33FJZN.js";
5
+ import {
6
+ getDefaultRedisStateAdapterFor,
7
+ getStateAdapter
8
+ } from "./chunk-3FYPXHPL.js";
9
+ import {
10
+ getChatConfig
11
+ } from "./chunk-ZJQPA67D.js";
12
+ import {
13
+ parseStoredSlackRequester
14
+ } from "./chunk-PP7AGSBU.js";
15
+ import {
16
+ isRecord,
17
+ toOptionalNumber,
18
+ toOptionalString
19
+ } from "./chunk-6GEYPE6T.js";
20
+
21
+ // src/chat/state/ttl.ts
22
+ var JUNIOR_THREAD_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
23
+
24
+ // src/chat/task-execution/store.ts
25
+ import { randomUUID } from "crypto";
26
+ var CONVERSATION_PREFIX = "junior:conversation";
27
+ var CONVERSATION_SCHEMA_VERSION = 1;
28
+ var CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH = 1e4;
29
+ var CONVERSATION_INDEX_LOCK_TTL_MS = 1e4;
30
+ var CONVERSATION_INDEX_LOCK_WAIT_MS = 2e3;
31
+ var CONVERSATION_INDEX_LOCK_RETRY_MS = 25;
32
+ var CONVERSATION_MUTATION_LOCK_TTL_MS = 1e4;
33
+ var CONVERSATION_MUTATION_WAIT_MS = 1e4;
34
+ var CONVERSATION_MUTATION_RETRY_MS = 25;
35
+ var InvalidConversationRecordError = class extends Error {
36
+ constructor(conversationId) {
37
+ super(`Conversation record is invalid for ${conversationId}`);
38
+ this.name = "InvalidConversationRecordError";
39
+ }
40
+ };
41
+ var CONVERSATION_BY_ACTIVITY_INDEX_KEY = `${CONVERSATION_PREFIX}:by-activity`;
42
+ var CONVERSATION_ACTIVE_INDEX_KEY = `${CONVERSATION_PREFIX}:active`;
43
+ var CONVERSATION_WORK_LEASE_TTL_MS = 9e4;
44
+ var CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15e3;
45
+ var CONVERSATION_WORK_STALE_ENQUEUE_MS = 6e4;
46
+ function duplicateInboundNudgeIdempotencyKey(message, nowMs) {
47
+ return `duplicate:${message.conversationId}:${message.inboundMessageId}:${nowMs}`;
48
+ }
49
+ function hasRecentEnqueueMarker(conversation, nowMs) {
50
+ const lastEnqueuedAtMs = conversation.execution.lastEnqueuedAtMs;
51
+ return typeof lastEnqueuedAtMs === "number" && lastEnqueuedAtMs + CONVERSATION_WORK_STALE_ENQUEUE_MS > nowMs;
52
+ }
53
+ function conversationKey(conversationId) {
54
+ return `${CONVERSATION_PREFIX}:${conversationId}`;
55
+ }
56
+ function indexLockKey(indexKey) {
57
+ return `${indexKey}:lock`;
58
+ }
59
+ function mutationLockKey(conversationId) {
60
+ return `${CONVERSATION_PREFIX}:mutation:${conversationId}`;
61
+ }
62
+ function now() {
63
+ return Date.now();
64
+ }
65
+ function compareMessages(left, right) {
66
+ return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
67
+ }
68
+ function compareIndexDescending(left, right) {
69
+ return right.score - left.score || right.conversationId.localeCompare(left.conversationId);
70
+ }
71
+ function compareIndexAscending(left, right) {
72
+ return left.score - right.score || left.conversationId.localeCompare(right.conversationId);
73
+ }
74
+ function uniqueStrings(values) {
75
+ return [...new Set(values)];
76
+ }
77
+ function normalizeSource(value) {
78
+ if (value === "api" || value === "internal" || value === "plugin" || value === "scheduler" || value === "slack") {
79
+ return value;
80
+ }
81
+ return void 0;
82
+ }
83
+ function normalizeExecutionStatus(value) {
84
+ if (value === "awaiting_resume" || value === "idle" || value === "pending" || value === "running") {
85
+ return value;
86
+ }
87
+ return void 0;
88
+ }
89
+ function normalizeMetadata(value) {
90
+ if (!isRecord(value)) {
91
+ return void 0;
92
+ }
93
+ return value;
94
+ }
95
+ function normalizeInput(value) {
96
+ if (!isRecord(value)) {
97
+ return void 0;
98
+ }
99
+ const text = toOptionalString(value.text);
100
+ if (!text) {
101
+ return void 0;
102
+ }
103
+ return {
104
+ text,
105
+ authorId: toOptionalString(value.authorId),
106
+ attachments: Array.isArray(value.attachments) ? [...value.attachments] : void 0,
107
+ metadata: normalizeMetadata(value.metadata)
108
+ };
109
+ }
110
+ function normalizeMessage(value) {
111
+ if (!isRecord(value)) {
112
+ return void 0;
113
+ }
114
+ const conversationId = toOptionalString(value.conversationId);
115
+ const inboundMessageId = toOptionalString(value.inboundMessageId);
116
+ const source = normalizeSource(value.source);
117
+ const destination = parseDestination(value.destination);
118
+ const createdAtMs = toOptionalNumber(value.createdAtMs);
119
+ const receivedAtMs = toOptionalNumber(value.receivedAtMs);
120
+ const input = normalizeInput(value.input);
121
+ if (!conversationId || !destination || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
122
+ return void 0;
123
+ }
124
+ return {
125
+ conversationId,
126
+ destination,
127
+ inboundMessageId,
128
+ source,
129
+ createdAtMs,
130
+ receivedAtMs,
131
+ input,
132
+ injectedAtMs: toOptionalNumber(value.injectedAtMs)
133
+ };
134
+ }
135
+ function normalizeRequester(value) {
136
+ return parseStoredSlackRequester(value);
137
+ }
138
+ function normalizeLease(value) {
139
+ if (!isRecord(value)) {
140
+ return void 0;
141
+ }
142
+ const token = toOptionalString(value.token);
143
+ const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
144
+ const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
145
+ const expiresAtMs = toOptionalNumber(value.expiresAtMs);
146
+ if (!token || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof expiresAtMs !== "number") {
147
+ return void 0;
148
+ }
149
+ return {
150
+ token,
151
+ acquiredAtMs,
152
+ lastCheckInAtMs,
153
+ expiresAtMs
154
+ };
155
+ }
156
+ function normalizeExecution(conversationId, value) {
157
+ if (!isRecord(value)) {
158
+ return void 0;
159
+ }
160
+ const status = normalizeExecutionStatus(value.status);
161
+ if (!status) {
162
+ return void 0;
163
+ }
164
+ const pendingMessages2 = Array.isArray(value.pendingMessages) ? value.pendingMessages.map(normalizeMessage).filter((message) => Boolean(message)).filter((message) => message.conversationId === conversationId).filter((message) => message.injectedAtMs === void 0).sort(compareMessages) : [];
165
+ const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(
166
+ value.inboundMessageIds.map((id) => typeof id === "string" ? id : void 0).filter((id) => Boolean(id))
167
+ ) : [];
168
+ const lease = normalizeLease(value.lease);
169
+ const normalizedStatus = status === "idle" && lease ? "running" : status === "idle" && pendingMessages2.length > 0 ? "pending" : status;
170
+ return {
171
+ status: normalizedStatus,
172
+ inboundMessageIds: uniqueStrings([
173
+ ...inboundMessageIds,
174
+ ...pendingMessages2.map((message) => message.inboundMessageId)
175
+ ]),
176
+ pendingCount: pendingMessages2.length,
177
+ pendingMessages: pendingMessages2,
178
+ lease,
179
+ lastCheckpointAtMs: toOptionalNumber(value.lastCheckpointAtMs),
180
+ lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
181
+ runId: toOptionalString(value.runId),
182
+ updatedAtMs: toOptionalNumber(value.updatedAtMs)
183
+ };
184
+ }
185
+ function normalizeConversation(conversationId, value) {
186
+ if (!isRecord(value) || value.schemaVersion !== CONVERSATION_SCHEMA_VERSION) {
187
+ return void 0;
188
+ }
189
+ const storedConversationId = toOptionalString(value.conversationId);
190
+ const createdAtMs = toOptionalNumber(value.createdAtMs);
191
+ const lastActivityAtMs = toOptionalNumber(value.lastActivityAtMs);
192
+ const updatedAtMs = toOptionalNumber(value.updatedAtMs);
193
+ const execution = normalizeExecution(conversationId, value.execution);
194
+ const destination = value.destination === void 0 ? void 0 : parseDestination(value.destination);
195
+ if (storedConversationId !== conversationId || typeof createdAtMs !== "number" || typeof lastActivityAtMs !== "number" || typeof updatedAtMs !== "number" || !execution || value.destination !== void 0 && !destination) {
196
+ return void 0;
197
+ }
198
+ if (execution.pendingMessages.length > 0 && (!destination || execution.pendingMessages.some(
199
+ (message) => !sameDestination(message.destination, destination)
200
+ ))) {
201
+ return void 0;
202
+ }
203
+ return {
204
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
205
+ conversationId,
206
+ createdAtMs,
207
+ lastActivityAtMs,
208
+ updatedAtMs,
209
+ execution,
210
+ ...destination ? { destination } : {},
211
+ ...toOptionalString(value.title) ? { title: toOptionalString(value.title) } : {},
212
+ ...toOptionalString(value.channelName) ? { channelName: toOptionalString(value.channelName) } : {},
213
+ ...normalizeRequester(value.requester) ? { requester: normalizeRequester(value.requester) } : {},
214
+ ...normalizeSource(value.source) ? { source: normalizeSource(value.source) } : {}
215
+ };
216
+ }
217
+ function emptyConversation(args) {
218
+ return {
219
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
220
+ conversationId: args.conversationId,
221
+ createdAtMs: args.nowMs,
222
+ lastActivityAtMs: args.nowMs,
223
+ updatedAtMs: args.nowMs,
224
+ ...args.destination ? { destination: args.destination } : {},
225
+ ...args.source ? { source: args.source } : {},
226
+ execution: {
227
+ status: "idle",
228
+ inboundMessageIds: [],
229
+ pendingCount: 0,
230
+ pendingMessages: [],
231
+ updatedAtMs: args.nowMs
232
+ }
233
+ };
234
+ }
235
+ function isLeaseActive(lease, nowMs) {
236
+ return Boolean(lease && lease.expiresAtMs > nowMs);
237
+ }
238
+ function pendingMessages(conversation) {
239
+ return [...conversation.execution.pendingMessages].sort(compareMessages);
240
+ }
241
+ function hasRunnableWork(conversation) {
242
+ return conversation.execution.status !== "idle" || pendingMessages(conversation).length > 0;
243
+ }
244
+ function executionWithPendingMessages(execution, pending) {
245
+ const pendingMessages2 = [...pending].sort(compareMessages);
246
+ const status = execution.status === "idle" && execution.lease ? "running" : execution.status === "idle" && pendingMessages2.length > 0 ? "pending" : execution.status;
247
+ return {
248
+ ...execution,
249
+ status,
250
+ inboundMessageIds: uniqueStrings([
251
+ ...execution.inboundMessageIds,
252
+ ...pendingMessages2.map((message) => message.inboundMessageId)
253
+ ]),
254
+ pendingMessages: pendingMessages2,
255
+ pendingCount: pendingMessages2.length
256
+ };
257
+ }
258
+ function withExecutionUpdate(conversation, execution, nowMs) {
259
+ return {
260
+ ...conversation,
261
+ updatedAtMs: nowMs,
262
+ execution: {
263
+ ...executionWithPendingMessages(execution, execution.pendingMessages),
264
+ updatedAtMs: nowMs
265
+ }
266
+ };
267
+ }
268
+ async function getConnectedState(stateAdapter) {
269
+ const state = stateAdapter ?? getStateAdapter();
270
+ await state.connect();
271
+ return state;
272
+ }
273
+ async function sleep(ms) {
274
+ await new Promise((resolve) => {
275
+ const timer = setTimeout(resolve, ms);
276
+ timer.unref?.();
277
+ });
278
+ }
279
+ async function withIndexLock(state, indexKey, callback) {
280
+ const startedAtMs = now();
281
+ let lock;
282
+ while (true) {
283
+ lock = await state.acquireLock(
284
+ indexLockKey(indexKey),
285
+ CONVERSATION_INDEX_LOCK_TTL_MS
286
+ );
287
+ if (lock) {
288
+ break;
289
+ }
290
+ if (now() - startedAtMs >= CONVERSATION_INDEX_LOCK_WAIT_MS) {
291
+ throw new Error(
292
+ `Could not acquire conversation index lock for ${indexKey}`
293
+ );
294
+ }
295
+ await sleep(CONVERSATION_INDEX_LOCK_RETRY_MS);
296
+ }
297
+ try {
298
+ return await callback();
299
+ } finally {
300
+ await state.releaseLock(lock);
301
+ }
302
+ }
303
+ function normalizeIndexEntry(value) {
304
+ if (!isRecord(value)) {
305
+ return void 0;
306
+ }
307
+ const conversationId = toOptionalString(value.conversationId);
308
+ const score = toOptionalNumber(value.score);
309
+ if (!conversationId || typeof score !== "number") {
310
+ return void 0;
311
+ }
312
+ return { conversationId, score };
313
+ }
314
+ function uniqueIndexEntries(value) {
315
+ if (!Array.isArray(value)) {
316
+ return [];
317
+ }
318
+ const entries = /* @__PURE__ */ new Map();
319
+ for (const item of value) {
320
+ const entry = normalizeIndexEntry(item);
321
+ if (!entry) {
322
+ continue;
323
+ }
324
+ const existing = entries.get(entry.conversationId);
325
+ if (!existing || entry.score > existing.score) {
326
+ entries.set(entry.conversationId, entry);
327
+ }
328
+ }
329
+ return [...entries.values()];
330
+ }
331
+ function retainedIndexEntries(indexKey, entries) {
332
+ if (indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
333
+ return entries.sort(compareIndexDescending).slice(0, CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH);
334
+ }
335
+ if (indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
336
+ return entries.sort(compareIndexAscending);
337
+ }
338
+ throw new Error(`Unknown conversation index ${indexKey}`);
339
+ }
340
+ function redisIndexKey(indexKey) {
341
+ const prefix = getChatConfig().state.keyPrefix;
342
+ return [...prefix ? [prefix] : [], indexKey].join(":");
343
+ }
344
+ function parseRedisIndexEntries(values) {
345
+ if (!Array.isArray(values)) {
346
+ return [];
347
+ }
348
+ const entries = [];
349
+ for (let index = 0; index < values.length; index += 2) {
350
+ const conversationId = toOptionalString(values[index]);
351
+ const score = typeof values[index + 1] === "number" ? values[index + 1] : Number(values[index + 1]);
352
+ if (!conversationId || !Number.isFinite(score)) {
353
+ continue;
354
+ }
355
+ entries.push({ conversationId, score });
356
+ }
357
+ return entries;
358
+ }
359
+ function redisConversationIndexStore(client) {
360
+ const upsertBoundedActivityScript = `
361
+ redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
362
+ redis.call("PEXPIRE", KEYS[1], ARGV[3])
363
+ local extra = redis.call("ZCARD", KEYS[1]) - tonumber(ARGV[4])
364
+ if extra > 0 then
365
+ redis.call("ZREMRANGEBYRANK", KEYS[1], 0, extra - 1)
366
+ end
367
+ return 1
368
+ `;
369
+ return {
370
+ async list(args) {
371
+ const key = redisIndexKey(args.indexKey);
372
+ const limit = args.limit;
373
+ if (limit === 0) {
374
+ return [];
375
+ }
376
+ const values = args.scoreMax !== void 0 ? await client.sendCommand([
377
+ "ZRANGEBYSCORE",
378
+ key,
379
+ "-inf",
380
+ String(args.scoreMax),
381
+ "WITHSCORES",
382
+ ...limit !== void 0 ? ["LIMIT", "0", String(limit)] : []
383
+ ]) : await client.sendCommand([
384
+ args.order === "asc" ? "ZRANGE" : "ZREVRANGE",
385
+ key,
386
+ "0",
387
+ String(limit === void 0 ? -1 : Math.max(0, limit - 1)),
388
+ "WITHSCORES"
389
+ ]);
390
+ return parseRedisIndexEntries(values);
391
+ },
392
+ async remove(args) {
393
+ await client.sendCommand([
394
+ "ZREM",
395
+ redisIndexKey(args.indexKey),
396
+ args.conversationId
397
+ ]);
398
+ },
399
+ async upsert(args) {
400
+ const key = redisIndexKey(args.indexKey);
401
+ if (args.indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
402
+ await client.sendCommand([
403
+ "EVAL",
404
+ upsertBoundedActivityScript,
405
+ "1",
406
+ key,
407
+ String(args.score),
408
+ args.conversationId,
409
+ String(JUNIOR_THREAD_STATE_TTL_MS),
410
+ String(CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH)
411
+ ]);
412
+ return;
413
+ }
414
+ if (args.indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
415
+ await client.sendCommand([
416
+ "ZADD",
417
+ key,
418
+ String(args.score),
419
+ args.conversationId
420
+ ]);
421
+ await client.sendCommand([
422
+ "PEXPIRE",
423
+ key,
424
+ String(JUNIOR_THREAD_STATE_TTL_MS)
425
+ ]);
426
+ return;
427
+ }
428
+ throw new Error(`Unknown conversation index ${args.indexKey}`);
429
+ }
430
+ };
431
+ }
432
+ function emulatedConversationIndexStore(state) {
433
+ const readIndex = async (indexKey) => uniqueIndexEntries(await state.get(indexKey));
434
+ const writeIndex = async (indexKey, entries) => {
435
+ await state.set(indexKey, entries, JUNIOR_THREAD_STATE_TTL_MS);
436
+ };
437
+ return {
438
+ async list(args) {
439
+ const entries = (await readIndex(args.indexKey)).filter(
440
+ (entry) => args.scoreMax === void 0 ? true : entry.score <= args.scoreMax
441
+ ).sort(
442
+ args.order === "asc" ? compareIndexAscending : compareIndexDescending
443
+ );
444
+ return entries.slice(0, args.limit ?? entries.length);
445
+ },
446
+ async remove(args) {
447
+ await withIndexLock(state, args.indexKey, async () => {
448
+ const entries = await readIndex(args.indexKey);
449
+ const next = entries.filter(
450
+ (entry) => entry.conversationId !== args.conversationId
451
+ );
452
+ if (next.length === entries.length) {
453
+ return;
454
+ }
455
+ await writeIndex(args.indexKey, next);
456
+ });
457
+ },
458
+ async upsert(args) {
459
+ await withIndexLock(state, args.indexKey, async () => {
460
+ const entries = await readIndex(args.indexKey);
461
+ const withoutCurrent = entries.filter(
462
+ (entry) => entry.conversationId !== args.conversationId
463
+ );
464
+ const next = retainedIndexEntries(args.indexKey, [
465
+ ...withoutCurrent,
466
+ { conversationId: args.conversationId, score: args.score }
467
+ ]);
468
+ await writeIndex(args.indexKey, next);
469
+ });
470
+ }
471
+ };
472
+ }
473
+ async function getConversationIndexStore(state) {
474
+ const redisStateAdapter = await getDefaultRedisStateAdapterFor(state);
475
+ if (redisStateAdapter) {
476
+ return redisConversationIndexStore(redisStateAdapter.getClient());
477
+ }
478
+ return emulatedConversationIndexStore(state);
479
+ }
480
+ async function upsertIndexEntry(args) {
481
+ const index = await getConversationIndexStore(args.state);
482
+ await index.upsert({
483
+ conversationId: args.conversationId,
484
+ indexKey: args.indexKey,
485
+ score: args.score
486
+ });
487
+ }
488
+ async function removeIndexEntry(args) {
489
+ const index = await getConversationIndexStore(args.state);
490
+ await index.remove({
491
+ conversationId: args.conversationId,
492
+ indexKey: args.indexKey
493
+ });
494
+ }
495
+ async function acquireMutationLock(state, conversationId) {
496
+ const startedAtMs = now();
497
+ while (true) {
498
+ const lock = await state.acquireLock(
499
+ mutationLockKey(conversationId),
500
+ CONVERSATION_MUTATION_LOCK_TTL_MS
501
+ );
502
+ if (lock) {
503
+ return lock;
504
+ }
505
+ if (now() - startedAtMs >= CONVERSATION_MUTATION_WAIT_MS) {
506
+ throw new Error(
507
+ `Could not acquire conversation mutation lock for ${conversationId}`
508
+ );
509
+ }
510
+ await sleep(CONVERSATION_MUTATION_RETRY_MS);
511
+ }
512
+ }
513
+ async function withConversationMutation(args, callback) {
514
+ const state = await getConnectedState(args.state);
515
+ const lock = await acquireMutationLock(state, args.conversationId);
516
+ try {
517
+ return await callback(state);
518
+ } finally {
519
+ await state.releaseLock(lock);
520
+ }
521
+ }
522
+ async function readConversation(state, conversationId) {
523
+ const raw = await state.get(conversationKey(conversationId));
524
+ if (raw == null) {
525
+ return void 0;
526
+ }
527
+ const conversation = normalizeConversation(conversationId, raw);
528
+ if (!conversation) {
529
+ throw new InvalidConversationRecordError(conversationId);
530
+ }
531
+ return conversation;
532
+ }
533
+ async function writeConversation(state, conversation) {
534
+ const execution = executionWithPendingMessages(
535
+ conversation.execution,
536
+ conversation.execution.pendingMessages
537
+ );
538
+ const next = {
539
+ ...conversation,
540
+ execution
541
+ };
542
+ await state.set(
543
+ conversationKey(next.conversationId),
544
+ next,
545
+ JUNIOR_THREAD_STATE_TTL_MS
546
+ );
547
+ await upsertIndexEntry({
548
+ state,
549
+ indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
550
+ conversationId: next.conversationId,
551
+ score: next.lastActivityAtMs
552
+ });
553
+ if (!hasRunnableWork(next)) {
554
+ await removeIndexEntry({
555
+ state,
556
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
557
+ conversationId: next.conversationId
558
+ });
559
+ return;
560
+ }
561
+ await upsertIndexEntry({
562
+ state,
563
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
564
+ conversationId: next.conversationId,
565
+ score: next.execution.updatedAtMs ?? next.updatedAtMs
566
+ });
567
+ }
568
+ function assertSameConversationDestination(args) {
569
+ if (!args.current || sameDestination(args.current, args.next)) {
570
+ return;
571
+ }
572
+ throw new Error(
573
+ `Conversation destination changed for ${args.conversationId}`
574
+ );
575
+ }
576
+ function conversationWorkState(conversation) {
577
+ const lease = conversation.execution.lease;
578
+ return {
579
+ ...conversation,
580
+ lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
581
+ ...lease ? {
582
+ lease: {
583
+ acquiredAtMs: lease.acquiredAtMs,
584
+ lastCheckInAtMs: lease.lastCheckInAtMs,
585
+ leaseExpiresAtMs: lease.expiresAtMs,
586
+ leaseToken: lease.token
587
+ }
588
+ } : {},
589
+ messages: pendingMessages(conversation),
590
+ needsRun: hasRunnableWork(conversation)
591
+ };
592
+ }
593
+ async function getConversation(args) {
594
+ const state = await getConnectedState(args.state);
595
+ return await readConversation(state, args.conversationId);
596
+ }
597
+ async function getConversationWorkState(args) {
598
+ const conversation = await getConversation(args);
599
+ return conversation ? conversationWorkState(conversation) : void 0;
600
+ }
601
+ function countPendingConversationMessages(conversation) {
602
+ return pendingMessages(conversation).length;
603
+ }
604
+ function hasRunnableConversationWork(conversation) {
605
+ return hasRunnableWork(conversation);
606
+ }
607
+ async function appendInboundMessage(args) {
608
+ const nowMs = args.nowMs ?? now();
609
+ return await withConversationMutation(
610
+ { conversationId: args.message.conversationId, state: args.state },
611
+ async (state) => {
612
+ const current = await readConversation(state, args.message.conversationId) ?? emptyConversation({
613
+ conversationId: args.message.conversationId,
614
+ destination: args.message.destination,
615
+ nowMs,
616
+ source: args.message.source
617
+ });
618
+ assertSameConversationDestination({
619
+ conversationId: args.message.conversationId,
620
+ current: current.destination,
621
+ next: args.message.destination
622
+ });
623
+ const existingPending = current.execution.pendingMessages.some(
624
+ (message) => message.inboundMessageId === args.message.inboundMessageId
625
+ );
626
+ const existing = current.execution.inboundMessageIds.includes(
627
+ args.message.inboundMessageId
628
+ );
629
+ if (existing) {
630
+ if (!existingPending) {
631
+ return { status: "duplicate" };
632
+ }
633
+ const nextStatus = current.execution.status === "idle" ? "pending" : current.execution.status;
634
+ await writeConversation(
635
+ state,
636
+ withExecutionUpdate(
637
+ current,
638
+ {
639
+ ...current.execution,
640
+ status: nextStatus,
641
+ inboundMessageIds: [
642
+ ...current.execution.inboundMessageIds,
643
+ args.message.inboundMessageId
644
+ ]
645
+ },
646
+ nowMs
647
+ )
648
+ );
649
+ return { status: "duplicate" };
650
+ }
651
+ const status = current.execution.lease && current.execution.status === "running" ? "running" : current.execution.lease ? "awaiting_resume" : "pending";
652
+ const next = {
653
+ ...current,
654
+ destination: current.destination ?? args.message.destination,
655
+ source: current.source ?? args.message.source,
656
+ lastActivityAtMs: nowMs
657
+ };
658
+ await writeConversation(
659
+ state,
660
+ withExecutionUpdate(
661
+ next,
662
+ {
663
+ ...current.execution,
664
+ status,
665
+ inboundMessageIds: [
666
+ ...current.execution.inboundMessageIds,
667
+ args.message.inboundMessageId
668
+ ],
669
+ pendingMessages: [
670
+ ...current.execution.pendingMessages,
671
+ args.message
672
+ ].sort(compareMessages)
673
+ },
674
+ nowMs
675
+ )
676
+ );
677
+ return { status: "appended" };
678
+ }
679
+ );
680
+ }
681
+ async function appendAndEnqueueInboundMessage(args) {
682
+ const nowMs = args.nowMs ?? now();
683
+ const appendResult = await appendInboundMessage({
684
+ message: args.message,
685
+ nowMs,
686
+ state: args.state
687
+ });
688
+ let idempotencyKey = args.message.inboundMessageId;
689
+ if (appendResult.status === "duplicate") {
690
+ const conversation = await getConversation({
691
+ conversationId: args.message.conversationId,
692
+ state: args.state
693
+ });
694
+ if (!conversation || hasRecentEnqueueMarker(conversation, nowMs)) {
695
+ return appendResult;
696
+ }
697
+ const duplicateStillPending = conversation.execution.pendingMessages.some(
698
+ (message) => message.inboundMessageId === args.message.inboundMessageId
699
+ );
700
+ if (!duplicateStillPending) {
701
+ return appendResult;
702
+ }
703
+ idempotencyKey = duplicateInboundNudgeIdempotencyKey(args.message, nowMs);
704
+ }
705
+ const queueResult = await args.queue.send(
706
+ {
707
+ conversationId: args.message.conversationId,
708
+ destination: args.message.destination
709
+ },
710
+ { idempotencyKey }
711
+ );
712
+ await markConversationWorkEnqueued({
713
+ conversationId: args.message.conversationId,
714
+ nowMs,
715
+ state: args.state
716
+ });
717
+ return {
718
+ ...appendResult,
719
+ queueMessageId: queueResult?.messageId
720
+ };
721
+ }
722
+ async function requestConversationWork(args) {
723
+ const nowMs = args.nowMs ?? now();
724
+ return await withConversationMutation(args, async (state) => {
725
+ const existing = await readConversation(state, args.conversationId);
726
+ if (existing) {
727
+ assertSameConversationDestination({
728
+ conversationId: args.conversationId,
729
+ current: existing.destination,
730
+ next: args.destination
731
+ });
732
+ }
733
+ const current = existing ?? emptyConversation({
734
+ conversationId: args.conversationId,
735
+ destination: args.destination,
736
+ nowMs
737
+ });
738
+ const status = current.execution.lease ? "awaiting_resume" : "pending";
739
+ await writeConversation(
740
+ state,
741
+ withExecutionUpdate(
742
+ {
743
+ ...current,
744
+ destination: current.destination ?? args.destination
745
+ },
746
+ {
747
+ ...current.execution,
748
+ status
749
+ },
750
+ nowMs
751
+ )
752
+ );
753
+ return { status: existing === void 0 ? "created" : "updated" };
754
+ });
755
+ }
756
+ async function recordConversationActivity(args) {
757
+ const nowMs = args.nowMs ?? now();
758
+ const activityAtMs = args.activityAtMs ?? nowMs;
759
+ await withConversationMutation(args, async (state) => {
760
+ const existing = await readConversation(state, args.conversationId);
761
+ if (existing && args.destination) {
762
+ assertSameConversationDestination({
763
+ conversationId: args.conversationId,
764
+ current: existing.destination,
765
+ next: args.destination
766
+ });
767
+ }
768
+ const current = existing ?? emptyConversation({
769
+ conversationId: args.conversationId,
770
+ destination: args.destination,
771
+ nowMs,
772
+ source: args.source
773
+ });
774
+ await writeConversation(state, {
775
+ ...current,
776
+ ...current.destination ?? args.destination ? { destination: current.destination ?? args.destination } : {},
777
+ ...current.source ?? args.source ? { source: current.source ?? args.source } : {},
778
+ ...current.channelName ?? args.channelName ? { channelName: current.channelName ?? args.channelName } : {},
779
+ ...current.requester ?? args.requester ? { requester: current.requester ?? args.requester } : {},
780
+ ...current.title ?? args.title ? { title: current.title ?? args.title } : {},
781
+ lastActivityAtMs: Math.max(current.lastActivityAtMs, activityAtMs),
782
+ updatedAtMs: nowMs,
783
+ execution: executionWithPendingMessages(
784
+ current.execution,
785
+ current.execution.pendingMessages
786
+ )
787
+ });
788
+ });
789
+ }
790
+ async function markConversationWorkEnqueued(args) {
791
+ const nowMs = args.nowMs ?? now();
792
+ await withConversationMutation(args, async (state) => {
793
+ const current = await readConversation(state, args.conversationId);
794
+ if (!current) {
795
+ return;
796
+ }
797
+ await writeConversation(
798
+ state,
799
+ withExecutionUpdate(
800
+ current,
801
+ {
802
+ ...current.execution,
803
+ lastEnqueuedAtMs: nowMs
804
+ },
805
+ nowMs
806
+ )
807
+ );
808
+ });
809
+ }
810
+ async function startConversationWork(args) {
811
+ const nowMs = args.nowMs ?? now();
812
+ return await withConversationMutation(args, async (state) => {
813
+ const current = await readConversation(state, args.conversationId);
814
+ if (!current) {
815
+ return { status: "no_work" };
816
+ }
817
+ if (isLeaseActive(current.execution.lease, nowMs)) {
818
+ return {
819
+ status: "active",
820
+ leaseExpiresAtMs: current.execution.lease.expiresAtMs
821
+ };
822
+ }
823
+ if (!hasRunnableWork(current)) {
824
+ return { status: "no_work" };
825
+ }
826
+ const lease = {
827
+ token: randomUUID(),
828
+ acquiredAtMs: nowMs,
829
+ lastCheckInAtMs: nowMs,
830
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
831
+ };
832
+ await writeConversation(
833
+ state,
834
+ withExecutionUpdate(
835
+ current,
836
+ {
837
+ ...current.execution,
838
+ lease,
839
+ status: "running",
840
+ runId: current.execution.runId ?? randomUUID(),
841
+ lastEnqueuedAtMs: void 0
842
+ },
843
+ nowMs
844
+ )
845
+ );
846
+ return {
847
+ status: "acquired",
848
+ leaseToken: lease.token,
849
+ leaseExpiresAtMs: lease.expiresAtMs
850
+ };
851
+ });
852
+ }
853
+ async function checkInConversationWork(args) {
854
+ const nowMs = args.nowMs ?? now();
855
+ return await withConversationMutation(args, async (state) => {
856
+ const current = await readConversation(state, args.conversationId);
857
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
858
+ return false;
859
+ }
860
+ await writeConversation(
861
+ state,
862
+ withExecutionUpdate(
863
+ current,
864
+ {
865
+ ...current.execution,
866
+ lease: {
867
+ ...current.execution.lease,
868
+ lastCheckInAtMs: nowMs,
869
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
870
+ }
871
+ },
872
+ nowMs
873
+ )
874
+ );
875
+ return true;
876
+ });
877
+ }
878
+ async function drainConversationMailbox(args) {
879
+ const nowMs = args.nowMs ?? now();
880
+ const pending = await withConversationMutation(args, async (state) => {
881
+ const current = await readConversation(state, args.conversationId);
882
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
883
+ throw new Error(
884
+ `Conversation lease is not held for ${args.conversationId}`
885
+ );
886
+ }
887
+ return pendingMessages(current);
888
+ });
889
+ if (pending.length === 0) {
890
+ return [];
891
+ }
892
+ await args.inject(pending);
893
+ await withConversationMutation(args, async (state) => {
894
+ const current = await readConversation(state, args.conversationId);
895
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
896
+ throw new Error(
897
+ `Conversation lease is not held for ${args.conversationId}`
898
+ );
899
+ }
900
+ const drainedIds = new Set(
901
+ pending.map((message) => message.inboundMessageId)
902
+ );
903
+ const pendingMessages2 = current.execution.pendingMessages.filter(
904
+ (message) => !drainedIds.has(message.inboundMessageId)
905
+ );
906
+ await writeConversation(
907
+ state,
908
+ withExecutionUpdate(
909
+ current,
910
+ {
911
+ ...current.execution,
912
+ status: current.execution.status === "pending" && pendingMessages2.length === 0 ? "running" : current.execution.status,
913
+ pendingMessages: pendingMessages2
914
+ },
915
+ nowMs
916
+ )
917
+ );
918
+ });
919
+ return pending;
920
+ }
921
+ async function markConversationMessagesInjected(args) {
922
+ const nowMs = args.nowMs ?? now();
923
+ const inboundMessageIds = new Set(args.inboundMessageIds);
924
+ return await withConversationMutation(args, async (state) => {
925
+ const current = await readConversation(state, args.conversationId);
926
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
927
+ return false;
928
+ }
929
+ if (inboundMessageIds.size === 0) {
930
+ return true;
931
+ }
932
+ const pendingMessages2 = current.execution.pendingMessages.filter(
933
+ (message) => !inboundMessageIds.has(message.inboundMessageId)
934
+ );
935
+ if (pendingMessages2.length === current.execution.pendingMessages.length) {
936
+ return true;
937
+ }
938
+ await writeConversation(
939
+ state,
940
+ withExecutionUpdate(
941
+ current,
942
+ {
943
+ ...current.execution,
944
+ pendingMessages: pendingMessages2
945
+ },
946
+ nowMs
947
+ )
948
+ );
949
+ return true;
950
+ });
951
+ }
952
+ async function requestConversationContinuation(args) {
953
+ const nowMs = args.nowMs ?? now();
954
+ return await withConversationMutation(args, async (state) => {
955
+ const current = await readConversation(state, args.conversationId);
956
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
957
+ return false;
958
+ }
959
+ assertSameConversationDestination({
960
+ conversationId: args.conversationId,
961
+ current: current.destination,
962
+ next: args.destination
963
+ });
964
+ await writeConversation(
965
+ state,
966
+ withExecutionUpdate(
967
+ current,
968
+ {
969
+ ...current.execution,
970
+ status: "awaiting_resume"
971
+ },
972
+ nowMs
973
+ )
974
+ );
975
+ return true;
976
+ });
977
+ }
978
+ async function releaseConversationWork(args) {
979
+ const nowMs = args.nowMs ?? now();
980
+ return await withConversationMutation(args, async (state) => {
981
+ const current = await readConversation(state, args.conversationId);
982
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
983
+ return false;
984
+ }
985
+ await writeConversation(
986
+ state,
987
+ withExecutionUpdate(
988
+ current,
989
+ {
990
+ ...current.execution,
991
+ lease: void 0,
992
+ status: current.execution.status === "running" ? "pending" : current.execution.status
993
+ },
994
+ nowMs
995
+ )
996
+ );
997
+ return true;
998
+ });
999
+ }
1000
+ async function completeConversationWork(args) {
1001
+ const nowMs = args.nowMs ?? now();
1002
+ return await withConversationMutation(args, async (state) => {
1003
+ const current = await readConversation(state, args.conversationId);
1004
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
1005
+ return "lost_lease";
1006
+ }
1007
+ const hasPending = pendingMessages(current).length > 0;
1008
+ const needsRun = current.execution.status === "awaiting_resume";
1009
+ const runnable = needsRun || hasPending;
1010
+ await writeConversation(
1011
+ state,
1012
+ withExecutionUpdate(
1013
+ current,
1014
+ {
1015
+ ...current.execution,
1016
+ lease: void 0,
1017
+ status: runnable ? "pending" : "idle",
1018
+ runId: runnable ? current.execution.runId : void 0
1019
+ },
1020
+ nowMs
1021
+ )
1022
+ );
1023
+ return runnable ? "pending" : "completed";
1024
+ });
1025
+ }
1026
+ async function clearExpiredConversationLease(args) {
1027
+ const nowMs = args.nowMs ?? now();
1028
+ return await withConversationMutation(args, async (state) => {
1029
+ const current = await readConversation(state, args.conversationId);
1030
+ if (!current?.execution.lease || current.execution.lease.expiresAtMs > nowMs) {
1031
+ return false;
1032
+ }
1033
+ await writeConversation(
1034
+ state,
1035
+ withExecutionUpdate(
1036
+ current,
1037
+ {
1038
+ ...current.execution,
1039
+ lease: void 0,
1040
+ status: "pending"
1041
+ },
1042
+ nowMs
1043
+ )
1044
+ );
1045
+ return true;
1046
+ });
1047
+ }
1048
+ async function removeActiveConversation(args) {
1049
+ const state = await getConnectedState(args.state);
1050
+ await removeIndexEntry({
1051
+ state,
1052
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1053
+ conversationId: args.conversationId
1054
+ });
1055
+ }
1056
+ async function listActiveConversationIds(args = {}) {
1057
+ const state = await getConnectedState(args.state);
1058
+ const index = await getConversationIndexStore(state);
1059
+ const entries = await index.list({
1060
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1061
+ limit: args.limit,
1062
+ order: "asc",
1063
+ scoreMax: args.staleBeforeMs
1064
+ });
1065
+ return entries.map((entry) => entry.conversationId);
1066
+ }
1067
+ async function listConversationsByActivity(args = {}) {
1068
+ const state = await getConnectedState(args.state);
1069
+ const index = await getConversationIndexStore(state);
1070
+ const entries = await index.list({
1071
+ indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
1072
+ limit: args.limit ?? CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH,
1073
+ order: "desc"
1074
+ });
1075
+ const conversations = [];
1076
+ for (const entry of entries) {
1077
+ try {
1078
+ const conversation = await readConversation(state, entry.conversationId);
1079
+ if (conversation) {
1080
+ conversations.push(conversation);
1081
+ }
1082
+ } catch (error) {
1083
+ if (!(error instanceof InvalidConversationRecordError)) {
1084
+ throw error;
1085
+ }
1086
+ await removeIndexEntry({
1087
+ state,
1088
+ indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
1089
+ conversationId: entry.conversationId
1090
+ });
1091
+ }
1092
+ }
1093
+ return conversations;
1094
+ }
1095
+
1096
+ export {
1097
+ JUNIOR_THREAD_STATE_TTL_MS,
1098
+ CONVERSATION_WORK_CHECK_IN_INTERVAL_MS,
1099
+ CONVERSATION_WORK_STALE_ENQUEUE_MS,
1100
+ getConversation,
1101
+ getConversationWorkState,
1102
+ countPendingConversationMessages,
1103
+ hasRunnableConversationWork,
1104
+ appendAndEnqueueInboundMessage,
1105
+ requestConversationWork,
1106
+ recordConversationActivity,
1107
+ markConversationWorkEnqueued,
1108
+ startConversationWork,
1109
+ checkInConversationWork,
1110
+ drainConversationMailbox,
1111
+ markConversationMessagesInjected,
1112
+ requestConversationContinuation,
1113
+ releaseConversationWork,
1114
+ completeConversationWork,
1115
+ clearExpiredConversationLease,
1116
+ removeActiveConversation,
1117
+ listActiveConversationIds,
1118
+ listConversationsByActivity
1119
+ };