@agent-native/core 0.77.8 → 0.77.10

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 (189) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +29 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/task-store.ts +25 -3
  5. package/corpus/core/src/agent/harness/store.ts +89 -55
  6. package/corpus/core/src/agent/observational-memory/store.ts +40 -14
  7. package/corpus/core/src/agent/run-store.ts +116 -59
  8. package/corpus/core/src/application-state/store.ts +36 -2
  9. package/corpus/core/src/audit/store.ts +37 -6
  10. package/corpus/core/src/browser-sessions/store.ts +31 -12
  11. package/corpus/core/src/chat-threads/store.ts +67 -3
  12. package/corpus/core/src/checkpoints/store.ts +14 -3
  13. package/corpus/core/src/cli/create.ts +9 -0
  14. package/corpus/core/src/collab/storage.ts +20 -2
  15. package/corpus/core/src/db/ddl-guard.ts +112 -0
  16. package/corpus/core/src/extensions/store.ts +76 -3
  17. package/corpus/core/src/integrations/a2a-continuations-store.ts +69 -26
  18. package/corpus/core/src/integrations/config-store.ts +19 -18
  19. package/corpus/core/src/integrations/pending-tasks-store.ts +40 -16
  20. package/corpus/core/src/integrations/remote-commands-store.ts +37 -23
  21. package/corpus/core/src/integrations/remote-devices-store.ts +64 -20
  22. package/corpus/core/src/integrations/remote-push-store.ts +75 -35
  23. package/corpus/core/src/integrations/remote-run-events-store.ts +38 -12
  24. package/corpus/core/src/integrations/thread-mapping-store.ts +11 -2
  25. package/corpus/core/src/mcp/connect-store.ts +53 -16
  26. package/corpus/core/src/mcp/oauth-store.ts +32 -7
  27. package/corpus/core/src/notifications/store.ts +20 -4
  28. package/corpus/core/src/oauth-tokens/store.ts +45 -2
  29. package/corpus/core/src/observability/store.ts +154 -47
  30. package/corpus/core/src/progress/store.ts +36 -21
  31. package/corpus/core/src/provider-api/corpus-jobs-store.ts +25 -5
  32. package/corpus/core/src/provider-api/custom-registry.ts +15 -4
  33. package/corpus/core/src/provider-api/quota-governor.ts +15 -3
  34. package/corpus/core/src/provider-api/staged-datasets-store.ts +22 -5
  35. package/corpus/core/src/resources/store.ts +66 -31
  36. package/corpus/core/src/secrets/storage.ts +18 -22
  37. package/corpus/core/src/server/agent-teams-run-queue.ts +22 -10
  38. package/corpus/core/src/server/auth.ts +22 -4
  39. package/corpus/core/src/server/better-auth-instance.ts +49 -15
  40. package/corpus/core/src/server/embed-session.ts +19 -4
  41. package/corpus/core/src/server/identity-sso-store.ts +40 -11
  42. package/corpus/core/src/server/recap-image-store.ts +28 -9
  43. package/corpus/core/src/settings/store.ts +14 -19
  44. package/corpus/core/src/usage/store.ts +51 -16
  45. package/corpus/core/src/workspace-connections/store.ts +200 -22
  46. package/corpus/templates/calendar/changelog/2026-06-25-booking-link-previews-now-show-the-content-directly-on-the-g.md +6 -0
  47. package/corpus/templates/calendar/server/lib/booking-og-image.ts +0 -2
  48. package/corpus/templates/forms/changelog/2026-06-25-form-previews-now-show-the-content-directly-on-the-grid-back.md +6 -0
  49. package/corpus/templates/forms/server/lib/form-og-image.ts +0 -2
  50. package/corpus/templates/plan/app/global.css +6 -11
  51. package/corpus/templates/plan/app/pages/PlansPage.tsx +12 -10
  52. package/corpus/templates/plan/changelog/2026-06-25-plan-access-request-pages-stay-visible-during-background-ref.md +6 -0
  53. package/corpus/templates/plan/changelog/2026-06-25-plan-documents-keep-their-full-reading-width-below-1200px-an.md +6 -0
  54. package/dist/a2a/task-store.d.ts.map +1 -1
  55. package/dist/a2a/task-store.js +19 -3
  56. package/dist/a2a/task-store.js.map +1 -1
  57. package/dist/agent/harness/store.d.ts.map +1 -1
  58. package/dist/agent/harness/store.js +63 -34
  59. package/dist/agent/harness/store.js.map +1 -1
  60. package/dist/agent/observational-memory/store.d.ts.map +1 -1
  61. package/dist/agent/observational-memory/store.js +30 -8
  62. package/dist/agent/observational-memory/store.js.map +1 -1
  63. package/dist/agent/run-store.d.ts.map +1 -1
  64. package/dist/agent/run-store.js +103 -46
  65. package/dist/agent/run-store.js.map +1 -1
  66. package/dist/application-state/store.d.ts.map +1 -1
  67. package/dist/application-state/store.js +28 -2
  68. package/dist/application-state/store.js.map +1 -1
  69. package/dist/audit/store.d.ts.map +1 -1
  70. package/dist/audit/store.js +18 -3
  71. package/dist/audit/store.js.map +1 -1
  72. package/dist/browser-sessions/store.d.ts.map +1 -1
  73. package/dist/browser-sessions/store.js +22 -7
  74. package/dist/browser-sessions/store.js.map +1 -1
  75. package/dist/chat-threads/store.d.ts.map +1 -1
  76. package/dist/chat-threads/store.js +51 -3
  77. package/dist/chat-threads/store.js.map +1 -1
  78. package/dist/checkpoints/store.d.ts.map +1 -1
  79. package/dist/checkpoints/store.js +12 -3
  80. package/dist/checkpoints/store.js.map +1 -1
  81. package/dist/cli/create.d.ts.map +1 -1
  82. package/dist/cli/create.js +9 -0
  83. package/dist/cli/create.js.map +1 -1
  84. package/dist/collab/routes.d.ts +2 -2
  85. package/dist/collab/storage.d.ts.map +1 -1
  86. package/dist/collab/storage.js +14 -2
  87. package/dist/collab/storage.js.map +1 -1
  88. package/dist/db/ddl-guard.d.ts +63 -0
  89. package/dist/db/ddl-guard.d.ts.map +1 -1
  90. package/dist/db/ddl-guard.js +85 -0
  91. package/dist/db/ddl-guard.js.map +1 -1
  92. package/dist/extensions/store.d.ts.map +1 -1
  93. package/dist/extensions/store.js +32 -3
  94. package/dist/extensions/store.js.map +1 -1
  95. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  96. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  97. package/dist/integrations/a2a-continuations-store.js +44 -24
  98. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  99. package/dist/integrations/config-store.d.ts.map +1 -1
  100. package/dist/integrations/config-store.js +17 -11
  101. package/dist/integrations/config-store.js.map +1 -1
  102. package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
  103. package/dist/integrations/pending-tasks-store.js +24 -16
  104. package/dist/integrations/pending-tasks-store.js.map +1 -1
  105. package/dist/integrations/remote-commands-store.d.ts.map +1 -1
  106. package/dist/integrations/remote-commands-store.js +29 -21
  107. package/dist/integrations/remote-commands-store.js.map +1 -1
  108. package/dist/integrations/remote-devices-store.d.ts.map +1 -1
  109. package/dist/integrations/remote-devices-store.js +33 -18
  110. package/dist/integrations/remote-devices-store.js.map +1 -1
  111. package/dist/integrations/remote-push-store.d.ts.map +1 -1
  112. package/dist/integrations/remote-push-store.js +53 -31
  113. package/dist/integrations/remote-push-store.js.map +1 -1
  114. package/dist/integrations/remote-run-events-store.d.ts.map +1 -1
  115. package/dist/integrations/remote-run-events-store.js +26 -10
  116. package/dist/integrations/remote-run-events-store.js.map +1 -1
  117. package/dist/integrations/thread-mapping-store.d.ts.map +1 -1
  118. package/dist/integrations/thread-mapping-store.js +10 -2
  119. package/dist/integrations/thread-mapping-store.js.map +1 -1
  120. package/dist/mcp/connect-store.d.ts.map +1 -1
  121. package/dist/mcp/connect-store.js +34 -16
  122. package/dist/mcp/connect-store.js.map +1 -1
  123. package/dist/mcp/oauth-store.d.ts.map +1 -1
  124. package/dist/mcp/oauth-store.js +22 -7
  125. package/dist/mcp/oauth-store.js.map +1 -1
  126. package/dist/notifications/routes.d.ts +2 -2
  127. package/dist/notifications/store.d.ts.map +1 -1
  128. package/dist/notifications/store.js +15 -3
  129. package/dist/notifications/store.js.map +1 -1
  130. package/dist/oauth-tokens/store.d.ts.map +1 -1
  131. package/dist/oauth-tokens/store.js +33 -2
  132. package/dist/oauth-tokens/store.js.map +1 -1
  133. package/dist/observability/routes.d.ts +7 -7
  134. package/dist/observability/store.d.ts.map +1 -1
  135. package/dist/observability/store.js +72 -29
  136. package/dist/observability/store.js.map +1 -1
  137. package/dist/progress/routes.d.ts +1 -1
  138. package/dist/progress/store.d.ts.map +1 -1
  139. package/dist/progress/store.js +31 -16
  140. package/dist/progress/store.js.map +1 -1
  141. package/dist/provider-api/corpus-jobs-store.d.ts.map +1 -1
  142. package/dist/provider-api/corpus-jobs-store.js +16 -5
  143. package/dist/provider-api/corpus-jobs-store.js.map +1 -1
  144. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  145. package/dist/provider-api/custom-registry.js +14 -4
  146. package/dist/provider-api/custom-registry.js.map +1 -1
  147. package/dist/provider-api/quota-governor.d.ts.map +1 -1
  148. package/dist/provider-api/quota-governor.js +12 -3
  149. package/dist/provider-api/quota-governor.js.map +1 -1
  150. package/dist/provider-api/staged-datasets-store.d.ts.map +1 -1
  151. package/dist/provider-api/staged-datasets-store.js +16 -5
  152. package/dist/provider-api/staged-datasets-store.js.map +1 -1
  153. package/dist/resources/handlers.d.ts +3 -3
  154. package/dist/resources/store.d.ts.map +1 -1
  155. package/dist/resources/store.js +56 -25
  156. package/dist/resources/store.js.map +1 -1
  157. package/dist/secrets/storage.d.ts.map +1 -1
  158. package/dist/secrets/storage.js +10 -14
  159. package/dist/secrets/storage.js.map +1 -1
  160. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  161. package/dist/server/agent-teams-run-queue.d.ts.map +1 -1
  162. package/dist/server/agent-teams-run-queue.js +15 -4
  163. package/dist/server/agent-teams-run-queue.js.map +1 -1
  164. package/dist/server/auth.d.ts.map +1 -1
  165. package/dist/server/auth.js +16 -2
  166. package/dist/server/auth.js.map +1 -1
  167. package/dist/server/better-auth-instance.d.ts.map +1 -1
  168. package/dist/server/better-auth-instance.js +55 -21
  169. package/dist/server/better-auth-instance.js.map +1 -1
  170. package/dist/server/embed-session.d.ts.map +1 -1
  171. package/dist/server/embed-session.js +15 -4
  172. package/dist/server/embed-session.js.map +1 -1
  173. package/dist/server/identity-sso-store.d.ts.map +1 -1
  174. package/dist/server/identity-sso-store.js +30 -11
  175. package/dist/server/identity-sso-store.js.map +1 -1
  176. package/dist/server/recap-image-store.d.ts.map +1 -1
  177. package/dist/server/recap-image-store.js +21 -7
  178. package/dist/server/recap-image-store.js.map +1 -1
  179. package/dist/server/transcribe-voice.d.ts +1 -1
  180. package/dist/settings/store.d.ts.map +1 -1
  181. package/dist/settings/store.js +11 -13
  182. package/dist/settings/store.js.map +1 -1
  183. package/dist/usage/store.d.ts.map +1 -1
  184. package/dist/usage/store.js +36 -11
  185. package/dist/usage/store.js.map +1 -1
  186. package/dist/workspace-connections/store.d.ts.map +1 -1
  187. package/dist/workspace-connections/store.js +57 -7
  188. package/dist/workspace-connections/store.js.map +1 -1
  189. package/package.json +1 -1
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { getDbExec, intType, isPostgres, type DbExec } from "../db/client.js";
17
+ import { ensureTableExists, ensureIndexExists } from "../db/ddl-guard.js";
17
18
 
