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.
- package/.env.example +13 -3
- package/.framework/cli/commands/bootstrap.ts +392 -0
- package/.framework/cli/commands/dev.ts +91 -218
- package/.framework/cli/commands/doctor.ts +132 -26
- package/.framework/cli/commands/generate.ts +38 -298
- package/.framework/cli/commands/init.ts +20 -1
- package/.framework/cli/commands/migrate.ts +36 -30
- package/.framework/cli/commands/migrations.ts +16 -56
- package/.framework/cli/commands/upgrade.ts +35 -0
- package/.framework/cli/commands/validate-schema.ts +147 -0
- package/.framework/cli/context.ts +223 -110
- package/.framework/cli/index.ts +6 -0
- package/.framework/cli/lib/project-files.ts +241 -1
- package/.framework/functions/_shared/agent-runner.ts +1000 -81
- package/.framework/functions/_shared/cil.ts +2 -1
- package/.framework/functions/_shared/db.ts +3 -6
- package/.framework/functions/_shared/hash-api-key.ts +183 -79
- package/.framework/functions/_shared/kb.ts +61 -7
- package/.framework/functions/_shared/logging.ts +562 -224
- package/.framework/functions/_shared/message-sequence.ts +53 -0
- package/.framework/functions/_shared/middleware.ts +47 -44
- package/.framework/functions/_shared/pipeline-runner.ts +150 -37
- package/.framework/functions/_shared/principal.ts +9 -6
- package/.framework/functions/_shared/trigger-engine.ts +149 -40
- package/.framework/functions/admin-data.ts +48 -31
- package/.framework/functions/ai-agents.ts +103 -33
- package/.framework/functions/api-keys.ts +301 -122
- package/.framework/functions/apps.ts +43 -26
- package/.framework/functions/cil-ingest.ts +48 -15
- package/.framework/functions/debug-auth.ts +14 -8
- package/.framework/functions/deliveries.ts +39 -12
- package/.framework/functions/embeddings.ts +145 -76
- package/.framework/functions/integration-outbound.ts +67 -27
- package/.framework/functions/integrations.ts +254 -127
- package/.framework/functions/logs.ts +273 -133
- package/.framework/functions/observability.ts +277 -147
- package/.framework/functions/pipeline-executions.ts +1 -1
- package/.framework/functions/pipeline-runner-background.ts +120 -0
- package/.framework/functions/pipelines.ts +14 -9
- package/.framework/functions/prompt-configs.ts +7 -4
- package/.framework/functions/roles.ts +8 -5
- package/.framework/functions/security.ts +30 -17
- package/.framework/functions/system-cron.ts +250 -128
- package/.framework/functions/tests.ts +7 -6
- package/.framework/functions/timers.ts +310 -107
- package/.framework/functions/trigger-scheduler.ts +65 -5
- package/.framework/functions/triggers.ts +28 -13
- package/.framework/functions/types.ts +13 -10
- package/.framework/migrations/064_episode_leases.sql +196 -0
- package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
- package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
- package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
- package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
- package/.framework/migrations/069_log_stats_window.sql +54 -0
- package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
- package/.framework/migrations/071_timer_interval_keys.sql +96 -0
- package/.framework/migrations/072_slug_autofill.sql +77 -0
- package/.framework/migrations/073_seed_member_roles.sql +26 -0
- package/.framework/schemas/spine.config.schema.json +62 -0
- package/.framework/src/App.tsx +3 -1
- package/.framework/src/apps/admin/index.tsx +1141 -775
- package/.framework/src/components/ErrorBoundary.tsx +136 -0
- package/.framework/src/components/admin/AdminListPage.tsx +81 -73
- package/.framework/src/components/admin/AdminSidebar.tsx +92 -81
- package/.framework/src/components/admin/EntitySelect.tsx +181 -0
- package/.framework/src/components/admin/PageInstructions.tsx +86 -127
- package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
- package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
- package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
- package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
- package/.framework/src/components/layout/Layout.tsx +4 -2
- package/.framework/src/components/layout/Sidebar.tsx +304 -179
- package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
- package/.framework/src/components/runtime/DataHeader.tsx +96 -48
- package/.framework/src/components/runtime/DataListPage.tsx +3 -26
- package/.framework/src/components/runtime/DataStats.tsx +10 -10
- package/.framework/src/components/runtime/DataTable.tsx +12 -10
- package/.framework/src/components/ui/sheet.tsx +24 -26
- package/.framework/src/components/ui/textarea.tsx +74 -48
- package/.framework/src/hooks/useBranding.ts +47 -0
- package/.framework/src/hooks/useEntityList.ts +12 -9
- package/.framework/src/hooks/useListSchema.ts +108 -58
- package/.framework/src/instructions/README.md +57 -0
- package/.framework/src/instructions/ai-agents.md +69 -0
- package/.framework/src/instructions/alerts.md +80 -0
- package/.framework/src/instructions/api-keys.md +87 -0
- package/.framework/src/instructions/apps.md +77 -0
- package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
- package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
- package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
- package/.framework/src/instructions/concepts/automation.md +186 -0
- package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
- package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
- package/.framework/src/instructions/concepts/design-schema.md +152 -0
- package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
- package/.framework/src/instructions/concepts/observability.md +116 -0
- package/.framework/src/instructions/concepts/permissions.md +150 -0
- package/.framework/src/instructions/concepts/runtime-records.md +111 -0
- package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
- package/.framework/src/instructions/concepts/spine-way.md +172 -0
- package/.framework/src/instructions/deliveries.md +59 -0
- package/.framework/src/instructions/guide/00-orientation.md +106 -0
- package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
- package/.framework/src/instructions/guide/02-project-map.md +147 -0
- package/.framework/src/instructions/guide/03-domain-model.md +223 -0
- package/.framework/src/instructions/guide/04-first-app.md +227 -0
- package/.framework/src/instructions/guide/05-glossary.md +183 -0
- package/.framework/src/instructions/integrations.md +96 -0
- package/.framework/src/instructions/internal/cil-markers.md +83 -0
- package/.framework/src/instructions/invites.md +60 -0
- package/.framework/src/instructions/kb.md +80 -0
- package/.framework/src/instructions/logs.md +77 -0
- package/.framework/src/instructions/observability.md +61 -0
- package/.framework/src/instructions/operations/deployment.md +115 -0
- package/.framework/src/instructions/operations/migrations.md +125 -0
- package/.framework/src/instructions/operations/security-checklist.md +148 -0
- package/.framework/src/instructions/operations/troubleshooting.md +170 -0
- package/.framework/src/instructions/operations/upgrade.md +116 -0
- package/.framework/src/instructions/pipeline-executions.md +62 -0
- package/.framework/src/instructions/pipelines.md +78 -0
- package/.framework/src/instructions/prompts.md +95 -0
- package/.framework/src/instructions/reference/api/admin-data.md +162 -0
- package/.framework/src/instructions/reference/api/auth.md +113 -0
- package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
- package/.framework/src/instructions/reference/app-manifest.md +150 -0
- package/.framework/src/instructions/reference/cli.md +124 -0
- package/.framework/src/instructions/reference/database.md +132 -0
- package/.framework/src/instructions/reference/design-schema.md +280 -0
- package/.framework/src/instructions/reference/env-vars.md +133 -0
- package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
- package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
- package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
- package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
- package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
- package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
- package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
- package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
- package/.framework/src/instructions/reference/functions/apps.md +140 -0
- package/.framework/src/instructions/reference/functions/auth.md +99 -0
- package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
- package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
- package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
- package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
- package/.framework/src/instructions/reference/functions/index.md +103 -0
- package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
- package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
- package/.framework/src/instructions/reference/functions/integrations.md +130 -0
- package/.framework/src/instructions/reference/functions/invites.md +182 -0
- package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
- package/.framework/src/instructions/reference/functions/kb.md +166 -0
- package/.framework/src/instructions/reference/functions/logs.md +141 -0
- package/.framework/src/instructions/reference/functions/observability.md +105 -0
- package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
- package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
- package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
- package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
- package/.framework/src/instructions/reference/functions/roles.md +121 -0
- package/.framework/src/instructions/reference/functions/security.md +140 -0
- package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
- package/.framework/src/instructions/reference/functions/system.md +106 -0
- package/.framework/src/instructions/reference/functions/tests.md +97 -0
- package/.framework/src/instructions/reference/functions/timers.md +117 -0
- package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
- package/.framework/src/instructions/reference/functions/triggers.md +140 -0
- package/.framework/src/instructions/reference/functions/types.md +146 -0
- package/.framework/src/instructions/reference/seeds.md +126 -0
- package/.framework/src/instructions/reference/shared-modules.md +127 -0
- package/.framework/src/instructions/reference/spine-config.md +114 -0
- package/.framework/src/instructions/roles.md +80 -0
- package/.framework/src/instructions/runtime-detail.md +70 -0
- package/.framework/src/instructions/runtime-list.md +77 -0
- package/.framework/src/instructions/security.md +67 -0
- package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
- package/.framework/src/instructions/tasks/add-a-role.md +96 -0
- package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
- package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
- package/.framework/src/instructions/tasks/add-a-type.md +151 -0
- package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
- package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
- package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
- package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
- package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
- package/.framework/src/instructions/tasks/test-your-code.md +108 -0
- package/.framework/src/instructions/tasks/write-a-function.md +132 -0
- package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
- package/.framework/src/instructions/testing.md +59 -0
- package/.framework/src/instructions/timers.md +82 -0
- package/.framework/src/instructions/triggers.md +69 -0
- package/.framework/src/instructions/types.md +93 -0
- package/.framework/src/lib/branding.ts +128 -0
- package/.framework/src/pages/NoAppPage.tsx +3 -1
- package/.framework/src/pages/NotFoundPage.tsx +62 -31
- package/.framework/src/pages/SetupPage.tsx +5 -2
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
- package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
- package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
- package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
- package/.framework/src/pages/admin/AppsPage.tsx +104 -66
- package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
- package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
- package/.framework/src/pages/admin/LogsPage.tsx +144 -76
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
- package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
- package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
- package/.framework/src/pages/admin/RolesPage.tsx +86 -43
- package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
- package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
- package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
- package/.framework/src/pages/admin/TimersPage.tsx +90 -62
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
- package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
- package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
- package/.framework/src/pages/admin/TypesPage.tsx +90 -48
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
- package/.framework/src/pages/auth/AuthConfirmPage.tsx +3 -1
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
- package/.framework/src/pages/auth/LoginPage.tsx +4 -2
- package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
- package/.framework/templates/agent/AGENTS.md +150 -0
- package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
- package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
- package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
- package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
- package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
- package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
- package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
- package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
- package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
- package/.framework/templates/project.json +5 -1
- package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
- package/.framework/tests/unit/agent-files.test.ts +355 -0
- package/.framework/tests/unit/agent-runner.test.ts +496 -47
- package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
- package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
- package/.framework/tests/unit/bootstrap.test.ts +242 -0
- package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
- package/.framework/tests/unit/debug-auth.test.ts +38 -0
- package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
- package/.framework/tests/unit/episode-lease.test.ts +695 -0
- package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
- package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
- package/.framework/tests/unit/kb-ingest-lease.test.ts +19 -0
- package/.framework/tests/unit/logging-app-id.test.ts +75 -0
- package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
- package/.framework/tests/unit/principal-cache.test.ts +15 -0
- package/.framework/tests/unit/system-context-denial.test.ts +145 -0
- package/.framework/tests/unit/system-cron.test.ts +240 -0
- package/.framework/tests/unit/timers.test.ts +159 -0
- package/.framework/tests/unit/trigger-engine.test.ts +278 -91
- package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
- package/README.md +125 -166
- package/config/tsconfig.cli.json +2 -2
- package/netlify.toml +5 -0
- package/package.json +15 -4
- package/templates/project/package-lock.json +2 -2
- package/.framework/docs/admin-routes.md +0 -76
- package/.framework/docs/api-docs-progress.md +0 -38
- package/.framework/docs/api-governance.md +0 -146
- package/.framework/docs/api-testing-results.md +0 -212
- package/.framework/docs/assembly-launch-guide.md +0 -138
- package/.framework/docs/audit-results.md +0 -590
- package/.framework/docs/db-api-inventory.md +0 -95
- package/.framework/docs/gap-remediation-backlog.md +0 -103
- package/.framework/docs/ui-adoption-verification.md +0 -111
- package/.framework/docs/ui-api-coverage.md +0 -84
- package/.framework/docs/v2-compatibility-audit.md +0 -228
- package/.framework/mcp/index.ts +0 -133
- package/.framework/mcp/load-env.ts +0 -60
- package/.framework/mcp/shared/app-manifest.ts +0 -275
- package/.framework/mcp/shared/db.ts +0 -477
- package/.framework/mcp/shared/kb.ts +0 -118
- package/.framework/mcp/tools/architect.ts +0 -431
- package/.framework/mcp/tools/cil/context.ts +0 -345
- package/.framework/mcp/tools/cil/graph.ts +0 -242
- package/.framework/mcp/tools/cil/ingest.ts +0 -1217
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
- package/.framework/mcp/tools/cil/pipeline.ts +0 -767
- package/.framework/mcp/tools/cil/project.ts +0 -791
- package/.framework/mcp/tools/cil/retrieval.ts +0 -336
- package/.framework/mcp/tools/cil/search-core.ts +0 -454
- package/.framework/mcp/tools/cil/search.ts +0 -97
- package/.framework/mcp/tools/cil/suggest.ts +0 -186
- package/.framework/mcp/tools/cil/validate.ts +0 -1369
- package/.framework/mcp/tools/kb/delete.ts +0 -139
- package/.framework/mcp/tools/kb/index.ts +0 -52
- package/.framework/mcp/tools/kb/ingest.ts +0 -84
- package/.framework/mcp/tools/kb/list-types.ts +0 -102
- package/.framework/mcp/tools/kb/publish.ts +0 -100
- package/.framework/mcp/tools/kb/reindex.ts +0 -86
- package/.framework/mcp/tools/kb/revoke.ts +0 -85
- package/.framework/mcp/tools/kb/search.ts +0 -101
- package/.framework/mcp/tools/kb/upsert.ts +0 -125
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"]
|