spine-framework 0.4.3 → 0.5.0-rc.4

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 (320) hide show
  1. package/.env.example +13 -3
  2. package/.framework/cli/commands/bootstrap.ts +392 -0
  3. package/.framework/cli/commands/dev.ts +91 -218
  4. package/.framework/cli/commands/doctor.ts +132 -26
  5. package/.framework/cli/commands/generate.ts +38 -298
  6. package/.framework/cli/commands/init.ts +20 -1
  7. package/.framework/cli/commands/migrate.ts +36 -30
  8. package/.framework/cli/commands/migrations.ts +16 -56
  9. package/.framework/cli/commands/upgrade.ts +35 -0
  10. package/.framework/cli/commands/validate-schema.ts +147 -0
  11. package/.framework/cli/context.ts +223 -110
  12. package/.framework/cli/index.ts +6 -0
  13. package/.framework/cli/lib/project-files.ts +241 -1
  14. package/.framework/functions/_shared/agent-runner.ts +1000 -81
  15. package/.framework/functions/_shared/cil.ts +2 -1
  16. package/.framework/functions/_shared/db.ts +3 -6
  17. package/.framework/functions/_shared/hash-api-key.ts +183 -79
  18. package/.framework/functions/_shared/kb.ts +61 -7
  19. package/.framework/functions/_shared/logging.ts +562 -224
  20. package/.framework/functions/_shared/message-sequence.ts +53 -0
  21. package/.framework/functions/_shared/middleware.ts +47 -44
  22. package/.framework/functions/_shared/pipeline-runner.ts +150 -37
  23. package/.framework/functions/_shared/principal.ts +9 -6
  24. package/.framework/functions/_shared/trigger-engine.ts +149 -40
  25. package/.framework/functions/admin-data.ts +48 -31
  26. package/.framework/functions/ai-agents.ts +103 -33
  27. package/.framework/functions/api-keys.ts +301 -122
  28. package/.framework/functions/apps.ts +43 -26
  29. package/.framework/functions/cil-ingest.ts +48 -15
  30. package/.framework/functions/debug-auth.ts +14 -8
  31. package/.framework/functions/deliveries.ts +39 -12
  32. package/.framework/functions/embeddings.ts +145 -76
  33. package/.framework/functions/integration-outbound.ts +67 -27
  34. package/.framework/functions/integrations.ts +254 -127
  35. package/.framework/functions/logs.ts +273 -133
  36. package/.framework/functions/observability.ts +277 -147
  37. package/.framework/functions/pipeline-executions.ts +1 -1
  38. package/.framework/functions/pipeline-runner-background.ts +120 -0
  39. package/.framework/functions/pipelines.ts +14 -9
  40. package/.framework/functions/prompt-configs.ts +7 -4
  41. package/.framework/functions/roles.ts +8 -5
  42. package/.framework/functions/security.ts +30 -17
  43. package/.framework/functions/system-cron.ts +250 -128
  44. package/.framework/functions/tests.ts +7 -6
  45. package/.framework/functions/timers.ts +310 -107
  46. package/.framework/functions/trigger-scheduler.ts +65 -5
  47. package/.framework/functions/triggers.ts +28 -13
  48. package/.framework/functions/types.ts +13 -10
  49. package/.framework/migrations/064_episode_leases.sql +196 -0
  50. package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
  51. package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
  52. package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
  53. package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
  54. package/.framework/migrations/069_log_stats_window.sql +54 -0
  55. package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
  56. package/.framework/migrations/071_timer_interval_keys.sql +96 -0
  57. package/.framework/migrations/072_slug_autofill.sql +77 -0
  58. package/.framework/migrations/073_seed_member_roles.sql +26 -0
  59. package/.framework/schemas/spine.config.schema.json +62 -0
  60. package/.framework/src/App.tsx +3 -1
  61. package/.framework/src/apps/admin/index.tsx +1141 -775
  62. package/.framework/src/components/ErrorBoundary.tsx +136 -0
  63. package/.framework/src/components/admin/AdminListPage.tsx +81 -73
  64. package/.framework/src/components/admin/AdminSidebar.tsx +92 -81
  65. package/.framework/src/components/admin/EntitySelect.tsx +181 -0
  66. package/.framework/src/components/admin/PageInstructions.tsx +86 -127
  67. package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
  68. package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
  69. package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
  70. package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
  71. package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
  72. package/.framework/src/components/layout/Layout.tsx +4 -2
  73. package/.framework/src/components/layout/Sidebar.tsx +304 -179
  74. package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
  75. package/.framework/src/components/runtime/DataHeader.tsx +96 -48
  76. package/.framework/src/components/runtime/DataListPage.tsx +3 -26
  77. package/.framework/src/components/runtime/DataStats.tsx +10 -10
  78. package/.framework/src/components/runtime/DataTable.tsx +12 -10
  79. package/.framework/src/components/ui/sheet.tsx +24 -26
  80. package/.framework/src/components/ui/textarea.tsx +74 -48
  81. package/.framework/src/hooks/useBranding.ts +47 -0
  82. package/.framework/src/hooks/useEntityList.ts +12 -9
  83. package/.framework/src/hooks/useListSchema.ts +108 -58
  84. package/.framework/src/instructions/README.md +57 -0
  85. package/.framework/src/instructions/ai-agents.md +69 -0
  86. package/.framework/src/instructions/alerts.md +80 -0
  87. package/.framework/src/instructions/api-keys.md +87 -0
  88. package/.framework/src/instructions/apps.md +77 -0
  89. package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
  90. package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
  91. package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
  92. package/.framework/src/instructions/concepts/automation.md +186 -0
  93. package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
  94. package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
  95. package/.framework/src/instructions/concepts/design-schema.md +152 -0
  96. package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
  97. package/.framework/src/instructions/concepts/observability.md +116 -0
  98. package/.framework/src/instructions/concepts/permissions.md +150 -0
  99. package/.framework/src/instructions/concepts/runtime-records.md +111 -0
  100. package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
  101. package/.framework/src/instructions/concepts/spine-way.md +172 -0
  102. package/.framework/src/instructions/deliveries.md +59 -0
  103. package/.framework/src/instructions/guide/00-orientation.md +106 -0
  104. package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
  105. package/.framework/src/instructions/guide/02-project-map.md +147 -0
  106. package/.framework/src/instructions/guide/03-domain-model.md +223 -0
  107. package/.framework/src/instructions/guide/04-first-app.md +227 -0
  108. package/.framework/src/instructions/guide/05-glossary.md +183 -0
  109. package/.framework/src/instructions/integrations.md +96 -0
  110. package/.framework/src/instructions/internal/cil-markers.md +83 -0
  111. package/.framework/src/instructions/invites.md +60 -0
  112. package/.framework/src/instructions/kb.md +80 -0
  113. package/.framework/src/instructions/logs.md +77 -0
  114. package/.framework/src/instructions/observability.md +61 -0
  115. package/.framework/src/instructions/operations/deployment.md +115 -0
  116. package/.framework/src/instructions/operations/migrations.md +125 -0
  117. package/.framework/src/instructions/operations/security-checklist.md +148 -0
  118. package/.framework/src/instructions/operations/troubleshooting.md +170 -0
  119. package/.framework/src/instructions/operations/upgrade.md +116 -0
  120. package/.framework/src/instructions/pipeline-executions.md +62 -0
  121. package/.framework/src/instructions/pipelines.md +78 -0
  122. package/.framework/src/instructions/prompts.md +95 -0
  123. package/.framework/src/instructions/reference/api/admin-data.md +162 -0
  124. package/.framework/src/instructions/reference/api/auth.md +113 -0
  125. package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
  126. package/.framework/src/instructions/reference/app-manifest.md +150 -0
  127. package/.framework/src/instructions/reference/cli.md +124 -0
  128. package/.framework/src/instructions/reference/database.md +132 -0
  129. package/.framework/src/instructions/reference/design-schema.md +280 -0
  130. package/.framework/src/instructions/reference/env-vars.md +133 -0
  131. package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
  132. package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
  133. package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
  134. package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
  135. package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
  136. package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
  137. package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
  138. package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
  139. package/.framework/src/instructions/reference/functions/apps.md +140 -0
  140. package/.framework/src/instructions/reference/functions/auth.md +99 -0
  141. package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
  142. package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
  143. package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
  144. package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
  145. package/.framework/src/instructions/reference/functions/index.md +103 -0
  146. package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
  147. package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
  148. package/.framework/src/instructions/reference/functions/integrations.md +130 -0
  149. package/.framework/src/instructions/reference/functions/invites.md +182 -0
  150. package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
  151. package/.framework/src/instructions/reference/functions/kb.md +166 -0
  152. package/.framework/src/instructions/reference/functions/logs.md +141 -0
  153. package/.framework/src/instructions/reference/functions/observability.md +105 -0
  154. package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
  155. package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
  156. package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
  157. package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
  158. package/.framework/src/instructions/reference/functions/roles.md +121 -0
  159. package/.framework/src/instructions/reference/functions/security.md +140 -0
  160. package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
  161. package/.framework/src/instructions/reference/functions/system.md +106 -0
  162. package/.framework/src/instructions/reference/functions/tests.md +97 -0
  163. package/.framework/src/instructions/reference/functions/timers.md +117 -0
  164. package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
  165. package/.framework/src/instructions/reference/functions/triggers.md +140 -0
  166. package/.framework/src/instructions/reference/functions/types.md +146 -0
  167. package/.framework/src/instructions/reference/seeds.md +126 -0
  168. package/.framework/src/instructions/reference/shared-modules.md +127 -0
  169. package/.framework/src/instructions/reference/spine-config.md +114 -0
  170. package/.framework/src/instructions/roles.md +80 -0
  171. package/.framework/src/instructions/runtime-detail.md +70 -0
  172. package/.framework/src/instructions/runtime-list.md +77 -0
  173. package/.framework/src/instructions/security.md +67 -0
  174. package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
  175. package/.framework/src/instructions/tasks/add-a-role.md +96 -0
  176. package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
  177. package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
  178. package/.framework/src/instructions/tasks/add-a-type.md +151 -0
  179. package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
  180. package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
  181. package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
  182. package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
  183. package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
  184. package/.framework/src/instructions/tasks/test-your-code.md +108 -0
  185. package/.framework/src/instructions/tasks/write-a-function.md +132 -0
  186. package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
  187. package/.framework/src/instructions/testing.md +59 -0
  188. package/.framework/src/instructions/timers.md +82 -0
  189. package/.framework/src/instructions/triggers.md +69 -0
  190. package/.framework/src/instructions/types.md +93 -0
  191. package/.framework/src/lib/branding.ts +128 -0
  192. package/.framework/src/pages/NoAppPage.tsx +3 -1
  193. package/.framework/src/pages/NotFoundPage.tsx +62 -31
  194. package/.framework/src/pages/SetupPage.tsx +5 -2
  195. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
  196. package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
  197. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
  198. package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
  199. package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
  200. package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
  201. package/.framework/src/pages/admin/AppsPage.tsx +104 -66
  202. package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
  203. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
  204. package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
  205. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
  206. package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
  207. package/.framework/src/pages/admin/LogsPage.tsx +144 -76
  208. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
  209. package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
  210. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
  211. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
  212. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
  213. package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
  214. package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
  215. package/.framework/src/pages/admin/RolesPage.tsx +86 -43
  216. package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
  217. package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
  218. package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
  219. package/.framework/src/pages/admin/TimersPage.tsx +90 -62
  220. package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
  221. package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
  222. package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
  223. package/.framework/src/pages/admin/TypesPage.tsx +90 -48
  224. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
  225. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
  226. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
  227. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
  228. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
  229. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
  230. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
  231. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
  232. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
  233. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
  234. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
  235. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
  236. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
  237. package/.framework/src/pages/auth/AuthConfirmPage.tsx +3 -1
  238. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
  239. package/.framework/src/pages/auth/LoginPage.tsx +4 -2
  240. package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
  241. package/.framework/templates/agent/AGENTS.md +150 -0
  242. package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
  243. package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
  244. package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
  245. package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
  246. package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
  247. package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
  248. package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
  249. package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
  250. package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
  251. package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
  252. package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
  253. package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
  254. package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
  255. package/.framework/templates/project.json +5 -1
  256. package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
  257. package/.framework/tests/unit/agent-files.test.ts +355 -0
  258. package/.framework/tests/unit/agent-runner.test.ts +496 -47
  259. package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
  260. package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
  261. package/.framework/tests/unit/bootstrap.test.ts +242 -0
  262. package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
  263. package/.framework/tests/unit/debug-auth.test.ts +38 -0
  264. package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
  265. package/.framework/tests/unit/episode-lease.test.ts +695 -0
  266. package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
  267. package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
  268. package/.framework/tests/unit/kb-ingest-lease.test.ts +19 -0
  269. package/.framework/tests/unit/logging-app-id.test.ts +75 -0
  270. package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
  271. package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
  272. package/.framework/tests/unit/principal-cache.test.ts +15 -0
  273. package/.framework/tests/unit/system-context-denial.test.ts +145 -0
  274. package/.framework/tests/unit/system-cron.test.ts +240 -0
  275. package/.framework/tests/unit/timers.test.ts +159 -0
  276. package/.framework/tests/unit/trigger-engine.test.ts +278 -91
  277. package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
  278. package/README.md +125 -166
  279. package/config/tsconfig.cli.json +2 -2
  280. package/netlify.toml +5 -0
  281. package/package.json +15 -4
  282. package/templates/project/package-lock.json +2 -2
  283. package/.framework/docs/admin-routes.md +0 -76
  284. package/.framework/docs/api-docs-progress.md +0 -38
  285. package/.framework/docs/api-governance.md +0 -146
  286. package/.framework/docs/api-testing-results.md +0 -212
  287. package/.framework/docs/assembly-launch-guide.md +0 -138
  288. package/.framework/docs/audit-results.md +0 -590
  289. package/.framework/docs/db-api-inventory.md +0 -95
  290. package/.framework/docs/gap-remediation-backlog.md +0 -103
  291. package/.framework/docs/ui-adoption-verification.md +0 -111
  292. package/.framework/docs/ui-api-coverage.md +0 -84
  293. package/.framework/docs/v2-compatibility-audit.md +0 -228
  294. package/.framework/mcp/index.ts +0 -133
  295. package/.framework/mcp/load-env.ts +0 -60
  296. package/.framework/mcp/shared/app-manifest.ts +0 -275
  297. package/.framework/mcp/shared/db.ts +0 -477
  298. package/.framework/mcp/shared/kb.ts +0 -118
  299. package/.framework/mcp/tools/architect.ts +0 -431
  300. package/.framework/mcp/tools/cil/context.ts +0 -345
  301. package/.framework/mcp/tools/cil/graph.ts +0 -242
  302. package/.framework/mcp/tools/cil/ingest.ts +0 -1217
  303. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
  304. package/.framework/mcp/tools/cil/pipeline.ts +0 -767
  305. package/.framework/mcp/tools/cil/project.ts +0 -791
  306. package/.framework/mcp/tools/cil/retrieval.ts +0 -336
  307. package/.framework/mcp/tools/cil/search-core.ts +0 -454
  308. package/.framework/mcp/tools/cil/search.ts +0 -97
  309. package/.framework/mcp/tools/cil/suggest.ts +0 -186
  310. package/.framework/mcp/tools/cil/validate.ts +0 -1369
  311. package/.framework/mcp/tools/kb/delete.ts +0 -139
  312. package/.framework/mcp/tools/kb/index.ts +0 -52
  313. package/.framework/mcp/tools/kb/ingest.ts +0 -84
  314. package/.framework/mcp/tools/kb/list-types.ts +0 -102
  315. package/.framework/mcp/tools/kb/publish.ts +0 -100
  316. package/.framework/mcp/tools/kb/reindex.ts +0 -86
  317. package/.framework/mcp/tools/kb/revoke.ts +0 -85
  318. package/.framework/mcp/tools/kb/search.ts +0 -101
  319. package/.framework/mcp/tools/kb/upsert.ts +0 -125
  320. package/.framework/mcp/tools/observability.ts +0 -510
