@sentry/junior 0.73.0 → 0.74.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 (54) hide show
  1. package/dist/api-reference.d.ts +1 -1
  2. package/dist/app.js +258 -62
  3. package/dist/chat/app/production.d.ts +3 -0
  4. package/dist/chat/config.d.ts +3 -0
  5. package/dist/chat/conversations/configured.d.ts +5 -0
  6. package/dist/chat/conversations/sql/migrations.d.ts +11 -0
  7. package/dist/chat/conversations/sql/schema/conversations.d.ts +435 -0
  8. package/dist/chat/conversations/sql/schema/destinations.d.ts +200 -0
  9. package/dist/chat/conversations/sql/schema/identities.d.ts +214 -0
  10. package/dist/chat/conversations/sql/schema/migrations.d.ts +58 -0
  11. package/dist/chat/conversations/sql/schema/timestamps.d.ts +1 -0
  12. package/dist/chat/conversations/sql/schema.d.ts +910 -0
  13. package/dist/chat/conversations/sql/store.d.ts +52 -0
  14. package/dist/chat/conversations/state.d.ts +4 -0
  15. package/dist/chat/conversations/store.d.ts +57 -0
  16. package/dist/chat/ingress/slack-webhook.d.ts +2 -0
  17. package/dist/chat/plugins/agent-hooks.d.ts +2 -2
  18. package/dist/chat/respond.d.ts +1 -1
  19. package/dist/chat/services/mcp-auth-orchestration.d.ts +6 -5
  20. package/dist/chat/services/pending-auth.d.ts +2 -0
  21. package/dist/chat/services/plugin-auth-orchestration.d.ts +7 -6
  22. package/dist/chat/sql/db.d.ts +20 -0
  23. package/dist/chat/sql/neon.d.ts +9 -0
  24. package/dist/chat/sql/schema.d.ts +906 -0
  25. package/dist/chat/state/turn-session.d.ts +3 -0
  26. package/dist/chat/task-execution/slack-work.d.ts +2 -0
  27. package/dist/chat/task-execution/state.d.ts +209 -0
  28. package/dist/chat/task-execution/store.d.ts +30 -114
  29. package/dist/chat/task-execution/vercel-callback.d.ts +2 -0
  30. package/dist/chat/task-execution/worker.d.ts +2 -0
  31. package/dist/{chunk-ZDA2HYX5.js → chunk-2LUZA3LY.js} +3 -3
  32. package/dist/{chunk-RY6AL5C7.js → chunk-6UP2Z2RZ.js} +2 -2
  33. package/dist/{chunk-DIMX5F3T.js → chunk-F6HWCPOC.js} +1 -1
  34. package/dist/{chunk-WS2EG3GW.js → chunk-GM7HTXYC.js} +6 -0
  35. package/dist/{chunk-UZVHXZ7V.js → chunk-HYHKTFG2.js} +59 -15
  36. package/dist/chunk-JL2SLRAT.js +1970 -0
  37. package/dist/{chunk-OQSYYOLM.js → chunk-SQGMG7OD.js} +128 -114
  38. package/dist/{chunk-QUXPUKBH.js → chunk-Y7X25LFY.js} +1 -1
  39. package/dist/{chunk-UOTZ3EEQ.js → chunk-YOHFWWBV.js} +1 -1
  40. package/dist/{chunk-V4VYUY4A.js → chunk-YRDS7VKO.js} +1 -1
  41. package/dist/cli/chat.js +2 -2
  42. package/dist/cli/init.js +1 -1
  43. package/dist/cli/snapshot-warmup.js +3 -3
  44. package/dist/cli/upgrade.js +77 -7
  45. package/dist/instrumentation.js +0 -1
  46. package/dist/nitro.js +3 -3
  47. package/dist/reporting/conversations.d.ts +13 -3
  48. package/dist/reporting.d.ts +9 -2
  49. package/dist/reporting.js +101 -37
  50. package/dist/{runner-LMAM4OGD.js → runner-27NP2TEO.js} +7 -7
  51. package/dist/vercel.d.ts +6 -1
  52. package/dist/vercel.js +1 -1
  53. package/package.json +9 -4
  54. package/dist/chunk-AL5T52ZD.js +0 -1119
