@rudderhq/db 0.4.6-canary.11 → 0.4.6-canary.12

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/client.test.js +627 -0
  2. package/dist/client.test.js.map +1 -1
  3. package/dist/embedded-postgres-recovery.d.ts +1 -0
  4. package/dist/embedded-postgres-recovery.d.ts.map +1 -1
  5. package/dist/embedded-postgres-recovery.js +33 -0
  6. package/dist/embedded-postgres-recovery.js.map +1 -1
  7. package/dist/embedded-postgres-recovery.test.js +19 -1
  8. package/dist/embedded-postgres-recovery.test.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/migration-runtime.d.ts.map +1 -1
  14. package/dist/migration-runtime.js +3 -20
  15. package/dist/migration-runtime.js.map +1 -1
  16. package/dist/migrations/0102_complex_retro_girl.sql +38 -0
  17. package/dist/migrations/0103_cute_colonel_america.sql +4 -0
  18. package/dist/migrations/0104_unusual_mister_fear.sql +173 -0
  19. package/dist/migrations/0105_chat_queue_actor_reconciliation.sql +54 -0
  20. package/dist/migrations/meta/0102_snapshot.json +19090 -0
  21. package/dist/migrations/meta/0103_snapshot.json +19114 -0
  22. package/dist/migrations/meta/0104_snapshot.json +20338 -0
  23. package/dist/migrations/meta/0105_snapshot.json +20338 -0
  24. package/dist/migrations/meta/_journal.json +29 -1
  25. package/dist/schema/activity_log.d.ts +17 -0
  26. package/dist/schema/activity_log.d.ts.map +1 -1
  27. package/dist/schema/activity_log.js +6 -1
  28. package/dist/schema/activity_log.js.map +1 -1
  29. package/dist/schema/agent_wakeup_requests.d.ts.map +1 -1
  30. package/dist/schema/agent_wakeup_requests.js +5 -1
  31. package/dist/schema/agent_wakeup_requests.js.map +1 -1
  32. package/dist/schema/chat_control_actions.d.ts +442 -0
  33. package/dist/schema/chat_control_actions.d.ts.map +1 -0
  34. package/dist/schema/chat_control_actions.js +35 -0
  35. package/dist/schema/chat_control_actions.js.map +1 -0
  36. package/dist/schema/chat_generation_events.d.ts +268 -0
  37. package/dist/schema/chat_generation_events.d.ts.map +1 -0
  38. package/dist/schema/chat_generation_events.js +32 -0
  39. package/dist/schema/chat_generation_events.js.map +1 -0
  40. package/dist/schema/chat_generation_terminal_outbox.d.ts +370 -0
  41. package/dist/schema/chat_generation_terminal_outbox.d.ts.map +1 -0
  42. package/dist/schema/chat_generation_terminal_outbox.js +33 -0
  43. package/dist/schema/chat_generation_terminal_outbox.js.map +1 -0
  44. package/dist/schema/chat_generations.d.ts +279 -2
  45. package/dist/schema/chat_generations.d.ts.map +1 -1
  46. package/dist/schema/chat_generations.js +23 -2
  47. package/dist/schema/chat_generations.js.map +1 -1
  48. package/dist/schema/chat_queued_messages.d.ts +285 -2
  49. package/dist/schema/chat_queued_messages.d.ts.map +1 -1
  50. package/dist/schema/chat_queued_messages.js +23 -1
  51. package/dist/schema/chat_queued_messages.js.map +1 -1
  52. package/dist/schema/cost_events.d.ts +34 -0
  53. package/dist/schema/cost_events.d.ts.map +1 -1
  54. package/dist/schema/cost_events.js +5 -1
  55. package/dist/schema/cost_events.js.map +1 -1
  56. package/dist/schema/heartbeat_run_events.d.ts +17 -0
  57. package/dist/schema/heartbeat_run_events.d.ts.map +1 -1
  58. package/dist/schema/heartbeat_run_events.js +6 -1
  59. package/dist/schema/heartbeat_run_events.js.map +1 -1
  60. package/dist/schema/heartbeat_runs.d.ts +303 -0
  61. package/dist/schema/heartbeat_runs.d.ts.map +1 -1
  62. package/dist/schema/heartbeat_runs.js +24 -2
  63. package/dist/schema/heartbeat_runs.js.map +1 -1
  64. package/dist/schema/index.d.ts +3 -0
  65. package/dist/schema/index.d.ts.map +1 -1
  66. package/dist/schema/index.js +3 -0
  67. package/dist/schema/index.js.map +1 -1
  68. package/package.json +2 -2
