Third commit of the Research + Loops arc. Lights up loops as durable
recurring topology executions:
GET /api/loops list workspace's loops
POST /api/loops create — returns webhook_token +
signing_key ONCE when webhook trigger
is enabled; never exposed again
GET /api/loops/:id detail
PATCH /api/loops/:id update definition
DELETE /api/loops/:id delete
POST /api/loops/:id/run trigger one iteration NOW
POST /api/loops/:id/enable set enabled=true
POST /api/loops/:id/disable set enabled=false
POST /webhooks/loops/:token public; HMAC-SHA256-verified
Scheduler (cm_runtime::spawn_loop_scheduler) wakes every 10s, queries the
partial index on (next_fire_at) for due loops, enqueues one topology_runs
row per fire with loop_id + iteration + parent_run_id chained back to the
previous iteration. Uses croner via the existing scheduling::next_occurrence
helper. Missed windows fire ONCE and skip the backlog — next_fire_at is
always computed strictly AFTER now(), so a late scheduler doesn't drain a
buildup.
Webhook signatures follow the same pattern as the Stripe billing webhook
(HMAC-SHA256 with constant-time hex compare). Token + signing key are
24-byte OS-RNG values; the URL uses base64-url for the token, and the
signing key is base64-std. Both surface exactly once at create time.
All three fire paths (scheduler, immediate-run, webhook) funnel through
`cm_db::repo::loops::enqueue_iteration` so the invariants stay in one
place. `iters` repeat policy is enforced by the scheduler tick; `until`
and `on_completion` land with the orchestrator hook in commit 4.
Adds cm-llm as a direct cm-api dep, getrandom for the webhook material
generator, and wires the scheduler spawn into the server binary alongside
the resume sweeper and outbox drainer.
65 lines
1.3 KiB
JSON
65 lines
1.3 KiB
JSON
{
|
|
"db_name": "PostgreSQL",
|
|
"query": "SELECT id, workspace_id, graph, task_template, triggers, repeat_policy,\n last_run_id, webhook_signing_key\n FROM loops\n WHERE webhook_token = $1 AND enabled",
|
|
"describe": {
|
|
"columns": [
|
|
{
|
|
"ordinal": 0,
|
|
"name": "id",
|
|
"type_info": "Uuid"
|
|
},
|
|
{
|
|
"ordinal": 1,
|
|
"name": "workspace_id",
|
|
"type_info": "Uuid"
|
|
},
|
|
{
|
|
"ordinal": 2,
|
|
"name": "graph",
|
|
"type_info": "Jsonb"
|
|
},
|
|
{
|
|
"ordinal": 3,
|
|
"name": "task_template",
|
|
"type_info": "Text"
|
|
},
|
|
{
|
|
"ordinal": 4,
|
|
"name": "triggers",
|
|
"type_info": "Jsonb"
|
|
},
|
|
{
|
|
"ordinal": 5,
|
|
"name": "repeat_policy",
|
|
"type_info": "Jsonb"
|
|
},
|
|
{
|
|
"ordinal": 6,
|
|
"name": "last_run_id",
|
|
"type_info": "Uuid"
|
|
},
|
|
{
|
|
"ordinal": 7,
|
|
"name": "webhook_signing_key",
|
|
"type_info": "Text"
|
|
}
|
|
],
|
|
"parameters": {
|
|
"Left": [
|
|
"Text"
|
|
]
|
|
},
|
|
"nullable": [
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
true,
|
|
true
|
|
]
|
|
},
|
|
"hash": "be08a19d993480156d961cb170633e0133d802db02e1d8a10fa1f8ccac819d18"
|
|
}
|