@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
@@ -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",
@@ -102,7 +102,7 @@ function pruneIfEmpty(docId: string, map: Map<number, AwarenessEntry>): void {
102
102
  *
103
103
  * Client sends its awareness state and receives other clients' states.
104
104
  *
105
- * Body: { clientId: number, state: string (base64) }
105
+ * Body: { clientId: number, state: string | null (JSON-encoded awareness state, or null to clear) }
106
106
  * Response: { states: Array<{ clientId: number, state: string }> }
107
107
  */
108
108
  export const postAwareness = defineEventHandler(async (event: H3Event) => {
@@ -115,28 +115,31 @@ export const postAwareness = defineEventHandler(async (event: H3Event) => {
115
115
  const body = await readBody(event);
116
116
  const { clientId, state } = body as {
117
117
  clientId?: number;
118
- state?: string;
118
+ state?: string | null;
119
119
  };
120
120
 
121
- if (clientId == null || !state) {
121
+ if (clientId == null || state === undefined) {
122
122
  // `!clientId` would wrongly reject clientId === 0, which is a valid
123
- // (if rare) Yjs client id. Only reject missing/null ids here.
123
+ // (if rare) Yjs client id. A null state is valid: it clears this client.
124
124
  setResponseStatus(event, 400);
125
125
  return { error: "clientId and state required" };
126
126
  }
127
127
 
128
128
  const map = getDocAwareness(docId);
129
129
 
130
- // Store this client's state
131
- map.set(clientId, { clientId, state, lastSeen: Date.now() });
130
+ if (state === null) {
131
+ map.delete(clientId);
132
+ } else {
133
+ // Store this client's state
134
+ map.set(clientId, { clientId, state, lastSeen: Date.now() });
135
+ }
132
136
 
133
137
  // Clean expired entries, then prune the outer-map entry if it becomes empty.
134
138
  // Without pruning, a deployment with many transient docIds (e.g. one per
135
139
  // session) would grow _awarenessMap without bound.
136
140
  cleanExpired(map);
137
- // map has at least the sender's entry so size >= 1 here; pruneIfEmpty is a
138
- // no-op in the normal path but guards against edge cases (e.g. clientId 0
139
- // that was immediately evicted by a concurrent cleanExpired run).
141
+ // Null-state clears and expiry can empty the map; prune the outer entry so
142
+ // transient document ids do not accumulate.
140
143
  pruneIfEmpty(docId, map);
141
144
 
142
145
  // Build the full list of current states (all clients including sender).
@@ -276,11 +276,13 @@ function scheduleAwarenessPush(
276
276
  const timer = setTimeout(() => {
277
277
  _awarenessThrottleTimers.delete(key);
278
278
  const state = getState();
279
- if (!state) return;
280
279
  fetch(`${baseUrl}/${docId}/awareness`, {
281
280
  method: "POST",
282
281
  headers: { "Content-Type": "application/json" },
283
- body: JSON.stringify({ clientId, state: JSON.stringify(state) }),
282
+ body: JSON.stringify({
283
+ clientId,
284
+ state: state ? JSON.stringify(state) : null,
285
+ }),
284
286
  }).catch(() => {}); // best-effort; poll cycle is the baseline fallback
285
287
  }, 150);
286
288
 
@@ -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}`)