@rudderhq/db 0.4.6-canary.9 → 0.5.1-canary.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 (84) hide show
  1. package/dist/client.test.js +633 -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/0106_damp_amphibian.sql +9 -0
  21. package/dist/migrations/0107_daffy_luke_cage.sql +20 -0
  22. package/dist/migrations/0108_atomic_chat_first_turn_cleanup.sql +74 -0
  23. package/dist/migrations/atomic-chat-first-turn-cleanup.test.ts +18 -0
  24. package/dist/migrations/meta/0102_snapshot.json +19090 -0
  25. package/dist/migrations/meta/0103_snapshot.json +19114 -0
  26. package/dist/migrations/meta/0104_snapshot.json +20338 -0
  27. package/dist/migrations/meta/0105_snapshot.json +20338 -0
  28. package/dist/migrations/meta/0106_snapshot.json +20442 -0
  29. package/dist/migrations/meta/0107_snapshot.json +20646 -0
  30. package/dist/migrations/meta/_journal.json +50 -1
  31. package/dist/schema/activity_log.d.ts +17 -0
  32. package/dist/schema/activity_log.d.ts.map +1 -1
  33. package/dist/schema/activity_log.js +6 -1
  34. package/dist/schema/activity_log.js.map +1 -1
  35. package/dist/schema/agent_wakeup_requests.d.ts.map +1 -1
  36. package/dist/schema/agent_wakeup_requests.js +5 -1
  37. package/dist/schema/agent_wakeup_requests.js.map +1 -1
  38. package/dist/schema/chat_control_actions.d.ts +442 -0
  39. package/dist/schema/chat_control_actions.d.ts.map +1 -0
  40. package/dist/schema/chat_control_actions.js +35 -0
  41. package/dist/schema/chat_control_actions.js.map +1 -0
  42. package/dist/schema/chat_conversations.d.ts +119 -0
  43. package/dist/schema/chat_conversations.d.ts.map +1 -1
  44. package/dist/schema/chat_conversations.js +10 -1
  45. package/dist/schema/chat_conversations.js.map +1 -1
  46. package/dist/schema/chat_generation_events.d.ts +268 -0
  47. package/dist/schema/chat_generation_events.d.ts.map +1 -0
  48. package/dist/schema/chat_generation_events.js +32 -0
  49. package/dist/schema/chat_generation_events.js.map +1 -0
  50. package/dist/schema/chat_generation_terminal_outbox.d.ts +370 -0
  51. package/dist/schema/chat_generation_terminal_outbox.d.ts.map +1 -0
  52. package/dist/schema/chat_generation_terminal_outbox.js +33 -0
  53. package/dist/schema/chat_generation_terminal_outbox.js.map +1 -0
  54. package/dist/schema/chat_generations.d.ts +279 -2
  55. package/dist/schema/chat_generations.d.ts.map +1 -1
  56. package/dist/schema/chat_generations.js +23 -2
  57. package/dist/schema/chat_generations.js.map +1 -1
  58. package/dist/schema/chat_queued_messages.d.ts +285 -2
  59. package/dist/schema/chat_queued_messages.d.ts.map +1 -1
  60. package/dist/schema/chat_queued_messages.js +23 -1
  61. package/dist/schema/chat_queued_messages.js.map +1 -1
  62. package/dist/schema/cost_events.d.ts +34 -0
  63. package/dist/schema/cost_events.d.ts.map +1 -1
  64. package/dist/schema/cost_events.js +5 -1
  65. package/dist/schema/cost_events.js.map +1 -1
  66. package/dist/schema/heartbeat_run_events.d.ts +17 -0
  67. package/dist/schema/heartbeat_run_events.d.ts.map +1 -1
  68. package/dist/schema/heartbeat_run_events.js +6 -1
  69. package/dist/schema/heartbeat_run_events.js.map +1 -1
  70. package/dist/schema/heartbeat_runs.d.ts +303 -0
  71. package/dist/schema/heartbeat_runs.d.ts.map +1 -1
  72. package/dist/schema/heartbeat_runs.js +24 -2
  73. package/dist/schema/heartbeat_runs.js.map +1 -1
  74. package/dist/schema/index.d.ts +4 -0
  75. package/dist/schema/index.d.ts.map +1 -1
  76. package/dist/schema/index.js +4 -0
  77. package/dist/schema/index.js.map +1 -1
  78. package/dist/schema/messenger_saved_views.d.ts +234 -0
  79. package/dist/schema/messenger_saved_views.d.ts.map +1 -0
  80. package/dist/schema/messenger_saved_views.js +27 -0
  81. package/dist/schema/messenger_saved_views.js.map +1 -0
  82. package/dist/seed.js +2 -2
  83. package/dist/seed.js.map +1 -1
  84. 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,550 @@ 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
