@agent-native/core 0.77.7 → 0.77.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +46 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/task-store.ts +25 -3
  5. package/corpus/core/src/agent/harness/store.ts +89 -55
  6. package/corpus/core/src/agent/observational-memory/store.ts +40 -14
  7. package/corpus/core/src/agent/production-agent.ts +18 -0
  8. package/corpus/core/src/agent/run-store.ts +116 -59
  9. package/corpus/core/src/application-state/store.ts +36 -2
  10. package/corpus/core/src/audit/store.ts +37 -6
  11. package/corpus/core/src/browser-sessions/store.ts +31 -12
  12. package/corpus/core/src/chat-threads/store.ts +67 -3
  13. package/corpus/core/src/checkpoints/store.ts +14 -3
  14. package/corpus/core/src/cli/create.ts +9 -0
  15. package/corpus/core/src/collab/storage.ts +20 -2
  16. package/corpus/core/src/db/ddl-guard.ts +302 -0
  17. package/corpus/core/src/extensions/store.ts +76 -3
  18. package/corpus/core/src/integrations/a2a-continuations-store.ts +69 -26
  19. package/corpus/core/src/integrations/config-store.ts +19 -18
  20. package/corpus/core/src/integrations/pending-tasks-store.ts +40 -16
  21. package/corpus/core/src/integrations/remote-commands-store.ts +37 -23
  22. package/corpus/core/src/integrations/remote-devices-store.ts +64 -20
  23. package/corpus/core/src/integrations/remote-push-store.ts +75 -35
  24. package/corpus/core/src/integrations/remote-run-events-store.ts +38 -12
  25. package/corpus/core/src/integrations/thread-mapping-store.ts +11 -2
  26. package/corpus/core/src/mcp/connect-store.ts +53 -16
  27. package/corpus/core/src/mcp/oauth-store.ts +32 -7
  28. package/corpus/core/src/notifications/store.ts +20 -4
  29. package/corpus/core/src/oauth-tokens/store.ts +45 -2
  30. package/corpus/core/src/observability/store.ts +154 -47
  31. package/corpus/core/src/progress/store.ts +36 -21
  32. package/corpus/core/src/provider-api/corpus-jobs-store.ts +25 -5
  33. package/corpus/core/src/provider-api/custom-registry.ts +15 -4
  34. package/corpus/core/src/provider-api/quota-governor.ts +15 -3
  35. package/corpus/core/src/provider-api/staged-datasets-store.ts +22 -5
  36. package/corpus/core/src/resources/store.ts +66 -31
  37. package/corpus/core/src/secrets/storage.ts +32 -2
  38. package/corpus/core/src/server/agent-teams-run-queue.ts +22 -10
  39. package/corpus/core/src/server/auth.ts +22 -4
  40. package/corpus/core/src/server/better-auth-instance.ts +49 -15
  41. package/corpus/core/src/server/embed-session.ts +19 -4
  42. package/corpus/core/src/server/identity-sso-store.ts +40 -11
  43. package/corpus/core/src/server/recap-image-store.ts +28 -9
  44. package/corpus/core/src/settings/store.ts +35 -6
  45. package/corpus/core/src/usage/store.ts +51 -16
  46. package/corpus/core/src/workspace-connections/store.ts +200 -22
  47. package/corpus/templates/calendar/changelog/2026-06-25-booking-link-previews-now-show-the-content-directly-on-the-g.md +6 -0
  48. package/corpus/templates/calendar/server/lib/booking-og-image.ts +0 -2
  49. package/corpus/templates/forms/changelog/2026-06-25-form-previews-now-show-the-content-directly-on-the-grid-back.md +6 -0
  50. package/corpus/templates/forms/server/lib/form-og-image.ts +0 -2
  51. package/corpus/templates/plan/app/global.css +6 -11
  52. package/corpus/templates/plan/app/pages/PlansPage.tsx +12 -10
  53. package/corpus/templates/plan/changelog/2026-06-25-plan-access-request-pages-stay-visible-during-background-ref.md +6 -0
  54. package/corpus/templates/plan/changelog/2026-06-25-plan-documents-keep-their-full-reading-width-below-1200px-an.md +6 -0
  55. package/dist/a2a/task-store.d.ts.map +1 -1
  56. package/dist/a2a/task-store.js +19 -3
  57. package/dist/a2a/task-store.js.map +1 -1
  58. package/dist/agent/harness/store.d.ts.map +1 -1
  59. package/dist/agent/harness/store.js +63 -34
  60. package/dist/agent/harness/store.js.map +1 -1
  61. package/dist/agent/observational-memory/store.d.ts.map +1 -1
  62. package/dist/agent/observational-memory/store.js +30 -8
  63. package/dist/agent/observational-memory/store.js.map +1 -1
  64. package/dist/agent/production-agent.d.ts.map +1 -1
  65. package/dist/agent/production-agent.js +18 -0
  66. package/dist/agent/production-agent.js.map +1 -1
  67. package/dist/agent/run-store.d.ts.map +1 -1
  68. package/dist/agent/run-store.js +103 -46
  69. package/dist/agent/run-store.js.map +1 -1
  70. package/dist/application-state/store.d.ts.map +1 -1
  71. package/dist/application-state/store.js +28 -2
  72. package/dist/application-state/store.js.map +1 -1
  73. package/dist/audit/store.d.ts.map +1 -1
  74. package/dist/audit/store.js +18 -3
  75. package/dist/audit/store.js.map +1 -1
  76. package/dist/browser-sessions/store.d.ts.map +1 -1
  77. package/dist/browser-sessions/store.js +22 -7
  78. package/dist/browser-sessions/store.js.map +1 -1
  79. package/dist/chat-threads/store.d.ts.map +1 -1
  80. package/dist/chat-threads/store.js +51 -3
  81. package/dist/chat-threads/store.js.map +1 -1
  82. package/dist/checkpoints/store.d.ts.map +1 -1
  83. package/dist/checkpoints/store.js +12 -3
  84. package/dist/checkpoints/store.js.map +1 -1
  85. package/dist/cli/create.d.ts.map +1 -1
  86. package/dist/cli/create.js +9 -0
  87. package/dist/cli/create.js.map +1 -1
  88. package/dist/collab/routes.d.ts +2 -2
  89. package/dist/collab/storage.d.ts.map +1 -1
  90. package/dist/collab/storage.js +14 -2
  91. package/dist/collab/storage.js.map +1 -1
  92. package/dist/db/ddl-guard.d.ts +151 -0
  93. package/dist/db/ddl-guard.d.ts.map +1 -0
  94. package/dist/db/ddl-guard.js +265 -0
  95. package/dist/db/ddl-guard.js.map +1 -0
  96. package/dist/extensions/store.d.ts.map +1 -1
  97. package/dist/extensions/store.js +32 -3
  98. package/dist/extensions/store.js.map +1 -1
  99. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  100. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  101. package/dist/integrations/a2a-continuations-store.js +44 -24
  102. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  103. package/dist/integrations/config-store.d.ts.map +1 -1
  104. package/dist/integrations/config-store.js +17 -11
  105. package/dist/integrations/config-store.js.map +1 -1
  106. package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
  107. package/dist/integrations/pending-tasks-store.js +24 -16
  108. package/dist/integrations/pending-tasks-store.js.map +1 -1
  109. package/dist/integrations/remote-commands-store.d.ts.map +1 -1
  110. package/dist/integrations/remote-commands-store.js +29 -21
  111. package/dist/integrations/remote-commands-store.js.map +1 -1
  112. package/dist/integrations/remote-devices-store.d.ts.map +1 -1
  113. package/dist/integrations/remote-devices-store.js +33 -18
  114. package/dist/integrations/remote-devices-store.js.map +1 -1
  115. package/dist/integrations/remote-push-store.d.ts.map +1 -1
  116. package/dist/integrations/remote-push-store.js +53 -31
  117. package/dist/integrations/remote-push-store.js.map +1 -1
  118. package/dist/integrations/remote-run-events-store.d.ts.map +1 -1
  119. package/dist/integrations/remote-run-events-store.js +26 -10
  120. package/dist/integrations/remote-run-events-store.js.map +1 -1
  121. package/dist/integrations/thread-mapping-store.d.ts.map +1 -1
  122. package/dist/integrations/thread-mapping-store.js +10 -2
  123. package/dist/integrations/thread-mapping-store.js.map +1 -1
  124. package/dist/mcp/connect-store.d.ts.map +1 -1
  125. package/dist/mcp/connect-store.js +34 -16
  126. package/dist/mcp/connect-store.js.map +1 -1
  127. package/dist/mcp/oauth-store.d.ts.map +1 -1
  128. package/dist/mcp/oauth-store.js +22 -7
  129. package/dist/mcp/oauth-store.js.map +1 -1
  130. package/dist/notifications/routes.d.ts +2 -2
  131. package/dist/notifications/store.d.ts.map +1 -1
  132. package/dist/notifications/store.js +15 -3
  133. package/dist/notifications/store.js.map +1 -1
  134. package/dist/oauth-tokens/store.d.ts.map +1 -1
  135. package/dist/oauth-tokens/store.js +33 -2
  136. package/dist/oauth-tokens/store.js.map +1 -1
  137. package/dist/observability/routes.d.ts +7 -7
  138. package/dist/observability/store.d.ts.map +1 -1
  139. package/dist/observability/store.js +72 -29
  140. package/dist/observability/store.js.map +1 -1
  141. package/dist/progress/store.d.ts.map +1 -1
  142. package/dist/progress/store.js +31 -16
  143. package/dist/progress/store.js.map +1 -1
  144. package/dist/provider-api/corpus-jobs-store.d.ts.map +1 -1
  145. package/dist/provider-api/corpus-jobs-store.js +16 -5
  146. package/dist/provider-api/corpus-jobs-store.js.map +1 -1
  147. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  148. package/dist/provider-api/custom-registry.js +14 -4
  149. package/dist/provider-api/custom-registry.js.map +1 -1
  150. package/dist/provider-api/quota-governor.d.ts.map +1 -1
  151. package/dist/provider-api/quota-governor.js +12 -3
  152. package/dist/provider-api/quota-governor.js.map +1 -1
  153. package/dist/provider-api/staged-datasets-store.d.ts.map +1 -1
  154. package/dist/provider-api/staged-datasets-store.js +16 -5
  155. package/dist/provider-api/staged-datasets-store.js.map +1 -1
  156. package/dist/resources/handlers.d.ts +2 -2
  157. package/dist/resources/store.d.ts.map +1 -1
  158. package/dist/resources/store.js +56 -25
  159. package/dist/resources/store.js.map +1 -1
  160. package/dist/secrets/storage.d.ts.map +1 -1
  161. package/dist/secrets/storage.js +22 -2
  162. package/dist/secrets/storage.js.map +1 -1
  163. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  164. package/dist/server/agent-teams-run-queue.d.ts.map +1 -1
  165. package/dist/server/agent-teams-run-queue.js +15 -4
  166. package/dist/server/agent-teams-run-queue.js.map +1 -1
  167. package/dist/server/auth.d.ts.map +1 -1
  168. package/dist/server/auth.js +16 -2
  169. package/dist/server/auth.js.map +1 -1
  170. package/dist/server/better-auth-instance.d.ts.map +1 -1
  171. package/dist/server/better-auth-instance.js +55 -21
  172. package/dist/server/better-auth-instance.js.map +1 -1
  173. package/dist/server/embed-session.d.ts.map +1 -1
  174. package/dist/server/embed-session.js +15 -4
  175. package/dist/server/embed-session.js.map +1 -1
  176. package/dist/server/identity-sso-store.d.ts.map +1 -1
  177. package/dist/server/identity-sso-store.js +30 -11
  178. package/dist/server/identity-sso-store.js.map +1 -1
  179. package/dist/server/recap-image-store.d.ts.map +1 -1
  180. package/dist/server/recap-image-store.js +21 -7
  181. package/dist/server/recap-image-store.js.map +1 -1
  182. package/dist/server/transcribe-voice.d.ts +1 -1
  183. package/dist/settings/store.d.ts.map +1 -1
  184. package/dist/settings/store.js +30 -6
  185. package/dist/settings/store.js.map +1 -1
  186. package/dist/usage/store.d.ts.map +1 -1
  187. package/dist/usage/store.js +36 -11
  188. package/dist/usage/store.js.map +1 -1
  189. package/dist/workspace-connections/store.d.ts.map +1 -1
  190. package/dist/workspace-connections/store.js +57 -7
  191. package/dist/workspace-connections/store.js.map +1 -1
  192. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -27,5 +27,5 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
