feat(workbench): persist operational workflows and events

This commit is contained in:
2026-09-24 04:19:02 +08:00
parent baff7dfe9e
commit 9335bdc1ea
66 changed files with 16580 additions and 238 deletions
@@ -0,0 +1,155 @@
-- Durable workbench event log and independent Agent execution runs.
BEGIN;
CREATE SCHEMA IF NOT EXISTS workbench;
CREATE TABLE IF NOT EXISTS workbench.domain_event_log (
event_seq bigserial PRIMARY KEY,
project_id uuid NOT NULL,
event_id uuid NOT NULL,
topic text NOT NULL CHECK (topic IN (
'operational.task.changed',
'operational.run.queued',
'operational.run.changed',
'operational.report.published',
'operational.action.changed',
'operational.notification.changed',
'workbench.alert.changed',
'workbench.work_order.changed',
'analysis.run.changed'
)),
aggregate_type text NOT NULL CHECK (length(aggregate_type) > 0),
aggregate_id text NOT NULL CHECK (length(aggregate_id) > 0),
aggregate_revision bigint NOT NULL CHECK (aggregate_revision >= 1),
schema_version integer NOT NULL DEFAULT 1 CHECK (schema_version = 1),
occurred_at timestamptz NOT NULL DEFAULT now(),
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
UNIQUE (project_id,event_id)
);
CREATE INDEX IF NOT EXISTS domain_event_log_project_cursor_idx
ON workbench.domain_event_log(project_id,event_seq);
CREATE INDEX IF NOT EXISTS domain_event_log_retention_idx
ON workbench.domain_event_log(occurred_at,event_seq);
ALTER TABLE IF EXISTS analysis.runs
ADD COLUMN IF NOT EXISTS revision bigint NOT NULL DEFAULT 1
CHECK (revision >= 1);
CREATE TABLE IF NOT EXISTS workbench.operational_task_runs (
project_id uuid NOT NULL,
run_id uuid NOT NULL,
task_id uuid NOT NULL,
generation bigint NOT NULL CHECK (generation >= 1),
execution_request_id uuid NOT NULL,
status text NOT NULL CHECK (status IN (
'queued','claimed','running','succeeded','failed','timed_out','cancelled'
)),
revision bigint NOT NULL CHECK (revision >= 1),
agent_session_id text,
lease_expires_at timestamptz,
heartbeat_at timestamptz,
started_at timestamptz,
finished_at timestamptz,
failure_code text,
failure_detail text,
dispatch_attempts integer NOT NULL DEFAULT 0 CHECK (dispatch_attempts >= 0),
next_dispatch_at timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
PRIMARY KEY (project_id,run_id),
UNIQUE (project_id,task_id,generation),
UNIQUE (project_id,execution_request_id),
FOREIGN KEY (project_id,task_id)
REFERENCES workbench.operational_tasks(project_id,task_id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS operational_task_runs_one_active_idx
ON workbench.operational_task_runs(project_id,task_id)
WHERE status IN ('queued','claimed','running');
CREATE INDEX IF NOT EXISTS operational_task_runs_queue_idx
ON workbench.operational_task_runs(
project_id,next_dispatch_at,created_at,run_id
) WHERE status='queued';
CREATE INDEX IF NOT EXISTS operational_task_runs_lease_idx
ON workbench.operational_task_runs(project_id,lease_expires_at,run_id)
WHERE status IN ('claimed','running') AND lease_expires_at IS NOT NULL;
ALTER TABLE workbench.operational_tasks
ADD COLUMN IF NOT EXISTS lifecycle_status text;
UPDATE workbench.operational_tasks
SET lifecycle_status = CASE status
WHEN 'waiting_human' THEN 'waiting_human'
WHEN 'completed' THEN 'completed'
WHEN 'failed' THEN 'failed'
WHEN 'cancelled' THEN 'cancelled'
ELSE 'open'
END
WHERE lifecycle_status IS NULL;
UPDATE workbench.operational_tasks
SET document = document || jsonb_build_object('lifecycle_status', lifecycle_status)
WHERE document->>'lifecycle_status' IS DISTINCT FROM lifecycle_status;
ALTER TABLE workbench.operational_tasks
ALTER COLUMN lifecycle_status SET NOT NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid='workbench.operational_tasks'::regclass
AND conname='operational_tasks_lifecycle_status_check'
) THEN
ALTER TABLE workbench.operational_tasks
ADD CONSTRAINT operational_tasks_lifecycle_status_check
CHECK (lifecycle_status IN
('open','waiting_human','completed','failed','cancelled'));
END IF;
END $$;
-- Deterministically preserve the currently active or last known execution as
-- generation history. uuid values are derived without requiring an extension.
INSERT INTO workbench.operational_task_runs (
project_id,run_id,task_id,generation,execution_request_id,status,revision,
agent_session_id,lease_expires_at,heartbeat_at,started_at,finished_at,
failure_code,failure_detail,dispatch_attempts,next_dispatch_at,created_at,updated_at
)
SELECT
project_id,
(substr(md5(project_id::text || ':' || task_id::text || ':' || execution_generation::text),1,8)
|| '-' || substr(md5(project_id::text || ':' || task_id::text || ':' || execution_generation::text),9,4)
|| '-5' || substr(md5(project_id::text || ':' || task_id::text || ':' || execution_generation::text),14,3)
|| '-a' || substr(md5(project_id::text || ':' || task_id::text || ':' || execution_generation::text),18,3)
|| '-' || substr(md5(project_id::text || ':' || task_id::text || ':' || execution_generation::text),21,12))::uuid,
task_id,
execution_generation,
execution_request_id,
CASE status
WHEN 'queued' THEN 'queued'
WHEN 'running' THEN 'running'
WHEN 'waiting_human' THEN 'succeeded'
WHEN 'completed' THEN 'succeeded'
WHEN 'failed' THEN 'failed'
WHEN 'cancelled' THEN 'cancelled'
END,
GREATEST(revision,1),
agent_session_id,
agent_lease_expires_at,
CASE WHEN status='running' THEN updated_at ELSE NULL END,
CASE WHEN status IN ('running','waiting_human','completed','failed','cancelled')
THEN created_at ELSE NULL END,
CASE WHEN status IN ('waiting_human','completed','failed','cancelled')
THEN updated_at ELSE NULL END,
CASE WHEN status='failed' THEN 'MIGRATED_FAILURE' ELSE NULL END,
NULL,
0,
created_at,
created_at,
updated_at
FROM workbench.operational_tasks
ON CONFLICT (project_id,task_id,generation) DO NOTHING;
COMMIT;