18
19
  // ---------------------------------------------------------------------------
19
20
  // Caps
@@ -32,7 +33,7 @@ async function ensureTables(): Promise<void> {
32
33
  _initPromise = (async () => {
33
34
  const db = getDbExec();
34
35
  const integerType = intType();
35
- await db.execute(`
36
+ const createDatasetsSql = `
36
37
  CREATE TABLE IF NOT EXISTS staged_datasets (
37
38
  id TEXT NOT NULL,
38
39
  app_id TEXT NOT NULL,
@@ -45,18 +46,34 @@ async function ensureTables(): Promise<void> {
45
46
  updated_at ${integerType} NOT NULL,
46
47
  PRIMARY KEY (id)
47
48
  )
48
- `);
49
- await db.execute(`
49
+ `;
50
+ const createRowsSql = `
50
51
  CREATE TABLE IF NOT EXISTS staged_dataset_rows (
51
52
  dataset_id TEXT NOT NULL,
52
53
  row_index ${integerType} NOT NULL,
53
54
  row_data TEXT NOT NULL,
54
55
  PRIMARY KEY (dataset_id, row_index)
55
56
  )
56
- `);
57
+ `;
57
58
  if (isPostgres()) {
58
- await widenPostgresIntegerColumns(db);
59
+ // PG guard: probe via information_schema, only issue DDL if missing, bounded lock_timeout
60
+ await ensureTableExists("staged_datasets", createDatasetsSql);
61
+ await ensureTableExists("staged_dataset_rows", createRowsSql);
62
+ await widenPostgresIntegerColumns(db); // best-effort type widening — unchanged
63
+ await ensureIndexExists(
64
+ "staged_datasets_scope_idx",
65
+ `CREATE INDEX IF NOT EXISTS staged_datasets_scope_idx ON staged_datasets (app_id, owner_email)`,
66
+ );
67
+ await ensureIndexExists(
68
+ "staged_dataset_rows_dataset_idx",
69
+ `CREATE INDEX IF NOT EXISTS staged_dataset_rows_dataset_idx ON staged_dataset_rows (dataset_id)`,
70
+ );
71
+ return;
59
72
  }
73
+ // SQLite (local dev): keep existing behavior
74
+ await db.execute(createDatasetsSql);
75
+ await db.execute(createRowsSql);
76
+ // Note: widenPostgresIntegerColumns is Postgres-only, skip on SQLite
60
77
  for (const ddl of [
61
78
  `CREATE INDEX IF NOT EXISTS staged_datasets_scope_idx ON staged_datasets (app_id, owner_email)`,
62
79
  `CREATE INDEX IF NOT EXISTS staged_dataset_rows_dataset_idx ON staged_dataset_rows (dataset_id)`,
@@ -7,6 +7,7 @@ import {
7
7
  retryOnDdlRace,
8
8
  type DbExec,
9
9
  } from "../db/client.js";
10
+ import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
10
11
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
11
12
  import {
12
13
  canUseLocalWorkspaceResourcePath,
@@ -751,37 +752,71 @@ async function ensureTable(): Promise<void> {
751
752
 
752
753
  async function _doEnsureTable(): Promise<void> {
753
754
  const client = getDbExec();
754
- await retryOnDdlRace(() =>
755
- client.execute(`
756
- CREATE TABLE IF NOT EXISTS resources (
757
- id TEXT PRIMARY KEY,
758
- path TEXT NOT NULL,
759
- owner TEXT NOT NULL,
760
- content TEXT NOT NULL DEFAULT '',
761
- mime_type TEXT NOT NULL DEFAULT 'text/markdown',
762
- size ${intType()} NOT NULL DEFAULT 0,
763
- created_at ${intType()} NOT NULL,
764
- updated_at ${intType()} NOT NULL,
765
- created_by TEXT NOT NULL DEFAULT 'user',
766
- visibility TEXT NOT NULL DEFAULT 'workspace',
767
- thread_id TEXT,
768
- run_id TEXT,
769
- expires_at ${intType()},
770
- metadata TEXT,
771
- UNIQUE(path, owner)
772
- )
773
- `),
774
- );
775
-
776
- await ensureResourceColumn(client, "created_by TEXT NOT NULL DEFAULT 'user'");
777
- await ensureResourceColumn(
778
- client,
779
- "visibility TEXT NOT NULL DEFAULT 'workspace'",
780
- );
781
- await ensureResourceColumn(client, "thread_id TEXT");
782
- await ensureResourceColumn(client, "run_id TEXT");
783
- await ensureResourceColumn(client, `expires_at ${intType()}`);
784
- await ensureResourceColumn(client, "metadata TEXT");
755
+ const createSql = `
756
+ CREATE TABLE IF NOT EXISTS resources (
757
+ id TEXT PRIMARY KEY,
758
+ path TEXT NOT NULL,
759
+ owner TEXT NOT NULL,
760
+ content TEXT NOT NULL DEFAULT '',
761
+ mime_type TEXT NOT NULL DEFAULT 'text/markdown',
762
+ size ${intType()} NOT NULL DEFAULT 0,
763
+ created_at ${intType()} NOT NULL,
764
+ updated_at ${intType()} NOT NULL,
765
+ created_by TEXT NOT NULL DEFAULT 'user',
766
+ visibility TEXT NOT NULL DEFAULT 'workspace',
767
+ thread_id TEXT,
768
+ run_id TEXT,
769
+ expires_at ${intType()},
770
+ metadata TEXT,
771
+ UNIQUE(path, owner)
772
+ )
773
+ `;
774
+
775
+ if (isPostgres()) {
776
+ // Hot path: the `resources` table and its additive columns are virtually
777
+ // always already present in production. Issuing `CREATE TABLE`/`ALTER
778
+ // TABLE` still takes an ACCESS EXCLUSIVE lock that, in a fresh
779
+ // background-worker process behind a concurrent connection on the shared
780
+ // Neon DB, can block ~indefinitely. The ensure* wrappers probe
781
+ // `information_schema` first (plain reads, no lock) and run DDL ONLY for
782
+ // what is actually missing, bounded by a transaction-scoped `lock_timeout`.
783
+ // If a swallowed lock-timeout leaves the schema still missing they RE-PROBE
784
+ // and THROW rather than letting init memoize success against absent schema.
785
+ await ensureTableExists("resources", createSql);
786
+ // Additive columns — guarded so the hot path (columns already present)
787
+ // skips the ACCESS EXCLUSIVE ALTER entirely.
788
+ const pgColumns: Array<[string, string]> = [
789
+ ["created_by", "TEXT NOT NULL DEFAULT 'user'"],
790
+ ["visibility", "TEXT NOT NULL DEFAULT 'workspace'"],
791
+ ["thread_id", "TEXT"],
792
+ ["run_id", "TEXT"],
793
+ ["expires_at", intType()],
794
+ ["metadata", "TEXT"],
795
+ ];
796
+ for (const [col, def] of pgColumns) {
797
+ await ensureColumnExists(
798
+ "resources",
799
+ col,
800
+ `ALTER TABLE resources ADD COLUMN IF NOT EXISTS ${col} ${def}`,
801
+ );
802
+ }
803
+ } else {
804
+ // SQLite (local dev): no lock problem — keep the original behaviour using
805
+ // `retryOnDdlRace` + `ensureResourceColumn` (duplicate-column catch).
806
+ await retryOnDdlRace(() => client.execute(createSql));
807
+ await ensureResourceColumn(
808
+ client,
809
+ "created_by TEXT NOT NULL DEFAULT 'user'",
810
+ );
811
+ await ensureResourceColumn(
812
+ client,
813
+ "visibility TEXT NOT NULL DEFAULT 'workspace'",
814
+ );
815
+ await ensureResourceColumn(client, "thread_id TEXT");
816
+ await ensureResourceColumn(client, "run_id TEXT");
817
+ await ensureResourceColumn(client, `expires_at ${intType()}`);
818
+ await ensureResourceColumn(client, "metadata TEXT");
819
+ }
785
820
 
786
821
  // Older deployments have 32-bit timestamp columns; on Postgres the
787
822
  // `Date.now()` written on insert/update overflows int4. Widen in place
@@ -15,11 +15,7 @@
15
15
  import { randomUUID } from "node:crypto";
16
16
 
17
17
  import { getDbExec, isPostgres } from "../db/client.js";
18
- import {
19
- pgColumnExists,
20
- pgTableExists,
21
- runGuardedDdl,
22
- } from "../db/ddl-guard.js";
18
+ import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
23
19
  import {
24
20
  encryptSecretValue as encryptValue,
25
21
  decryptSecretValue as decryptValue,
@@ -48,23 +44,23 @@ async function ensureTable(): Promise<void> {
48
44
  // virtually always already present. Issuing `CREATE`/`ALTER` would
49
45
  // still take an ACCESS EXCLUSIVE lock — which, in a fresh background
50
46
  // worker process behind a concurrent connection on the shared Neon DB,
51
- // can block ~indefinitely. So check `information_schema` first (a plain
52
- // read, no lock) and run DDL ONLY for what is actually missing. When
53
- // DDL must run, `runGuardedDdl` wraps it in a transaction-scoped
54
- // `lock_timeout` so a contended lock fails fast instead of hanging.
55
- if (!(await pgTableExists("app_secrets"))) {
56
- await runGuardedDdl(createSql);
57
- }
58
- if (!(await pgColumnExists("app_secrets", "description"))) {
59
- await runGuardedDdl(
60
- `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS description TEXT`,
61
- );
62
- }
63
- if (!(await pgColumnExists("app_secrets", "url_allowlist"))) {
64
- await runGuardedDdl(
65
- `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS url_allowlist TEXT`,
66
- );
67
- }
47
+ // can block ~indefinitely. `ensureTableExists` / `ensureColumnExists`
48
+ // check `information_schema` first (a plain read, no lock) and run DDL
49
+ // ONLY for what is actually missing, wrapping any DDL that must run in a
50
+ // transaction-scoped `lock_timeout` so a contended lock fails fast. They
51
+ // also re-probe after a swallowed lock-timeout and THROW if the schema
52
+ // is still missing, so a timed-out DDL never poisons this init memo.
53
+ await ensureTableExists("app_secrets", createSql);
54
+ await ensureColumnExists(
55
+ "app_secrets",
56
+ "description",
57
+ `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS description TEXT`,
58
+ );
59
+ await ensureColumnExists(
60
+ "app_secrets",
61
+ "url_allowlist",
62
+ `ALTER TABLE app_secrets ADD COLUMN IF NOT EXISTS url_allowlist TEXT`,
63
+ );
68
64
  return;
69
65
  }
70
66
 
@@ -14,7 +14,13 @@
14
14
  * for UI/status; this table only drives dispatch, idempotent claiming, and
15
15
  * cross-invocation continuation.
16
16
  */
17
- import { getDbExec, intType, retryOnDdlRace } from "../db/client.js";
17
+ import {
18
+ getDbExec,
19
+ intType,
20
+ isPostgres,
21
+ retryOnDdlRace,
22
+ } from "../db/client.js";
23
+ import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
18
24
 
19
25
  /** Max cross-invocation continuations for one sub-agent run. Each continuation
20
26
  * is one ~40s soft-timeout chunk, so ~60 ≈ ~40 minutes of wall-clock work. Two
@@ -77,8 +83,7 @@ async function ensureTable(): Promise<void> {
77
83
  if (!_initPromise) {
78
84
  _initPromise = (async () => {
79
85
  const client = getDbExec();
80
- await retryOnDdlRace(() =>
81
- client.execute(`
86
+ const createSql = `
82
87
  CREATE TABLE IF NOT EXISTS agent_team_run_queue (
83
88
  task_id TEXT PRIMARY KEY,
84
89
  thread_id TEXT NOT NULL,
@@ -92,13 +97,20 @@ async function ensureTable(): Promise<void> {
92
97
  created_at ${intType()} NOT NULL,
93
98
  updated_at ${intType()} NOT NULL
94
99
  )
95
- `),
96
- );
97
- await retryOnDdlRace(() =>
98
- client.execute(
99
- `CREATE INDEX IF NOT EXISTS idx_agent_team_run_queue_status ON agent_team_run_queue (status, updated_at)`,
100
- ),
101
- );
100
+ `;
101
+ const indexSql = `CREATE INDEX IF NOT EXISTS idx_agent_team_run_queue_status ON agent_team_run_queue (status, updated_at)`;
102
+
103
+ // PG guard: probe information_schema / pg_indexes first (no lock), run
104
+ // DDL only when missing, bounded by a transaction-scoped lock_timeout.
105
+ if (isPostgres()) {
106
+ await ensureTableExists("agent_team_run_queue", createSql);
107
+ await ensureIndexExists("idx_agent_team_run_queue_status", indexSql);
108
+ return;
109
+ }
110
+
111
+ // SQLite (local dev): no lock problem — keep the original behaviour.
112
+ await retryOnDdlRace(() => client.execute(createSql));
113
+ await retryOnDdlRace(() => client.execute(indexSql));
102
114
  })().catch((err) => {
103
115
  _initPromise = undefined;
104
116
  throw err;
@@ -72,6 +72,7 @@ import {
72
72
  retryOnDdlRace,
73
73
  describeDbError,
74
74
  } from "../db/client.js";
75
+ import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
75
76
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
76
77
  import { readBody } from "../server/h3-helpers.js";
77
78
  import { putSetting } from "../settings/store.js";
@@ -865,15 +866,32 @@ async function ensureSessionTable(): Promise<void> {
865
866
  if (!_sessionInitPromise) {
866
867
  _sessionInitPromise = (async () => {
867
868
  const client = getDbExec();
868
- await retryOnDdlRace(() =>
869
- client.execute(`
869
+ const createSql = `
870
870
  CREATE TABLE IF NOT EXISTS sessions (
871
871
  token TEXT PRIMARY KEY,
872
872
  email TEXT,
873
873
  created_at ${intType()} NOT NULL
874
874
  )
875
- `),
876
- );
875
+ `;
876
+
877
+ // PG guard: probe information_schema first (no lock), run DDL only when
878
+ // missing, bounded by a transaction-scoped lock_timeout.
879
+ if (isPostgres()) {
880
+ await ensureTableExists("sessions", createSql);
881
+ await ensureColumnExists(
882
+ "sessions",
883
+ "email",
884
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS email TEXT`,
885
+ );
886
+ // Older deployments have a 32-bit `created_at`; on Postgres the
887
+ // `Date.now()` written on session create overflows int4. Widen in place
888
+ // (no-op once done / on fresh DBs).
889
+ await widenIntColumnsToBigInt("sessions", ["created_at"]);
890
+ return;
891
+ }
892
+
893
+ // SQLite (local dev): no lock problem — keep the original behaviour.
894
+ await retryOnDdlRace(() => client.execute(createSql));
877
895
  try {
878
896
  await client.execute(`ALTER TABLE sessions ADD COLUMN email TEXT`);
879
897
  } catch {
@@ -39,6 +39,7 @@ import {
39
39
  neonPoolMax,
40
40
  attachNeonPoolErrorLogger,
41
41
  } from "../db/client.js";
42
+ import { ensureTableExists } from "../db/ddl-guard.js";
42
43
  import { saveOAuthTokens } from "../oauth-tokens/store.js";
43
44
  import { acceptPendingInvitationsForEmail } from "../org/accept-pending.js";
44
45
  import { autoJoinDomainMatchingOrgs } from "../org/auto-join-domain.js";
@@ -693,29 +694,62 @@ async function mirrorGoogleAccountToOAuthTokens(account: {
693
694
 
694
695
  async function ensureBetterAuthTables(): Promise<void> {
695
696
  const db = getDbExec();
696
- const statements = isPostgres()
697
- ? [
697
+
698
+ // PG guard: probe information_schema first (no lock) for each table; run
699
+ // DDL only when missing, bounded by a transaction-scoped lock_timeout.
700
+ // Probe names are UNQUOTED (what information_schema.tables.table_name stores);
701
+ // createSql keeps the QUOTED "user"/"session"/… form required by Postgres.
702
+ if (isPostgres()) {
703
+ const pgTables: Array<[name: string, createSql: string]> = [
704
+ [
705
+ "user",
698
706
  `CREATE TABLE IF NOT EXISTS "user" (id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, email_verified BOOLEAN NOT NULL DEFAULT FALSE, image TEXT, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL)`,
707
+ ],
708
+ [
709
+ "session",
699
710
  `CREATE TABLE IF NOT EXISTS "session" (id TEXT PRIMARY KEY, expires_at TIMESTAMPTZ NOT NULL, token TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, ip_address TEXT, user_agent TEXT, user_id TEXT NOT NULL, active_organization_id TEXT)`,
711
+ ],
712
+ [
713
+ "account",
700
714
  `CREATE TABLE IF NOT EXISTS "account" (id TEXT PRIMARY KEY, account_id TEXT NOT NULL, provider_id TEXT NOT NULL, user_id TEXT NOT NULL, access_token TEXT, refresh_token TEXT, id_token TEXT, access_token_expires_at TIMESTAMPTZ, refresh_token_expires_at TIMESTAMPTZ, scope TEXT, password TEXT, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL)`,
715
+ ],
716
+ [
717
+ "verification",
701
718
  `CREATE TABLE IF NOT EXISTS "verification" (id TEXT PRIMARY KEY, identifier TEXT NOT NULL, value TEXT NOT NULL, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL)`,
719
+ ],
720
+ [
721
+ "organization",
702
722
  `CREATE TABLE IF NOT EXISTS "organization" (id TEXT PRIMARY KEY, name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, logo TEXT, metadata TEXT, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL)`,
723
+ ],
724
+ [
725
+ "member",
703
726
  `CREATE TABLE IF NOT EXISTS "member" (id TEXT PRIMARY KEY, organization_id TEXT NOT NULL, user_id TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'member', created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL)`,
727
+ ],
728
+ [
729
+ "invitation",
704
730
  `CREATE TABLE IF NOT EXISTS "invitation" (id TEXT PRIMARY KEY, organization_id TEXT NOT NULL, email TEXT NOT NULL, role TEXT, status TEXT NOT NULL DEFAULT 'pending', expires_at TIMESTAMPTZ NOT NULL, inviter_id TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL)`,
731
+ ],
732
+ [
733
+ "jwks",
705
734
  `CREATE TABLE IF NOT EXISTS "jwks" (id TEXT PRIMARY KEY, public_key TEXT NOT NULL, private_key TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL, expires_at TIMESTAMPTZ)`,
706
- ]
707
- : [
708
- `CREATE TABLE IF NOT EXISTS user (id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, email_verified INTEGER NOT NULL DEFAULT 0, image TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
709
- `CREATE TABLE IF NOT EXISTS session (id TEXT PRIMARY KEY, expires_at INTEGER NOT NULL, token TEXT NOT NULL UNIQUE, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, ip_address TEXT, user_agent TEXT, user_id TEXT NOT NULL, active_organization_id TEXT)`,
710
- `CREATE TABLE IF NOT EXISTS account (id TEXT PRIMARY KEY, account_id TEXT NOT NULL, provider_id TEXT NOT NULL, user_id TEXT NOT NULL, access_token TEXT, refresh_token TEXT, id_token TEXT, access_token_expires_at INTEGER, refresh_token_expires_at INTEGER, scope TEXT, password TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
711
- `CREATE TABLE IF NOT EXISTS verification (id TEXT PRIMARY KEY, identifier TEXT NOT NULL, value TEXT NOT NULL, expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
712
- `CREATE TABLE IF NOT EXISTS organization (id TEXT PRIMARY KEY, name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, logo TEXT, metadata TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
713
- `CREATE TABLE IF NOT EXISTS member (id TEXT PRIMARY KEY, organization_id TEXT NOT NULL, user_id TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'member', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
714
- `CREATE TABLE IF NOT EXISTS invitation (id TEXT PRIMARY KEY, organization_id TEXT NOT NULL, email TEXT NOT NULL, role TEXT, status TEXT NOT NULL DEFAULT 'pending', expires_at INTEGER NOT NULL, inviter_id TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
715
- `CREATE TABLE IF NOT EXISTS jwks (id TEXT PRIMARY KEY, public_key TEXT NOT NULL, private_key TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER)`,
716
- ];
717
-
718
- for (const sql of statements) await db.execute(sql);
735
+ ],
736
+ ];
737
+ for (const [name, sql] of pgTables) await ensureTableExists(name, sql);
738
+ return;
739
+ }
740
+
741
+ // SQLite (local dev): no lock problem keep the original behaviour.
742
+ const sqliteStatements = [
743
+ `CREATE TABLE IF NOT EXISTS user (id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, email_verified INTEGER NOT NULL DEFAULT 0, image TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
744
+ `CREATE TABLE IF NOT EXISTS session (id TEXT PRIMARY KEY, expires_at INTEGER NOT NULL, token TEXT NOT NULL UNIQUE, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, ip_address TEXT, user_agent TEXT, user_id TEXT NOT NULL, active_organization_id TEXT)`,
745
+ `CREATE TABLE IF NOT EXISTS account (id TEXT PRIMARY KEY, account_id TEXT NOT NULL, provider_id TEXT NOT NULL, user_id TEXT NOT NULL, access_token TEXT, refresh_token TEXT, id_token TEXT, access_token_expires_at INTEGER, refresh_token_expires_at INTEGER, scope TEXT, password TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
746
+ `CREATE TABLE IF NOT EXISTS verification (id TEXT PRIMARY KEY, identifier TEXT NOT NULL, value TEXT NOT NULL, expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
747
+ `CREATE TABLE IF NOT EXISTS organization (id TEXT PRIMARY KEY, name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, logo TEXT, metadata TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
748
+ `CREATE TABLE IF NOT EXISTS member (id TEXT PRIMARY KEY, organization_id TEXT NOT NULL, user_id TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'member', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
749
+ `CREATE TABLE IF NOT EXISTS invitation (id TEXT PRIMARY KEY, organization_id TEXT NOT NULL, email TEXT NOT NULL, role TEXT, status TEXT NOT NULL DEFAULT 'pending', expires_at INTEGER NOT NULL, inviter_id TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`,
750
+ `CREATE TABLE IF NOT EXISTS jwks (id TEXT PRIMARY KEY, public_key TEXT NOT NULL, private_key TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER)`,
751
+ ];
752
+ for (const sql of sqliteStatements) await db.execute(sql);
719
753
  }
720
754
 
721
755
  /**
@@ -9,7 +9,8 @@ import {
9
9
  setResponseHeader,
10
10
  } from "h3";
11
11
 
12
- import { getDbExec, intType } from "../db/client.js";
12
+ import { getDbExec, intType, isPostgres } from "../db/client.js";
13
+ import { ensureTableExists } from "../db/ddl-guard.js";
13
14
  import {
14
15
  EMBED_MODE_QUERY_PARAM,
15
16
  EMBED_SESSION_COOKIE,
@@ -103,8 +104,10 @@ export type ResolvedEmbedSession = {
103
104
  async function ensureTable(): Promise<void> {
104
105
  if (!_initPromise) {
105
106
  _initPromise = (async () => {
106
- const client = getDbExec();
107
- await client.execute(`
107
+ // Build the CREATE SQL here (not at module scope) so intType() runs at
108
+ // RUNTIME, not import time — a module-scope call breaks any consumer whose
109
+ // db/client mock doesn't stub intType (e.g. db-admin specs).
110
+ const embedTicketsCreateSql = `
108
111
  CREATE TABLE IF NOT EXISTS agent_native_embed_tickets (
109
112
  ticket_hash TEXT PRIMARY KEY,
110
113
  owner_email TEXT NOT NULL,
@@ -115,7 +118,19 @@ async function ensureTable(): Promise<void> {
115
118
  expires_at ${intType()} NOT NULL,
116
119
  consumed_at ${intType()}
117
120
  )
118
- `);
121
+ `;
122
+ if (isPostgres()) {
123
+ // PG guard: probe → guarded DDL → re-probe; skips lock on already-migrated path
124
+ await ensureTableExists(
125
+ "agent_native_embed_tickets",
126
+ embedTicketsCreateSql,
127
+ );
128
+ return;
129
+ }
130
+
131
+ // SQLite (local dev): no lock problem — keep the original behaviour.
132
+ const client = getDbExec();
133
+ await client.execute(embedTicketsCreateSql);
119
134
  })().catch((err) => {
120
135
  _initPromise = undefined;
121
136
  throw err;
@@ -24,7 +24,13 @@
24
24
 
25
25
  import { randomBytes } from "node:crypto";
26
26
 
27
- import { getDbExec, isConnectionError, intType } from "../db/client.js";
27
+ import {
28
+ getDbExec,
29
+ isConnectionError,
30
+ intType,
31
+ isPostgres,
32
+ } from "../db/client.js";
33
+ import { ensureTableExists } from "../db/ddl-guard.js";
28
34
 
29
35
  let _initPromise: Promise<void> | undefined;
30
36
 
@@ -98,13 +104,11 @@ export const SSO_STATE_TTL_MS = 10 * 60_000;
98
104
  export const SSO_LOGIN_MAX = 60;
99
105
  export const SSO_LOGIN_WINDOW_MS = 60_000;
100
106
 
101
- async function ensureTable(): Promise<void> {
102
- if (!_initPromise) {
103
- _initPromise = (async () => {
104
- const client = getDbExec();
105
- // Additive only. Never DROP / ALTER — this DB is shared across every
106
- // deploy context (preview/branch/prod) for hosted templates.
107
- await client.execute(`
107
+ // Build the CREATE SQL lazily (not at module scope) so intType() runs at
108
+ // RUNTIME, not import time — a module-scope call breaks any consumer whose
109
+ // db/client mock doesn't stub intType (e.g. db-admin specs).
110
+ function buildIdentitySsoStateCreateSql(): string {
111
+ return `
108
112
  CREATE TABLE IF NOT EXISTS identity_sso_state (
109
113
  state TEXT PRIMARY KEY,
110
114
  return_path TEXT,
@@ -112,13 +116,38 @@ async function ensureTable(): Promise<void> {
112
116
  expires_at ${intType()},
113
117
  consumed_at ${intType()}
114
118
  )
115
- `);
116
- await client.execute(`
119
+ `;
120
+ }
121
+ function buildIdentitySsoJtiCreateSql(): string {
122
+ return `
117
123
  CREATE TABLE IF NOT EXISTS identity_sso_jti (
118
124
  jti TEXT PRIMARY KEY,
119
125
  seen_at ${intType()}
120
126
  )
121
- `);
127
+ `;
128
+ }
129
+
130
+ async function ensureTable(): Promise<void> {
131
+ if (!_initPromise) {
132
+ _initPromise = (async () => {
133
+ const client = getDbExec();
134
+ const identitySsoStateCreateSql = buildIdentitySsoStateCreateSql();
135
+ const identitySsoJtiCreateSql = buildIdentitySsoJtiCreateSql();
136
+ // Additive only. Never DROP / ALTER — this DB is shared across every
137
+ // deploy context (preview/branch/prod) for hosted templates.
138
+ if (isPostgres()) {
139
+ // PG guard: probe → guarded DDL → re-probe; skips lock on already-migrated path
140
+ await ensureTableExists(
141
+ "identity_sso_state",
142
+ identitySsoStateCreateSql,
143
+ );
144
+ await ensureTableExists("identity_sso_jti", identitySsoJtiCreateSql);
145
+ return;
146
+ }
147
+
148
+ // SQLite (local dev): no lock problem — keep the original behaviour.
149
+ await client.execute(identitySsoStateCreateSql);
150
+ await client.execute(identitySsoJtiCreateSql);
122
151
  })().catch((err) => {
123
152
  // Don't cache a rejection — let the next caller retry a fresh init.
124
153
  _initPromise = undefined;
@@ -19,7 +19,13 @@
19
19
  */
20
20
  import { randomBytes } from "node:crypto";
21
21
 
22
- import { getDbExec, intType, retryOnDdlRace } from "../db/client.js";
22
+ import {
23
+ getDbExec,
24
+ intType,
25
+ isPostgres,
26
+ retryOnDdlRace,
27
+ } from "../db/client.js";
28
+ import { ensureTableExists } from "../db/ddl-guard.js";
23
29
 
24
30
  /** Maximum stored image size (~5 MB of raw PNG bytes). */
25
31
  export const RECAP_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
@@ -44,12 +50,11 @@ const TOKEN_PATTERN = /^[0-9a-f]{32,128}$/;
44
50
 
45
51
  let _initPromise: Promise<void> | undefined;
46
52
 
47
- export async function ensureRecapImageTable(): Promise<void> {
48
- if (!_initPromise) {
49
- _initPromise = (async () => {
50
- const client = getDbExec();
51
- await retryOnDdlRace(() =>
52
- client.execute(`
53
+ // Build the CREATE SQL lazily (not at module scope) so intType() runs at
54
+ // RUNTIME, not import time — a module-scope call breaks any consumer whose
55
+ // db/client mock doesn't stub intType (e.g. db-admin specs).
56
+ function buildRecapImagesCreateSql(): string {
57
+ return `
53
58
  CREATE TABLE IF NOT EXISTS recap_images (
54
59
  token TEXT PRIMARY KEY,
55
60
  png_base64 TEXT NOT NULL,
@@ -58,8 +63,22 @@ export async function ensureRecapImageTable(): Promise<void> {
58
63
  owner_email TEXT,
59
64
  created_at ${intType()} NOT NULL
60
65
  )
61
- `),
62
- );
66
+ `;
67
+ }
68
+
69
+ export async function ensureRecapImageTable(): Promise<void> {
70
+ if (!_initPromise) {
71
+ _initPromise = (async () => {
72
+ const recapImagesCreateSql = buildRecapImagesCreateSql();
73
+ if (isPostgres()) {
74
+ // PG guard: probe → guarded DDL → re-probe; skips lock on already-migrated path
75
+ await ensureTableExists("recap_images", recapImagesCreateSql);
76
+ return;
77
+ }
78
+
79
+ // SQLite (local dev): no lock problem — keep the original behaviour.
80
+ const client = getDbExec();
81
+ await retryOnDdlRace(() => client.execute(recapImagesCreateSql));
63
82
  })().catch((error) => {
64
83
  // Allow a later call to retry if the first init lost a DDL race.
65
84
  _initPromise = undefined;
@@ -1,11 +1,7 @@
1
1
  import { EventEmitter } from "events";
2
2
 
3
3
  import { getDbExec, isPostgres, intType } from "../db/client.js";
4
- import {
5
- pgIndexExists,
6
- pgTableExists,
7
- runGuardedDdl,
8
- } from "../db/ddl-guard.js";
4
+ import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
9
5
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
10
6
 
11
7
  let _initPromise: Promise<void> | undefined;
@@ -39,26 +35,25 @@ async function ensureTable(): Promise<void> {
39
35
  // `CREATE INDEX` still takes a lock that, in a fresh background-worker
40
36
  // process behind a concurrent connection on the shared Neon DB, can
41
37
  // block ~indefinitely (ACCESS EXCLUSIVE for CREATE TABLE; a write-
42
- // blocking SHARE lock for CREATE INDEX). So check `information_schema`/
43
- // `pg_indexes` first (plain reads, no lock) and run DDL ONLY for what
44
- // is actually missing. `runGuardedDdl` bounds any DDL that must run
45
- // with a transaction-scoped `lock_timeout` so a contended lock fails
46
- // fast instead of hanging. `settingsTable()` is `public.settings` on
47
- // Postgres; the existence checks use the unqualified table name.
48
- if (!(await pgTableExists("settings"))) {
49
- await runGuardedDdl(createSql);
50
- }
38
+ // blocking SHARE lock for CREATE INDEX). `ensureTableExists` /
39
+ // `ensureIndexExists` probe `information_schema`/`pg_indexes` first
40
+ // (plain reads, no lock) and run DDL ONLY for what is actually missing,
41
+ // bounding any DDL with a transaction-scoped `lock_timeout`. They also
42
+ // re-probe after a swallowed lock-timeout and THROW if the schema is
43
+ // still missing, so a timed-out DDL never poisons this init memo with
44
+ // missing schema. `settingsTable()` is `public.settings` on Postgres;
45
+ // the existence checks use the unqualified table name.
46
+ await ensureTableExists("settings", createSql);
51
47
  // Older deployments (pre BIGINT-compat) have a 32-bit `updated_at`; on
52
48
  // Postgres the `Date.now()` written on every setSetting overflows int4.
53
49
  // widenIntColumnsToBigInt already probes information_schema and only
54
50
  // ALTERs columns that are still int4 — a no-op on fresh/widened DBs.
55
51
  await widenIntColumnsToBigInt("settings", ["updated_at"]);
56
52
  // Index for the poll watermark query: `SELECT MAX(updated_at)`.
57
- if (!(await pgIndexExists("settings_updated_at_idx"))) {
58
- await runGuardedDdl(
59
- `CREATE INDEX IF NOT EXISTS settings_updated_at_idx ON ${table} (updated_at)`,
60
- );
61
- }
53
+ await ensureIndexExists(
54
+ "settings_updated_at_idx",
55
+ `CREATE INDEX IF NOT EXISTS settings_updated_at_idx ON ${table} (updated_at)`,
56
+ );
62
57
  return;
63
58
  }
64
59