27
27
 
28
28
  ## Generated Counts
29
29
 
30
- - core files: 1932
31
- - template files: 4289
30
+ - core files: 1933
31
+ - template files: 4293
@@ -1,5 +1,51 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.77.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 12545bc: Keep standalone scaffold installs stable when fresh Sentry transitive packages are published.
8
+
9
+ ## 0.77.9
10
+
11
+ ### Patch Changes
12
+
13
+ - 3eeec7f: Generalize the `ensureTable()` DDL guard to ALL on-demand schema-init paths
14
+ (~36 stores: agent run-store, application-state, chat-threads, usage,
15
+ oauth-tokens, resources, observability, integrations, provider-api, mcp,
16
+ workspace-connections, extensions, audit, harness, a2a, notifications,
17
+ browser-sessions, auth/sessions, agent-teams run-queue, better-auth, and more).
18
+
19
+ Every `ensureTable` now probes `information_schema`/`pg_indexes` before issuing
20
+ `CREATE TABLE`/`ALTER TABLE ADD COLUMN`/`CREATE INDEX`, so the already-migrated
21
+ hot path takes NO `ACCESS EXCLUSIVE` lock on Postgres. Any DDL that must run is
22
+ wrapped in a transaction-scoped `SET LOCAL lock_timeout` (never leaks onto the
23
+ pooled connection), and a swallowed lock-timeout triggers a RE-PROBE: if the
24
+ schema is still missing it throws (so the per-store init memo rejects and retries)
25
+ rather than memoizing init success against absent schema. This fixes
26
+ background-function (durable agent-chat) workers hanging indefinitely on the
27
+ first-touch DDL lock of any table on shared Neon. Shared helpers live in
28
+ `db/ddl-guard.ts` (`ensureSchemaObject`/`ensureTableExists`/`ensureColumnExists`/
29
+ `ensureIndexExists`). SQLite (local dev) behavior is unchanged. CREATE SQL that
30
+ references `intType()` is built at runtime, not module scope.
31
+
32
+ ## 0.77.8
33
+
34
+ ### Patch Changes
35
+
36
+ - 7dc2828: Guard on-demand `ensureTable()` schema-init so the already-migrated path takes no
37
+ `ACCESS EXCLUSIVE` lock on Postgres. The app-secrets (`app_secrets`) and `settings`
38
+ stores now probe `information_schema`/`pg_indexes` first (plain reads, no lock) and
39
+ issue `CREATE`/`ALTER`/`CREATE INDEX` only when something is actually missing; any
40
+ DDL that must run is wrapped in a transaction-scoped `SET LOCAL lock_timeout` so a
41
+ contended lock fails fast instead of hanging (and never leaks onto the pooled
42
+ connection). This fixes background-function workers hanging indefinitely on
43
+ first-touch schema DDL behind a concurrent connection on shared Neon — observed as
44
+ durable agent-chat workers stalling right after auth and never claiming the run.
45
+ SQLite (local dev) behavior is unchanged. Adds a shared `db/ddl-guard.ts` helper
46
+ (`pgTableExists`/`pgColumnExists`/`pgIndexExists`/`runGuardedDdl`). Also adds
47
+ diagnostic-only worker setup sub-stage breadcrumbs to localize such stalls.
48
+
3
49
  ## 0.77.7