@@ -1,3 +1,5 @@
1
+ import { drizzle as drizzlePg } from "drizzle-orm/postgres-js";
2
+ import { migrate as migratePg } from "drizzle-orm/postgres-js/migrator";
1
3
  import { createHash } from "node:crypto";
2
4
  import fs from "node:fs";
3
5
  import net from "node:net";
@@ -62,6 +64,93 @@ async function migrationHash(migrationFile) {
62
64
  const content = await fs.promises.readFile(new URL(`./migrations/${migrationFile}`, import.meta.url), "utf8");
63
65
  return createHash("sha256").update(content).digest("hex");
64
66
  }
67
+ function createLegacyTerminalMigrationsFolder() {
68
+ const migrationsFolder = fs.mkdtempSync(path.join(os.tmpdir(), "rudder-legacy-terminal-migrations-"));
69
+ tempPaths.push(migrationsFolder);
70
+ fs.mkdirSync(path.join(migrationsFolder, "meta"));
71
+ const currentMigrationsUrl = new URL("./migrations/", import.meta.url);
72
+ const currentJournal = JSON.parse(fs.readFileSync(new URL("meta/_journal.json", currentMigrationsUrl), "utf8"));
73
+ const baseEntries = currentJournal.entries.filter((entry) => entry.idx < 102);
74
+ if (baseEntries.at(-1)?.tag !== "0101_bizarre_morlun") {
75
+ throw new Error("Legacy terminal migration fixture requires the 0101 base schema");
76
+ }
77
+ for (const entry of baseEntries) {
78
+ fs.copyFileSync(new URL(`${entry.tag}.sql`, currentMigrationsUrl), path.join(migrationsFolder, `${entry.tag}.sql`));
79
+ }
80
+ const fixtureUrl = new URL("./test-fixtures/legacy-terminal-state-migrations/", import.meta.url);
81
+ const manifest = JSON.parse(fs.readFileSync(new URL("manifest.json", fixtureUrl), "utf8"));
82
+ for (const entry of manifest.entries) {
83
+ const fixtureContent = fs.readFileSync(new URL(`${entry.tag}.sql`, fixtureUrl), "utf8");
84
+ const normalizedFixtureContent = fixtureContent.replaceAll("\r\n", "\n");
85
+ const historicalContent = normalizedFixtureContent.endsWith("\n")
86
+ ? normalizedFixtureContent.slice(0, -1)
87
+ : normalizedFixtureContent;
88
+ const actualHash = createHash("sha256").update(historicalContent).digest("hex");
89
+ if (actualHash !== entry.sha256) {
90
+ throw new Error(`Legacy migration fixture hash mismatch for ${entry.tag}: expected ${entry.sha256}, received ${actualHash}`);
91
+ }
92
+ fs.writeFileSync(path.join(migrationsFolder, `${entry.tag}.sql`), historicalContent);
93
+ }
94
+ fs.writeFileSync(path.join(migrationsFolder, "meta", "_journal.json"), JSON.stringify({
95
+ version: currentJournal.version,
96
+ dialect: currentJournal.dialect,
97
+ entries: [
98
+ ...baseEntries,
99
+ ...manifest.entries.map(({ sha256: _sha256, ...entry }) => entry),
100
+ ],
101
+ }));
102
+ return { manifest, migrationsFolder };
103
+ }
104
+ function createLegacyChatRuntimeControlsMigrationsFolder() {
105
+ const migrationsFolder = fs.mkdtempSync(path.join(os.tmpdir(), "rudder-legacy-chat-controls-migrations-"));
106
+ tempPaths.push(migrationsFolder);
107
+ fs.mkdirSync(path.join(migrationsFolder, "meta"));
108
+ const currentMigrationsUrl = new URL("./migrations/", import.meta.url);
109
+ const currentJournal = JSON.parse(fs.readFileSync(new URL("meta/_journal.json", currentMigrationsUrl), "utf8"));
110
+ const baseEntries = currentJournal.entries.filter((entry) => entry.idx < 102);
111
+ if (baseEntries.at(-1)?.tag !== "0101_bizarre_morlun") {
112
+ throw new Error("Legacy chat control migration fixture requires the 0101 base schema");
113
+ }
114
+ for (const entry of baseEntries) {
115
+ fs.copyFileSync(new URL(`${entry.tag}.sql`, currentMigrationsUrl), path.join(migrationsFolder, `${entry.tag}.sql`));
116
+ }
117
+ const historicalContent = fs.readFileSync(new URL("0104_unusual_mister_fear.sql", currentMigrationsUrl), "utf8");
118
+ const historicalHash = createHash("sha256").update(historicalContent).digest("hex");
119
+ if (historicalHash !== "a1fc0446af5ec1640890bb9cf36208eab8dce6687c233029bd54e179613e1af7") {
120
+ throw new Error(`Legacy chat runtime controls fixture hash changed: ${historicalHash}`);
121
+ }
122
+ const legacyEntry = {
123
+ idx: 102,
124
+ version: "7",
125
+ when: 1784159513933,
126
+ tag: "0102_chat_runtime_controls",
127
+ breakpoints: true,
128
+ };
129
+ fs.writeFileSync(path.join(migrationsFolder, `${legacyEntry.tag}.sql`), historicalContent);
130
+ fs.writeFileSync(path.join(migrationsFolder, "meta", "_journal.json"), JSON.stringify({
131
+ version: currentJournal.version,
132
+ dialect: currentJournal.dialect,
133
+ entries: [...baseEntries, legacyEntry],
134
+ }));
135
+ return { historicalHash, migrationsFolder };
136
+ }
137
+ function createCurrentMigrationsFolderThrough(maxIdx) {
138
+ const migrationsFolder = fs.mkdtempSync(path.join(os.tmpdir(), `rudder-migrations-through-${maxIdx}-`));
139
+ tempPaths.push(migrationsFolder);
140
+ fs.mkdirSync(path.join(migrationsFolder, "meta"));
141
+ const currentMigrationsUrl = new URL("./migrations/", import.meta.url);
142
+ const currentJournal = JSON.parse(fs.readFileSync(new URL("meta/_journal.json", currentMigrationsUrl), "utf8"));
143
+ const entries = currentJournal.entries.filter((entry) => entry.idx <= maxIdx);
144
+ for (const entry of entries) {
145
+ fs.copyFileSync(new URL(`${entry.tag}.sql`, currentMigrationsUrl), path.join(migrationsFolder, `${entry.tag}.sql`));
146
+ }
147
+ fs.writeFileSync(path.join(migrationsFolder, "meta", "_journal.json"), JSON.stringify({
148
+ version: currentJournal.version,
149
+ dialect: currentJournal.dialect,
150
+ entries,
151
+ }));
152
+ return migrationsFolder;
153
+ }
65
154
  afterEach(async () => {
66
155
  while (runningInstances.length > 0) {
67
156
  const instance = runningInstances.pop();
@@ -245,6 +334,544 @@ describe("applyPendingMigrations", () => {
245
334
  await verifySql.end();
246
335
  }
247
336
  }, 20_000);
337
+ it("reapplies consolidated migration 0102 over the legacy 0102-0104 feature shape", async () => {
338
+ const connectionString = await createTempDatabase();
339
+ const { manifest, migrationsFolder } = createLegacyTerminalMigrationsFolder();
340
+ expect(manifest.sourceCommit).toBe("0491a75db25a24e644e67a502239fd1209840f40");
341
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
342
+ try {
343
+ await migratePg(drizzlePg(sql), { migrationsFolder });
344
+ const legacyHistory = await sql.unsafe(`
345
+ SELECT hash, created_at
346
+ FROM "drizzle"."__drizzle_migrations"
347
+ ORDER BY created_at DESC
348
+ LIMIT 3
349
+ `);
350
+ expect(legacyHistory.map((row) => ({
351
+ hash: row.hash,
352
+ createdAt: Number(row.created_at),
353
+ }))).toEqual([...manifest.entries].reverse().map((entry) => ({
354
+ hash: entry.sha256,
355
+ createdAt: entry.when,
356
+ })));
357
+ const legacyColumns = await sql.unsafe(`
358
+ SELECT table_name, column_name
359
+ FROM information_schema.columns
360
+ WHERE table_schema = 'public'
361
+ AND table_name IN ('heartbeat_runs', 'cost_events')
362
+ AND column_name IN (
363
+ 'terminal_effects_pending',
364
+ 'process_exited_at',
365
+ 'terminal_effects_json',
366
+ 'terminal_effects_last_error',
367
+ 'budget_evaluated_at',
368
+ 'execution_owner_token',
369
+ 'execution_lease_expires_at',
370
+ 'terminal_effects_completed_json'
371
+ )
372
+ ORDER BY table_name, column_name
373
+ `);
374
+ expect(legacyColumns).toEqual([
375
+ { table_name: "cost_events", column_name: "budget_evaluated_at" },
376
+ { table_name: "heartbeat_runs", column_name: "process_exited_at" },
377
+ { table_name: "heartbeat_runs", column_name: "terminal_effects_json" },
378
+ { table_name: "heartbeat_runs", column_name: "terminal_effects_last_error" },
379
+ { table_name: "heartbeat_runs", column_name: "terminal_effects_pending" },
380
+ ]);
381
+ }
382
+ finally {
383
+ await sql.end();
384
+ }
385
+ const pendingState = await inspectMigrations(connectionString);
386
+ expect(pendingState).toMatchObject({
387
+ status: "needsMigrations",
388
+ pendingMigrations: [
389
+ "0055_illegal_sheva_callister.sql",
390
+ "0102_complex_retro_girl.sql",
391
+ "0103_cute_colonel_america.sql",
392
+ "0104_unusual_mister_fear.sql",
393
+ "0105_chat_queue_actor_reconciliation.sql",
394
+ ],
395
+ reason: "pending-migrations",
396
+ });
397
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
398
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
399
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
400
+ try {
401
+ const consolidatedHash = await migrationHash("0102_complex_retro_girl.sql");
402
+ const consolidatedHistory = await verifySql.unsafe(`
403
+ SELECT hash
404
+ FROM "drizzle"."__drizzle_migrations"
405
+ WHERE hash = '${consolidatedHash}'
406
+ `);
407
+ expect(consolidatedHistory).toEqual([{ hash: consolidatedHash }]);
408
+ const columns = await verifySql.unsafe(`
409
+ SELECT column_name
410
+ FROM information_schema.columns
411
+ WHERE table_schema = 'public'
412
+ AND table_name = 'heartbeat_runs'
413
+ AND column_name IN (
414
+ 'execution_owner_token',
415
+ 'execution_lease_expires_at',
416
+ 'session_params_after_json',
417
+ 'session_params_before_json',
418
+ 'session_reuse_scope',
419
+ 'terminal_effects_completed_json',
420
+ 'terminal_effects_last_error'
421
+ )
422
+ ORDER BY column_name
423
+ `);
424
+ expect(columns.map((row) => row.column_name)).toEqual([
425
+ "execution_lease_expires_at",
426
+ "execution_owner_token",
427
+ "session_params_after_json",
428
+ "session_params_before_json",
429
+ "session_reuse_scope",
430
+ "terminal_effects_completed_json",
431
+ "terminal_effects_last_error",
432
+ ]);
433
+ const indexes = await verifySql.unsafe(`
434
+ SELECT indexname
435
+ FROM pg_indexes
436
+ WHERE schemaname = 'public'
437
+ AND indexname IN (
438
+ 'heartbeat_runs_active_chat_conversation_uq',
439
+ 'heartbeat_runs_status_execution_lease_created_idx',
440
+ 'heartbeat_run_events_run_idempotency_key_uq'
441
+ )
442
+ ORDER BY indexname
443
+ `);
444
+ expect(indexes.map((row) => row.indexname)).toEqual([
445
+ "heartbeat_run_events_run_idempotency_key_uq",
446
+ "heartbeat_runs_active_chat_conversation_uq",
447
+ "heartbeat_runs_status_execution_lease_created_idx",
448
+ ]);
449
+ }
450
+ finally {
451
+ await verifySql.end();
452
+ }
453
+ }, 20_000);
454
+ it("upgrades databases that applied the legacy 0102 chat runtime controls migration", async () => {
455
+ const connectionString = await createTempDatabase();
456
+ const { historicalHash, migrationsFolder } = createLegacyChatRuntimeControlsMigrationsFolder();
457
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
458
+ try {
459
+ await migratePg(drizzlePg(sql), { migrationsFolder });
460
+ await sql.unsafe(`
461
+ INSERT INTO "organizations" ("id", "url_key", "name", "issue_prefix")
462
+ VALUES (
463
+ '00000000-0000-0000-0000-000000000122',
464
+ 'legacy-chat-controls',
465
+ 'Legacy Chat Controls',
466
+ 'LCC'
467
+ )
468
+ `);
469
+ await sql.unsafe(`
470
+ INSERT INTO "chat_conversations" ("id", "org_id", "title")
471
+ VALUES (
472
+ '00000000-0000-0000-0000-000000000123',
473
+ '00000000-0000-0000-0000-000000000122',
474
+ 'Legacy chat runtime controls'
475
+ )
476
+ `);
477
+ await sql.unsafe(`
478
+ INSERT INTO "chat_queued_messages" (
479
+ "id",
480
+ "org_id",
481
+ "conversation_id",
482
+ "position",
483
+ "status",
484
+ "client_mutation_id",
485
+ "payload",
486
+ "delivery_intent",
487
+ "delivery_disposition"
488
+ )
489
+ VALUES (
490
+ '00000000-0000-0000-0000-000000000124',
491
+ '00000000-0000-0000-0000-000000000122',
492
+ '00000000-0000-0000-0000-000000000123',
493
+ 1,
494
+ 'continuation_pending',
495
+ 'legacy-wip-steer',
496
+ '{"body":"legacy WIP steer"}'::jsonb,
497
+ 'steer',
498
+ 'continuation_pending'
499
+ )
500
+ `);
501
+ }
502
+ finally {
503
+ await sql.end();
504
+ }
505
+ const pendingState = await inspectMigrations(connectionString);
506
+ expect(pendingState).toMatchObject({
507
+ status: "needsMigrations",
508
+ pendingMigrations: [
509
+ "0055_illegal_sheva_callister.sql",
510
+ "0102_complex_retro_girl.sql",
511
+ "0103_cute_colonel_america.sql",
512
+ "0105_chat_queue_actor_reconciliation.sql",
513
+ ],
514
+ reason: "pending-migrations",
515
+ });
516
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
517
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
518
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
519
+ try {
520
+ const legacyHistory = await verifySql.unsafe(`
521
+ SELECT hash
522
+ FROM "drizzle"."__drizzle_migrations"
523
+ WHERE hash = '${historicalHash}'
524
+ `);
525
+ expect(legacyHistory).toEqual([{ hash: historicalHash }]);
526
+ const [queueItem] = await verifySql.unsafe(`
527
+ SELECT
528
+ status,
529
+ delivery_intent,
530
+ delivery_disposition,
531
+ request_actor,
532
+ reconciliation_reason
533
+ FROM "chat_queued_messages"
534
+ WHERE "id" = '00000000-0000-0000-0000-000000000124'
535
+ `);
536
+ expect(queueItem).toEqual({
537
+ status: "queued",
538
+ delivery_intent: "queue",
539
+ delivery_disposition: null,
540
+ request_actor: null,
541
+ reconciliation_reason: "legacy_request_actor_unavailable",
542
+ });
543
+ }
544
+ finally {
545
+ await verifySql.end();
546
+ }
547
+ }, 20_000);
548
+ it("upgrades 0102 heartbeat runs with durable session lineage columns", async () => {
549
+ const connectionString = await createTempDatabase();
550
+ const migrationsThrough0102 = createCurrentMigrationsFolderThrough(102);
551
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
552
+ try {
553
+ await migratePg(drizzlePg(sql), { migrationsFolder: migrationsThrough0102 });
554
+ await sql.unsafe(`
555
+ INSERT INTO "organizations" ("id", "url_key", "name", "issue_prefix")
556
+ VALUES ('00000000-0000-0000-0000-000000000103', 'session-lineage', 'Session Lineage', 'SLG')
557
+ `);
558
+ await sql.unsafe(`
559
+ INSERT INTO "agents" ("id", "org_id", "name")
560
+ VALUES (
561
+ '00000000-0000-0000-0000-000000000104',
562
+ '00000000-0000-0000-0000-000000000103',
563
+ 'Migration Agent'
564
+ )
565
+ `);
566
+ await sql.unsafe(`
567
+ INSERT INTO "heartbeat_runs" ("id", "org_id", "agent_id")
568
+ VALUES (
569
+ '00000000-0000-0000-0000-000000000105',
570
+ '00000000-0000-0000-0000-000000000103',
571
+ '00000000-0000-0000-0000-000000000104'
572
+ )
573
+ `);
574
+ }
575
+ finally {
576
+ await sql.end();
577
+ }
578
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
579
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
580
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
581
+ try {
582
+ const [legacyRun] = await verifySql.unsafe(`
583
+ SELECT session_params_before_json, session_params_after_json, session_reuse_scope
584
+ FROM "heartbeat_runs"
585
+ WHERE "id" = '00000000-0000-0000-0000-000000000105'
586
+ `);
587
+ expect(legacyRun).toEqual({
588
+ session_params_before_json: null,
589
+ session_params_after_json: null,
590
+ session_reuse_scope: "unknown",
591
+ });
592
+ for (const scope of ["explicit", "task", "none", "unknown"]) {
593
+ await expect(verifySql.unsafe(`
594
+ UPDATE "heartbeat_runs"
595
+ SET "session_reuse_scope" = '${scope}',
596
+ "session_params_before_json" = '{"sessionId":"before"}'::jsonb,
597
+ "session_params_after_json" = '{"sessionId":"after"}'::jsonb
598
+ WHERE "id" = '00000000-0000-0000-0000-000000000105'
599
+ `)).resolves.toBeDefined();
600
+ }
601
+ await expect(verifySql.unsafe(`
602
+ UPDATE "heartbeat_runs"
603
+ SET "session_reuse_scope" = 'global'
604
+ WHERE "id" = '00000000-0000-0000-0000-000000000105'
605
+ `)).rejects.toMatchObject({ code: "23514" });
606
+ }
607
+ finally {
608
+ await verifySql.end();
609
+ }
610
+ }, 20_000);
611
+ it("reconciles legacy chat control states before creating active-generation constraints", async () => {
612
+ const connectionString = await createTempDatabase();
613
+ const migrationsThrough0103 = createCurrentMigrationsFolderThrough(103);
614
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
615
+ try {
616
+ await migratePg(drizzlePg(sql), { migrationsFolder: migrationsThrough0103 });
617
+ await sql.unsafe(`
618
+ INSERT INTO "organizations" ("id", "url_key", "name", "issue_prefix")
619
+ VALUES (
620
+ '00000000-0000-0000-0000-000000000110',
621
+ 'chat-control-migration',
622
+ 'Chat Control Migration',
623
+ 'CCM'
624
+ )
625
+ `);
626
+ await sql.unsafe(`
627
+ INSERT INTO "agents" ("id", "org_id", "name")
628
+ VALUES (
629
+ '00000000-0000-0000-0000-000000000119',
630
+ '00000000-0000-0000-0000-000000000110',
631
+ 'Legacy queue agent'
632
+ )
633
+ `);
634
+ await sql.unsafe(`
635
+ INSERT INTO "chat_conversations" ("id", "org_id", "title")
636
+ VALUES (
637
+ '00000000-0000-0000-0000-000000000111',
638
+ '00000000-0000-0000-0000-000000000110',
639
+ 'Legacy chat control state'
640
+ )
641
+ `);
642
+ await sql.unsafe(`
643
+ INSERT INTO "chat_generations" ("id", "org_id", "conversation_id", "status")
644
+ VALUES
645
+ (
646
+ '00000000-0000-0000-0000-000000000112',
647
+ '00000000-0000-0000-0000-000000000110',
648
+ '00000000-0000-0000-0000-000000000111',
649
+ 'active'
650
+ ),
651
+ (
652
+ '00000000-0000-0000-0000-000000000113',
653
+ '00000000-0000-0000-0000-000000000110',
654
+ '00000000-0000-0000-0000-000000000111',
655
+ 'running'
656
+ )
657
+ `);
658
+ await sql.unsafe(`
659
+ INSERT INTO "chat_queued_messages" (
660
+ "id",
661
+ "org_id",
662
+ "conversation_id",
663
+ "position",
664
+ "status",
665
+ "client_mutation_id",
666
+ "payload",
667
+ "last_delivery_reason"
668
+ )
669
+ VALUES
670
+ (
671
+ '00000000-0000-0000-0000-000000000114',
672
+ '00000000-0000-0000-0000-000000000110',
673
+ '00000000-0000-0000-0000-000000000111',
674
+ 1,
675
+ 'steer_pending',
676
+ 'legacy-steer',
677
+ '{"body":"steer"}'::jsonb,
678
+ 'unsupported'
679
+ ),
680
+ (
681
+ '00000000-0000-0000-0000-000000000115',
682
+ '00000000-0000-0000-0000-000000000110',
683
+ '00000000-0000-0000-0000-000000000111',
684
+ 2,
685
+ 'dequeue_claimed',
686
+ 'legacy-claim',
687
+ '{"body":"claim"}'::jsonb,
688
+ NULL
689
+ ),
690
+ (
691
+ '00000000-0000-0000-0000-000000000116',
692
+ '00000000-0000-0000-0000-000000000110',
693
+ '00000000-0000-0000-0000-000000000111',
694
+ 3,
695
+ 'running',
696
+ 'legacy-running',
697
+ '{"body":"running"}'::jsonb,
698
+ NULL
699
+ ),
700
+ (
701
+ '00000000-0000-0000-0000-000000000117',
702
+ '00000000-0000-0000-0000-000000000110',
703
+ '00000000-0000-0000-0000-000000000111',
704
+ 4,
705
+ 'queued',
706
+ 'legacy-actorless-queue',
707
+ '{"body":"actorless queue"}'::jsonb,
708
+ NULL
709
+ ),
710
+ (
711
+ '00000000-0000-0000-0000-000000000118',
712
+ '00000000-0000-0000-0000-000000000110',
713
+ '00000000-0000-0000-0000-000000000111',
714
+ 5,
715
+ 'steer_pending',
716
+ 'legacy-actorless-steer',
717
+ '{"body":"actorless steer"}'::jsonb,
718
+ 'unsupported'
719
+ )
720
+ `);
721
+ await sql.unsafe(`
722
+ INSERT INTO "activity_log" (
723
+ "id",
724
+ "org_id",
725
+ "actor_type",
726
+ "actor_id",
727
+ "action",
728
+ "entity_type",
729
+ "entity_id",
730
+ "agent_id",
731
+ "details"
732
+ )
733
+ VALUES
734
+ (
735
+ '00000000-0000-0000-0000-000000000120',
736
+ '00000000-0000-0000-0000-000000000110',
737
+ 'user',
738
+ 'legacy-board-user',
739
+ 'chat.queue.created',
740
+ 'chat',
741
+ '00000000-0000-0000-0000-000000000111',
742
+ NULL,
743
+ '{"queuedMessageId":"00000000-0000-0000-0000-000000000114"}'::jsonb
744
+ ),
745
+ (
746
+ '00000000-0000-0000-0000-000000000121',
747
+ '00000000-0000-0000-0000-000000000110',
748
+ 'agent',
749
+ '00000000-0000-0000-0000-000000000119',
750
+ 'chat.queue.created',
751
+ 'chat',
752
+ '00000000-0000-0000-0000-000000000111',
753
+ '00000000-0000-0000-0000-000000000119',
754
+ '{"queuedMessageId":"00000000-0000-0000-0000-000000000115"}'::jsonb
755
+ )
756
+ `);
757
+ }
758
+ finally {
759
+ await sql.end();
760
+ }
761
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
762
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
763
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
764
+ try {
765
+ const generations = await verifySql.unsafe(`
766
+ SELECT
767
+ status,
768
+ terminal_reason,
769
+ control_state,
770
+ runtime_terminal_at IS NOT NULL AS runtime_terminal_recorded,
771
+ completed_at IS NOT NULL AS completed_recorded
772
+ FROM "chat_generations"
773
+ WHERE "conversation_id" = '00000000-0000-0000-0000-000000000111'
774
+ ORDER BY "id"
775
+ `);
776
+ expect(generations).toEqual([
777
+ {
778
+ status: "aborted",
779
+ terminal_reason: "ownerless_during_durable_control_migration",
780
+ control_state: "terminal",
781
+ runtime_terminal_recorded: true,
782
+ completed_recorded: true,
783
+ },
784
+ {
785
+ status: "aborted",
786
+ terminal_reason: "ownerless_during_durable_control_migration",
787
+ control_state: "terminal",
788
+ runtime_terminal_recorded: true,
789
+ completed_recorded: true,
790
+ },
791
+ ]);
792
+ const queueItems = await verifySql.unsafe(`
793
+ SELECT
794
+ position,
795
+ status,
796
+ delivery_intent,
797
+ delivery_disposition,
798
+ request_actor,
799
+ reconciliation_reason,
800
+ last_delivery_reason
801
+ FROM "chat_queued_messages"
802
+ WHERE "conversation_id" = '00000000-0000-0000-0000-000000000111'
803
+ ORDER BY "position"
804
+ `);
805
+ expect(queueItems).toEqual([
806
+ {
807
+ position: 1,
808
+ status: "continuation_pending",
809
+ delivery_intent: "steer",
810
+ delivery_disposition: "continuation_pending",
811
+ request_actor: {
812
+ type: "board",
813
+ source: "session",
814
+ userId: "legacy-board-user",
815
+ orgIds: ["00000000-0000-0000-0000-000000000110"],
816
+ isInstanceAdmin: false,
817
+ },
818
+ reconciliation_reason: "legacy_steer_recovered_on_upgrade",
819
+ last_delivery_reason: null,
820
+ },
821
+ {
822
+ position: 2,
823
+ status: "queued",
824
+ delivery_intent: "queue",
825
+ delivery_disposition: null,
826
+ request_actor: {
827
+ type: "agent",
828
+ source: "agent_key",
829
+ orgId: "00000000-0000-0000-0000-000000000110",
830
+ agentId: "00000000-0000-0000-0000-000000000119",
831
+ },
832
+ reconciliation_reason: "legacy_claim_released_on_upgrade",
833
+ last_delivery_reason: "legacy_claim_released_on_upgrade",
834
+ },
835
+ {
836
+ position: 3,
837
+ status: "failed_actionable",
838
+ delivery_intent: "queue",
839
+ delivery_disposition: "failed_actionable",
840
+ request_actor: null,
841
+ reconciliation_reason: "legacy_running_delivery_unconfirmed_on_upgrade",
842
+ last_delivery_reason: "legacy_running_delivery_unconfirmed_on_upgrade",
843
+ },
844
+ {
845
+ position: 4,
846
+ status: "queued",
847
+ delivery_intent: "queue",
848
+ delivery_disposition: null,
849
+ request_actor: null,
850
+ reconciliation_reason: "legacy_request_actor_unavailable",
851
+ last_delivery_reason: "legacy_request_actor_unavailable",
852
+ },
853
+ {
854
+ position: 5,
855
+ status: "queued",
856
+ delivery_intent: "queue",
857
+ delivery_disposition: null,
858
+ request_actor: null,
859
+ reconciliation_reason: "legacy_request_actor_unavailable",
860
+ last_delivery_reason: "legacy_request_actor_unavailable",
861
+ },
862
+ ]);
863
+ const activeIndex = await verifySql.unsafe(`
864
+ SELECT indexname
865
+ FROM pg_indexes
866
+ WHERE schemaname = 'public'
867
+ AND indexname = 'chat_generations_active_conversation_uq'
868
+ `);
869
+ expect(activeIndex).toEqual([{ indexname: "chat_generations_active_conversation_uq" }]);
870
+ }
871
+ finally {
872
+ await verifySql.end();
873
+ }
874
+ }, 20_000);
248
875
  it("rolls back migration 0100 when a legacy alias conflicts with another organization route", async () => {
249
876
  const connectionString = await createTempDatabase();
250
877
  await applyPendingMigrations(connectionString);