@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
@@ -7,7 +7,8 @@
7
7
  * `agent_audit_log`; reads are scoped to the caller's identity in SQL (no
8
8
  * shares table — audit rows are never individually shared).
9
9
  */
10
- import { getDbExec, intType, retryOnDdlRace } from "../db/client.js";
10
+ import { getDbExec, intType, isPostgres } from "../db/client.js";
11
+ import { ensureTableExists, ensureIndexExists } from "../db/ddl-guard.js";
11
12
  import type {
12
13
  AuditEvent,
13
14
  AuditQueryFilters,
@@ -20,9 +21,7 @@ export async function ensureAuditTables(): Promise<void> {
20
21
  if (!_initPromise) {
21
22
  _initPromise = (async () => {
22
23
  const client = getDbExec();
23
-
24
- await retryOnDdlRace(() =>
25
- client.execute(`
24
+ const createSql = `
26
25
  CREATE TABLE IF NOT EXISTS agent_audit_log (
27
26
  id TEXT PRIMARY KEY,
28
27
  created_at ${intType()} NOT NULL,
@@ -42,9 +41,41 @@ export async function ensureAuditTables(): Promise<void> {
42
41
  owner_email TEXT,
43
42
  visibility TEXT NOT NULL DEFAULT 'private'
44
43
  )
45
- `),
46
- );
44
+ `;
45
+
46
+ if (isPostgres()) {
47
+ // PG-guard: probe information_schema / pg_indexes before issuing DDL to
48
+ // avoid ACCESS EXCLUSIVE lock contention in fresh background-worker processes.
49
+ await ensureTableExists("agent_audit_log", createSql);
50
+ await ensureIndexExists(
51
+ "idx_audit_owner",
52
+ `CREATE INDEX IF NOT EXISTS idx_audit_owner ON agent_audit_log (owner_email, created_at)`,
53
+ );
54
+ await ensureIndexExists(
55
+ "idx_audit_org",
56
+ `CREATE INDEX IF NOT EXISTS idx_audit_org ON agent_audit_log (org_id, created_at)`,
57
+ );
58
+ await ensureIndexExists(
59
+ "idx_audit_target",
60
+ `CREATE INDEX IF NOT EXISTS idx_audit_target ON agent_audit_log (target_type, target_id, created_at)`,
61
+ );
62
+ await ensureIndexExists(
63
+ "idx_audit_turn",
64
+ `CREATE INDEX IF NOT EXISTS idx_audit_turn ON agent_audit_log (turn_id)`,
65
+ );
66
+ await ensureIndexExists(
67
+ "idx_audit_actor",
68
+ `CREATE INDEX IF NOT EXISTS idx_audit_actor ON agent_audit_log (actor_email, created_at)`,
69
+ );
70
+ await ensureIndexExists(
71
+ "idx_audit_created",
72
+ `CREATE INDEX IF NOT EXISTS idx_audit_created ON agent_audit_log (created_at)`,
73
+ );
74
+ return;
75
+ }
47
76
 
77
+ // SQLite (local dev): no lock problem — keep the original behaviour.
78
+ await client.execute(createSql);
48
79
  const indexes = [
49
80
  `CREATE INDEX IF NOT EXISTS idx_audit_owner ON agent_audit_log (owner_email, created_at)`,
50
81
  `CREATE INDEX IF NOT EXISTS idx_audit_org ON agent_audit_log (org_id, created_at)`,
@@ -5,6 +5,7 @@ import {
5
5
  retryOnDdlRace,
6
6
  safeJsonParse,
7
7
  } from "../db/client.js";
8
+ import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
8
9
  import type {
9
10
  AgentNativeBrowserSession,
10
11
  AgentNativeBrowserSessionAction,
@@ -38,8 +39,7 @@ async function ensureTables(): Promise<void> {
38
39
  if (!initPromise) {
39
40
  initPromise = (async () => {
40
41
  const client = getDbExec();
41
- await retryOnDdlRace(() =>
42
- client.execute(`
42
+ const createSessionsSql = `
43
43
  CREATE TABLE IF NOT EXISTS ${SESSION_TABLE} (
44
44
  owner_email TEXT NOT NULL,
45
45
  session_id TEXT NOT NULL,
@@ -53,16 +53,8 @@ async function ensureTables(): Promise<void> {
53
53
  expires_at ${intType()} NOT NULL,
54
54
  PRIMARY KEY (owner_email, session_id)
55
55
  )
56
- `),
57
- );
58
- await retryOnDdlRace(() =>
59
- client.execute(`
60
- CREATE INDEX IF NOT EXISTS agent_native_browser_sessions_owner_seen_idx
61
- ON ${SESSION_TABLE} (owner_email, last_seen_at)
62
- `),
63
- );
64
- await retryOnDdlRace(() =>
65
- client.execute(`
56
+ `;
57
+ const createRequestsSql = `
66
58
  CREATE TABLE IF NOT EXISTS ${REQUEST_TABLE} (
67
59
  owner_email TEXT NOT NULL,
68
60
  session_id TEXT NOT NULL,
@@ -80,8 +72,35 @@ async function ensureTables(): Promise<void> {
80
72
  error TEXT,
81
73
  PRIMARY KEY (owner_email, request_id)
82
74
  )
75
+ `;
76
+
77
+ if (isPostgres()) {
78
+ // PG-guard: probe information_schema / pg_indexes first (no lock) and
79
+ // only issue DDL when the table/index is actually missing, wrapped in
80
+ // a transaction-scoped lock_timeout so a contended lock fails fast.
81
+ await ensureTableExists(SESSION_TABLE, createSessionsSql);
82
+ await ensureIndexExists(
83
+ "agent_native_browser_sessions_owner_seen_idx",
84
+ `CREATE INDEX IF NOT EXISTS agent_native_browser_sessions_owner_seen_idx ON ${SESSION_TABLE} (owner_email, last_seen_at)`,
85
+ );
86
+ await ensureTableExists(REQUEST_TABLE, createRequestsSql);
87
+ await ensureIndexExists(
88
+ "agent_native_browser_session_requests_pending_idx",
89
+ `CREATE INDEX IF NOT EXISTS agent_native_browser_session_requests_pending_idx ON ${REQUEST_TABLE} (owner_email, session_id, status, created_at)`,
90
+ );
91
+ return;
92
+ }
93
+
94
+ // SQLite (local dev): no ACCESS EXCLUSIVE lock problem — keep existing
95
+ // retryOnDdlRace behaviour.
96
+ await retryOnDdlRace(() => client.execute(createSessionsSql));
97
+ await retryOnDdlRace(() =>
98
+ client.execute(`
99
+ CREATE INDEX IF NOT EXISTS agent_native_browser_sessions_owner_seen_idx
100
+ ON ${SESSION_TABLE} (owner_email, last_seen_at)
83
101
  `),
84
102
  );
103
+ await retryOnDdlRace(() => client.execute(createRequestsSql));
85
104
  await retryOnDdlRace(() =>
86
105
  client.execute(`
87
106
  CREATE INDEX IF NOT EXISTS agent_native_browser_session_requests_pending_idx
@@ -5,7 +5,12 @@ import {
5
5
  normalizeThreadRepository,
6
6
  normalizeThreadTitle,
7
7
  } from "../agent/thread-data-builder.js";
8
- import { getDbExec, intType } from "../db/client.js";
8
+ import { getDbExec, intType, isPostgres } from "../db/client.js";
9
+ import {
10
+ ensureColumnExists,
11
+ ensureIndexExists,
12
+ ensureTableExists,
13
+ } from "../db/ddl-guard.js";
9
14
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
10
15
  import { emitChatThreadChange } from "./emitter.js";
11
16
 
@@ -53,7 +58,7 @@ async function ensureTable(): Promise<void> {
53
58
  if (!_initPromise) {
54
59
  _initPromise = (async () => {
55
60
  const client = getDbExec();
56
- await client.execute(`
61
+ const createSql = `
57
62
  CREATE TABLE IF NOT EXISTS chat_threads (
58
63
  id TEXT PRIMARY KEY,
59
64
  owner_email TEXT NOT NULL,
@@ -69,7 +74,66 @@ async function ensureTable(): Promise<void> {
69
74
  pinned_at ${intType()},
70
75
  archived_at ${intType()}
71
76
  )
72
- `);
77
+ `;
78
+
79
+ if (isPostgres()) {
80
+ // Hot path: the `chat_threads` table and its indexes are virtually
81
+ // always already present in production. Issuing `CREATE TABLE`/
82
+ // `CREATE INDEX` still takes a lock that, in a fresh background-worker
83
+ // process behind a concurrent connection on the shared Neon DB, can
84
+ // block ~indefinitely (ACCESS EXCLUSIVE for CREATE TABLE; a write-
85
+ // blocking SHARE lock for CREATE INDEX). The ensure* wrappers probe
86
+ // `information_schema`/`pg_indexes` first (plain reads, no lock) and
87
+ // run DDL ONLY for what is actually missing, bounded by a transaction-
88
+ // scoped `lock_timeout`. If a swallowed lock-timeout leaves the schema
89
+ // still missing they RE-PROBE and THROW rather than letting init
90
+ // memoize success against absent schema. `chat_threads` is the
91
+ // unqualified name even though the table lives in `public`.
92
+ await ensureTableExists("chat_threads", createSql);
93
+ // Additive columns — guarded so the hot path (columns already present)
94
+ // skips the ACCESS EXCLUSIVE ALTER entirely.
95
+ for (const [col, type] of [
96
+ ["scope_type", "TEXT"],
97
+ ["scope_id", "TEXT"],
98
+ ["scope_label", "TEXT"],
99
+ ["pinned_at", intType()],
100
+ ["archived_at", intType()],
101
+ ] as const) {
102
+ await ensureColumnExists(
103
+ "chat_threads",
104
+ col,
105
+ `ALTER TABLE chat_threads ADD COLUMN IF NOT EXISTS ${col} ${type}`,
106
+ );
107
+ }
108
+ // Widen millisecond-timestamp columns that older deployments created as
109
+ // 32-bit `INTEGER`; on Postgres the `Date.now()` written on every turn
110
+ // overflows int4. No-op once widened / on fresh BIGINT databases.
111
+ await widenIntColumnsToBigInt("chat_threads", [
112
+ "created_at",
113
+ "updated_at",
114
+ "pinned_at",
115
+ "archived_at",
116
+ ]);
117
+ // Indexes for the hot read paths. Both the sidebar list and the
118
+ // scoped/per-resource list filter on owner_email (and optionally
119
+ // scope) and sort by updated_at. Probe pg_indexes first (no lock)
120
+ // and skip the SHARE-locking CREATE INDEX when already present.
121
+ await ensureIndexExists(
122
+ "chat_threads_owner_updated_idx",
123
+ `CREATE INDEX IF NOT EXISTS chat_threads_owner_updated_idx ON chat_threads (owner_email, updated_at)`,
124
+ );
125
+ await ensureIndexExists(
126
+ "chat_threads_scope_updated_idx",
127
+ `CREATE INDEX IF NOT EXISTS chat_threads_scope_updated_idx ON chat_threads (scope_type, scope_id, updated_at)`,
128
+ );
129
+ // One-time backfill of message_count for legacy rows written before
130
+ // the column was maintained.
131
+ await backfillLegacyMessageCounts(client);
132
+ return;
133
+ }
134
+
135
+ // SQLite (local dev): no lock problem — keep the original behaviour.
136
+ await client.execute(createSql);
73
137
  // Additive migration for existing tables. Both SQLite and Postgres
74
138
  // accept `ALTER TABLE ADD COLUMN` and will raise when the column
75
139
  // already exists; the try/catch makes the call idempotent across
@@ -1,4 +1,5 @@
1
- import { getDbExec, intType } from "../db/client.js";
1
+ import { getDbExec, intType, isPostgres } from "../db/client.js";
2
+ import { ensureTableExists } from "../db/ddl-guard.js";
2
3
 
3
4
  let _initPromise: Promise<void> | undefined;
4
5
 
@@ -6,7 +7,7 @@ async function ensureCheckpointTable(): Promise<void> {
6
7
  if (!_initPromise) {
7
8
  _initPromise = (async () => {
8
9
  const client = getDbExec();
9
- await client.execute(`
10
+ const createSql = `
10
11
  CREATE TABLE IF NOT EXISTS agent_checkpoints (
11
12
  id TEXT PRIMARY KEY,
12
13
  thread_id TEXT NOT NULL,
@@ -15,7 +16,17 @@ async function ensureCheckpointTable(): Promise<void> {
15
16
  message TEXT NOT NULL DEFAULT '',
16
17
  created_at ${intType()} NOT NULL
17
18
  )
18
- `);
19
+ `;
20
+
21
+ if (isPostgres()) {
22
+ // PG-guard: probe information_schema before issuing DDL to avoid ACCESS
23
+ // EXCLUSIVE lock contention in fresh background-worker processes.
24
+ await ensureTableExists("agent_checkpoints", createSql);
25
+ return;
26
+ }
27
+
28
+ // SQLite (local dev): no lock problem — keep the original behaviour.
29
+ await client.execute(createSql);
19
30
  })().catch((err) => {
20
31
  // Retry init on the next call after a failed startup.
21
32
  _initPromise = undefined;
@@ -29,7 +29,16 @@ const STANDALONE_EXACT_DEPENDENCY_OVERRIDES: Record<string, string> = {
29
29
  };
30
30
  const SENTRY_MINIMUM_RELEASE_AGE_EXCLUDES = [
31
31
  '"@sentry/browser"',
32
+ '"@sentry/browser-utils"',
33
+ '"@sentry/conventions"',
34
+ '"@sentry/core"',
35
+ '"@sentry/feedback"',
32
36
  '"@sentry/node"',
37
+ '"@sentry/node-core"',
38
+ '"@sentry/opentelemetry"',
39
+ '"@sentry/replay"',
40
+ '"@sentry/replay-canvas"',
41
+ '"@sentry/server-utils"',
33
42
  ];
34
43
  const FIRST_PARTY_TARBALL_SYMLINK_EXCLUDES = [
35
44
  "*/CLAUDE.md",
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { getDbExec, isPostgres } from "../db/client.js";
9
+ import { ensureTableExists, ensureColumnExists } from "../db/ddl-guard.js";
9
10
 
10
11
  let _initPromise: Promise<void> | undefined;
11
12
 
@@ -14,7 +15,7 @@ async function ensureTable(): Promise<void> {
14
15
  _initPromise = (async () => {
15
16
  const client = getDbExec();
16
17
  const nowDefault = isPostgres() ? "NOW()::text" : "datetime('now')";
17
- await client.execute(`
18
+ const createSql = `
18
19
  CREATE TABLE IF NOT EXISTS _collab_docs (
19
20
  doc_id TEXT PRIMARY KEY,
20
21
  yjs_state TEXT NOT NULL,
@@ -22,7 +23,24 @@ async function ensureTable(): Promise<void> {
22
23
  version INTEGER NOT NULL DEFAULT 0,
23
24
  updated_at TEXT NOT NULL DEFAULT (${nowDefault})
24
25
  )
25
- `);
26
+ `;
27
+
28
+ if (isPostgres()) {
29
+ // PG-guard: probe information_schema first (no lock) and only issue
30
+ // DDL when the table/column is actually missing, wrapped in a
31
+ // transaction-scoped lock_timeout so a contended lock fails fast.
32
+ await ensureTableExists("_collab_docs", createSql);
33
+ await ensureColumnExists(
34
+ "_collab_docs",
35
+ "version",
36
+ `ALTER TABLE _collab_docs ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 0`,
37
+ );
38
+ return;
39
+ }
40
+
41
+ // SQLite (local dev): no ACCESS EXCLUSIVE lock problem — keep existing
42
+ // create-then-additive-alter behaviour.
43
+ await client.execute(createSql);
26
44
  try {
27
45
  await client.execute(
28
46
  `ALTER TABLE _collab_docs ADD COLUMN version INTEGER NOT NULL DEFAULT 0`,
@@ -121,6 +121,118 @@ export async function pgIndexExists(
121
121
  }
122
122
  }
123
123
 
124
+ /**
125
+ * Probe → guarded-DDL → re-probe sequence for one piece of on-demand schema.
126
+ *
127
+ * This is the single safe primitive every `ensureTable()` should use so a
128
+ * swallowed `lock_timeout` can NEVER poison a store's init memo with missing
129
+ * schema. The flow:
130
+ *
131
+ * 1. `probe()` — cheap, lock-free existence check (pgTableExists / etc.).
132
+ * Already present → return `false` (nothing to do; the hot path takes NO
133
+ * lock).
134
+ * 2. Missing → run `ddl` through `runGuardedDdl` (bounded `lock_timeout`).
135
+ * - DDL completed → return `true`.
136
+ * - DDL was swallowed by a lock-timeout (`runGuardedDdl` returned `false`)
137
+ * → RE-PROBE. A concurrent connection virtually always created the
138
+ * object meanwhile, so the re-probe usually now reports it present →
139
+ * return `true`. But if it is STILL missing, the required schema does
140
+ * NOT exist and we must NOT let the caller memoize success — so THROW.
141
+ * The caller's `_initPromise` rejects and the next call retries instead
142
+ * of running forever against absent schema.
143
+ *
144
+ * On SQLite the probe is always `false` (helpers no-op there) and the SQLite
145
+ * branch of each store keeps its own create-then-catch behaviour, so this is a
146
+ * Postgres-path primitive in practice. Returns `true` when the object exists
147
+ * after this call (either pre-existing-after-timeout-race or freshly created),
148
+ * `false` only when it already existed up front (no DDL issued).
149
+ */
150
+ export async function ensureSchemaObject(options: {
151
+ /** Lock-free existence check; `true` ⇒ already present, skip DDL. */
152
+ probe: () => Promise<boolean>;
153
+ /** DDL to run only when `probe()` reports the object missing. */
154
+ ddl: string;
155
+ /** Human-readable name of what's being ensured, for the error message. */
156
+ label: string;
157
+ /** Forwarded to `runGuardedDdl`. */
158
+ lockTimeout?: string;
159
+ /** Injectable client for tests. */
160
+ injectedClient?: DbExec;
161
+ }): Promise<boolean> {
162
+ const { probe, ddl, label, lockTimeout, injectedClient } = options;
163
+ if (await probe()) return false;
164
+ const ran = await runGuardedDdl(ddl, { lockTimeout, injectedClient });
165
+ if (ran) return true;
166
+ // The DDL was swallowed by a lock-timeout. The object is virtually always
167
+ // already correct by the time a contended boot retries (a concurrent
168
+ // connection created it), so re-probe before giving up.
169
+ if (await probe()) return true;
170
+ // Still missing after a swallowed timeout: do NOT memoize success with absent
171
+ // schema. Throw so the caller's init promise rejects and the next call
172
+ // retries, rather than leaving ensureTable "initialized" against a table/
173
+ // column/index that does not exist.
174
+ throw new Error(
175
+ `ensureSchemaObject: required schema "${label}" is still missing after a ` +
176
+ `lock-timed-out DDL; refusing to memoize init success. The next call will retry.`,
177
+ );
178
+ }
179
+
180
+ /**
181
+ * Convenience wrapper: ensure a TABLE exists (probe via `pgTableExists`).
182
+ * No-op-returns `false` on SQLite (probe is always false there) — callers run
183
+ * the SQLite create on their own branch, so this is used on the Postgres path.
184
+ */
185
+ export async function ensureTableExists(
186
+ table: string,
187
+ createSql: string,
188
+ options: { lockTimeout?: string; injectedClient?: DbExec } = {},
189
+ ): Promise<boolean> {
190
+ return ensureSchemaObject({
191
+ probe: () => pgTableExists(table, options.injectedClient),
192
+ ddl: createSql,
193
+ label: `table ${table}`,
194
+ lockTimeout: options.lockTimeout,
195
+ injectedClient: options.injectedClient,
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Convenience wrapper: ensure a COLUMN exists (probe via `pgColumnExists`).
201
+ * `addColumnSql` should be the full `ALTER TABLE … ADD COLUMN IF NOT EXISTS …`.
202
+ */
203
+ export async function ensureColumnExists(
204
+ table: string,
205
+ column: string,
206
+ addColumnSql: string,
207
+ options: { lockTimeout?: string; injectedClient?: DbExec } = {},
208
+ ): Promise<boolean> {
209
+ return ensureSchemaObject({
210
+ probe: () => pgColumnExists(table, column, options.injectedClient),
211
+ ddl: addColumnSql,
212
+ label: `column ${table}.${column}`,
213
+ lockTimeout: options.lockTimeout,
214
+ injectedClient: options.injectedClient,
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Convenience wrapper: ensure an INDEX exists (probe via `pgIndexExists`).
220
+ * `createIndexSql` should be the full `CREATE INDEX IF NOT EXISTS <name> …`.
221
+ */
222
+ export async function ensureIndexExists(
223
+ indexName: string,
224
+ createIndexSql: string,
225
+ options: { lockTimeout?: string; injectedClient?: DbExec } = {},
226
+ ): Promise<boolean> {
227
+ return ensureSchemaObject({
228
+ probe: () => pgIndexExists(indexName, options.injectedClient),
229
+ ddl: createIndexSql,
230
+ label: `index ${indexName}`,
231
+ lockTimeout: options.lockTimeout,
232
+ injectedClient: options.injectedClient,
233
+ });
234
+ }
235
+
124
236
  /** True when an error looks like a Postgres `lock_timeout` (SQLSTATE 55P03). */
125
237
  export function isLockTimeoutError(err: unknown): boolean {
126
238
  const anyErr = err as { code?: unknown; message?: unknown } | null;
@@ -5,6 +5,11 @@ import { and, eq, gte, isNull } from "drizzle-orm";
5
5
  import { appStatePut } from "../application-state/store.js";
6
6
  import { getDbExec, isPostgres, retryOnDdlRace } from "../db/client.js";
7
7
  import { createGetDb } from "../db/create-get-db.js";
8
+ import {
9
+ ensureTableExists,
10
+ ensureColumnExists,
11
+ ensureIndexExists,
12
+ } from "../db/ddl-guard.js";
8
13
  import { recordChange } from "../server/poll.js";
9
14
  import {
10
15
  getRequestUserEmail,
@@ -77,6 +82,70 @@ export async function ensureExtensionsTables(): Promise<void> {
77
82
  _initPromise = (async () => {
78
83
  const client = getDbExec();
79
84
  const pg = isPostgres();
85
+ if (pg) {
86
+ // PG guard: probe via information_schema, only issue DDL if missing, bounded lock_timeout
87
+ await ensureTableExists("tools", EXTENSIONS_CREATE_SQL_PG);
88
+ await migrateMisnamedExtensionsTable(client, pg); // data migration, not DDL — unchanged
89
+ await ensureTableExists("tool_shares", EXTENSION_SHARES_CREATE_SQL_PG);
90
+ await ensureTableExists("tool_data", EXTENSION_DATA_CREATE_SQL_PG);
91
+ await ensureExtensionDataItemId(client, pg); // ADD COLUMN — guarded inside
92
+ await ensureExtensionDataScope(client, pg); // ADD COLUMN — guarded inside
93
+ // DROP INDEX (for old index) — safe DDL, runs via client.execute; keep on both paths
94
+ await client.execute(EXTENSION_DATA_DROP_OLD_INDEX_SQL_PG);
95
+ await ensureIndexExists(
96
+ "tool_data_scoped_item_idx",
97
+ EXTENSION_DATA_ITEM_INDEX_SQL_PG,
98
+ );
99
+ await ensureIndexExists("tools_owner_idx", EXTENSIONS_OWNER_INDEX_SQL);
100
+ await ensureIndexExists("tools_org_idx", EXTENSIONS_ORG_INDEX_SQL);
101
+ await ensureIndexExists(
102
+ "tools_updated_at_idx",
103
+ EXTENSIONS_UPDATED_INDEX_SQL,
104
+ );
105
+ await ensureExtensionsGlobalHideColumns(client, pg); // ADD COLUMN — guarded inside
106
+ await ensureIndexExists(
107
+ "tools_hidden_at_idx",
108
+ EXTENSIONS_HIDDEN_AT_INDEX_SQL,
109
+ );
110
+ await ensureIndexExists(
111
+ "tool_shares_resource_idx",
112
+ EXTENSION_SHARES_RESOURCE_INDEX_SQL,
113
+ );
114
+ await ensureTableExists(
115
+ "tool_hidden_extensions",
116
+ EXTENSION_HIDES_CREATE_SQL_PG,
117
+ );
118
+ await ensureIndexExists(
119
+ "tool_hidden_extensions_user_tool_idx",
120
+ EXTENSION_HIDES_UNIQUE_INDEX_SQL,
121
+ );
122
+ await ensureIndexExists(
123
+ "tool_hidden_extensions_owner_idx",
124
+ EXTENSION_HIDES_OWNER_INDEX_SQL,
125
+ );
126
+ await ensureTableExists(
127
+ "tool_history",
128
+ EXTENSION_HISTORY_CREATE_SQL_PG,
129
+ );
130
+ await ensureIndexExists(
131
+ "tool_history_tool_version_idx",
132
+ EXTENSION_HISTORY_VERSION_INDEX_SQL,
133
+ );
134
+ await ensureIndexExists(
135
+ "tool_history_tool_created_idx",
136
+ EXTENSION_HISTORY_CREATED_INDEX_SQL,
137
+ );
138
+ await ensureTableExists(
139
+ "tool_consents",
140
+ EXTENSION_CONSENTS_CREATE_SQL_PG,
141
+ );
142
+ await ensureIndexExists(
143
+ "tool_consents_viewer_idx",
144
+ EXTENSION_CONSENTS_VIEWER_INDEX_SQL,
145
+ );
146
+ return;
147
+ }
148
+ // SQLite (local dev): keep existing behavior
80
149
  await retryOnDdlRace(() =>
81
150
  client.execute(pg ? EXTENSIONS_CREATE_SQL_PG : EXTENSIONS_CREATE_SQL),
82
151
  );
@@ -192,7 +261,9 @@ async function ensureExtensionDataItemId(
192
261
  pg: boolean,
193
262
  ): Promise<void> {
194
263
  if (pg) {
195
- await client.execute(
264
+ await ensureColumnExists(
265
+ "tool_data",
266
+ "item_id",
196
267
  `ALTER TABLE tool_data ADD COLUMN IF NOT EXISTS item_id TEXT`,
197
268
  );
198
269
  return;
@@ -219,7 +290,9 @@ async function ensureExtensionDataScope(
219
290
  ): Promise<void> {
220
291
  const addCol = (name: string, def: string) => {
221
292
  if (pg) {
222
- return client.execute(
293
+ return ensureColumnExists(
294
+ "tool_data",
295
+ name,
223
296
  `ALTER TABLE tool_data ADD COLUMN IF NOT EXISTS ${name} ${def}`,
224
297
  );
225
298
  }
@@ -255,7 +328,7 @@ async function ensureExtensionsGlobalHideColumns(
255
328
  // does not, so we drop the clause and swallow the duplicate-column error.
256
329
  const addCol = (pgSql: string, name: string, def: string) => {
257
330
  if (pg) {
258
- return retryOnDdlRace(() => client.execute(pgSql));
331
+ return ensureColumnExists("tools", name, pgSql);
259
332
  }
260
333
  return client
261
334
  .execute(`ALTER TABLE tools ADD COLUMN ${name} ${def}`)
@@ -4,6 +4,11 @@ import {
4
4
  intType,
5
5
  retryOnDdlRace,
6
6
  } from "../db/client.js";
7
+ import {
8
+ ensureTableExists,
9
+ ensureColumnExists,
10
+ ensureIndexExists,
11
+ } from "../db/ddl-guard.js";
7
12
  import { isDuplicateColumnError } from "../db/migrations.js";
8
13
  import type { IncomingMessage } from "./types.js";
9
14
 
@@ -11,36 +16,74 @@ let _initPromise: Promise<void> | undefined;
11
16
  const PROCESSING_STUCK_AFTER_MS = 5 * 60 * 1000;
12
17
  const PROCESSING_NEXT_CHECK_STALE_AFTER_MS = 60 * 1000;
13
18
 
19
+ // Build the CREATE SQL lazily (not at module scope) so intType() runs at
20
+ // RUNTIME, not import time — a module-scope call breaks any consumer whose
21
+ // db/client mock doesn't stub intType (e.g. db-admin specs).
22
+ function buildCreateSql(): string {
23
+ return `
24
+ CREATE TABLE IF NOT EXISTS integration_a2a_continuations (
25
+ id TEXT PRIMARY KEY,
26
+ integration_task_id TEXT NOT NULL,
27
+ platform TEXT NOT NULL,
28
+ external_thread_id TEXT NOT NULL,
29
+ incoming_payload TEXT NOT NULL,
30
+ placeholder_ref TEXT,
31
+ owner_email TEXT NOT NULL,
32
+ org_id TEXT,
33
+ agent_name TEXT NOT NULL,
34
+ agent_url TEXT NOT NULL,
35
+ dedupe_key TEXT,
36
+ a2a_task_id TEXT NOT NULL,
37
+ a2a_auth_token TEXT,
38
+ status TEXT NOT NULL,
39
+ attempts ${intType()} NOT NULL DEFAULT 0,
40
+ next_check_at ${intType()} NOT NULL,
41
+ error_message TEXT,
42
+ created_at ${intType()} NOT NULL,
43
+ updated_at ${intType()} NOT NULL,
44
+ completed_at ${intType()}
45
+ )
46
+ `;
47
+ }
48
+
14
49
  async function ensureTable(): Promise<void> {
15
50
  if (!_initPromise) {
16
51
  _initPromise = (async () => {
17
52
  const client = getDbExec();
18
- await retryOnDdlRace(() =>
19
- client.execute(`
20
- CREATE TABLE IF NOT EXISTS integration_a2a_continuations (
21
- id TEXT PRIMARY KEY,
22
- integration_task_id TEXT NOT NULL,
23
- platform TEXT NOT NULL,
24
- external_thread_id TEXT NOT NULL,
25
- incoming_payload TEXT NOT NULL,
26
- placeholder_ref TEXT,
27
- owner_email TEXT NOT NULL,
28
- org_id TEXT,
29
- agent_name TEXT NOT NULL,
30
- agent_url TEXT NOT NULL,
31
- dedupe_key TEXT,
32
- a2a_task_id TEXT NOT NULL,
33
- a2a_auth_token TEXT,
34
- status TEXT NOT NULL,
35
- attempts ${intType()} NOT NULL DEFAULT 0,
36
- next_check_at ${intType()} NOT NULL,
37
- error_message TEXT,
38
- created_at ${intType()} NOT NULL,
39
- updated_at ${intType()} NOT NULL,
40
- completed_at ${intType()}
41
- )
42
- `),
43
- );
53
+ const createSql = buildCreateSql();
54
+ if (isPostgres()) {
55
+ // PG guard: probe via information_schema, only issue DDL if missing, bounded lock_timeout
56
+ await ensureTableExists("integration_a2a_continuations", createSql);
57
+ await ensureIndexExists(
58
+ "idx_a2a_continuations_status_next",
59
+ `CREATE INDEX IF NOT EXISTS idx_a2a_continuations_status_next ON integration_a2a_continuations(status, next_check_at)`,
60
+ );
61
+ await ensureIndexExists(
62
+ "idx_a2a_continuations_integration_task",
63
+ `CREATE INDEX IF NOT EXISTS idx_a2a_continuations_integration_task ON integration_a2a_continuations(integration_task_id)`,
64
+ );
65
+ await ensureIndexExists(
66
+ "idx_a2a_continuations_remote_task",
67
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_a2a_continuations_remote_task ON integration_a2a_continuations(integration_task_id, agent_url, a2a_task_id)`,
68
+ );
69
+ await ensureColumnExists(
70
+ "integration_a2a_continuations",
71
+ "a2a_auth_token",
72
+ `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS a2a_auth_token TEXT`,
73
+ );
74
+ await ensureColumnExists(
75
+ "integration_a2a_continuations",
76
+ "dedupe_key",
77
+ `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS dedupe_key TEXT`,
78
+ );
79
+ await ensureIndexExists(
80
+ "idx_a2a_continuations_dedupe_key",
81
+ `CREATE INDEX IF NOT EXISTS idx_a2a_continuations_dedupe_key ON integration_a2a_continuations(integration_task_id, agent_url, dedupe_key)`,
82
+ );
83
+ return;
84
+ }
85
+ // SQLite (local dev): keep existing behavior
86
+ await retryOnDdlRace(() => client.execute(createSql));
44
87
  await retryOnDdlRace(() =>
45
88
  client.execute(
46
89
  `CREATE INDEX IF NOT EXISTS idx_a2a_continuations_status_next ON integration_a2a_continuations(status, next_check_at)`,