@agent-native/core 0.77.8 → 0.77.11

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 (216) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +35 -0
  3. package/corpus/core/package.json +3 -3
  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/awareness.ts +12 -9
  15. package/corpus/core/src/collab/client.ts +4 -2
  16. package/corpus/core/src/collab/storage.ts +20 -2
  17. package/corpus/core/src/db/ddl-guard.ts +112 -0
  18. package/corpus/core/src/extensions/store.ts +76 -3
  19. package/corpus/core/src/integrations/a2a-continuations-store.ts +69 -26
  20. package/corpus/core/src/integrations/config-store.ts +19 -18
  21. package/corpus/core/src/integrations/pending-tasks-store.ts +40 -16
  22. package/corpus/core/src/integrations/remote-commands-store.ts +37 -23
  23. package/corpus/core/src/integrations/remote-devices-store.ts +64 -20
  24. package/corpus/core/src/integrations/remote-push-store.ts +75 -35
  25. package/corpus/core/src/integrations/remote-run-events-store.ts +38 -12
  26. package/corpus/core/src/integrations/thread-mapping-store.ts +11 -2
  27. package/corpus/core/src/mcp/connect-store.ts +53 -16
  28. package/corpus/core/src/mcp/oauth-store.ts +32 -7
  29. package/corpus/core/src/notifications/store.ts +20 -4
  30. package/corpus/core/src/oauth-tokens/store.ts +45 -2
  31. package/corpus/core/src/observability/store.ts +154 -47
  32. package/corpus/core/src/progress/store.ts +36 -21
  33. package/corpus/core/src/provider-api/corpus-jobs-store.ts +25 -5
  34. package/corpus/core/src/provider-api/custom-registry.ts +15 -4
  35. package/corpus/core/src/provider-api/quota-governor.ts +15 -3
  36. package/corpus/core/src/provider-api/staged-datasets-store.ts +22 -5
  37. package/corpus/core/src/resources/store.ts +66 -31
  38. package/corpus/core/src/secrets/storage.ts +18 -22
  39. package/corpus/core/src/server/agent-teams-run-queue.ts +22 -10
  40. package/corpus/core/src/server/auth.ts +22 -4
  41. package/corpus/core/src/server/better-auth-instance.ts +49 -15
  42. package/corpus/core/src/server/embed-session.ts +19 -4
  43. package/corpus/core/src/server/identity-sso-store.ts +40 -11
  44. package/corpus/core/src/server/recap-image-store.ts +28 -9
  45. package/corpus/core/src/settings/store.ts +14 -19
  46. package/corpus/core/src/usage/store.ts +51 -16
  47. package/corpus/core/src/workspace-connections/store.ts +200 -22
  48. package/corpus/templates/calendar/changelog/2026-06-25-booking-link-previews-now-show-the-content-directly-on-the-g.md +6 -0
  49. package/corpus/templates/calendar/server/lib/booking-og-image.ts +0 -2
  50. package/corpus/templates/clips/chrome-extension/package.json +1 -1
  51. package/corpus/templates/content/actions/list-trashed-content-databases.ts +36 -2
  52. package/corpus/templates/content/actions/move-document.ts +79 -33
  53. package/corpus/templates/content/actions/restore-content-database.ts +40 -3
  54. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +84 -2
  55. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +177 -49
  56. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +393 -219
  57. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +632 -467
  58. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +114 -14
  59. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +56 -15
  60. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +6 -5
  61. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +12 -4
  62. package/corpus/templates/content/app/global.css +9 -0
  63. package/corpus/templates/content/app/hooks/use-create-page.ts +36 -7
  64. package/corpus/templates/content/app/i18n-data.ts +140 -10
  65. package/corpus/templates/content/changelog/2026-06-25-comment-cards-now-track-their-highlighted-text-while-scrolli.md +6 -0
  66. package/corpus/templates/content/changelog/2026-06-25-comments-now-scroll-with-the-document-and-stay-below-the-pag.md +6 -0
  67. package/corpus/templates/content/changelog/2026-06-25-creating-a-database-from-the-slash-menu-no-longer-leaves-sta.md +6 -0
  68. package/corpus/templates/content/changelog/2026-06-25-sidebar-hover-controls-no-longer-leave-a-lingering-fade-when.md +6 -0
  69. package/corpus/templates/content/changelog/2026-06-25-sidebar-page-actions-are-now-easier-to-see-when-hovering-ina.md +6 -0
  70. package/corpus/templates/content/changelog/2026-06-25-text-selections-in-the-editor-no-longer-show-white-gaps-when.md +6 -0
  71. package/corpus/templates/content/changelog/2026-06-25-the-editor-toolbar-now-has-a-one-click-page-link-copy-button.md +6 -0
  72. package/corpus/templates/content/changelog/2026-06-25-the-editor-toolbar-now-shows-page-breadcrumbs-and-the-latest.md +6 -0
  73. package/corpus/templates/content/changelog/2026-06-25-the-page-command-now-leaves-a-notion-style-page-reference-an.md +6 -0
  74. package/corpus/templates/content/shared/nfm.ts +16 -0
  75. package/corpus/templates/forms/changelog/2026-06-25-form-previews-now-show-the-content-directly-on-the-grid-back.md +6 -0
  76. package/corpus/templates/forms/server/lib/form-og-image.ts +0 -2
  77. package/corpus/templates/plan/app/global.css +6 -11
  78. package/corpus/templates/plan/app/pages/PlansPage.tsx +12 -10
  79. package/corpus/templates/plan/changelog/2026-06-25-plan-access-request-pages-stay-visible-during-background-ref.md +6 -0
  80. package/corpus/templates/plan/changelog/2026-06-25-plan-documents-keep-their-full-reading-width-below-1200px-an.md +6 -0
  81. package/dist/a2a/task-store.d.ts.map +1 -1
  82. package/dist/a2a/task-store.js +19 -3
  83. package/dist/a2a/task-store.js.map +1 -1
  84. package/dist/agent/harness/store.d.ts.map +1 -1
  85. package/dist/agent/harness/store.js +63 -34
  86. package/dist/agent/harness/store.js.map +1 -1
  87. package/dist/agent/observational-memory/store.d.ts.map +1 -1
  88. package/dist/agent/observational-memory/store.js +30 -8
  89. package/dist/agent/observational-memory/store.js.map +1 -1
  90. package/dist/agent/run-store.d.ts.map +1 -1
  91. package/dist/agent/run-store.js +103 -46
  92. package/dist/agent/run-store.js.map +1 -1
  93. package/dist/application-state/store.d.ts.map +1 -1
  94. package/dist/application-state/store.js +28 -2
  95. package/dist/application-state/store.js.map +1 -1
  96. package/dist/audit/store.d.ts.map +1 -1
  97. package/dist/audit/store.js +18 -3
  98. package/dist/audit/store.js.map +1 -1
  99. package/dist/browser-sessions/store.d.ts.map +1 -1
  100. package/dist/browser-sessions/store.js +22 -7
  101. package/dist/browser-sessions/store.js.map +1 -1
  102. package/dist/chat-threads/store.d.ts.map +1 -1
  103. package/dist/chat-threads/store.js +51 -3
  104. package/dist/chat-threads/store.js.map +1 -1
  105. package/dist/checkpoints/store.d.ts.map +1 -1
  106. package/dist/checkpoints/store.js +12 -3
  107. package/dist/checkpoints/store.js.map +1 -1
  108. package/dist/cli/create.d.ts.map +1 -1
  109. package/dist/cli/create.js +9 -0
  110. package/dist/cli/create.js.map +1 -1
  111. package/dist/collab/awareness.d.ts +1 -1
  112. package/dist/collab/awareness.d.ts.map +1 -1
  113. package/dist/collab/awareness.js +12 -8
  114. package/dist/collab/awareness.js.map +1 -1
  115. package/dist/collab/client.d.ts.map +1 -1
  116. package/dist/collab/client.js +4 -3
  117. package/dist/collab/client.js.map +1 -1
  118. package/dist/collab/routes.d.ts +1 -1
  119. package/dist/collab/storage.d.ts.map +1 -1
  120. package/dist/collab/storage.js +14 -2
  121. package/dist/collab/storage.js.map +1 -1
  122. package/dist/db/ddl-guard.d.ts +63 -0
  123. package/dist/db/ddl-guard.d.ts.map +1 -1
  124. package/dist/db/ddl-guard.js +85 -0
  125. package/dist/db/ddl-guard.js.map +1 -1
  126. package/dist/extensions/store.d.ts.map +1 -1
  127. package/dist/extensions/store.js +32 -3
  128. package/dist/extensions/store.js.map +1 -1
  129. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  130. package/dist/integrations/a2a-continuations-store.js +44 -24
  131. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  132. package/dist/integrations/config-store.d.ts.map +1 -1
  133. package/dist/integrations/config-store.js +17 -11
  134. package/dist/integrations/config-store.js.map +1 -1
  135. package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
  136. package/dist/integrations/pending-tasks-store.js +24 -16
  137. package/dist/integrations/pending-tasks-store.js.map +1 -1
  138. package/dist/integrations/remote-commands-store.d.ts.map +1 -1
  139. package/dist/integrations/remote-commands-store.js +29 -21
  140. package/dist/integrations/remote-commands-store.js.map +1 -1
  141. package/dist/integrations/remote-devices-store.d.ts.map +1 -1
  142. package/dist/integrations/remote-devices-store.js +33 -18
  143. package/dist/integrations/remote-devices-store.js.map +1 -1
  144. package/dist/integrations/remote-push-store.d.ts.map +1 -1
  145. package/dist/integrations/remote-push-store.js +53 -31
  146. package/dist/integrations/remote-push-store.js.map +1 -1
  147. package/dist/integrations/remote-run-events-store.d.ts.map +1 -1
  148. package/dist/integrations/remote-run-events-store.js +26 -10
  149. package/dist/integrations/remote-run-events-store.js.map +1 -1
  150. package/dist/integrations/thread-mapping-store.d.ts.map +1 -1
  151. package/dist/integrations/thread-mapping-store.js +10 -2
  152. package/dist/integrations/thread-mapping-store.js.map +1 -1
  153. package/dist/mcp/connect-store.d.ts.map +1 -1
  154. package/dist/mcp/connect-store.js +34 -16
  155. package/dist/mcp/connect-store.js.map +1 -1
  156. package/dist/mcp/oauth-store.d.ts.map +1 -1
  157. package/dist/mcp/oauth-store.js +22 -7
  158. package/dist/mcp/oauth-store.js.map +1 -1
  159. package/dist/notifications/store.d.ts.map +1 -1
  160. package/dist/notifications/store.js +15 -3
  161. package/dist/notifications/store.js.map +1 -1
  162. package/dist/oauth-tokens/store.d.ts.map +1 -1
  163. package/dist/oauth-tokens/store.js +33 -2
  164. package/dist/oauth-tokens/store.js.map +1 -1
  165. package/dist/observability/store.d.ts.map +1 -1
  166. package/dist/observability/store.js +72 -29
  167. package/dist/observability/store.js.map +1 -1
  168. package/dist/progress/store.d.ts.map +1 -1
  169. package/dist/progress/store.js +31 -16
  170. package/dist/progress/store.js.map +1 -1
  171. package/dist/provider-api/corpus-jobs-store.d.ts.map +1 -1
  172. package/dist/provider-api/corpus-jobs-store.js +16 -5
  173. package/dist/provider-api/corpus-jobs-store.js.map +1 -1
  174. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  175. package/dist/provider-api/custom-registry.js +14 -4
  176. package/dist/provider-api/custom-registry.js.map +1 -1
  177. package/dist/provider-api/quota-governor.d.ts.map +1 -1
  178. package/dist/provider-api/quota-governor.js +12 -3
  179. package/dist/provider-api/quota-governor.js.map +1 -1
  180. package/dist/provider-api/staged-datasets-store.d.ts.map +1 -1
  181. package/dist/provider-api/staged-datasets-store.js +16 -5
  182. package/dist/provider-api/staged-datasets-store.js.map +1 -1
  183. package/dist/resources/store.d.ts.map +1 -1
  184. package/dist/resources/store.js +56 -25
  185. package/dist/resources/store.js.map +1 -1
  186. package/dist/secrets/storage.d.ts.map +1 -1
  187. package/dist/secrets/storage.js +10 -14
  188. package/dist/secrets/storage.js.map +1 -1
  189. package/dist/server/agent-teams-run-queue.d.ts.map +1 -1
  190. package/dist/server/agent-teams-run-queue.js +15 -4
  191. package/dist/server/agent-teams-run-queue.js.map +1 -1
  192. package/dist/server/auth.d.ts.map +1 -1
  193. package/dist/server/auth.js +16 -2
  194. package/dist/server/auth.js.map +1 -1
  195. package/dist/server/better-auth-instance.d.ts.map +1 -1
  196. package/dist/server/better-auth-instance.js +55 -21
  197. package/dist/server/better-auth-instance.js.map +1 -1
  198. package/dist/server/embed-session.d.ts.map +1 -1
  199. package/dist/server/embed-session.js +15 -4
  200. package/dist/server/embed-session.js.map +1 -1
  201. package/dist/server/identity-sso-store.d.ts.map +1 -1
  202. package/dist/server/identity-sso-store.js +30 -11
  203. package/dist/server/identity-sso-store.js.map +1 -1
  204. package/dist/server/recap-image-store.d.ts.map +1 -1
  205. package/dist/server/recap-image-store.js +21 -7
  206. package/dist/server/recap-image-store.js.map +1 -1
  207. package/dist/settings/store.d.ts.map +1 -1
  208. package/dist/settings/store.js +11 -13
  209. package/dist/settings/store.js.map +1 -1
  210. package/dist/usage/store.d.ts.map +1 -1
  211. package/dist/usage/store.js +36 -11
  212. package/dist/usage/store.js.map +1 -1
  213. package/dist/workspace-connections/store.d.ts.map +1 -1
  214. package/dist/workspace-connections/store.js +57 -7
  215. package/dist/workspace-connections/store.js.map +1 -1
  216. package/package.json +3 -3
