@sentry/junior 0.107.1 → 0.109.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 (68) hide show
  1. package/dist/{agent-hooks-ICPIJAFY.js → agent-hooks-FHHVLNPA.js} +12 -12
  2. package/dist/api/conversations/events.d.ts +1 -1
  3. package/dist/api/conversations/list.d.ts +1 -0
  4. package/dist/api/conversations/schema.d.ts +57 -6
  5. package/dist/api/schema.js +1 -1
  6. package/dist/api.js +116 -27
  7. package/dist/app.d.ts +1 -0
  8. package/dist/app.js +1741 -458
  9. package/dist/{catalog-runtime-FOFNPR3P.js → catalog-runtime-DMGJSLV2.js} +6 -4
  10. package/dist/chat/conversations/history.d.ts +16 -5
  11. package/dist/chat/conversations/message-projection.d.ts +3 -5
  12. package/dist/chat/conversations/sql/store.d.ts +0 -7
  13. package/dist/chat/log-context.d.ts +40 -0
  14. package/dist/chat/logging.d.ts +3 -26
  15. package/dist/chat/resource-events/store.d.ts +6 -0
  16. package/dist/chat/runtime/slack-resume.d.ts +2 -2
  17. package/dist/chat/runtime/slack-runtime.d.ts +3 -0
  18. package/dist/chat/services/turn-session-record.d.ts +0 -2
  19. package/dist/chat/slack/reply.d.ts +7 -19
  20. package/dist/chat/state/conversation.d.ts +1 -0
  21. package/dist/chat/task-execution/slack-work.d.ts +237 -10
  22. package/dist/chat/tool-support/turn-deadline-result.d.ts +3 -0
  23. package/dist/chunk-3SXUV423.js +10 -0
  24. package/dist/{chunk-YFQ7CQDE.js → chunk-72OMGPDS.js} +1017 -39
  25. package/dist/{chunk-2XXEKR44.js → chunk-7Q6WCISF.js} +1 -1
  26. package/dist/{chunk-NVOTGWYX.js → chunk-FENLIFJD.js} +97 -601
  27. package/dist/{chunk-IGHMVDWI.js → chunk-FWJRXN4F.js} +3 -3
  28. package/dist/{chunk-PUAMO4PI.js → chunk-HHVY4SMP.js} +41 -7
  29. package/dist/{chunk-SPUAJVVH.js → chunk-HO5NT6OE.js} +5 -7
  30. package/dist/{chunk-O24R4DCK.js → chunk-ISMWX44P.js} +93 -55
  31. package/dist/{chunk-7FBGKXPE.js → chunk-IVVGFRQQ.js} +5 -0
  32. package/dist/{chunk-EDLNHZH3.js → chunk-IXKSQGIO.js} +226 -191
  33. package/dist/{chunk-SS67LUOK.js → chunk-JNAVF45E.js} +6 -7
  34. package/dist/{chunk-DY7TRYB5.js → chunk-MGUMR35T.js} +1 -1
  35. package/dist/{chunk-CQ7KSO2B.js → chunk-MXW4GL53.js} +11 -15
  36. package/dist/{chunk-RT5BUHR5.js → chunk-O3JMSZLI.js} +1 -1
  37. package/dist/chunk-OCD3OZRK.js +125 -0
  38. package/dist/{chunk-B2Z2H66D.js → chunk-OUHNWH46.js} +60 -5
  39. package/dist/{chunk-VIXFG2VC.js → chunk-PLB7MRCQ.js} +1 -2
  40. package/dist/{chunk-RMVOAJRL.js → chunk-QY636HZU.js} +114 -12
  41. package/dist/{chunk-VH6KWKG2.js → chunk-RGX4V75M.js} +3 -3
  42. package/dist/{chunk-B7PUBQQM.js → chunk-SYAQIR5Z.js} +1 -1
  43. package/dist/chunk-TMBRFULF.js +471 -0
  44. package/dist/{chunk-YIPD3L6L.js → chunk-TT6GMXNA.js} +8 -10
  45. package/dist/{chunk-YT72TOEK.js → chunk-WHMOBKT7.js} +2 -2
  46. package/dist/cli/chat.js +23 -23
  47. package/dist/cli/check.js +8 -6
  48. package/dist/cli/plugins.js +16 -16
  49. package/dist/cli/snapshot-warmup.js +10 -9
  50. package/dist/cli/upgrade.js +314 -3505
  51. package/dist/db/db.d.ts +1 -1
  52. package/dist/{db-DIGO4TGW.js → db-V3NU72O2.js} +8 -6
  53. package/dist/instrumentation.js +3 -2
  54. package/dist/nitro.js +5 -4
  55. package/dist/{runner-ACR2HAIC.js → runner-JYBCQBLK.js} +17 -20
  56. package/dist/{skills-ITNHBARB.js → skills-U6MDAST7.js} +7 -5
  57. package/dist/{validation-AGQ46ZGV.js → validation-72N3JVRU.js} +7 -5
  58. package/dist/version.d.ts +2 -0
  59. package/dist/version.js +7 -0
  60. package/migrations/0006_drop_legacy_conversation_messages.sql +1 -0
  61. package/migrations/README.md +12 -9
  62. package/migrations/meta/0006_snapshot.json +948 -0
  63. package/migrations/meta/_journal.json +7 -0
  64. package/package.json +11 -7
  65. package/dist/chunk-4YF7Z6IA.js +0 -1061
  66. package/dist/chunk-AUUOHQAT.js +0 -115
  67. package/dist/chunk-G3E7SCME.js +0 -28
  68. package/dist/chunk-YNP2ATQX.js +0 -1282
@@ -1,3550 +1,353 @@
1
- import {
2
- getConversation,
3
- listConversationsByActivity,
4
- recordConversationActivity,
5
- recordConversationExecution,
6
- requestConversationWork
7
- } from "../chunk-YNP2ATQX.js";
8
1
  import {
9
2
  defineJuniorPlugins,
10
3
  pluginCatalogConfigFromEnv,
11
- pluginCatalogConfigFromPluginSet,
12
- pluginRuntimeRegistrationsFromPluginSet
4
+ pluginCatalogConfigFromPluginSet
13
5
  } from "../chunk-SG5WAA7H.js";
14
- import {
15
- AGENT_TURN_SESSION_PREFIX,
16
- JUNIOR_THREAD_STATE_TTL_MS,
17
- agentTurnSessionConversationIndexKey,
18
- agentTurnSessionKey,
19
- coerceThreadConversationState,
20
- listAgentTurnSessionSummariesForConversations,
21
- piMessageSchema
22
- } from "../chunk-4YF7Z6IA.js";
23
- import {
24
- addAgentTurnUsage
25
- } from "../chunk-MU6HHZEN.js";
26
- import "../chunk-PDO5BLNM.js";
27
6
  import {
28
7
  loadAppPluginSet
29
8
  } from "../chunk-Y2CM7HXH.js";
30
9
  import {
31
- createPluginLogger,
32
- createPluginState
33
- } from "../chunk-AUUOHQAT.js";
34
- import {
35
- disconnectStateAdapter,
36
- getConnectedStateContext,
37
- getStateAdapter
38
- } from "../chunk-B2Z2H66D.js";
10
+ createJuniorSqlExecutor
11
+ } from "../chunk-TMBRFULF.js";
39
12
  import {
40
- contextProvenance,
41
- conversationEventDataSchema,
42
- conversationMessageProvenanceSchema,
43
- createJuniorSqlExecutor,
44
- createSqlStore,
45
- getDb,
46
- instructionProvenanceFor,
47
- juniorConversationEvents,
48
- juniorConversations,
49
- newConversationEventSchema,
50
- sanitizePostgresJson,
51
- withConversationEventLock
52
- } from "../chunk-NVOTGWYX.js";
53
- import {
54
- COMPACTION_SUMMARY_PREFIX,
55
- getChatConfig,
56
- isCompactionSummaryText,
57
- modelIdForProfile,
58
- modelProfileSchema,
59
- standardModelId,
60
- unescapeXml,
61
- unwrapCurrentInstruction
62
- } from "../chunk-SS67LUOK.js";
13
+ getChatConfig
14
+ } from "../chunk-JNAVF45E.js";
63
15
  import "../chunk-VFUK3X5B.js";
64
- import "../chunk-RT5BUHR5.js";
65
- import {
66
- parseDestination,
67
- sameDestination
68
- } from "../chunk-B7PUBQQM.js";
69
- import {
70
- pluginCatalogRuntime
71
- } from "../chunk-YIPD3L6L.js";
72
- import {
73
- storedSlackActorSchema
74
- } from "../chunk-VIXFG2VC.js";
75
- import "../chunk-O24R4DCK.js";
16
+ import "../chunk-O3JMSZLI.js";
76
17
  import {
77
- isRecord,
78
- toOptionalNumber,
79
- toOptionalString
80
- } from "../chunk-OB42YVAE.js";
18
+ createPluginCatalogRuntime
19
+ } from "../chunk-TT6GMXNA.js";
20
+ import "../chunk-PLB7MRCQ.js";
21
+ import "../chunk-ISMWX44P.js";
22
+ import "../chunk-OB42YVAE.js";
81
23
  import "../chunk-6GWA276C.js";
82
24
  import "../chunk-B3IT5END.js";
83
- import "../chunk-7FBGKXPE.js";
84
25
  import "../chunk-3V7OFJZC.js";
26
+ import "../chunk-IVVGFRQQ.js";
27
+ import "../chunk-OCD3OZRK.js";
85
28
  import "../chunk-MLKGABMK.js";
86
29
 
87
- // src/cli/upgrade.ts
88
- import { createJiti } from "jiti";
89
-
90
- // src/cli/upgrade/migrations/conversation-history/state-conversation-store.ts
91
- function createStateConversationStore(state) {
92
- return {
93
- get: (args) => getConversation({ ...args, state }),
94
- // Task-execution state has no destination records, so visibility is never
95
- // source-confirmed here and cross-context reads fail closed to private.
96
- getDestinationVisibility: async () => void 0,
97
- recordActivity: (args) => recordConversationActivity({ ...args, state }),
98
- recordExecution: (args) => recordConversationExecution({ ...args, state }),
99
- listByActivity: (args) => listConversationsByActivity({ ...args, state })
100
- };
101
- }
102
-
103
- // src/cli/upgrade/migrations/conversations-sql.ts
104
- var CONVERSATION_BACKFILL_LIMIT = 1e4;
105
- async function migrateConversationsToSql(context, options = {}) {
106
- const source = createStateConversationStore(context.stateAdapter);
107
- let target = options.target;
108
- let closeTarget;
109
- if (!target) {
110
- const { sql: sql2 } = getChatConfig();
111
- const executor = createJuniorSqlExecutor({
112
- connectionString: sql2.databaseUrl,
113
- driver: sql2.driver
114
- });
115
- target = createSqlStore(executor);
116
- closeTarget = () => executor.close();
117
- }
118
- const limit = Math.max(1, options.batchSize ?? CONVERSATION_BACKFILL_LIMIT);
119
- try {
120
- const [stateConversations, sqlConversations] = await Promise.all([
121
- source.listByActivity({ limit }),
122
- target.listByActivity({ limit })
123
- ]);
124
- const byId = new Map(
125
- sqlConversations.map((conversation) => [
126
- conversation.conversationId,
127
- conversation
128
- ])
129
- );
130
- for (const conversation of stateConversations) {
131
- const existing = byId.get(conversation.conversationId);
132
- const existingExecutionAt = existing?.execution.updatedAtMs ?? existing?.updatedAtMs ?? 0;
133
- const stateExecutionAt = conversation.execution.updatedAtMs ?? conversation.updatedAtMs;
134
- if (!existing || stateExecutionAt >= existingExecutionAt) {
135
- byId.set(conversation.conversationId, conversation);
136
- }
137
- }
138
- const conversations = [...byId.values()].sort(
139
- (left, right) => right.lastActivityAtMs - left.lastActivityAtMs || left.conversationId.localeCompare(right.conversationId)
140
- ).slice(0, limit);
141
- const summaries = await listAgentTurnSessionSummariesForConversations(
142
- context.stateAdapter,
143
- conversations.map((conversation) => conversation.conversationId)
144
- );
145
- for (const conversation of conversations) {
146
- const conversationSummaries = summaries.get(conversation.conversationId) ?? [];
147
- const executionSummary = conversation.execution.runId ? conversationSummaries.find(
148
- (summary) => summary.sessionId === conversation.execution.runId
149
- ) : void 0;
150
- await target.backfillConversation(
151
- conversation,
152
- conversationSummaries.length > 0 ? {
153
- durationMs: conversationSummaries.reduce(
154
- (total, summary) => total + summary.cumulativeDurationMs,
155
- 0
156
- ),
157
- usage: addAgentTurnUsage(
158
- ...conversationSummaries.map(
159
- (summary) => summary.cumulativeUsage
160
- )
161
- ),
162
- executionDurationMs: executionSummary?.cumulativeDurationMs ?? 0,
163
- executionUsage: executionSummary?.cumulativeUsage
164
- } : void 0
165
- );
166
- }
167
- return {
168
- existing: 0,
169
- migrated: conversations.length,
170
- missing: 0,
171
- scanned: conversations.length
172
- };
173
- } finally {
174
- await closeTarget?.();
175
- }
176
- }
177
- var sqlConversationMigration = {
178
- name: "backfill-conversations-sql",
179
- run: migrateConversationsToSql
180
- };
181
-
182
30
  // src/chat/conversations/sql/migrations.ts
183
31
  import { basename, dirname, join, resolve } from "path";
184
32
  import { fileURLToPath } from "url";
185
33
  import { readMigrationFiles } from "drizzle-orm/migrator";
186
- var LEGACY_CORE_MIGRATION_IDS = [
187
- "0001_conversation_core",
188
- "0002_slack_destination_visibility_backfill",
189
- "0003_user_identities",
190
- "0004_actor_cutover",
191
- "0005_conversation_transcripts"
192
- ];
193
- var LEGACY_METRICS_MIGRATION_ID = "0006_conversation_metrics";
194
- var LEGACY_CORE_MIGRATION_CHECKSUMS = {
195
- "0001_conversation_core": "78fe050d8bec8ba18e2e3192497b3d8ad6b45fbb66ad4859377fb2202ed57651",
196
- "0002_slack_destination_visibility_backfill": "fb590a09fa51db471a748e3d7abb4137f521ee8df97f6e9ef5563121be98c394",
197
- "0003_user_identities": "67d9c9c26cbd76213614eb6d7a7cc7e2501fc20e92321eb5176a08ce39cd2efb",
198
- "0004_actor_cutover": "d41b8bfa66b8a88d69e84af38950025ba4c9be56341565cbe1411f0ca50c1dc2",
199
- "0005_conversation_transcripts": "add299d1b254e023f89b5993c417dd2248dc009e874efdeaf31ec0732e0d4fb4",
200
- "0006_conversation_metrics": "7c7ca5c9e11ed4b0e14737fd90d3348ea46e306c88fdf31199b7afb2a11c6a41"
201
- };
34
+
35
+ // src/db/postgres-error.ts
36
+ function isPostgresErrorCode(error, code) {
37
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
38
+ }
39
+
40
+ // src/chat/conversations/sql/migrations.ts
41
+ var CORE_MIGRATION_BRIDGE_VERSION = "0.107.1";
202
42
  var MIGRATIONS_TABLE = "__drizzle_junior_core";
