First commit of the Research + Loops feature arc. Schema only — routes,
runtime hooks, and the UI arrive in subsequent commits.
0030 — research_topics
Container entity with a small state machine (standby → processing →
reviewing → publishing → published). Owns runs via a nullable
topology_runs.research_topic_id FK, so all existing SSE/audit/gated-
approval plumbing surfaces without changes. Many-to-many join table
captures the role_slot the wizard assigns each agent ("lead", "critic",
"writer") so the canvas can group avatars sensibly.
0031 — loops
Durable recurring topology execution. graph + task_template pair with a
triggers JSONB (any of cron / on_completion / webhook, all can be on
simultaneously) and a repeat_policy (infinite / N iters / until). Every
iteration writes a topology_runs row with loop_id, iteration (1-indexed),
and parent_run_id chained back to N-1 — cross-iteration context comes
from that hop, no extra state store needed. Webhook auth is HMAC-SHA256
keyed by webhook_signing_key. Missed cron windows fire once and skip the
backlog (see comment header).
Both migrations only ADD tables/columns and use ON DELETE SET NULL for the
back-refs, so they're safe to run against prod without downtime. The
existing indexes on topology_runs keep serving legacy (non-research,
non-loop) runs unchanged.
Publish approval extension (approvals.kind for the reviewing → publishing
gate) comes as a separate migration in the publish-gate commit.
72 lines
3.7 KiB
SQL
72 lines
3.7 KiB
SQL
-- Loops: durable recurring topology executions. A loop pairs a stored
|
|
-- topology graph + task template with a trigger configuration (any of
|
|
-- cron / on_completion / webhook) and a repeat policy (infinite / N iters /
|
|
-- until condition). Every iteration writes a normal topology_runs row with
|
|
-- loop_id + iteration + parent_run_id set so the existing SSE observer,
|
|
-- audit log, and gated approvals surface transparently.
|
|
--
|
|
-- Cross-iteration state: iteration N+1's run driver reads parent_run_id and
|
|
-- injects the parent's final assistant message into the first system prompt
|
|
-- as prior-iteration context (refine-over-time by default).
|
|
--
|
|
-- Missed schedules: if the loop scheduler wakes and finds next_fire_at in
|
|
-- the past (a restart or long stall), it fires ONCE and computes the next
|
|
-- fire from the current cron expression. Missed backlog is not drained.
|
|
|
|
CREATE TABLE loops (
|
|
id UUID PRIMARY KEY,
|
|
workspace_id UUID NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL,
|
|
description TEXT NOT NULL,
|
|
-- Same shape as topology_runs.graph: JSON-encoded agent/node connectivity
|
|
-- from the wizard's topology builder (or a template pick).
|
|
graph JSONB NOT NULL,
|
|
-- Text prompt used as the task for every iteration. Templating is out of
|
|
-- scope for v1 — the same string runs each time (with prior-iteration
|
|
-- output prepended by the run driver for refine-over-time).
|
|
task_template TEXT NOT NULL,
|
|
-- {cron?: '0 */6 * * *', on_completion?: bool, webhook_enabled?: bool}
|
|
-- All three can be enabled simultaneously; whichever fires first wins.
|
|
triggers JSONB NOT NULL DEFAULT '{}'::JSONB,
|
|
-- {kind: 'infinite' | 'iters' | 'until', spec: <kind-specific>}
|
|
-- iters: {n: 10} — stop after N iterations
|
|
-- until: {event: 'ok', within_iters: 20} — stop when N events match
|
|
repeat_policy JSONB NOT NULL DEFAULT '{"kind":"infinite"}'::JSONB,
|
|
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
-- Computed by the scheduler from `triggers.cron` after each fire. NULL
|
|
-- when no cron trigger is set (loop runs only on webhook / on_completion
|
|
-- / manual).
|
|
next_fire_at TIMESTAMPTZ,
|
|
last_run_id UUID REFERENCES topology_runs (id) ON DELETE SET NULL,
|
|
-- Opaque secret ID for the webhook endpoint. HMAC-SHA256 verification of
|
|
-- the request body uses webhook_signing_key. Both are NULL when the
|
|
-- webhook trigger is disabled; both are set when it's on. Rotation:
|
|
-- disable trigger, re-enable to regenerate both.
|
|
webhook_token TEXT UNIQUE,
|
|
webhook_signing_key TEXT,
|
|
created_by UUID NOT NULL REFERENCES users (id) ON DELETE RESTRICT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX loops_workspace_idx
|
|
ON loops (workspace_id, updated_at DESC);
|
|
-- Scheduler tick query: SELECT ... WHERE enabled AND next_fire_at <= now().
|
|
-- Partial index keeps the scanner tight; disabled + no-cron loops stay out.
|
|
CREATE INDEX loops_next_fire_idx
|
|
ON loops (next_fire_at)
|
|
WHERE enabled AND next_fire_at IS NOT NULL;
|
|
|
|
-- Back-refs from durable runs. loop_id groups all iterations of a loop;
|
|
-- iteration is 1-indexed per loop; parent_run_id chains iteration N+1 back
|
|
-- to N so the run driver can pull prior context in one hop.
|
|
ALTER TABLE topology_runs
|
|
ADD COLUMN loop_id UUID REFERENCES loops (id) ON DELETE SET NULL,
|
|
ADD COLUMN iteration INT,
|
|
ADD COLUMN parent_run_id UUID REFERENCES topology_runs (id) ON DELETE SET NULL;
|
|
|
|
-- Listing "recent iterations of loop X" hits this.
|
|
CREATE INDEX topology_runs_loop_idx
|
|
ON topology_runs (loop_id, iteration DESC)
|
|
WHERE loop_id IS NOT NULL;
|