@@ -0,0 +1,1970 @@
1
+ import {
2
+ parseDestination,
3
+ sameDestination
4
+ } from "./chunk-YRDS7VKO.js";
5
+ import {
6
+ getDefaultRedisStateAdapterFor,
7
+ getStateAdapter
8
+ } from "./chunk-F6HWCPOC.js";
9
+ import {
10
+ getChatConfig
11
+ } from "./chunk-GM7HTXYC.js";
12
+ import {
13
+ parseStoredSlackRequester
14
+ } from "./chunk-CYUI7JU5.js";
15
+ import {
16
+ isRecord,
17
+ toOptionalNumber,
18
+ toOptionalString
19
+ } from "./chunk-3BYAPS6B.js";
20
+
21
+ // src/chat/state/ttl.ts
22
+ var JUNIOR_THREAD_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
23
+
24
+ // src/chat/sql/neon.ts
25
+ import { AsyncLocalStorage } from "async_hooks";
26
+ import {
27
+ Pool
28
+ } from "@neondatabase/serverless";
29
+ import { drizzle } from "drizzle-orm/neon-serverless";
30
+
31
+ // src/chat/conversations/sql/schema/conversations.ts
32
+ import { sql } from "drizzle-orm";
33
+ import { index as index3, integer, jsonb as jsonb3, pgTable as pgTable3, text as text3 } from "drizzle-orm/pg-core";
34
+
35
+ // src/chat/conversations/sql/schema/destinations.ts
36
+ import { index, jsonb, pgTable, text, uniqueIndex } from "drizzle-orm/pg-core";
37
+
38
+ // src/chat/conversations/sql/schema/timestamps.ts
39
+ import { timestamp } from "drizzle-orm/pg-core";
40
+ var timestamptz = (name) => timestamp(name, { withTimezone: true });
41
+
42
+ // src/chat/conversations/sql/schema/destinations.ts
43
+ var juniorDestinations = pgTable(
44
+ "junior_destinations",
45
+ {
46
+ id: text("id").primaryKey(),
47
+ provider: text("provider").notNull(),
48
+ providerTenantId: text("provider_tenant_id").notNull().default(""),
49
+ providerDestinationId: text("provider_destination_id").notNull(),
50
+ kind: text("kind").$type().notNull(),
51
+ parentDestinationId: text("parent_destination_id"),
52
+ displayName: text("display_name"),
53
+ visibility: text("visibility").$type().notNull().default("unknown"),
54
+ metadata: jsonb("metadata_json"),
55
+ createdAt: timestamptz("created_at").notNull(),
56
+ updatedAt: timestamptz("updated_at").notNull()
57
+ },
58
+ (table) => [
59
+ uniqueIndex("junior_destinations_provider_destination_uidx").on(
60
+ table.provider,
61
+ table.providerTenantId,
62
+ table.providerDestinationId
63
+ ),
64
+ index("junior_destinations_provider_kind_idx").on(
65
+ table.provider,
66
+ table.kind
67
+ )
68
+ ]
69
+ );
70
+
71
+ // src/chat/conversations/sql/schema/identities.ts
72
+ import { index as index2, jsonb as jsonb2, pgTable as pgTable2, text as text2, uniqueIndex as uniqueIndex2 } from "drizzle-orm/pg-core";
73
+ var juniorIdentities = pgTable2(
74
+ "junior_identities",
75
+ {
76
+ id: text2("id").primaryKey(),
77
+ kind: text2("kind").$type().notNull(),
78
+ provider: text2("provider").notNull(),
79
+ providerTenantId: text2("provider_tenant_id").notNull().default(""),
80
+ providerSubjectId: text2("provider_subject_id").notNull(),
81
+ displayName: text2("display_name"),
82
+ handle: text2("handle"),
83
+ email: text2("email"),
84
+ avatarUrl: text2("avatar_url"),
85
+ metadata: jsonb2("metadata_json"),
86
+ createdAt: timestamptz("created_at").notNull(),
87
+ updatedAt: timestamptz("updated_at").notNull()
88
+ },
89
+ (table) => [
90
+ uniqueIndex2("junior_identities_provider_subject_uidx").on(
91
+ table.provider,
92
+ table.providerTenantId,
93
+ table.providerSubjectId
94
+ ),
95
+ index2("junior_identities_kind_provider_idx").on(table.kind, table.provider)
96
+ ]
97
+ );
98
+
99
+ // src/chat/conversations/sql/schema/conversations.ts
100
+ var juniorConversations = pgTable3(
101
+ "junior_conversations",
102
+ {
103
+ conversationId: text3("conversation_id").primaryKey(),
104
+ schemaVersion: integer("schema_version").notNull().default(1),
105
+ source: text3("source").$type(),
106
+ originType: text3("origin_type"),
107
+ originId: text3("origin_id"),
108
+ originRunId: text3("origin_run_id"),
109
+ destinationId: text3("destination_id").references(
110
+ () => juniorDestinations.id
111
+ ),
112
+ destination: jsonb3("destination_json").$type(),
113
+ actorIdentityId: text3("actor_identity_id").references(
114
+ () => juniorIdentities.id
115
+ ),
116
+ requesterIdentityId: text3("requester_identity_id").references(
117
+ () => juniorIdentities.id
118
+ ),
119
+ creatorIdentityId: text3("creator_identity_id").references(
120
+ () => juniorIdentities.id
121
+ ),
122
+ credentialSubjectIdentityId: text3(
123
+ "credential_subject_identity_id"
124
+ ).references(() => juniorIdentities.id),
125
+ requester: jsonb3("requester_json").$type(),
126
+ channelName: text3("channel_name"),
127
+ title: text3("title"),
128
+ createdAt: timestamptz("created_at").notNull(),
129
+ lastActivityAt: timestamptz("last_activity_at").notNull(),
130
+ updatedAt: timestamptz("updated_at").notNull(),
131
+ executionUpdatedAt: timestamptz("execution_updated_at"),
132
+ executionStatus: text3("execution_status").$type().notNull(),
133
+ runId: text3("run_id"),
134
+ lastCheckpointAt: timestamptz("last_checkpoint_at"),
135
+ lastEnqueuedAt: timestamptz("last_enqueued_at")
136
+ },
137
+ (table) => [
138
+ index3("junior_conversations_last_activity_idx").on(
139
+ table.lastActivityAt.desc(),
140
+ table.conversationId
141
+ ),
142
+ index3("junior_conversations_active_idx").using(
143
+ "btree",
144
+ sql`coalesce(${table.executionUpdatedAt}, ${table.updatedAt})`,
145
+ table.conversationId
146
+ ).where(sql`${table.executionStatus} <> 'idle'`),
147
+ index3("junior_conversations_destination_activity_idx").on(
148
+ table.destinationId,
149
+ table.lastActivityAt.desc()
150
+ ),
151
+ index3("junior_conversations_actor_activity_idx").on(
152
+ table.actorIdentityId,
153
+ table.lastActivityAt.desc()
154
+ ),
155
+ index3("junior_conversations_requester_activity_idx").on(
156
+ table.requesterIdentityId,
157
+ table.lastActivityAt.desc()
158
+ ),
159
+ index3("junior_conversations_origin_idx").on(
160
+ table.originType,
161
+ table.originId,
162
+ table.lastActivityAt.desc()
163
+ )
164
+ ]
165
+ );
166
+
167
+ // src/chat/conversations/sql/schema/migrations.ts
168
+ import { pgTable as pgTable4, text as text4 } from "drizzle-orm/pg-core";
169
+ var juniorSchemaMigrations = pgTable4("junior_schema_migrations", {
170
+ id: text4("id").primaryKey(),
171
+ checksum: text4("checksum").notNull(),
172
+ appliedAt: timestamptz("applied_at").notNull().defaultNow()
173
+ });
174
+
175
+ // src/chat/conversations/sql/schema.ts
176
+ var schema = {
177
+ juniorConversations,
178
+ juniorDestinations,
179
+ juniorIdentities,
180
+ juniorSchemaMigrations
181
+ };
182
+
183
+ // src/chat/sql/schema.ts
184
+ var juniorSqlSchema = {
185
+ ...schema
186
+ };
187
+
188
+ // src/chat/sql/neon.ts
189
+ var NeonExecutor = class {
190
+ constructor(pool) {
191
+ this.pool = pool;
192
+ }
193
+ pool;
194
+ transactionClient = new AsyncLocalStorage();
195
+ db() {
196
+ return drizzle(this.queryClient(), {
197
+ schema: juniorSqlSchema
198
+ });
199
+ }
200
+ async execute(statement, params = []) {
201
+ await this.queryClient().query(statement, [...params]);
202
+ }
203
+ async query(statement, params = []) {
204
+ const result = await this.queryClient().query(statement, [
205
+ ...params
206
+ ]);
207
+ return result.rows;
208
+ }
209
+ async transaction(callback) {
210
+ const existingClient = this.transactionClient.getStore();
211
+ if (existingClient) {
212
+ return await callback();
213
+ }
214
+ const client = await this.pool.connect();
215
+ try {
216
+ await client.query("BEGIN");
217
+ const result = await this.transactionClient.run(client, callback);
218
+ await client.query("COMMIT");
219
+ return result;
220
+ } catch (error) {
221
+ await client.query("ROLLBACK");
222
+ throw error;
223
+ } finally {
224
+ client.release();
225
+ }
226
+ }
227
+ async withLock(lockName, callback) {
228
+ if (!lockName) {
229
+ throw new Error("SQL lock name is required");
230
+ }
231
+ const existingClient = this.transactionClient.getStore();
232
+ if (existingClient) {
233
+ await existingClient.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
234
+ lockName
235
+ ]);
236
+ return await callback();
237
+ }
238
+ const client = await this.pool.connect();
239
+ try {
240
+ await client.query("BEGIN");
241
+ return await this.transactionClient.run(client, async () => {
242
+ try {
243
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
244
+ lockName
245
+ ]);
246
+ const result = await callback();
247
+ await client.query("COMMIT");
248
+ return result;
249
+ } catch (error) {
250
+ await client.query("ROLLBACK");
251
+ throw error;
252
+ }
253
+ });
254
+ } finally {
255
+ client.release();
256
+ }
257
+ }
258
+ async close() {
259
+ await this.pool.end();
260
+ }
261
+ queryClient() {
262
+ return this.transactionClient.getStore() ?? this.pool;
263
+ }
264
+ };
265
+ function createNeonJuniorSqlExecutor(args) {
266
+ return new NeonExecutor(
267
+ new Pool({
268
+ connectionString: args.connectionString,
269
+ max: 3
270
+ })
271
+ );
272
+ }
273
+
274
+ // src/chat/task-execution/state.ts
275
+ import { randomUUID } from "crypto";
276
+ var CONVERSATION_PREFIX = "junior:conversation";
277
+ var CONVERSATION_SCHEMA_VERSION = 1;
278
+ var CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH = 1e4;
279
+ var CONVERSATION_INDEX_LOCK_TTL_MS = 1e4;
280
+ var CONVERSATION_INDEX_LOCK_WAIT_MS = 2e3;
281
+ var CONVERSATION_INDEX_LOCK_RETRY_MS = 25;
282
+ var CONVERSATION_MUTATION_LOCK_TTL_MS = 1e4;
283
+ var CONVERSATION_MUTATION_WAIT_MS = 1e4;
284
+ var CONVERSATION_MUTATION_RETRY_MS = 25;
285
+ var InvalidConversationRecordError = class extends Error {
286
+ constructor(conversationId) {
287
+ super(`Conversation record is invalid for ${conversationId}`);
288
+ this.name = "InvalidConversationRecordError";
289
+ }
290
+ };
291
+ var CONVERSATION_BY_ACTIVITY_INDEX_KEY = `${CONVERSATION_PREFIX}:by-activity`;
292
+ var CONVERSATION_ACTIVE_INDEX_KEY = `${CONVERSATION_PREFIX}:active`;
293
+ var CONVERSATION_WORK_LEASE_TTL_MS = 9e4;
294
+ var CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15e3;
295
+ var CONVERSATION_WORK_STALE_ENQUEUE_MS = 6e4;
296
+ function conversationKey(conversationId) {
297
+ return `${CONVERSATION_PREFIX}:${conversationId}`;
298
+ }
299
+ function indexLockKey(indexKey) {
300
+ return `${indexKey}:lock`;
301
+ }
302
+ function mutationLockKey(conversationId) {
303
+ return `${CONVERSATION_PREFIX}:mutation:${conversationId}`;
304
+ }
305
+ function now() {
306
+ return Date.now();
307
+ }
308
+ function compareMessages(left, right) {
309
+ return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
310
+ }
311
+ function compareIndexDescending(left, right) {
312
+ return right.score - left.score || right.conversationId.localeCompare(left.conversationId);
313
+ }
314
+ function compareIndexAscending(left, right) {
315
+ return left.score - right.score || left.conversationId.localeCompare(right.conversationId);
316
+ }
317
+ function uniqueStrings(values) {
318
+ return [...new Set(values)];
319
+ }
320
+ function normalizeSource(value) {
321
+ if (value === "api" || value === "internal" || value === "local" || value === "plugin" || value === "scheduler" || value === "slack") {
322
+ return value;
323
+ }
324
+ return void 0;
325
+ }
326
+ function normalizeExecutionStatus(value) {
327
+ if (value === "awaiting_resume" || value === "idle" || value === "pending" || value === "running") {
328
+ return value;
329
+ }
330
+ return void 0;
331
+ }
332
+ function normalizeMetadata(value) {
333
+ if (!isRecord(value)) {
334
+ return void 0;
335
+ }
336
+ return value;
337
+ }
338
+ function normalizeInput(value) {
339
+ if (!isRecord(value)) {
340
+ return void 0;
341
+ }
342
+ const text5 = toOptionalString(value.text);
343
+ if (!text5) {
344
+ return void 0;
345
+ }
346
+ return {
347
+ text: text5,
348
+ authorId: toOptionalString(value.authorId),
349
+ attachments: Array.isArray(value.attachments) ? [...value.attachments] : void 0,
350
+ metadata: normalizeMetadata(value.metadata)
351
+ };
352
+ }
353
+ function normalizeMessage(value) {
354
+ if (!isRecord(value)) {
355
+ return void 0;
356
+ }
357
+ const conversationId = toOptionalString(value.conversationId);
358
+ const inboundMessageId = toOptionalString(value.inboundMessageId);
359
+ const source = normalizeSource(value.source);
360
+ const destination = parseDestination(value.destination);
361
+ const createdAtMs = toOptionalNumber(value.createdAtMs);
362
+ const receivedAtMs = toOptionalNumber(value.receivedAtMs);
363
+ const input = normalizeInput(value.input);
364
+ if (!conversationId || !destination || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
365
+ return void 0;
366
+ }
367
+ return {
368
+ conversationId,
369
+ destination,
370
+ inboundMessageId,
371
+ source,
372
+ createdAtMs,
373
+ receivedAtMs,
374
+ input,
375
+ injectedAtMs: toOptionalNumber(value.injectedAtMs)
376
+ };
377
+ }
378
+ function normalizeRequester(value) {
379
+ return parseStoredSlackRequester(value);
380
+ }
381
+ function normalizeLease(value) {
382
+ if (!isRecord(value)) {
383
+ return void 0;
384
+ }
385
+ const token = toOptionalString(value.token);
386
+ const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
387
+ const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
388
+ const expiresAtMs = toOptionalNumber(value.expiresAtMs);
389
+ if (!token || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof expiresAtMs !== "number") {
390
+ return void 0;
391
+ }
392
+ return {
393
+ token,
394
+ acquiredAtMs,
395
+ lastCheckInAtMs,
396
+ expiresAtMs
397
+ };
398
+ }
399
+ function normalizeExecution(conversationId, value) {
400
+ if (!isRecord(value)) {
401
+ return void 0;
402
+ }
403
+ const status = normalizeExecutionStatus(value.status);
404
+ if (!status) {
405
+ return void 0;
406
+ }
407
+ 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) : [];
408
+ const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(
409
+ value.inboundMessageIds.map((id) => typeof id === "string" ? id : void 0).filter((id) => Boolean(id))
410
+ ) : [];
411
+ const lease = normalizeLease(value.lease);
412
+ const normalizedStatus = status === "idle" && lease ? "running" : status === "idle" && pendingMessages2.length > 0 ? "pending" : status;
413
+ return {
414
+ status: normalizedStatus,
415
+ inboundMessageIds: uniqueStrings([
416
+ ...inboundMessageIds,
417
+ ...pendingMessages2.map((message) => message.inboundMessageId)
418
+ ]),
419
+ pendingCount: pendingMessages2.length,
420
+ pendingMessages: pendingMessages2,
421
+ lease,
422
+ lastCheckpointAtMs: toOptionalNumber(value.lastCheckpointAtMs),
423
+ lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
424
+ runId: toOptionalString(value.runId),
425
+ updatedAtMs: toOptionalNumber(value.updatedAtMs)
426
+ };
427
+ }
428
+ function normalizeConversation(conversationId, value) {
429
+ if (!isRecord(value) || value.schemaVersion !== CONVERSATION_SCHEMA_VERSION) {
430
+ return void 0;
431
+ }
432
+ const storedConversationId = toOptionalString(value.conversationId);
433
+ const createdAtMs = toOptionalNumber(value.createdAtMs);
434
+ const lastActivityAtMs = toOptionalNumber(value.lastActivityAtMs);
435
+ const updatedAtMs = toOptionalNumber(value.updatedAtMs);
436
+ const execution = normalizeExecution(conversationId, value.execution);
437
+ const destination = value.destination === void 0 ? void 0 : parseDestination(value.destination);
438
+ if (storedConversationId !== conversationId || typeof createdAtMs !== "number" || typeof lastActivityAtMs !== "number" || typeof updatedAtMs !== "number" || !execution || value.destination !== void 0 && !destination) {
439
+ return void 0;
440
+ }
441
+ if (execution.pendingMessages.length > 0 && (!destination || execution.pendingMessages.some(
442
+ (message) => !sameDestination(message.destination, destination)
443
+ ))) {
444
+ return void 0;
445
+ }
446
+ return {
447
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
448
+ conversationId,
449
+ createdAtMs,
450
+ lastActivityAtMs,
451
+ updatedAtMs,
452
+ execution,
453
+ ...destination ? { destination } : {},
454
+ ...toOptionalString(value.title) ? { title: toOptionalString(value.title) } : {},
455
+ ...toOptionalString(value.channelName) ? { channelName: toOptionalString(value.channelName) } : {},
456
+ ...normalizeRequester(value.requester) ? { requester: normalizeRequester(value.requester) } : {},
457
+ ...normalizeSource(value.source) ? { source: normalizeSource(value.source) } : {}
458
+ };
459
+ }
460
+ function emptyConversation(args) {
461
+ return {
462
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
463
+ conversationId: args.conversationId,
464
+ createdAtMs: args.nowMs,
465
+ lastActivityAtMs: args.nowMs,
466
+ updatedAtMs: args.nowMs,
467
+ ...args.destination ? { destination: args.destination } : {},
468
+ ...args.source ? { source: args.source } : {},
469
+ execution: {
470
+ status: "idle",
471
+ inboundMessageIds: [],
472
+ pendingCount: 0,
473
+ pendingMessages: [],
474
+ updatedAtMs: args.nowMs
475
+ }
476
+ };
477
+ }
478
+ function isLeaseActive(lease, nowMs) {
479
+ return Boolean(lease && lease.expiresAtMs > nowMs);
480
+ }
481
+ function pendingMessages(conversation) {
482
+ return [...conversation.execution.pendingMessages].sort(compareMessages);
483
+ }
484
+ function hasRunnableWork(conversation) {
485
+ return conversation.execution.status !== "idle" || pendingMessages(conversation).length > 0;
486
+ }
487
+ function executionWithPendingMessages(execution, pending) {
488
+ const pendingMessages2 = [...pending].sort(compareMessages);
489
+ const status = execution.status === "idle" && execution.lease ? "running" : execution.status === "idle" && pendingMessages2.length > 0 ? "pending" : execution.status;
490
+ return {
491
+ ...execution,
492
+ status,
493
+ inboundMessageIds: uniqueStrings([
494
+ ...execution.inboundMessageIds,
495
+ ...pendingMessages2.map((message) => message.inboundMessageId)
496
+ ]),
497
+ pendingMessages: pendingMessages2,
498
+ pendingCount: pendingMessages2.length
499
+ };
500
+ }
501
+ function withExecutionUpdate(conversation, execution, nowMs) {
502
+ return {
503
+ ...conversation,
504
+ updatedAtMs: nowMs,
505
+ execution: {
506
+ ...executionWithPendingMessages(execution, execution.pendingMessages),
507
+ updatedAtMs: nowMs
508
+ }
509
+ };
510
+ }
511
+ async function getConnectedState(stateAdapter) {
512
+ const state = stateAdapter ?? getStateAdapter();
513
+ await state.connect();
514
+ return state;
515
+ }
516
+ async function sleep(ms) {
517
+ await new Promise((resolve) => {
518
+ const timer = setTimeout(resolve, ms);
519
+ timer.unref?.();
520
+ });
521
+ }
522
+ async function withIndexLock(state, indexKey, callback) {
523
+ const startedAtMs = now();
524
+ let lock;
525
+ while (true) {
526
+ lock = await state.acquireLock(
527
+ indexLockKey(indexKey),
528
+ CONVERSATION_INDEX_LOCK_TTL_MS
529
+ );
530
+ if (lock) {
531
+ break;
532
+ }
533
+ if (now() - startedAtMs >= CONVERSATION_INDEX_LOCK_WAIT_MS) {
534
+ throw new Error(
535
+ `Could not acquire conversation index lock for ${indexKey}`
536
+ );
537
+ }
538
+ await sleep(CONVERSATION_INDEX_LOCK_RETRY_MS);
539
+ }
540
+ try {
541
+ return await callback();
542
+ } finally {
543
+ await state.releaseLock(lock);
544
+ }
545
+ }
546
+ function normalizeIndexEntry(value) {
547
+ if (!isRecord(value)) {
548
+ return void 0;
549
+ }
550
+ const conversationId = toOptionalString(value.conversationId);
551
+ const score = toOptionalNumber(value.score);
552
+ if (!conversationId || typeof score !== "number") {
553
+ return void 0;
554
+ }
555
+ return { conversationId, score };
556
+ }
557
+ function uniqueIndexEntries(value) {
558
+ if (!Array.isArray(value)) {
559
+ return [];
560
+ }
561
+ const entries = /* @__PURE__ */ new Map();
562
+ for (const item of value) {
563
+ const entry = normalizeIndexEntry(item);
564
+ if (!entry) {
565
+ continue;
566
+ }
567
+ const existing = entries.get(entry.conversationId);
568
+ if (!existing || entry.score > existing.score) {
569
+ entries.set(entry.conversationId, entry);
570
+ }
571
+ }
572
+ return [...entries.values()];
573
+ }
574
+ function retainedIndexEntries(indexKey, entries) {
575
+ if (indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
576
+ return entries.sort(compareIndexDescending).slice(0, CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH);
577
+ }
578
+ if (indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
579
+ return entries.sort(compareIndexAscending);
580
+ }
581
+ throw new Error(`Unknown conversation index ${indexKey}`);
582
+ }
583
+ function redisIndexKey(indexKey) {
584
+ const prefix = getChatConfig().state.keyPrefix;
585
+ return [...prefix ? [prefix] : [], indexKey].join(":");
586
+ }
587
+ function parseRedisIndexEntries(values) {
588
+ if (!Array.isArray(values)) {
589
+ return [];
590
+ }
591
+ const entries = [];
592
+ for (let index4 = 0; index4 < values.length; index4 += 2) {
593
+ const conversationId = toOptionalString(values[index4]);
594
+ const score = typeof values[index4 + 1] === "number" ? values[index4 + 1] : Number(values[index4 + 1]);
595
+ if (!conversationId || !Number.isFinite(score)) {
596
+ continue;
597
+ }
598
+ entries.push({ conversationId, score });
599
+ }
600
+ return entries;
601
+ }
602
+ function redisConversationIndexStore(client) {
603
+ const upsertBoundedActivityScript = `
604
+ redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
605
+ redis.call("PEXPIRE", KEYS[1], ARGV[3])
606
+ local extra = redis.call("ZCARD", KEYS[1]) - tonumber(ARGV[4])
607
+ if extra > 0 then
608
+ redis.call("ZREMRANGEBYRANK", KEYS[1], 0, extra - 1)
609
+ end
610
+ return 1
611
+ `;
612
+ return {
613
+ async list(args) {
614
+ const key = redisIndexKey(args.indexKey);
615
+ const limit = args.limit;
616
+ const offset = Math.max(0, args.offset ?? 0);
617
+ if (limit === 0) {
618
+ return [];
619
+ }
620
+ const values = args.scoreMax !== void 0 ? await client.sendCommand([
621
+ "ZRANGEBYSCORE",
622
+ key,
623
+ "-inf",
624
+ String(args.scoreMax),
625
+ "WITHSCORES",
626
+ ...limit !== void 0 || offset > 0 ? ["LIMIT", String(offset), String(limit ?? 1e9)] : []
627
+ ]) : await client.sendCommand([
628
+ args.order === "asc" ? "ZRANGE" : "ZREVRANGE",
629
+ key,
630
+ String(offset),
631
+ String(
632
+ limit === void 0 ? -1 : offset + Math.max(0, limit - 1)
633
+ ),
634
+ "WITHSCORES"
635
+ ]);
636
+ return parseRedisIndexEntries(values);
637
+ },
638
+ async remove(args) {
639
+ await client.sendCommand([
640
+ "ZREM",
641
+ redisIndexKey(args.indexKey),
642
+ args.conversationId
643
+ ]);
644
+ },
645
+ async upsert(args) {
646
+ const key = redisIndexKey(args.indexKey);
647
+ if (args.indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
648
+ await client.sendCommand([
649
+ "EVAL",
650
+ upsertBoundedActivityScript,
651
+ "1",
652
+ key,
653
+ String(args.score),
654
+ args.conversationId,
655
+ String(JUNIOR_THREAD_STATE_TTL_MS),
656
+ String(CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH)
657
+ ]);
658
+ return;
659
+ }
660
+ if (args.indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
661
+ await client.sendCommand([
662
+ "ZADD",
663
+ key,
664
+ String(args.score),
665
+ args.conversationId
666
+ ]);
667
+ await client.sendCommand([
668
+ "PEXPIRE",
669
+ key,
670
+ String(JUNIOR_THREAD_STATE_TTL_MS)
671
+ ]);
672
+ return;
673
+ }
674
+ throw new Error(`Unknown conversation index ${args.indexKey}`);
675
+ }
676
+ };
677
+ }
678
+ function emulatedConversationIndexStore(state) {
679
+ const readIndex = async (indexKey) => uniqueIndexEntries(await state.get(indexKey));
680
+ const writeIndex = async (indexKey, entries) => {
681
+ await state.set(indexKey, entries, JUNIOR_THREAD_STATE_TTL_MS);
682
+ };
683
+ return {
684
+ async list(args) {
685
+ const entries = (await readIndex(args.indexKey)).filter(
686
+ (entry) => args.scoreMax === void 0 ? true : entry.score <= args.scoreMax
687
+ ).sort(
688
+ args.order === "asc" ? compareIndexAscending : compareIndexDescending
689
+ );
690
+ const offset = Math.max(0, args.offset ?? 0);
691
+ return entries.slice(
692
+ offset,
693
+ args.limit === void 0 ? entries.length : offset + args.limit
694
+ );
695
+ },
696
+ async remove(args) {
697
+ await withIndexLock(state, args.indexKey, async () => {
698
+ const entries = await readIndex(args.indexKey);
699
+ const next = entries.filter(
700
+ (entry) => entry.conversationId !== args.conversationId
701
+ );
702
+ if (next.length === entries.length) {
703
+ return;
704
+ }
705
+ await writeIndex(args.indexKey, next);
706
+ });
707
+ },
708
+ async upsert(args) {
709
+ await withIndexLock(state, args.indexKey, async () => {
710
+ const entries = await readIndex(args.indexKey);
711
+ const withoutCurrent = entries.filter(
712
+ (entry) => entry.conversationId !== args.conversationId
713
+ );
714
+ const next = retainedIndexEntries(args.indexKey, [
715
+ ...withoutCurrent,
716
+ { conversationId: args.conversationId, score: args.score }
717
+ ]);
718
+ await writeIndex(args.indexKey, next);
719
+ });
720
+ }
721
+ };
722
+ }
723
+ async function getConversationIndexStore(state) {
724
+ const redisStateAdapter = await getDefaultRedisStateAdapterFor(state);
725
+ if (redisStateAdapter) {
726
+ return redisConversationIndexStore(redisStateAdapter.getClient());
727
+ }
728
+ return emulatedConversationIndexStore(state);
729
+ }
730
+ async function upsertIndexEntry(args) {
731
+ const index4 = await getConversationIndexStore(args.state);
732
+ await index4.upsert({
733
+ conversationId: args.conversationId,
734
+ indexKey: args.indexKey,
735
+ score: args.score
736
+ });
737
+ }
738
+ async function removeIndexEntry(args) {
739
+ const index4 = await getConversationIndexStore(args.state);
740
+ await index4.remove({
741
+ conversationId: args.conversationId,
742
+ indexKey: args.indexKey
743
+ });
744
+ }
745
+ async function acquireMutationLock(state, conversationId) {
746
+ const startedAtMs = now();
747
+ while (true) {
748
+ const lock = await state.acquireLock(
749
+ mutationLockKey(conversationId),
750
+ CONVERSATION_MUTATION_LOCK_TTL_MS
751
+ );
752
+ if (lock) {
753
+ return lock;
754
+ }
755
+ if (now() - startedAtMs >= CONVERSATION_MUTATION_WAIT_MS) {
756
+ throw new Error(
757
+ `Could not acquire conversation mutation lock for ${conversationId}`
758
+ );
759
+ }
760
+ await sleep(CONVERSATION_MUTATION_RETRY_MS);
761
+ }
762
+ }
763
+ async function withConversationMutation(args, callback) {
764
+ const state = await getConnectedState(args.state);
765
+ const lock = await acquireMutationLock(state, args.conversationId);
766
+ try {
767
+ return await callback(state);
768
+ } finally {
769
+ await state.releaseLock(lock);
770
+ }
771
+ }
772
+ async function readConversation(state, conversationId) {
773
+ const raw = await state.get(conversationKey(conversationId));
774
+ if (raw == null) {
775
+ return void 0;
776
+ }
777
+ const conversation = normalizeConversation(conversationId, raw);
778
+ if (!conversation) {
779
+ throw new InvalidConversationRecordError(conversationId);
780
+ }
781
+ return conversation;
782
+ }
783
+ async function writeConversation(state, conversation) {
784
+ const execution = executionWithPendingMessages(
785
+ conversation.execution,
786
+ conversation.execution.pendingMessages
787
+ );
788
+ const next = {
789
+ ...conversation,
790
+ execution
791
+ };
792
+ await state.set(
793
+ conversationKey(next.conversationId),
794
+ next,
795
+ JUNIOR_THREAD_STATE_TTL_MS
796
+ );
797
+ await upsertIndexEntry({
798
+ state,
799
+ indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
800
+ conversationId: next.conversationId,
801
+ score: next.lastActivityAtMs
802
+ });
803
+ if (!hasRunnableWork(next)) {
804
+ await removeIndexEntry({
805
+ state,
806
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
807
+ conversationId: next.conversationId
808
+ });
809
+ return;
810
+ }
811
+ await upsertIndexEntry({
812
+ state,
813
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
814
+ conversationId: next.conversationId,
815
+ score: next.execution.updatedAtMs ?? next.updatedAtMs
816
+ });
817
+ }
818
+ function assertSameConversationDestination(args) {
819
+ if (!args.current || sameDestination(args.current, args.next)) {
820
+ return;
821
+ }
822
+ throw new Error(
823
+ `Conversation destination changed for ${args.conversationId}`
824
+ );
825
+ }
826
+ function conversationWorkState(conversation) {
827
+ const lease = conversation.execution.lease;
828
+ return {
829
+ ...conversation,
830
+ lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
831
+ ...lease ? {
832
+ lease: {
833
+ acquiredAtMs: lease.acquiredAtMs,
834
+ lastCheckInAtMs: lease.lastCheckInAtMs,
835
+ leaseExpiresAtMs: lease.expiresAtMs,
836
+ leaseToken: lease.token
837
+ }
838
+ } : {},
839
+ messages: pendingMessages(conversation),
840
+ needsRun: hasRunnableWork(conversation)
841
+ };
842
+ }
843
+ async function getConversation(args) {
844
+ const state = await getConnectedState(args.state);
845
+ return await readConversation(state, args.conversationId);
846
+ }
847
+ async function getConversationWorkState(args) {
848
+ const conversation = await getConversation(args);
849
+ return conversation ? conversationWorkState(conversation) : void 0;
850
+ }
851
+ async function appendInboundMessage(args) {
852
+ const nowMs = args.nowMs ?? now();
853
+ return await withConversationMutation(
854
+ { conversationId: args.message.conversationId, state: args.state },
855
+ async (state) => {
856
+ const current = await readConversation(state, args.message.conversationId) ?? emptyConversation({
857
+ conversationId: args.message.conversationId,
858
+ destination: args.message.destination,
859
+ nowMs,
860
+ source: args.message.source
861
+ });
862
+ assertSameConversationDestination({
863
+ conversationId: args.message.conversationId,
864
+ current: current.destination,
865
+ next: args.message.destination
866
+ });
867
+ const existingPending = current.execution.pendingMessages.some(
868
+ (message) => message.inboundMessageId === args.message.inboundMessageId
869
+ );
870
+ const existing = current.execution.inboundMessageIds.includes(
871
+ args.message.inboundMessageId
872
+ );
873
+ if (existing) {
874
+ if (!existingPending) {
875
+ return { status: "duplicate" };
876
+ }
877
+ const nextStatus = current.execution.status === "idle" ? "pending" : current.execution.status;
878
+ await writeConversation(
879
+ state,
880
+ withExecutionUpdate(
881
+ current,
882
+ {
883
+ ...current.execution,
884
+ status: nextStatus
885
+ },
886
+ nowMs
887
+ )
888
+ );
889
+ return { status: "duplicate" };
890
+ }
891
+ const status = current.execution.lease && current.execution.status === "running" ? "running" : current.execution.lease ? "awaiting_resume" : "pending";
892
+ const next = {
893
+ ...current,
894
+ destination: current.destination ?? args.message.destination,
895
+ source: current.source ?? args.message.source,
896
+ lastActivityAtMs: nowMs
897
+ };
898
+ await writeConversation(
899
+ state,
900
+ withExecutionUpdate(
901
+ next,
902
+ {
903
+ ...current.execution,
904
+ status,
905
+ inboundMessageIds: [
906
+ ...current.execution.inboundMessageIds,
907
+ args.message.inboundMessageId
908
+ ],
909
+ pendingMessages: [
910
+ ...current.execution.pendingMessages,
911
+ args.message
912
+ ].sort(compareMessages)
913
+ },
914
+ nowMs
915
+ )
916
+ );
917
+ return { status: "appended" };
918
+ }
919
+ );
920
+ }
921
+ async function requestConversationWork(args) {
922
+ const nowMs = args.nowMs ?? now();
923
+ return await withConversationMutation(args, async (state) => {
924
+ const existing = await readConversation(state, args.conversationId);
925
+ if (existing) {
926
+ assertSameConversationDestination({
927
+ conversationId: args.conversationId,
928
+ current: existing.destination,
929
+ next: args.destination
930
+ });
931
+ }
932
+ const current = existing ?? emptyConversation({
933
+ conversationId: args.conversationId,
934
+ destination: args.destination,
935
+ nowMs
936
+ });
937
+ const status = current.execution.lease ? "awaiting_resume" : "pending";
938
+ await writeConversation(
939
+ state,
940
+ withExecutionUpdate(
941
+ {
942
+ ...current,
943
+ destination: current.destination ?? args.destination
944
+ },
945
+ {
946
+ ...current.execution,
947
+ status
948
+ },
949
+ nowMs
950
+ )
951
+ );
952
+ return { status: existing === void 0 ? "created" : "updated" };
953
+ });
954
+ }
955
+ async function recordConversationActivity(args) {
956
+ const nowMs = args.nowMs ?? now();
957
+ const activityAtMs = args.activityAtMs ?? nowMs;
958
+ await withConversationMutation(args, async (state) => {
959
+ const existing = await readConversation(state, args.conversationId);
960
+ if (existing && args.destination) {
961
+ assertSameConversationDestination({
962
+ conversationId: args.conversationId,
963
+ current: existing.destination,
964
+ next: args.destination
965
+ });
966
+ }
967
+ const current = existing ?? emptyConversation({
968
+ conversationId: args.conversationId,
969
+ destination: args.destination,
970
+ nowMs,
971
+ source: args.source
972
+ });
973
+ await writeConversation(state, {
974
+ ...current,
975
+ ...current.destination ?? args.destination ? { destination: current.destination ?? args.destination } : {},
976
+ ...current.source ?? args.source ? { source: current.source ?? args.source } : {},
977
+ ...current.channelName ?? args.channelName ? { channelName: current.channelName ?? args.channelName } : {},
978
+ ...current.requester ?? args.requester ? { requester: current.requester ?? args.requester } : {},
979
+ ...current.title ?? args.title ? { title: current.title ?? args.title } : {},
980
+ lastActivityAtMs: Math.max(current.lastActivityAtMs, activityAtMs),
981
+ updatedAtMs: nowMs,
982
+ execution: executionWithPendingMessages(
983
+ current.execution,
984
+ current.execution.pendingMessages
985
+ )
986
+ });
987
+ });
988
+ }
989
+ async function markConversationWorkEnqueued(args) {
990
+ const nowMs = args.nowMs ?? now();
991
+ await withConversationMutation(args, async (state) => {
992
+ const current = await readConversation(state, args.conversationId);
993
+ if (!current) {
994
+ return;
995
+ }
996
+ await writeConversation(
997
+ state,
998
+ withExecutionUpdate(
999
+ current,
1000
+ {
1001
+ ...current.execution,
1002
+ lastEnqueuedAtMs: nowMs
1003
+ },
1004
+ nowMs
1005
+ )
1006
+ );
1007
+ });
1008
+ }
1009
+ async function startConversationWork(args) {
1010
+ const nowMs = args.nowMs ?? now();
1011
+ return await withConversationMutation(args, async (state) => {
1012
+ const current = await readConversation(state, args.conversationId);
1013
+ if (!current) {
1014
+ return { status: "no_work" };
1015
+ }
1016
+ if (isLeaseActive(current.execution.lease, nowMs)) {
1017
+ return {
1018
+ status: "active",
1019
+ leaseExpiresAtMs: current.execution.lease.expiresAtMs
1020
+ };
1021
+ }
1022
+ if (!hasRunnableWork(current)) {
1023
+ return { status: "no_work" };
1024
+ }
1025
+ const lease = {
1026
+ token: randomUUID(),
1027
+ acquiredAtMs: nowMs,
1028
+ lastCheckInAtMs: nowMs,
1029
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
1030
+ };
1031
+ await writeConversation(
1032
+ state,
1033
+ withExecutionUpdate(
1034
+ current,
1035
+ {
1036
+ ...current.execution,
1037
+ lease,
1038
+ status: "running",
1039
+ runId: current.execution.runId ?? randomUUID(),
1040
+ lastEnqueuedAtMs: void 0
1041
+ },
1042
+ nowMs
1043
+ )
1044
+ );
1045
+ return {
1046
+ status: "acquired",
1047
+ leaseToken: lease.token,
1048
+ leaseExpiresAtMs: lease.expiresAtMs
1049
+ };
1050
+ });
1051
+ }
1052
+ async function checkInConversationWork(args) {
1053
+ const nowMs = args.nowMs ?? now();
1054
+ return await withConversationMutation(args, async (state) => {
1055
+ const current = await readConversation(state, args.conversationId);
1056
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
1057
+ return false;
1058
+ }
1059
+ await writeConversation(
1060
+ state,
1061
+ withExecutionUpdate(
1062
+ current,
1063
+ {
1064
+ ...current.execution,
1065
+ lease: {
1066
+ ...current.execution.lease,
1067
+ lastCheckInAtMs: nowMs,
1068
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
1069
+ }
1070
+ },
1071
+ nowMs
1072
+ )
1073
+ );
1074
+ return true;
1075
+ });
1076
+ }
1077
+ async function drainConversationMailbox(args) {
1078
+ const nowMs = args.nowMs ?? now();
1079
+ const pending = await withConversationMutation(args, async (state) => {
1080
+ const current = await readConversation(state, args.conversationId);
1081
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
1082
+ throw new Error(
1083
+ `Conversation lease is not held for ${args.conversationId}`
1084
+ );
1085
+ }
1086
+ return pendingMessages(current);
1087
+ });
1088
+ if (pending.length === 0) {
1089
+ return [];
1090
+ }
1091
+ await args.inject(pending);
1092
+ await withConversationMutation(args, async (state) => {
1093
+ const current = await readConversation(state, args.conversationId);
1094
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
1095
+ throw new Error(
1096
+ `Conversation lease is not held for ${args.conversationId}`
1097
+ );
1098
+ }
1099
+ const drainedIds = new Set(
1100
+ pending.map((message) => message.inboundMessageId)
1101
+ );
1102
+ const pendingMessages2 = current.execution.pendingMessages.filter(
1103
+ (message) => !drainedIds.has(message.inboundMessageId)
1104
+ );
1105
+ await writeConversation(
1106
+ state,
1107
+ withExecutionUpdate(
1108
+ current,
1109
+ {
1110
+ ...current.execution,
1111
+ status: current.execution.status === "pending" && pendingMessages2.length === 0 ? "running" : current.execution.status,
1112
+ pendingMessages: pendingMessages2
1113
+ },
1114
+ nowMs
1115
+ )
1116
+ );
1117
+ });
1118
+ return pending;
1119
+ }
1120
+ async function markConversationMessagesInjected(args) {
1121
+ const nowMs = args.nowMs ?? now();
1122
+ const inboundMessageIds = new Set(args.inboundMessageIds);
1123
+ return await withConversationMutation(args, async (state) => {
1124
+ const current = await readConversation(state, args.conversationId);
1125
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
1126
+ return false;
1127
+ }
1128
+ if (inboundMessageIds.size === 0) {
1129
+ return true;
1130
+ }
1131
+ const pendingMessages2 = current.execution.pendingMessages.filter(
1132
+ (message) => !inboundMessageIds.has(message.inboundMessageId)
1133
+ );
1134
+ if (pendingMessages2.length === current.execution.pendingMessages.length) {
1135
+ return true;
1136
+ }
1137
+ await writeConversation(
1138
+ state,
1139
+ withExecutionUpdate(
1140
+ current,
1141
+ {
1142
+ ...current.execution,
1143
+ pendingMessages: pendingMessages2
1144
+ },
1145
+ nowMs
1146
+ )
1147
+ );
1148
+ return true;
1149
+ });
1150
+ }
1151
+ async function requestConversationContinuation(args) {
1152
+ const nowMs = args.nowMs ?? now();
1153
+ return await withConversationMutation(args, async (state) => {
1154
+ const current = await readConversation(state, args.conversationId);
1155
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
1156
+ return false;
1157
+ }
1158
+ assertSameConversationDestination({
1159
+ conversationId: args.conversationId,
1160
+ current: current.destination,
1161
+ next: args.destination
1162
+ });
1163
+ await writeConversation(
1164
+ state,
1165
+ withExecutionUpdate(
1166
+ current,
1167
+ {
1168
+ ...current.execution,
1169
+ status: "awaiting_resume"
1170
+ },
1171
+ nowMs
1172
+ )
1173
+ );
1174
+ return true;
1175
+ });
1176
+ }
1177
+ async function releaseConversationWork(args) {
1178
+ const nowMs = args.nowMs ?? now();
1179
+ return await withConversationMutation(args, async (state) => {
1180
+ const current = await readConversation(state, args.conversationId);
1181
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
1182
+ return false;
1183
+ }
1184
+ await writeConversation(
1185
+ state,
1186
+ withExecutionUpdate(
1187
+ current,
1188
+ {
1189
+ ...current.execution,
1190
+ lease: void 0,
1191
+ status: current.execution.status === "running" ? "pending" : current.execution.status
1192
+ },
1193
+ nowMs
1194
+ )
1195
+ );
1196
+ return true;
1197
+ });
1198
+ }
1199
+ async function completeConversationWork(args) {
1200
+ const nowMs = args.nowMs ?? now();
1201
+ return await withConversationMutation(args, async (state) => {
1202
+ const current = await readConversation(state, args.conversationId);
1203
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
1204
+ return "lost_lease";
1205
+ }
1206
+ const hasPending = pendingMessages(current).length > 0;
1207
+ const needsRun = current.execution.status === "awaiting_resume";
1208
+ const runnable = needsRun || hasPending;
1209
+ await writeConversation(
1210
+ state,
1211
+ withExecutionUpdate(
1212
+ current,
1213
+ {
1214
+ ...current.execution,
1215
+ lease: void 0,
1216
+ status: runnable ? "pending" : "idle",
1217
+ runId: runnable ? current.execution.runId : void 0
1218
+ },
1219
+ nowMs
1220
+ )
1221
+ );
1222
+ return runnable ? "pending" : "completed";
1223
+ });
1224
+ }
1225
+ async function clearExpiredConversationLease(args) {
1226
+ const nowMs = args.nowMs ?? now();
1227
+ return await withConversationMutation(args, async (state) => {
1228
+ const current = await readConversation(state, args.conversationId);
1229
+ if (!current?.execution.lease || current.execution.lease.expiresAtMs > nowMs) {
1230
+ return false;
1231
+ }
1232
+ await writeConversation(
1233
+ state,
1234
+ withExecutionUpdate(
1235
+ current,
1236
+ {
1237
+ ...current.execution,
1238
+ lease: void 0,
1239
+ status: "pending"
1240
+ },
1241
+ nowMs
1242
+ )
1243
+ );
1244
+ return true;
1245
+ });
1246
+ }
1247
+ async function removeActiveConversation(args) {
1248
+ const state = await getConnectedState(args.state);
1249
+ await removeIndexEntry({
1250
+ state,
1251
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1252
+ conversationId: args.conversationId
1253
+ });
1254
+ }
1255
+ async function listActiveConversationIds(args = {}) {
1256
+ const state = await getConnectedState(args.state);
1257
+ const index4 = await getConversationIndexStore(state);
1258
+ const entries = await index4.list({
1259
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1260
+ limit: args.limit,
1261
+ order: "asc",
1262
+ scoreMax: args.staleBeforeMs
1263
+ });
1264
+ return entries.map((entry) => entry.conversationId);
1265
+ }
1266
+ async function listConversationsByActivity(args = {}) {
1267
+ const state = await getConnectedState(args.state);
1268
+ const index4 = await getConversationIndexStore(state);
1269
+ const entries = await index4.list({
1270
+ indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
1271
+ limit: args.limit ?? CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH,
1272
+ offset: args.offset,
1273
+ order: "desc"
1274
+ });
1275
+ const conversations = [];
1276
+ for (const entry of entries) {
1277
+ try {
1278
+ const conversation = await readConversation(state, entry.conversationId);
1279
+ if (conversation) {
1280
+ conversations.push(conversation);
1281
+ }
1282
+ } catch (error) {
1283
+ if (!(error instanceof InvalidConversationRecordError)) {
1284
+ throw error;
1285
+ }
1286
+ await removeIndexEntry({
1287
+ state,
1288
+ indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
1289
+ conversationId: entry.conversationId
1290
+ });
1291
+ }
1292
+ }
1293
+ return conversations;
1294
+ }
1295
+
1296
+ // src/chat/conversations/state.ts
1297
+ function createStateConversationStore(state) {
1298
+ return {
1299
+ get: (args) => getConversation({ ...args, state }),
1300
+ recordActivity: (args) => recordConversationActivity({ ...args, state }),
1301
+ recordExecution: async () => {
1302
+ },
1303
+ listByActivity: (args) => listConversationsByActivity({ ...args, state })
1304
+ };
1305
+ }
1306
+
1307
+ // src/chat/conversations/sql/store.ts
1308
+ import { randomUUID as randomUUID2 } from "crypto";
1309
+ import { asc, desc, eq, sql as sql2 } from "drizzle-orm";
1310
+
1311
+ // src/chat/conversations/sql/migrations.ts
1312
+ import { createHash } from "crypto";
1313
+ import { z } from "zod";
1314
+ var MIGRATION_LOCK_NAME = "junior_conversation_schema";
1315
+ var migrationRecordSchema = z.object({
1316
+ id: z.string().min(1),
1317
+ checksum: z.string().min(1)
1318
+ }).strict();
1319
+ function checksumStatements(statements) {
1320
+ const hash = createHash("sha256");
1321
+ for (const statement of statements) {
1322
+ hash.update(statement);
1323
+ hash.update("\0");
1324
+ }
1325
+ return hash.digest("hex");
1326
+ }
1327
+ function defineMigration(id, statements) {
1328
+ return {
1329
+ id,
1330
+ checksum: checksumStatements(statements),
1331
+ statements
1332
+ };
1333
+ }
1334
+ var createMigrationTable = `
1335
+ CREATE TABLE IF NOT EXISTS junior_schema_migrations (
1336
+ id TEXT PRIMARY KEY,
1337
+ checksum TEXT NOT NULL,
1338
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
1339
+ )
1340
+ `;
1341
+ var coreMetadataStatements = [
1342
+ `
1343
+ CREATE TABLE IF NOT EXISTS junior_identities (
1344
+ id TEXT PRIMARY KEY,
1345
+ kind TEXT NOT NULL,
1346
+ provider TEXT NOT NULL,
1347
+ provider_tenant_id TEXT NOT NULL DEFAULT '',
1348
+ provider_subject_id TEXT NOT NULL,
1349
+ display_name TEXT,
1350
+ handle TEXT,
1351
+ email TEXT,
1352
+ avatar_url TEXT,
1353
+ metadata_json JSONB,
1354
+ created_at TIMESTAMPTZ NOT NULL,
1355
+ updated_at TIMESTAMPTZ NOT NULL
1356
+ )
1357
+ `,
1358
+ `
1359
+ CREATE UNIQUE INDEX IF NOT EXISTS junior_identities_provider_subject_uidx
1360
+ ON junior_identities (provider, provider_tenant_id, provider_subject_id)
1361
+ `,
1362
+ `
1363
+ CREATE INDEX IF NOT EXISTS junior_identities_kind_provider_idx
1364
+ ON junior_identities (kind, provider)
1365
+ `,
1366
+ `
1367
+ CREATE TABLE IF NOT EXISTS junior_destinations (
1368
+ id TEXT PRIMARY KEY,
1369
+ provider TEXT NOT NULL,
1370
+ provider_tenant_id TEXT NOT NULL DEFAULT '',
1371
+ provider_destination_id TEXT NOT NULL,
1372
+ kind TEXT NOT NULL,
1373
+ parent_destination_id TEXT,
1374
+ display_name TEXT,
1375
+ visibility TEXT NOT NULL DEFAULT 'unknown',
1376
+ metadata_json JSONB,
1377
+ created_at TIMESTAMPTZ NOT NULL,
1378
+ updated_at TIMESTAMPTZ NOT NULL
1379
+ )
1380
+ `,
1381
+ `
1382
+ CREATE UNIQUE INDEX IF NOT EXISTS junior_destinations_provider_destination_uidx
1383
+ ON junior_destinations (provider, provider_tenant_id, provider_destination_id)
1384
+ `,
1385
+ `
1386
+ CREATE INDEX IF NOT EXISTS junior_destinations_provider_kind_idx
1387
+ ON junior_destinations (provider, kind)
1388
+ `,
1389
+ `
1390
+ CREATE TABLE IF NOT EXISTS junior_conversations (
1391
+ conversation_id TEXT PRIMARY KEY,
1392
+ schema_version INTEGER NOT NULL DEFAULT 1,
1393
+ source TEXT,
1394
+ origin_type TEXT,
1395
+ origin_id TEXT,
1396
+ origin_run_id TEXT,
1397
+ destination_id TEXT REFERENCES junior_destinations (id),
1398
+ destination_json JSONB,
1399
+ actor_identity_id TEXT REFERENCES junior_identities (id),
1400
+ requester_identity_id TEXT REFERENCES junior_identities (id),
1401
+ creator_identity_id TEXT REFERENCES junior_identities (id),
1402
+ credential_subject_identity_id TEXT REFERENCES junior_identities (id),
1403
+ requester_json JSONB,
1404
+ channel_name TEXT,
1405
+ title TEXT,
1406
+ created_at TIMESTAMPTZ NOT NULL,
1407
+ last_activity_at TIMESTAMPTZ NOT NULL,
1408
+ updated_at TIMESTAMPTZ NOT NULL,
1409
+ execution_updated_at TIMESTAMPTZ,
1410
+ execution_status TEXT NOT NULL,
1411
+ run_id TEXT,
1412
+ last_checkpoint_at TIMESTAMPTZ,
1413
+ last_enqueued_at TIMESTAMPTZ
1414
+ )
1415
+ `,
1416
+ `
1417
+ CREATE INDEX IF NOT EXISTS junior_conversations_last_activity_idx
1418
+ ON junior_conversations (last_activity_at DESC, conversation_id)
1419
+ `,
1420
+ `
1421
+ CREATE INDEX IF NOT EXISTS junior_conversations_active_idx
1422
+ ON junior_conversations (coalesce(execution_updated_at, updated_at) ASC, conversation_id)
1423
+ WHERE execution_status <> 'idle'
1424
+ `,
1425
+ `
1426
+ CREATE INDEX IF NOT EXISTS junior_conversations_destination_activity_idx
1427
+ ON junior_conversations (destination_id, last_activity_at DESC)
1428
+ `,
1429
+ `
1430
+ CREATE INDEX IF NOT EXISTS junior_conversations_actor_activity_idx
1431
+ ON junior_conversations (actor_identity_id, last_activity_at DESC)
1432
+ `,
1433
+ `
1434
+ CREATE INDEX IF NOT EXISTS junior_conversations_requester_activity_idx
1435
+ ON junior_conversations (requester_identity_id, last_activity_at DESC)
1436
+ `,
1437
+ `
1438
+ CREATE INDEX IF NOT EXISTS junior_conversations_origin_idx
1439
+ ON junior_conversations (origin_type, origin_id, last_activity_at DESC)
1440
+ `
1441
+ ];
1442
+ var migrations = [
1443
+ defineMigration("0001_conversation_core", coreMetadataStatements)
1444
+ ];
1445
+ function parseStoredMigrationRecord(value) {
1446
+ return migrationRecordSchema.parse(value);
1447
+ }
1448
+ async function listAppliedMigrations(executor) {
1449
+ const rows = await executor.query(
1450
+ "SELECT id, checksum FROM junior_schema_migrations ORDER BY id ASC"
1451
+ );
1452
+ const records = /* @__PURE__ */ new Map();
1453
+ for (const row of rows) {
1454
+ const record = parseStoredMigrationRecord(row);
1455
+ records.set(record.id, record);
1456
+ }
1457
+ return records;
1458
+ }
1459
+ async function applyMigration(executor, migration) {
1460
+ await executor.transaction(async () => {
1461
+ for (const statement of migration.statements) {
1462
+ await executor.execute(statement);
1463
+ }
1464
+ await executor.execute(
1465
+ "INSERT INTO junior_schema_migrations (id, checksum) VALUES ($1, $2)",
1466
+ [migration.id, migration.checksum]
1467
+ );
1468
+ });
1469
+ }
1470
+ async function migrateSchema(executor, migrationList = migrations) {
1471
+ await executor.withLock(MIGRATION_LOCK_NAME, async () => {
1472
+ await executor.execute(createMigrationTable);
1473
+ const applied = await listAppliedMigrations(executor);
1474
+ for (const migration of migrationList) {
1475
+ const existing = applied.get(migration.id);
1476
+ if (existing) {
1477
+ if (existing.checksum !== migration.checksum) {
1478
+ throw new Error(
1479
+ `Conversation migration ${migration.id} checksum changed`
1480
+ );
1481
+ }
1482
+ continue;
1483
+ }
1484
+ await applyMigration(executor, migration);
1485
+ }
1486
+ });
1487
+ }
1488
+
1489
+ // src/chat/conversations/sql/store.ts
1490
+ var CONVERSATION_MUTATION_LOCK_PREFIX = "junior_conversation";
1491
+ function now2() {
1492
+ return Date.now();
1493
+ }
1494
+ function dateFromMs(ms) {
1495
+ return new Date(ms);
1496
+ }
1497
+ function tenantId(value) {
1498
+ return value ?? "";
1499
+ }
1500
+ function msFromDate(value) {
1501
+ if (value === null || value === void 0) {
1502
+ return void 0;
1503
+ }
1504
+ const date = value instanceof Date ? value : new Date(value);
1505
+ return date.getTime();
1506
+ }
1507
+ function requiredMsFromDate(value) {
1508
+ const ms = msFromDate(value);
1509
+ if (typeof ms !== "number" || Number.isNaN(ms)) {
1510
+ throw new Error("Conversation record timestamp is invalid");
1511
+ }
1512
+ return ms;
1513
+ }
1514
+ function sourceFromValue(value) {
1515
+ if (value === "api" || value === "internal" || value === "local" || value === "plugin" || value === "scheduler" || value === "slack") {
1516
+ return value;
1517
+ }
1518
+ return void 0;
1519
+ }
1520
+ function identityFromRequester(requester) {
1521
+ if (!requester?.slackUserId) {
1522
+ return void 0;
1523
+ }
1524
+ return {
1525
+ kind: "user",
1526
+ provider: "slack",
1527
+ providerTenantId: requester.teamId,
1528
+ providerSubjectId: requester.slackUserId,
1529
+ ...requester.fullName ? { displayName: requester.fullName } : {},
1530
+ ...requester.slackUserName ? { handle: requester.slackUserName } : {},
1531
+ ...requester.email ? { email: requester.email } : {},
1532
+ metadata: { platform: "slack" }
1533
+ };
1534
+ }
1535
+ function systemIdentityFromSource(source) {
1536
+ if (source === "scheduler") {
1537
+ return {
1538
+ kind: "system",
1539
+ provider: "junior",
1540
+ providerSubjectId: "scheduler",
1541
+ displayName: "Junior Scheduler"
1542
+ };
1543
+ }
1544
+ if (source === "local") {
1545
+ return {
1546
+ kind: "system",
1547
+ provider: "junior",
1548
+ providerSubjectId: "local-cli",
1549
+ displayName: "Local CLI"
1550
+ };
1551
+ }
1552
+ return void 0;
1553
+ }
1554
+ function actorIdentityForConversation(conversation) {
1555
+ return identityFromRequester(conversation.requester) ?? systemIdentityFromSource(conversation.source);
1556
+ }
1557
+ function originTypeFromSource(source) {
1558
+ return source;
1559
+ }
1560
+ function localWorkspaceFromConversationId(conversationId) {
1561
+ const match = /^local:([^:]+):/.exec(conversationId);
1562
+ return match?.[1];
1563
+ }
1564
+ function destinationUpsertFromDestination(args) {
1565
+ const { destination } = args;
1566
+ if (!destination) {
1567
+ return void 0;
1568
+ }
1569
+ if (destination.platform === "slack") {
1570
+ const channelId = destination.channelId;
1571
+ const channelKind = channelId.startsWith("D") ? "dm" : channelId.startsWith("G") ? "group" : "channel";
1572
+ const visibility = channelId.startsWith("D") ? "direct" : channelId.startsWith("G") ? "private" : "public";
1573
+ return {
1574
+ kind: channelKind,
1575
+ provider: "slack",
1576
+ providerTenantId: destination.teamId,
1577
+ providerDestinationId: channelId,
1578
+ visibility,
1579
+ ...args.channelName ? { displayName: args.channelName } : {},
1580
+ metadata: { platform: "slack" }
1581
+ };
1582
+ }
1583
+ return {
1584
+ kind: "local_conversation",
1585
+ provider: "local",
1586
+ providerTenantId: localWorkspaceFromConversationId(destination.conversationId) ?? localWorkspaceFromConversationId(args.conversationId ?? ""),
1587
+ providerDestinationId: destination.conversationId,
1588
+ visibility: "direct",
1589
+ metadata: { platform: "local" }
1590
+ };
1591
+ }
1592
+ function executionStatusFromValue(value) {
1593
+ if (value === "awaiting_resume" || value === "idle" || value === "pending" || value === "running") {
1594
+ return value;
1595
+ }
1596
+ throw new Error("Conversation record execution status is invalid");
1597
+ }
1598
+ function conversationFromRow(row) {
1599
+ if (row.schemaVersion !== 1) {
1600
+ throw new Error("Conversation record schema version is invalid");
1601
+ }
1602
+ const destination = row.destination === void 0 || row.destination === null ? void 0 : parseDestination(row.destination);
1603
+ const requester = parseStoredSlackRequester(row.requester);
1604
+ if (row.destination !== void 0 && row.destination !== null && !destination) {
1605
+ throw new Error("Conversation record destination is invalid");
1606
+ }
1607
+ if (row.requester !== void 0 && row.requester !== null && !requester) {
1608
+ throw new Error("Conversation record requester is invalid");
1609
+ }
1610
+ const source = row.source === void 0 || row.source === null ? void 0 : sourceFromValue(row.source);
1611
+ if (row.source !== void 0 && row.source !== null && !source) {
1612
+ throw new Error("Conversation record source is invalid");
1613
+ }
1614
+ const execution = {
1615
+ status: executionStatusFromValue(row.executionStatus),
1616
+ lastCheckpointAtMs: msFromDate(row.lastCheckpointAt),
1617
+ lastEnqueuedAtMs: msFromDate(row.lastEnqueuedAt),
1618
+ ...row.runId ? { runId: row.runId } : {},
1619
+ updatedAtMs: msFromDate(row.executionUpdatedAt) ?? requiredMsFromDate(row.updatedAt)
1620
+ };
1621
+ return {
1622
+ schemaVersion: 1,
1623
+ conversationId: row.conversationId,
1624
+ createdAtMs: requiredMsFromDate(row.createdAt),
1625
+ lastActivityAtMs: requiredMsFromDate(row.lastActivityAt),
1626
+ updatedAtMs: requiredMsFromDate(row.updatedAt),
1627
+ execution,
1628
+ ...destination ? { destination } : {},
1629
+ ...requester ? { requester } : {},
1630
+ ...row.channelName ? { channelName: row.channelName } : {},
1631
+ ...source ? { source } : {},
1632
+ ...row.title ? { title: row.title } : {}
1633
+ };
1634
+ }
1635
+ function emptyConversation2(args) {
1636
+ return {
1637
+ schemaVersion: 1,
1638
+ conversationId: args.conversationId,
1639
+ createdAtMs: args.nowMs,
1640
+ lastActivityAtMs: args.nowMs,
1641
+ updatedAtMs: args.nowMs,
1642
+ ...args.destination ? { destination: args.destination } : {},
1643
+ ...args.source ? { source: args.source } : {},
1644
+ execution: {
1645
+ status: "idle",
1646
+ updatedAtMs: args.nowMs
1647
+ }
1648
+ };
1649
+ }
1650
+ function assertSameConversationDestination2(args) {
1651
+ if (!args.current || sameDestination(args.current, args.next)) {
1652
+ return;
1653
+ }
1654
+ throw new Error(
1655
+ `Conversation destination changed for ${args.conversationId}`
1656
+ );
1657
+ }
1658
+ var SqlStore = class {
1659
+ constructor(executor, migrationExecutor) {
1660
+ this.executor = executor;
1661
+ this.migrationExecutor = migrationExecutor;
1662
+ }
1663
+ executor;
1664
+ migrationExecutor;
1665
+ schemaReady;
1666
+ /** Apply SQL schema migrations before runtime uses this store. */
1667
+ async migrate() {
1668
+ if (!this.schemaReady) {
1669
+ this.schemaReady = migrateSchema(this.migrationExecutor);
1670
+ }
1671
+ const schemaReady = this.schemaReady;
1672
+ try {
1673
+ await schemaReady;
1674
+ } catch (error) {
1675
+ if (this.schemaReady === schemaReady) {
1676
+ this.schemaReady = void 0;
1677
+ }
1678
+ throw error;
1679
+ }
1680
+ }
1681
+ async get(args) {
1682
+ const row = await this.readConversationRow(args.conversationId);
1683
+ if (!row) {
1684
+ return void 0;
1685
+ }
1686
+ return conversationFromRow(row);
1687
+ }
1688
+ async recordActivity(args) {
1689
+ const nowMs = args.nowMs ?? now2();
1690
+ const activityAtMs = args.activityAtMs ?? nowMs;
1691
+ await this.withConversationMutation(args.conversationId, async () => {
1692
+ const existing = await this.get({
1693
+ conversationId: args.conversationId
1694
+ });
1695
+ if (existing && args.destination) {
1696
+ assertSameConversationDestination2({
1697
+ conversationId: args.conversationId,
1698
+ current: existing.destination,
1699
+ next: args.destination
1700
+ });
1701
+ }
1702
+ const current = existing ?? emptyConversation2({
1703
+ conversationId: args.conversationId,
1704
+ destination: args.destination,
1705
+ nowMs,
1706
+ source: args.source
1707
+ });
1708
+ await this.upsertConversation({
1709
+ conversation: {
1710
+ ...current,
1711
+ destination: current.destination ?? args.destination,
1712
+ source: current.source ?? args.source,
1713
+ channelName: current.channelName ?? args.channelName,
1714
+ requester: current.requester ?? args.requester,
1715
+ title: current.title ?? args.title,
1716
+ lastActivityAtMs: Math.max(current.lastActivityAtMs, activityAtMs),
1717
+ updatedAtMs: nowMs,
1718
+ execution: {
1719
+ ...current.execution,
1720
+ updatedAtMs: current.execution.updatedAtMs ?? nowMs
1721
+ }
1722
+ }
1723
+ });
1724
+ });
1725
+ }
1726
+ async recordExecution(args) {
1727
+ await this.withConversationMutation(args.conversationId, async () => {
1728
+ await this.upsertConversation({
1729
+ conversation: {
1730
+ schemaVersion: 1,
1731
+ conversationId: args.conversationId,
1732
+ createdAtMs: args.createdAtMs,
1733
+ lastActivityAtMs: args.lastActivityAtMs,
1734
+ updatedAtMs: args.updatedAtMs,
1735
+ ...args.channelName ? { channelName: args.channelName } : {},
1736
+ ...args.destination ? { destination: args.destination } : {},
1737
+ ...args.requester ? { requester: args.requester } : {},
1738
+ ...args.source ? { source: args.source } : {},
1739
+ ...args.title ? { title: args.title } : {},
1740
+ execution: args.execution
1741
+ }
1742
+ });
1743
+ });
1744
+ }
1745
+ /** Copy one conversation record into SQL during backfill. */
1746
+ async backfillConversation(conversation) {
1747
+ await this.withConversationMutation(
1748
+ conversation.conversationId,
1749
+ async () => {
1750
+ const existing = await this.get({
1751
+ conversationId: conversation.conversationId
1752
+ });
1753
+ const sourceExecutionAtMs = conversation.execution.updatedAtMs ?? conversation.updatedAtMs;
1754
+ const existingExecutionAtMs = existing === void 0 ? void 0 : existing.execution.updatedAtMs ?? existing.updatedAtMs;
1755
+ const refreshExecutionFromSource = existingExecutionAtMs === void 0 || sourceExecutionAtMs >= existingExecutionAtMs;
1756
+ const mergedConversation = existing ? {
1757
+ ...conversation,
1758
+ channelName: existing.channelName ?? conversation.channelName,
1759
+ createdAtMs: Math.min(
1760
+ existing.createdAtMs,
1761
+ conversation.createdAtMs
1762
+ ),
1763
+ destination: existing.destination ?? conversation.destination,
1764
+ lastActivityAtMs: Math.max(
1765
+ existing.lastActivityAtMs,
1766
+ conversation.lastActivityAtMs
1767
+ ),
1768
+ requester: existing.requester ?? conversation.requester,
1769
+ source: existing.source ?? conversation.source,
1770
+ title: existing.title ?? conversation.title,
1771
+ updatedAtMs: Math.max(
1772
+ existing.updatedAtMs,
1773
+ conversation.updatedAtMs
1774
+ ),
1775
+ execution: refreshExecutionFromSource ? conversation.execution : existing.execution
1776
+ } : conversation;
1777
+ await this.upsertConversation({ conversation: mergedConversation });
1778
+ }
1779
+ );
1780
+ }
1781
+ async listByActivity(args = {}) {
1782
+ const rows = await this.executor.db().select().from(juniorConversations).orderBy(
1783
+ desc(juniorConversations.lastActivityAt),
1784
+ asc(juniorConversations.conversationId)
1785
+ ).limit(Math.max(0, args.limit ?? 1e4)).offset(Math.max(0, args.offset ?? 0));
1786
+ const conversations = [];
1787
+ for (const row of rows) {
1788
+ conversations.push(conversationFromRow(row));
1789
+ }
1790
+ return conversations;
1791
+ }
1792
+ /** Serialize all durable mutations for one conversation inside a SQL transaction. */
1793
+ async withConversationMutation(conversationId, callback) {
1794
+ return await this.executor.withLock(
1795
+ `${CONVERSATION_MUTATION_LOCK_PREFIX}:${conversationId}`,
1796
+ async () => await this.executor.transaction(callback)
1797
+ );
1798
+ }
1799
+ async readConversationRow(conversationId) {
1800
+ const rows = await this.executor.db().select().from(juniorConversations).where(eq(juniorConversations.conversationId, conversationId));
1801
+ return rows[0];
1802
+ }
1803
+ /** Upsert the conversation row while preserving previously discovered nullable metadata fields. */
1804
+ async upsertConversation(args) {
1805
+ const { conversation } = args;
1806
+ const incomingExecutionVersion = sql2`coalesce(excluded.execution_updated_at, excluded.updated_at)`;
1807
+ const currentExecutionVersion = sql2`coalesce(${juniorConversations.executionUpdatedAt}, ${juniorConversations.updatedAt})`;
1808
+ const incomingExecutionIsFresh = sql2`${incomingExecutionVersion} >= ${currentExecutionVersion}`;
1809
+ const destinationId = await this.upsertDestination(
1810
+ destinationUpsertFromDestination({
1811
+ channelName: conversation.channelName,
1812
+ conversationId: conversation.conversationId,
1813
+ destination: conversation.destination
1814
+ }),
1815
+ conversation.updatedAtMs
1816
+ );
1817
+ const requesterIdentityId = await this.upsertIdentity(
1818
+ identityFromRequester(conversation.requester),
1819
+ conversation.updatedAtMs
1820
+ );
1821
+ const actorIdentityId = await this.upsertIdentity(
1822
+ actorIdentityForConversation(conversation),
1823
+ conversation.updatedAtMs
1824
+ );
1825
+ await this.executor.db().insert(juniorConversations).values({
1826
+ conversationId: conversation.conversationId,
1827
+ schemaVersion: 1,
1828
+ source: conversation.source ?? null,
1829
+ originType: originTypeFromSource(conversation.source) ?? null,
1830
+ originId: null,
1831
+ originRunId: null,
1832
+ destinationId: destinationId ?? null,
1833
+ destination: conversation.destination ?? null,
1834
+ actorIdentityId: actorIdentityId ?? null,
1835
+ requesterIdentityId: requesterIdentityId ?? null,
1836
+ creatorIdentityId: null,
1837
+ credentialSubjectIdentityId: null,
1838
+ requester: conversation.requester ?? null,
1839
+ channelName: conversation.channelName ?? null,
1840
+ title: conversation.title ?? null,
1841
+ createdAt: dateFromMs(conversation.createdAtMs),
1842
+ lastActivityAt: dateFromMs(conversation.lastActivityAtMs),
1843
+ updatedAt: dateFromMs(conversation.updatedAtMs),
1844
+ executionUpdatedAt: conversation.execution.updatedAtMs === void 0 ? null : dateFromMs(conversation.execution.updatedAtMs),
1845
+ executionStatus: conversation.execution.status,
1846
+ runId: conversation.execution.runId ?? null,
1847
+ lastCheckpointAt: conversation.execution.lastCheckpointAtMs === void 0 ? null : dateFromMs(conversation.execution.lastCheckpointAtMs),
1848
+ lastEnqueuedAt: conversation.execution.lastEnqueuedAtMs === void 0 ? null : dateFromMs(conversation.execution.lastEnqueuedAtMs)
1849
+ }).onConflictDoUpdate({
1850
+ target: juniorConversations.conversationId,
1851
+ set: {
1852
+ source: sql2`coalesce(excluded.source, ${juniorConversations.source})`,
1853
+ originType: sql2`coalesce(excluded.origin_type, ${juniorConversations.originType})`,
1854
+ originId: sql2`coalesce(excluded.origin_id, ${juniorConversations.originId})`,
1855
+ originRunId: sql2`coalesce(excluded.origin_run_id, ${juniorConversations.originRunId})`,
1856
+ destinationId: sql2`coalesce(excluded.destination_id, ${juniorConversations.destinationId})`,
1857
+ destination: sql2`coalesce(excluded.destination_json, ${juniorConversations.destination})`,
1858
+ actorIdentityId: sql2`coalesce(excluded.actor_identity_id, ${juniorConversations.actorIdentityId})`,
1859
+ requesterIdentityId: sql2`coalesce(excluded.requester_identity_id, ${juniorConversations.requesterIdentityId})`,
1860
+ creatorIdentityId: sql2`coalesce(excluded.creator_identity_id, ${juniorConversations.creatorIdentityId})`,
1861
+ credentialSubjectIdentityId: sql2`coalesce(excluded.credential_subject_identity_id, ${juniorConversations.credentialSubjectIdentityId})`,
1862
+ requester: sql2`coalesce(excluded.requester_json, ${juniorConversations.requester})`,
1863
+ channelName: sql2`coalesce(excluded.channel_name, ${juniorConversations.channelName})`,
1864
+ title: sql2`coalesce(excluded.title, ${juniorConversations.title})`,
1865
+ createdAt: sql2`least(${juniorConversations.createdAt}, excluded.created_at)`,
1866
+ lastActivityAt: sql2`greatest(${juniorConversations.lastActivityAt}, excluded.last_activity_at)`,
1867
+ updatedAt: sql2`greatest(${juniorConversations.updatedAt}, excluded.updated_at)`,
1868
+ executionUpdatedAt: sql2`case when ${incomingExecutionIsFresh} then excluded.execution_updated_at else ${juniorConversations.executionUpdatedAt} end`,
1869
+ executionStatus: sql2`case when ${incomingExecutionIsFresh} then excluded.execution_status else ${juniorConversations.executionStatus} end`,
1870
+ runId: sql2`case when ${incomingExecutionIsFresh} then excluded.run_id else ${juniorConversations.runId} end`,
1871
+ lastCheckpointAt: sql2`case when ${incomingExecutionIsFresh} then excluded.last_checkpoint_at else ${juniorConversations.lastCheckpointAt} end`,
1872
+ lastEnqueuedAt: sql2`case when ${incomingExecutionIsFresh} then excluded.last_enqueued_at else ${juniorConversations.lastEnqueuedAt} end`
1873
+ }
1874
+ });
1875
+ }
1876
+ async upsertIdentity(identity, nowMs) {
1877
+ if (!identity) {
1878
+ return void 0;
1879
+ }
1880
+ const rows = await this.executor.db().insert(juniorIdentities).values({
1881
+ id: randomUUID2(),
1882
+ kind: identity.kind,
1883
+ provider: identity.provider,
1884
+ providerTenantId: tenantId(identity.providerTenantId),
1885
+ providerSubjectId: identity.providerSubjectId,
1886
+ displayName: identity.displayName ?? null,
1887
+ handle: identity.handle ?? null,
1888
+ email: identity.email ?? null,
1889
+ avatarUrl: null,
1890
+ metadata: identity.metadata ?? null,
1891
+ createdAt: dateFromMs(nowMs),
1892
+ updatedAt: dateFromMs(nowMs)
1893
+ }).onConflictDoUpdate({
1894
+ target: [
1895
+ juniorIdentities.provider,
1896
+ juniorIdentities.providerTenantId,
1897
+ juniorIdentities.providerSubjectId
1898
+ ],
1899
+ set: {
1900
+ kind: sql2`excluded.kind`,
1901
+ displayName: sql2`coalesce(excluded.display_name, ${juniorIdentities.displayName})`,
1902
+ handle: sql2`coalesce(excluded.handle, ${juniorIdentities.handle})`,
1903
+ email: sql2`coalesce(excluded.email, ${juniorIdentities.email})`,
1904
+ avatarUrl: sql2`coalesce(excluded.avatar_url, ${juniorIdentities.avatarUrl})`,
1905
+ metadata: sql2`coalesce(excluded.metadata_json, ${juniorIdentities.metadata})`,
1906
+ updatedAt: sql2`excluded.updated_at`
1907
+ }
1908
+ }).returning({ id: juniorIdentities.id });
1909
+ return rows[0]?.id;
1910
+ }
1911
+ async upsertDestination(destination, nowMs) {
1912
+ if (!destination) {
1913
+ return void 0;
1914
+ }
1915
+ const rows = await this.executor.db().insert(juniorDestinations).values({
1916
+ id: randomUUID2(),
1917
+ provider: destination.provider,
1918
+ providerTenantId: tenantId(destination.providerTenantId),
1919
+ providerDestinationId: destination.providerDestinationId,
1920
+ kind: destination.kind,
1921
+ parentDestinationId: null,
1922
+ displayName: destination.displayName ?? null,
1923
+ visibility: destination.visibility,
1924
+ metadata: destination.metadata ?? null,
1925
+ createdAt: dateFromMs(nowMs),
1926
+ updatedAt: dateFromMs(nowMs)
1927
+ }).onConflictDoUpdate({
1928
+ target: [
1929
+ juniorDestinations.provider,
1930
+ juniorDestinations.providerTenantId,
1931
+ juniorDestinations.providerDestinationId
1932
+ ],
1933
+ set: {
1934
+ kind: sql2`excluded.kind`,
1935
+ displayName: sql2`coalesce(excluded.display_name, ${juniorDestinations.displayName})`,
1936
+ visibility: sql2`excluded.visibility`,
1937
+ metadata: sql2`coalesce(excluded.metadata_json, ${juniorDestinations.metadata})`,
1938
+ updatedAt: sql2`excluded.updated_at`
1939
+ }
1940
+ }).returning({ id: juniorDestinations.id });
1941
+ return rows[0]?.id;
1942
+ }
1943
+ };
1944
+ function createSqlStore(executor) {
1945
+ return new SqlStore(executor, executor);
1946
+ }
1947
+
1948
+ export {
1949
+ JUNIOR_THREAD_STATE_TTL_MS,
1950
+ createNeonJuniorSqlExecutor,
1951
+ CONVERSATION_WORK_CHECK_IN_INTERVAL_MS,
1952
+ CONVERSATION_WORK_STALE_ENQUEUE_MS,
1953
+ getConversation,
1954
+ getConversationWorkState,
1955
+ appendInboundMessage,
1956
+ requestConversationWork,
1957
+ markConversationWorkEnqueued,
1958
+ startConversationWork,
1959
+ checkInConversationWork,
1960
+ drainConversationMailbox,
1961
+ markConversationMessagesInjected,
1962
+ requestConversationContinuation,
1963
+ releaseConversationWork,
1964
+ completeConversationWork,
1965
+ clearExpiredConversationLease,
1966
+ removeActiveConversation,
1967
+ listActiveConversationIds,
1968
+ createStateConversationStore,
1969
+ createSqlStore
1970
+ };