203
43
  function migrationFolder() {
204
44
  const moduleDir = dirname(fileURLToPath(import.meta.url));
205
45
  const packageRoot = basename(moduleDir) === "dist" ? dirname(moduleDir) : basename(dirname(moduleDir)) === "dist" ? resolve(moduleDir, "../..") : resolve(moduleDir, "../../../..");
206
- return join(packageRoot, "migrations");
207
- }
208
- async function adoptLegacyMigrationState(executor, migrationsFolder) {
209
- const [tables] = await executor.query(`
210
- SELECT
211
- to_regclass('drizzle.__drizzle_junior_core')::text AS "drizzleTable",
212
- to_regclass('public.junior_schema_migrations')::text AS "legacyTable"
213
- `);
214
- if (!tables?.legacyTable || tables.drizzleTable) {
215
- return;
216
- }
217
- const migrations = readMigrationFiles({ migrationsFolder });
218
- const [metrics] = await executor.query(`
219
- SELECT count(*)::integer AS "columnCount"
220
- FROM information_schema.columns
221
- WHERE table_schema = 'public'
222
- AND table_name = 'junior_conversations'
223
- AND column_name IN (
224
- 'duration_ms',
225
- 'usage_json',
226
- 'execution_duration_ms',
227
- 'execution_usage_json'
228
- )
229
- `);
230
- const legacyRecords = await executor.query("SELECT id, checksum FROM junior_schema_migrations");
231
- const legacyRecordsById = new Map(
232
- legacyRecords.map((record) => [record.id, record.checksum])
233
- );
234
- const metricColumnCount = metrics?.columnCount ?? 0;
235
- if (metricColumnCount !== 0 && metricColumnCount !== 4) {
236
- throw new Error(
237
- `Cannot adopt partial legacy metrics state: found ${metricColumnCount} of 4 required columns`
238
- );
239
- }
240
- const metricsComplete = metricColumnCount === 4;
241
- const hasMetricsRecord = legacyRecordsById.has(LEGACY_METRICS_MIGRATION_ID);
242
- if (metricsComplete !== hasMetricsRecord) {
243
- throw new Error(
244
- "Cannot adopt legacy core migration state: legacy metrics migration record does not match physical metric columns"
245
- );
246
- }
247
- const expectedIds = metricsComplete ? [...LEGACY_CORE_MIGRATION_IDS, LEGACY_METRICS_MIGRATION_ID] : [...LEGACY_CORE_MIGRATION_IDS];
248
- const missingIds = expectedIds.filter((id) => !legacyRecordsById.has(id));
249
- if (missingIds.length > 0) {
250
- throw new Error(
251
- `Cannot adopt partial legacy core migration state; missing: ${missingIds.join(", ")}`
252
- );
253
- }
254
- const checksumMismatches = expectedIds.filter(
255
- (id) => legacyRecordsById.get(id) !== LEGACY_CORE_MIGRATION_CHECKSUMS[id]
256
- );
257
- if (checksumMismatches.length > 0) {
258
- throw new Error(
259
- `Cannot adopt legacy core migration state: checksum mismatch: ${checksumMismatches.join(", ")}`
260
- );
261
- }
262
- const [baseline] = await executor.query(`
263
- SELECT
264
- to_regclass('public.junior_conversation_events')::text AS "conversationEventsTable",
265
- to_regclass('public.junior_conversation_messages_search_idx')::text AS "searchIndex",
266
- EXISTS (
267
- SELECT 1
268
- FROM information_schema.tables
269
- WHERE table_schema = 'public'
270
- AND table_name = 'junior_agent_steps'
271
- AND table_type = 'BASE TABLE'
272
- ) AS "legacyAgentStepsTable",
273
- EXISTS (
274
- SELECT 1
275
- FROM information_schema.columns
276
- WHERE table_schema = 'public'
277
- AND table_name = 'junior_conversations'
278
- AND column_name = 'metric_run_id'
279
- ) AS "metricRunIdColumn"
280
- `);
281
- if (!baseline?.legacyAgentStepsTable || baseline.conversationEventsTable) {
282
- throw new Error(
283
- "Cannot adopt legacy core migration state: expected the pre-Drizzle junior_agent_steps table and no junior_conversation_events table"
284
- );
285
- }
286
- const postBaselineMarkers = [
287
- baseline.searchIndex ? "junior_conversation_messages_search_idx" : void 0,
288
- baseline.metricRunIdColumn ? "junior_conversations.metric_run_id" : void 0
289
- ].filter((marker) => marker !== void 0);
290
- if (postBaselineMarkers.length > 0) {
291
- throw new Error(
292
- `Cannot adopt legacy core migration state: post-baseline schema markers are already present: ${postBaselineMarkers.join(", ")}`
293
- );
294
- }
295
- const migration = metricsComplete ? migrations[1] : migrations[0];
296
- if (!migration) {
297
- throw new Error("No core Drizzle migrations were packaged");
298
- }
299
- await executor.transaction(async () => {
300
- await executor.execute("CREATE SCHEMA IF NOT EXISTS drizzle");
301
- await executor.execute(`
302
- CREATE TABLE IF NOT EXISTS drizzle.__drizzle_junior_core (
303
- id SERIAL PRIMARY KEY,
304
- hash TEXT NOT NULL,
305
- created_at BIGINT
306
- )
307
- `);
308
- await executor.execute(
309
- `INSERT INTO drizzle.__drizzle_junior_core (hash, created_at)
310
- VALUES ($1, $2)`,
311
- [migration.hash, migration.folderMillis]
312
- );
313
- });
314
- }
315
- async function migrateSchema(executor) {
316
- const migrationsFolder = migrationFolder();
317
- await executor.withMigrationLock(MIGRATIONS_TABLE, async () => {
318
- await adoptLegacyMigrationState(executor, migrationsFolder);
319
- await executor.migrate({
320
- migrationsFolder,
321
- migrationsTable: MIGRATIONS_TABLE
322
- });
323
- });
324
- }
325
-
326
- // src/cli/upgrade/migrations/core-sql.ts
327
- async function migrateCoreSqlSchema(_context) {
328
- const { sql: sql2 } = getChatConfig();
329
- const executor = createJuniorSqlExecutor({
330
- connectionString: sql2.databaseUrl,
331
- driver: sql2.driver
332
- });
333
- try {
334
- await migrateSchema(executor);
335
- return { existing: 0, migrated: 0, missing: 0, scanned: 0 };
336
- } finally {
337
- await executor.close();
338
- }
339
- }
340
- var coreSqlSchemaMigration = {
341
- name: "core-sql-schema",
342
- run: migrateCoreSqlSchema
343
- };
344
-
345
- // src/cli/upgrade/migrations/conversation-history/import.ts
346
- import { z as z3 } from "zod";
347
- import { eq as eq2 } from "drizzle-orm";
348
-
349
- // src/cli/upgrade/migrations/conversation-history/session-log.ts
350
- import { z } from "zod";
351
- var AGENT_SESSION_LOG_PREFIX = "junior:agent-session-log";
352
- var AGENT_SESSION_LOG_SCHEMA_VERSION = 2;
353
- var INITIAL_SESSION_ID = "session_0";
354
- var schemaVersionSchema = z.union([z.literal(1), z.literal(2)]);
355
- function legacyActorProvenance(actor) {
356
- if (actor.teamId && actor.slackUserId && actor.platform) {
357
- return instructionProvenanceFor({
358
- platform: "slack",
359
- teamId: actor.teamId,
360
- userId: actor.slackUserId,
361
- ...actor.slackUserName ? { userName: actor.slackUserName } : {},
362
- ...actor.fullName ? { fullName: actor.fullName } : {},
363
- ...actor.email ? { email: actor.email } : {}
364
- });
365
- }
366
- return contextProvenance;
367
- }
368
- var piMessageEntrySchema = z.object({
369
- schemaVersion: schemaVersionSchema,
370
- type: z.literal("pi_message"),
371
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
372
- message: piMessageSchema,
373
- provenance: conversationMessageProvenanceSchema.optional(),
374
- // Legacy v1 latest-wins actor, decoded into provenance on read.
375
- actor: storedSlackActorSchema.optional()
376
- });
377
- var projectionResetEntrySchema = z.object({
378
- schemaVersion: schemaVersionSchema,
379
- type: z.literal("projection_reset"),
380
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
381
- messages: z.array(piMessageSchema),
382
- provenance: z.array(conversationMessageProvenanceSchema).optional(),
383
- // Legacy v1 latest-wins actor; v1 resets carry no per-message provenance.
384
- actor: storedSlackActorSchema.optional()
385
- });
386
- var actorRecordedEntrySchema = z.object({
387
- schemaVersion: schemaVersionSchema,
388
- type: z.literal("actor_recorded"),
389
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
390
- actor: storedSlackActorSchema
391
- });
392
- var mcpProviderConnectedEntrySchema = z.object({
393
- schemaVersion: schemaVersionSchema,
394
- type: z.literal("mcp_provider_connected"),
395
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
396
- provider: z.string().min(1)
397
- });
398
- var authorizationKindSchema = z.union([
399
- z.literal("plugin"),
400
- z.literal("mcp")
401
- ]);
402
- var authorizationRequestedEntrySchema = z.object({
403
- schemaVersion: schemaVersionSchema,
404
- type: z.literal("authorization_requested"),
405
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
406
- createdAtMs: z.number().int().nonnegative(),
407
- kind: authorizationKindSchema,
408
- provider: z.string().min(1),
409
- actorId: z.string().min(1),
410
- authorizationId: z.string().min(1),
411
- delivery: z.union([
412
- z.literal("private_link_sent"),
413
- z.literal("private_link_reused")
414
- ])
415
- });
416
- var authorizationCompletedEntrySchema = z.object({
417
- schemaVersion: schemaVersionSchema,
418
- type: z.literal("authorization_completed"),
419
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
420
- createdAtMs: z.number().int().nonnegative(),
421
- kind: authorizationKindSchema,
422
- provider: z.string().min(1),
423
- actorId: z.string().min(1),
424
- authorizationId: z.string().min(1)
425
- });
426
- var transcriptRefSchema = z.object({
427
- type: z.literal("advisor_session"),
428
- parentConversationId: z.string().min(1),
429
- key: z.string().min(1)
430
- });
431
- var toolExecutionStartedEntrySchema = z.object({
432
- schemaVersion: schemaVersionSchema,
433
- type: z.literal("tool_execution_started"),
434
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
435
- createdAtMs: z.number().int().nonnegative(),
436
- toolCallId: z.string().min(1),
437
- toolName: z.string().min(1),
438
- args: z.unknown().optional()
439
- });
440
- var subagentStartedEntrySchema = z.object({
441
- schemaVersion: schemaVersionSchema,
442
- type: z.literal("subagent_started"),
443
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
444
- subagentInvocationId: z.string().min(1),
445
- subagentKind: z.string().min(1),
446
- parentToolCallId: z.string().min(1).optional(),
447
- parentConversationId: z.string().min(1),
448
- parentSessionId: z.string().min(1).optional(),
449
- transcriptRef: transcriptRefSchema,
450
- historyMode: z.literal("shared"),
451
- modelId: z.string().min(1).optional(),
452
- reasoningLevel: z.string().min(1).optional(),
453
- createdAtMs: z.number().int().nonnegative()
454
- });
455
- var subagentEndedEntrySchema = z.object({
456
- schemaVersion: schemaVersionSchema,
457
- type: z.literal("subagent_ended"),
458
- sessionId: z.string().min(1).default(INITIAL_SESSION_ID),
459
- subagentInvocationId: z.string().min(1),
460
- outcome: z.union([
461
- z.literal("success"),
462
- z.literal("error"),
463
- z.literal("aborted")
464
- ]),
465
- errorCode: z.string().min(1).optional(),
466
- transcriptEndMessageIndex: z.number().int().nonnegative().optional(),
467
- transcriptStartMessageIndex: z.number().int().nonnegative().optional(),
468
- createdAtMs: z.number().int().nonnegative()
469
- });
470
- var sessionLogEntrySchema = z.discriminatedUnion("type", [
471
- piMessageEntrySchema,
472
- projectionResetEntrySchema,
473
- actorRecordedEntrySchema,
474
- mcpProviderConnectedEntrySchema,
475
- authorizationRequestedEntrySchema,
476
- authorizationCompletedEntrySchema,
477
- toolExecutionStartedEntrySchema,
478
- subagentStartedEntrySchema,
479
- subagentEndedEntrySchema
480
- ]);
481
- function key(scope) {
482
- const prefix = getChatConfig().state.keyPrefix;
483
- return [
484
- ...prefix ? [prefix] : [],
485
- AGENT_SESSION_LOG_PREFIX,
486
- scope.conversationId
487
- ].join(":");
488
- }
489
- function rawKey(scope) {
490
- return [AGENT_SESSION_LOG_PREFIX, scope.conversationId].join(":");
491
- }
492
- function storedRecord(value) {
493
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
494
- }
495
- function migrateStoredEntry(value) {
496
- const record = storedRecord(value);
497
- if (!record) {
498
- return value;
499
- }
500
- const migrated = { ...record };
501
- if ("requester" in migrated && !("actor" in migrated)) {
502
- migrated.actor = migrated.requester;
503
- }
504
- delete migrated.requester;
505
- if (migrated.type === "requester_recorded") {
506
- migrated.type = "actor_recorded";
507
- }
508
- if ("requesterId" in migrated && !("actorId" in migrated)) {
509
- migrated.actorId = migrated.requesterId;
510
- }
511
- delete migrated.requesterId;
512
- return migrated;
513
- }
514
- function decode(value) {
515
- if (typeof value === "string") {
516
- return decode(JSON.parse(value));
517
- }
518
- const parsed = sessionLogEntrySchema.safeParse(migrateStoredEntry(value));
519
- if (parsed.success) {
520
- return parsed.data;
521
- }
522
- return {
523
- schemaVersion: AGENT_SESSION_LOG_SCHEMA_VERSION,
524
- type: "pi_message",
525
- sessionId: INITIAL_SESSION_ID,
526
- message: piMessageSchema.parse(value)
527
- };
528
- }
529
- function redisStore(redisStateAdapter) {
530
- const client = redisStateAdapter.getClient();
531
- return {
532
- async read(scope) {
533
- const values = await client.lRange(key(scope), 0, -1);
534
- return values.map(decode);
535
- }
536
- };
537
- }
538
- function stateStore() {
539
- const stateAdapter = getStateAdapter();
540
- return {
541
- async read(scope) {
542
- const listKey = rawKey(scope);
543
- const value = await stateAdapter.get(listKey);
544
- if (Array.isArray(value)) {
545
- return value.map(decode);
546
- }
547
- const values = await stateAdapter.getList(listKey);
548
- return values.map(decode);
549
- }
550
- };
551
- }
552
- async function defaultStore() {
553
- const { redisStateAdapter, stateAdapter } = await getConnectedStateContext();
554
- if (redisStateAdapter) {
555
- return redisStore(redisStateAdapter);
556
- }
557
- await stateAdapter.connect();
558
- return stateStore();
559
- }
560
- async function loadEntries(args) {
561
- const store = args.store ?? await defaultStore();
562
- return (await store.read(args)).map(decode);
563
- }
564
- async function readSessionLogEntries(args) {
565
- return loadEntries(args);
566
- }
567
-
568
- // src/cli/upgrade/migrations/conversation-history/advisor-session.ts
569
- import { z as z2 } from "zod";
570
- var legacyAdvisorMessagesSchema = z2.array(piMessageSchema);
571
- function key2(conversationId) {
572
- return `junior:${conversationId}:advisor_session`;
573
- }
574
- function createLegacyAdvisorSessionReader() {
575
- return {
576
- load: async (conversationId) => {
577
- const stateAdapter = getStateAdapter();
578
- await stateAdapter.connect();
579
- return legacyAdvisorMessagesSchema.parse(
580
- structuredClone(
581
- await stateAdapter.get(key2(conversationId)) ?? []
582
- )
583
- );
584
- }
585
- };
586
- }
587
-
588
- // src/cli/upgrade/migrations/conversation-history/legacy-history-import.ts
589
- import { eq, sql } from "drizzle-orm";
590
-
591
- // src/cli/upgrade/migrations/conversation-history/legacy-context-message.ts
592
- var LEGACY_COMPACTION_SUMMARY_PREFIX = "Context handoff summary for future Junior turns:";
593
- function isLegacyCheckpointText(text) {
594
- const normalized = (unwrapCurrentInstruction(text) ?? text).trimStart();
595
- return normalized.startsWith(LEGACY_COMPACTION_SUMMARY_PREFIX);
596
- }
597
- function isLegacyOrCurrentCheckpointText(text) {
598
- return isCompactionSummaryText(text) || isLegacyCheckpointText(text);
599
- }
600
- function normalizeText(text) {
601
- if (!isLegacyCheckpointText(text)) return text;
602
- return text.replace(
603
- LEGACY_COMPACTION_SUMMARY_PREFIX,
604
- COMPACTION_SUMMARY_PREFIX
605
- );
606
- }
607
- function normalizeLegacyContextMessage(message) {
608
- const record = message;
609
- if (typeof record.content === "string") {
610
- const content2 = normalizeText(record.content);
611
- return content2 === record.content ? message : { ...record, content: content2 };
612
- }
613
- if (!Array.isArray(record.content)) return message;
614
- let changed = false;
615
- const content = record.content.map((part) => {
616
- if (!part || typeof part !== "object" || typeof part.text !== "string") {
617
- return part;
618
- }
619
- const text = normalizeText(part.text);
620
- if (text === part.text) return part;
621
- changed = true;
622
- return { ...part, text };
623
- });
624
- return changed ? { ...record, content } : message;
625
- }
626
- function isLegacyOrCurrentCheckpointMessage(message) {
627
- const record = message;
628
- if (typeof record.content === "string") {
629
- return isLegacyOrCurrentCheckpointText(record.content);
630
- }
631
- if (!Array.isArray(record.content)) return false;
632
- return record.content.some(
633
- (part) => part !== null && typeof part === "object" && typeof part.text === "string" && isLegacyOrCurrentCheckpointText(part.text)
634
- );
635
- }
636
-
637
- // src/cli/upgrade/migrations/conversation-history/legacy-history-import.ts
638
- var INITIAL_SESSION_ID2 = "session_0";
639
- var ADVISOR_TASK_OPEN = "<advisor-task>\n";
640
- var ADVISOR_TASK_CLOSE = "\n</advisor-task>";
641
- var ADVISOR_CONTEXT_OPEN = "<executor-context>\n";
642
- var ADVISOR_CONTEXT_CLOSE = "\n</executor-context>";
643
- function importedAdvisorChildConversationId(parentConversationId) {
644
- return `advisor:${parentConversationId}`;
645
- }
646
- function epochFromSessionId(sessionId) {
647
- const match = /^session_(\d+)$/.exec(sessionId);
648
- return match ? Number(match[1]) : 0;
649
- }
650
- function messageTimestampMs(message) {
651
- const timestamp = message.timestamp;
652
- return typeof timestamp === "number" ? timestamp : void 0;
653
- }
654
- function readAdvisorRequest(text) {
655
- if (!text.startsWith(ADVISOR_TASK_OPEN) || !text.endsWith(ADVISOR_CONTEXT_CLOSE)) {
656
- return void 0;
657
- }
658
- const taskEnd = text.indexOf(ADVISOR_TASK_CLOSE, ADVISOR_TASK_OPEN.length);
659
- if (taskEnd < 0) {
660
- return void 0;
661
- }
662
- const contextStart = taskEnd + ADVISOR_TASK_CLOSE.length + 2;
663
- if (!text.startsWith(ADVISOR_CONTEXT_OPEN, contextStart)) {
664
- return void 0;
665
- }
666
- const task = text.slice(ADVISOR_TASK_OPEN.length, taskEnd);
667
- const context = text.slice(
668
- contextStart + ADVISOR_CONTEXT_OPEN.length,
669
- -ADVISOR_CONTEXT_CLOSE.length
670
- );
671
- return `${unescapeXml(task)}
672
-
673
- Executor context:
674
- ${unescapeXml(context)}`;
675
- }
676
- function normalizeAdvisorMessage(message) {
677
- const record = message;
678
- if (record.role !== "user" || !Array.isArray(record.content)) {
679
- return message;
680
- }
681
- let changed = false;
682
- const content = record.content.map((part) => {
683
- if (!part || typeof part !== "object" || part.type !== "text" || typeof part.text !== "string") {
684
- return part;
685
- }
686
- const text = readAdvisorRequest(part.text);
687
- if (text === void 0) {
688
- return part;
689
- }
690
- changed = true;
691
- return { ...part, text };
692
- });
693
- return changed ? { ...record, content } : message;
694
- }
695
- function piEntryProvenance(entry) {
696
- if (entry.provenance) {
697
- return entry.provenance;
698
- }
699
- if (entry.actor) {
700
- return legacyActorProvenance(entry.actor);
701
- }
702
- return contextProvenance;
703
- }
704
- function convertLegacySessionLog(args) {
705
- const events = [];
706
- const fallback = args.fallbackCreatedAtMs;
707
- let advisorChildConversationId;
708
- let seq = 0;
709
- const push = (historyVersion, data, createdAtMs) => {
710
- events.push({ seq, historyVersion, data, createdAtMs });
711
- seq += 1;
712
- };
713
- for (const entry of args.entries) {
714
- const epoch = epochFromSessionId(entry.sessionId ?? INITIAL_SESSION_ID2);
715
- switch (entry.type) {
716
- case "pi_message": {
717
- const message = entry.message;
718
- push(
719
- epoch,
720
- {
721
- type: "agent_step",
722
- message,
723
- provenance: piEntryProvenance(entry)
724
- },
725
- messageTimestampMs(message) ?? fallback
726
- );
727
- break;
728
- }
729
- case "projection_reset": {
730
- const provenance = entry.provenance ?? entry.messages.map(() => contextProvenance);
731
- if (provenance.length !== entry.messages.length) {
732
- throw new Error(
733
- "projection_reset provenance must align one-to-one with messages"
734
- );
735
- }
736
- const resetCreatedAtMs = entry.messages.map(messageTimestampMs).find(
737
- (timestamp) => timestamp !== void 0
738
- ) ?? fallback;
739
- const checkpointIndex = entry.messages.findIndex(
740
- isLegacyOrCurrentCheckpointMessage
741
- );
742
- push(
743
- epoch,
744
- {
745
- type: "compaction",
746
- modelProfile: "standard",
747
- modelId: args.modelId,
748
- replacementHistory: entry.messages.map((message, index) => ({
749
- message: index === checkpointIndex ? normalizeLegacyContextMessage(message) : message,
750
- provenance: provenance[index]
751
- }))
752
- },
753
- resetCreatedAtMs
754
- );
755
- break;
756
- }
757
- case "mcp_provider_connected": {
758
- push(
759
- epoch,
760
- { type: "mcp_provider_connected", provider: entry.provider },
761
- fallback
762
- );
763
- break;
764
- }
765
- case "authorization_requested": {
766
- push(
767
- epoch,
768
- {
769
- type: "authorization_requested",
770
- kind: entry.kind,
771
- provider: entry.provider,
772
- actorId: entry.actorId,
773
- authorizationId: entry.authorizationId,
774
- delivery: entry.delivery
775
- },
776
- entry.createdAtMs
777
- );
778
- break;
779
- }
780
- case "authorization_completed": {
781
- push(
782
- epoch,
783
- {
784
- type: "authorization_completed",
785
- kind: entry.kind,
786
- provider: entry.provider,
787
- actorId: entry.actorId,
788
- authorizationId: entry.authorizationId
789
- },
790
- entry.createdAtMs
791
- );
792
- break;
793
- }
794
- case "tool_execution_started": {
795
- push(
796
- epoch,
797
- {
798
- type: "tool_execution_started",
799
- toolCallId: entry.toolCallId,
800
- toolName: entry.toolName
801
- },
802
- entry.createdAtMs
803
- );
804
- break;
805
- }
806
- case "subagent_started": {
807
- const childConversationId = importedAdvisorChildConversationId(
808
- args.conversationId
809
- );
810
- advisorChildConversationId = childConversationId;
811
- push(
812
- epoch,
813
- {
814
- type: "subagent_started",
815
- subagentInvocationId: entry.subagentInvocationId,
816
- subagentKind: entry.subagentKind,
817
- ...entry.parentToolCallId ? { parentToolCallId: entry.parentToolCallId } : {},
818
- childConversationId
819
- },
820
- entry.createdAtMs
821
- );
822
- break;
823
- }
824
- case "subagent_ended": {
825
- push(
826
- epoch,
827
- {
828
- type: "subagent_ended",
829
- subagentInvocationId: entry.subagentInvocationId,
830
- outcome: entry.outcome,
831
- ...entry.errorCode ? { errorCode: entry.errorCode } : {}
832
- },
833
- entry.createdAtMs
834
- );
835
- break;
836
- }
837
- case "actor_recorded": {
838
- break;
839
- }
840
- }
841
- }
842
- return {
843
- events,
844
- ...advisorChildConversationId ? { advisorChildConversationId } : {}
845
- };
846
- }
847
- function convertAdvisorMessages(messages, fallbackCreatedAtMs) {
848
- return messages.map((sourceMessage, seq) => {
849
- const message = normalizeAdvisorMessage(sourceMessage);
850
- return {
851
- seq,
852
- historyVersion: 0,
853
- data: { type: "agent_step", message, provenance: contextProvenance },
854
- createdAtMs: messageTimestampMs(message) ?? fallbackCreatedAtMs
855
- };
856
- });
857
- }
858
- function insertRow(conversationId, event) {
859
- const { type, ...payload } = conversationEventDataSchema.parse(event.data);
860
- return {
861
- conversationId,
862
- seq: event.seq,
863
- historyVersion: event.historyVersion,
864
- schemaVersion: 1,
865
- idempotencyKey: event.idempotencyKey ?? null,
866
- type,
867
- payload: sanitizePostgresJson(payload),
868
- createdAt: new Date(event.createdAtMs)
869
- };
870
- }
871
- function mergeImportedChronology(executionEvents, messages) {
872
- const visibleEvents = messages.flatMap((message) => {
873
- const recorded = {
874
- seq: 0,
875
- historyVersion: 0,
876
- idempotencyKey: `message:${message.messageId}`,
877
- data: {
878
- type: "message",
879
- messageId: message.messageId,
880
- role: message.role,
881
- text: message.text,
882
- ...message.authorIdentityId ? { authorIdentityId: message.authorIdentityId } : {},
883
- ...message.meta ? { meta: message.meta } : {}
884
- },
885
- createdAtMs: message.createdAtMs
886
- };
887
- return message.repliedAtMs === void 0 ? [recorded] : [
888
- recorded,
889
- {
890
- seq: 0,
891
- historyVersion: 0,
892
- idempotencyKey: `message:${message.messageId}:handled`,
893
- data: {
894
- type: "message_handled",
895
- messageId: message.messageId
896
- },
897
- createdAtMs: message.repliedAtMs
898
- }
899
- ];
900
- }).map((event, sourceOrder) => ({ event, sourceOrder })).sort(
901
- (left, right) => left.event.createdAtMs - right.event.createdAtMs || left.sourceOrder - right.sourceOrder
902
- );
903
- const merged = [];
904
- let visibleIndex = 0;
905
- let currentEpoch = 0;
906
- const push = (event, historyVersion) => {
907
- merged.push({ ...event, seq: merged.length, historyVersion });
908
- };
909
- for (const executionEvent of [...executionEvents].sort(
910
- (left, right) => left.seq - right.seq
911
- )) {
912
- while (visibleIndex < visibleEvents.length && visibleEvents[visibleIndex].event.createdAtMs <= executionEvent.createdAtMs) {
913
- push(visibleEvents[visibleIndex].event, currentEpoch);
914
- visibleIndex += 1;
915
- }
916
- push(executionEvent, executionEvent.historyVersion);
917
- currentEpoch = Math.max(currentEpoch, executionEvent.historyVersion);
918
- }
919
- while (visibleIndex < visibleEvents.length) {
920
- push(visibleEvents[visibleIndex].event, currentEpoch);
921
- visibleIndex += 1;
922
- }
923
- const firstVisibleSeq = merged.find(
924
- (event) => event.data.type === "message"
925
- )?.seq;
926
- return merged.map(
927
- (event) => event.data.type === "messages_summarized" ? {
928
- ...event,
929
- data: {
930
- ...event.data,
931
- historyFromSeq: firstVisibleSeq ?? event.seq + 1
932
- }
933
- } : event
934
- );
935
- }
936
- async function writeLegacyImport(executor, args) {
937
- return withConversationEventLock(executor, args.conversationId, async () => {
938
- const db = executor.db();
939
- const conversations = await db.select({
940
- transcriptPurgedAt: juniorConversations.transcriptPurgedAt
941
- }).from(juniorConversations).where(eq(juniorConversations.conversationId, args.conversationId)).for("update");
942
- if (conversations[0]?.transcriptPurgedAt) {
943
- return false;
944
- }
945
- const existing = await db.select({ seq: juniorConversationEvents.seq }).from(juniorConversationEvents).where(eq(juniorConversationEvents.conversationId, args.conversationId)).limit(1);
946
- if (existing.length > 0) {
947
- return false;
948
- }
949
- const createdAt = new Date(args.fallbackCreatedAtMs);
950
- await ensureConversationRow(
951
- executor,
952
- args.conversationId,
953
- createdAt,
954
- new Date(args.lastActivityAtMs)
955
- );
956
- const mergedEvents = mergeImportedChronology(
957
- args.events,
958
- args.messages ?? []
959
- );
960
- if (mergedEvents.length > 0) {
961
- await db.insert(juniorConversationEvents).values(
962
- mergedEvents.map((event) => insertRow(args.conversationId, event))
963
- );
964
- }
965
- if (args.child) {
966
- const childCreatedAtMs = args.child.events.length > 0 ? Math.min(...args.child.events.map((event) => event.createdAtMs)) : args.fallbackCreatedAtMs;
967
- const childLastActivityAtMs = args.child.events.length > 0 ? Math.max(...args.child.events.map((event) => event.createdAtMs)) : childCreatedAtMs;
968
- await insertLegacyAdvisorChildRow(
969
- executor,
970
- args.child.conversationId,
971
- args.conversationId,
972
- new Date(childCreatedAtMs),
973
- new Date(childLastActivityAtMs)
974
- );
975
- if (args.child.events.length > 0) {
976
- await db.insert(juniorConversationEvents).values(
977
- args.child.events.map(
978
- (event) => insertRow(args.child.conversationId, event)
979
- )
980
- );
981
- }
982
- }
983
- return true;
984
- });
985
- }
986
- async function ensureConversationRow(executor, conversationId, createdAt, lastActivityAt) {
987
- await executor.db().insert(juniorConversations).values({
988
- conversationId,
989
- schemaVersion: 1,
990
- createdAt,
991
- lastActivityAt,
992
- updatedAt: lastActivityAt,
993
- executionStatus: "idle"
994
- }).onConflictDoUpdate({
995
- target: juniorConversations.conversationId,
996
- set: {
997
- createdAt: sql`least(${juniorConversations.createdAt}, excluded.created_at)`,
998
- lastActivityAt: sql`greatest(${juniorConversations.lastActivityAt}, excluded.last_activity_at)`,
999
- updatedAt: sql`greatest(${juniorConversations.updatedAt}, excluded.updated_at)`
1000
- }
1001
- });
1002
- }
1003
- async function insertLegacyAdvisorChildRow(executor, childConversationId, parentConversationId, createdAt, lastActivityAt) {
1004
- await ensureConversationRow(
1005
- executor,
1006
- parentConversationId,
1007
- createdAt,
1008
- lastActivityAt
1009
- );
1010
- await executor.db().insert(juniorConversations).values({
1011
- conversationId: childConversationId,
1012
- schemaVersion: 1,
1013
- parentConversationId,
1014
- createdAt,
1015
- lastActivityAt,
1016
- updatedAt: lastActivityAt,
1017
- executionStatus: "idle"
1018
- }).onConflictDoUpdate({
1019
- target: juniorConversations.conversationId,
1020
- set: {
1021
- createdAt: sql`least(${juniorConversations.createdAt}, excluded.created_at)`,
1022
- lastActivityAt: sql`greatest(${juniorConversations.lastActivityAt}, excluded.last_activity_at)`,
1023
- updatedAt: sql`greatest(${juniorConversations.updatedAt}, excluded.updated_at)`
1024
- }
1025
- });
1026
- const rows = await executor.db().select({
1027
- parentConversationId: juniorConversations.parentConversationId
1028
- }).from(juniorConversations).where(eq(juniorConversations.conversationId, childConversationId));
1029
- const child = rows[0];
1030
- if (!child || child.parentConversationId !== parentConversationId) {
1031
- throw new Error(
1032
- "Legacy advisor child conflicts with existing conversation lineage"
1033
- );
1034
- }
1035
- }
1036
-
1037
- // src/cli/upgrade/migrations/conversation-history/import.ts
1038
- var legacyVisibleMessageSchema = z3.object({
1039
- id: z3.string(),
1040
- role: z3.enum(["user", "assistant", "system"]),
1041
- text: z3.string(),
1042
- createdAtMs: z3.number().finite(),
1043
- author: z3.object({}).passthrough().optional(),
1044
- meta: z3.object({}).passthrough().optional()
1045
- });
1046
- var legacyCompactionSchema = z3.object({
1047
- coveredMessageIds: z3.array(z3.string()),
1048
- createdAtMs: z3.number().finite(),
1049
- id: z3.string(),
1050
- summary: z3.string()
1051
- });
1052
- function toImportedMessage(message) {
1053
- const meta = {};
1054
- if (message.author) meta.author = message.author;
1055
- const { replied: _replied, ...restMeta } = message.meta ?? {};
1056
- Object.assign(meta, restMeta);
1057
- return {
1058
- messageId: message.id,
1059
- role: message.role,
1060
- text: message.text,
1061
- ...Object.keys(meta).length > 0 ? { meta } : {},
1062
- createdAtMs: message.createdAtMs
1063
- };
1064
- }
1065
- var legacyThreadStateSnapshotSchema = z3.object({
1066
- conversation: z3.object({
1067
- compactions: z3.array(legacyCompactionSchema).optional(),
1068
- messages: z3.array(legacyVisibleMessageSchema).optional(),
1069
- stats: z3.object({
1070
- compactedMessageCount: z3.number().finite().optional(),
1071
- updatedAtMs: z3.number().finite().optional()
1072
- }).passthrough().optional()
1073
- }).passthrough().optional()
1074
- });
1075
- async function loadThreadStateSnapshot(conversationId) {
1076
- const stateAdapter = getStateAdapter();
1077
- await stateAdapter.connect();
1078
- const raw = await stateAdapter.get(`thread-state:${conversationId}`);
1079
- if (!raw) {
1080
- return { compactions: [], messages: [] };
1081
- }
1082
- const conversation = legacyThreadStateSnapshotSchema.parse(raw).conversation;
1083
- return {
1084
- compactions: conversation?.compactions ?? [],
1085
- messages: conversation?.messages ?? [],
1086
- ...conversation?.stats?.compactedMessageCount !== void 0 ? { compactedMessageCount: conversation.stats.compactedMessageCount } : {},
1087
- ...conversation?.stats?.updatedAtMs !== void 0 ? { lastActivityAtMs: conversation.stats.updatedAtMs } : {}
1088
- };
1089
- }
1090
- function intrinsicTimestamps(entries, visible, compactions) {
1091
- const candidates = [];
1092
- const pushMessageTs = (message) => {
1093
- const timestamp = message.timestamp;
1094
- if (typeof timestamp === "number") {
1095
- candidates.push(timestamp);
1096
- }
1097
- };
1098
- for (const entry of entries) {
1099
- if (entry.type === "pi_message") {
1100
- pushMessageTs(entry.message);
1101
- } else if (entry.type === "projection_reset") {
1102
- entry.messages.forEach(pushMessageTs);
1103
- } else if ("createdAtMs" in entry) {
1104
- candidates.push(entry.createdAtMs);
1105
- }
1106
- }
1107
- for (const message of visible) {
1108
- candidates.push(message.createdAtMs);
1109
- }
1110
- for (const compaction of compactions) {
1111
- candidates.push(compaction.createdAtMs);
1112
- }
1113
- return candidates;
1114
- }
1115
- async function importConversationFromLegacy(conversationId, deps) {
1116
- const existing = await deps.executor.db().select({ seq: juniorConversationEvents.seq }).from(juniorConversationEvents).where(eq2(juniorConversationEvents.conversationId, conversationId)).limit(1);
1117
- if (existing.length > 0) {
1118
- return { imported: false };
1119
- }
1120
- const entries = deps.sessionLogStore ? await deps.sessionLogStore.read({ conversationId }) : await readSessionLogEntries({ conversationId });
1121
- const snapshot = deps.loadVisibleMessages ? {
1122
- compactions: deps.legacyCompactions ?? [],
1123
- messages: await deps.loadVisibleMessages(conversationId)
1124
- } : await loadThreadStateSnapshot(conversationId);
1125
- const { compactedMessageCount, compactions, messages: visible } = snapshot;
1126
- if (entries.length === 0 && visible.length === 0 && compactions.length === 0) {
1127
- return { imported: false };
1128
- }
1129
- const hasAdvisor = entries.some((entry) => entry.type === "subagent_started");
1130
- const advisorMessages = hasAdvisor ? await (deps.advisorSessionStore ?? createLegacyAdvisorSessionReader()).load(conversationId) : [];
1131
- const intrinsic = intrinsicTimestamps(entries, visible, compactions);
1132
- for (const message of advisorMessages) {
1133
- const timestamp = message.timestamp;
1134
- if (typeof timestamp === "number") {
1135
- intrinsic.push(timestamp);
1136
- }
1137
- }
1138
- const fallbackCreatedAtMs = deps.conversationRecord?.createdAtMs ?? (intrinsic.length > 0 ? Math.min(...intrinsic) : void 0) ?? 0;
1139
- const lastActivityAtMs = Math.max(
1140
- fallbackCreatedAtMs,
1141
- deps.conversationRecord?.lastActivityAtMs ?? 0,
1142
- deps.legacyLastActivityAtMs ?? 0,
1143
- intrinsic.length > 0 ? Math.max(...intrinsic) : 0
1144
- );
1145
- const converted = convertLegacySessionLog({
1146
- conversationId,
1147
- entries,
1148
- fallbackCreatedAtMs,
1149
- modelId: deps.modelId
1150
- });
1151
- if (compactions.length > 0) {
1152
- const storedCount = compactions.reduce(
1153
- (count, compaction) => count + compaction.coveredMessageIds.length,
1154
- 0
1155
- );
1156
- const statsCount = typeof compactedMessageCount === "number" && Number.isFinite(compactedMessageCount) && compactedMessageCount > 0 ? Math.floor(compactedMessageCount) : 0;
1157
- const totalCount = Math.max(storedCount, statsCount);
1158
- const compacted = compactions.map((compaction, index) => ({
1159
- id: compaction.id,
1160
- summary: compaction.summary,
1161
- createdAtMs: compaction.createdAtMs,
1162
- coveredMessageCount: compaction.coveredMessageIds.length + (index === 0 ? totalCount - storedCount : 0)
1163
- }));
1164
- converted.events.push({
1165
- seq: converted.events.length,
1166
- historyVersion: converted.events.at(-1)?.historyVersion ?? 0,
1167
- data: {
1168
- type: "messages_summarized",
1169
- historyFromSeq: 0,
1170
- compactions: compacted
1171
- },
1172
- createdAtMs: compactions.at(-1)?.createdAtMs ?? fallbackCreatedAtMs
1173
- });
1174
- }
1175
- let child;
1176
- if (converted.advisorChildConversationId) {
1177
- child = {
1178
- conversationId: converted.advisorChildConversationId,
1179
- events: convertAdvisorMessages(advisorMessages, fallbackCreatedAtMs)
1180
- };
1181
- }
1182
- const messages = visible.map((message) => ({
1183
- ...toImportedMessage(message),
1184
- ...message.meta?.replied ? { repliedAtMs: message.createdAtMs } : {}
1185
- }));
1186
- const imported = await writeLegacyImport(deps.executor, {
1187
- conversationId,
1188
- fallbackCreatedAtMs,
1189
- lastActivityAtMs,
1190
- ...messages.length > 0 ? { messages } : {},
1191
- events: converted.events,
1192
- ...child ? { child } : {}
1193
- });
1194
- return { imported };
1195
- }
1196
-
1197
- // src/cli/upgrade/migrations/conversations-history-sql.ts
1198
- var HISTORY_BACKFILL_PAGE_SIZE = 500;
1199
- async function migrateConversationHistoryToSql(context, options = {}) {
1200
- const source = createStateConversationStore(context.stateAdapter);
1201
- const chatConfig = getChatConfig();
1202
- let executor = options.executor;
1203
- let closeExecutor;
1204
- if (!executor) {
1205
- const { sql: sql2 } = chatConfig;
1206
- executor = createJuniorSqlExecutor({
1207
- connectionString: sql2.databaseUrl,
1208
- driver: sql2.driver
1209
- });
1210
- closeExecutor = () => executor.close();
1211
- }
1212
- const pageSize = Math.max(
1213
- 1,
1214
- Math.floor(options.batchSize ?? HISTORY_BACKFILL_PAGE_SIZE)
1215
- );
1216
- try {
1217
- let migrated = 0;
1218
- let existing = 0;
1219
- let scanned = 0;
1220
- let offset = 0;
1221
- while (true) {
1222
- const conversations = await source.listByActivity({
1223
- limit: pageSize,
1224
- offset
1225
- });
1226
- if (conversations.length === 0) {
1227
- break;
1228
- }
1229
- for (const conversation of conversations) {
1230
- const result = await importConversationFromLegacy(
1231
- conversation.conversationId,
1232
- {
1233
- executor,
1234
- modelId: standardModelId(chatConfig.bot),
1235
- conversationRecord: conversation,
1236
- ...options.sessionLogStore ? { sessionLogStore: options.sessionLogStore } : {},
1237
- ...options.advisorSessionStore ? { advisorSessionStore: options.advisorSessionStore } : {},
1238
- ...options.loadVisibleMessages ? { loadVisibleMessages: options.loadVisibleMessages } : {}
1239
- }
1240
- );
1241
- if (result.imported) {
1242
- migrated += 1;
1243
- } else {
1244
- existing += 1;
1245
- }
1246
- }
1247
- scanned += conversations.length;
1248
- offset += conversations.length;
1249
- }
1250
- return {
1251
- existing,
1252
- migrated,
1253
- missing: 0,
1254
- scanned
1255
- };
1256
- } finally {
1257
- await closeExecutor?.();
1258
- }
1259
- }
1260
- var sqlConversationHistoryMigration = {
1261
- name: "backfill-conversation-events-sql",
1262
- run: migrateConversationHistoryToSql
1263
- };
1264
-
1265
- // src/cli/upgrade/migrations/upgrade-plugins.ts
1266
- function unique(values) {
1267
- return [...new Set(values)];
1268
- }
1269
- function baseCatalogConfig(context) {
1270
- return context.pluginCatalogConfig ?? (context.pluginSet ? pluginCatalogConfigFromPluginSet(context.pluginSet) : pluginCatalogConfigFromEnv());
1271
- }
1272
- function inlinePluginName(definition) {
1273
- return definition.manifest.name;
1274
- }
1275
- function mergeInlineManifests(left, right) {
1276
- const merged = /* @__PURE__ */ new Map();
1277
- for (const definition of [...left ?? [], ...right ?? []]) {
1278
- merged.set(inlinePluginName(definition), definition);
1279
- }
1280
- return merged.size > 0 ? [...merged.values()] : void 0;
1281
- }
1282
- function mergeCatalogConfig(base, added) {
1283
- if (!base) {
1284
- return added;
1285
- }
1286
- if (!added) {
1287
- return base;
1288
- }
1289
- const inlineManifests = mergeInlineManifests(
1290
- base.inlineManifests,
1291
- added.inlineManifests
1292
- );
1293
- const packages = unique([
1294
- ...base.packages ?? [],
1295
- ...added.packages ?? []
1296
- ]);
1297
- const manifests = base.manifests || added.manifests ? { ...base.manifests, ...added.manifests } : void 0;
1298
- return {
1299
- ...inlineManifests ? { inlineManifests } : {},
1300
- ...packages.length > 0 ? { packages } : {},
1301
- ...manifests ? { manifests } : {}
1302
- };
1303
- }
1304
- function packageNamesFromContext(context, catalog) {
1305
- return unique([
1306
- ...context.pluginSet?.packageNames ?? [],
1307
- ...catalog?.packages ?? []
1308
- ]);
1309
- }
1310
- async function resolveUpgradePlugins(context) {
1311
- const catalog = baseCatalogConfig(context);
1312
- const packageNames = packageNamesFromContext(context, catalog);
1313
- const registrations = context.pluginSet?.registrations ?? [];
1314
- const manifests = context.pluginSet?.manifests || catalog?.manifests ? {
1315
- ...catalog?.manifests,
1316
- ...context.pluginSet?.manifests
1317
- } : void 0;
1318
- const pluginSet = packageNames.length > 0 || registrations.length > 0 || context.pluginSet ? defineJuniorPlugins(
1319
- [...packageNames, ...registrations],
1320
- manifests ? { manifests } : {}
1321
- ) : void 0;
1322
- return {
1323
- pluginCatalogConfig: mergeCatalogConfig(
1324
- catalog,
1325
- pluginCatalogConfigFromPluginSet(pluginSet)
1326
- ),
1327
- ...pluginSet ? { pluginSet } : {}
1328
- };
1329
- }
1330
-
1331
- // src/cli/upgrade/migrations/plugin-storage.ts
1332
- function emptyResult() {
1333
- return {
1334
- existing: 0,
1335
- migrated: 0,
1336
- missing: 0,
1337
- scanned: 0
1338
- };
1339
- }
1340
- function addResult(left, right) {
1341
- return {
1342
- existing: left.existing + right.existing,
1343
- migrated: left.migrated + right.migrated,
1344
- missing: left.missing + right.missing,
1345
- scanned: left.scanned + right.scanned,
1346
- ...left.skipped !== void 0 || right.skipped !== void 0 ? { skipped: (left.skipped ?? 0) + (right.skipped ?? 0) } : {}
1347
- };
1348
- }
1349
- function dbForPlugin(context, sqlUrlDb) {
1350
- return context.db ?? sqlUrlDb ?? getDb();
1351
- }
1352
- async function runPluginStorageMigrations(context) {
1353
- const { pluginCatalogConfig, pluginSet } = await resolveUpgradePlugins(context);
1354
- if (!pluginSet) {
1355
- return emptyResult();
1356
- }
1357
- const previousConfig = pluginCatalogRuntime.setConfig(pluginCatalogConfig);
1358
- const sql2 = getChatConfig().sql;
1359
- const ownedExecutor = context.db ? void 0 : createJuniorSqlExecutor({
1360
- connectionString: sql2.databaseUrl,
1361
- driver: sql2.driver
1362
- });
1363
- const sqlUrlDb = ownedExecutor ? ownedExecutor.db() : void 0;
1364
- try {
1365
- let result = emptyResult();
1366
- const plugins = pluginRuntimeRegistrationsFromPluginSet(pluginSet).filter((plugin) => plugin.hooks?.migrateStorage).sort(
1367
- (left, right) => left.manifest.name.localeCompare(right.manifest.name)
1368
- );
1369
- for (const plugin of plugins) {
1370
- const pluginName = plugin.manifest.name;
1371
- const hook = plugin.hooks?.migrateStorage;
1372
- if (!hook) {
1373
- continue;
1374
- }
1375
- const db = dbForPlugin(context, sqlUrlDb);
1376
- const pluginResult = await hook({
1377
- db,
1378
- log: createPluginLogger(pluginName),
1379
- plugin: { name: pluginName },
1380
- state: createPluginState(pluginName, context.stateAdapter)
1381
- });
1382
- if (pluginResult) {
1383
- result = addResult(result, pluginResult);
1384
- }
1385
- }
1386
- return result;
1387
- } finally {
1388
- pluginCatalogRuntime.setConfig(previousConfig);
1389
- await ownedExecutor?.close();
1390
- }
1391
- }
1392
- var pluginStorageMigration = {
1393
- name: "run-plugin-storage-migrations",
1394
- run: runPluginStorageMigrations
1395
- };
1396
-
1397
- // src/chat/plugins/migrations.ts
1398
- import { createHash } from "crypto";
1399
- import { readMigrationFiles as readMigrationFiles2 } from "drizzle-orm/migrator";
1400
- var LEGACY_SCHEDULER_BASELINE_HASH = "d1d2f712181dd3a0557808f0fc67fd0722691d25f4c8cfb816b77c71d19e1e42";
1401
- function migrationTable(pluginName) {
1402
- const label = pluginName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32);
1403
- const hash = createHash("sha256").update(pluginName).digest("hex").slice(0, 8);
1404
- return `__drizzle_${label}_${hash}`;
1405
- }
1406
- async function appliedMigrationTime(executor, table) {
1407
- const [exists] = await executor.query(
1408
- `SELECT to_regclass($1)::text AS "tableName"`,
1409
- [`drizzle.${table}`]
1410
- );
1411
- if (!exists?.tableName) {
1412
- return void 0;
1413
- }
1414
- const [row] = await executor.query(
1415
- `SELECT created_at::text AS "createdAt"
1416
- FROM drizzle.${table}
1417
- ORDER BY created_at DESC
1418
- LIMIT 1`
1419
- );
1420
- return row?.createdAt === null || row?.createdAt === void 0 ? void 0 : Number(row.createdAt);
1421
- }
1422
- async function legacyMigrationHashes(executor, pluginName) {
1423
- const [exists] = await executor.query(
1424
- `SELECT to_regclass('public.junior_schema_migrations')::text AS "tableName"`
1425
- );
1426
- if (!exists?.tableName) {
1427
- return /* @__PURE__ */ new Set();
1428
- }
1429
- const rows = await executor.query(
1430
- `SELECT checksum
1431
- FROM junior_schema_migrations
1432
- WHERE id LIKE $1
1433
- ORDER BY id ASC`,
1434
- [`plugin:${pluginName}/%`]
1435
- );
1436
- return new Set(rows.map((row) => row.checksum));
1437
- }
1438
- function adoptedMigration(migrations, legacyHashes, pluginName) {
1439
- let adopted;
1440
- for (const migration of migrations) {
1441
- if (!legacyHashes.has(migration.hash)) {
1442
- break;
1443
- }
1444
- adopted = migration;
1445
- }
1446
- if (!adopted && pluginName === "scheduler" && legacyHashes.has(LEGACY_SCHEDULER_BASELINE_HASH)) {
1447
- return migrations[0];
1448
- }
1449
- return adopted;
1450
- }
1451
- async function adoptLegacyMigrationState2(args) {
1452
- const legacyHashes = await legacyMigrationHashes(
1453
- args.executor,
1454
- args.pluginName
1455
- );
1456
- const migration = adoptedMigration(
1457
- args.migrations,
1458
- legacyHashes,
1459
- args.pluginName
1460
- );
1461
- if (!migration) {
1462
- return void 0;
1463
- }
1464
- await args.executor.transaction(async () => {
1465
- await args.executor.execute("CREATE SCHEMA IF NOT EXISTS drizzle");
1466
- await args.executor.execute(`
1467
- CREATE TABLE IF NOT EXISTS drizzle.${args.table} (
1468
- id SERIAL PRIMARY KEY,
1469
- hash TEXT NOT NULL,
1470
- created_at BIGINT
1471
- )
1472
- `);
1473
- await args.executor.execute(
1474
- `INSERT INTO drizzle.${args.table} (hash, created_at) VALUES ($1, $2)`,
1475
- [migration.hash, migration.folderMillis]
1476
- );
1477
- });
1478
- return migration.folderMillis;
1479
- }
1480
- function appliedCount(migrations, createdAt) {
1481
- return createdAt === void 0 ? 0 : migrations.filter((migration) => migration.folderMillis <= createdAt).length;
1482
- }
1483
- async function migratePluginSchemas(executor, roots) {
1484
- const result = {
1485
- existing: 0,
1486
- migrated: 0,
1487
- scanned: 0
1488
- };
1489
- const orderedRoots = [...roots].sort(
1490
- (left, right) => left.pluginName.localeCompare(right.pluginName)
1491
- );
1492
- for (const root of orderedRoots) {
1493
- const migrations = readMigrationFiles2({ migrationsFolder: root.dir });
1494
- const table = migrationTable(root.pluginName);
1495
- await executor.withMigrationLock(table, async () => {
1496
- const currentTime = await appliedMigrationTime(executor, table) ?? await adoptLegacyMigrationState2({
1497
- executor,
1498
- migrations,
1499
- pluginName: root.pluginName,
1500
- table
1501
- });
1502
- const existing = appliedCount(migrations, currentTime);
1503
- await executor.migrate({
1504
- migrationsFolder: root.dir,
1505
- migrationsTable: table
1506
- });
1507
- result.scanned += migrations.length;
1508
- result.existing += existing;
1509
- result.migrated += migrations.length - existing;
1510
- });
1511
- }
1512
- return result;
1513
- }
1514
-
1515
- // src/cli/upgrade/migrations/plugin-sql.ts
1516
- async function migratePluginsToSql(context) {
1517
- const { sql: sql2 } = getChatConfig();
1518
- const { pluginCatalogConfig } = await resolveUpgradePlugins(context);
1519
- const previousConfig = pluginCatalogRuntime.setConfig(pluginCatalogConfig);
1520
- const executor = createJuniorSqlExecutor({
1521
- connectionString: sql2.databaseUrl,
1522
- driver: sql2.driver
1523
- });
1524
- try {
1525
- const result = await migratePluginSchemas(
1526
- executor,
1527
- pluginCatalogRuntime.getMigrationRoots()
1528
- );
1529
- return {
1530
- existing: result.existing,
1531
- migrated: result.migrated,
1532
- missing: 0,
1533
- scanned: result.scanned
1534
- };
1535
- } finally {
1536
- pluginCatalogRuntime.setConfig(previousConfig);
1537
- await executor.close();
1538
- }
1539
- }
1540
- var sqlPluginMigration = {
1541
- name: "migrate-plugin-sql",
1542
- run: migratePluginsToSql
1543
- };
1544
-
1545
- // src/cli/upgrade/migrations/redis-conversation-state.ts
1546
- var CONVERSATION_PREFIX = "junior:conversation";
1547
- var CONVERSATION_SCHEMA_VERSION = 1;
1548
- var CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH = 1e4;
1549
- var CONVERSATION_BY_ACTIVITY_INDEX_KEY = `${CONVERSATION_PREFIX}:by-activity`;
1550
- var CONVERSATION_ACTIVE_INDEX_KEY = `${CONVERSATION_PREFIX}:active`;
1551
- var LEGACY_CONVERSATION_WORK_PREFIX = "junior:conversation-work";
1552
- var LEGACY_CONVERSATION_WORK_SCHEMA_VERSION = 1;
1553
- var LEGACY_CONVERSATION_WORK_INDEX_KEY = `${LEGACY_CONVERSATION_WORK_PREFIX}:index`;
1554
- var AGENT_TURN_SESSION_PREFIX2 = "junior:agent_turn_session";
1555
- var AGENT_TURN_SESSION_INDEX_KEY = `${AGENT_TURN_SESSION_PREFIX2}:index`;
1556
- var THREAD_STATE_PREFIX = "thread-state";
1557
- function conversationKey(conversationId) {
1558
- return `${CONVERSATION_PREFIX}:${conversationId}`;
1559
- }
1560
- function legacyConversationWorkKey(conversationId) {
1561
- return `${LEGACY_CONVERSATION_WORK_PREFIX}:state:${conversationId}`;
1562
- }
1563
- function threadStateKey(conversationId) {
1564
- return `${THREAD_STATE_PREFIX}:${conversationId}`;
1565
- }
1566
- function uniqueStrings(values) {
1567
- return [...new Set(values)];
1568
- }
1569
- function uniqueStringValues(value) {
1570
- if (!Array.isArray(value)) {
1571
- return [];
1572
- }
1573
- return uniqueStrings(
1574
- value.map((value2) => typeof value2 === "string" ? value2 : void 0).filter((value2) => Boolean(value2))
1575
- );
1576
- }
1577
- function normalizeSource(value) {
1578
- if (value === "api" || value === "internal" || value === "plugin" || value === "scheduler" || value === "slack") {
1579
- return value;
1580
- }
1581
- return void 0;
1582
- }
1583
- function normalizeMetadata(value) {
1584
- if (!isRecord(value)) {
1585
- return void 0;
1586
- }
1587
- return value;
1588
- }
1589
- function normalizeInput(value) {
1590
- if (!isRecord(value)) {
1591
- return void 0;
1592
- }
1593
- const text = toOptionalString(value.text);
1594
- if (!text) {
1595
- return void 0;
1596
- }
1597
- return {
1598
- text,
1599
- authorId: toOptionalString(value.authorId),
1600
- attachments: Array.isArray(value.attachments) ? [...value.attachments] : void 0,
1601
- metadata: normalizeMetadata(value.metadata)
1602
- };
1603
- }
1604
- function normalizeMessage(value) {
1605
- if (!isRecord(value)) {
1606
- return void 0;
1607
- }
1608
- const conversationId = toOptionalString(value.conversationId);
1609
- const inboundMessageId = toOptionalString(value.inboundMessageId);
1610
- const source = normalizeSource(value.source);
1611
- const destination = parseDestination(value.destination);
1612
- const createdAtMs = toOptionalNumber(value.createdAtMs);
1613
- const receivedAtMs = toOptionalNumber(value.receivedAtMs);
1614
- const input = normalizeInput(value.input);
1615
- if (!conversationId || !destination || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
1616
- return void 0;
1617
- }
1618
- return {
1619
- conversationId,
1620
- destination,
1621
- inboundMessageId,
1622
- source,
1623
- createdAtMs,
1624
- receivedAtMs,
1625
- input,
1626
- injectedAtMs: toOptionalNumber(value.injectedAtMs)
1627
- };
1628
- }
1629
- function normalizeLegacyLease(value) {
1630
- if (!isRecord(value)) {
1631
- return void 0;
1632
- }
1633
- const token = toOptionalString(value.leaseToken);
1634
- const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
1635
- const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
1636
- const expiresAtMs = toOptionalNumber(value.leaseExpiresAtMs);
1637
- if (!token || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof expiresAtMs !== "number") {
1638
- return void 0;
1639
- }
1640
- return {
1641
- token,
1642
- acquiredAtMs,
1643
- lastCheckInAtMs,
1644
- expiresAtMs
1645
- };
1646
- }
1647
- function compareMessages(left, right) {
1648
- return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
1649
- }
1650
- function normalizeLegacyConversation(conversationId, value) {
1651
- if (!isRecord(value) || value.schemaVersion !== LEGACY_CONVERSATION_WORK_SCHEMA_VERSION) {
1652
- return void 0;
1653
- }
1654
- const storedConversationId = toOptionalString(value.conversationId);
1655
- const destination = parseDestination(value.destination);
1656
- const updatedAtMs = toOptionalNumber(value.updatedAtMs);
1657
- if (storedConversationId !== conversationId || !destination || typeof updatedAtMs !== "number") {
1658
- return void 0;
1659
- }
1660
- const normalizedMessages = Array.isArray(value.messages) ? value.messages.map(normalizeMessage).filter((message) => Boolean(message)) : [];
1661
- if (normalizedMessages.some(
1662
- (message) => message.conversationId === conversationId && !sameDestination(message.destination, destination)
1663
- )) {
1664
- return void 0;
1665
- }
1666
- const messages = normalizedMessages.filter((message) => message.conversationId === conversationId).sort(compareMessages);
1667
- const pendingMessages = messages.filter(
1668
- (message) => message.injectedAtMs === void 0
1669
- );
1670
- const lease = normalizeLegacyLease(value.lease);
1671
- const needsRun = value.needsRun === true || pendingMessages.length > 0;
1672
- const status = lease ? value.needsRun === true ? "awaiting_resume" : "running" : needsRun ? "pending" : "idle";
1673
- const messageTimes = messages.flatMap((message) => [
1674
- message.createdAtMs,
1675
- message.receivedAtMs
1676
- ]);
1677
- const createdAtMs = messageTimes.length > 0 ? Math.min(...messageTimes) : updatedAtMs;
1678
- const lastActivityAtMs = messageTimes.length > 0 ? Math.max(...messageTimes) : updatedAtMs;
1679
- return {
1680
- schemaVersion: CONVERSATION_SCHEMA_VERSION,
1681
- conversationId,
1682
- createdAtMs,
1683
- destination,
1684
- lastActivityAtMs,
1685
- source: messages[0]?.source,
1686
- updatedAtMs,
1687
- execution: {
1688
- status,
1689
- inboundMessageIds: uniqueStrings(
1690
- messages.map((message) => message.inboundMessageId)
1691
- ),
1692
- pendingCount: pendingMessages.length,
1693
- pendingMessages,
1694
- ...lease ? { lease } : {},
1695
- lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
1696
- updatedAtMs
1697
- }
1698
- };
1699
- }
1700
- function mergeLegacyConversation(existing, legacy) {
1701
- if (existing.destination && legacy.destination && !sameDestination(existing.destination, legacy.destination)) {
1702
- throw new Error(
1703
- `Legacy conversation work destination does not match conversation ${existing.conversationId}`
1704
- );
1705
- }
1706
- const knownInboundIds = new Set(existing.execution.inboundMessageIds);
1707
- const pendingMessages = [
1708
- ...existing.execution.pendingMessages,
1709
- ...legacy.execution.pendingMessages.filter(
1710
- (message) => !knownInboundIds.has(message.inboundMessageId)
1711
- )
1712
- ].sort(compareMessages);
1713
- const legacyIsRunnable = legacy.execution.status !== "idle";
1714
- const existingIsIdle = existing.execution.status === "idle";
1715
- const legacyLease = existingIsIdle && legacy.execution.lease ? { lease: legacy.execution.lease } : {};
1716
- const legacyRunId = existingIsIdle && legacy.execution.runId ? { runId: legacy.execution.runId } : {};
1717
- const legacyCheckpoint = existing.execution.lastCheckpointAtMs === void 0 && legacy.execution.lastCheckpointAtMs !== void 0 ? { lastCheckpointAtMs: legacy.execution.lastCheckpointAtMs } : {};
1718
- const legacyEnqueue = existing.execution.lastEnqueuedAtMs === void 0 && legacy.execution.lastEnqueuedAtMs !== void 0 ? { lastEnqueuedAtMs: legacy.execution.lastEnqueuedAtMs } : {};
1719
- const executionUpdatedAtMs = Math.max(
1720
- existing.execution.updatedAtMs ?? existing.updatedAtMs,
1721
- legacy.execution.updatedAtMs ?? legacy.updatedAtMs
1722
- );
1723
- const status = existingIsIdle && legacyIsRunnable ? legacy.execution.lease ? legacy.execution.status : "pending" : pendingMessages.length > 0 && existingIsIdle ? "pending" : existing.execution.status;
1724
- return {
1725
- ...existing,
1726
- destination: existing.destination ?? legacy.destination,
1727
- source: existing.source ?? legacy.source,
1728
- createdAtMs: Math.min(existing.createdAtMs, legacy.createdAtMs),
1729
- lastActivityAtMs: Math.max(
1730
- existing.lastActivityAtMs,
1731
- legacy.lastActivityAtMs
1732
- ),
1733
- updatedAtMs: Math.max(existing.updatedAtMs, legacy.updatedAtMs),
1734
- execution: {
1735
- ...existing.execution,
1736
- ...legacyLease,
1737
- ...legacyRunId,
1738
- ...legacyCheckpoint,
1739
- ...legacyEnqueue,
1740
- status,
1741
- inboundMessageIds: uniqueStrings([
1742
- ...existing.execution.inboundMessageIds,
1743
- ...legacy.execution.inboundMessageIds
1744
- ]),
1745
- pendingCount: pendingMessages.length,
1746
- pendingMessages,
1747
- updatedAtMs: executionUpdatedAtMs
1748
- }
1749
- };
1750
- }
1751
- function compareIndexDescending(left, right) {
1752
- return right.score - left.score || right.conversationId.localeCompare(left.conversationId);
1753
- }
1754
- function compareIndexAscending(left, right) {
1755
- return left.score - right.score || left.conversationId.localeCompare(right.conversationId);
1756
- }
1757
- function normalizeIndexEntry(value) {
1758
- if (!isRecord(value)) {
1759
- return void 0;
1760
- }
1761
- const conversationId = toOptionalString(value.conversationId);
1762
- const score = toOptionalNumber(value.score);
1763
- if (!conversationId || typeof score !== "number") {
1764
- return void 0;
1765
- }
1766
- return { conversationId, score };
1767
- }
1768
- function uniqueIndexEntries(value) {
1769
- if (!Array.isArray(value)) {
1770
- return [];
1771
- }
1772
- const entries = /* @__PURE__ */ new Map();
1773
- for (const item of value) {
1774
- const entry = normalizeIndexEntry(item);
1775
- if (!entry) {
1776
- continue;
1777
- }
1778
- const existing = entries.get(entry.conversationId);
1779
- if (!existing || entry.score > existing.score) {
1780
- entries.set(entry.conversationId, entry);
1781
- }
1782
- }
1783
- return [...entries.values()];
1784
- }
1785
- function normalizeAwaitingContinuationSummary(value) {
1786
- if (!isRecord(value)) {
1787
- return void 0;
1788
- }
1789
- const conversationId = toOptionalString(value.conversationId);
1790
- const sessionId = toOptionalString(value.sessionId);
1791
- const state = value.state;
1792
- const resumeReason = value.resumeReason;
1793
- const destination = parseDestination(value.destination);
1794
- const updatedAtMs = toOptionalNumber(value.updatedAtMs);
1795
- if (!conversationId || !sessionId || state !== "awaiting_resume" || resumeReason !== "timeout" && resumeReason !== "yield" || !destination || typeof updatedAtMs !== "number") {
1796
- return void 0;
1797
- }
1798
- return {
1799
- conversationId,
1800
- destination,
1801
- resumeReason,
1802
- sessionId,
1803
- state,
1804
- updatedAtMs
1805
- };
1806
- }
1807
- function uniqueAwaitingContinuationSummaries(values) {
1808
- const summaries = /* @__PURE__ */ new Map();
1809
- for (const value of [...values].reverse()) {
1810
- const summary = normalizeAwaitingContinuationSummary(value);
1811
- if (!summary) {
1812
- continue;
1813
- }
1814
- const key3 = `${summary.conversationId}:${summary.sessionId}`;
1815
- if (!summaries.has(key3)) {
1816
- summaries.set(key3, summary);
1817
- }
1818
- }
1819
- return [...summaries.values()];
1820
- }
1821
- function redisIndexKey(indexKey) {
1822
- const prefix = getChatConfig().state.keyPrefix;
1823
- return [...prefix ? [prefix] : [], indexKey].join(":");
1824
- }
1825
- async function upsertEmulatedIndexEntry(args) {
1826
- const existing = uniqueIndexEntries(
1827
- await args.stateAdapter.get(args.indexKey)
1828
- );
1829
- const next = [
1830
- ...existing.filter((entry) => entry.conversationId !== args.conversationId),
1831
- { conversationId: args.conversationId, score: args.score }
1832
- ];
1833
- const retained = args.indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY ? next.sort(compareIndexDescending).slice(0, CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH) : next.sort(compareIndexAscending);
1834
- await args.stateAdapter.set(
1835
- args.indexKey,
1836
- retained,
1837
- JUNIOR_THREAD_STATE_TTL_MS
1838
- );
1839
- }
1840
- async function removeEmulatedIndexEntry(args) {
1841
- const existing = uniqueIndexEntries(
1842
- await args.stateAdapter.get(args.indexKey)
1843
- );
1844
- const next = existing.filter(
1845
- (entry) => entry.conversationId !== args.conversationId
1846
- );
1847
- if (next.length === existing.length) {
1848
- return;
1849
- }
1850
- await args.stateAdapter.set(args.indexKey, next, JUNIOR_THREAD_STATE_TTL_MS);
1851
- }
1852
- async function upsertRedisIndexEntry(args) {
1853
- const key3 = redisIndexKey(args.indexKey);
1854
- if (args.indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
1855
- const upsertBoundedActivityScript = `
1856
- redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
1857
- redis.call("PEXPIRE", KEYS[1], ARGV[3])
1858
- local extra = redis.call("ZCARD", KEYS[1]) - tonumber(ARGV[4])
1859
- if extra > 0 then
1860
- redis.call("ZREMRANGEBYRANK", KEYS[1], 0, extra - 1)
1861
- end
1862
- return 1
1863
- `;
1864
- await args.client.sendCommand([
1865
- "EVAL",
1866
- upsertBoundedActivityScript,
1867
- "1",
1868
- key3,
1869
- String(args.score),
1870
- args.conversationId,
1871
- String(JUNIOR_THREAD_STATE_TTL_MS),
1872
- String(CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH)
1873
- ]);
1874
- return;
1875
- }
1876
- await args.client.sendCommand([
1877
- "ZADD",
1878
- key3,
1879
- String(args.score),
1880
- args.conversationId
1881
- ]);
1882
- await args.client.sendCommand([
1883
- "PEXPIRE",
1884
- key3,
1885
- String(JUNIOR_THREAD_STATE_TTL_MS)
1886
- ]);
1887
- }
1888
- async function removeRedisIndexEntry(args) {
1889
- await args.client.sendCommand([
1890
- "ZREM",
1891
- redisIndexKey(args.indexKey),
1892
- args.conversationId
1893
- ]);
1894
- }
1895
- async function upsertConversationIndexes(args) {
1896
- const redisClient = args.redisStateAdapter?.getClient();
1897
- const upsert = redisClient ? (indexKey, score) => upsertRedisIndexEntry({
1898
- client: redisClient,
1899
- conversationId: args.conversation.conversationId,
1900
- indexKey,
1901
- score
1902
- }) : (indexKey, score) => upsertEmulatedIndexEntry({
1903
- stateAdapter: args.stateAdapter,
1904
- conversationId: args.conversation.conversationId,
1905
- indexKey,
1906
- score
1907
- });
1908
- const remove = redisClient ? (indexKey) => removeRedisIndexEntry({
1909
- client: redisClient,
1910
- conversationId: args.conversation.conversationId,
1911
- indexKey
1912
- }) : (indexKey) => removeEmulatedIndexEntry({
1913
- stateAdapter: args.stateAdapter,
1914
- conversationId: args.conversation.conversationId,
1915
- indexKey
1916
- });
1917
- await upsert(
1918
- CONVERSATION_BY_ACTIVITY_INDEX_KEY,
1919
- args.conversation.lastActivityAtMs
1920
- );
1921
- if (args.conversation.execution.status === "idle") {
1922
- await remove(CONVERSATION_ACTIVE_INDEX_KEY);
1923
- return;
1924
- }
1925
- await upsert(
1926
- CONVERSATION_ACTIVE_INDEX_KEY,
1927
- args.conversation.execution.updatedAtMs ?? args.conversation.updatedAtMs
1928
- );
1929
- }
1930
- async function removeLegacyIndexEntry(args) {
1931
- const existing = uniqueStringValues(
1932
- await args.stateAdapter.get(LEGACY_CONVERSATION_WORK_INDEX_KEY)
1933
- );
1934
- const next = existing.filter((id) => id !== args.conversationId);
1935
- if (next.length === existing.length) {
1936
- return;
1937
- }
1938
- if (next.length === 0) {
1939
- await args.stateAdapter.delete(LEGACY_CONVERSATION_WORK_INDEX_KEY);
1940
- return;
1941
- }
1942
- await args.stateAdapter.set(
1943
- LEGACY_CONVERSATION_WORK_INDEX_KEY,
1944
- next,
1945
- JUNIOR_THREAD_STATE_TTL_MS
1946
- );
1947
- }
1948
- async function migrateLegacyConversationWorkRedisState(context) {
1949
- const legacyIds = uniqueStringValues(
1950
- await context.stateAdapter.get(LEGACY_CONVERSATION_WORK_INDEX_KEY)
1951
- );
1952
- const result = {
1953
- existing: 0,
1954
- migrated: 0,
1955
- missing: 0,
1956
- scanned: legacyIds.length
1957
- };
1958
- for (const conversationId of legacyIds) {
1959
- const legacyKey = legacyConversationWorkKey(conversationId);
1960
- const raw = await context.stateAdapter.get(legacyKey);
1961
- if (raw == null) {
1962
- result.missing += 1;
1963
- await removeLegacyIndexEntry({
1964
- conversationId,
1965
- stateAdapter: context.stateAdapter
1966
- });
1967
- continue;
1968
- }
1969
- const conversation = normalizeLegacyConversation(conversationId, raw);
1970
- if (!conversation) {
1971
- throw new Error(
1972
- `Legacy conversation work state is invalid for ${conversationId}`
1973
- );
1974
- }
1975
- const existingConversation = await getConversation({
1976
- conversationId,
1977
- state: context.stateAdapter
1978
- });
1979
- if (existingConversation) {
1980
- const mergedConversation = mergeLegacyConversation(
1981
- existingConversation,
1982
- conversation
1983
- );
1984
- await context.stateAdapter.set(
1985
- conversationKey(conversationId),
1986
- mergedConversation,
1987
- JUNIOR_THREAD_STATE_TTL_MS
1988
- );
1989
- await upsertConversationIndexes({
1990
- conversation: mergedConversation,
1991
- redisStateAdapter: context.redisStateAdapter,
1992
- stateAdapter: context.stateAdapter
1993
- });
1994
- result.existing += 1;
1995
- await context.stateAdapter.delete(legacyKey);
1996
- await removeLegacyIndexEntry({
1997
- conversationId,
1998
- stateAdapter: context.stateAdapter
1999
- });
2000
- continue;
2001
- }
2002
- await context.stateAdapter.set(
2003
- conversationKey(conversationId),
2004
- conversation,
2005
- JUNIOR_THREAD_STATE_TTL_MS
2006
- );
2007
- await upsertConversationIndexes({
2008
- conversation,
2009
- redisStateAdapter: context.redisStateAdapter,
2010
- stateAdapter: context.stateAdapter
2011
- });
2012
- await context.stateAdapter.delete(legacyKey);
2013
- await removeLegacyIndexEntry({
2014
- conversationId,
2015
- stateAdapter: context.stateAdapter
2016
- });
2017
- result.migrated += 1;
2018
- }
2019
- return result;
2020
- }
2021
- async function isActiveContinuationSummary(context, summary) {
2022
- const rawState = await context.stateAdapter.get(
2023
- threadStateKey(summary.conversationId)
2024
- ) ?? {};
2025
- const conversation = coerceThreadConversationState(rawState);
2026
- return conversation.processing.activeTurnId === summary.sessionId;
2027
- }
2028
- async function seedAwaitingContinuationConversationWork(context, result) {
2029
- const summaries = uniqueAwaitingContinuationSummaries(
2030
- await context.stateAdapter.getList(AGENT_TURN_SESSION_INDEX_KEY)
2031
- );
2032
- result.scanned += summaries.length;
2033
- for (const summary of summaries) {
2034
- if (!await isActiveContinuationSummary(context, summary)) {
2035
- continue;
2036
- }
2037
- const existingConversation = await getConversation({
2038
- conversationId: summary.conversationId,
2039
- state: context.stateAdapter
2040
- });
2041
- if (existingConversation?.destination && !sameDestination(existingConversation.destination, summary.destination)) {
2042
- throw new Error(
2043
- `Awaiting continuation destination does not match conversation ${summary.conversationId}`
2044
- );
2045
- }
2046
- if (existingConversation && existingConversation.execution.status !== "idle") {
2047
- continue;
2048
- }
2049
- await requestConversationWork({
2050
- conversationId: summary.conversationId,
2051
- destination: summary.destination,
2052
- nowMs: Math.max(
2053
- summary.updatedAtMs,
2054
- existingConversation?.updatedAtMs ?? 0
2055
- ),
2056
- state: context.stateAdapter
2057
- });
2058
- if (existingConversation) {
2059
- result.existing += 1;
2060
- } else {
2061
- result.migrated += 1;
2062
- }
2063
- }
2064
- }
2065
- async function migrateRedisConversationState(context) {
2066
- const result = await migrateLegacyConversationWorkRedisState(context);
2067
- await seedAwaitingContinuationConversationWork(context, result);
2068
- return result;
2069
- }
2070
- var redisConversationStateMigration = {
2071
- // TODO(after 2026-07-01): remove after deployed installs have had a release
2072
- // window to move legacy conversation-work Redis state forward.
2073
- name: "migrate-redis-conversation-state",
2074
- run: migrateRedisConversationState
2075
- };
2076
-
2077
- // src/cli/upgrade/migrations/agent-turn-session-actor.ts
2078
- import { THREAD_STATE_TTL_MS } from "chat";
2079
- var AGENT_TURN_SESSION_PREFIX3 = "junior:agent_turn_session";
2080
- var AGENT_TURN_SESSION_INDEX_KEY2 = `${AGENT_TURN_SESSION_PREFIX3}:index`;
2081
- var AGENT_TURN_SESSION_INDEX_MAX_LENGTH = 5e3;
2082
- var REDIS_SCAN_COUNT = 500;
2083
- function conversationIndexKey(conversationId) {
2084
- return `${AGENT_TURN_SESSION_PREFIX3}:conversation:${conversationId}:index`;
2085
- }
2086
- function sessionRecordKey(conversationId, sessionId) {
2087
- return `${AGENT_TURN_SESSION_PREFIX3}:${conversationId}:${sessionId}`;
2088
- }
2089
- function logicalConversationIndexPrefix() {
2090
- const prefix = getChatConfig().state.keyPrefix;
2091
- return [
2092
- ...prefix ? [prefix] : [],
2093
- `${AGENT_TURN_SESSION_PREFIX3}:conversation:`
2094
- ].join(":");
2095
- }
2096
- function conversationIdFromRedisListKey(key3) {
2097
- const marker = `:list:${logicalConversationIndexPrefix()}`;
2098
- const markerIndex = key3.indexOf(marker);
2099
- if (markerIndex < 0 || !key3.endsWith(":index")) {
2100
- return void 0;
2101
- }
2102
- return toOptionalString(
2103
- key3.slice(markerIndex + marker.length, -":index".length)
2104
- );
2105
- }
2106
- async function discoverRedisConversationIds(redisStateAdapter) {
2107
- const client = redisStateAdapter?.getClient();
2108
- if (!client) {
2109
- return [];
2110
- }
2111
- const conversationIds = /* @__PURE__ */ new Set();
2112
- const match = `*:list:${logicalConversationIndexPrefix()}*:index`;
2113
- let cursor = "0";
2114
- do {
2115
- const reply = await client.sendCommand([
2116
- "SCAN",
2117
- cursor,
2118
- "MATCH",
2119
- match,
2120
- "COUNT",
2121
- String(REDIS_SCAN_COUNT)
2122
- ]);
2123
- if (!Array.isArray(reply) || reply.length !== 2 || typeof reply[0] !== "string" && typeof reply[0] !== "number" || !Array.isArray(reply[1])) {
2124
- throw new Error(
2125
- "Unexpected Redis SCAN response while migrating turn sessions"
2126
- );
2127
- }
2128
- cursor = String(reply[0]);
2129
- for (const key3 of reply[1]) {
2130
- if (typeof key3 !== "string") {
2131
- continue;
2132
- }
2133
- const conversationId = conversationIdFromRedisListKey(key3);
2134
- if (conversationId) {
2135
- conversationIds.add(conversationId);
2136
- }
2137
- }
2138
- } while (cursor !== "0");
2139
- return [...conversationIds];
2140
- }
2141
- function migrateRequesterToActor(value) {
2142
- if (!isRecord(value) || value.requester === void 0) {
2143
- return { changed: false, value };
2144
- }
2145
- const { requester, ...record } = value;
2146
- return {
2147
- changed: true,
2148
- value: {
2149
- ...record,
2150
- ...record.actor === void 0 ? { actor: requester } : {}
2151
- }
2152
- };
2153
- }
2154
- async function rewriteList(args) {
2155
- const values = await args.stateAdapter.getList(args.key);
2156
- const migrated = values.map(migrateRequesterToActor);
2157
- const changed = migrated.filter((entry) => entry.changed).length;
2158
- if (changed === 0) {
2159
- return { migrated: 0, values };
2160
- }
2161
- await args.stateAdapter.delete(args.key);
2162
- for (const entry of migrated) {
2163
- await args.stateAdapter.appendToList(args.key, entry.value, {
2164
- ...args.maxLength !== void 0 ? { maxLength: args.maxLength } : {},
2165
- ttlMs: THREAD_STATE_TTL_MS
2166
- });
2167
- }
2168
- return { migrated: changed, values: migrated.map((entry) => entry.value) };
2169
- }
2170
- async function migrateSessionRecord(args) {
2171
- const key3 = sessionRecordKey(args.conversationId, args.sessionId);
2172
- const existing = await args.stateAdapter.get(key3);
2173
- if (existing === void 0) {
2174
- return "missing";
2175
- }
2176
- const migrated = migrateRequesterToActor(existing);
2177
- if (!migrated.changed) {
2178
- return "existing";
2179
- }
2180
- await args.stateAdapter.set(key3, migrated.value, THREAD_STATE_TTL_MS);
2181
- return "migrated";
2182
- }
2183
- async function migrateAgentTurnSessionActor(context) {
2184
- const result = {
2185
- existing: 0,
2186
- migrated: 0,
2187
- missing: 0,
2188
- scanned: 0
2189
- };
2190
- const global = await rewriteList({
2191
- key: AGENT_TURN_SESSION_INDEX_KEY2,
2192
- maxLength: AGENT_TURN_SESSION_INDEX_MAX_LENGTH,
2193
- stateAdapter: context.stateAdapter
2194
- });
2195
- result.scanned += global.values.length;
2196
- result.migrated += global.migrated;
2197
- const conversations = /* @__PURE__ */ new Set();
2198
- const sessions = /* @__PURE__ */ new Set();
2199
- for (const conversationId of await discoverRedisConversationIds(
2200
- context.redisStateAdapter
2201
- )) {
2202
- conversations.add(conversationId);
2203
- }
2204
- for (const value of global.values) {
2205
- if (!isRecord(value)) {
2206
- continue;
2207
- }
2208
- const conversationId = toOptionalString(value.conversationId);
2209
- const sessionId = toOptionalString(value.sessionId);
2210
- if (!conversationId) {
2211
- continue;
2212
- }
2213
- conversations.add(conversationId);
2214
- if (sessionId) {
2215
- sessions.add(`${conversationId}\0${sessionId}`);
2216
- }
2217
- }
2218
- for (const conversationId of conversations) {
2219
- const conversation = await rewriteList({
2220
- key: conversationIndexKey(conversationId),
2221
- stateAdapter: context.stateAdapter
2222
- });
2223
- result.scanned += conversation.values.length;
2224
- result.migrated += conversation.migrated;
2225
- for (const value of conversation.values) {
2226
- if (!isRecord(value)) {
2227
- continue;
2228
- }
2229
- const sessionId = toOptionalString(value.sessionId);
2230
- if (sessionId) {
2231
- sessions.add(`${conversationId}\0${sessionId}`);
2232
- }
2233
- }
2234
- }
2235
- for (const session of sessions) {
2236
- const separator = session.indexOf("\0");
2237
- const conversationId = session.slice(0, separator);
2238
- const sessionId = session.slice(separator + 1);
2239
- result.scanned += 1;
2240
- const status = await migrateSessionRecord({
2241
- conversationId,
2242
- sessionId,
2243
- stateAdapter: context.stateAdapter
2244
- });
2245
- if (status === "migrated") {
2246
- result.migrated += 1;
2247
- } else if (status === "existing") {
2248
- result.existing += 1;
2249
- } else {
2250
- result.missing += 1;
2251
- }
2252
- }
2253
- return result;
2254
- }
2255
- var agentTurnSessionActorMigration = {
2256
- name: "migrate-agent-turn-session-requester-to-actor",
2257
- run: migrateAgentTurnSessionActor
2258
- };
2259
-
2260
- // src/cli/upgrade/migrations/conversation-usage.ts
2261
- var CONVERSATION_USAGE_REPAIR_BATCH_SIZE = 500;
2262
- var MAX_SAFE_INTEGER = 9007199254740991;
2263
- var MAX_FINITE_DOUBLE = "1.7976931348623157e308";
2264
- var REPAIR_LOCK = "junior:upgrade:repair-conversation-usage";
2265
- var USAGE_REPAIR_BATCH_SQL = `
2266
- WITH candidates AS MATERIALIZED (
2267
- SELECT
2268
- conversation_id,
2269
- updated_at,
2270
- execution_updated_at
2271
- FROM junior_conversations
2272
- WHERE parent_conversation_id IS NULL
2273
- AND execution_status = 'idle'
2274
- AND conversation_id > $1
2275
- ORDER BY conversation_id
2276
- LIMIT $2
2277
- ),
2278
- -- Occurrence numbers preserve intentional duplicates within one history version
2279
- -- while collapsing context copies of the same message across replacements.
2280
- message_occurrences AS (
2281
- SELECT
2282
- event.conversation_id,
2283
- event.payload -> 'message' AS message,
2284
- row_number() OVER (
2285
- PARTITION BY
2286
- event.conversation_id,
2287
- event.history_version,
2288
- event.payload -> 'message'
2289
- ORDER BY event.seq
2290
- ) AS occurrence
2291
- FROM junior_conversation_events AS event
2292
- INNER JOIN candidates AS candidate
2293
- ON candidate.conversation_id = event.conversation_id
2294
- WHERE event.type = 'agent_step'
2295
- AND event.payload -> 'message' ->> 'role' = 'assistant'
2296
- AND jsonb_typeof(event.payload -> 'message' -> 'usage') = 'object'
2297
- ),
2298
- canonical_messages AS (
2299
- SELECT DISTINCT conversation_id, message, occurrence
2300
- FROM message_occurrences
2301
- ),
2302
- usage_values AS (
2303
- SELECT
2304
- conversation_id,
2305
- CASE
2306
- WHEN jsonb_typeof(message -> 'usage' -> 'input') = 'number'
2307
- THEN greatest(0, floor((message -> 'usage' ->> 'input')::numeric))
2308
- WHEN jsonb_typeof(message -> 'usage' -> 'inputTokens') = 'number'
2309
- THEN greatest(0, floor((message -> 'usage' ->> 'inputTokens')::numeric))
2310
- END AS input_tokens,
2311
- CASE
2312
- WHEN jsonb_typeof(message -> 'usage' -> 'output') = 'number'
2313
- THEN greatest(0, floor((message -> 'usage' ->> 'output')::numeric))
2314
- WHEN jsonb_typeof(message -> 'usage' -> 'outputTokens') = 'number'
2315
- THEN greatest(0, floor((message -> 'usage' ->> 'outputTokens')::numeric))
2316
- END AS output_tokens,
2317
- CASE
2318
- WHEN jsonb_typeof(message -> 'usage' -> 'cacheRead') = 'number'
2319
- THEN greatest(0, floor((message -> 'usage' ->> 'cacheRead')::numeric))
2320
- WHEN jsonb_typeof(message -> 'usage' -> 'cachedInputTokens') = 'number'
2321
- THEN greatest(0, floor((message -> 'usage' ->> 'cachedInputTokens')::numeric))
2322
- END AS cached_input_tokens,
2323
- CASE
2324
- WHEN jsonb_typeof(message -> 'usage' -> 'cacheWrite') = 'number'
2325
- THEN greatest(0, floor((message -> 'usage' ->> 'cacheWrite')::numeric))
2326
- WHEN jsonb_typeof(message -> 'usage' -> 'cacheCreationTokens') = 'number'
2327
- THEN greatest(0, floor((message -> 'usage' ->> 'cacheCreationTokens')::numeric))
2328
- END AS cache_creation_tokens,
2329
- CASE
2330
- WHEN jsonb_typeof(message -> 'usage' -> 'reasoning') = 'number'
2331
- THEN greatest(0, floor((message -> 'usage' ->> 'reasoning')::numeric))
2332
- WHEN jsonb_typeof(message -> 'usage' -> 'reasoningTokens') = 'number'
2333
- THEN greatest(0, floor((message -> 'usage' ->> 'reasoningTokens')::numeric))
2334
- END AS reasoning_tokens,
2335
- CASE
2336
- WHEN jsonb_typeof(message -> 'usage' -> 'totalTokens') = 'number'
2337
- THEN greatest(0, floor((message -> 'usage' ->> 'totalTokens')::numeric))
2338
- END AS total_tokens,
2339
- CASE WHEN jsonb_typeof(message -> 'usage' -> 'cost' -> 'input') = 'number'
2340
- THEN greatest(0, (message -> 'usage' -> 'cost' ->> 'input')::numeric)
2341
- END AS cost_input,
2342
- CASE WHEN jsonb_typeof(message -> 'usage' -> 'cost' -> 'output') = 'number'
2343
- THEN greatest(0, (message -> 'usage' -> 'cost' ->> 'output')::numeric)
2344
- END AS cost_output,
2345
- CASE WHEN jsonb_typeof(message -> 'usage' -> 'cost' -> 'cacheRead') = 'number'
2346
- THEN greatest(0, (message -> 'usage' -> 'cost' ->> 'cacheRead')::numeric)
2347
- END AS cost_cache_read,
2348
- CASE WHEN jsonb_typeof(message -> 'usage' -> 'cost' -> 'cacheWrite') = 'number'
2349
- THEN greatest(0, (message -> 'usage' -> 'cost' ->> 'cacheWrite')::numeric)
2350
- END AS cost_cache_write,
2351
- CASE WHEN jsonb_typeof(message -> 'usage' -> 'cost' -> 'total') = 'number'
2352
- THEN greatest(0, (message -> 'usage' -> 'cost' ->> 'total')::numeric)
2353
- END AS cost_total
2354
- FROM canonical_messages
2355
- ),
2356
- rollups AS (
2357
- SELECT
2358
- conversation_id,
2359
- sum(input_tokens) AS input_tokens,
2360
- sum(output_tokens) AS output_tokens,
2361
- sum(cached_input_tokens) AS cached_input_tokens,
2362
- sum(cache_creation_tokens) AS cache_creation_tokens,
2363
- sum(reasoning_tokens) AS reasoning_tokens,
2364
- sum(
2365
- CASE
2366
- WHEN input_tokens IS NULL
2367
- AND output_tokens IS NULL
2368
- AND cached_input_tokens IS NULL
2369
- AND cache_creation_tokens IS NULL
2370
- THEN total_tokens
2371
- END
2372
- ) AS total_only_tokens,
2373
- round(sum(cost_input), 12) AS cost_input,
2374
- round(sum(cost_output), 12) AS cost_output,
2375
- round(sum(cost_cache_read), 12) AS cost_cache_read,
2376
- round(sum(cost_cache_write), 12) AS cost_cache_write,
2377
- round(sum(cost_total), 12) AS cost_total
2378
- FROM usage_values
2379
- GROUP BY conversation_id
2380
- HAVING count(input_tokens)
2381
- + count(output_tokens)
2382
- + count(cached_input_tokens)
2383
- + count(cache_creation_tokens)
2384
- + count(reasoning_tokens)
2385
- + count(total_tokens)
2386
- + count(cost_input)
2387
- + count(cost_output)
2388
- + count(cost_cache_read)
2389
- + count(cost_cache_write)
2390
- + count(cost_total) > 0
2391
- ),
2392
- valid_rollups AS (
2393
- SELECT *
2394
- FROM rollups
2395
- WHERE (input_tokens IS NULL OR input_tokens <= ${MAX_SAFE_INTEGER})
2396
- AND (output_tokens IS NULL OR output_tokens <= ${MAX_SAFE_INTEGER})
2397
- AND (cached_input_tokens IS NULL OR cached_input_tokens <= ${MAX_SAFE_INTEGER})
2398
- AND (cache_creation_tokens IS NULL OR cache_creation_tokens <= ${MAX_SAFE_INTEGER})
2399
- AND (reasoning_tokens IS NULL OR reasoning_tokens <= ${MAX_SAFE_INTEGER})
2400
- AND (total_only_tokens IS NULL OR total_only_tokens <= ${MAX_SAFE_INTEGER})
2401
- AND (
2402
- total_only_tokens IS NULL
2403
- OR total_only_tokens
2404
- + coalesce(input_tokens, 0)
2405
- + coalesce(output_tokens, 0)
2406
- + coalesce(cached_input_tokens, 0)
2407
- + coalesce(cache_creation_tokens, 0) <= ${MAX_SAFE_INTEGER}
2408
- )
2409
- AND (cost_input IS NULL OR cost_input <= ${MAX_FINITE_DOUBLE}::numeric)
2410
- AND (cost_output IS NULL OR cost_output <= ${MAX_FINITE_DOUBLE}::numeric)
2411
- AND (cost_cache_read IS NULL OR cost_cache_read <= ${MAX_FINITE_DOUBLE}::numeric)
2412
- AND (cost_cache_write IS NULL OR cost_cache_write <= ${MAX_FINITE_DOUBLE}::numeric)
2413
- AND (cost_total IS NULL OR cost_total <= ${MAX_FINITE_DOUBLE}::numeric)
2414
- ),
2415
- normalized AS (
2416
- SELECT
2417
- conversation_id,
2418
- jsonb_strip_nulls(jsonb_build_object(
2419
- 'inputTokens', CASE WHEN total_only_tokens IS NULL THEN input_tokens END,
2420
- 'outputTokens', CASE WHEN total_only_tokens IS NULL THEN output_tokens END,
2421
- 'cachedInputTokens', CASE
2422
- WHEN total_only_tokens IS NULL THEN cached_input_tokens
2423
- END,
2424
- 'cacheCreationTokens', CASE
2425
- WHEN total_only_tokens IS NULL THEN cache_creation_tokens
2426
- END,
2427
- 'reasoningTokens', reasoning_tokens,
2428
- 'totalTokens', CASE
2429
- WHEN total_only_tokens IS NOT NULL
2430
- THEN total_only_tokens
2431
- + coalesce(input_tokens, 0)
2432
- + coalesce(output_tokens, 0)
2433
- + coalesce(cached_input_tokens, 0)
2434
- + coalesce(cache_creation_tokens, 0)
2435
- END,
2436
- 'cost', CASE
2437
- WHEN cost_input IS NULL
2438
- AND cost_output IS NULL
2439
- AND cost_cache_read IS NULL
2440
- AND cost_cache_write IS NULL
2441
- AND cost_total IS NULL
2442
- THEN NULL
2443
- ELSE jsonb_strip_nulls(jsonb_build_object(
2444
- 'input', cost_input,
2445
- 'output', cost_output,
2446
- 'cacheRead', cost_cache_read,
2447
- 'cacheWrite', cost_cache_write,
2448
- 'total', cost_total
2449
- ))
2450
- END
2451
- )) AS usage
2452
- FROM valid_rollups
2453
- ),
2454
- matched AS MATERIALIZED (
2455
- SELECT conversation.conversation_id
2456
- FROM junior_conversations AS conversation
2457
- INNER JOIN candidates AS candidate USING (conversation_id)
2458
- INNER JOIN normalized USING (conversation_id)
2459
- WHERE conversation.execution_status = 'idle'
2460
- AND conversation.updated_at = candidate.updated_at
2461
- AND conversation.execution_updated_at IS NOT DISTINCT FROM candidate.execution_updated_at
2462
- FOR UPDATE OF conversation
2463
- ),
2464
- updated AS (
2465
- UPDATE junior_conversations AS conversation
2466
- SET usage_json = normalized.usage
2467
- FROM normalized
2468
- INNER JOIN matched USING (conversation_id)
2469
- WHERE conversation.conversation_id = normalized.conversation_id
2470
- AND conversation.usage_json IS DISTINCT FROM normalized.usage
2471
- RETURNING conversation.conversation_id
2472
- )
2473
- SELECT
2474
- candidate.conversation_id AS "conversationId",
2475
- updated.conversation_id IS NOT NULL AS changed,
2476
- matched.conversation_id IS NOT NULL AS matched,
2477
- normalized.conversation_id IS NOT NULL AS repairable
2478
- FROM candidates AS candidate
2479
- LEFT JOIN normalized USING (conversation_id)
2480
- LEFT JOIN matched USING (conversation_id)
2481
- LEFT JOIN updated USING (conversation_id)
2482
- ORDER BY candidate.conversation_id
2483
- `;
2484
- async function repairConversationUsage(_context, options = {}) {
2485
- let executor = options.executor;
2486
- let closeExecutor;
2487
- if (!executor) {
2488
- const { sql: sql2 } = getChatConfig();
2489
- executor = createJuniorSqlExecutor({
2490
- connectionString: sql2.databaseUrl,
2491
- driver: sql2.driver
2492
- });
2493
- closeExecutor = () => executor.close();
2494
- }
2495
- const batchSize = Math.max(
2496
- 1,
2497
- Math.floor(options.batchSize ?? CONVERSATION_USAGE_REPAIR_BATCH_SIZE)
2498
- );
2499
- try {
2500
- let cursor = "";
2501
- let existing = 0;
2502
- let migrated = 0;
2503
- let missing = 0;
2504
- let scanned = 0;
2505
- let skipped = 0;
2506
- while (true) {
2507
- const sources = await executor.withLock(
2508
- REPAIR_LOCK,
2509
- () => executor.query(USAGE_REPAIR_BATCH_SQL, [
2510
- cursor,
2511
- batchSize
2512
- ])
2513
- );
2514
- if (sources.length === 0) break;
2515
- scanned += sources.length;
2516
- for (const source of sources) {
2517
- if (!source.repairable) {
2518
- missing += 1;
2519
- } else if (source.changed) {
2520
- migrated += 1;
2521
- } else if (source.matched) {
2522
- existing += 1;
2523
- } else {
2524
- skipped += 1;
2525
- }
2526
- }
2527
- cursor = sources.at(-1).conversationId;
2528
- }
2529
- return {
2530
- existing,
2531
- migrated,
2532
- missing,
2533
- scanned,
2534
- ...skipped > 0 ? { skipped } : {}
2535
- };
2536
- } finally {
2537
- await closeExecutor?.();
2538
- }
2539
- }
2540
- var conversationUsageRepairMigration = {
2541
- name: "repair-conversation-usage",
2542
- run: repairConversationUsage
2543
- };
2544
-
2545
- // src/cli/upgrade/migrations/conversation-event-cursors.ts
2546
- var REDIS_SCAN_COUNT2 = 500;
2547
- function rawTurnSessionKeyPattern() {
2548
- const statePrefix = getChatConfig().state.keyPrefix;
2549
- return `*:cache:${statePrefix ? `${statePrefix}:` : ""}${AGENT_TURN_SESSION_PREFIX}:*`;
2550
- }
2551
- function logicalStateKey(rawKey2) {
2552
- const statePrefix = getChatConfig().state.keyPrefix;
2553
- const cachePrefix = `:cache:${statePrefix ? `${statePrefix}:` : ""}`;
2554
- const cacheAt = rawKey2.indexOf(cachePrefix);
2555
- return cacheAt < 0 ? void 0 : rawKey2.slice(cacheAt + cachePrefix.length);
2556
- }
2557
- async function discoverRawTurnSessionKeys(redisStateAdapter, conversationIds) {
2558
- const client = redisStateAdapter?.getClient();
2559
- const discovered = /* @__PURE__ */ new Map();
2560
- if (!client) return discovered;
2561
- const targetKeyPrefixes = [...conversationIds].map(
2562
- (conversationId) => `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:`
2563
- );
2564
- let cursor = "0";
2565
- do {
2566
- const reply = await client.sendCommand([
2567
- "SCAN",
2568
- cursor,
2569
- "MATCH",
2570
- rawTurnSessionKeyPattern(),
2571
- "COUNT",
2572
- String(REDIS_SCAN_COUNT2)
2573
- ]);
2574
- if (!Array.isArray(reply) || reply.length !== 2 || typeof reply[0] !== "string" && typeof reply[0] !== "number" || !Array.isArray(reply[1])) {
2575
- throw new Error(
2576
- "Unexpected Redis SCAN response while checking turn-session cursors"
2577
- );
2578
- }
2579
- cursor = String(reply[0]);
2580
- for (const rawKey2 of reply[1]) {
2581
- if (typeof rawKey2 !== "string") continue;
2582
- const logicalKey = logicalStateKey(rawKey2);
2583
- if (!logicalKey || !targetKeyPrefixes.some((prefix) => logicalKey.startsWith(prefix))) {
2584
- continue;
2585
- }
2586
- const encoded = await client.sendCommand(["GET", rawKey2]);
2587
- if (encoded === null) continue;
2588
- if (typeof encoded !== "string") {
2589
- throw new Error(`Turn-session cursor ${rawKey2} is not JSON text`);
2590
- }
2591
- let value;
2592
- try {
2593
- value = JSON.parse(encoded);
2594
- } catch (error) {
2595
- throw new Error(`Turn-session cursor ${rawKey2} is invalid JSON`, {
2596
- cause: error
2597
- });
2598
- }
2599
- if (!isRecord(value)) {
2600
- throw new Error(`Turn-session cursor ${rawKey2} is not an object`);
2601
- }
2602
- const conversationId = toOptionalString(value.conversationId);
2603
- const sessionId = toOptionalString(value.sessionId);
2604
- if (!conversationId || !sessionId) {
2605
- throw new Error(`Turn-session cursor ${rawKey2} has no identity`);
2606
- }
2607
- const recordKey = agentTurnSessionKey(conversationId, sessionId);
2608
- if (logicalKey !== recordKey) {
2609
- throw new Error(
2610
- `Turn-session cursor ${rawKey2} identity does not match its key`
2611
- );
2612
- }
2613
- if (!conversationIds.has(conversationId)) continue;
2614
- discovered.set(recordKey, conversationId);
2615
- }
2616
- } while (cursor !== "0");
2617
- return discovered;
2618
- }
2619
- async function indexedTurnSessionIds(context, conversationId) {
2620
- const summaries = await context.stateAdapter.getList(
2621
- agentTurnSessionConversationIndexKey(conversationId)
2622
- );
2623
- return [
2624
- ...new Set(
2625
- summaries.flatMap((summary) => {
2626
- if (!isRecord(summary)) return [];
2627
- const sessionId = toOptionalString(summary.sessionId);
2628
- return sessionId ? [sessionId] : [];
2629
- })
2630
- )
2631
- ];
2632
- }
2633
- function hasSeqCursor(record) {
2634
- return Number.isInteger(record.committedSeq) || Number.isInteger(record.turnStartSeq);
2635
- }
2636
- async function prepareConversationEventResequence(context, conversationIds) {
2637
- if (conversationIds.size === 0) return;
2638
- await context.stateAdapter.connect();
2639
- const keys = /* @__PURE__ */ new Set();
2640
- for (const conversationId of conversationIds) {
2641
- for (const sessionId of await indexedTurnSessionIds(
2642
- context,
2643
- conversationId
2644
- )) {
2645
- keys.add(agentTurnSessionKey(conversationId, sessionId));
2646
- }
2647
- }
2648
- for (const [key3] of await discoverRawTurnSessionKeys(
2649
- context.redisStateAdapter,
2650
- conversationIds
2651
- )) {
2652
- keys.add(key3);
2653
- }
2654
- const terminalKeys = [];
2655
- for (const key3 of keys) {
2656
- const value = await context.stateAdapter.get(key3);
2657
- if (!isRecord(value) || !hasSeqCursor(value)) continue;
2658
- if (value.state === "running" || value.state === "awaiting_resume") {
2659
- throw new Error(
2660
- `Cannot resequence conversation events while unfinished turn session ${key3} retains a seq cursor`
2661
- );
2662
- }
2663
- if (value.state !== "completed" && value.state !== "failed" && value.state !== "abandoned") {
2664
- throw new Error(
2665
- `Cannot resequence conversation events with invalid cursor-bearing turn session ${key3}`
2666
- );
2667
- }
2668
- terminalKeys.push(key3);
2669
- }
2670
- for (const key3 of terminalKeys) {
2671
- await context.stateAdapter.delete(key3);
2672
- }
2673
- }
2674
-
2675
- // src/cli/upgrade/migrations/conversation-visible-message-events.ts
2676
- var VISIBLE_EVENT_BATCH_SIZE = 500;
2677
- var VISIBLE_EVENT_BACKFILL_LOCK = "junior:upgrade:conversation-visible-message-events";
2678
- var LOAD_VISIBLE_CONVERSATION_IDS_SQL = `
2679
- SELECT DISTINCT conversation_id
2680
- FROM junior_conversation_messages
2681
- WHERE $2::text IS NULL OR conversation_id > $2::text
2682
- ORDER BY conversation_id
2683
- LIMIT $1
2684
- `;
2685
- var LOAD_COMPACTED_CONVERSATION_IDS_SQL = `
2686
- SELECT DISTINCT conversation_id
2687
- FROM junior_conversation_events
2688
- WHERE type = 'messages_summarized'
2689
- AND payload @? '$.compactions[*].coveredMessageIds'
2690
- AND ($2::text IS NULL OR conversation_id > $2::text)
2691
- ORDER BY conversation_id
2692
- LIMIT $1
2693
- `;
2694
- var LOAD_MISSING_VISIBLE_EVENTS_SQL = `
2695
- SELECT
2696
- message.conversation_id,
2697
- message.message_id,
2698
- message.role,
2699
- message.text,
2700
- message.author_identity_id,
2701
- message.meta,
2702
- message.replied_at,
2703
- message.created_at
2704
- FROM junior_conversation_messages message
2705
- WHERE (
2706
- NOT EXISTS (
2707
- SELECT 1
2708
- FROM junior_conversation_events event
2709
- WHERE event.conversation_id = message.conversation_id
2710
- AND event.idempotency_key =
2711
- 'message:' || message.message_id
2712
- )
2713
- OR (
2714
- message.replied_at IS NOT NULL
2715
- AND NOT EXISTS (
2716
- SELECT 1
2717
- FROM junior_conversation_events event
2718
- WHERE event.conversation_id = message.conversation_id
2719
- AND event.idempotency_key =
2720
- 'message:' || message.message_id || ':handled'
2721
- )
2722
- )
2723
- )
2724
- AND (
2725
- $2::text IS NULL
2726
- OR (message.conversation_id, message.created_at, message.message_id) >
2727
- ($2::text, $3::timestamptz, $4::text)
2728
- )
2729
- ORDER BY message.conversation_id, message.created_at, message.message_id
2730
- LIMIT $1
2731
- `;
2732
- var COUNT_MISSING_VISIBLE_EVENTS_SQL = `
2733
- SELECT count(*)::integer AS count
2734
- FROM junior_conversation_messages message
2735
- WHERE NOT EXISTS (
2736
- SELECT 1
2737
- FROM junior_conversation_events event
2738
- WHERE event.conversation_id = message.conversation_id
2739
- AND event.idempotency_key =
2740
- 'message:' || message.message_id
2741
- )
2742
- OR (
2743
- message.replied_at IS NOT NULL
2744
- AND NOT EXISTS (
2745
- SELECT 1
2746
- FROM junior_conversation_events event
2747
- WHERE event.conversation_id = message.conversation_id
2748
- AND event.idempotency_key =
2749
- 'message:' || message.message_id || ':handled'
2750
- )
2751
- )
2752
- `;
2753
- function timestampMs(value) {
2754
- return value instanceof Date ? value.getTime() : new Date(value).getTime();
46
+ return join(packageRoot, "migrations");
2755
47
  }
