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
@@ -0,0 +1,93 @@
1
+ -- Migration 067: stale-gated detached-driver steal
2
+ --
3
+ -- 066 let a request-bound driver steal any live 'detached:' lease
4
+ -- unconditionally. That guaranteed a mid-turn displacement on every
5
+ -- guided run (triage kick claims first, the client's episode POST steals
6
+ -- seconds later), fencing a healthy driver and leaving research_state
7
+ -- mid-flight — the residual duplicate-narration class. Steal is a
8
+ -- recovery path for STALLED background work, so it now requires the
9
+ -- holder's heartbeat (updated_at, renewed by heartbeat_episode_lease)
10
+ -- to be older than p_stale_seconds. Healthy detached drivers are
11
+ -- refused 'held' like any other live driver.
12
+
13
+ -- CIL[BEGIN stable_id="~/.framework/migrations/067_episode_lease_stale_steal.sql:module:migration" type="migration" identity="episode-lease-stale-steal" parent="~/.framework/migrations/067_episode_lease_stale_steal.sql"]
14
+ -- CIL[DOC-MACRO purpose="Gate p_steal_detached on the holder's heartbeat age so request drivers only displace stalled background work"]
15
+ -- CIL[DOC-RESPONSIBILITY responsibility="Redefine acquire_episode_lease with a p_stale_seconds parameter; detach-prefix holders are stealable only when updated_at exceeds the staleness window"]
16
+ -- CIL[DOC-CONTEXT module="episode-concurrency" layer="schema" related_files=".framework/functions/_shared/agent-runner.ts,.framework/functions/ai-agents.ts"]
17
+ -- CIL[DOC-CONTRACT inputs="p_thread_id uuid, p_account_id uuid, p_driver_id text, p_ttl_seconds int default 90, p_steal_detached bool default false, p_stale_seconds int default 30" outputs="jsonb verdict {outcome: claimed|held, lease_token?, holder_driver_id?}" errors="none — fail-open verdict" side_effects="inserts or updates episode_leases"]
18
+ -- CIL[DOC-MICRO logic_summary="Identical to 066 except the steal branch additionally requires v_lease.updated_at to be older than p_stale_seconds; heartbeat renewals keep a healthy detached driver unstealable" notable_decisions="30s default bounds takeover latency well under the 90s TTL while tolerating slow single turns between heartbeats"]
19
+ -- CIL[DOC-CONFIDENCE pass="first" score="0.92" flags="none"]
20
+ -- CIL[DOC-STABILITY stability="stable"]
21
+ -- CIL[DOC-AUDIENCE audience="system-admin"]
22
+ -- CIL[DOC-VECTOR type="context" content="Third lease RPC revision: 063 atomic claim, 066 detached steal, 067 stale-gated steal" status="ready"]
23
+ -- CIL[END stable_id="~/.framework/migrations/067_episode_lease_stale_steal.sql:module:migration"]
24
+
25
+ DROP FUNCTION IF EXISTS public.acquire_episode_lease(uuid, uuid, text, integer, boolean);
26
+
27
+ CREATE FUNCTION public.acquire_episode_lease(
28
+ p_thread_id uuid,
29
+ p_account_id uuid,
30
+ p_driver_id text,
31
+ p_ttl_seconds integer DEFAULT 90,
32
+ p_steal_detached boolean DEFAULT false,
33
+ p_stale_seconds integer DEFAULT 30
34
+ )
35
+ RETURNS jsonb
36
+ LANGUAGE plpgsql
37
+ SECURITY DEFINER
38
+ SET search_path = public
39
+ AS $$
40
+ DECLARE
41
+ v_lease public.episode_leases%ROWTYPE;
42
+ v_new_token uuid := gen_random_uuid();
43
+ BEGIN
44
+ INSERT INTO public.episode_leases (thread_id, account_id, driver_id, lease_token, expires_at)
45
+ VALUES (
46
+ p_thread_id,
47
+ p_account_id,
48
+ p_driver_id,
49
+ v_new_token,
50
+ now() + make_interval(secs => p_ttl_seconds)
51
+ )
52
+ ON CONFLICT (thread_id) DO NOTHING;
53
+
54
+ IF FOUND THEN
55
+ RETURN jsonb_build_object('outcome', 'claimed', 'lease_token', v_new_token);
56
+ END IF;
57
+
58
+ SELECT * INTO v_lease
59
+ FROM public.episode_leases
60
+ WHERE thread_id = p_thread_id
61
+ FOR UPDATE;
62
+
63
+ -- Held: a different live driver owns the episode. Steal is allowed
64
+ -- only for a 'detached:'-prefixed holder whose heartbeat has gone
65
+ -- stale — a healthy background driver is refused like anyone else so
66
+ -- request claims stop displacing mid-turn work.
67
+ IF v_lease.driver_id <> p_driver_id
68
+ AND v_lease.expires_at > now()
69
+ AND NOT (p_steal_detached
70
+ AND v_lease.driver_id LIKE 'detached:%'
71
+ AND v_lease.updated_at < now() - make_interval(secs => p_stale_seconds)) THEN
72
+ RETURN jsonb_build_object(
73
+ 'outcome', 'held',
74
+ 'holder_driver_id', v_lease.driver_id
75
+ );
76
+ END IF;
77
+
78
+ UPDATE public.episode_leases SET
79
+ driver_id = p_driver_id,
80
+ lease_token = v_new_token,
81
+ account_id = p_account_id,
82
+ expires_at = now() + make_interval(secs => p_ttl_seconds),
83
+ updated_at = now()
84
+ WHERE thread_id = p_thread_id;
85
+
86
+ RETURN jsonb_build_object('outcome', 'claimed', 'lease_token', v_new_token);
87
+ END;
88
+ $$;
89
+
90
+ REVOKE ALL ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer, boolean, integer) FROM PUBLIC;
91
+ REVOKE ALL ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer, boolean, integer) FROM anon;
92
+ REVOKE ALL ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer, boolean, integer) FROM authenticated;
93
+ GRANT EXECUTE ON FUNCTION public.acquire_episode_lease(uuid, uuid, text, integer, boolean, integer) TO service_role;
@@ -0,0 +1,251 @@
1
+ -- Migration 068: bootstrap the RPCs system-cron expects
2
+ --
3
+ -- system-cron calls nine Postgres functions that were documented as
4
+ -- "bootstrapped by framework migrations" but never created — every tick
5
+ -- hard-failed on get_due_schedules with
6
+ -- "Could not find the function public.get_due_schedules(p_now) in the
7
+ -- schema cache"
8
+ -- This migration creates the missing RPCs against the existing
9
+ -- schedules / timers / schedule_executions / pipeline_executions / logs
10
+ -- tables.
11
+ --
12
+ -- Honest limits (the tables are half-designed — schedules has no
13
+ -- config/delegated_scopes columns and there is no cron-expression
14
+ -- evaluator in the DB):
15
+ -- * get_due_schedules returns delegated_scopes=NULL and config='{}'
16
+ -- (the cron handler already tolerates both).
17
+ -- * update_schedule_after_run reschedules only the every-N-minutes
18
+ -- cron form ('*/N * * * *'); anything else gets next_run_at=NULL so
19
+ -- a schedule can never re-fire every tick forever.
20
+ -- * update_timer_after_run reschedules when
21
+ -- config->>'interval_minutes' is a positive number, else NULL.
22
+
23
+ -- CIL[BEGIN stable_id="~/.framework/migrations/068_cron_schedule_timer_rpcs.sql:module:migration" type="migration" identity="cron-schedule-timer-rpcs" parent="~/.framework/migrations/068_cron_schedule_timer_rpcs.sql"]
24
+ -- CIL[DOC-MACRO purpose="Create the nine Postgres RPCs system-cron invokes for schedules, timers, threshold metrics and log cleanup"]
25
+ -- CIL[DOC-RESPONSIBILITY responsibility="Provide get_due_schedules, validate_schedule_creator, update_schedule_after_run, get_due_timers, update_timer_after_run, get_error_rate, get_latency_percentiles, get_pipeline_stats, cleanup_old_logs"]
26
+ -- CIL[DOC-CONTEXT module="system-cron" layer="schema" related_files=".framework/functions/system-cron.ts"]
27
+ -- CIL[DOC-CONTRACT inputs="timestamptz/uuid/bool/int params per function" outputs="setof rows or scalars matching the shapes system-cron reads" errors="none — SECURITY DEFINER, stable where read-only" side_effects="update_schedule_after_run/update_timer_after_run mutate schedule/timer rows; cleanup_old_logs deletes old logs"]
28
+ -- CIL[DOC-MICRO logic_summary="Simple SELECT/UPDATE/DELETE implementations; cron reschedule limited to '*/N * * * *', timers honor config->>'interval_minutes', everything else next_*=NULL to prevent infinite re-fire" notable_decisions="No speculative cron parser — a NULL next_run_at is a safe one-shot stop until the feature gets a real scheduler model"]
29
+ -- CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
30
+ -- CIL[DOC-STABILITY stability="stable"]
31
+ -- CIL[DOC-AUDIENCE audience="system-admin"]
32
+ -- CIL[END stable_id="~/.framework/migrations/068_cron_schedule_timer_rpcs.sql:module:migration"]
33
+
34
+ -- ── schedules ─────────────────────────────────────────────────────────
35
+
36
+ CREATE OR REPLACE FUNCTION public.get_due_schedules(p_now timestamptz)
37
+ RETURNS TABLE (
38
+ schedule_id uuid,
39
+ action_id uuid,
40
+ account_id uuid,
41
+ machine_principal_id uuid,
42
+ delegated_scopes jsonb,
43
+ config jsonb
44
+ )
45
+ LANGUAGE sql
46
+ STABLE
47
+ SECURITY DEFINER
48
+ SET search_path = public
49
+ AS $$
50
+ SELECT
51
+ s.id,
52
+ s.action_id,
53
+ s.account_id,
54
+ s.machine_principal_id,
55
+ NULL::jsonb,
56
+ '{}'::jsonb
57
+ FROM public.schedules s
58
+ WHERE s.is_active
59
+ AND s.next_run_at IS NOT NULL
60
+ AND s.next_run_at <= p_now
61
+ $$;
62
+
63
+ CREATE OR REPLACE FUNCTION public.validate_schedule_creator(p_schedule_id uuid)
64
+ RETURNS TABLE (is_valid boolean, error_message text)
65
+ LANGUAGE sql
66
+ STABLE
67
+ SECURITY DEFINER
68
+ SET search_path = public
69
+ AS $$
70
+ SELECT
71
+ EXISTS (
72
+ SELECT 1 FROM public.schedules s
73
+ WHERE s.id = p_schedule_id AND s.is_active
74
+ ),
75
+ CASE
76
+ WHEN NOT EXISTS (SELECT 1 FROM public.schedules s WHERE s.id = p_schedule_id)
77
+ THEN 'schedule not found'
78
+ WHEN NOT EXISTS (SELECT 1 FROM public.schedules s WHERE s.id = p_schedule_id AND s.is_active)
79
+ THEN 'schedule is not active'
80
+ ELSE NULL
81
+ END
82
+ $$;
83
+
84
+ CREATE OR REPLACE FUNCTION public.update_schedule_after_run(
85
+ p_schedule_id uuid,
86
+ p_success boolean,
87
+ p_error_message text
88
+ )
89
+ RETURNS void
90
+ LANGUAGE plpgsql
91
+ SECURITY DEFINER
92
+ SET search_path = public
93
+ AS $$
94
+ DECLARE
95
+ v_cron text;
96
+ v_minutes integer;
97
+ BEGIN
98
+ SELECT cron_expression INTO v_cron FROM public.schedules WHERE id = p_schedule_id;
99
+
100
+ -- Only the every-N-minutes form is reschedulable here; a full cron
101
+ -- evaluator does not exist in the DB. Anything else becomes a
102
+ -- one-shot (NULL next_run_at) rather than re-firing every tick.
103
+ v_minutes := NULL;
104
+ IF v_cron ~ '^\*/(\d+) \* \* \* \*$' THEN
105
+ v_minutes := substring(v_cron FROM '^\*/(\d+)')::integer;
106
+ END IF;
107
+
108
+ UPDATE public.schedules
109
+ SET
110
+ last_run_at = now(),
111
+ next_run_at = CASE
112
+ WHEN v_minutes IS NOT NULL THEN now() + make_interval(mins => v_minutes)
113
+ ELSE NULL
114
+ END,
115
+ updated_at = now()
116
+ WHERE id = p_schedule_id;
117
+ END;
118
+ $$;
119
+
120
+ -- ── timers ────────────────────────────────────────────────────────────
121
+
122
+ CREATE OR REPLACE FUNCTION public.get_due_timers(p_now timestamptz)
123
+ RETURNS TABLE (
124
+ timer_id uuid,
125
+ pipeline_id uuid,
126
+ account_id uuid,
127
+ app_id uuid,
128
+ name text,
129
+ execution_count integer,
130
+ created_by uuid
131
+ )
132
+ LANGUAGE sql
133
+ STABLE
134
+ SECURITY DEFINER
135
+ SET search_path = public
136
+ AS $$
137
+ SELECT
138
+ t.id,
139
+ t.pipeline_id,
140
+ t.account_id,
141
+ t.app_id,
142
+ t.name,
143
+ COALESCE(t.execution_count, 0),
144
+ t.created_by
145
+ FROM public.timers t
146
+ WHERE t.is_active
147
+ AND t.pipeline_id IS NOT NULL
148
+ AND t.next_execution IS NOT NULL
149
+ AND t.next_execution <= p_now
150
+ $$;
151
+
152
+ CREATE OR REPLACE FUNCTION public.update_timer_after_run(
153
+ p_timer_id uuid,
154
+ p_success boolean,
155
+ p_error_message text
156
+ )
157
+ RETURNS void
158
+ LANGUAGE plpgsql
159
+ SECURITY DEFINER
160
+ SET search_path = public
161
+ AS $$
162
+ DECLARE
163
+ v_interval numeric;
164
+ BEGIN
165
+ SELECT CASE
166
+ WHEN config->>'interval_minutes' ~ '^\d+(\.\d+)?$'
167
+ THEN (config->>'interval_minutes')::numeric
168
+ ELSE NULL
169
+ END INTO v_interval
170
+ FROM public.timers WHERE id = p_timer_id;
171
+
172
+ UPDATE public.timers
173
+ SET
174
+ last_execution = now(),
175
+ execution_count = COALESCE(execution_count, 0) + 1,
176
+ success_count = COALESCE(success_count, 0) + CASE WHEN p_success THEN 1 ELSE 0 END,
177
+ failure_count = COALESCE(failure_count, 0) + CASE WHEN p_success THEN 0 ELSE 1 END,
178
+ next_execution = CASE
179
+ WHEN v_interval IS NOT NULL AND v_interval > 0
180
+ THEN now() + make_interval(mins => v_interval::double precision)
181
+ ELSE NULL
182
+ END,
183
+ updated_at = now()
184
+ WHERE id = p_timer_id;
185
+ END;
186
+ $$;
187
+
188
+ -- ── threshold metrics ─────────────────────────────────────────────────
189
+
190
+ CREATE OR REPLACE FUNCTION public.get_error_rate(p_window_minutes integer)
191
+ RETURNS TABLE (error_rate numeric)
192
+ LANGUAGE sql
193
+ STABLE
194
+ SECURITY DEFINER
195
+ SET search_path = public
196
+ AS $$
197
+ SELECT CASE WHEN count(*) = 0 THEN 0
198
+ ELSE round(count(*) FILTER (WHERE outcome = 'failure' OR level = 'error')::numeric * 100 / count(*), 2)
199
+ END
200
+ FROM public.logs
201
+ WHERE created_at >= now() - make_interval(mins => p_window_minutes)
202
+ $$;
203
+
204
+ CREATE OR REPLACE FUNCTION public.get_latency_percentiles(p_window_minutes integer)
205
+ RETURNS TABLE (p50 numeric, p95 numeric, p99 numeric)
206
+ LANGUAGE sql
207
+ STABLE
208
+ SECURITY DEFINER
209
+ SET search_path = public
210
+ AS $$
211
+ SELECT
212
+ percentile_cont(0.50) WITHIN GROUP (ORDER BY duration_ms)::numeric,
213
+ percentile_cont(0.95) WITHIN GROUP (ORDER BY duration_ms)::numeric,
214
+ percentile_cont(0.99) WITHIN GROUP (ORDER BY duration_ms)::numeric
215
+ FROM public.logs
216
+ WHERE created_at >= now() - make_interval(mins => p_window_minutes)
217
+ AND duration_ms IS NOT NULL
218
+ $$;
219
+
220
+ CREATE OR REPLACE FUNCTION public.get_pipeline_stats(p_window_minutes integer)
221
+ RETURNS TABLE (total_success bigint, total_failure bigint)
222
+ LANGUAGE sql
223
+ STABLE
224
+ SECURITY DEFINER
225
+ SET search_path = public
226
+ AS $$
227
+ SELECT
228
+ count(*) FILTER (WHERE status = 'completed'),
229
+ count(*) FILTER (WHERE status = 'failed')
230
+ FROM public.pipeline_executions
231
+ WHERE created_at >= now() - make_interval(mins => p_window_minutes)
232
+ $$;
233
+
234
+ -- ── log retention ─────────────────────────────────────────────────────
235
+
236
+ CREATE OR REPLACE FUNCTION public.cleanup_old_logs(p_retention_days integer DEFAULT 90)
237
+ RETURNS TABLE (deleted_count bigint)
238
+ LANGUAGE plpgsql
239
+ SECURITY DEFINER
240
+ SET search_path = public
241
+ AS $$
242
+ BEGIN
243
+ RETURN QUERY
244
+ WITH deleted AS (
245
+ DELETE FROM public.logs
246
+ WHERE created_at < now() - make_interval(days => p_retention_days)
247
+ RETURNING 1
248
+ )
249
+ SELECT count(*) FROM deleted;
250
+ END;
251
+ $$;
@@ -0,0 +1,54 @@
1
+ CREATE OR REPLACE FUNCTION public.get_log_stats(
2
+ p_account_id uuid,
3
+ p_level text DEFAULT NULL::text,
4
+ p_source text DEFAULT NULL::text,
5
+ p_source_type text DEFAULT NULL::text,
6
+ p_source_id text DEFAULT NULL::text,
7
+ p_person_id text DEFAULT NULL::text,
8
+ p_query text DEFAULT NULL::text,
9
+ p_date_from timestamp with time zone DEFAULT NULL::timestamp with time zone,
10
+ p_date_to timestamp with time zone DEFAULT NULL::timestamp with time zone
11
+ )
12
+ RETURNS TABLE(total bigint, error_count bigint, user_action_count bigint, event_type_count bigint, levels text[], sources text[], source_types text[])
13
+ LANGUAGE plpgsql
14
+ AS $function$
15
+ DECLARE
16
+ v_from timestamptz := COALESCE(p_date_from, now() - interval '7 days');
17
+ v_total bigint;
18
+ v_error_count bigint;
19
+ v_user_action_count bigint;
20
+ v_event_type_count bigint;
21
+ v_levels text[];
22
+ v_sources text[];
23
+ v_source_types text[];
24
+ BEGIN
25
+ SELECT
26
+ count(*),
27
+ count(*) FILTER (WHERE level ILIKE '%error%'),
28
+ count(*) FILTER (WHERE person_id IS NOT NULL),
29
+ count(DISTINCT level)
30
+ INTO v_total, v_error_count, v_user_action_count, v_event_type_count
31
+ FROM public.logs
32
+ WHERE account_id = p_account_id
33
+ AND created_at >= v_from
34
+ AND (p_level IS NULL OR level = p_level)
35
+ AND (p_source IS NULL OR source = p_source)
36
+ AND (p_source_type IS NULL OR source_type = p_source_type)
37
+ AND (p_source_id IS NULL OR source_id = p_source_id::uuid)
38
+ AND (p_person_id IS NULL OR person_id = p_person_id::uuid)
39
+ AND (p_query IS NULL OR message ILIKE '%' || p_query || '%')
40
+ AND (p_date_to IS NULL OR created_at <= p_date_to);
41
+
42
+ SELECT
43
+ coalesce(array_agg(DISTINCT level ORDER BY level) FILTER (WHERE level IS NOT NULL), ARRAY[]::text[]),
44
+ coalesce(array_agg(DISTINCT source ORDER BY source) FILTER (WHERE source IS NOT NULL), ARRAY[]::text[]),
45
+ coalesce(array_agg(DISTINCT source_type ORDER BY source_type) FILTER (WHERE source_type IS NOT NULL), ARRAY[]::text[])
46
+ INTO v_levels, v_sources, v_source_types
47
+ FROM public.logs
48
+ WHERE account_id = p_account_id
49
+ AND created_at >= v_from
50
+ AND (p_date_to IS NULL OR created_at <= p_date_to);
51
+
52
+ RETURN QUERY SELECT v_total, v_error_count, v_user_action_count, v_event_type_count, v_levels, v_sources, v_source_types;
53
+ END;
54
+ $function$;
@@ -0,0 +1,166 @@
1
+ -- Migration 070: bootstrap the RPCs the types/apps handlers expect
2
+ --
3
+ -- types.ts getSchema (?action=schema) and apps.ts getSchema /
4
+ -- checkAvailability / updateVersion (?action=schema|available|version)
5
+ -- call four Postgres functions that existed in the pre-consolidation v2
6
+ -- schema (v2-core/migrations/010_apps.sql, 004_types.sql +
7
+ -- 049_schema_driven_permissions.sql) but were never ported into
8
+ -- .framework/migrations — each action fails at runtime on a stock
9
+ -- install with "Could not find the function public.<name>(...) in the
10
+ -- schema cache". This migration recreates them against the public
11
+ -- schema.
12
+ --
13
+ -- Semantics are ported from the recovered v2 definitions:
14
+ -- * get_type_schema returns the matching active type's design_schema
15
+ -- ('{}' when none). An app_id argument prefers that app's row and
16
+ -- falls back to the global (app_id IS NULL) type — the documented
17
+ -- "app-specific fallback to system" behavior.
18
+ -- * get_app_schema returns a jsonb projection of the active app row
19
+ -- ('{}' when none), extended with the required_roles / route_prefix /
20
+ -- renderer columns added since v2.
21
+ -- * is_app_available returns true when an active app with the slug is
22
+ -- available to the account under the same ownership rule
23
+ -- get_account_apps uses (is_system OR owner_account_id IS NULL OR
24
+ -- owner_account_id = account_id).
25
+ -- * update_app_version bumps apps.version + updated_at and returns
26
+ -- void — the handler only checks for an error.
27
+ --
28
+ -- All four run as SECURITY INVOKER (the v2 default): callers go through
29
+ -- ctx.db, so human callers keep RLS scoping (types are world-readable via
30
+ -- types_read; apps via apps_access) and machine principals bypass RLS as
31
+ -- service_role. GRANTs match the sibling RPC convention in
32
+ -- 004_core_rpc_and_broker.sql.
33
+
34
+ -- CIL[BEGIN stable_id="~/.framework/migrations/070_app_type_schema_rpcs.sql:module:migration" type="migration" identity="app-type-schema-rpcs" parent="~/.framework/migrations/070_app_type_schema_rpcs.sql"]
35
+ -- CIL[DOC-MACRO purpose="Create the four Postgres RPCs the types and apps Netlify handlers invoke: get_type_schema, get_app_schema, is_app_available, update_app_version"]
36
+ -- CIL[DOC-RESPONSIBILITY responsibility="Provide schema lookup, app-schema projection, account-scoped app availability, and app version bump functions missing since the v2-to-public consolidation"]
37
+ -- CIL[DOC-CONTEXT module="functions" layer="schema" related_files=".framework/functions/types.ts,.framework/functions/apps.ts,.framework/migrations/013_get_account_apps_role_filter.sql"]
38
+ -- CIL[DOC-CONTRACT inputs="text/uuid params matching the .rpc named args in types.ts and apps.ts" outputs="jsonb scalars, boolean, void matching the shapes the handlers read" errors="none — SECURITY INVOKER relies on caller RLS" side_effects="update_app_version mutates public.apps version and updated_at"]
39
+ -- CIL[DOC-MICRO logic_summary="Ports the recovered v2-core function bodies to the public schema; get_type_schema reads design_schema with app-specific fallback to the global type; is_app_available reuses the get_account_apps ownership rule including NULL-owner apps" notable_decisions="SECURITY INVOKER rather than DEFINER so human callers keep RLS scoping and no caller-supplied privilege claims are trusted"]
40
+ -- CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
41
+ -- CIL[DOC-STABILITY stability="stable"]
42
+ -- CIL[DOC-AUDIENCE audience="framework_developers"]
43
+ -- CIL[END stable_id="~/.framework/migrations/070_app_type_schema_rpcs.sql:module:migration"]
44
+
45
+ -- ── types ─────────────────────────────────────────────────────────────
46
+
47
+ CREATE OR REPLACE FUNCTION public.get_type_schema(
48
+ kind text,
49
+ slug text,
50
+ app_id uuid DEFAULT NULL
51
+ )
52
+ RETURNS jsonb
53
+ LANGUAGE plpgsql
54
+ STABLE
55
+ AS $$
56
+ DECLARE
57
+ type_schema jsonb;
58
+ BEGIN
59
+ SELECT t.design_schema INTO type_schema
60
+ FROM public.types t
61
+ WHERE t.kind = get_type_schema.kind
62
+ AND t.slug = get_type_schema.slug
63
+ AND t.is_active = true
64
+ AND (t.app_id = get_type_schema.app_id OR t.app_id IS NULL)
65
+ ORDER BY t.app_id NULLS LAST -- prefer app-specific types over global
66
+ LIMIT 1;
67
+
68
+ RETURN COALESCE(type_schema, '{}'::jsonb);
69
+ END;
70
+ $$;
71
+
72
+ GRANT EXECUTE ON FUNCTION public.get_type_schema(text, text, uuid) TO authenticated;
73
+ GRANT EXECUTE ON FUNCTION public.get_type_schema(text, text, uuid) TO service_role;
74
+
75
+ COMMENT ON FUNCTION public.get_type_schema(text, text, uuid) IS 'Get type design_schema by kind+slug, app-specific with fallback to global';
76
+
77
+ -- ── apps ──────────────────────────────────────────────────────────────
78
+
79
+ CREATE OR REPLACE FUNCTION public.get_app_schema(app_slug text)
80
+ RETURNS jsonb
81
+ LANGUAGE plpgsql
82
+ STABLE
83
+ AS $$
84
+ DECLARE
85
+ app_schema jsonb;
86
+ BEGIN
87
+ SELECT jsonb_build_object(
88
+ 'id', a.id,
89
+ 'slug', a.slug,
90
+ 'name', a.name,
91
+ 'description', a.description,
92
+ 'icon', a.icon,
93
+ 'color', a.color,
94
+ 'version', a.version,
95
+ 'app_type', a.app_type,
96
+ 'source', a.source,
97
+ 'config', a.config,
98
+ 'nav_items', a.nav_items,
99
+ 'min_role', a.min_role,
100
+ 'required_roles', a.required_roles,
101
+ 'integration_deps', a.integration_deps,
102
+ 'metadata', a.metadata,
103
+ 'route_prefix', a.route_prefix,
104
+ 'renderer', a.renderer
105
+ ) INTO app_schema
106
+ FROM public.apps a
107
+ WHERE a.slug = get_app_schema.app_slug
108
+ AND a.is_active = true;
109
+
110
+ RETURN COALESCE(app_schema, '{}'::jsonb);
111
+ END;
112
+ $$;
113
+
114
+ GRANT EXECUTE ON FUNCTION public.get_app_schema(text) TO authenticated;
115
+ GRANT EXECUTE ON FUNCTION public.get_app_schema(text) TO service_role;
116
+
117
+ COMMENT ON FUNCTION public.get_app_schema(text) IS 'Get full app schema by slug';
118
+
119
+ CREATE OR REPLACE FUNCTION public.is_app_available(
120
+ app_slug text,
121
+ account_id uuid
122
+ )
123
+ RETURNS boolean
124
+ LANGUAGE plpgsql
125
+ STABLE
126
+ AS $$
127
+ BEGIN
128
+ RETURN EXISTS (
129
+ SELECT 1
130
+ FROM public.apps a
131
+ WHERE a.slug = is_app_available.app_slug
132
+ AND a.is_active = true
133
+ AND (
134
+ a.is_system = true
135
+ OR a.owner_account_id IS NULL
136
+ OR a.owner_account_id = is_app_available.account_id
137
+ )
138
+ );
139
+ END;
140
+ $$;
141
+
142
+ GRANT EXECUTE ON FUNCTION public.is_app_available(text, uuid) TO authenticated;
143
+ GRANT EXECUTE ON FUNCTION public.is_app_available(text, uuid) TO service_role;
144
+
145
+ COMMENT ON FUNCTION public.is_app_available(text, uuid) IS 'Check if app is available to account';
146
+
147
+ CREATE OR REPLACE FUNCTION public.update_app_version(
148
+ app_id uuid,
149
+ new_version text
150
+ )
151
+ RETURNS void
152
+ LANGUAGE plpgsql
153
+ AS $$
154
+ BEGIN
155
+ UPDATE public.apps
156
+ SET
157
+ version = update_app_version.new_version,
158
+ updated_at = now()
159
+ WHERE id = update_app_version.app_id;
160
+ END;
161
+ $$;
162
+
163
+ GRANT EXECUTE ON FUNCTION public.update_app_version(uuid, text) TO authenticated;
164
+ GRANT EXECUTE ON FUNCTION public.update_app_version(uuid, text) TO service_role;
165
+
166
+ COMMENT ON FUNCTION public.update_app_version(uuid, text) IS 'Update app version';
@@ -0,0 +1,96 @@
1
+ -- Migration 071: F-056 — make timers created via the API/admin UI fire.
2
+ --
3
+ -- Two halves of the fix:
4
+ -- 1. timers.ts now writes next_execution server-side on
5
+ -- create/update/activate (068's get_due_timers gates on
6
+ -- next_execution IS NOT NULL and nothing else ever set it).
7
+ -- 2. This file: update_timer_after_run previously rescheduled only
8
+ -- from config->>'interval_minutes', but TimerDetailPage seeded
9
+ -- config.interval_seconds — UI-created timers would fire once (if
10
+ -- armed) and never reschedule. The RPC now accepts both keys with
11
+ -- the same precedence the TS handler uses: interval_minutes
12
+ -- (minutes) first, then interval_seconds (seconds / 60).
13
+ -- Existing rows keep working under either key.
14
+ --
15
+ -- A backfill at the bottom arms existing active timers that have a
16
+ -- computable interval but next_execution IS NULL — rows created before
17
+ -- the API could write the column could never fire.
18
+
19
+ -- CIL[BEGIN stable_id="~/.framework/migrations/071_timer_interval_keys.sql:module:migration" type="migration" identity="timer-interval-keys" parent="~/.framework/migrations/071_timer_interval_keys.sql"]
20
+ -- CIL[DOC-MACRO purpose="Teach update_timer_after_run the interval_seconds config key and backfill next_execution for dormant armed-by-interval timers"]
21
+ -- CIL[DOC-RESPONSIBILITY responsibility="Replace update_timer_after_run with a dual-key interval resolver; arm existing active timers whose next_execution is NULL"]
22
+ -- CIL[DOC-CONTEXT module="system-cron" layer="schema" related_files=".framework/functions/system-cron.ts, .framework/functions/timers.ts, .framework/migrations/068_cron_schedule_timer_rpcs.sql"]
23
+ -- CIL[DOC-CONTRACT inputs="p_timer_id uuid, p_success boolean, p_error_message text" outputs="void" errors="none — SECURITY DEFINER" side_effects="mutates timers bookkeeping columns; backfill sets next_execution on dormant interval timers"]
24
+ -- CIL[DOC-MICRO logic_summary="Interval precedence: config->>'interval_minutes' (minutes) then config->>'interval_seconds' (seconds/60); non-positive or absent => next_execution NULL one-shot stop. Backfill UPDATE arms is_active + next_execution IS NULL + positive-interval rows to now()+interval" notable_decisions="Both keys kept forever so rows seeded with the old UI default (interval_seconds) and the canonical key (interval_minutes) both reschedule; 'once'/cron timers still dead-end to NULL since no cron evaluator exists in the DB"]
25
+ -- CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
26
+ -- CIL[DOC-STABILITY stability="stable"]
27
+ -- CIL[DOC-AUDIENCE audience="system-admin"]
28
+ -- CIL[END stable_id="~/.framework/migrations/071_timer_interval_keys.sql:module:migration"]
29
+
30
+ CREATE OR REPLACE FUNCTION public.update_timer_after_run(
31
+ p_timer_id uuid,
32
+ p_success boolean,
33
+ p_error_message text
34
+ )
35
+ RETURNS void
36
+ LANGUAGE plpgsql
37
+ SECURITY DEFINER
38
+ SET search_path = public
39
+ AS $$
40
+ DECLARE
41
+ v_config jsonb;
42
+ v_interval numeric; -- resolved interval in MINUTES
43
+ BEGIN
44
+ SELECT config INTO v_config FROM public.timers WHERE id = p_timer_id;
45
+
46
+ -- interval_minutes (canonical) wins; interval_seconds is the legacy UI
47
+ -- default and is converted seconds -> minutes.
48
+ v_interval := CASE
49
+ WHEN v_config->>'interval_minutes' ~ '^\d+(\.\d+)?$'
50
+ THEN (v_config->>'interval_minutes')::numeric
51
+ WHEN v_config->>'interval_seconds' ~ '^\d+(\.\d+)?$'
52
+ THEN (v_config->>'interval_seconds')::numeric / 60
53
+ ELSE NULL
54
+ END;
55
+
56
+ UPDATE public.timers
57
+ SET
58
+ last_execution = now(),
59
+ execution_count = COALESCE(execution_count, 0) + 1,
60
+ success_count = COALESCE(success_count, 0) + CASE WHEN p_success THEN 1 ELSE 0 END,
61
+ failure_count = COALESCE(failure_count, 0) + CASE WHEN p_success THEN 0 ELSE 1 END,
62
+ next_execution = CASE
63
+ WHEN v_interval IS NOT NULL AND v_interval > 0
64
+ THEN now() + (v_interval * interval '1 minute')
65
+ ELSE NULL
66
+ END,
67
+ updated_at = now()
68
+ WHERE id = p_timer_id;
69
+ END;
70
+ $$;
71
+
72
+ -- Arm timers that are active and have a resolvable interval but were
73
+ -- created while next_execution was unwritable (pre-F-056 API). One-shot
74
+ -- and unparseable configs stay NULL — a NULL next_execution is a safe
75
+ -- never-fire state, matching update_timer_after_run's stop semantics.
76
+ UPDATE public.timers t
77
+ SET
78
+ next_execution = now() + (v.mins * interval '1 minute'),
79
+ updated_at = now()
80
+ FROM (
81
+ SELECT
82
+ id,
83
+ CASE
84
+ WHEN config->>'interval_minutes' ~ '^\d+(\.\d+)?$'
85
+ THEN (config->>'interval_minutes')::numeric
86
+ WHEN config->>'interval_seconds' ~ '^\d+(\.\d+)?$'
87
+ THEN (config->>'interval_seconds')::numeric / 60
88
+ ELSE NULL
89
+ END AS mins
90
+ FROM public.timers
91
+ ) v
92
+ WHERE t.id = v.id
93
+ AND t.is_active
94
+ AND t.next_execution IS NULL
95
+ AND v.mins IS NOT NULL
96
+ AND v.mins > 0;