4
50
 
5
51
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.77.7",
3
+ "version": "0.77.10",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -1,6 +1,7 @@
1
1
  import crypto from "crypto";
2
2
 
3
- import { getDbExec, intType } from "../db/client.js";
3
+ import { getDbExec, intType, isPostgres } from "../db/client.js";
4
+ import { ensureTableExists, ensureColumnExists } from "../db/ddl-guard.js";
4
5
  import type { Task, Message, TaskState, Artifact } from "./types.js";
5
6
 
6
7
  let _initPromise: Promise<void> | undefined;
@@ -9,7 +10,7 @@ async function ensureTable(): Promise<void> {
9
10
  if (!_initPromise) {
10
11
  _initPromise = (async () => {
11
12
  const client = getDbExec();
12
- await client.execute(`
13
+ const createSql = `
13
14
  CREATE TABLE IF NOT EXISTS a2a_tasks (
14
15
  id TEXT PRIMARY KEY,
15
16
  context_id TEXT,
@@ -22,7 +23,28 @@ async function ensureTable(): Promise<void> {
22
23
  created_at ${intType()} NOT NULL,
23
24
  updated_at ${intType()} NOT NULL
24
25
  )
25
- `);
26
+ `;
27
+
28
+ if (isPostgres()) {
29
+ // PG-guard: probe information_schema before issuing DDL to avoid ACCESS
30
+ // EXCLUSIVE lock contention in fresh background-worker processes.
31
+ await ensureTableExists("a2a_tasks", createSql);
32
+ // Additive migration: owner_email column. Bound to the JWT-verified
33
+ // caller at task-creation time so handleGet / handleCancel can reject
34
+ // mismatched callers (the IDOR class fixed in PR #369). Existing rows
35
+ // have NULL owner_email and remain accessible to legacy callers via
36
+ // the legacy-token apiKeyEnv path; new rows are scoped from this point
37
+ // forward.
38
+ await ensureColumnExists(
39
+ "a2a_tasks",
40
+ "owner_email",
41
+ `ALTER TABLE a2a_tasks ADD COLUMN IF NOT EXISTS owner_email TEXT`,
42
+ );
43
+ return;
44
+ }
45
+
46
+ // SQLite (local dev): no lock problem — keep the original behaviour.
47
+ await client.execute(createSql);
26
48
  // Additive migration: owner_email column. Bound to the JWT-verified
27
49
  // caller at task-creation time so handleGet / handleCancel can reject
28
50
  // mismatched callers (the IDOR class fixed in PR #369). Existing rows
@@ -1,9 +1,9 @@
1
+ import { getDbExec, intType, isPostgres } from "../../db/client.js";
1
2
  import {
2
- getDbExec,
3
- intType,
4
- isPostgres,
5
- retryOnDdlRace,
6
- } from "../../db/client.js";
3
+ ensureColumnExists,
4
+ ensureIndexExists,
5
+ ensureTableExists,
6
+ } from "../../db/ddl-guard.js";
7
7
 
8
8
  export type AgentHarnessSessionStatus =
9
9
  | "running"
@@ -50,26 +50,66 @@ export async function ensureAgentHarnessSessionTables(): Promise<void> {
50
50
  if (!initPromise) {
51
51
  initPromise = (async () => {
52
52
  const client = getDbExec();
53
- await retryOnDdlRace(() =>
54
- client.execute(`
55
- CREATE TABLE IF NOT EXISTS agent_harness_sessions (
56
- id TEXT PRIMARY KEY,
57
- harness_name TEXT NOT NULL,
58
- thread_id TEXT NOT NULL,
59
- run_id TEXT,
60
- provider_session_id TEXT,
61
- status TEXT NOT NULL DEFAULT 'idle',
62
- resume_state TEXT,
63
- workspace_ref TEXT,
64
- pending_approval TEXT,
65
- owner_email TEXT,
66
- org_id TEXT,
67
- created_at ${intType()} NOT NULL,
68
- updated_at ${intType()} NOT NULL,
69
- stopped_at ${intType()}
70
- )
71
- `),
72
- );
53
+ const createSql = `
54
+ CREATE TABLE IF NOT EXISTS agent_harness_sessions (
55
+ id TEXT PRIMARY KEY,
56
+ harness_name TEXT NOT NULL,
57
+ thread_id TEXT NOT NULL,
58
+ run_id TEXT,
59
+ provider_session_id TEXT,
60
+ status TEXT NOT NULL DEFAULT 'idle',
61
+ resume_state TEXT,
62
+ workspace_ref TEXT,
63
+ pending_approval TEXT,
64
+ owner_email TEXT,
65
+ org_id TEXT,
66
+ created_at ${intType()} NOT NULL,
67
+ updated_at ${intType()} NOT NULL,
68
+ stopped_at ${intType()}
69
+ )
70
+ `;
71
+
72
+ if (isPostgres()) {
73
+ // PG-guard: probe information_schema / pg_indexes before issuing DDL to
74
+ // avoid ACCESS EXCLUSIVE lock contention in fresh background-worker processes.
75
+ await ensureTableExists("agent_harness_sessions", createSql);
76
+ for (const col of [
77
+ "run_id",
78
+ "provider_session_id",
79
+ "resume_state",
80
+ "workspace_ref",
81
+ "pending_approval",
82
+ "owner_email",
83
+ "org_id",
84
+ ] as const) {
85
+ await ensureColumnExists(
86
+ "agent_harness_sessions",
87
+ col,
88
+ `ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS ${col} TEXT`,
89
+ );
90
+ }
91
+ await ensureColumnExists(
92
+ "agent_harness_sessions",
93
+ "stopped_at",
94
+ `ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS stopped_at ${intType()}`,
95
+ );
96
+ await ensureIndexExists(
97
+ "idx_agent_harness_sessions_thread",
98
+ `CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_thread ON agent_harness_sessions(thread_id, updated_at)`,
99
+ );
100
+ await ensureIndexExists(
101
+ "idx_agent_harness_sessions_status",
102
+ `CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_status ON agent_harness_sessions(status, updated_at)`,
103
+ );
104
+ await ensureIndexExists(
105
+ "idx_agent_harness_sessions_owner",
106
+ `CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_owner ON agent_harness_sessions(owner_email, updated_at)`,
107
+ );
108
+ return;
109
+ }
110
+
111
+ // SQLite (local dev): no lock problem — keep the original behaviour.
112
+ await client.execute(createSql);
73
113
  for (const col of [
74
114
  "run_id",
75
115
  "provider_session_id",
@@ -80,47 +120,41 @@ export async function ensureAgentHarnessSessionTables(): Promise<void> {
80
120
  "org_id",
81
121
  ] as const) {
82
122
  try {
83
- if (isPostgres()) {
84
- await client.execute(
85
- `ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS ${col} TEXT`,
86
- );
87
- } else {
88
- await client.execute(
89
- `ALTER TABLE agent_harness_sessions ADD COLUMN ${col} TEXT`,
90
- );
91
- }
123
+ await client.execute(
124
+ `ALTER TABLE agent_harness_sessions ADD COLUMN ${col} TEXT`,
125
+ );
92
126
  } catch {
93
127
  // Column already exists.
94
128
  }
95
129
  }
96
130
  try {
97
- if (isPostgres()) {
98
- await client.execute(
99
- `ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS stopped_at ${intType()}`,
100
- );
101
- } else {
102
- await client.execute(
103
- `ALTER TABLE agent_harness_sessions ADD COLUMN stopped_at ${intType()}`,
104
- );
105
- }
131
+ await client.execute(
132
+ `ALTER TABLE agent_harness_sessions ADD COLUMN stopped_at ${intType()}`,
133
+ );
106
134
  } catch {
107
135
  // Column already exists.
108
136
  }
109
- await retryOnDdlRace(() =>
110
- client.execute(
137
+ try {
138
+ await client.execute(
111
139
  `CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_thread ON agent_harness_sessions(thread_id, updated_at)`,
112
- ),
113
- );
114
- await retryOnDdlRace(() =>
115
- client.execute(
140
+ );
141
+ } catch {
142
+ // Index already exists.
143
+ }
144
+ try {
145
+ await client.execute(
116
146
  `CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_status ON agent_harness_sessions(status, updated_at)`,
117
- ),
118
- );
119
- await retryOnDdlRace(() =>
120
- client.execute(
147
+ );
148
+ } catch {
149
+ // Index already exists.
150
+ }
151
+ try {
152
+ await client.execute(
121
153
  `CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_owner ON agent_harness_sessions(owner_email, updated_at)`,
122
- ),
123
- );
154
+ );
155
+ } catch {
156
+ // Index already exists.
157
+ }
124
158
  })().catch((err) => {
125
159
  initPromise = undefined;
126
160
  throw err;
@@ -12,8 +12,8 @@
12
12
  * runtime even before the migration plugin is registered.
13
13
  */
14
14
 
15
- import { getDbExec } from "../../db/client.js";
16
- import { isPostgres } from "../../db/client.js";
15
+ import { getDbExec, isPostgres } from "../../db/client.js";
16
+ import { ensureTableExists, ensureIndexExists } from "../../db/ddl-guard.js";
17
17
  import type {
18
18
  ObservationalMemoryEntry,
19
19
  ObservationalMemoryOwner,
@@ -27,8 +27,7 @@ async function ensureTable(): Promise<void> {
27
27
  tableReady = (async () => {
28
28
  const client = getDbExec();
29
29
  const intType = isPostgres() ? "BIGINT" : "INTEGER";
30
- await client.execute(
31
- `CREATE TABLE IF NOT EXISTS observational_memory (
30
+ const createSql = `CREATE TABLE IF NOT EXISTS observational_memory (
32
31
  id TEXT PRIMARY KEY,
33
32
  thread_id TEXT NOT NULL,
34
33
  tier TEXT NOT NULL,
@@ -42,16 +41,43 @@ async function ensureTable(): Promise<void> {
42
41
  owner_email TEXT NOT NULL,
43
42
  org_id TEXT,
44
43
  visibility TEXT NOT NULL DEFAULT 'private'
45
- )`,
46
- );
47
- await client.execute(
48
- `CREATE INDEX IF NOT EXISTS observational_memory_thread_tier_idx
49
- ON observational_memory(thread_id, tier, created_at)`,
50
- );
51
- await client.execute(
52
- `CREATE INDEX IF NOT EXISTS observational_memory_thread_owner_idx
53
- ON observational_memory(thread_id, owner_email)`,
54
- );
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("observational_memory", createSql);
50
+ await ensureIndexExists(
51
+ "observational_memory_thread_tier_idx",
52
+ `CREATE INDEX IF NOT EXISTS observational_memory_thread_tier_idx
53
+ ON observational_memory(thread_id, tier, created_at)`,
54
+ );
55
+ await ensureIndexExists(
56
+ "observational_memory_thread_owner_idx",
57
+ `CREATE INDEX IF NOT EXISTS observational_memory_thread_owner_idx
58
+ ON observational_memory(thread_id, owner_email)`,
59
+ );
60
+ return;
61
+ }
62
+
63
+ // SQLite (local dev): no lock problem — keep the original behaviour.
64
+ await client.execute(createSql);
65
+ try {
66
+ await client.execute(
67
+ `CREATE INDEX IF NOT EXISTS observational_memory_thread_tier_idx
68
+ ON observational_memory(thread_id, tier, created_at)`,
69
+ );
70
+ } catch {
71
+ // Index already exists.
72
+ }
73
+ try {
74
+ await client.execute(
75
+ `CREATE INDEX IF NOT EXISTS observational_memory_thread_owner_idx
76
+ ON observational_memory(thread_id, owner_email)`,
77
+ );
78
+ } catch {
79
+ // Index already exists.
80
+ }
55
81
  })().catch((err) => {
56
82
  // Reset so a transient failure (e.g. SQLITE_BUSY on HMR) can retry.
57
83
  tableReady = null;
@@ -4023,6 +4023,8 @@ export function createProductionAgentHandler(
4023
4023
  // owner/request-context awaits.
4024
4024
  workerStep("db_request_ctx");
4025
4025
 
4026
+ // DIAGNOSTIC-ONLY: bracket attachment upload + text-attachment persistence.
4027
+ workerStep("attach_start");
4026
4028
  // Pre-upload chat attachments (images AND files/PDFs) through the framework
4027
4029
  // file-upload provider (Builder.io by default). The model still sees the
4028
4030
  // base64 multimodal content for the current turn; each uploaded attachment
@@ -4085,9 +4087,13 @@ export function createProductionAgentHandler(
4085
4087
  );
4086
4088
  }
4087
4089
  }
4090
+ // DIAGNOSTIC-ONLY: attachment upload + persistence finished.
4091
+ workerStep("attach_done");
4088
4092
 
4089
4093
  // When a per-request engine override is specified, resolve the API key
4090
4094
  // for that provider instead of the global active engine's provider.
4095
+ // DIAGNOSTIC-ONLY: bracket per-owner API-key resolution (settings/app_secrets reads).
4096
+ workerStep("apikey_start");
4091
4097
  let userApiKey: string | undefined;
4092
4098
  if (requestEngine) {
4093
4099
  const provider = engineToProvider(requestEngine);
@@ -4103,6 +4109,8 @@ export function createProductionAgentHandler(
4103
4109
  } else {
4104
4110
  userApiKey = await getOwnerActiveApiKey(ownerEmail);
4105
4111
  }
4112
+ // DIAGNOSTIC-ONLY: API-key resolution finished.
4113
+ workerStep("apikey_done");
4106
4114
 
4107
4115
  // `options.apiKey` is the value the template constructed the plugin with
4108
4116
  // (e.g. wired from a deployment env var). On a shared hosted deploy this
@@ -4117,6 +4125,9 @@ export function createProductionAgentHandler(
4117
4125
  readDeployCredentialEnv("ANTHROPIC_API_KEY"));
4118
4126
 
4119
4127
  // Resolve engine — per-request engine override takes priority
4128
+ // DIAGNOSTIC-ONLY: bracket engine resolution (Builder credential / app-default
4129
+ // settings reads inside resolveEngine).
4130
+ workerStep("engine_start");
4120
4131
  let engine: AgentEngine;
4121
4132
  try {
4122
4133
  engine = await resolveEngine({
@@ -4131,17 +4142,24 @@ export function createProductionAgentHandler(
4131
4142
  appId: options.appId,
4132
4143
  });
4133
4144
  }
4145
+ // DIAGNOSTIC-ONLY: engine resolution finished.
4146
+ workerStep("engine_done");
4134
4147
 
4135
4148
  // Honor the model the user picked in the settings UI (written via
4136
4149
  // `manage-agent-engine` action="set"), but only when the caller hasn't overridden it for
4137
4150
  // this request or at plugin construction time. Read per-request so a
4138
4151
  // dropdown change in the UI takes effect without a server restart. Skip
4139
4152
  // the DB read entirely when a higher-precedence value is set.
4153
+ // DIAGNOSTIC-ONLY: bracket stored-model resolution (getStoredModelForEngine
4154
+ // settings read).
4155
+ workerStep("model_start");
4140
4156
  const modelCandidate =
4141
4157
  requestModel ??
4142
4158
  configuredModel ??
4143
4159
  (await getStoredModelForEngine(engine, { appId: options.appId })) ??
4144
4160
  engine.defaultModel;
4161
+ // DIAGNOSTIC-ONLY: stored-model resolution finished.
4162
+ workerStep("model_done");
4145
4163
  const model = normalizeModelForEngine(engine, modelCandidate);
4146
4164
  const reasoningEffort = normalizeReasoningEffortForModel(
4147
4165
  model,
@@ -4,6 +4,7 @@
4
4
  * reliable reconnection after page refreshes.
5
5
  */
6
6
  import { getDbExec, intType, isPostgres } from "../db/client.js";
7
+ import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
7
8
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
8
9
  import { captureError } from "../server/capture-error.js";
9
10
  import type { AgentChatEvent } from "./types.js";
@@ -82,7 +83,10 @@ async function ensureRunTables(): Promise<void> {
82
83
  if (!_initPromise) {
83
84
  _initPromise = (async () => {
84
85
  const client = getDbExec();
85
- await client.execute(`
86
+
87
+ // Shared CREATE SQL strings — referenced by both the Postgres and SQLite
88
+ // branches so the column definitions stay in one place.
89
+ const agentRunsCreateSql = `
86
90
  CREATE TABLE IF NOT EXISTS agent_runs (
87
91
  id TEXT PRIMARY KEY,
88
92
  thread_id TEXT NOT NULL,
@@ -98,30 +102,117 @@ async function ensureRunTables(): Promise<void> {
98
102
  dispatch_mode TEXT,
99
103
  diag_stage TEXT
100
104
  )
101
- `);
102
- // Backfill heartbeat_at on older deployments.
103
- try {
104
- if (isPostgres()) {
105
- await client.execute(
106
- `ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS heartbeat_at ${intType()}`,
107
- );
108
- await client.execute(
109
- `ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS abort_reason TEXT`,
110
- );
111
- } else {
112
- await client.execute(
113
- `ALTER TABLE agent_runs ADD COLUMN heartbeat_at ${intType()}`,
105
+ `;
106
+ const agentRunEventsCreateSql = `
107
+ CREATE TABLE IF NOT EXISTS agent_run_events (
108
+ run_id TEXT NOT NULL,
109
+ seq ${intType()} NOT NULL,
110
+ event_data TEXT NOT NULL,
111
+ PRIMARY KEY (run_id, seq)
112
+ )
113
+ `;
114
+ // Tool-call result ledger: persists the outcome of write tool calls that
115
+ // completed AFTER their chunk was abandoned (zombie completions). A
116
+ // resumed continuation can recover the real result by matching
117
+ // thread_id + tool_key (name:stableInputHash) instead of re-executing
118
+ // the side effect. Entries are scoped to the thread and expire with it.
119
+ const agentToolLedgerCreateSql = `
120
+ CREATE TABLE IF NOT EXISTS agent_tool_ledger (
121
+ thread_id TEXT NOT NULL,
122
+ tool_key TEXT NOT NULL,
123
+ result_summary TEXT NOT NULL,
124
+ completed_at ${intType()} NOT NULL,
125
+ PRIMARY KEY (thread_id, tool_key)
126
+ )
127
+ `;
128
+
129
+ if (isPostgres()) {
130
+ // Hot path: in production the tables and all additive columns are
131
+ // virtually always already present. Issuing `CREATE TABLE`/`ALTER TABLE
132
+ // ADD COLUMN` still takes an ACCESS EXCLUSIVE lock — which, in a fresh
133
+ // background-worker process behind a concurrent connection on the shared
134
+ // Neon DB, can block ~indefinitely. So check `information_schema` first
135
+ // (plain reads, no lock) and run DDL ONLY for what is actually missing.
136
+ // The `ensureTableExists` / `ensureColumnExists` wrappers probe →
137
+ // guarded-DDL (bounded `lock_timeout`) → re-probe, and THROW if the
138
+ // schema is still missing after a swallowed lock-timeout so a poisoned
139
+ // init never memoizes success against absent schema (the `_initPromise`
140
+ // rejects and the next call retries).
141
+ await ensureTableExists("agent_runs", agentRunsCreateSql);
142
+ // Additive columns — all listed in the CREATE TABLE above, so on a
143
+ // fresh DB they already exist after the CREATE and these checks are
144
+ // instant short-circuits. On an older deployment that predates a
145
+ // column, the wrapper issues one bounded ALTER.
146
+ //
147
+ // Backfill heartbeat_at on older deployments.
148
+ // heartbeat_at = "the producer process is alive" (bumped on a timer).
149
+ // last_progress_at = "the agent is actually emitting events" (bumped on
150
+ // each emit). The gap between them is the stuck-detector signal.
151
+ for (const [col, colType] of [
152
+ ["heartbeat_at", intType()],
153
+ ["abort_reason", "TEXT"],
154
+ ["last_progress_at", intType()],
155
+ // Backfill turn_id / error_code / error_detail.
156
+ // turn_id = stable identity for one logical assistant turn that may
157
+ // span several continuation runs, so the durable record
158
+ // can be folded across runs instead of dropped per-run.
159
+ // error_code / error_detail = terminal failure classification captured
160
+ // at completion so errored/cut-off runs are queryable for
161
+ // pattern analysis (see listErroredRuns).
162
+ // dispatch_mode marks how a run was started: NULL/"foreground" for the
163
+ // normal synchronous path, "background" for a run dispatched into a
164
+ // Netlify background function. The reaper/claim widen the stale window
165
+ // for background rows so a slow cold-start isn't falsely reaped.
166
+ // diag_stage records the last reached pipeline stage (+ any error) for a
167
+ // background-dispatched run so a silent worker death is DIAGNOSABLE from
168
+ // the client (/runs/active surfaces it) without reading the unreadable
169
+ // Netlify background-function logs. See recordRunDiagnostic.
170
+ ["turn_id", "TEXT"],
171
+ ["error_code", "TEXT"],
172
+ ["error_detail", "TEXT"],
173
+ ["dispatch_mode", "TEXT"],
174
+ ["diag_stage", "TEXT"],
175
+ ] as const) {
176
+ await ensureColumnExists(
177
+ "agent_runs",
178
+ col,
179
+ `ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS ${col} ${colType}`,
114
180
  );
115
181
  }
182
+ await ensureTableExists("agent_run_events", agentRunEventsCreateSql);
183
+ await ensureTableExists("agent_tool_ledger", agentToolLedgerCreateSql);
184
+ // Widen millisecond-timestamp columns that older deployments created as
185
+ // 32-bit `INTEGER`. `insertRun()` writes `Date.now()` into `started_at`
186
+ // on every turn, so an int4 column makes every agent prompt fail on
187
+ // Postgres with "value … is out of range for type integer". No-op once
188
+ // widened (and on fresh DBs that already use BIGINT). See
189
+ // widenIntColumnsToBigInt.
190
+ await widenIntColumnsToBigInt("agent_runs", [
191
+ "started_at",
192
+ "completed_at",
193
+ "heartbeat_at",
194
+ "last_progress_at",
195
+ ]);
196
+ await widenIntColumnsToBigInt("agent_tool_ledger", ["completed_at"]);
197
+ return;
198
+ }
199
+
200
+ // SQLite (local dev): no ACCESS EXCLUSIVE lock problem — keep the
201
+ // original create-then-additive-alter behaviour. SQLite has no
202
+ // `ADD COLUMN IF NOT EXISTS`, so the ALTERs stay wrapped in try/catch.
203
+ await client.execute(agentRunsCreateSql);
204
+ // Backfill heartbeat_at on older deployments.
205
+ try {
206
+ await client.execute(
207
+ `ALTER TABLE agent_runs ADD COLUMN heartbeat_at ${intType()}`,
208
+ );
116
209
  } catch {
117
210
  // Column already exists — ignore
118
211
  }
119
212
  try {
120
- if (!isPostgres()) {
121
- await client.execute(
122
- `ALTER TABLE agent_runs ADD COLUMN abort_reason TEXT`,
123
- );
124
- }
213
+ await client.execute(
214
+ `ALTER TABLE agent_runs ADD COLUMN abort_reason TEXT`,
215
+ );
125
216
  } catch {
126
217
  // Column already exists — ignore
127
218
  }
@@ -130,15 +221,9 @@ async function ensureRunTables(): Promise<void> {
130
221
  // last_progress_at = "the agent is actually emitting events" (bumped on
131
222
  // each emit). The gap between them is the stuck-detector signal.
132
223
  try {
133
- if (isPostgres()) {
134
- await client.execute(
135
- `ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS last_progress_at ${intType()}`,
136
- );
137
- } else {
138
- await client.execute(
139
- `ALTER TABLE agent_runs ADD COLUMN last_progress_at ${intType()}`,
140
- );
141
- }
224
+ await client.execute(
225
+ `ALTER TABLE agent_runs ADD COLUMN last_progress_at ${intType()}`,
226
+ );
142
227
  } catch {
143
228
  // Column already exists — ignore
144
229
  }
@@ -165,41 +250,13 @@ async function ensureRunTables(): Promise<void> {
165
250
  "diag_stage",
166
251
  ] as const) {
167
252
  try {
168
- if (isPostgres()) {
169
- await client.execute(
170
- `ALTER TABLE agent_runs ADD COLUMN IF NOT EXISTS ${col} TEXT`,
171
- );
172
- } else {
173
- await client.execute(
174
- `ALTER TABLE agent_runs ADD COLUMN ${col} TEXT`,
175
- );
176
- }
253
+ await client.execute(`ALTER TABLE agent_runs ADD COLUMN ${col} TEXT`);
177
254
  } catch {
178
255
  // Column already exists — ignore
179
256
  }
180
257
  }
181
- await client.execute(`
182
- CREATE TABLE IF NOT EXISTS agent_run_events (
183
- run_id TEXT NOT NULL,
184
- seq ${intType()} NOT NULL,
185
- event_data TEXT NOT NULL,
186
- PRIMARY KEY (run_id, seq)
187
- )
188
- `);
189
- // Tool-call result ledger: persists the outcome of write tool calls that
190
- // completed AFTER their chunk was abandoned (zombie completions). A
191
- // resumed continuation can recover the real result by matching
192
- // thread_id + tool_key (name:stableInputHash) instead of re-executing
193
- // the side effect. Entries are scoped to the thread and expire with it.
194
- await client.execute(`
195
- CREATE TABLE IF NOT EXISTS agent_tool_ledger (
196
- thread_id TEXT NOT NULL,
197
- tool_key TEXT NOT NULL,
198
- result_summary TEXT NOT NULL,
199
- completed_at ${intType()} NOT NULL,
200
- PRIMARY KEY (thread_id, tool_key)
201
- )
202
- `);
258
+ await client.execute(agentRunEventsCreateSql);
259
+ await client.execute(agentToolLedgerCreateSql);
203
260
  // Widen millisecond-timestamp columns that older deployments created as
204
261
  // 32-bit `INTEGER`. `insertRun()` writes `Date.now()` into `started_at`
205
262
  // on every turn, so an int4 column makes every agent prompt fail on