2756
- function eventsForRow(row) {
2757
- const events = [
2758
- {
2759
- idempotencyKey: `message:${row.message_id}`,
2760
- data: {
2761
- type: "message",
2762
- messageId: row.message_id,
2763
- role: row.role,
2764
- text: row.text,
2765
- ...row.author_identity_id ? { authorIdentityId: row.author_identity_id } : {},
2766
- ...row.meta ? { meta: row.meta } : {}
2767
- },
2768
- createdAtMs: timestampMs(row.created_at)
48
+ async function loadCoreMigrationState(executor) {
49
+ try {
50
+ const [migration] = await executor.query(`
51
+ SELECT created_at::text AS "appliedAt"
52
+ FROM drizzle.__drizzle_junior_core
53
+ ORDER BY created_at DESC
54
+ LIMIT 1
55
+ `);
56
+ if (migration?.appliedAt !== null && migration?.appliedAt !== void 0) {
57
+ return {
58
+ appliedAt: Number(migration.appliedAt),
59
+ hasJuniorTables: true
60
+ };
2769
61
  }
2770
- ];
2771
- if (row.replied_at) {
2772
- events.push({
2773
- idempotencyKey: `message:${row.message_id}:handled`,
2774
- data: { type: "message_handled", messageId: row.message_id },
2775
- createdAtMs: timestampMs(row.replied_at)
2776
- });
2777
- }
2778
- return events;
2779
- }
2780
- async function visitConversationIds(executor, query, batchSize, visit) {
2781
- let cursor;
2782
- while (true) {
2783
- const rows = await executor.query(query, [
2784
- batchSize,
2785
- cursor ?? null
2786
- ]);
2787
- if (rows.length === 0) return;
2788
- for (const row of rows) {
2789
- await visit(row.conversation_id);
62
+ } catch (error) {
63
+ if (!isPostgresErrorCode(error, "42P01")) {
64
+ throw error;
2790
65
  }
2791
- cursor = rows.at(-1).conversation_id;
2792
66
  }
2793
- }
2794
- async function visitVisibleConversations(executor, batchSize, visit) {
2795
- await visitConversationIds(
2796
- executor,
2797
- LOAD_VISIBLE_CONVERSATION_IDS_SQL,
2798
- batchSize,
2799
- visit
2800
- );
2801
- }
2802
- async function readLegacyCompactionState(context, conversationId) {
2803
- const raw = await context.stateAdapter.get(
2804
- `thread-state:${conversationId}`
2805
- );
2806
- const conversation = isRecord(raw) && isRecord(raw.conversation) ? raw.conversation : void 0;
2807
- const stats = conversation && isRecord(conversation.stats) ? conversation.stats : void 0;
2808
- const rawCount = toOptionalNumber(stats?.compactedMessageCount);
2809
- const compactedMessageCount = rawCount === void 0 ? void 0 : Math.max(0, Math.floor(rawCount));
2810
- const messages = conversation && Array.isArray(conversation.messages) ? conversation.messages : [];
2811
- const liveMessageIds = new Set(
2812
- messages.flatMap((message) => {
2813
- if (!isRecord(message)) return [];
2814
- const id = toOptionalString(message.id);
2815
- return id ? [id] : [];
2816
- })
2817
- );
67
+ const [tables] = await executor.query(`
68
+ SELECT EXISTS (
69
+ SELECT 1
70
+ FROM information_schema.tables
71
+ WHERE table_schema = 'public'
72
+ AND table_name LIKE 'junior\\_%' ESCAPE '\\'
73
+ ) AS "hasJuniorTables"
74
+ `);
2818
75
  return {
2819
- ...compactedMessageCount === void 0 ? {} : { compactedMessageCount },
2820
- liveMessageIds
76
+ hasJuniorTables: tables?.hasJuniorTables ?? false
2821
77
  };
2822
78
  }
2823
- function sqlVisibleEvents(events) {
2824
- return events.map((event) => {
2825
- const parsed = newConversationEventSchema.parse(event);
2826
- if (!parsed.idempotencyKey) {
2827
- throw new Error(
2828
- "Visible-message backfill events require idempotency keys"
2829
- );
2830
- }
2831
- const { type, ...payload } = conversationEventDataSchema.parse(parsed.data);
2832
- return {
2833
- createdAtMs: parsed.createdAtMs,
2834
- idempotencyKey: parsed.idempotencyKey,
2835
- payload: sanitizePostgresJson(payload),
2836
- type
2837
- };
2838
- });
79
+ function isCurrentMigrationState(state, latestMigrationAt) {
80
+ return state.appliedAt !== void 0 && state.appliedAt >= latestMigrationAt;
2839
81
  }
2840
- async function loadVisibleCompactionAnchor(executor, conversationId) {
2841
- const [row] = await executor.query(
2842
- `WITH snapshot AS (
2843
- SELECT payload
2844
- FROM junior_conversation_events
2845
- WHERE conversation_id = $1
2846
- AND type = 'messages_summarized'
2847
- ORDER BY seq DESC
2848
- LIMIT 1
2849
- )
2850
- SELECT (
2851
- SELECT event.payload->>'messageId'
2852
- FROM junior_conversation_events event
2853
- WHERE event.conversation_id = $1
2854
- AND event.type = 'message'
2855
- AND event.seq >= (snapshot.payload->>'historyFromSeq')::integer
2856
- ORDER BY event.seq
2857
- LIMIT 1
2858
- ) AS "messageId"
2859
- FROM snapshot`,
2860
- [conversationId]
2861
- );
2862
- return row;
82
+ function migrationResult(migrations, state) {
83
+ const appliedAt = state.appliedAt;
84
+ const existing = appliedAt === void 0 ? 0 : migrations.filter((migration) => migration.folderMillis <= appliedAt).length;
85
+ return {
86
+ existing,
87
+ migrated: migrations.length - existing,
88
+ scanned: migrations.length
89
+ };
2863
90
  }
2864
- async function normalizeVisibleCompactionBoundary(executor, conversationId, anchor) {
2865
- if (!anchor) return;
2866
- let historyFromSeq;
2867
- if (anchor.messageId) {
2868
- const [baseline] = await executor.query(
2869
- `SELECT seq
2870
- FROM junior_conversation_events
2871
- WHERE conversation_id = $1
2872
- AND type = 'message'
2873
- AND payload->>'messageId' = $2
2874
- ORDER BY seq
2875
- LIMIT 1`,
2876
- [conversationId, anchor.messageId]
2877
- );
2878
- if (!baseline) {
2879
- throw new Error(
2880
- `Visible compaction anchor ${anchor.messageId} disappeared during resequence`
2881
- );
2882
- }
2883
- historyFromSeq = baseline.seq;
2884
- } else {
2885
- const [end] = await executor.query(
2886
- `SELECT coalesce(max(seq), -1)::integer + 1 AS seq
2887
- FROM junior_conversation_events
2888
- WHERE conversation_id = $1`,
2889
- [conversationId]
2890
- );
2891
- historyFromSeq = end?.seq ?? 0;
91
+ function assertSupportedMigrationState(state) {
92
+ if (!state.hasJuniorTables || state.appliedAt !== void 0) {
93
+ return;
2892
94
  }
2893
- await executor.execute(
2894
- `WITH snapshot AS (
2895
- SELECT ctid
2896
- FROM junior_conversation_events
2897
- WHERE conversation_id = $1
2898
- AND type = 'messages_summarized'
2899
- ORDER BY seq DESC
2900
- LIMIT 1
2901
- )
2902
- UPDATE junior_conversation_events event
2903
- SET payload = jsonb_set(
2904
- event.payload,
2905
- '{historyFromSeq}',
2906
- to_jsonb($2::integer)
2907
- )
2908
- FROM snapshot
2909
- WHERE event.ctid = snapshot.ctid`,
2910
- [conversationId, historyFromSeq]
95
+ throw new Error(
96
+ `Existing Junior SQL tables have no core Drizzle migration history. Stop old Junior workers, install @sentry/junior@${CORE_MIGRATION_BRIDGE_VERSION}, run \`junior upgrade\`, then restore this Junior version and rerun \`junior upgrade\` before restarting workers.`
2911
97
  );
2912
98
  }
2913
- function coveredMessageIds(payload) {
2914
- if (!Array.isArray(payload.compactions)) return [];
2915
- return payload.compactions.flatMap((compaction) => {
2916
- if (!isRecord(compaction) || !Array.isArray(compaction.coveredMessageIds)) {
2917
- return [];
99
+ async function migrateSchema(executor) {
100
+ const migrationsFolder = migrationFolder();
101
+ const migrations = readMigrationFiles({ migrationsFolder });
102
+ const latestMigration = migrations.at(-1);
103
+ if (!latestMigration) {
104
+ throw new Error("No core Drizzle migrations were packaged");
105
+ }
106
+ const initialState = await loadCoreMigrationState(executor);
107
+ if (isCurrentMigrationState(initialState, latestMigration.folderMillis)) {
108
+ return migrationResult(migrations, initialState);
109
+ }
110
+ assertSupportedMigrationState(initialState);
111
+ return await executor.withMigrationLock(MIGRATIONS_TABLE, async () => {
112
+ const lockedState = await loadCoreMigrationState(executor);
113
+ if (isCurrentMigrationState(lockedState, latestMigration.folderMillis)) {
114
+ return migrationResult(migrations, lockedState);
2918
115
  }
2919
- return compaction.coveredMessageIds.flatMap((id) => {
2920
- const value = toOptionalString(id);
2921
- return value ? [value] : [];
116
+ assertSupportedMigrationState(lockedState);
117
+ await executor.migrate({
118
+ migrationsFolder,
119
+ migrationsTable: MIGRATIONS_TABLE
2922
120
  });
121
+ return migrationResult(migrations, lockedState);
2923
122
  });
2924
123
  }
2925
- function retainedCompactionCount(payload) {
2926
- if (!Array.isArray(payload.compactions)) return 0;
2927
- return payload.compactions.reduce((total, value) => {
2928
- if (!isRecord(value)) return total;
2929
- const stored = toOptionalNumber(value.coveredMessageCount);
2930
- const legacy = Array.isArray(value.coveredMessageIds) ? value.coveredMessageIds.length : 0;
2931
- return total + (stored === void 0 ? legacy : Math.max(0, stored));
2932
- }, 0);
124
+
125
+ // src/cli/upgrade.ts
126
+ import { createJiti } from "jiti";
127
+
128
+ // src/chat/plugins/migrations.ts
129
+ import { createHash } from "crypto";
130
+ import { readMigrationFiles as readMigrationFiles2 } from "drizzle-orm/migrator";
131
+ var LEGACY_SCHEDULER_BASELINE_HASH = "d1d2f712181dd3a0557808f0fc67fd0722691d25f4c8cfb816b77c71d19e1e42";
132
+ function migrationTable(pluginName) {
133
+ const label = pluginName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32);
134
+ const hash = createHash("sha256").update(pluginName).digest("hex").slice(0, 8);
135
+ return `__drizzle_${label}_${hash}`;
2933
136
  }
2934
- function resolvedHistoryFromSeq(args) {
2935
- if (args.legacyState.liveMessageIds.size > 0) {
2936
- const live = args.visibleEvents.find(
2937
- (event) => args.legacyState.liveMessageIds.has(event.messageId)
2938
- );
2939
- if (live) return live.seq;
2940
- }
2941
- if (args.legacyState.compactedMessageCount !== void 0) {
2942
- return args.visibleEvents[args.legacyState.compactedMessageCount]?.seq ?? args.endSeq;
2943
- }
2944
- const coveredIds = new Set(args.legacyCoveredMessageIds);
2945
- if (coveredIds.size > 0) {
2946
- const lastCoveredSeq = args.visibleEvents.reduce(
2947
- (last, event) => coveredIds.has(event.messageId) ? Math.max(last, event.seq) : last,
2948
- -1
137
+ async function appliedMigrationTime(executor, table) {
138
+ try {
139
+ const [row] = await executor.query(
140
+ `SELECT created_at::text AS "createdAt"
141
+ FROM drizzle.${table}
142
+ ORDER BY created_at DESC
143
+ LIMIT 1`
2949
144
  );
2950
- return args.visibleEvents.find((event) => event.seq > lastCoveredSeq)?.seq ?? args.endSeq;
145
+ return row?.createdAt === null || row?.createdAt === void 0 ? void 0 : Number(row.createdAt);
146
+ } catch (error) {
147
+ if (isPostgresErrorCode(error, "42P01")) {
148
+ return void 0;
149
+ }
150
+ throw error;
2951
151
  }
2952
- return args.currentHistoryFromSeq > 0 ? args.currentHistoryFromSeq : args.visibleEvents[0]?.seq ?? args.endSeq;
2953
152
  }
2954
- async function finalizeVisibleCompactions(context, executor, conversationId) {
2955
- const legacyState = await readLegacyCompactionState(context, conversationId);
2956
- await executor.withLock(
2957
- `junior_conversation:event:${conversationId}`,
2958
- async () => {
2959
- const [latest] = await executor.query(
2960
- `SELECT seq, payload
2961
- FROM junior_conversation_events
2962
- WHERE conversation_id = $1
2963
- AND type = 'messages_summarized'
2964
- ORDER BY seq DESC
2965
- LIMIT 1`,
2966
- [conversationId]
2967
- );
2968
- if (!latest) return;
2969
- const visibleEvents = await executor.query(
2970
- `SELECT seq, payload->>'messageId' AS "messageId"
2971
- FROM junior_conversation_events
2972
- WHERE conversation_id = $1
2973
- AND type = 'message'
2974
- ORDER BY seq`,
2975
- [conversationId]
2976
- );
2977
- const [cursor] = await executor.query(
2978
- `SELECT coalesce(max(seq), -1)::integer + 1 AS "endSeq"
2979
- FROM junior_conversation_events
2980
- WHERE conversation_id = $1`,
2981
- [conversationId]
2982
- );
2983
- const currentHistoryFromSeq = toOptionalNumber(latest.payload.historyFromSeq) ?? 0;
2984
- const historyFromSeq = resolvedHistoryFromSeq({
2985
- currentHistoryFromSeq: Math.max(0, Math.floor(currentHistoryFromSeq)),
2986
- endSeq: cursor?.endSeq ?? 0,
2987
- legacyCoveredMessageIds: coveredMessageIds(latest.payload),
2988
- legacyState,
2989
- visibleEvents
2990
- });
2991
- const retainedCount = retainedCompactionCount(latest.payload);
2992
- const countDelta = Math.max(
2993
- 0,
2994
- (legacyState.compactedMessageCount ?? retainedCount) - retainedCount
2995
- );
2996
- await executor.execute(
2997
- `WITH rewritten AS (
2998
- SELECT
2999
- event.ctid,
3000
- jsonb_agg(
3001
- (compaction.value - 'coveredMessageIds') ||
3002
- jsonb_build_object(
3003
- 'coveredMessageCount',
3004
- coalesce(
3005
- (compaction.value->>'coveredMessageCount')::integer,
3006
- jsonb_array_length(
3007
- coalesce(compaction.value->'coveredMessageIds', '[]'::jsonb)
3008
- )
3009
- ) + CASE
3010
- WHEN event.seq = $2 AND compaction.position = 1 THEN $4
3011
- ELSE 0
3012
- END
3013
- )
3014
- ORDER BY compaction.position
3015
- ) AS compactions
3016
- FROM junior_conversation_events event
3017
- CROSS JOIN LATERAL jsonb_array_elements(
3018
- event.payload->'compactions'
3019
- ) WITH ORDINALITY AS compaction(value, position)
3020
- WHERE event.conversation_id = $1
3021
- AND event.type = 'messages_summarized'
3022
- GROUP BY event.ctid
3023
- )
3024
- UPDATE junior_conversation_events event
3025
- SET payload =
3026
- (event.payload - 'compactions') ||
3027
- jsonb_build_object('compactions', rewritten.compactions) ||
3028
- CASE WHEN event.seq = $2
3029
- THEN jsonb_build_object('historyFromSeq', $3::integer)
3030
- ELSE '{}'::jsonb
3031
- END
3032
- FROM rewritten
3033
- WHERE event.ctid = rewritten.ctid`,
3034
- [conversationId, latest.seq, historyFromSeq, countDelta]
3035
- );
3036
- }
153
+ async function legacyMigrationHashes(executor, pluginName) {
154
+ const [exists] = await executor.query(
155
+ `SELECT to_regclass('public.junior_schema_migrations')::text AS "tableName"`
3037
156
  );
3038
- }
3039
- async function mergeVisibleEvents(executor, conversationId, events) {
3040
- const encoded = sqlVisibleEvents(events);
3041
- await executor.withLock(
3042
- `junior_conversation:event:${conversationId}`,
3043
- async () => {
3044
- await executor.transaction(async () => {
3045
- const compactionAnchor = await loadVisibleCompactionAnchor(
3046
- executor,
3047
- conversationId
3048
- );
3049
- await executor.execute(
3050
- `WITH input AS (
3051
- SELECT *
3052
- FROM jsonb_to_recordset($2::jsonb) AS event(
3053
- "createdAtMs" bigint,
3054
- "idempotencyKey" text,
3055
- payload jsonb,
3056
- type text
3057
- )
3058
- ), base AS (
3059
- SELECT coalesce(max(seq), -1) AS max_seq
3060
- FROM junior_conversation_events
3061
- WHERE conversation_id = $1
3062
- ), pending AS (
3063
- SELECT
3064
- input.*,
3065
- row_number() OVER (
3066
- ORDER BY input."createdAtMs", input."idempotencyKey"
3067
- ) AS offset
3068
- FROM input
3069
- WHERE NOT EXISTS (
3070
- SELECT 1
3071
- FROM junior_conversation_events existing
3072
- WHERE existing.conversation_id = $1
3073
- AND existing.idempotency_key = input."idempotencyKey"
3074
- )
3075
- )
3076
- INSERT INTO junior_conversation_events (
3077
- conversation_id,
3078
- seq,
3079
- history_version,
3080
- schema_version,
3081
- idempotency_key,
3082
- type,
3083
- payload,
3084
- created_at
3085
- )
3086
- SELECT
3087
- $1,
3088
- base.max_seq + pending.offset,
3089
- coalesce((
3090
- SELECT prior.history_version
3091
- FROM junior_conversation_events prior
3092
- WHERE prior.conversation_id = $1
3093
- AND prior.created_at < to_timestamp(pending."createdAtMs" / 1000.0)
3094
- ORDER BY prior.seq DESC
3095
- LIMIT 1
3096
- ), 0),
3097
- 1,
3098
- pending."idempotencyKey",
3099
- pending.type,
3100
- pending.payload,
3101
- to_timestamp(pending."createdAtMs" / 1000.0)
3102
- FROM pending
3103
- CROSS JOIN base
3104
- ON CONFLICT (conversation_id, idempotency_key) DO NOTHING`,
3105
- [conversationId, JSON.stringify(encoded)]
3106
- );
3107
- await executor.execute(
3108
- `UPDATE junior_conversation_events
3109
- SET seq = -seq - 1
3110
- WHERE conversation_id = $1`,
3111
- [conversationId]
3112
- );
3113
- await executor.execute(
3114
- `WITH ranked AS (
3115
- SELECT
3116
- event.ctid,
3117
- row_number() OVER (
3118
- ORDER BY
3119
- CASE
3120
- WHEN event.type IN (
3121
- 'message',
3122
- 'message_handled'
3123
- ) THEN coalesce((
3124
- SELECT max(-execution.seq - 1)
3125
- FROM junior_conversation_events execution
3126
- WHERE execution.conversation_id = event.conversation_id
3127
- AND execution.type NOT IN (
3128
- 'message',
3129
- 'message_handled'
3130
- )
3131
- AND execution.created_at < event.created_at
3132
- ), -1)
3133
- ELSE -event.seq - 1
3134
- END,
3135
- CASE
3136
- WHEN event.type IN (
3137
- 'message',
3138
- 'message_handled'
3139
- ) THEN 1
3140
- ELSE 0
3141
- END,
3142
- event.created_at,
3143
- CASE event.type
3144
- WHEN 'message' THEN 0
3145
- WHEN 'message_handled' THEN 1
3146
- ELSE 0
3147
- END,
3148
- event.idempotency_key NULLS FIRST,
3149
- event.type
3150
- ) - 1 AS next_seq
3151
- FROM junior_conversation_events event
3152
- WHERE event.conversation_id = $1
3153
- )
3154
- UPDATE junior_conversation_events event
3155
- SET seq = ranked.next_seq
3156
- FROM ranked
3157
- WHERE event.ctid = ranked.ctid`,
3158
- [conversationId]
3159
- );
3160
- await normalizeVisibleCompactionBoundary(
3161
- executor,
3162
- conversationId,
3163
- compactionAnchor
3164
- );
3165
- });
3166
- }
157
+ if (!exists?.tableName) {
158
+ return /* @__PURE__ */ new Set();
159
+ }
160
+ const rows = await executor.query(
161
+ `SELECT checksum
162
+ FROM junior_schema_migrations
163
+ WHERE id LIKE $1
164
+ ORDER BY id ASC`,
165
+ [`plugin:${pluginName}/%`]
3167
166
  );
167
+ return new Set(rows.map((row) => row.checksum));
3168
168
  }
3169
- async function migrateConversationVisibleMessageEvents(context, options = {}) {
3170
- let executor = options.executor;
3171
- let closeExecutor;
3172
- if (!executor) {
3173
- const { sql: sql2 } = getChatConfig();
3174
- executor = createJuniorSqlExecutor({
3175
- connectionString: sql2.databaseUrl,
3176
- driver: sql2.driver
3177
- });
3178
- closeExecutor = () => executor.close();
169
+ function adoptedMigration(migrations, legacyHashes, pluginName) {
170
+ let adopted;
171
+ for (const migration of migrations) {
172
+ if (!legacyHashes.has(migration.hash)) {
173
+ break;
174
+ }
175
+ adopted = migration;
176
+ }
177
+ if (!adopted && pluginName === "scheduler" && legacyHashes.has(LEGACY_SCHEDULER_BASELINE_HASH)) {
178
+ return migrations[0];
3179
179
  }
3180
- const batchSize = Math.max(
3181
- 1,
3182
- Math.floor(options.batchSize ?? VISIBLE_EVENT_BATCH_SIZE)
180
+ return adopted;
181
+ }
182
+ async function adoptLegacyMigrationState(args) {
183
+ const legacyHashes = await legacyMigrationHashes(
184
+ args.executor,
185
+ args.pluginName
3183
186
  );
3184
- let migrated = 0;
3185
- let cursor;
3186
- try {
3187
- const [legacyTable] = await executor.query(
3188
- `SELECT to_regclass('public.junior_conversation_messages') IS NOT NULL AS exists`
187
+ const migration = adoptedMigration(
188
+ args.migrations,
189
+ legacyHashes,
190
+ args.pluginName
191
+ );
192
+ if (!migration) {
193
+ return void 0;
194
+ }
195
+ await args.executor.transaction(async () => {
196
+ await args.executor.execute("CREATE SCHEMA IF NOT EXISTS drizzle");
197
+ await args.executor.execute(`
198
+ CREATE TABLE IF NOT EXISTS drizzle.${args.table} (
199
+ id SERIAL PRIMARY KEY,
200
+ hash TEXT NOT NULL,
201
+ created_at BIGINT
202
+ )
203
+ `);
204
+ await args.executor.execute(
205
+ `INSERT INTO drizzle.${args.table} (hash, created_at) VALUES ($1, $2)`,
206
+ [migration.hash, migration.folderMillis]
3189
207
  );
3190
- if (!legacyTable?.exists) {
3191
- return { existing: 0, migrated: 0, missing: 0, scanned: 0 };
208
+ });
209
+ return migration.folderMillis;
210
+ }
211
+ function appliedCount(migrations, createdAt) {
212
+ return createdAt === void 0 ? 0 : migrations.filter((migration) => migration.folderMillis <= createdAt).length;
213
+ }
214
+ async function migratePluginSchemas(executor, roots, options = {}) {
215
+ const result = {
216
+ existing: 0,
217
+ migrated: 0,
218
+ scanned: 0
219
+ };
220
+ const orderedRoots = [...roots].sort(
221
+ (left, right) => left.pluginName.localeCompare(right.pluginName)
222
+ );
223
+ for (const root of orderedRoots) {
224
+ const migrations = readMigrationFiles2({ migrationsFolder: root.dir });
225
+ if (migrations.length === 0) {
226
+ continue;
3192
227
  }
3193
- const [before] = await executor.query(
3194
- COUNT_MISSING_VISIBLE_EVENTS_SQL
3195
- );
3196
- if (!before) {
3197
- throw new Error(
3198
- "Visible-message event migration could not inspect its projection"
3199
- );
228
+ const table = migrationTable(root.pluginName);
229
+ const initialTime = await appliedMigrationTime(executor, table);
230
+ const initialExisting = appliedCount(migrations, initialTime);
231
+ if (initialExisting === migrations.length) {
232
+ const summary2 = {
233
+ existing: initialExisting,
234
+ migrated: 0,
235
+ pluginName: root.pluginName,
236
+ scanned: migrations.length
237
+ };
238
+ result.scanned += summary2.scanned;
239
+ result.existing += summary2.existing;
240
+ options.onPluginMigration?.(summary2);
241
+ continue;
3200
242
  }
3201
- if (before.count > 0) {
3202
- const visibleConversationIds = /* @__PURE__ */ new Set();
3203
- await visitVisibleConversations(
243
+ const summary = await executor.withMigrationLock(table, async () => {
244
+ const currentTime = await appliedMigrationTime(executor, table) ?? await adoptLegacyMigrationState({
3204
245
  executor,
3205
- batchSize,
3206
- async (conversationId) => {
3207
- visibleConversationIds.add(conversationId);
3208
- }
3209
- );
3210
- await prepareConversationEventResequence(context, visibleConversationIds);
3211
- }
3212
- while (true) {
3213
- const rows = await executor.withLock(
3214
- VISIBLE_EVENT_BACKFILL_LOCK,
3215
- () => executor.query(LOAD_MISSING_VISIBLE_EVENTS_SQL, [
3216
- batchSize,
3217
- cursor?.conversationId ?? null,
3218
- cursor?.createdAt ?? null,
3219
- cursor?.messageId ?? null
3220
- ])
3221
- );
3222
- if (rows.length === 0) {
3223
- break;
3224
- }
3225
- const grouped = /* @__PURE__ */ new Map();
3226
- for (const row of rows) {
3227
- const events = grouped.get(row.conversation_id) ?? [];
3228
- events.push(...eventsForRow(row));
3229
- grouped.set(row.conversation_id, events);
3230
- }
3231
- for (const [conversationId, events] of grouped) {
3232
- await mergeVisibleEvents(executor, conversationId, events);
246
+ migrations,
247
+ pluginName: root.pluginName,
248
+ table
249
+ });
250
+ const existing = appliedCount(migrations, currentTime);
251
+ if (existing < migrations.length) {
252
+ await executor.migrate({
253
+ migrationsFolder: root.dir,
254
+ migrationsTable: table
255
+ });
3233
256
  }
3234
- migrated += rows.length;
3235
- const last = rows.at(-1);
3236
- cursor = {
3237
- conversationId: last.conversation_id,
3238
- createdAt: last.created_at,
3239
- messageId: last.message_id
257
+ return {
258
+ existing,
259
+ migrated: migrations.length - existing,
260
+ pluginName: root.pluginName,
261
+ scanned: migrations.length
3240
262
  };
3241
- }
3242
- const [remaining] = await executor.query(
3243
- COUNT_MISSING_VISIBLE_EVENTS_SQL
3244
- );
3245
- if (!remaining) {
3246
- throw new Error(
3247
- "Visible-message event migration could not verify its projection"
3248
- );
3249
- }
3250
- if (remaining.count > 0) {
3251
- throw new Error(
3252
- `Visible-message event migration left ${remaining.count} message row(s) without canonical events`
3253
- );
3254
- }
3255
- await visitConversationIds(
3256
- executor,
3257
- LOAD_COMPACTED_CONVERSATION_IDS_SQL,
3258
- batchSize,
3259
- async (conversationId) => await finalizeVisibleCompactions(context, executor, conversationId)
3260
- );
3261
- return {
3262
- existing: 0,
3263
- migrated,
3264
- missing: 0,
3265
- scanned: migrated
3266
- };
3267
- } finally {
3268
- await closeExecutor?.();
263
+ });
264
+ result.scanned += summary.scanned;
265
+ result.existing += summary.existing;
266
+ result.migrated += summary.migrated;
267
+ options.onPluginMigration?.(summary);
3269
268
  }
269
+ return result;
3270
270
  }
3271
- var conversationVisibleMessageEventsMigration = {
3272
- name: "move-conversation-messages-to-events",
3273
- run: migrateConversationVisibleMessageEvents
3274
- };
3275
271
 
3276
- // src/cli/upgrade/migrations/conversation-context-checkpoints.ts
3277
- import { isDeepStrictEqual } from "util";
3278
- var PAGE_SIZE = 250;
3279
- function messageTextParts(message) {
3280
- if (typeof message.content === "string") {
3281
- return [message.content];
3282
- }
3283
- if (!Array.isArray(message.content)) {
3284
- return [];
3285
- }
3286
- return message.content.flatMap(
3287
- (part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string" ? [part.text] : []
3288
- );
272
+ // src/cli/upgrade/migrations/upgrade-plugins.ts
273
+ function unique(values) {
274
+ return [...new Set(values)];
3289
275
  }
3290
- function checkpointEnd(messages) {
3291
- return messages.findIndex((row) => {
3292
- const message = row.payload.message;
3293
- return message !== null && typeof message === "object" && !Array.isArray(message) && messageTextParts(message).some(
3294
- isLegacyOrCurrentCheckpointText
3295
- );
3296
- });
276
+ function baseCatalogConfig(context) {
277
+ return context.pluginCatalogConfig ?? (context.pluginSet ? pluginCatalogConfigFromPluginSet(context.pluginSet) : pluginCatalogConfigFromEnv());
3297
278
  }
3298
- function matchingMessagePrefix(previous, current) {
3299
- const limit = Math.min(previous.length, current.length);
3300
- let count = 0;
3301
- while (count < limit && isDeepStrictEqual(
3302
- previous[count].payload.message,
3303
- current[count].payload.message
3304
- )) {
3305
- count += 1;
3306
- }
3307
- return count;
279
+ function inlinePluginName(definition) {
280
+ return definition.manifest.name;
3308
281
  }
3309
- function replacementEntry(row, normalizeCheckpoint) {
3310
- const message = row.payload.message;
3311
- if (message === null || typeof message !== "object" || Array.isArray(message)) {
3312
- throw new Error(
3313
- `Conversation message event ${row.seq} has no message object`
3314
- );
282
+ function mergeInlineManifests(left, right) {
283
+ const merged = /* @__PURE__ */ new Map();
284
+ for (const definition of [...left ?? [], ...right ?? []]) {
285
+ merged.set(inlinePluginName(definition), definition);
3315
286
  }
3316
- return {
3317
- message: normalizeCheckpoint ? normalizeLegacyContextMessage(message) : message,
3318
- ...row.payload.provenance === void 0 ? {} : { provenance: row.payload.provenance }
3319
- };
287
+ return merged.size > 0 ? [...merged.values()] : void 0;
3320
288
  }
3321
- function resolvedBinding(payload, bot) {
3322
- const modelProfile = payload.modelProfile === void 0 ? "standard" : modelProfileSchema.parse(payload.modelProfile);
3323
- const modelId = payload.modelId;
3324
- if (modelId !== void 0 && (typeof modelId !== "string" || !modelId)) {
3325
- throw new Error("Context checkpoint modelId must be a non-empty string");
289
+ function mergeCatalogConfig(base, added) {
290
+ if (!base) {
291
+ return added;
292
+ }
293
+ if (!added) {
294
+ return base;
3326
295
  }
296
+ const inlineManifests = mergeInlineManifests(
297
+ base.inlineManifests,
298
+ added.inlineManifests
299
+ );
300
+ const packages = unique([
301
+ ...base.packages ?? [],
302
+ ...added.packages ?? []
303
+ ]);
304
+ const manifests = base.manifests || added.manifests ? { ...base.manifests, ...added.manifests } : void 0;
3327
305
  return {
3328
- modelProfile,
3329
- modelId: modelId ?? modelIdForProfile(bot, modelProfile)
306
+ ...inlineManifests ? { inlineManifests } : {},
307
+ ...packages.length > 0 ? { packages } : {},
308
+ ...manifests ? { manifests } : {}
3330
309
  };
3331
310
  }
3332
- function handoffToolCallId(rows, marker) {
3333
- if (typeof marker.payload.triggeringToolCallId === "string" && marker.payload.triggeringToolCallId.length > 0) {
3334
- return marker.payload.triggeringToolCallId;
3335
- }
3336
- const tool = [...rows].reverse().find(
3337
- (row) => row.seq < marker.seq && row.type === "tool_execution_started" && row.payload.toolName === "handoff" && typeof row.payload.toolCallId === "string" && row.payload.toolCallId.length > 0
3338
- );
3339
- if (typeof tool?.payload.toolCallId !== "string") {
3340
- throw new Error(
3341
- `Cannot correlate handoff checkpoint at event ${marker.seq} during upgrade`
3342
- );
3343
- }
3344
- return tool.payload.toolCallId;
311
+ function packageNamesFromContext(context, catalog) {
312
+ return unique([
313
+ ...context.pluginSet?.packageNames ?? [],
314
+ ...catalog?.packages ?? []
315
+ ]);
3345
316
  }
3346
- async function normalizeConversation(args) {
3347
- return await args.executor.withLock(
3348
- `junior_conversation:event:${args.conversationId}`,
3349
- async () => await args.executor.transaction(async () => {
3350
- const rows = await args.executor.query(
3351
- `SELECT
3352
- seq,
3353
- history_version AS "historyVersion",
3354
- type,
3355
- payload
3356
- FROM junior_conversation_events
3357
- WHERE conversation_id = $1
3358
- AND type IN ('context_epoch_started', 'agent_step', 'tool_execution_started')
3359
- ORDER BY seq`,
3360
- [args.conversationId]
3361
- );
3362
- const messagesByEpoch = /* @__PURE__ */ new Map();
3363
- for (const row of rows) {
3364
- if (row.type !== "agent_step") continue;
3365
- const messages = messagesByEpoch.get(row.historyVersion) ?? [];
3366
- messages.push(row);
3367
- messagesByEpoch.set(row.historyVersion, messages);
3368
- }
3369
- let migrated = 0;
3370
- const removedSeqs = [];
3371
- for (const marker of rows) {
3372
- if (marker.type !== "context_epoch_started") {
3373
- continue;
3374
- }
3375
- const reason = marker.payload.reason;
3376
- if (reason === "initial") {
3377
- removedSeqs.push(marker.seq);
3378
- migrated += 1;
3379
- continue;
3380
- }
3381
- if (reason !== "compaction" && reason !== "handoff" && reason !== "rollback") {
3382
- throw new Error(
3383
- `Unknown context checkpoint reason at event ${marker.seq}`
3384
- );
3385
- }
3386
- let replacementHistory = marker.payload.replacementHistory;
3387
- if (!Array.isArray(replacementHistory)) {
3388
- const current = messagesByEpoch.get(marker.historyVersion) ?? [];
3389
- let replacementCount;
3390
- if (reason === "rollback") {
3391
- const previous = messagesByEpoch.get(marker.historyVersion - 1) ?? [];
3392
- replacementCount = matchingMessagePrefix(previous, current);
3393
- } else {
3394
- const end = checkpointEnd(current);
3395
- replacementCount = end < 0 ? current.length : end + 1;
3396
- }
3397
- const replacementRows = current.slice(0, replacementCount);
3398
- replacementHistory = replacementRows.map(
3399
- (row, index) => replacementEntry(
3400
- row,
3401
- reason !== "rollback" && index === replacementRows.length - 1
3402
- )
3403
- );
3404
- removedSeqs.push(...replacementRows.map((row) => row.seq));
3405
- }
3406
- const binding = resolvedBinding(marker.payload, args.bot);
3407
- const parsed = conversationEventDataSchema.parse({
3408
- type: reason,
3409
- ...binding,
3410
- ...reason === "handoff" ? { triggeringToolCallId: handoffToolCallId(rows, marker) } : {},
3411
- replacementHistory
3412
- });
3413
- const { type, ...payload } = parsed;
3414
- await args.executor.execute(
3415
- `UPDATE junior_conversation_events
3416
- SET type = $3, payload = $4::jsonb
3417
- WHERE conversation_id = $1 AND seq = $2`,
3418
- [
3419
- args.conversationId,
3420
- marker.seq,
3421
- type,
3422
- JSON.stringify(sanitizePostgresJson(payload))
3423
- ]
3424
- );
3425
- migrated += 1;
3426
- }
3427
- if (removedSeqs.length > 0) {
3428
- await args.executor.execute(
3429
- `UPDATE junior_conversation_events
3430
- SET payload = jsonb_set(
3431
- payload,
3432
- '{historyFromSeq}',
3433
- to_jsonb(
3434
- (payload->>'historyFromSeq')::integer - (
3435
- SELECT count(*)::integer
3436
- FROM unnest($2::integer[]) removed(seq)
3437
- WHERE removed.seq < (payload->>'historyFromSeq')::integer
3438
- )
3439
- )
3440
- )
3441
- WHERE conversation_id = $1
3442
- AND type = 'messages_summarized'
3443
- AND jsonb_typeof(payload->'historyFromSeq') = 'number'`,
3444
- [args.conversationId, removedSeqs]
3445
- );
3446
- await args.executor.execute(
3447
- `DELETE FROM junior_conversation_events
3448
- WHERE conversation_id = $1 AND seq = ANY($2::integer[])`,
3449
- [args.conversationId, removedSeqs]
3450
- );
3451
- await args.executor.execute(
3452
- `UPDATE junior_conversation_events
3453
- SET seq = -seq - 1
3454
- WHERE conversation_id = $1`,
3455
- [args.conversationId]
3456
- );
3457
- await args.executor.execute(
3458
- `WITH ranked AS (
3459
- SELECT
3460
- ctid,
3461
- row_number() OVER (ORDER BY -seq - 1) - 1 AS next_seq
3462
- FROM junior_conversation_events
3463
- WHERE conversation_id = $1
3464
- )
3465
- UPDATE junior_conversation_events event
3466
- SET seq = ranked.next_seq
3467
- FROM ranked
3468
- WHERE event.ctid = ranked.ctid`,
3469
- [args.conversationId]
3470
- );
3471
- }
3472
- return migrated;
3473
- })
317
+ async function resolveUpgradePluginCatalog(context) {
318
+ const catalog = baseCatalogConfig(context);
319
+ const packageNames = packageNamesFromContext(context, catalog);
320
+ const registrations = context.pluginSet?.registrations ?? [];
321
+ const manifests = context.pluginSet?.manifests || catalog?.manifests ? {
322
+ ...catalog?.manifests,
323
+ ...context.pluginSet?.manifests
324
+ } : void 0;
325
+ const pluginSet = packageNames.length > 0 || registrations.length > 0 || context.pluginSet ? defineJuniorPlugins(
326
+ [...packageNames, ...registrations],
327
+ manifests ? { manifests } : {}
328
+ ) : void 0;
329
+ return mergeCatalogConfig(
330
+ catalog,
331
+ pluginCatalogConfigFromPluginSet(pluginSet)
3474
332
  );
3475
333
  }
3476
- async function normalizeConversationContextCheckpoints(context, options = {}) {
3477
- const config = options.executor && options.bot ? void 0 : getChatConfig();
3478
- const bot = options.bot ?? config?.bot;
3479
- if (!bot) throw new Error("Context checkpoint upgrade requires bot config");
3480
- let executor = options.executor;
3481
- if (!executor) {
3482
- if (!config)
3483
- throw new Error("Context checkpoint upgrade requires SQL config");
3484
- executor = createJuniorSqlExecutor({
3485
- connectionString: config.sql.databaseUrl,
3486
- driver: config.sql.driver
3487
- });
3488
- }
3489
- try {
3490
- const result = {
3491
- existing: 0,
3492
- migrated: 0,
3493
- missing: 0,
3494
- scanned: 0
3495
- };
3496
- while (true) {
3497
- const conversations = await executor.query(
3498
- `SELECT DISTINCT conversation_id AS "conversationId"
3499
- FROM junior_conversation_events
3500
- WHERE type = 'context_epoch_started'
3501
- ORDER BY conversation_id
3502
- LIMIT $1`,
3503
- [PAGE_SIZE]
3504
- );
3505
- if (conversations.length === 0) break;
3506
- await prepareConversationEventResequence(
3507
- context,
3508
- new Set(conversations.map(({ conversationId }) => conversationId))
3509
- );
3510
- for (const { conversationId } of conversations) {
3511
- result.scanned += 1;
3512
- result.migrated += await normalizeConversation({
3513
- bot,
3514
- conversationId,
3515
- executor
3516
- });
3517
- }
3518
- }
3519
- return result;
3520
- } finally {
3521
- if (!options.executor) await executor.close();
3522
- }
334
+
335
+ // src/cli/upgrade/migrations/plugin-sql.ts
336
+ async function migratePluginsToSql(context, options = {}) {
337
+ const pluginCatalog = createPluginCatalogRuntime();
338
+ pluginCatalog.setConfig(await resolveUpgradePluginCatalog(context));
339
+ return await migratePluginSchemas(
340
+ context.sqlExecutor,
341
+ pluginCatalog.getMigrationRoots(),
342
+ options
343
+ );
3523
344
  }
3524
- var conversationContextCheckpointMigration = {
3525
- name: "normalize-conversation-context-checkpoints",
3526
- run: normalizeConversationContextCheckpoints
3527
- };
3528
345
 
3529
346
  // src/cli/upgrade.ts
3530
347
  var DEFAULT_IO = {
3531
348
  info: console.log
3532
349
  };
3533
350
  var localPluginLoader = createJiti(import.meta.url, { moduleCache: false });
3534
- var MIGRATIONS = [
3535
- agentTurnSessionActorMigration,
3536
- redisConversationStateMigration,
3537
- coreSqlSchemaMigration,
3538
- sqlConversationMigration,
3539
- conversationContextCheckpointMigration,
3540
- // The external history import must precede the SQL read-model backfill.
3541
- // Visible events seal a conversation against legacy Redis/session import.
3542
- sqlConversationHistoryMigration,
3543
- conversationVisibleMessageEventsMigration,
3544
- conversationUsageRepairMigration,
3545
- sqlPluginMigration,
3546
- pluginStorageMigration
3547
- ];
3548
351
  function isMissingVirtualConfig(error) {
3549
352
  if (!(error instanceof Error)) {
3550
353
  return false;
@@ -3566,50 +369,56 @@ async function resolveUpgradePluginSet() {
3566
369
  async (moduleRef) => localPluginLoader.import(moduleRef.importPath)
3567
370
  );
3568
371
  }
3569
- function formatMigrationResult(result) {
3570
- const fields = [
3571
- `scanned=${result.scanned}`,
3572
- `migrated=${result.migrated}`,
3573
- `existing=${result.existing}`,
3574
- `missing=${result.missing}`
3575
- ];
3576
- if (result.skipped !== void 0) {
3577
- fields.push(`skipped=${result.skipped}`);
3578
- }
3579
- return fields.join(" ");
372
+ function migrationCount(count) {
373
+ return `${count} ${count === 1 ? "migration" : "migrations"}`;
3580
374
  }
3581
- async function runUpgradeMigrations(context) {
3582
- const plugins = await resolveUpgradePlugins(context);
3583
- const migrationContext = { ...context, ...plugins };
3584
- const results = [];
3585
- for (const migration of MIGRATIONS) {
3586
- migrationContext.io.info(`Running migration ${migration.name}...`);
3587
- const result = await migration.run(migrationContext);
3588
- migrationContext.io.info(
3589
- `Finished migration ${migration.name}: ${formatMigrationResult(result)}`
3590
- );
3591
- results.push(result);
375
+ function formatOwnerSummary(owner, summary) {
376
+ if (summary.migrated === 0) {
377
+ return ` ${owner}: up to date (${migrationCount(summary.scanned)})`;
3592
378
  }
3593
- return results;
379
+ return ` ${owner}: applied ${migrationCount(summary.migrated)} (${summary.scanned} total)`;
380
+ }
381
+ function pluginMigrationOwner(pluginName) {
382
+ const unscopedName = pluginName.split("/").at(-1) ?? pluginName;
383
+ return unscopedName.startsWith("junior-") ? unscopedName : `junior-${unscopedName}`;
384
+ }
385
+ async function runDatabaseMigrations(context, io) {
386
+ io.info("Checking database migrations...");
387
+ const core = await migrateSchema(context.sqlExecutor);
388
+ io.info(formatOwnerSummary("junior", core));
389
+ const plugins = await migratePluginsToSql(context, {
390
+ onPluginMigration: (summary) => {
391
+ io.info(
392
+ formatOwnerSummary(pluginMigrationOwner(summary.pluginName), summary)
393
+ );
394
+ }
395
+ });
396
+ const migrated = core.migrated + plugins.migrated;
397
+ const scanned = core.scanned + plugins.scanned;
398
+ io.info(
399
+ migrated === 0 ? `Database is up to date (${migrationCount(scanned)}).` : `Applied ${migrationCount(migrated)} (${scanned} total).`
400
+ );
3594
401
  }
3595
402
  async function runUpgrade(io = DEFAULT_IO, options = {}) {
403
+ const { sql } = getChatConfig();
404
+ const sqlExecutor = createJuniorSqlExecutor({
405
+ connectionString: sql.databaseUrl,
406
+ driver: sql.driver
407
+ });
3596
408
  try {
3597
- const { redisStateAdapter, stateAdapter } = await getConnectedStateContext();
3598
409
  const pluginSet = options.pluginSet === void 0 ? await resolveUpgradePluginSet() : options.pluginSet ?? void 0;
3599
- io.info("Running Junior upgrade migrations...");
3600
- await runUpgradeMigrations({
3601
- io,
3602
- pluginSet,
3603
- redisStateAdapter,
3604
- stateAdapter
3605
- });
3606
- io.info("Junior upgrade complete.");
410
+ await runDatabaseMigrations(
411
+ {
412
+ pluginSet,
413
+ sqlExecutor
414
+ },
415
+ io
416
+ );
3607
417
  } finally {
3608
- await disconnectStateAdapter();
418
+ await sqlExecutor.close();
3609
419
  }
3610
420
  }
3611
421
  export {
3612
422
  resolveUpgradePluginSet,
3613
- runUpgrade,
3614
- runUpgradeMigrations
423
+ runUpgrade
3615
424
  };