@penclipai/db 2026.704.0 → 2026.714.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 (122) hide show
  1. package/dist/backup-lib.test.js +1 -1
  2. package/dist/backup-lib.test.js.map +1 -1
  3. package/dist/check-migration-safety.d.ts +27 -0
  4. package/dist/check-migration-safety.d.ts.map +1 -0
  5. package/dist/check-migration-safety.js +811 -0
  6. package/dist/check-migration-safety.js.map +1 -0
  7. package/dist/check-migration-safety.test.d.ts +2 -0
  8. package/dist/check-migration-safety.test.d.ts.map +1 -0
  9. package/dist/check-migration-safety.test.js +391 -0
  10. package/dist/check-migration-safety.test.js.map +1 -0
  11. package/dist/client.test.js +743 -0
  12. package/dist/client.test.js.map +1 -1
  13. package/dist/issue-comment-derived-attribution-migration.test.d.ts +2 -0
  14. package/dist/issue-comment-derived-attribution-migration.test.d.ts.map +1 -0
  15. package/dist/issue-comment-derived-attribution-migration.test.js +287 -0
  16. package/dist/issue-comment-derived-attribution-migration.test.js.map +1 -0
  17. package/dist/migration-safety-baseline.d.ts +135 -0
  18. package/dist/migration-safety-baseline.d.ts.map +1 -0
  19. package/dist/migration-safety-baseline.js +150 -0
  20. package/dist/migration-safety-baseline.js.map +1 -0
  21. package/dist/migrations/0128_user_specific_secrets.sql +178 -0
  22. package/dist/migrations/0129_agent_api_key_responsible_user.sql +41 -0
  23. package/dist/migrations/0131_repair_run_responsible_user_context_refs.sql +67 -0
  24. package/dist/migrations/0132_issue_comment_derived_attribution_fast.sql +82 -0
  25. package/dist/migrations/0133_resource_membership_stars.sql +7 -0
  26. package/dist/migrations/0134_run_responsible_user_invariant.sql +211 -0
  27. package/dist/migrations/0135_repair_run_responsible_user_updated_at_sweep.sql +84 -0
  28. package/dist/migrations/0136_acpx_default_engine_migration.sql +97 -0
  29. package/dist/migrations/0137_skill_studio_server_foundation.sql +76 -0
  30. package/dist/migrations/0138_skill_studio_run_retention.sql +5 -0
  31. package/dist/migrations/0139_skill_studio_run_templates.sql +44 -0
  32. package/dist/migrations/0140_built_in_managed_resources.sql +43 -0
  33. package/dist/migrations/0141_heartbeat_runs_company_created_at_index.sql +2 -0
  34. package/dist/migrations/0142_company_search_sort_indexes.sql +3 -0
  35. package/dist/migrations/0143_cases_foundation.sql +248 -0
  36. package/dist/migrations/0144_case_document_annotations.sql +31 -0
  37. package/dist/migrations/0145_inbox_dismissal_snooze_kind.sql +15 -0
  38. package/dist/migrations/0146_routine_activity_gate.sql +3 -0
  39. package/dist/migrations/0147_cost_event_status.sql +1 -0
  40. package/dist/migrations/meta/_journal.json +132 -6
  41. package/dist/schema/agent_api_keys.d.ts +23 -0
  42. package/dist/schema/agent_api_keys.d.ts.map +1 -1
  43. package/dist/schema/agent_api_keys.js +1 -0
  44. package/dist/schema/agent_api_keys.js.map +1 -1
  45. package/dist/schema/agent_memberships.d.ts +17 -0
  46. package/dist/schema/agent_memberships.d.ts.map +1 -1
  47. package/dist/schema/agent_memberships.js +2 -0
  48. package/dist/schema/agent_memberships.js.map +1 -1
  49. package/dist/schema/built_in_managed_resources.d.ts +197 -0
  50. package/dist/schema/built_in_managed_resources.d.ts.map +1 -0
  51. package/dist/schema/built_in_managed_resources.js +22 -0
  52. package/dist/schema/built_in_managed_resources.js.map +1 -0
  53. package/dist/schema/cases.d.ts +982 -0
  54. package/dist/schema/cases.d.ts.map +1 -0
  55. package/dist/schema/cases.js +122 -0
  56. package/dist/schema/cases.js.map +1 -0
  57. package/dist/schema/companies.d.ts +17 -0
  58. package/dist/schema/companies.d.ts.map +1 -1
  59. package/dist/schema/companies.js +1 -0
  60. package/dist/schema/companies.js.map +1 -1
  61. package/dist/schema/company_secrets.d.ts +51 -0
  62. package/dist/schema/company_secrets.d.ts.map +1 -1
  63. package/dist/schema/company_secrets.js +27 -3
  64. package/dist/schema/company_secrets.js.map +1 -1
  65. package/dist/schema/company_skills.d.ts +788 -0
  66. package/dist/schema/company_skills.d.ts.map +1 -1
  67. package/dist/schema/company_skills.js +63 -0
  68. package/dist/schema/company_skills.js.map +1 -1
  69. package/dist/schema/cost_events.d.ts +17 -0
  70. package/dist/schema/cost_events.d.ts.map +1 -1
  71. package/dist/schema/cost_events.js +1 -0
  72. package/dist/schema/cost_events.js.map +1 -1
  73. package/dist/schema/document_annotation_comments.d.ts +17 -0
  74. package/dist/schema/document_annotation_comments.d.ts.map +1 -1
  75. package/dist/schema/document_annotation_comments.js +4 -1
  76. package/dist/schema/document_annotation_comments.js.map +1 -1
  77. package/dist/schema/document_annotation_threads.d.ts +17 -0
  78. package/dist/schema/document_annotation_threads.d.ts.map +1 -1
  79. package/dist/schema/document_annotation_threads.js +4 -1
  80. package/dist/schema/document_annotation_threads.js.map +1 -1
  81. package/dist/schema/heartbeat_runs.d.ts +17 -0
  82. package/dist/schema/heartbeat_runs.d.ts.map +1 -1
  83. package/dist/schema/heartbeat_runs.js +3 -0
  84. package/dist/schema/heartbeat_runs.js.map +1 -1
  85. package/dist/schema/inbox_dismissals.d.ts +36 -0
  86. package/dist/schema/inbox_dismissals.d.ts.map +1 -1
  87. package/dist/schema/inbox_dismissals.js +2 -0
  88. package/dist/schema/inbox_dismissals.js.map +1 -1
  89. package/dist/schema/index.d.ts +5 -1
  90. package/dist/schema/index.d.ts.map +1 -1
  91. package/dist/schema/index.js +5 -1
  92. package/dist/schema/index.js.map +1 -1
  93. package/dist/schema/issues.d.ts +34 -0
  94. package/dist/schema/issues.d.ts.map +1 -1
  95. package/dist/schema/issues.js +7 -0
  96. package/dist/schema/issues.js.map +1 -1
  97. package/dist/schema/project_memberships.d.ts +17 -0
  98. package/dist/schema/project_memberships.d.ts.map +1 -1
  99. package/dist/schema/project_memberships.js +2 -0
  100. package/dist/schema/project_memberships.js.map +1 -1
  101. package/dist/schema/routines.d.ts +85 -0
  102. package/dist/schema/routines.d.ts.map +1 -1
  103. package/dist/schema/routines.js +8 -0
  104. package/dist/schema/routines.js.map +1 -1
  105. package/dist/schema/secret_access_events.d.ts +103 -1
  106. package/dist/schema/secret_access_events.d.ts.map +1 -1
  107. package/dist/schema/secret_access_events.js +10 -1
  108. package/dist/schema/secret_access_events.js.map +1 -1
  109. package/dist/schema/user_secret_declarations.d.ts +229 -0
  110. package/dist/schema/user_secret_declarations.d.ts.map +1 -0
  111. package/dist/schema/user_secret_declarations.js +31 -0
  112. package/dist/schema/user_secret_declarations.js.map +1 -0
  113. package/dist/schema/user_secret_definitions.d.ts +316 -0
  114. package/dist/schema/user_secret_definitions.d.ts.map +1 -0
  115. package/dist/schema/user_secret_definitions.js +33 -0
  116. package/dist/schema/user_secret_definitions.js.map +1 -0
  117. package/dist/table-size-estimates.d.ts +372 -0
  118. package/dist/table-size-estimates.d.ts.map +1 -0
  119. package/dist/table-size-estimates.js +155 -0
  120. package/dist/table-size-estimates.js.map +1 -0
  121. package/package.json +3 -3
  122. package/dist/migrations/0126_issue_comment_derived_attribution.sql +0 -69