+ "0106_damp_amphibian.sql",
395
+ "0107_daffy_luke_cage.sql",
396
+ "0108_atomic_chat_first_turn_cleanup.sql",
397
+ ],
398
+ reason: "pending-migrations",
399
+ });
400
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
401
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
402
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
403
+ try {
404
+ const consolidatedHash = await migrationHash("0102_complex_retro_girl.sql");
405
+ const consolidatedHistory = await verifySql.unsafe(`
406
+ SELECT hash
407
+ FROM "drizzle"."__drizzle_migrations"
408
+ WHERE hash = '${consolidatedHash}'
409
+ `);
410
+ expect(consolidatedHistory).toEqual([{ hash: consolidatedHash }]);
411
+ const columns = await verifySql.unsafe(`
412
+ SELECT column_name
413
+ FROM information_schema.columns
414
+ WHERE table_schema = 'public'
415
+ AND table_name = 'heartbeat_runs'
416
+ AND column_name IN (
417
+ 'execution_owner_token',
418
+ 'execution_lease_expires_at',
419
+ 'session_params_after_json',
420
+ 'session_params_before_json',
421
+ 'session_reuse_scope',
422
+ 'terminal_effects_completed_json',
423
+ 'terminal_effects_last_error'
424
+ )
425
+ ORDER BY column_name
426
+ `);
427
+ expect(columns.map((row) => row.column_name)).toEqual([
428
+ "execution_lease_expires_at",
429
+ "execution_owner_token",
430
+ "session_params_after_json",
431
+ "session_params_before_json",
432
+ "session_reuse_scope",
433
+ "terminal_effects_completed_json",
434
+ "terminal_effects_last_error",
435
+ ]);
436
+ const indexes = await verifySql.unsafe(`
437
+ SELECT indexname
438
+ FROM pg_indexes
439
+ WHERE schemaname = 'public'
440
+ AND indexname IN (
441
+ 'heartbeat_runs_active_chat_conversation_uq',
442
+ 'heartbeat_runs_status_execution_lease_created_idx',
443
+ 'heartbeat_run_events_run_idempotency_key_uq'
444
+ )
445
+ ORDER BY indexname
446
+ `);
447
+ expect(indexes.map((row) => row.indexname)).toEqual([
448
+ "heartbeat_run_events_run_idempotency_key_uq",
449
+ "heartbeat_runs_active_chat_conversation_uq",
450
+ "heartbeat_runs_status_execution_lease_created_idx",
451
+ ]);
452
+ }
453
+ finally {
454
+ await verifySql.end();
455
+ }
456
+ }, 20_000);
457
+ it("upgrades databases that applied the legacy 0102 chat runtime controls migration", async () => {
458
+ const connectionString = await createTempDatabase();
459
+ const { historicalHash, migrationsFolder } = createLegacyChatRuntimeControlsMigrationsFolder();
460
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
461
+ try {
462
+ await migratePg(drizzlePg(sql), { migrationsFolder });
463
+ await sql.unsafe(`
464
+ INSERT INTO "organizations" ("id", "url_key", "name", "issue_prefix")
465
+ VALUES (
466
+ '00000000-0000-0000-0000-000000000122',
467
+ 'legacy-chat-controls',
468
+ 'Legacy Chat Controls',
469
+ 'LCC'
470
+ )
471
+ `);
472
+ await sql.unsafe(`
473
+ INSERT INTO "chat_conversations" ("id", "org_id", "title")
474
+ VALUES (
475
+ '00000000-0000-0000-0000-000000000123',
476
+ '00000000-0000-0000-0000-000000000122',
477
+ 'Legacy chat runtime controls'
478
+ )
479
+ `);
480
+ await sql.unsafe(`
481
+ INSERT INTO "chat_queued_messages" (
482
+ "id",
483
+ "org_id",
484
+ "conversation_id",
485
+ "position",
486
+ "status",
487
+ "client_mutation_id",
488
+ "payload",
489
+ "delivery_intent",
490
+ "delivery_disposition"
491
+ )
492
+ VALUES (
493
+ '00000000-0000-0000-0000-000000000124',
494
+ '00000000-0000-0000-0000-000000000122',
495
+ '00000000-0000-0000-0000-000000000123',
496
+ 1,
497
+ 'continuation_pending',
498
+ 'legacy-wip-steer',
499
+ '{"body":"legacy WIP steer"}'::jsonb,
500
+ 'steer',
501
+ 'continuation_pending'
502
+ )
503
+ `);
504
+ }
505
+ finally {
506
+ await sql.end();
507
+ }
508
+ const pendingState = await inspectMigrations(connectionString);
509
+ expect(pendingState).toMatchObject({
510
+ status: "needsMigrations",
511
+ pendingMigrations: [
512
+ "0055_illegal_sheva_callister.sql",
513
+ "0102_complex_retro_girl.sql",
514
+ "0103_cute_colonel_america.sql",
515
+ "0105_chat_queue_actor_reconciliation.sql",
516
+ "0106_damp_amphibian.sql",
517
+ "0107_daffy_luke_cage.sql",
518
+ "0108_atomic_chat_first_turn_cleanup.sql",
519
+ ],
520
+ reason: "pending-migrations",
521
+ });
522
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
523
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
524
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
525
+ try {
526
+ const legacyHistory = await verifySql.unsafe(`
527
+ SELECT hash
528
+ FROM "drizzle"."__drizzle_migrations"
529
+ WHERE hash = '${historicalHash}'
530
+ `);
531
+ expect(legacyHistory).toEqual([{ hash: historicalHash }]);
532
+ const [queueItem] = await verifySql.unsafe(`
533
+ SELECT
534
+ status,
535
+ delivery_intent,
536
+ delivery_disposition,
537
+ request_actor,
538
+ reconciliation_reason
539
+ FROM "chat_queued_messages"
540
+ WHERE "id" = '00000000-0000-0000-0000-000000000124'
541
+ `);
542
+ expect(queueItem).toEqual({
543
+ status: "queued",
544
+ delivery_intent: "queue",
545
+ delivery_disposition: null,
546
+ request_actor: null,
547
+ reconciliation_reason: "legacy_request_actor_unavailable",
548
+ });
549
+ }
550
+ finally {
551
+ await verifySql.end();
552
+ }
553
+ }, 20_000);
554
+ it("upgrades 0102 heartbeat runs with durable session lineage columns", async () => {
555
+ const connectionString = await createTempDatabase();
556
+ const migrationsThrough0102 = createCurrentMigrationsFolderThrough(102);
557
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
558
+ try {
559
+ await migratePg(drizzlePg(sql), { migrationsFolder: migrationsThrough0102 });
560
+ await sql.unsafe(`
561
+ INSERT INTO "organizations" ("id", "url_key", "name", "issue_prefix")
562
+ VALUES ('00000000-0000-0000-0000-000000000103', 'session-lineage', 'Session Lineage', 'SLG')
563
+ `);
564
+ await sql.unsafe(`
565
+ INSERT INTO "agents" ("id", "org_id", "name")
566
+ VALUES (
567
+ '00000000-0000-0000-0000-000000000104',
568
+ '00000000-0000-0000-0000-000000000103',
569
+ 'Migration Agent'
570
+ )
571
+ `);
572
+ await sql.unsafe(`
573
+ INSERT INTO "heartbeat_runs" ("id", "org_id", "agent_id")
574
+ VALUES (
575
+ '00000000-0000-0000-0000-000000000105',
576
+ '00000000-0000-0000-0000-000000000103',
577
+ '00000000-0000-0000-0000-000000000104'
578
+ )
579
+ `);
580
+ }
581
+ finally {
582
+ await sql.end();
583
+ }
584
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
585
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
586
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
587
+ try {
588
+ const [legacyRun] = await verifySql.unsafe(`
589
+ SELECT session_params_before_json, session_params_after_json, session_reuse_scope
590
+ FROM "heartbeat_runs"
591
+ WHERE "id" = '00000000-0000-0000-0000-000000000105'
592
+ `);
593
+ expect(legacyRun).toEqual({
594
+ session_params_before_json: null,
595
+ session_params_after_json: null,
596
+ session_reuse_scope: "unknown",
597
+ });
598
+ for (const scope of ["explicit", "task", "none", "unknown"]) {
599
+ await expect(verifySql.unsafe(`
600
+ UPDATE "heartbeat_runs"
601
+ SET "session_reuse_scope" = '${scope}',
602
+ "session_params_before_json" = '{"sessionId":"before"}'::jsonb,
603
+ "session_params_after_json" = '{"sessionId":"after"}'::jsonb
604
+ WHERE "id" = '00000000-0000-0000-0000-000000000105'
605
+ `)).resolves.toBeDefined();
606
+ }
607
+ await expect(verifySql.unsafe(`
608
+ UPDATE "heartbeat_runs"
609
+ SET "session_reuse_scope" = 'global'
610
+ WHERE "id" = '00000000-0000-0000-0000-000000000105'
611
+ `)).rejects.toMatchObject({ code: "23514" });
612
+ }
613
+ finally {
614
+ await verifySql.end();
615
+ }
616
+ }, 20_000);
617
+ it("reconciles legacy chat control states before creating active-generation constraints", async () => {
618
+ const connectionString = await createTempDatabase();
619
+ const migrationsThrough0103 = createCurrentMigrationsFolderThrough(103);
620
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
621
+ try {
622
+ await migratePg(drizzlePg(sql), { migrationsFolder: migrationsThrough0103 });
623
+ await sql.unsafe(`
624
+ INSERT INTO "organizations" ("id", "url_key", "name", "issue_prefix")
625
+ VALUES (
626
+ '00000000-0000-0000-0000-000000000110',
627
+ 'chat-control-migration',
628
+ 'Chat Control Migration',
629
+ 'CCM'
630
+ )
631
+ `);
632
+ await sql.unsafe(`
633
+ INSERT INTO "agents" ("id", "org_id", "name")
634
+ VALUES (
635
+ '00000000-0000-0000-0000-000000000119',
636
+ '00000000-0000-0000-0000-000000000110',
637
+ 'Legacy queue agent'
638
+ )
639
+ `);
640
+ await sql.unsafe(`
641
+ INSERT INTO "chat_conversations" ("id", "org_id", "title")
642
+ VALUES (
643
+ '00000000-0000-0000-0000-000000000111',
644
+ '00000000-0000-0000-0000-000000000110',
645
+ 'Legacy chat control state'
646
+ )
647
+ `);
648
+ await sql.unsafe(`
649
+ INSERT INTO "chat_generations" ("id", "org_id", "conversation_id", "status")
650
+ VALUES
651
+ (
652
+ '00000000-0000-0000-0000-000000000112',
653
+ '00000000-0000-0000-0000-000000000110',
654
+ '00000000-0000-0000-0000-000000000111',
655
+ 'active'
656
+ ),
657
+ (
658
+ '00000000-0000-0000-0000-000000000113',
659
+ '00000000-0000-0000-0000-000000000110',
660
+ '00000000-0000-0000-0000-000000000111',
661
+ 'running'
662
+ )
663
+ `);
664
+ await sql.unsafe(`
665
+ INSERT INTO "chat_queued_messages" (
666
+ "id",
667
+ "org_id",
668
+ "conversation_id",
669
+ "position",
670
+ "status",
671
+ "client_mutation_id",
672
+ "payload",
673
+ "last_delivery_reason"
674
+ )
675
+ VALUES
676
+ (
677
+ '00000000-0000-0000-0000-000000000114',
678
+ '00000000-0000-0000-0000-000000000110',
679
+ '00000000-0000-0000-0000-000000000111',
680
+ 1,
681
+ 'steer_pending',
682
+ 'legacy-steer',
683
+ '{"body":"steer"}'::jsonb,
684
+ 'unsupported'
685
+ ),
686
+ (
687
+ '00000000-0000-0000-0000-000000000115',
688
+ '00000000-0000-0000-0000-000000000110',
689
+ '00000000-0000-0000-0000-000000000111',
690
+ 2,
691
+ 'dequeue_claimed',
692
+ 'legacy-claim',
693
+ '{"body":"claim"}'::jsonb,
694
+ NULL
695
+ ),
696
+ (
697
+ '00000000-0000-0000-0000-000000000116',
698
+ '00000000-0000-0000-0000-000000000110',
699
+ '00000000-0000-0000-0000-000000000111',
700
+ 3,
701
+ 'running',
702
+ 'legacy-running',
703
+ '{"body":"running"}'::jsonb,
704
+ NULL
705
+ ),
706
+ (
707
+ '00000000-0000-0000-0000-000000000117',
708
+ '00000000-0000-0000-0000-000000000110',
709
+ '00000000-0000-0000-0000-000000000111',
710
+ 4,
711
+ 'queued',
712
+ 'legacy-actorless-queue',
713
+ '{"body":"actorless queue"}'::jsonb,
714
+ NULL
715
+ ),
716
+ (
717
+ '00000000-0000-0000-0000-000000000118',
718
+ '00000000-0000-0000-0000-000000000110',
719
+ '00000000-0000-0000-0000-000000000111',
720
+ 5,
721
+ 'steer_pending',
722
+ 'legacy-actorless-steer',
723
+ '{"body":"actorless steer"}'::jsonb,
724
+ 'unsupported'
725
+ )
726
+ `);
727
+ await sql.unsafe(`
728
+ INSERT INTO "activity_log" (
729
+ "id",
730
+ "org_id",
731
+ "actor_type",
732
+ "actor_id",
733
+ "action",
734
+ "entity_type",
735
+ "entity_id",
736
+ "agent_id",
737
+ "details"
738
+ )
739
+ VALUES
740
+ (
741
+ '00000000-0000-0000-0000-000000000120',
742
+ '00000000-0000-0000-0000-000000000110',
743
+ 'user',
744
+ 'legacy-board-user',
745
+ 'chat.queue.created',
746
+ 'chat',
747
+ '00000000-0000-0000-0000-000000000111',
748
+ NULL,
749
+ '{"queuedMessageId":"00000000-0000-0000-0000-000000000114"}'::jsonb
750
+ ),
751
+ (
752
+ '00000000-0000-0000-0000-000000000121',
753
+ '00000000-0000-0000-0000-000000000110',
754
+ 'agent',
755
+ '00000000-0000-0000-0000-000000000119',
756
+ 'chat.queue.created',
757
+ 'chat',
758
+ '00000000-0000-0000-0000-000000000111',
759
+ '00000000-0000-0000-0000-000000000119',
760
+ '{"queuedMessageId":"00000000-0000-0000-0000-000000000115"}'::jsonb
761
+ )
762
+ `);
763
+ }
764
+ finally {
765
+ await sql.end();
766
+ }
767
+ await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
768
+ expect((await inspectMigrations(connectionString)).status).toBe("upToDate");
769
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
770
+ try {
771
+ const generations = await verifySql.unsafe(`
772
+ SELECT
773
+ status,
774
+ terminal_reason,
775
+ control_state,
776
+ runtime_terminal_at IS NOT NULL AS runtime_terminal_recorded,
777
+ completed_at IS NOT NULL AS completed_recorded
778
+ FROM "chat_generations"
779
+ WHERE "conversation_id" = '00000000-0000-0000-0000-000000000111'
780
+ ORDER BY "id"
781
+ `);
782
+ expect(generations).toEqual([
783
+ {
784
+ status: "aborted",
785
+ terminal_reason: "ownerless_during_durable_control_migration",
786
+ control_state: "terminal",
787
+ runtime_terminal_recorded: true,
788
+ completed_recorded: true,
789
+ },
790
+ {
791
+ status: "aborted",
792
+ terminal_reason: "ownerless_during_durable_control_migration",
793
+ control_state: "terminal",
794
+ runtime_terminal_recorded: true,
795
+ completed_recorded: true,
796
+ },
797
+ ]);
798
+ const queueItems = await verifySql.unsafe(`
799
+ SELECT
800
+ position,
801
+ status,
802
+ delivery_intent,
803
+ delivery_disposition,
804
+ request_actor,
805
+ reconciliation_reason,
806
+ last_delivery_reason
807
+ FROM "chat_queued_messages"
808
+ WHERE "conversation_id" = '00000000-0000-0000-0000-000000000111'
809
+ ORDER BY "position"
810
+ `);
811
+ expect(queueItems).toEqual([
812
+ {
813
+ position: 1,
814
+ status: "continuation_pending",
815
+ delivery_intent: "steer",
816
+ delivery_disposition: "continuation_pending",
817
+ request_actor: {
818
+ type: "board",
819
+ source: "session",
820
+ userId: "legacy-board-user",
821
+ orgIds: ["00000000-0000-0000-0000-000000000110"],
822
+ isInstanceAdmin: false,
823
+ },
824
+ reconciliation_reason: "legacy_steer_recovered_on_upgrade",
825
+ last_delivery_reason: null,
826
+ },
827
+ {
828
+ position: 2,
829
+ status: "queued",
830
+ delivery_intent: "queue",
831
+ delivery_disposition: null,
832
+ request_actor: {
833
+ type: "agent",
834
+ source: "agent_key",
835
+ orgId: "00000000-0000-0000-0000-000000000110",
836
+ agentId: "00000000-0000-0000-0000-000000000119",
837
+ },
838
+ reconciliation_reason: "legacy_claim_released_on_upgrade",
839
+ last_delivery_reason: "legacy_claim_released_on_upgrade",
840
+ },
841
+ {
842
+ position: 3,
843
+ status: "failed_actionable",
844
+ delivery_intent: "queue",
845
+ delivery_disposition: "failed_actionable",
846
+ request_actor: null,
847
+ reconciliation_reason: "legacy_running_delivery_unconfirmed_on_upgrade",
848
+ last_delivery_reason: "legacy_running_delivery_unconfirmed_on_upgrade",
849
+ },
850
+ {
851
+ position: 4,
852
+ status: "queued",
853
+ delivery_intent: "queue",
854
+ delivery_disposition: null,
855
+ request_actor: null,
856
+ reconciliation_reason: "legacy_request_actor_unavailable",
857
+ last_delivery_reason: "legacy_request_actor_unavailable",
858
+ },
859
+ {
860
+ position: 5,
861
+ status: "queued",
862
+ delivery_intent: "queue",
863
+ delivery_disposition: null,
864
+ request_actor: null,
865
+ reconciliation_reason: "legacy_request_actor_unavailable",
866
+ last_delivery_reason: "legacy_request_actor_unavailable",
867
+ },
868
+ ]);
869
+ const activeIndex = await verifySql.unsafe(`
870
+ SELECT indexname
871
+ FROM pg_indexes
872
+ WHERE schemaname = 'public'
873
+ AND indexname = 'chat_generations_active_conversation_uq'
874
+ `);
875
+ expect(activeIndex).toEqual([{ indexname: "chat_generations_active_conversation_uq" }]);
876
+ }
877
+ finally {
878
+ await verifySql.end();
879
+ }
880
+ }, 20_000);
248
881
  it("rolls back migration 0100 when a legacy alias conflicts with another organization route", async () => {
249
882
  const connectionString = await createTempDatabase();
250
883
  await applyPendingMigrations(connectionString);