-- 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;