@@ -248,7 +248,8 @@ export const getExecutions = createHandler(async (ctx, body) => {
248
248
  throw new Error('Account context required')
249
249
  }
250
250
 
251
- const { data, error: err } = await ctx.db
251
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
252
+ const { data, error: err } = await db
252
253
  .from('trigger_executions')
253
254
  .select('*')
254
255
  .eq('trigger_id', trigger_id)
@@ -275,7 +276,7 @@ export const getExecutions = createHandler(async (ctx, body) => {
275
276
  // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
276
277
  // CIL[DOC-STABILITY stability="stable"]
277
278
  export const create = createHandler(async (ctx, body) => {
278
- const { app_id, name, description, event_type, pipeline_id, config, metadata } = body
279
+ const { app_id, name, description, event_type, trigger_type, pipeline_id, config, metadata } = body
279
280
 
280
281
  if (!name || !event_type || !pipeline_id) {
281
282
  throw new Error('name, event_type, and pipeline_id are required')
@@ -305,7 +306,8 @@ export const create = createHandler(async (ctx, body) => {
305
306
  }
306
307
  }
307
308
 
308
- const { data, error: err } = await ctx.db
309
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
310
+ const { data, error: err } = await db
309
311
  .from('triggers')
310
312
  .insert({
311
313
  app_id: app_id || null,
@@ -316,7 +318,8 @@ export const create = createHandler(async (ctx, body) => {
316
318
  pipeline_id,
317
319
  config: config || {},
318
320
  metadata: metadata || {},
319
- created_by: ctx.principal.id
321
+ trigger_type: trigger_type || 'event',
322
+ created_by: ctx.principal.type === 'human' ? ctx.principal.id : null
320
323
  })
321
324
  .select()
322
325
  .single()
@@ -353,13 +356,14 @@ export const update = createHandler(async (ctx, body) => {
353
356
  throw new Error('Trigger ID is required')
354
357
  }
355
358
 
356
- const allowed = ['name', 'description', 'config', 'pipeline_id', 'metadata', 'is_active', 'event_type', 'app_id']
359
+ const allowed = ['name', 'description', 'config', 'pipeline_id', 'metadata', 'is_active', 'event_type', 'trigger_type', 'app_id']
357
360
  const updateData: Record<string, any> = { updated_at: new Date().toISOString() }
358
361
  for (const key of allowed) {
359
362
  if (updates[key] !== undefined) updateData[key] = updates[key]
360
363
  }
361
364
 
362
- const { data, error: err } = await ctx.db
365
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
366
+ const { data, error: err } = await db
363
367
  .from('triggers')
364
368
  .update(updateData)
365
369
  .eq('id', id)
@@ -397,7 +401,8 @@ export const remove = createHandler(async (ctx, body) => {
397
401
  throw new Error('Trigger ID is required')
398
402
  }
399
403
 
400
- const { data: existing } = await ctx.db
404
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
405
+ const { data: existing } = await db
401
406
  .from('triggers')
402
407
  .select('name, event_type')
403
408
  .eq('id', id)
@@ -407,9 +412,9 @@ export const remove = createHandler(async (ctx, body) => {
407
412
  throw new Error('Trigger not found')
408
413
  }
409
414
 
410
- const { error: err } = await ctx.db
415
+ const { error: err } = await db
411
416
  .from('triggers')
412
- .update({
417
+ .update({
413
418
  is_active: false,
414
419
  updated_at: new Date().toISOString()
415
420
  })
@@ -446,7 +451,8 @@ export const toggle = createHandler(async (ctx, body) => {
446
451
  throw new Error('Trigger ID is required')
447
452
  }
448
453
 
449
- const { data: existing } = await ctx.db
454
+ const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
455
+ const { data: existing } = await db
450
456
  .from('triggers')
451
457
  .select('is_active, name')
452
458
  .eq('id', id)
@@ -458,9 +464,9 @@ export const toggle = createHandler(async (ctx, body) => {
458
464
 
459
465
  const newStatus = !existing.is_active
460
466
 
461
- const { data, error: err } = await ctx.db
467
+ const { data, error: err } = await db
462
468
  .from('triggers')
463
- .update({
469
+ .update({
464
470
  is_active: newStatus,
465
471
  updated_at: new Date().toISOString()
466
472
  })
@@ -497,7 +503,16 @@ export const toggle = createHandler(async (ctx, body) => {
497
503
  // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
498
504
  // CIL[DOC-STABILITY stability="stable"]
499
505
  export const handler = createHandler(async (ctx, body) => {
500
- const { action } = ctx.query || {}
506
+ const { action, id } = ctx.query || {}
507
+ const method = ctx.query?.method || 'GET'
508
+
509
+ if (!action) {
510
+ if (method === 'GET') return id ? await get(ctx, body) : await list(ctx, body)
511
+ if (method === 'POST') return await create(ctx, body)
512
+ if (method === 'PATCH') return await update(ctx, body)
513
+ if (method === 'DELETE') return await remove(ctx, body)
514
+ throw new Error(`Unsupported method: ${method}`)
515
+ }
501
516
 
502
517
  switch (action) {
503
518
  case 'list':
@@ -42,22 +42,27 @@ const permissions = PermissionEngine as any
42
42
  // CIL[DOC-MACRO purpose="List active type definitions with optional filters and pagination"]
43
43
  // CIL[DOC-RESPONSIBILITY responsibility="Query the public.types table and sanitize records based on the caller principal"]
44
44
  // CIL[DOC-CONTEXT module="types function" layer="framework/functions" related_files=".framework/functions/_shared/audit.ts,.framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts,.framework/functions/_shared/principal.ts,.framework/functions/_shared/schema-utils.ts,.framework/functions/types.ts"]
45
- // CIL[DOC-CONTRACT inputs="kind, app_id, ownership, limit, offset, include_schema query parameters" outputs="array of sanitized type rows" errors="Database query errors" side_effects="none"]
46
- // CIL[DOC-MICRO logic_summary="Builds a filtered, paginated Supabase query against the types table and returns sanitized results" notable_decisions="Returns minimal fields for unauthenticated callers; includes schema only when requested"]
45
+ // CIL[DOC-CONTRACT inputs="kind, app_id, ownership, active, limit, offset, include_schema query parameters" outputs="array of sanitized type rows" errors="Database query errors" side_effects="none"]
46
+ // CIL[DOC-MICRO logic_summary="Builds a filtered, paginated Supabase query against the types table and returns sanitized results" notable_decisions="Returns minimal fields for unauthenticated callers; includes schema only when requested; active defaults to true, 'all' lists active and inactive, 'false' lists inactive only"]
47
47
  // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
48
48
  // CIL[DOC-STABILITY stability="stable"]
49
49
  export const list = createHandler(async (ctx, body) => {
50
- const { kind, app_id, ownership, limit = '50', offset = '0', include_schema } = ctx.query || {}
50
+ const { kind, app_id, ownership, active, limit = '50', offset = '0', include_schema } = ctx.query || {}
51
51
 
52
52
  const db = isSystemAdmin(ctx.principal) ? adminDb : ctx.db
53
53
 
54
54
  let query = db
55
55
  .from('types')
56
56
  .select('*')
57
- .eq('is_active', true)
58
57
  .order('kind')
59
58
  .order('name')
60
59
 
60
+ if (active === 'false') {
61
+ query = query.eq('is_active', false)
62
+ } else if (active !== 'all') {
63
+ query = query.eq('is_active', true)
64
+ }
65
+
61
66
  if (kind) {
62
67
  query = query.eq('kind', kind)
63
68
  }
@@ -132,7 +137,6 @@ export const get = createHandler(async (ctx, body) => {
132
137
  .from('types')
133
138
  .select('*')
134
139
  .eq('id', id)
135
- .eq('is_active', true)
136
140
  .single()
137
141
 
138
142
  if (err) throw err
@@ -237,7 +241,6 @@ export const create = createHandler(async (ctx, body) => {
237
241
  app_id = null,
238
242
  design_schema = {},
239
243
  validation_schema = null,
240
- view_config = {},
241
244
  description = null,
242
245
  icon = null,
243
246
  color = null
@@ -272,7 +275,6 @@ export const create = createHandler(async (ctx, body) => {
272
275
  app_id,
273
276
  design_schema,
274
277
  validation_schema: validation,
275
- view_config,
276
278
  description,
277
279
  icon,
278
280
  color,
@@ -325,7 +327,6 @@ export const update = createHandler(async (ctx, body) => {
325
327
  .from('types')
326
328
  .select('*')
327
329
  .eq('id', id)
328
- .eq('is_active', true)
329
330
  .single()
330
331
 
331
332
  if (!existing) {
@@ -340,7 +341,6 @@ export const update = createHandler(async (ctx, body) => {
340
341
  name,
341
342
  slug,
342
343
  design_schema,
343
- view_config,
344
344
  description,
345
345
  icon,
346
346
  color,
@@ -354,7 +354,6 @@ export const update = createHandler(async (ctx, body) => {
354
354
 
355
355
  if (name !== undefined) updates.name = name
356
356
  if (slug !== undefined) updates.slug = slug
357
- if (view_config !== undefined) updates.view_config = view_config
358
357
  if (description !== undefined) updates.description = description
359
358
  if (icon !== undefined) updates.icon = icon
360
359
  if (color !== undefined) updates.color = color
@@ -368,6 +367,10 @@ export const update = createHandler(async (ctx, body) => {
368
367
  updates.validation_schema = generateValidationSchema(design_schema)
369
368
  }
370
369
 
370
+ if (Object.keys(updates).length === 0) {
371
+ return existing
372
+ }
373
+
371
374
  const { data, error } = await adminDb
372
375
  .from('types')
373
376
  .update(updates)
@@ -0,0 +1,196 @@
1
+ -- CIL[BEGIN stable_id="~/.framework/migrations/064_episode_leases.sql:episode_leases:file" type="file" identity="episode_leases" parent="~/.framework/migrations/064_episode_leases.sql"]
2
+ -- CIL[DOC-MACRO purpose="Atomic single-flight lease for agent episodes: episode_leases table plus service-role-only acquire/heartbeat/release RPCs. Replaces the check-then-write driver_id claim in agent-runner so concurrent kicks can never both drive a thread. Also adds record_trigger_fire, the atomic trigger bookkeeping RPC that replaces trigger-engine's rpc-Promise-in-UPDATE bug."]
3
+ -- CIL[DOC-RESPONSIBILITY responsibility="Create public.episode_leases (one row per in-flight episode keyed by thread_id), provide acquire_episode_lease/heartbeat_episode_lease/release_episode_lease as the single atomic writers, and fence writes by lease_token."]
4
+ -- CIL[DOC-CONTEXT module="migrations" layer="database" related_files=".framework/functions/_shared/agent-runner.ts,.framework/functions/ai-agents.ts,.framework/migrations/054_kb_ingest_lease.sql"]
5
+ -- CIL[DOC-CONTRACT inputs="acquire: p_thread_id/p_account_id/p_driver_id/p_ttl_seconds; heartbeat: p_thread_id/p_lease_token/p_ttl_seconds; release: p_thread_id/p_lease_token" outputs="acquire jsonb {outcome: claimed|held, lease_token?, holder_driver_id?}; heartbeat boolean; release boolean" errors="none — all branches return" side_effects="Upserts/updates/deletes episode_leases; agent-runner uses the lease to serialize drivers and fence takeover writes"]
6
+ -- CIL[DOC-MICRO logic_summary="acquire_episode_lease INSERTs ON CONFLICT DO NOTHING; on conflict it SELECTs FOR UPDATE and renews when the caller is the same driver or the lease expired (takeover, fresh token), else returns 'held' with the live holder's driver_id. heartbeat/release are token-gated — a displaced driver's heartbeat fails, which is what fences its pending writes." notable_decisions="Lease is keyed by thread (not episode) because a thread can only ever have one live episode; driver_id is text to carry client-generated session tokens; same-driver re-acquire renews so step-mode drivers persist across HTTP requests; RLS grants service_role FOR ALL and EXECUTE goes to service_role only, matching 049/052/054's service-only convention"]
7
+ -- CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
8
+ -- CIL[DOC-STABILITY stability="stable"]
9
+
10
+ -- ── episode_leases ─────────────────────────────────────────────────
11
+ -- One row per in-flight agent episode (threads.id). Entry paths
12
+ -- (ticket-creation kicks, episode-mode POSTs, step/complete POSTs)
13
+ -- acquire before running; the holder heartbeats per turn and releases
14
+ -- on terminal state. A live lease refuses competing drivers atomically —
15
+ -- this replaces the read-then-write research_state claim that allowed
16
+ -- two drivers to race within the same window.
17
+ CREATE TABLE IF NOT EXISTS public.episode_leases (
18
+ thread_id uuid PRIMARY KEY,
19
+ account_id uuid NOT NULL,
20
+ driver_id text NOT NULL,
21
+ lease_token uuid NOT NULL,
22
+ expires_at timestamptz NOT NULL,
23
+ created_at timestamptz DEFAULT now(),
24
+ updated_at timestamptz DEFAULT now()
25
+ );
26
+
27
+ ALTER TABLE public.episode_leases ENABLE ROW LEVEL SECURITY;
28
+
29
+ -- Service-role only: all reads/writes flow through _shared/agent-runner.ts
30
+ -- (adminDb) and the three RPCs below.
31
+ DROP POLICY IF EXISTS episode_leases_service_role ON public.episode_leases;
32
+ CREATE POLICY episode_leases_service_role ON public.episode_leases
33
+ FOR ALL TO service_role USING (true) WITH CHECK (true);
34
+
35
+ -- ── acquire_episode_lease ──────────────────────────────────────────
36
+ -- Atomic claim. Returns {outcome, lease_token?, holder_driver_id?}:
37
+ -- claimed — caller owns the episode; must heartbeat/release with the token
38
+ -- held — a live driver owns the thread; caller watches or retries later
39
+ -- The same driver re-acquiring renews (new token, fresh TTL) so a client
40
+ -- step loop can hold the lease across HTTP requests.
41
+ CREATE OR REPLACE FUNCTION public.acquire_episode_lease(
42
+ p_thread_id uuid,
43
+ p_account_id uuid,
44
+ p_driver_id text,
45
+ p_ttl_seconds integer DEFAULT 90
46
+ )
47
+ RETURNS jsonb
48
+ LANGUAGE plpgsql
49
+ SECURITY DEFINER
50
+ SET search_path = public
51
+ AS $$
52
+ DECLARE
53
+ v_lease public.episode_leases%ROWTYPE;
54
+ v_new_token uuid := gen_random_uuid();
55
+ BEGIN
56
+ -- Fast path: claim the empty slot. The thread_id PK serializes
57
+ -- concurrent first-writers — the loser's INSERT is a no-op.
58
+ INSERT INTO public.episode_leases (thread_id, account_id, driver_id, lease_token, expires_at)
59
+ VALUES (
60
+ p_thread_id,
61
+ p_account_id,
62
+ p_driver_id,
63
+ v_new_token,
64
+ now() + make_interval(secs => p_ttl_seconds)
65
+ )
66
+ ON CONFLICT (thread_id) DO NOTHING;
67
+
68
+ IF FOUND THEN
69
+ RETURN jsonb_build_object('outcome', 'claimed', 'lease_token', v_new_token);
70
+ END IF;
71
+
72
+ -- Conflict path: lock the row so a concurrent release/takeover cannot
73
+ -- interleave between our read and our verdict.
74
+ SELECT * INTO v_lease
75
+ FROM public.episode_leases
76
+ WHERE thread_id = p_thread_id
77
+ FOR UPDATE;
78
+
79
+ -- A live different driver owns the episode — refuse, surfacing the
80
+ -- holder so the caller can attribute the 'running' status.
81
+ IF v_lease.driver_id <> p_driver_id AND v_lease.expires_at > now() THEN
82
+ RETURN jsonb_build_object(
83
+ 'outcome', 'held',
84
+ 'holder_driver_id', v_lease.driver_id
85
+ );
86
+ END IF;
87
+
88
+ -- Same driver (renew) or expired lease (takeover): fresh token + TTL.
89
+ -- A displaced driver's later heartbeat/release fails the token check,
90
+ -- which is what fences its pending writes.
91
+ UPDATE public.episode_leases SET
92
+ driver_id = p_driver_id,
93
+ lease_token = v_new_token,
94
+ account_id = p_account_id,
95
+ expires_at = now() + make_interval(secs => p_ttl_seconds),
96
+ updated_at = now()
97
+ WHERE thread_id = p_thread_id;
98
+
99
+ RETURN jsonb_build_object('outcome', 'claimed', 'lease_token', v_new_token);
100
+ END;
101
+ $$;
102
+
103
+ REVOKE ALL ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer) FROM PUBLIC;
104
+ REVOKE ALL ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer) FROM anon;
105
+ REVOKE ALL ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer) FROM authenticated;
106
+ GRANT EXECUTE ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer) TO service_role;
107
+
108
+ -- ── heartbeat_episode_lease ────────────────────────────────────────
109
+ -- Token-gated TTL extension. Returns false when the lease_token no
110
+ -- longer matches (an expired-lease takeover displaced this driver) —
111
+ -- the caller must treat that as a fence and stop writing, not as an
112
+ -- error to retry.
113
+ CREATE OR REPLACE FUNCTION public.heartbeat_episode_lease(
114
+ p_thread_id uuid,
115
+ p_lease_token uuid,
116
+ p_ttl_seconds integer DEFAULT 90
117
+ )
118
+ RETURNS boolean
119
+ LANGUAGE plpgsql
120
+ SECURITY DEFINER
121
+ SET search_path = public
122
+ AS $$
123
+ BEGIN
124
+ UPDATE public.episode_leases SET
125
+ expires_at = now() + make_interval(secs => p_ttl_seconds),
126
+ updated_at = now()
127
+ WHERE thread_id = p_thread_id
128
+ AND lease_token = p_lease_token;
129
+
130
+ RETURN FOUND;
131
+ END;
132
+ $$;
133
+
134
+ REVOKE ALL ON FUNCTION public.heartbeat_episode_lease(uuid, uuid, integer) FROM PUBLIC;
135
+ REVOKE ALL ON FUNCTION public.heartbeat_episode_lease(uuid, uuid, integer) FROM anon;
136
+ REVOKE ALL ON FUNCTION public.heartbeat_episode_lease(uuid, uuid, integer) FROM authenticated;
137
+ GRANT EXECUTE ON FUNCTION public.heartbeat_episode_lease(uuid, uuid, integer) TO service_role;
138
+
139
+ -- ── release_episode_lease ──────────────────────────────────────────
140
+ -- Token-gated release on terminal state. Returns false when the token
141
+ -- no longer matches (a takeover superseded this driver) — warn-only,
142
+ -- never an error.
143
+ CREATE OR REPLACE FUNCTION public.release_episode_lease(
144
+ p_thread_id uuid,
145
+ p_lease_token uuid
146
+ )
147
+ RETURNS boolean
148
+ LANGUAGE plpgsql
149
+ SECURITY DEFINER
150
+ SET search_path = public
151
+ AS $$
152
+ BEGIN
153
+ DELETE FROM public.episode_leases
154
+ WHERE thread_id = p_thread_id
155
+ AND lease_token = p_lease_token;
156
+
157
+ RETURN FOUND;
158
+ END;
159
+ $$;
160
+
161
+ REVOKE ALL ON FUNCTION public.release_episode_lease(uuid, uuid) FROM PUBLIC;
162
+ REVOKE ALL ON FUNCTION public.release_episode_lease(uuid, uuid) FROM anon;
163
+ REVOKE ALL ON FUNCTION public.release_episode_lease(uuid, uuid) FROM authenticated;
164
+ GRANT EXECUTE ON FUNCTION public.release_episode_lease(uuid, uuid) TO service_role;
165
+
166
+ -- ── record_trigger_fire ────────────────────────────────────────────
167
+ -- Atomic trigger bookkeeping. trigger-engine previously passed an
168
+ -- adminDb.rpc() Promise as a column value inside an UPDATE payload —
169
+ -- the update failed silently, so trigger_count stayed 0 and
170
+ -- last_triggered stayed null on every fire.
171
+ CREATE OR REPLACE FUNCTION public.record_trigger_fire(
172
+ p_trigger_id uuid
173
+ )
174
+ RETURNS void
175
+ LANGUAGE sql
176
+ SECURITY DEFINER
177
+ SET search_path = public
178
+ AS $$
179
+ UPDATE public.triggers SET
180
+ trigger_count = COALESCE(trigger_count, 0) + 1,
181
+ last_triggered = now()
182
+ WHERE id = p_trigger_id;
183
+ $$;
184
+
185
+ REVOKE ALL ON FUNCTION public.record_trigger_fire(uuid) FROM PUBLIC;
186
+ REVOKE ALL ON FUNCTION public.record_trigger_fire(uuid) FROM anon;
187
+ REVOKE ALL ON FUNCTION public.record_trigger_fire(uuid) FROM authenticated;
188
+ GRANT EXECUTE ON FUNCTION public.record_trigger_fire(uuid) TO service_role;
189
+
190
+ -- CIL[DOC-VECTOR type="context" content="purpose='Atomic single-flight lease for agent episodes keyed by thread_id, plus atomic record_trigger_fire bookkeeping.'; Callers: _shared/agent-runner.ts (kickAgentEpisode, runAgentStep heartbeat, runAgent release), functions/ai-agents.ts (run handler), _shared/trigger-engine.ts (record_trigger_fire); imports: none" status="ready"]
191
+ -- CIL[DOC-VECTOR type="blast_radius" content="Any agent episode entry path now serializes drivers atomically; callers that do not claim proceed unchanged when the RPCs are absent" status="ready"]
192
+ -- CIL[DOC-VECTOR type="configuration" content="Runs after thread/message tables exist; consumed by _shared/agent-runner.ts via adminDb.rpc" status="ready"]
193
+ -- CIL[DOC-VECTOR type="constraint" content="episode_leases and all three RPCs are service-role only; heartbeat and release are fenced by lease_token; same-driver acquire renews; expired leases are taken over with a fresh token" status="ready"]
194
+ -- CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
195
+ -- CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
196
+ -- CIL[END stable_id="~/.framework/migrations/064_episode_leases.sql:episode_leases:file"]
@@ -0,0 +1,20 @@
1
+ -- CIL[BEGIN stable_id="~/.framework/migrations/065_execution_tables_updated_at.sql:execution_tables_updated_at:file" type="file" identity="execution_tables_updated_at" parent="~/.framework/migrations/065_execution_tables_updated_at.sql"]
2
+ -- CIL[DOC-MACRO purpose="Add the missing updated_at column to tables that carry the set_*_updated_at trigger but were created without the column — every UPDATE on them fails with 'record NEW has no field updated_at', which is what wedged pipeline_executions rows in 'running' forever."]
3
+ -- CIL[DOC-RESPONSIBILITY responsibility="Backfill updated_at on pipeline_executions (required for terminal bookkeeping writes) and on logs/observability (defensive — same broken trigger, though those tables are append-only today)."]
4
+ -- CIL[DOC-CONTEXT module="migrations" layer="database" related_files=".framework/migrations/001_core_schema.sql,.framework/functions/_shared/pipeline-runner.ts"]
5
+ -- CIL[DOC-CONTRACT inputs="none" outputs="three tables gain updated_at timestamptz DEFAULT now()" errors="none — idempotent ADD COLUMN IF NOT EXISTS" side_effects="Enables previously-failing UPDATEs on pipeline_executions; trigger set_*_updated_at now has a column to stamp"]
6
+ -- CIL[DOC-MICRO logic_summary="001_core_schema attached updated_at_column() triggers to tables whose CREATE TABLE omitted updated_at; the trigger raises on every UPDATE, so pipeline-runner's terminal status writes silently failed (unchecked .update()). Adding the column makes the trigger a no-op stamp instead of an error." notable_decisions="Column added to all three affected tables for uniformity rather than dropping the triggers on append-only logs/observability — keeps the bookkeeping convention consistent if either table ever becomes updateable."]
7
+ -- CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
8
+ -- CIL[DOC-STABILITY stability="stable"]
9
+
10
+ ALTER TABLE public.pipeline_executions ADD COLUMN IF NOT EXISTS updated_at timestamptz DEFAULT now();
11
+ ALTER TABLE public.logs ADD COLUMN IF NOT EXISTS updated_at timestamptz DEFAULT now();
12
+ ALTER TABLE public.observability ADD COLUMN IF NOT EXISTS updated_at timestamptz DEFAULT now();
13
+
14
+ -- CIL[DOC-VECTOR type="context" content="purpose='Fix broken updated_at triggers on execution/log tables.'; Callers: _shared/pipeline-runner.ts terminal bookkeeping; imports: none" status="ready"]
15
+ -- CIL[DOC-VECTOR type="blast_radius" content="pipeline_executions UPDATEs begin succeeding immediately; rows stuck 'running' from the silent failure remain and must be repaired separately" status="ready"]
16
+ -- CIL[DOC-VECTOR type="configuration" content="Applies to any deployment created from 001_core_schema before this fix" status="ready"]
17
+ -- CIL[DOC-VECTOR type="constraint" content="Idempotent; safe to re-run; does not repair historical stuck rows — that is a separate bookkeeping UPDATE" status="ready"]
18
+ -- CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
19
+ -- CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
20
+ -- CIL[END stable_id="~/.framework/migrations/065_execution_tables_updated_at.sql:execution_tables_updated_at:file"]
@@ -0,0 +1,148 @@
1
+ -- CIL[BEGIN stable_id="~/.framework/migrations/066_episode_steal_thread_merge.sql:episode_steal_thread_merge:file" type="file" identity="episode_steal_thread_merge" parent="~/.framework/migrations/066_episode_steal_thread_merge.sql"]
2
+ -- CIL[DOC-MACRO purpose="Episode-lease steal + fenced thread-data merge: request-bound drivers can immediately take over a thread whose lease is held by a 'detached:'-prefixed background driver, and threads.data writers get an atomic shallow-merge RPC with an optional lease-token fence so terminal research_state checkpoints cannot be clobbered by stale read-modify-write blobs."]
3
+ -- CIL[DOC-RESPONSIBILITY responsibility="Extend acquire_episode_lease with p_steal_detached so live 'detached:' lease holders yield instantly to request-bound drivers, and add merge_thread_data as the single atomic writer for threads.data patches."]
4
+ -- CIL[DOC-CONTEXT module="migrations" layer="database" related_files=".framework/functions/_shared/agent-runner.ts,.framework/functions/ai-agents.ts,.framework/migrations/064_episode_leases.sql"]
5
+ -- CIL[DOC-CONTRACT inputs="acquire: +p_steal_detached boolean; merge_thread_data: p_thread_id/p_patch/p_lease_token" outputs="acquire jsonb {outcome: claimed|held, lease_token?, holder_driver_id?}; merge_thread_data boolean" errors="none — all branches return" side_effects="Updates episode_leases / threads"]
6
+ -- CIL[DOC-MICRO logic_summary="acquire_episode_lease keeps the 064 fast-path INSERT plus FOR UPDATE conflict path; the 'held' verdict is skipped when p_steal_detached and the holder is 'detached:%' — detached background drivers exist to yield. merge_thread_data does data = coalesce(data,'{}'::jsonb) || p_patch with WHERE-fencing: when p_lease_token is provided the write only lands if no differently-tokened lease holds the thread, so a displaced driver's stale checkpoint is dropped." notable_decisions="Detached steal is safer than client-side driver_id adoption: the server decides who yields, and live (non-detached) holders are still never preempted — only TTL expiry displaces them; jsonb || is a shallow merge so siblings written concurrently (escalation_status, research_state) no longer clobber each other; the lease-token fence makes a loser's terminal write a silent no-op rather than a resurrected mid-episode state"]
7
+ -- CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
8
+ -- CIL[DOC-STABILITY stability="stable"]
9
+
10
+ -- ── acquire_episode_lease: detached-holder steal ────────────────────
11
+ -- 'detached:' drivers are spawned background work (ticket-creation
12
+ -- kicks, trigger-fired episodes) — they exist so an episode survives a
13
+ -- dead client, not to block one. On serverless platforms detached work
14
+ -- is CPU-throttled after the response flush (observed ~37s dead window
15
+ -- before its first persisted message), so a request-bound driver must
16
+ -- be able to take over immediately instead of watching a stalled
17
+ -- driver or waiting out the TTL. Live non-detached holders are never
18
+ -- preempted — only TTL expiry displaces them, exactly as in 064.
19
+ DROP FUNCTION IF EXISTS public.acquire_episode_lease(uuid, uuid, text, integer);
20
+ CREATE FUNCTION public.acquire_episode_lease(
21
+ p_thread_id uuid,
22
+ p_account_id uuid,
23
+ p_driver_id text,
24
+ p_ttl_seconds integer DEFAULT 90,
25
+ p_steal_detached boolean DEFAULT false
26
+ )
27
+ RETURNS jsonb
28
+ LANGUAGE plpgsql
29
+ SECURITY DEFINER
30
+ SET search_path = public
31
+ AS $$
32
+ DECLARE
33
+ v_lease public.episode_leases%ROWTYPE;
34
+ v_new_token uuid := gen_random_uuid();
35
+ BEGIN
36
+ -- Fast path: claim the empty slot. The thread_id PK serializes
37
+ -- concurrent first-writers — the loser's INSERT is a no-op.
38
+ INSERT INTO public.episode_leases (thread_id, account_id, driver_id, lease_token, expires_at)
39
+ VALUES (
40
+ p_thread_id,
41
+ p_account_id,
42
+ p_driver_id,
43
+ v_new_token,
44
+ now() + make_interval(secs => p_ttl_seconds)
45
+ )
46
+ ON CONFLICT (thread_id) DO NOTHING;
47
+
48
+ IF FOUND THEN
49
+ RETURN jsonb_build_object('outcome', 'claimed', 'lease_token', v_new_token);
50
+ END IF;
51
+
52
+ -- Conflict path: lock the row so a concurrent release/takeover cannot
53
+ -- interleave between our read and our verdict.
54
+ SELECT * INTO v_lease
55
+ FROM public.episode_leases
56
+ WHERE thread_id = p_thread_id
57
+ FOR UPDATE;
58
+
59
+ -- A live different driver owns the episode — refuse, unless the holder
60
+ -- is a detached background driver and the caller asked to steal it.
61
+ -- Detached drivers are a liveness fallback, not a real owner: a live
62
+ -- request driver always wins over one. The detached driver's next
63
+ -- heartbeat fails the token check and it aborts cleanly, so the steal
64
+ -- never produces duplicate side effects.
65
+ IF v_lease.driver_id <> p_driver_id
66
+ AND v_lease.expires_at > now()
67
+ AND NOT (p_steal_detached AND v_lease.driver_id LIKE 'detached:%') THEN
68
+ RETURN jsonb_build_object(
69
+ 'outcome', 'held',
70
+ 'holder_driver_id', v_lease.driver_id
71
+ );
72
+ END IF;
73
+
74
+ -- Same driver (renew), expired lease (takeover), or detached steal:
75
+ -- fresh token + TTL. A displaced driver's later heartbeat/release
76
+ -- fails the token check, which is what fences its pending writes.
77
+ UPDATE public.episode_leases SET
78
+ driver_id = p_driver_id,
79
+ lease_token = v_new_token,
80
+ account_id = p_account_id,
81
+ expires_at = now() + make_interval(secs => p_ttl_seconds),
82
+ updated_at = now()
83
+ WHERE thread_id = p_thread_id;
84
+
85
+ RETURN jsonb_build_object('outcome', 'claimed', 'lease_token', v_new_token);
86
+ END;
87
+ $$;
88
+
89
+ REVOKE ALL ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer, boolean) FROM PUBLIC;
90
+ REVOKE ALL ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer, boolean) FROM anon;
91
+ REVOKE ALL ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer, boolean) FROM authenticated;
92
+ GRANT EXECUTE ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer, boolean) TO service_role;
93
+
94
+ -- ── merge_thread_data ──────────────────────────────────────────────
95
+ -- Atomic shallow merge into threads.data. The previous pattern —
96
+ -- read threads.data, merge in memory, write the whole blob back —
97
+ -- loses any key written between the read and the write; during the
98
+ -- guided demo a concurrent threads.data write resurrected a stale
99
+ -- research_state over the terminal 'complete' checkpoint, leaving the
100
+ -- thread showing planning/turn 0 after the final answer landed.
101
+ -- jsonb || merges keys atomically so sibling writes never clobber.
102
+ --
103
+ -- Lease fence: when p_lease_token is provided, the write only lands
104
+ -- while no differently-tokened episode_lease holds the thread — a
105
+ -- displaced driver's stale checkpoint becomes a no-op instead of
106
+ -- resurrecting mid-episode state over the takeover owner's terminal
107
+ -- write. Pass NULL (or omit) for writers with no lease identity; the
108
+ -- merge is still atomic, just unfenced.
109
+ --
110
+ -- Returns true when the row was written, false when fenced or the
111
+ -- thread does not exist.
112
+ CREATE OR REPLACE FUNCTION public.merge_thread_data(
113
+ p_thread_id uuid,
114
+ p_patch jsonb,
115
+ p_lease_token uuid DEFAULT null
116
+ )
117
+ RETURNS boolean
118
+ LANGUAGE plpgsql
119
+ SECURITY DEFINER
120
+ SET search_path = public
121
+ AS $$
122
+ BEGIN
123
+ UPDATE public.threads t SET
124
+ data = coalesce(t.data, '{}'::jsonb) || p_patch,
125
+ updated_at = now()
126
+ WHERE t.id = p_thread_id
127
+ AND (p_lease_token IS NULL OR NOT EXISTS (
128
+ SELECT 1 FROM public.episode_leases l
129
+ WHERE l.thread_id = p_thread_id
130
+ AND l.lease_token IS DISTINCT FROM p_lease_token
131
+ ));
132
+
133
+ RETURN FOUND;
134
+ END;
135
+ $$;
136
+
137
+ REVOKE ALL ON FUNCTION public.merge_thread_data(uuid, jsonb, uuid) FROM PUBLIC;
138
+ REVOKE ALL ON FUNCTION public.merge_thread_data(uuid, jsonb, uuid) FROM anon;
139
+ REVOKE ALL ON FUNCTION public.merge_thread_data(uuid, jsonb, uuid) FROM authenticated;
140
+ GRANT EXECUTE ON FUNCTION public.merge_thread_data(uuid, jsonb, uuid) TO service_role;
141
+
142
+ -- CIL[DOC-VECTOR type="context" content="purpose='Detached-driver lease steal plus atomic fenced threads.data merge.'; Callers: _shared/agent-runner.ts (acquireEpisodeLease, mergeThreadData, setResearchState, handleEscalation, claimAgentEpisode/kickAgentEpisode), functions/ai-agents.ts (run handler); imports: none" status="ready"]
143
+ -- CIL[DOC-VECTOR type="blast_radius" content="Episode entry paths can preempt detached fallback drivers instantly; all threads.data writers through merge_thread_data gain atomicity and optional lease fencing" status="ready"]
144
+ -- CIL[DOC-VECTOR type="configuration" content="Runs after 064_episode_leases.sql; consumed by _shared/agent-runner.ts via adminDb.rpc" status="ready"]
145
+ -- CIL[DOC-VECTOR type="constraint" content="service-role only; 'detached:%' driver_id prefix is a contract with kickAgentEpisode; merge_thread_data is a shallow merge — nested objects in p_patch replace wholesale" status="ready"]
146
+ -- CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
147
+ -- CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
148
+ -- CIL[END stable_id="~/.framework/migrations/066_episode_steal_thread_merge.sql:episode_steal_thread_merge:file"]