@agent-native/core 0.77.7 → 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 (192) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +46 -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/production-agent.ts +18 -0
  8. package/corpus/core/src/agent/run-store.ts +116 -59
  9. package/corpus/core/src/application-state/store.ts +36 -2
  10. package/corpus/core/src/audit/store.ts +37 -6
  11. package/corpus/core/src/browser-sessions/store.ts +31 -12
  12. package/corpus/core/src/chat-threads/store.ts +67 -3
  13. package/corpus/core/src/checkpoints/store.ts +14 -3
  14. package/corpus/core/src/cli/create.ts +9 -0
  15. package/corpus/core/src/collab/storage.ts +20 -2
  16. package/corpus/core/src/db/ddl-guard.ts +302 -0
  17. package/corpus/core/src/extensions/store.ts +76 -3
  18. package/corpus/core/src/integrations/a2a-continuations-store.ts +69 -26
  19. package/corpus/core/src/integrations/config-store.ts +19 -18
  20. package/corpus/core/src/integrations/pending-tasks-store.ts +40 -16
  21. package/corpus/core/src/integrations/remote-commands-store.ts +37 -23
  22. package/corpus/core/src/integrations/remote-devices-store.ts +64 -20
  23. package/corpus/core/src/integrations/remote-push-store.ts +75 -35
  24. package/corpus/core/src/integrations/remote-run-events-store.ts +38 -12
  25. package/corpus/core/src/integrations/thread-mapping-store.ts +11 -2
  26. package/corpus/core/src/mcp/connect-store.ts +53 -16
  27. package/corpus/core/src/mcp/oauth-store.ts +32 -7
  28. package/corpus/core/src/notifications/store.ts +20 -4
  29. package/corpus/core/src/oauth-tokens/store.ts +45 -2
  30. package/corpus/core/src/observability/store.ts +154 -47
  31. package/corpus/core/src/progress/store.ts +36 -21
  32. package/corpus/core/src/provider-api/corpus-jobs-store.ts +25 -5
  33. package/corpus/core/src/provider-api/custom-registry.ts +15 -4
  34. package/corpus/core/src/provider-api/quota-governor.ts +15 -3
  35. package/corpus/core/src/provider-api/staged-datasets-store.ts +22 -5
  36. package/corpus/core/src/resources/store.ts +66 -31
  37. package/corpus/core/src/secrets/storage.ts +32 -2
  38. package/corpus/core/src/server/agent-teams-run-queue.ts +22 -10
  39. package/corpus/core/src/server/auth.ts +22 -4
  40. package/corpus/core/src/server/better-auth-instance.ts +49 -15
  41. package/corpus/core/src/server/embed-session.ts +19 -4
  42. package/corpus/core/src/server/identity-sso-store.ts +40 -11
  43. package/corpus/core/src/server/recap-image-store.ts +28 -9
  44. package/corpus/core/src/settings/store.ts +35 -6
  45. package/corpus/core/src/usage/store.ts +51 -16
  46. package/corpus/core/src/workspace-connections/store.ts +200 -22
  47. package/corpus/templates/calendar/changelog/2026-06-25-booking-link-previews-now-show-the-content-directly-on-the-g.md +6 -0
  48. package/corpus/templates/calendar/server/lib/booking-og-image.ts +0 -2
  49. package/corpus/templates/forms/changelog/2026-06-25-form-previews-now-show-the-content-directly-on-the-grid-back.md +6 -0
  50. package/corpus/templates/forms/server/lib/form-og-image.ts +0 -2
  51. package/corpus/templates/plan/app/global.css +6 -11
  52. package/corpus/templates/plan/app/pages/PlansPage.tsx +12 -10
  53. package/corpus/templates/plan/changelog/2026-06-25-plan-access-request-pages-stay-visible-during-background-ref.md +6 -0
  54. package/corpus/templates/plan/changelog/2026-06-25-plan-documents-keep-their-full-reading-width-below-1200px-an.md +6 -0
  55. package/dist/a2a/task-store.d.ts.map +1 -1
  56. package/dist/a2a/task-store.js +19 -3
  57. package/dist/a2a/task-store.js.map +1 -1
  58. package/dist/agent/harness/store.d.ts.map +1 -1
  59. package/dist/agent/harness/store.js +63 -34
  60. package/dist/agent/harness/store.js.map +1 -1
  61. package/dist/agent/observational-memory/store.d.ts.map +1 -1
  62. package/dist/agent/observational-memory/store.js +30 -8
  63. package/dist/agent/observational-memory/store.js.map +1 -1
  64. package/dist/agent/production-agent.d.ts.map +1 -1
  65. package/dist/agent/production-agent.js +18 -0
  66. package/dist/agent/production-agent.js.map +1 -1
  67. package/dist/agent/run-store.d.ts.map +1 -1
  68. package/dist/agent/run-store.js +103 -46
  69. package/dist/agent/run-store.js.map +1 -1
  70. package/dist/application-state/store.d.ts.map +1 -1
  71. package/dist/application-state/store.js +28 -2
  72. package/dist/application-state/store.js.map +1 -1
  73. package/dist/audit/store.d.ts.map +1 -1
  74. package/dist/audit/store.js +18 -3
  75. package/dist/audit/store.js.map +1 -1
  76. package/dist/browser-sessions/store.d.ts.map +1 -1
  77. package/dist/browser-sessions/store.js +22 -7
  78. package/dist/browser-sessions/store.js.map +1 -1
  79. package/dist/chat-threads/store.d.ts.map +1 -1
  80. package/dist/chat-threads/store.js +51 -3
  81. package/dist/chat-threads/store.js.map +1 -1
  82. package/dist/checkpoints/store.d.ts.map +1 -1
  83. package/dist/checkpoints/store.js +12 -3
  84. package/dist/checkpoints/store.js.map +1 -1
  85. package/dist/cli/create.d.ts.map +1 -1
  86. package/dist/cli/create.js +9 -0
  87. package/dist/cli/create.js.map +1 -1
  88. package/dist/collab/routes.d.ts +2 -2
  89. package/dist/collab/storage.d.ts.map +1 -1
  90. package/dist/collab/storage.js +14 -2
  91. package/dist/collab/storage.js.map +1 -1
  92. package/dist/db/ddl-guard.d.ts +151 -0
  93. package/dist/db/ddl-guard.d.ts.map +1 -0
  94. package/dist/db/ddl-guard.js +265 -0
  95. package/dist/db/ddl-guard.js.map +1 -0
  96. package/dist/extensions/store.d.ts.map +1 -1
  97. package/dist/extensions/store.js +32 -3
  98. package/dist/extensions/store.js.map +1 -1
  99. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  100. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  101. package/dist/integrations/a2a-continuations-store.js +44 -24
  102. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  103. package/dist/integrations/config-store.d.ts.map +1 -1
  104. package/dist/integrations/config-store.js +17 -11
  105. package/dist/integrations/config-store.js.map +1 -1
  106. package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
  107. package/dist/integrations/pending-tasks-store.js +24 -16
  108. package/dist/integrations/pending-tasks-store.js.map +1 -1
  109. package/dist/integrations/remote-commands-store.d.ts.map +1 -1
  110. package/dist/integrations/remote-commands-store.js +29 -21
  111. package/dist/integrations/remote-commands-store.js.map +1 -1
  112. package/dist/integrations/remote-devices-store.d.ts.map +1 -1
  113. package/dist/integrations/remote-devices-store.js +33 -18
  114. package/dist/integrations/remote-devices-store.js.map +1 -1
  115. package/dist/integrations/remote-push-store.d.ts.map +1 -1
  116. package/dist/integrations/remote-push-store.js +53 -31
  117. package/dist/integrations/remote-push-store.js.map +1 -1
  118. package/dist/integrations/remote-run-events-store.d.ts.map +1 -1
  119. package/dist/integrations/remote-run-events-store.js +26 -10
  120. package/dist/integrations/remote-run-events-store.js.map +1 -1
  121. package/dist/integrations/thread-mapping-store.d.ts.map +1 -1
  122. package/dist/integrations/thread-mapping-store.js +10 -2
  123. package/dist/integrations/thread-mapping-store.js.map +1 -1
  124. package/dist/mcp/connect-store.d.ts.map +1 -1
  125. package/dist/mcp/connect-store.js +34 -16
  126. package/dist/mcp/connect-store.js.map +1 -1
  127. package/dist/mcp/oauth-store.d.ts.map +1 -1
  128. package/dist/mcp/oauth-store.js +22 -7
  129. package/dist/mcp/oauth-store.js.map +1 -1
  130. package/dist/notifications/routes.d.ts +2 -2
  131. package/dist/notifications/store.d.ts.map +1 -1
  132. package/dist/notifications/store.js +15 -3
  133. package/dist/notifications/store.js.map +1 -1
  134. package/dist/oauth-tokens/store.d.ts.map +1 -1
  135. package/dist/oauth-tokens/store.js +33 -2
  136. package/dist/oauth-tokens/store.js.map +1 -1
  137. package/dist/observability/routes.d.ts +7 -7
  138. package/dist/observability/store.d.ts.map +1 -1
  139. package/dist/observability/store.js +72 -29
  140. package/dist/observability/store.js.map +1 -1
  141. package/dist/progress/store.d.ts.map +1 -1
  142. package/dist/progress/store.js +31 -16
  143. package/dist/progress/store.js.map +1 -1
  144. package/dist/provider-api/corpus-jobs-store.d.ts.map +1 -1
  145. package/dist/provider-api/corpus-jobs-store.js +16 -5
  146. package/dist/provider-api/corpus-jobs-store.js.map +1 -1
  147. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  148. package/dist/provider-api/custom-registry.js +14 -4
  149. package/dist/provider-api/custom-registry.js.map +1 -1
  150. package/dist/provider-api/quota-governor.d.ts.map +1 -1
  151. package/dist/provider-api/quota-governor.js +12 -3
  152. package/dist/provider-api/quota-governor.js.map +1 -1
  153. package/dist/provider-api/staged-datasets-store.d.ts.map +1 -1
  154. package/dist/provider-api/staged-datasets-store.js +16 -5
  155. package/dist/provider-api/staged-datasets-store.js.map +1 -1
  156. package/dist/resources/handlers.d.ts +2 -2
  157. package/dist/resources/store.d.ts.map +1 -1
  158. package/dist/resources/store.js +56 -25
  159. package/dist/resources/store.js.map +1 -1
  160. package/dist/secrets/storage.d.ts.map +1 -1
  161. package/dist/secrets/storage.js +22 -2
  162. package/dist/secrets/storage.js.map +1 -1
  163. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  164. package/dist/server/agent-teams-run-queue.d.ts.map +1 -1
  165. package/dist/server/agent-teams-run-queue.js +15 -4
  166. package/dist/server/agent-teams-run-queue.js.map +1 -1
  167. package/dist/server/auth.d.ts.map +1 -1
  168. package/dist/server/auth.js +16 -2
  169. package/dist/server/auth.js.map +1 -1
  170. package/dist/server/better-auth-instance.d.ts.map +1 -1
  171. package/dist/server/better-auth-instance.js +55 -21
  172. package/dist/server/better-auth-instance.js.map +1 -1
  173. package/dist/server/embed-session.d.ts.map +1 -1
  174. package/dist/server/embed-session.js +15 -4
  175. package/dist/server/embed-session.js.map +1 -1
  176. package/dist/server/identity-sso-store.d.ts.map +1 -1
  177. package/dist/server/identity-sso-store.js +30 -11
  178. package/dist/server/identity-sso-store.js.map +1 -1
  179. package/dist/server/recap-image-store.d.ts.map +1 -1
  180. package/dist/server/recap-image-store.js +21 -7
  181. package/dist/server/recap-image-store.js.map +1 -1
  182. package/dist/server/transcribe-voice.d.ts +1 -1
  183. package/dist/settings/store.d.ts.map +1 -1
  184. package/dist/settings/store.js +30 -6
  185. package/dist/settings/store.js.map +1 -1
  186. package/dist/usage/store.d.ts.map +1 -1
  187. package/dist/usage/store.js +36 -11
  188. package/dist/usage/store.js.map +1 -1
  189. package/dist/workspace-connections/store.d.ts.map +1 -1
  190. package/dist/workspace-connections/store.js +57 -7
  191. package/dist/workspace-connections/store.js.map +1 -1
  192. package/package.json +1 -1