@@ -18,6 +18,46 @@ async function migrationHash(migrationFile) {
18
18
  const content = await fs.promises.readFile(new URL(`./migrations/${migrationFile}`, import.meta.url), "utf8");
19
19
  return createHash("sha256").update(content).digest("hex");
20
20
  }
21
+ const userVisibleUpdatedAtTables = new Set([
22
+ "companies",
23
+ "heartbeat_runs",
24
+ "issue_comments",
25
+ "issues",
26
+ "routine_runs",
27
+ "routines",
28
+ ]);
29
+ const migrationUpdatedAtUpdateAllowlist = new Map([
30
+ [
31
+ "0105_instance_scoped_environments.sql",
32
+ new Set(["issues"]),
33
+ ],
34
+ [
35
+ "0131_repair_run_responsible_user_context_refs.sql",
36
+ new Set(["heartbeat_runs"]),
37
+ ],
38
+ [
39
+ "0135_repair_run_responsible_user_updated_at_sweep.sql",
40
+ new Set(["companies", "heartbeat_runs", "issues", "routine_runs", "routines"]),
41
+ ],
42
+ ]);
43
+ function findUserVisibleUpdatedAtBackfillViolations(migrationFile, content) {
44
+ const allowedTables = migrationUpdatedAtUpdateAllowlist.get(migrationFile) ?? new Set();
45
+ const violations = [];
46
+ for (const statement of content.split("--> statement-breakpoint")) {
47
+ const updateMatch = statement.match(/\bUPDATE\s+"([^"]+)"/i);
48
+ if (!updateMatch)
49
+ continue;
50
+ const tableName = updateMatch[1];
51
+ if (!userVisibleUpdatedAtTables.has(tableName))
52
+ continue;
53
+ if (!/\bSET\b[\s\S]*"updated_at"\s*=/i.test(statement))
54
+ continue;
55
+ if (allowedTables.has(tableName))
56
+ continue;
57
+ violations.push(`${migrationFile}: UPDATE "${tableName}" sets updated_at`);
58
+ }
59
+ return violations;
60
+ }
21
61
  afterEach(async () => {
22
62
  while (cleanups.length > 0) {
23
63
  const cleanup = cleanups.pop();
@@ -28,6 +68,25 @@ if (!embeddedPostgresSupport.supported) {
28
68
  console.warn(`Skipping embedded Postgres migration tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`);
29
69
  }
30
70
  describeEmbeddedPostgres("applyPendingMigrations", () => {
71
+ it("rejects unallowlisted migration backfills that bump updated_at on user-visible tables", async () => {
72
+ const entries = await fs.promises.readdir(new URL("./migrations", import.meta.url), {
73
+ withFileTypes: true,
74
+ });
75
+ const violations = [];
76
+ for (const entry of entries) {
77
+ if (!entry.isFile() || !entry.name.endsWith(".sql"))
78
+ continue;
79
+ const content = await fs.promises.readFile(new URL(`./migrations/${entry.name}`, import.meta.url), "utf8");
80
+ violations.push(...findUserVisibleUpdatedAtBackfillViolations(entry.name, content));
81
+ }
82
+ expect(violations).toEqual([]);
83
+ expect(findUserVisibleUpdatedAtBackfillViolations("9999_bad_backfill.sql", `
84
+ UPDATE "issues" AS i
85
+ SET "responsible_user_id" = 'owner-user',
86
+ "updated_at" = now()
87
+ WHERE i."responsible_user_id" IS NULL;
88
+ `)).toEqual(['9999_bad_backfill.sql: UPDATE "issues" sets updated_at']);
89
+ });
31
90
  it("applies an inserted earlier migration without replaying later legacy migrations", async () => {
32
91
  const connectionString = await createTempDatabase();
33
92
  await applyPendingMigrations(connectionString);
@@ -398,5 +457,689 @@ describeEmbeddedPostgres("applyPendingMigrations", () => {
398
457
  await verifySql.end();
399
458
  }
400
459
  }, MIGRATION_TEST_TIMEOUT);
460
+ it("replays the built-in managed resources migration after the legacy 0136 journal entry", async () => {
461
+ const connectionString = await createTempDatabase();
462
+ await applyPendingMigrations(connectionString);
463
+ const builtInResourcesHash = await migrationHash("0140_built_in_managed_resources.sql");
464
+ const legacyBuiltInResourcesHash = createHash("sha256")
465
+ .update("legacy 0136_built_in_managed_resources.sql")
466
+ .digest("hex");
467
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
468
+ try {
469
+ await sql.unsafe(`DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${builtInResourcesHash}'`);
470
+ await sql.unsafe(`
471
+ INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at")
472
+ VALUES ('${legacyBuiltInResourcesHash}', 1783555200000)
473
+ `);
474
+ await sql.unsafe(`
475
+ ALTER TABLE "built_in_managed_resources"
476
+ DROP CONSTRAINT IF EXISTS "built_in_managed_resources_company_id_companies_id_fk"
477
+ `);
478
+ await sql.unsafe(`DROP INDEX IF EXISTS "built_in_managed_resources_company_idx"`);
479
+ await sql.unsafe(`DROP INDEX IF EXISTS "built_in_managed_resources_resource_idx"`);
480
+ await sql.unsafe(`DROP INDEX IF EXISTS "built_in_managed_resources_company_bundle_resource_uq"`);
481
+ }
482
+ finally {
483
+ await sql.end();
484
+ }
485
+ const pendingState = await inspectMigrations(connectionString);
486
+ expect(pendingState).toMatchObject({
487
+ status: "needsMigrations",
488
+ pendingMigrations: ["0140_built_in_managed_resources.sql"],
489
+ reason: "pending-migrations",
490
+ });
491
+ await applyPendingMigrations(connectionString);
492
+ const finalState = await inspectMigrations(connectionString);
493
+ expect(finalState.status).toBe("upToDate");
494
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
495
+ try {
496
+ const rows = await verifySql.unsafe(`
497
+ SELECT
498
+ EXISTS (
499
+ SELECT 1
500
+ FROM "pg_constraint" c
501
+ JOIN "pg_class" t ON t.oid = c.conrelid
502
+ JOIN "pg_namespace" n ON n.oid = t.relnamespace
503
+ WHERE n.nspname = 'public'
504
+ AND t.relname = 'built_in_managed_resources'
505
+ AND c.conname = 'built_in_managed_resources_company_id_companies_id_fk'
506
+ ) AS "foreign_key_exists",
507
+ EXISTS (
508
+ SELECT 1
509
+ FROM "pg_class" c
510
+ JOIN "pg_namespace" n ON n.oid = c.relnamespace
511
+ WHERE n.nspname = 'public'
512
+ AND c.relkind = 'i'
513
+ AND c.relname = 'built_in_managed_resources_company_idx'
514
+ ) AS "company_index_exists",
515
+ EXISTS (
516
+ SELECT 1
517
+ FROM "pg_class" c
518
+ JOIN "pg_namespace" n ON n.oid = c.relnamespace
519
+ WHERE n.nspname = 'public'
520
+ AND c.relkind = 'i'
521
+ AND c.relname = 'built_in_managed_resources_resource_idx'
522
+ ) AS "resource_index_exists",
523
+ EXISTS (
524
+ SELECT 1
525
+ FROM "pg_class" c
526
+ JOIN "pg_namespace" n ON n.oid = c.relnamespace
527
+ WHERE n.nspname = 'public'
528
+ AND c.relkind = 'i'
529
+ AND c.relname = 'built_in_managed_resources_company_bundle_resource_uq'
530
+ ) AS "unique_index_exists"
531
+ `);
532
+ expect(rows[0]).toEqual({
533
+ foreign_key_exists: true,
534
+ company_index_exists: true,
535
+ resource_index_exists: true,
536
+ unique_index_exists: true,
537
+ });
538
+ }
539
+ finally {
540
+ await verifySql.end();
541
+ }
542
+ }, 20_000);
543
+ it("replays migration 0134 without bumping issue updated_at for inbox archives", async () => {
544
+ const connectionString = await createTempDatabase();
545
+ await applyPendingMigrations(connectionString);
546
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
547
+ try {
548
+ const runResponsibleUserHash = await migrationHash("0134_run_responsible_user_invariant.sql");
549
+ await sql.unsafe(`
550
+ INSERT INTO "companies" ("id", "name", "issue_prefix", "created_at", "updated_at")
551
+ VALUES (
552
+ '00000000-0000-0000-0000-000000000120',
553
+ 'Migration Inbox Co',
554
+ 'TST120',
555
+ '2026-03-26T09:00:00.000Z',
556
+ '2026-03-26T09:00:00.000Z'
557
+ )
558
+ `);
559
+ await sql.unsafe(`
560
+ INSERT INTO "company_memberships" (
561
+ "id",
562
+ "company_id",
563
+ "principal_type",
564
+ "principal_id",
565
+ "status",
566
+ "membership_role",
567
+ "created_at",
568
+ "updated_at"
569
+ )
570
+ VALUES (
571
+ '00000000-0000-0000-0000-000000000121',
572
+ '00000000-0000-0000-0000-000000000120',
573
+ 'user',
574
+ 'owner-user',
575
+ 'active',
576
+ 'owner',
577
+ '2026-03-26T09:00:00.000Z',
578
+ '2026-03-26T09:00:00.000Z'
579
+ )
580
+ `);
581
+ await sql.unsafe(`
582
+ INSERT INTO "issues" (
583
+ "id",
584
+ "company_id",
585
+ "title",
586
+ "status",
587
+ "responsible_user_id",
588
+ "created_at",
589
+ "updated_at"
590
+ )
591
+ VALUES (
592
+ '00000000-0000-0000-0000-000000000122',
593
+ '00000000-0000-0000-0000-000000000120',
594
+ 'Archived issue needing responsible user backfill',
595
+ 'todo',
596
+ NULL,
597
+ '2026-03-26T10:00:00.000Z',
598
+ '2026-03-26T10:00:00.000Z'
599
+ )
600
+ `);
601
+ await sql.unsafe(`
602
+ INSERT INTO "issue_inbox_archives" (
603
+ "id",
604
+ "company_id",
605
+ "issue_id",
606
+ "user_id",
607
+ "archived_at",
608
+ "created_at",
609
+ "updated_at"
610
+ )
611
+ VALUES (
612
+ '00000000-0000-0000-0000-000000000123',
613
+ '00000000-0000-0000-0000-000000000120',
614
+ '00000000-0000-0000-0000-000000000122',
615
+ 'owner-user',
616
+ '2026-03-26T12:00:00.000Z',
617
+ '2026-03-26T12:00:00.000Z',
618
+ '2026-03-26T12:00:00.000Z'
619
+ )
620
+ `);
621
+ await sql.unsafe(`DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${runResponsibleUserHash}'`);
622
+ }
623
+ finally {
624
+ await sql.end();
625
+ }
626
+ const pendingState = await inspectMigrations(connectionString);
627
+ expect(pendingState).toMatchObject({
628
+ status: "needsMigrations",
629
+ pendingMigrations: ["0134_run_responsible_user_invariant.sql"],
630
+ reason: "pending-migrations",
631
+ });
632
+ await applyPendingMigrations(connectionString);
633
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
634
+ try {
635
+ const rows = await verifySql.unsafe(`
636
+ SELECT
637
+ i."responsible_user_id",
638
+ i."updated_at",
639
+ EXISTS (
640
+ SELECT 1
641
+ FROM "issue_inbox_archives" AS archive
642
+ WHERE archive."company_id" = i."company_id"
643
+ AND archive."issue_id" = i."id"
644
+ AND archive."user_id" = 'owner-user'
645
+ AND archive."archived_at" >= i."updated_at"
646
+ ) AS "inbox_archive_still_current"
647
+ FROM "issues" AS i
648
+ WHERE i."id" = '00000000-0000-0000-0000-000000000122'
649
+ `);
650
+ expect(rows).toHaveLength(1);
651
+ expect(rows[0]?.responsible_user_id).toBe("owner-user");
652
+ expect(rows[0]?.updated_at.toISOString()).toBe("2026-03-26T10:00:00.000Z");
653
+ expect(rows[0]?.inbox_archive_still_current).toBe(true);
654
+ }
655
+ finally {
656
+ await verifySql.end();
657
+ }
658
+ }, 20_000);
659
+ it("replays migration 0135 to repair updated_at sweeps and no-op when clean", async () => {
660
+ const connectionString = await createTempDatabase();
661
+ await applyPendingMigrations(connectionString);
662
+ const repairSweepHash = await migrationHash("0135_repair_run_responsible_user_updated_at_sweep.sql");
663
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
664
+ try {
665
+ await sql.unsafe(`
666
+ INSERT INTO "companies" ("id", "name", "issue_prefix", "created_at", "updated_at")
667
+ VALUES (
668
+ '00000000-0000-0000-0000-000000000240',
669
+ 'Clean Migration Co',
670
+ 'CLN134',
671
+ '2026-04-01T09:00:00.000Z',
672
+ '2026-04-02T09:00:00.000Z'
673
+ )
674
+ `);
675
+ await sql.unsafe(`
676
+ INSERT INTO "issues" ("id", "company_id", "title", "status", "created_at", "updated_at")
677
+ VALUES (
678
+ '00000000-0000-0000-0000-000000000241',
679
+ '00000000-0000-0000-0000-000000000240',
680
+ 'Clean issue should not be touched',
681
+ 'todo',
682
+ '2026-04-01T10:00:00.000Z',
683
+ '2026-04-02T10:00:00.000Z'
684
+ )
685
+ `);
686
+ await sql.unsafe(`DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${repairSweepHash}'`);
687
+ }
688
+ finally {
689
+ await sql.end();
690
+ }
691
+ await applyPendingMigrations(connectionString);
692
+ const afterCleanReplay = postgres(connectionString, { max: 1, onnotice: () => { } });
693
+ try {
694
+ const cleanRows = await afterCleanReplay.unsafe(`
695
+ SELECT "updated_at"
696
+ FROM "issues"
697
+ WHERE "id" = '00000000-0000-0000-0000-000000000241'
698
+ `);
699
+ expect(cleanRows[0]?.updated_at.toISOString()).toBe("2026-04-02T10:00:00.000Z");
700
+ await afterCleanReplay.unsafe(`
701
+ INSERT INTO "companies" ("id", "name", "issue_prefix", "created_at", "updated_at")
702
+ VALUES (
703
+ '00000000-0000-0000-0000-000000000250',
704
+ 'Sweep Migration Co',
705
+ 'SWP134',
706
+ '2026-01-01T00:00:00.000Z',
707
+ '2026-04-03T12:00:00.123456Z'
708
+ )
709
+ `);
710
+ await afterCleanReplay.unsafe(`
711
+ INSERT INTO "agents" ("id", "company_id", "name", "role", "adapter_type", "created_at", "updated_at")
712
+ VALUES (
713
+ '00000000-0000-0000-0000-000000000251',
714
+ '00000000-0000-0000-0000-000000000250',
715
+ 'Sweep Agent',
716
+ 'general',
717
+ 'process',
718
+ '2026-01-02T00:00:00.000Z',
719
+ '2026-01-02T00:00:00.000Z'
720
+ )
721
+ `);
722
+ await afterCleanReplay.unsafe(`
723
+ INSERT INTO "issues" ("id", "company_id", "title", "status", "created_at", "updated_at")
724
+ SELECT
725
+ ('10000000-0000-0000-0000-' || lpad(gs::text, 12, '0'))::uuid,
726
+ '00000000-0000-0000-0000-000000000250',
727
+ 'Swept issue ' || gs::text,
728
+ 'todo',
729
+ '2026-02-01T00:00:00.000Z'::timestamptz + (gs::text || ' minutes')::interval,
730
+ '2026-04-03T12:00:00.123456Z'
731
+ FROM generate_series(1, 101) AS gs
732
+ `);
733
+ await afterCleanReplay.unsafe(`
734
+ UPDATE "issues"
735
+ SET
736
+ "status" = 'done',
737
+ "completed_at" = '2026-04-03T12:00:00.123456Z'
738
+ WHERE "id" = '10000000-0000-0000-0000-000000000003'
739
+ `);
740
+ await afterCleanReplay.unsafe(`
741
+ INSERT INTO "issue_comments" ("id", "company_id", "issue_id", "body", "created_at", "updated_at")
742
+ VALUES (
743
+ '00000000-0000-0000-0000-000000000252',
744
+ '00000000-0000-0000-0000-000000000250',
745
+ '10000000-0000-0000-0000-000000000001',
746
+ 'Latest pre-sweep activity',
747
+ '2026-03-01T15:30:00.000Z',
748
+ '2026-03-02T16:45:00.000Z'
749
+ )
750
+ `);
751
+ await afterCleanReplay.unsafe(`
752
+ INSERT INTO "heartbeat_runs" (
753
+ "id",
754
+ "company_id",
755
+ "agent_id",
756
+ "status",
757
+ "started_at",
758
+ "finished_at",
759
+ "created_at",
760
+ "updated_at"
761
+ )
762
+ VALUES (
763
+ '00000000-0000-0000-0000-000000000253',
764
+ '00000000-0000-0000-0000-000000000250',
765
+ '00000000-0000-0000-0000-000000000251',
766
+ 'completed',
767
+ '2026-02-10T10:00:00.000Z',
768
+ '2026-02-10T10:30:00.000Z',
769
+ '2026-02-10T09:55:00.000Z',
770
+ '2026-04-03T12:00:00.123456Z'
771
+ )
772
+ `);
773
+ await afterCleanReplay.unsafe(`
774
+ INSERT INTO "heartbeat_runs" (
775
+ "id",
776
+ "company_id",
777
+ "agent_id",
778
+ "status",
779
+ "started_at",
780
+ "last_output_at",
781
+ "created_at",
782
+ "updated_at"
783
+ )
784
+ VALUES (
785
+ '00000000-0000-0000-0000-000000000256',
786
+ '00000000-0000-0000-0000-000000000250',
787
+ '00000000-0000-0000-0000-000000000251',
788
+ 'running',
789
+ '2026-02-10T11:00:00.000Z',
790
+ '2026-04-03T12:00:00.123456Z',
791
+ '2026-02-10T10:55:00.000Z',
792
+ '2026-04-03T12:00:00.123456Z'
793
+ )
794
+ `);
795
+ await afterCleanReplay.unsafe(`
796
+ INSERT INTO "routines" (
797
+ "id",
798
+ "company_id",
799
+ "title",
800
+ "last_triggered_at",
801
+ "last_enqueued_at",
802
+ "created_at",
803
+ "updated_at"
804
+ )
805
+ VALUES (
806
+ '00000000-0000-0000-0000-000000000254',
807
+ '00000000-0000-0000-0000-000000000250',
808
+ 'Swept routine',
809
+ '2026-03-20T10:00:00.000Z',
810
+ '2026-03-21T11:00:00.000Z',
811
+ '2026-02-11T00:00:00.000Z',
812
+ '2026-04-03T12:00:00.123456Z'
813
+ )
814
+ `);
815
+ await afterCleanReplay.unsafe(`
816
+ INSERT INTO "routines" (
817
+ "id",
818
+ "company_id",
819
+ "title",
820
+ "last_triggered_at",
821
+ "last_enqueued_at",
822
+ "created_at",
823
+ "updated_at"
824
+ )
825
+ VALUES (
826
+ '00000000-0000-0000-0000-000000000257',
827
+ '00000000-0000-0000-0000-000000000250',
828
+ 'Same-timestamp active routine',
829
+ '2026-03-20T10:00:00.000Z',
830
+ '2026-04-03T12:00:00.123456Z',
831
+ '2026-02-11T00:00:00.000Z',
832
+ '2026-04-03T12:00:00.123456Z'
833
+ )
834
+ `);
835
+ await afterCleanReplay.unsafe(`
836
+ INSERT INTO "routine_runs" (
837
+ "id",
838
+ "company_id",
839
+ "routine_id",
840
+ "source",
841
+ "status",
842
+ "completed_at",
843
+ "created_at",
844
+ "updated_at"
845
+ )
846
+ VALUES (
847
+ '00000000-0000-0000-0000-000000000255',
848
+ '00000000-0000-0000-0000-000000000250',
849
+ '00000000-0000-0000-0000-000000000254',
850
+ 'schedule',
851
+ 'completed',
852
+ '2026-02-12T12:00:00.000Z',
853
+ '2026-02-12T11:00:00.000Z',
854
+ '2026-04-03T12:00:00.123456Z'
855
+ )
856
+ `);
857
+ await afterCleanReplay.unsafe(`
858
+ INSERT INTO "routine_runs" (
859
+ "id",
860
+ "company_id",
861
+ "routine_id",
862
+ "source",
863
+ "status",
864
+ "triggered_at",
865
+ "created_at",
866
+ "updated_at"
867
+ )
868
+ VALUES (
869
+ '00000000-0000-0000-0000-000000000258',
870
+ '00000000-0000-0000-0000-000000000250',
871
+ '00000000-0000-0000-0000-000000000257',
872
+ 'schedule',
873
+ 'running',
874
+ '2026-04-03T12:00:00.123456Z',
875
+ '2026-02-12T13:00:00.000Z',
876
+ '2026-04-03T12:00:00.123456Z'
877
+ )
878
+ `);
879
+ await afterCleanReplay.unsafe(`
880
+ INSERT INTO "companies" ("id", "name", "issue_prefix", "created_at", "updated_at")
881
+ VALUES (
882
+ '00000000-0000-0000-0000-000000000260',
883
+ 'Coincident Timestamp Co',
884
+ 'CTS134',
885
+ '2026-01-05T00:00:00.000Z',
886
+ '2026-04-03T12:00:00.123456Z'
887
+ )
888
+ `);
889
+ await afterCleanReplay.unsafe(`
890
+ INSERT INTO "agents" ("id", "company_id", "name", "role", "adapter_type", "created_at", "updated_at")
891
+ VALUES (
892
+ '00000000-0000-0000-0000-000000000261',
893
+ '00000000-0000-0000-0000-000000000260',
894
+ 'Coincident Agent',
895
+ 'general',
896
+ 'process',
897
+ '2026-01-05T00:10:00.000Z',
898
+ '2026-01-05T00:10:00.000Z'
899
+ )
900
+ `);
901
+ await afterCleanReplay.unsafe(`
902
+ INSERT INTO "issues" ("id", "company_id", "title", "status", "created_at", "updated_at")
903
+ VALUES (
904
+ '20000000-0000-0000-0000-000000000001',
905
+ '00000000-0000-0000-0000-000000000260',
906
+ 'Coincident timestamp issue should not be touched',
907
+ 'todo',
908
+ '2026-02-05T00:00:00.000Z',
909
+ '2026-04-03T12:00:00.123456Z'
910
+ )
911
+ `);
912
+ await afterCleanReplay.unsafe(`
913
+ INSERT INTO "heartbeat_runs" (
914
+ "id",
915
+ "company_id",
916
+ "agent_id",
917
+ "status",
918
+ "started_at",
919
+ "finished_at",
920
+ "created_at",
921
+ "updated_at"
922
+ )
923
+ VALUES (
924
+ '00000000-0000-0000-0000-000000000262',
925
+ '00000000-0000-0000-0000-000000000260',
926
+ '00000000-0000-0000-0000-000000000261',
927
+ 'completed',
928
+ '2026-02-05T10:00:00.000Z',
929
+ '2026-02-05T10:30:00.000Z',
930
+ '2026-02-05T09:55:00.000Z',
931
+ '2026-04-03T12:00:00.123456Z'
932
+ )
933
+ `);
934
+ await afterCleanReplay.unsafe(`DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${repairSweepHash}'`);
935
+ }
936
+ finally {
937
+ await afterCleanReplay.end();
938
+ }
939
+ await applyPendingMigrations(connectionString);
940
+ const afterRepair = postgres(connectionString, { max: 1, onnotice: () => { } });
941
+ try {
942
+ const repairedRows = await afterRepair.unsafe(`
943
+ SELECT 'company' AS subject, "updated_at"
944
+ FROM "companies"
945
+ WHERE "id" = '00000000-0000-0000-0000-000000000250'
946
+ UNION ALL
947
+ SELECT 'issue_with_comment' AS subject, "updated_at"
948
+ FROM "issues"
949
+ WHERE "id" = '10000000-0000-0000-0000-000000000001'
950
+ UNION ALL
951
+ SELECT 'issue_without_comment' AS subject, "updated_at"
952
+ FROM "issues"
953
+ WHERE "id" = '10000000-0000-0000-0000-000000000002'
954
+ UNION ALL
955
+ SELECT 'issue_with_state_activity' AS subject, "updated_at"
956
+ FROM "issues"
957
+ WHERE "id" = '10000000-0000-0000-0000-000000000003'
958
+ UNION ALL
959
+ SELECT 'heartbeat_run' AS subject, "updated_at"
960
+ FROM "heartbeat_runs"
961
+ WHERE "id" = '00000000-0000-0000-0000-000000000253'
962
+ UNION ALL
963
+ SELECT 'heartbeat_run_with_output' AS subject, "updated_at"
964
+ FROM "heartbeat_runs"
965
+ WHERE "id" = '00000000-0000-0000-0000-000000000256'
966
+ UNION ALL
967
+ SELECT 'other_company' AS subject, "updated_at"
968
+ FROM "companies"
969
+ WHERE "id" = '00000000-0000-0000-0000-000000000260'
970
+ UNION ALL
971
+ SELECT 'other_heartbeat_run' AS subject, "updated_at"
972
+ FROM "heartbeat_runs"
973
+ WHERE "id" = '00000000-0000-0000-0000-000000000262'
974
+ UNION ALL
975
+ SELECT 'other_issue' AS subject, "updated_at"
976
+ FROM "issues"
977
+ WHERE "id" = '20000000-0000-0000-0000-000000000001'
978
+ UNION ALL
979
+ SELECT 'routine' AS subject, "updated_at"
980
+ FROM "routines"
981
+ WHERE "id" = '00000000-0000-0000-0000-000000000254'
982
+ UNION ALL
983
+ SELECT 'routine_with_activity' AS subject, "updated_at"
984
+ FROM "routines"
985
+ WHERE "id" = '00000000-0000-0000-0000-000000000257'
986
+ UNION ALL
987
+ SELECT 'routine_run' AS subject, "updated_at"
988
+ FROM "routine_runs"
989
+ WHERE "id" = '00000000-0000-0000-0000-000000000255'
990
+ UNION ALL
991
+ SELECT 'routine_run_with_trigger' AS subject, "updated_at"
992
+ FROM "routine_runs"
993
+ WHERE "id" = '00000000-0000-0000-0000-000000000258'
994
+ ORDER BY subject
995
+ `);
996
+ const repaired = Object.fromEntries(repairedRows.map((row) => [row.subject, row.updated_at.toISOString()]));
997
+ expect(repaired).toEqual({
998
+ company: "2026-01-01T00:00:00.000Z",
999
+ heartbeat_run: "2026-02-10T10:30:00.000Z",
1000
+ heartbeat_run_with_output: "2026-04-03T12:00:00.123Z",
1001
+ issue_with_comment: "2026-03-02T16:45:00.000Z",
1002
+ issue_with_state_activity: "2026-04-03T12:00:00.123Z",
1003
+ issue_without_comment: "2026-02-01T00:02:00.000Z",
1004
+ other_company: "2026-04-03T12:00:00.123Z",
1005
+ other_heartbeat_run: "2026-04-03T12:00:00.123Z",
1006
+ other_issue: "2026-04-03T12:00:00.123Z",
1007
+ routine: "2026-03-21T11:00:00.000Z",
1008
+ routine_run_with_trigger: "2026-04-03T12:00:00.123Z",
1009
+ routine_with_activity: "2026-04-03T12:00:00.123Z",
1010
+ routine_run: "2026-02-12T12:00:00.000Z",
1011
+ });
1012
+ await afterRepair.unsafe(`DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${repairSweepHash}'`);
1013
+ }
1014
+ finally {
1015
+ await afterRepair.end();
1016
+ }
1017
+ await applyPendingMigrations(connectionString);
1018
+ const afterSecondRun = postgres(connectionString, { max: 1, onnotice: () => { } });
1019
+ try {
1020
+ const secondRunRows = await afterSecondRun.unsafe(`
1021
+ SELECT "updated_at"
1022
+ FROM "issues"
1023
+ WHERE "id" = '10000000-0000-0000-0000-000000000001'
1024
+ `);
1025
+ expect(secondRunRows[0]?.updated_at.toISOString()).toBe("2026-03-02T16:45:00.000Z");
1026
+ }
1027
+ finally {
1028
+ await afterSecondRun.end();
1029
+ }
1030
+ }, 20_000);
1031
+ it("replays the run responsible user repair migration when heartbeat run issue refs are identifiers", async () => {
1032
+ const connectionString = await createTempDatabase();
1033
+ await applyPendingMigrations(connectionString);
1034
+ const sql = postgres(connectionString, { max: 1, onnotice: () => { } });
1035
+ try {
1036
+ const runResponsibleUserRepairHash = await migrationHash("0131_repair_run_responsible_user_context_refs.sql");
1037
+ await sql.unsafe(`
1038
+ INSERT INTO "companies" ("id", "name", "issue_prefix", "created_at", "updated_at")
1039
+ VALUES ('00000000-0000-0000-0000-000000000130', 'Migration Test Co', 'TST130', now(), now())
1040
+ `);
1041
+ await sql.unsafe(`
1042
+ INSERT INTO "company_memberships" (
1043
+ "id",
1044
+ "company_id",
1045
+ "principal_type",
1046
+ "principal_id",
1047
+ "status",
1048
+ "membership_role",
1049
+ "created_at",
1050
+ "updated_at"
1051
+ )
1052
+ VALUES (
1053
+ '00000000-0000-0000-0000-000000000131',
1054
+ '00000000-0000-0000-0000-000000000130',
1055
+ 'user',
1056
+ 'owner-user',
1057
+ 'active',
1058
+ 'owner',
1059
+ now(),
1060
+ now()
1061
+ )
1062
+ `);
1063
+ await sql.unsafe(`
1064
+ INSERT INTO "agents" ("id", "company_id", "name", "role", "adapter_type", "created_at", "updated_at")
1065
+ VALUES (
1066
+ '00000000-0000-0000-0000-000000000132',
1067
+ '00000000-0000-0000-0000-000000000130',
1068
+ 'Migration Agent',
1069
+ 'general',
1070
+ 'process',
1071
+ now(),
1072
+ now()
1073
+ )
1074
+ `);
1075
+ await sql.unsafe(`
1076
+ INSERT INTO "issues" (
1077
+ "id",
1078
+ "company_id",
1079
+ "title",
1080
+ "status",
1081
+ "responsible_user_id",
1082
+ "identifier",
1083
+ "created_at",
1084
+ "updated_at"
1085
+ )
1086
+ VALUES (
1087
+ '00000000-0000-0000-0000-000000000133',
1088
+ '00000000-0000-0000-0000-000000000130',
1089
+ 'Identifier referenced issue',
1090
+ 'todo',
1091
+ 'issue-user',
1092
+ 'TST130-1',
1093
+ now(),
1094
+ now()
1095
+ )
1096
+ `);
1097
+ await sql.unsafe(`
1098
+ INSERT INTO "heartbeat_runs" (
1099
+ "id",
1100
+ "company_id",
1101
+ "agent_id",
1102
+ "status",
1103
+ "responsible_user_id",
1104
+ "context_snapshot",
1105
+ "created_at",
1106
+ "updated_at"
1107
+ )
1108
+ VALUES (
1109
+ '00000000-0000-0000-0000-000000000134',
1110
+ '00000000-0000-0000-0000-000000000130',
1111
+ '00000000-0000-0000-0000-000000000132',
1112
+ 'completed',
1113
+ NULL,
1114
+ '{"issueId":"TST130-1"}'::jsonb,
1115
+ now(),
1116
+ now()
1117
+ )
1118
+ `);
1119
+ await sql.unsafe(`DELETE FROM "drizzle"."__drizzle_migrations" WHERE hash = '${runResponsibleUserRepairHash}'`);
1120
+ }
1121
+ finally {
1122
+ await sql.end();
1123
+ }
1124
+ const pendingState = await inspectMigrations(connectionString);
1125
+ expect(pendingState).toMatchObject({
1126
+ status: "needsMigrations",
1127
+ pendingMigrations: ["0131_repair_run_responsible_user_context_refs.sql"],
1128
+ reason: "pending-migrations",
1129
+ });
1130
+ await applyPendingMigrations(connectionString);
1131
+ const verifySql = postgres(connectionString, { max: 1, onnotice: () => { } });
1132
+ try {
1133
+ const runs = await verifySql.unsafe(`
1134
+ SELECT "responsible_user_id"
1135
+ FROM "heartbeat_runs"
1136
+ WHERE "id" = '00000000-0000-0000-0000-000000000134'
1137
+ `);
1138
+ expect(runs).toEqual([{ responsible_user_id: "issue-user" }]);
1139
+ }
1140
+ finally {
1141
+ await verifySql.end();
1142
+ }
1143
+ }, 20_000);
401
1144
  });
402
1145
  //# sourceMappingURL=client.test.js.map