@@ -19,7 +19,13 @@
19
19
 
20
20
  import { randomBytes, randomUUID } from "node:crypto";
21
21
 
22
- import { getDbExec, isConnectionError, intType } from "../db/client.js";
22
+ import {
23
+ getDbExec,
24
+ isConnectionError,
25
+ intType,
26
+ isPostgres,
27
+ } from "../db/client.js";
28
+ import { ensureTableExists, ensureColumnExists } from "../db/ddl-guard.js";
23
29
 
24
30
  let _initPromise: Promise<void> | undefined;
25
31
 
@@ -59,7 +65,7 @@ async function ensureTable(): Promise<void> {
59
65
  const client = getDbExec();
60
66
  // Additive only. Never DROP / ALTER — this DB is shared across every
61
67
  // deploy context (preview/branch/prod) for hosted templates.
62
- await client.execute(`
68
+ const createTokensSql = `
63
69
  CREATE TABLE IF NOT EXISTS mcp_connect_tokens (
64
70
  id TEXT PRIMARY KEY,
65
71
  jti TEXT UNIQUE NOT NULL,
@@ -73,7 +79,50 @@ async function ensureTable(): Promise<void> {
73
79
  last_used_at ${intType()},
74
80
  revoked_at ${intType()}
75
81
  )
76
- `);
82
+ `;
83
+ const createDeviceCodesSql = `
84
+ CREATE TABLE IF NOT EXISTS mcp_device_codes (
85
+ device_code TEXT PRIMARY KEY,
86
+ user_code TEXT NOT NULL,
87
+ owner_email TEXT,
88
+ org_id TEXT,
89
+ status TEXT NOT NULL DEFAULT 'pending',
90
+ token_jti TEXT,
91
+ created_at ${intType()},
92
+ expires_at ${intType()},
93
+ consumed_at ${intType()}
94
+ )
95
+ `;
96
+
97
+ if (isPostgres()) {
98
+ // PG-guard: probe information_schema first (no lock) and only issue
99
+ // DDL when the table/column is actually missing, wrapped in a
100
+ // transaction-scoped lock_timeout so a contended lock fails fast.
101
+ await ensureTableExists("mcp_connect_tokens", createTokensSql);
102
+ // Additive columns for org service tokens — added after initial
103
+ // deployment; ensureColumnExists probes before ALTERing.
104
+ await ensureColumnExists(
105
+ "mcp_connect_tokens",
106
+ "kind",
107
+ `ALTER TABLE mcp_connect_tokens ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'personal'`,
108
+ );
109
+ await ensureColumnExists(
110
+ "mcp_connect_tokens",
111
+ "service_name",
112
+ `ALTER TABLE mcp_connect_tokens ADD COLUMN IF NOT EXISTS service_name TEXT`,
113
+ );
114
+ await ensureColumnExists(
115
+ "mcp_connect_tokens",
116
+ "created_by",
117
+ `ALTER TABLE mcp_connect_tokens ADD COLUMN IF NOT EXISTS created_by TEXT`,
118
+ );
119
+ await ensureTableExists("mcp_device_codes", createDeviceCodesSql);
120
+ return;
121
+ }
122
+
123
+ // SQLite (local dev): no ACCESS EXCLUSIVE lock problem — keep existing
124
+ // create-then-additive-alter behaviour.
125
+ await client.execute(createTokensSql);
77
126
  // Additive columns for org service tokens (deployments that created the
78
127
  // table before these columns existed; fresh DBs get them via the CREATE
79
128
  // TABLE above). kind='personal' (default) preserves the original
@@ -107,19 +156,7 @@ async function ensureTable(): Promise<void> {
107
156
  }
108
157
  }
109
158
  }
110
- await client.execute(`
111
- CREATE TABLE IF NOT EXISTS mcp_device_codes (
112
- device_code TEXT PRIMARY KEY,
113
- user_code TEXT NOT NULL,
114
- owner_email TEXT,
115
- org_id TEXT,
116
- status TEXT NOT NULL DEFAULT 'pending',
117
- token_jti TEXT,
118
- created_at ${intType()},
119
- expires_at ${intType()},
120
- consumed_at ${intType()}
121
- )
122
- `);
159
+ await client.execute(createDeviceCodesSql);
123
160
  })().catch((err) => {
124
161
  // Don't cache a rejected init. A transient DB blip should let the next
125
162
  // connect/mint/revoke call retry rather than wedging the process.
@@ -8,7 +8,13 @@
8
8
 
9
9
  import { randomBytes, randomUUID, createHash } from "node:crypto";
10
10
 
11
- import { getDbExec, isConnectionError, intType } from "../db/client.js";
11
+ import {
12
+ getDbExec,
13
+ isConnectionError,
14
+ intType,
15
+ isPostgres,
16
+ } from "../db/client.js";
17
+ import { ensureTableExists } from "../db/ddl-guard.js";
12
18
 
13
19
  let _initPromise: Promise<void> | undefined;
14
20
 
@@ -79,7 +85,7 @@ async function ensureTable(): Promise<void> {
79
85
  if (!_initPromise) {
80
86
  _initPromise = (async () => {
81
87
  const client = getDbExec();
82
- await client.execute(`
88
+ const createClientsSql = `
83
89
  CREATE TABLE IF NOT EXISTS mcp_oauth_clients (
84
90
  client_id TEXT PRIMARY KEY,
85
91
  client_name TEXT,
@@ -89,8 +95,8 @@ async function ensureTable(): Promise<void> {
89
95
  token_endpoint_auth_method TEXT,
90
96
  created_at ${intType()}
91
97
  )
92
- `);
93
- await client.execute(`
98
+ `;
99
+ const createCodesSql = `
94
100
  CREATE TABLE IF NOT EXISTS mcp_oauth_codes (
95
101
  code TEXT PRIMARY KEY,
96
102
  client_id TEXT NOT NULL,
@@ -106,8 +112,8 @@ async function ensureTable(): Promise<void> {
106
112
  expires_at ${intType()},
107
113
  consumed_at ${intType()}
108
114
  )
109
- `);
110
- await client.execute(`
115
+ `;
116
+ const createRefreshTokensSql = `
111
117
  CREATE TABLE IF NOT EXISTS mcp_oauth_refresh_tokens (
112
118
  id TEXT PRIMARY KEY,
113
119
  token_hash TEXT UNIQUE NOT NULL,
@@ -123,7 +129,26 @@ async function ensureTable(): Promise<void> {
123
129
  revoked_at ${intType()},
124
130
  replaced_by_hash TEXT
125
131
  )
126
- `);
132
+ `;
133
+
134
+ if (isPostgres()) {
135
+ // PG-guard: probe information_schema first (no lock) and only issue
136
+ // DDL when the table is actually missing, wrapped in a
137
+ // transaction-scoped lock_timeout so a contended lock fails fast.
138
+ await ensureTableExists("mcp_oauth_clients", createClientsSql);
139
+ await ensureTableExists("mcp_oauth_codes", createCodesSql);
140
+ await ensureTableExists(
141
+ "mcp_oauth_refresh_tokens",
142
+ createRefreshTokensSql,
143
+ );
144
+ return;
145
+ }
146
+
147
+ // SQLite (local dev): no ACCESS EXCLUSIVE lock problem — keep existing
148
+ // create-then-execute behaviour.
149
+ await client.execute(createClientsSql);
150
+ await client.execute(createCodesSql);
151
+ await client.execute(createRefreshTokensSql);
127
152
  })().catch((err) => {
128
153
  _initPromise = undefined;
129
154
  throw err;
@@ -3,9 +3,11 @@ import { randomUUID } from "node:crypto";
3
3
  import {
4
4
  getDbExec,
5
5
  intType,
6
+ isPostgres,
6
7
  retryOnDdlRace,
7
8
  safeJsonParse,
8
9
  } from "../db/client.js";
10
+ import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
9
11
  import { recordChange } from "../server/poll.js";
10
12
  import type { Notification, NotificationSeverity } from "./types.js";
11
13
 
@@ -24,8 +26,7 @@ async function ensureTable(): Promise<void> {
24
26
  if (!_initPromise) {
25
27
  _initPromise = (async () => {
26
28
  const client = getDbExec();
27
- await retryOnDdlRace(() =>
28
- client.execute(`
29
+ const createSql = `
29
30
  CREATE TABLE IF NOT EXISTS notifications (
30
31
  id TEXT PRIMARY KEY,
31
32
  owner TEXT NOT NULL,
@@ -37,8 +38,23 @@ async function ensureTable(): Promise<void> {
37
38
  created_at ${intType()} NOT NULL,
38
39
  read_at ${intType()}
39
40
  )
40
- `),
41
- );
41
+ `;
42
+
43
+ if (isPostgres()) {
44
+ // PG-guard: probe information_schema / pg_indexes first (no lock) and
45
+ // only issue DDL when the table/index is actually missing, wrapped in
46
+ // a transaction-scoped lock_timeout so a contended lock fails fast.
47
+ await ensureTableExists("notifications", createSql);
48
+ await ensureIndexExists(
49
+ "idx_notifications_owner_unread",
50
+ `CREATE INDEX IF NOT EXISTS idx_notifications_owner_unread ON notifications (owner, read_at)`,
51
+ );
52
+ return;
53
+ }
54
+
55
+ // SQLite (local dev): no ACCESS EXCLUSIVE lock problem — keep existing
56
+ // retryOnDdlRace behaviour.
57
+ await retryOnDdlRace(() => client.execute(createSql));
42
58
  await retryOnDdlRace(() =>
43
59
  client.execute(
44
60
  `CREATE INDEX IF NOT EXISTS idx_notifications_owner_unread ON notifications (owner, read_at)`,
@@ -1,4 +1,5 @@
1
1
  import { getDbExec, isPostgres, intType } from "../db/client.js";
2
+ import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
2
3
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
3
4
  import {
4
5
  encryptSecretValue,
@@ -54,7 +55,7 @@ async function ensureTable(): Promise<void> {
54
55
  _initPromise = (async () => {
55
56
  const client = getDbExec();
56
57
  const table = oauthTokensTable();
57
- await client.execute(`
58
+ const createSql = `
58
59
  CREATE TABLE IF NOT EXISTS ${table} (
59
60
  provider TEXT NOT NULL,
60
61
  account_id TEXT NOT NULL,
@@ -63,7 +64,49 @@ async function ensureTable(): Promise<void> {
63
64
  updated_at ${intType()} NOT NULL,
64
65
  PRIMARY KEY (provider, account_id)
65
66
  )
66
- `);
67
+ `;
68
+
69
+ if (isPostgres()) {
70
+ // Hot path: the `oauth_tokens` table and its additive columns are
71
+ // virtually always already present in production. Issuing `CREATE
72
+ // TABLE`/`ALTER TABLE` still takes an ACCESS EXCLUSIVE lock that, in a
73
+ // fresh background-worker process behind a concurrent connection on the
74
+ // shared Neon DB, can block ~indefinitely. The ensure* wrappers probe
75
+ // `information_schema` first (plain reads, no lock) and run DDL ONLY for
76
+ // what is actually missing, bounded by a transaction-scoped
77
+ // `lock_timeout`. If a swallowed lock-timeout leaves the schema still
78
+ // missing they RE-PROBE and THROW rather than letting init memoize
79
+ // success against absent schema. `oauthTokensTable()` is
80
+ // `public.oauth_tokens` on Postgres; the wrappers take the unqualified
81
+ // table name.
82
+ await ensureTableExists("oauth_tokens", createSql);
83
+ // Migration: add owner column to existing tables — guarded so the hot
84
+ // path (column already present) skips the ACCESS EXCLUSIVE ALTER.
85
+ await ensureColumnExists(
86
+ "oauth_tokens",
87
+ "owner",
88
+ `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS owner TEXT`,
89
+ );
90
+ // Migration: add display_name column
91
+ await ensureColumnExists(
92
+ "oauth_tokens",
93
+ "display_name",
94
+ `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS display_name TEXT`,
95
+ );
96
+ // Backfill: set owner = account_id for existing rows without an owner
97
+ await client.execute(
98
+ `UPDATE ${table} SET owner = account_id WHERE owner IS NULL`,
99
+ );
100
+ // Older deployments have a 32-bit `updated_at`; on Postgres the
101
+ // `Date.now()` written on every token save overflows int4. Widen in place
102
+ // (no-op once done / on fresh DBs). Unqualified name — the helper scopes
103
+ // to the `public` schema.
104
+ await widenIntColumnsToBigInt("oauth_tokens", ["updated_at"]);
105
+ return;
106
+ }
107
+
108
+ // SQLite (local dev): no lock problem — keep the original behaviour.
109
+ await client.execute(createSql);
67
110
  // Migration: add owner column to existing tables
68
111
  try {
69
112
  await client.execute(`ALTER TABLE ${table} ADD COLUMN owner TEXT`);
@@ -12,6 +12,11 @@ import {
12
12
  isPostgres,
13
13
  retryOnDdlRace,
14
14
  } from "../db/client.js";
15
+ import {
16
+ ensureTableExists,
17
+ ensureColumnExists,
18
+ ensureIndexExists,
19
+ } from "../db/ddl-guard.js";
15
20
  import { isDuplicateColumnError } from "../db/migrations.js";
16
21
  import type {
17
22
  TraceSpan,
@@ -70,8 +75,7 @@ export async function ensureObservabilityTables(): Promise<void> {
70
75
  _initPromise = (async () => {
71
76
  const client = getDbExec();
72
77
 
73
- await retryOnDdlRace(() =>
74
- client.execute(`
78
+ const traceSpansCreateSql = `
75
79
  CREATE TABLE IF NOT EXISTS agent_trace_spans (
76
80
  id TEXT PRIMARY KEY,
77
81
  run_id TEXT NOT NULL,
@@ -91,11 +95,9 @@ export async function ensureObservabilityTables(): Promise<void> {
91
95
  metadata TEXT,
92
96
  created_at ${intType()} NOT NULL
93
97
  )
94
- `),
95
- );
98
+ `;
96
99
 
97
- await retryOnDdlRace(() =>
98
- client.execute(`
100
+ const traceSummariesCreateSql = `
99
101
  CREATE TABLE IF NOT EXISTS agent_trace_summaries (
100
102
  run_id TEXT PRIMARY KEY,
101
103
  thread_id TEXT,
@@ -112,11 +114,9 @@ export async function ensureObservabilityTables(): Promise<void> {
112
114
  model TEXT NOT NULL DEFAULT '',
113
115
  created_at ${intType()} NOT NULL
114
116
  )
115
- `),
116
- );
117
+ `;
117
118
 
118
- await retryOnDdlRace(() =>
119
- client.execute(`
119
+ const feedbackCreateSql = `
120
120
  CREATE TABLE IF NOT EXISTS agent_feedback (
121
121
  id TEXT PRIMARY KEY,
122
122
  run_id TEXT,
@@ -127,11 +127,9 @@ export async function ensureObservabilityTables(): Promise<void> {
127
127
  user_id TEXT,
128
128
  created_at ${intType()} NOT NULL
129
129
  )
130
- `),
131
- );
130
+ `;
132
131
 
133
- await retryOnDdlRace(() =>
134
- client.execute(`
132
+ const satisfactionScoresCreateSql = `
135
133
  CREATE TABLE IF NOT EXISTS agent_satisfaction_scores (
136
134
  id TEXT PRIMARY KEY,
137
135
  thread_id TEXT NOT NULL,
@@ -143,11 +141,9 @@ export async function ensureObservabilityTables(): Promise<void> {
143
141
  length_trend_score REAL NOT NULL DEFAULT 0,
144
142
  computed_at ${intType()} NOT NULL
145
143
  )
146
- `),
147
- );
144
+ `;
148
145
 
149
- await retryOnDdlRace(() =>
150
- client.execute(`
146
+ const evalsCreateSql = `
151
147
  CREATE TABLE IF NOT EXISTS agent_evals (
152
148
  id TEXT PRIMARY KEY,
153
149
  run_id TEXT NOT NULL,
@@ -160,11 +156,9 @@ export async function ensureObservabilityTables(): Promise<void> {
160
156
  metadata TEXT,
161
157
  created_at ${intType()} NOT NULL
162
158
  )
163
- `),
164
- );
159
+ `;
165
160
 
166
- await retryOnDdlRace(() =>
167
- client.execute(`
161
+ const evalDatasetsCreateSql = `
168
162
  CREATE TABLE IF NOT EXISTS agent_eval_datasets (
169
163
  id TEXT PRIMARY KEY,
170
164
  name TEXT NOT NULL,
@@ -173,11 +167,9 @@ export async function ensureObservabilityTables(): Promise<void> {
173
167
  created_at ${intType()} NOT NULL,
174
168
  updated_at ${intType()} NOT NULL
175
169
  )
176
- `),
177
- );
170
+ `;
178
171
 
179
- await retryOnDdlRace(() =>
180
- client.execute(`
172
+ const experimentsCreateSql = `
181
173
  CREATE TABLE IF NOT EXISTS agent_experiments (
182
174
  id TEXT PRIMARY KEY,
183
175
  name TEXT NOT NULL,
@@ -190,23 +182,9 @@ export async function ensureObservabilityTables(): Promise<void> {
190
182
  created_at ${intType()} NOT NULL,
191
183
  owner_email TEXT
192
184
  )
193
- `),
194
- );
195
- // Additive migration for DBs created before the owner column shipped
196
- // (any pre-existing rows have NULL owner — see `updateExperiment` for
197
- // the migration semantics). Mutations on those rows fall back to the
198
- // standard authentication gate but cannot enforce per-owner scoping
199
- // until they're re-saved.
200
- try {
201
- await client.execute(
202
- `ALTER TABLE agent_experiments ADD COLUMN owner_email TEXT`,
203
- );
204
- } catch {
205
- // Column already exists — expected after first run.
206
- }
185
+ `;
207
186
 
208
- await retryOnDdlRace(() =>
209
- client.execute(`
187
+ const experimentAssignmentsCreateSql = `
210
188
  CREATE TABLE IF NOT EXISTS agent_experiment_assignments (
211
189
  experiment_id TEXT NOT NULL,
212
190
  user_id TEXT NOT NULL,
@@ -214,11 +192,9 @@ export async function ensureObservabilityTables(): Promise<void> {
214
192
  assigned_at ${intType()} NOT NULL,
215
193
  PRIMARY KEY (experiment_id, user_id)
216
194
  )
217
- `),
218
- );
195
+ `;
219
196
 
220
- await retryOnDdlRace(() =>
221
- client.execute(`
197
+ const experimentResultsCreateSql = `
222
198
  CREATE TABLE IF NOT EXISTS agent_experiment_results (
223
199
  id TEXT PRIMARY KEY,
224
200
  experiment_id TEXT NOT NULL,
@@ -230,9 +206,140 @@ export async function ensureObservabilityTables(): Promise<void> {
230
206
  confidence_high REAL NOT NULL DEFAULT 0,
231
207
  computed_at ${intType()} NOT NULL
232
208
  )
233
- `),
209
+ `;
210
+
211
+ if (isPostgres()) {
212
+ // PG guard: probe → guarded DDL → re-probe; skips lock on already-migrated path
213
+ await ensureTableExists("agent_trace_spans", traceSpansCreateSql);
214
+ await ensureTableExists(
215
+ "agent_trace_summaries",
216
+ traceSummariesCreateSql,
217
+ );
218
+ await ensureTableExists("agent_feedback", feedbackCreateSql);
219
+ await ensureTableExists(
220
+ "agent_satisfaction_scores",
221
+ satisfactionScoresCreateSql,
222
+ );
223
+ await ensureTableExists("agent_evals", evalsCreateSql);
224
+ await ensureTableExists("agent_eval_datasets", evalDatasetsCreateSql);
225
+ await ensureTableExists("agent_experiments", experimentsCreateSql);
226
+ await ensureTableExists(
227
+ "agent_experiment_assignments",
228
+ experimentAssignmentsCreateSql,
229
+ );
230
+ await ensureTableExists(
231
+ "agent_experiment_results",
232
+ experimentResultsCreateSql,
233
+ );
234
+ await ensureColumnExists(
235
+ "agent_experiments",
236
+ "owner_email",
237
+ `ALTER TABLE agent_experiments ADD COLUMN IF NOT EXISTS owner_email TEXT`,
238
+ );
239
+ for (const table of USER_SCOPED_TABLES) {
240
+ await ensureColumnExists(
241
+ table,
242
+ "user_id",
243
+ `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS user_id TEXT`,
244
+ );
245
+ }
246
+ await ensureIndexExists(
247
+ "idx_trace_spans_run",
248
+ `CREATE INDEX IF NOT EXISTS idx_trace_spans_run ON agent_trace_spans (run_id)`,
249
+ );
250
+ await ensureIndexExists(
251
+ "idx_trace_spans_thread",
252
+ `CREATE INDEX IF NOT EXISTS idx_trace_spans_thread ON agent_trace_spans (thread_id)`,
253
+ );
254
+ await ensureIndexExists(
255
+ "idx_trace_spans_created",
256
+ `CREATE INDEX IF NOT EXISTS idx_trace_spans_created ON agent_trace_spans (created_at)`,
257
+ );
258
+ await ensureIndexExists(
259
+ "idx_trace_summaries_created",
260
+ `CREATE INDEX IF NOT EXISTS idx_trace_summaries_created ON agent_trace_summaries (created_at)`,
261
+ );
262
+ await ensureIndexExists(
263
+ "idx_trace_summaries_user",
264
+ `CREATE INDEX IF NOT EXISTS idx_trace_summaries_user ON agent_trace_summaries (user_id, created_at)`,
265
+ );
266
+ await ensureIndexExists(
267
+ "idx_trace_spans_user",
268
+ `CREATE INDEX IF NOT EXISTS idx_trace_spans_user ON agent_trace_spans (user_id)`,
269
+ );
270
+ await ensureIndexExists(
271
+ "idx_feedback_thread",
272
+ `CREATE INDEX IF NOT EXISTS idx_feedback_thread ON agent_feedback (thread_id)`,
273
+ );
274
+ await ensureIndexExists(
275
+ "idx_feedback_created",
276
+ `CREATE INDEX IF NOT EXISTS idx_feedback_created ON agent_feedback (created_at)`,
277
+ );
278
+ await ensureIndexExists(
279
+ "idx_feedback_user",
280
+ `CREATE INDEX IF NOT EXISTS idx_feedback_user ON agent_feedback (user_id, created_at)`,
281
+ );
282
+ await ensureIndexExists(
283
+ "idx_satisfaction_thread",
284
+ `CREATE INDEX IF NOT EXISTS idx_satisfaction_thread ON agent_satisfaction_scores (thread_id)`,
285
+ );
286
+ await ensureIndexExists(
287
+ "idx_satisfaction_user",
288
+ `CREATE INDEX IF NOT EXISTS idx_satisfaction_user ON agent_satisfaction_scores (user_id, computed_at)`,
289
+ );
290
+ await ensureIndexExists(
291
+ "idx_evals_run",
292
+ `CREATE INDEX IF NOT EXISTS idx_evals_run ON agent_evals (run_id)`,
293
+ );
294
+ await ensureIndexExists(
295
+ "idx_evals_created",
296
+ `CREATE INDEX IF NOT EXISTS idx_evals_created ON agent_evals (created_at)`,
297
+ );
298
+ await ensureIndexExists(
299
+ "idx_evals_user",
300
+ `CREATE INDEX IF NOT EXISTS idx_evals_user ON agent_evals (user_id, created_at)`,
301
+ );
302
+ await ensureIndexExists(
303
+ "idx_experiment_results_exp",
304
+ `CREATE INDEX IF NOT EXISTS idx_experiment_results_exp ON agent_experiment_results (experiment_id)`,
305
+ );
306
+ return;
307
+ }
308
+
309
+ // SQLite (local dev): no lock problem — keep the original behaviour.
310
+ await retryOnDdlRace(() => client.execute(traceSpansCreateSql));
311
+
312
+ await retryOnDdlRace(() => client.execute(traceSummariesCreateSql));
313
+
314
+ await retryOnDdlRace(() => client.execute(feedbackCreateSql));
315
+
316
+ await retryOnDdlRace(() => client.execute(satisfactionScoresCreateSql));
317
+
318
+ await retryOnDdlRace(() => client.execute(evalsCreateSql));
319
+
320
+ await retryOnDdlRace(() => client.execute(evalDatasetsCreateSql));
321
+
322
+ await retryOnDdlRace(() => client.execute(experimentsCreateSql));
323
+
324
+ // Additive migration for DBs created before the owner column shipped
325
+ // (any pre-existing rows have NULL owner — see `updateExperiment` for
326
+ // the migration semantics). Mutations on those rows fall back to the
327
+ // standard authentication gate but cannot enforce per-owner scoping
328
+ // until they're re-saved.
329
+ try {
330
+ await client.execute(
331
+ `ALTER TABLE agent_experiments ADD COLUMN owner_email TEXT`,
332
+ );
333
+ } catch {
334
+ // Column already exists — expected after first run.
335
+ }
336
+
337
+ await retryOnDdlRace(() =>
338
+ client.execute(experimentAssignmentsCreateSql),
234
339
  );
235
340
 
341
+ await retryOnDdlRace(() => client.execute(experimentResultsCreateSql));
342
+
236
343
  // Idempotent column upgrades for DBs created before per-user
237
344
  // isolation. SQLite has no `ADD COLUMN IF NOT EXISTS`; Postgres
238
345
  // surfaces "column ... already exists". `isDuplicateColumnError`
@@ -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