@@ -3,10 +3,11 @@ import { randomUUID } from "node:crypto";
3
3
  import {
4
4
  getDbExec,
5
5
  intType,
6
+ isPostgres,
6
7
  isUniqueViolation,
7
- retryOnDdlRace,
8
8
  safeJsonParse,
9
9
  } from "../db/client.js";
10
+ import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
10
11
  import { recordChange } from "../server/poll.js";
11
12
  import type {
12
13
  AgentRun,
@@ -42,32 +43,46 @@ async function ensureTable(): Promise<void> {
42
43
  if (!_initPromise) {
43
44
  _initPromise = (async () => {
44
45
  const client = getDbExec();
45
- await retryOnDdlRace(() =>
46
- client.execute(`
47
- CREATE TABLE IF NOT EXISTS progress_runs (
48
- id TEXT PRIMARY KEY,
49
- owner TEXT NOT NULL,
50
- title TEXT NOT NULL,
51
- step TEXT,
52
- percent ${intType()},
53
- status TEXT NOT NULL DEFAULT 'running',
54
- metadata TEXT,
55
- started_at ${intType()} NOT NULL,
56
- updated_at ${intType()} NOT NULL,
57
- completed_at ${intType()}
58
- )
59
- `),
60
- );
61
- await retryOnDdlRace(() =>
62
- client.execute(
46
+ const createSql = `
47
+ CREATE TABLE IF NOT EXISTS progress_runs (
48
+ id TEXT PRIMARY KEY,
49
+ owner TEXT NOT NULL,
50
+ title TEXT NOT NULL,
51
+ step TEXT,
52
+ percent ${intType()},
53
+ status TEXT NOT NULL DEFAULT 'running',
54
+ metadata TEXT,
55
+ started_at ${intType()} NOT NULL,
56
+ updated_at ${intType()} NOT NULL,
57
+ completed_at ${intType()}
58
+ )
59
+ `;
60
+
61
+ if (isPostgres()) {
62
+ // PG-guard: probe information_schema / pg_indexes before issuing DDL to
63
+ // avoid ACCESS EXCLUSIVE lock contention in fresh background-worker processes.
64
+ await ensureTableExists("progress_runs", createSql);
65
+ await ensureIndexExists(
66
+ "idx_progress_runs_owner_status",
63
67
  `CREATE INDEX IF NOT EXISTS idx_progress_runs_owner_status ON progress_runs (owner, status, started_at)`,
64
- ),
65
- );
68
+ );
69
+ return;
70
+ }
71
+
72
+ // SQLite (local dev): no lock problem — keep the original behaviour.
66
73
  // NOTE: table name is `progress_runs` (not `agent_runs`) to avoid
67
74
  // colliding with core's existing agent/run-store.ts which uses
68
75
  // `agent_runs` for agent-chat turn lifecycle tracking. These are
69
76
  // separate concerns — progress = user-facing task status, agent_runs =
70
77
  // internal chat turn bookkeeping.
78
+ await client.execute(createSql);
79
+ try {
80
+ await client.execute(
81
+ `CREATE INDEX IF NOT EXISTS idx_progress_runs_owner_status ON progress_runs (owner, status, started_at)`,
82
+ );
83
+ } catch {
84
+ // Index already exists or the dialect rejected a duplicate.
85
+ }
71
86
  })().catch((err) => {
72
87
  // Reset on failure so a transient DB outage doesn't poison the cached
73
88
  // promise and reject every future insert/update call for the lifetime
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { getDbExec, intType, isPostgres, type DbExec } from "../db/client.js";
11
+ import { ensureTableExists, ensureIndexExists } from "../db/ddl-guard.js";
11
12
 
12
13
  export type ProviderCorpusJobStatus =
13
14
  | "running"
@@ -81,7 +82,7 @@ async function ensureTables(): Promise<void> {
81
82
  initPromise = (async () => {
82
83
  const db = getDbExec();
83
84
  const integerType = intType();
84
- await db.execute(`
85
+ const createJobsSql = `
85
86
  CREATE TABLE IF NOT EXISTS provider_corpus_jobs (
86
87
  id TEXT NOT NULL,
87
88
  app_id TEXT NOT NULL,
@@ -108,18 +109,37 @@ async function ensureTables(): Promise<void> {
108
109
  updated_at ${integerType} NOT NULL,
109
110
  PRIMARY KEY (id)
110
111
  )
111
- `);
112
- await db.execute(`
112
+ `;
113
+ const createHitsSql = `
113
114
  CREATE TABLE IF NOT EXISTS provider_corpus_job_hits (
114
115
  job_id TEXT NOT NULL,
115
116
  hit_index ${integerType} NOT NULL,
116
117
  hit_data TEXT NOT NULL,
117
118
  PRIMARY KEY (job_id, hit_index)
118
119
  )
119
- `);
120
+ `;
120
121
  if (isPostgres()) {
121
- await widenPostgresIntegerColumns(db);
122
+ // PG guard: probe via information_schema, only issue DDL if missing, bounded lock_timeout
123
+ await ensureTableExists("provider_corpus_jobs", createJobsSql);
124
+ await ensureTableExists("provider_corpus_job_hits", createHitsSql);
125
+ await widenPostgresIntegerColumns(db); // best-effort type widening — unchanged
126
+ await ensureIndexExists(
127
+ "provider_corpus_jobs_scope_idx",
128
+ `CREATE INDEX IF NOT EXISTS provider_corpus_jobs_scope_idx ON provider_corpus_jobs (app_id, owner_email, updated_at)`,
129
+ );
130
+ await ensureIndexExists(
131
+ "provider_corpus_jobs_status_idx",
132
+ `CREATE INDEX IF NOT EXISTS provider_corpus_jobs_status_idx ON provider_corpus_jobs (app_id, owner_email, status)`,
133
+ );
134
+ await ensureIndexExists(
135
+ "provider_corpus_job_hits_job_idx",
136
+ `CREATE INDEX IF NOT EXISTS provider_corpus_job_hits_job_idx ON provider_corpus_job_hits (job_id)`,
137
+ );
138
+ return;
122
139
  }
140
+ // SQLite (local dev): keep existing behavior
141
+ await db.execute(createJobsSql);
142
+ await db.execute(createHitsSql);
123
143
  for (const ddl of [
124
144
  `CREATE INDEX IF NOT EXISTS provider_corpus_jobs_scope_idx ON provider_corpus_jobs (app_id, owner_email, updated_at)`,
125
145
  `CREATE INDEX IF NOT EXISTS provider_corpus_jobs_status_idx ON provider_corpus_jobs (app_id, owner_email, status)`,
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import { getDbExec, isPostgres } from "../db/client.js";
24
+ import { ensureTableExists } from "../db/ddl-guard.js";
24
25
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
25
26
  import { isBlockedExtensionUrlWithDns } from "../extensions/url-safety.js";
26
27
 
@@ -91,10 +92,20 @@ async function ensureTable(): Promise<void> {
91
92
  if (!_initPromise) {
92
93
  _initPromise = (async () => {
93
94
  const client = getDbExec();
94
- const sql = isPostgres()
95
- ? CREATE_SQL.replace(/\bINTEGER\b/g, "BIGINT")
96
- : CREATE_SQL;
97
- await client.execute(sql);
95
+
96
+ if (isPostgres()) {
97
+ const pgSql = CREATE_SQL.replace(/\bINTEGER\b/g, "BIGINT");
98
+ // PG guard: probe via information_schema, only issue DDL if missing, bounded lock_timeout
99
+ await ensureTableExists("custom_api_providers", pgSql);
100
+ // Widen any pre-existing int4 timestamp columns to BIGINT (no-op once done)
101
+ await widenIntColumnsToBigInt("custom_api_providers", [
102
+ "created_at",
103
+ "updated_at",
104
+ ]);
105
+ return;
106
+ }
107
+ // SQLite (local dev): keep existing behavior
108
+ await client.execute(CREATE_SQL);
98
109
  // Fresh Postgres tables get BIGINT via the replace above, but tables
99
110
  // created before that compat existed kept int4 timestamp columns; widen
100
111
  // them so `Date.now()` writes don't overflow. No-op once done.
@@ -1,7 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
3
  import type { CredentialContext } from "../credentials/index.js";
4
- import { getDbExec, intType } from "../db/client.js";
4
+ import { getDbExec, intType, isPostgres } from "../db/client.js";
5
+ import { ensureTableExists, ensureIndexExists } from "../db/ddl-guard.js";
5
6
 
6
7
  export interface ProviderQuotaIdentityInput {
7
8
  appId: string;
@@ -398,7 +399,7 @@ async function ensureCooldownTable(): Promise<void> {
398
399
  state.initPromise = (async () => {
399
400
  const db = getDbExec();
400
401
  const integerType = intType();
401
- await db.execute(`
402
+ const createSql = `
402
403
  CREATE TABLE IF NOT EXISTS provider_api_cooldowns (
403
404
  quota_key TEXT NOT NULL,
404
405
  provider_id TEXT NOT NULL,
@@ -409,7 +410,18 @@ async function ensureCooldownTable(): Promise<void> {
409
410
  updated_at ${integerType} NOT NULL,
410
411
  PRIMARY KEY (quota_key)
411
412
  )
412
- `);
413
+ `;
414
+ if (isPostgres()) {
415
+ // PG guard: probe via information_schema, only issue DDL if missing, bounded lock_timeout
416
+ await ensureTableExists("provider_api_cooldowns", createSql);
417
+ await ensureIndexExists(
418
+ "provider_api_cooldowns_provider_idx",
419
+ `CREATE INDEX IF NOT EXISTS provider_api_cooldowns_provider_idx ON provider_api_cooldowns (provider_id, cooldown_until)`,
420
+ );
421
+ return;
422
+ }
423
+ // SQLite (local dev): keep existing behavior
424
+ await db.execute(createSql);
413
425
  try {
414
426
  await db.execute(
415
427
  `CREATE INDEX IF NOT EXISTS provider_api_cooldowns_provider_idx ON provider_api_cooldowns (provider_id, cooldown_until)`,
@@ -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,6 +15,7 @@
15
15
  import { randomUUID } from "node:crypto";
16
16
 
17
17
  import { getDbExec, isPostgres } from "../db/client.js";
18
+ import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
18
19
  import {
19
20
  encryptSecretValue as encryptValue,
20
21
  decryptSecretValue as decryptValue,
@@ -34,10 +35,39 @@ async function ensureTable(): Promise<void> {
34
35
  const client = getDbExec();
35
36
  // Postgres version of the CREATE TABLE — the generic `INTEGER` maps to
36
37
  // BIGINT on Postgres, which we need for millisecond timestamps.
37
- const sql = isPostgres()
38
+ const createSql = isPostgres()
38
39
  ? APP_SECRETS_CREATE_SQL.replace(/\bINTEGER\b/g, "BIGINT")
39
40
  : APP_SECRETS_CREATE_SQL;
40
- await client.execute(sql);
41
+
42
+ if (isPostgres()) {
43
+ // Hot path: in production the table and both additive columns are
44
+ // virtually always already present. Issuing `CREATE`/`ALTER` would
45
+ // still take an ACCESS EXCLUSIVE lock — which, in a fresh background
46
+ // worker process behind a concurrent connection on the shared Neon DB,
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
+ );
64
+ return;
65
+ }
66
+
67
+ // SQLite (local dev): no ACCESS EXCLUSIVE lock problem, keep the original
68
+ // create-then-additive-alter behaviour. SQLite has no
69
+ // `ADD COLUMN IF NOT EXISTS`, so the ALTERs stay wrapped in try/catch.
70
+ await client.execute(createSql);
41
71
 
42
72
  // Additive migration: description column (for ad-hoc keys)
43
73
  try {
@@ -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
  /**