Migrating to version 111 (91 migrations in total): -- migrating version 001 -> CREATE EXTENSION IF NOT EXISTS vector; -> CREATE EXTENSION IF NOT EXISTS timescaledb; -> CREATE EXTENSION IF NOT EXISTS pg_trgm; -> CREATE TABLE organizations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, plan TEXT NOT NULL DEFAULT 'oss', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE INDEX idx_organizations_slug ON organizations (slug); -> INSERT INTO organizations (id, name, slug, plan, created_at, updated_at) VALUES ( '00000000-0000-0000-0000-000000000000', 'Default', 'default', 'oss', now(), now() ) ON CONFLICT DO NOTHING; -> CREATE TABLE agents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agent_id TEXT NOT NULL, name TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'agent' CHECK (role IN ('platform_admin', 'org_owner', 'admin', 'agent', 'reader')), api_key_hash TEXT, metadata JSONB NOT NULL DEFAULT '{}', org_id UUID NOT NULL REFERENCES organizations(id), tags TEXT[] NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE UNIQUE INDEX idx_agents_org_agent ON agents (org_id, agent_id); -> CREATE INDEX idx_agents_agent_id_global ON agents (agent_id); -> CREATE INDEX idx_agents_metadata ON agents USING GIN (metadata); -> CREATE INDEX idx_agents_tags ON agents USING GIN (tags); -> CREATE TABLE agent_runs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agent_id TEXT NOT NULL, trace_id TEXT, parent_run_id UUID REFERENCES agent_runs(id), status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed')), started_at TIMESTAMPTZ NOT NULL DEFAULT now(), completed_at TIMESTAMPTZ, metadata JSONB NOT NULL DEFAULT '{}', org_id UUID NOT NULL REFERENCES organizations(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE INDEX idx_agent_runs_agent_id ON agent_runs(agent_id); -> CREATE INDEX idx_agent_runs_trace_id ON agent_runs(trace_id) WHERE trace_id IS NOT NULL; -> CREATE INDEX idx_agent_runs_status ON agent_runs(status); -> CREATE INDEX idx_agent_runs_started_at ON agent_runs(started_at DESC); -> CREATE INDEX idx_agent_runs_org ON agent_runs (org_id, id); -> CREATE INDEX idx_agent_runs_parent_run ON agent_runs (parent_run_id) WHERE parent_run_id IS NOT NULL; -> CREATE TABLE decisions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), run_id UUID NOT NULL REFERENCES agent_runs(id), agent_id TEXT NOT NULL, decision_type TEXT NOT NULL, outcome TEXT NOT NULL, confidence REAL NOT NULL CHECK (confidence >= 0.0 AND confidence <= 1.0), reasoning TEXT, embedding vector(1024), metadata JSONB NOT NULL DEFAULT '{}', valid_from TIMESTAMPTZ NOT NULL DEFAULT now(), valid_to TIMESTAMPTZ, transaction_time TIMESTAMPTZ NOT NULL DEFAULT now(), quality_score REAL DEFAULT 0.0, precedent_ref UUID REFERENCES decisions(id), supersedes_id UUID REFERENCES decisions(id), content_hash TEXT, org_id UUID NOT NULL REFERENCES organizations(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE INDEX idx_decisions_agent_id ON decisions(agent_id, valid_from DESC); -> CREATE INDEX idx_decisions_run_id ON decisions(run_id); -> CREATE INDEX idx_decisions_type ON decisions(decision_type, valid_from DESC); -> CREATE INDEX idx_decisions_confidence ON decisions(confidence DESC); -> CREATE INDEX idx_decisions_metadata ON decisions USING GIN (metadata); -> CREATE INDEX idx_decisions_quality ON decisions (quality_score DESC); -> CREATE INDEX idx_decisions_type_quality ON decisions (decision_type, quality_score DESC); -> CREATE INDEX idx_decisions_precedent_ref ON decisions (precedent_ref) WHERE precedent_ref IS NOT NULL; -> CREATE INDEX idx_decisions_temporal ON decisions(transaction_time, valid_from DESC) WHERE valid_to IS NULL; -> CREATE INDEX idx_decisions_check_order ON decisions(decision_type, valid_from DESC, quality_score DESC) WHERE valid_to IS NULL; -> CREATE INDEX idx_decisions_org_agent_current ON decisions (org_id, agent_id, valid_from DESC) WHERE valid_to IS NULL; -> CREATE INDEX idx_decisions_org_type ON decisions (org_id, decision_type, valid_from DESC); -> CREATE INDEX idx_decisions_temporal_historical ON decisions (org_id, transaction_time, valid_to) WHERE valid_to IS NOT NULL; -> CREATE INDEX idx_decisions_supersedes ON decisions(supersedes_id) WHERE supersedes_id IS NOT NULL; -> CREATE INDEX idx_decisions_content_hash ON decisions(content_hash) WHERE content_hash IS NOT NULL; -> CREATE INDEX idx_decisions_embedding ON decisions USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); -> CREATE INDEX idx_decisions_outcome_trgm ON decisions USING GIN (outcome gin_trgm_ops); -> CREATE INDEX idx_decisions_type_trgm ON decisions USING GIN (decision_type gin_trgm_ops); -> CREATE TABLE alternatives ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), decision_id UUID NOT NULL REFERENCES decisions(id), label TEXT NOT NULL, score REAL, selected BOOLEAN NOT NULL DEFAULT false, rejection_reason TEXT, metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE INDEX idx_alternatives_decision_id ON alternatives(decision_id); -> CREATE INDEX idx_alternatives_selected ON alternatives(decision_id) WHERE selected = true; -> CREATE TABLE evidence ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), decision_id UUID NOT NULL REFERENCES decisions(id), source_type TEXT NOT NULL CHECK (source_type ~ '^[a-z][a-z0-9_]*$'), source_uri TEXT, content TEXT NOT NULL, relevance_score REAL, embedding vector(1024), metadata JSONB NOT NULL DEFAULT '{}', org_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE INDEX idx_evidence_decision_id ON evidence(decision_id); -> CREATE INDEX idx_evidence_source_type ON evidence(source_type); -> CREATE INDEX idx_evidence_embedding ON evidence USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); -> CREATE INDEX idx_evidence_org ON evidence (org_id, decision_id); -> CREATE TABLE agent_events ( id UUID NOT NULL DEFAULT gen_random_uuid(), run_id UUID NOT NULL, event_type TEXT NOT NULL, sequence_num BIGINT NOT NULL, occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), agent_id TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}', org_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (id, occurred_at) ); -> SELECT create_hypertable('agent_events', 'occurred_at', if_not_exists => TRUE); -> SELECT set_chunk_time_interval('agent_events', INTERVAL '1 day'); -> ALTER TABLE agent_events SET ( timescaledb.compress, timescaledb.compress_segmentby = 'agent_id,run_id', timescaledb.compress_orderby = 'occurred_at DESC' ); -> SELECT add_compression_policy('agent_events', INTERVAL '7 days'); -> CREATE INDEX idx_agent_events_run_id ON agent_events(run_id, sequence_num); -> CREATE INDEX idx_agent_events_type ON agent_events(event_type, occurred_at DESC); -> CREATE INDEX idx_agent_events_agent_id ON agent_events(agent_id, occurred_at DESC); -> CREATE INDEX idx_agent_events_payload ON agent_events USING GIN (payload); -> CREATE INDEX idx_agent_events_org ON agent_events (org_id); -> CREATE INDEX idx_agent_events_org_type ON agent_events (org_id, event_type, occurred_at DESC); -> CREATE UNIQUE INDEX idx_agent_events_run_seq_unique ON agent_events(run_id, sequence_num, occurred_at); -> CREATE SEQUENCE event_sequence_num_seq START WITH 1; -> CREATE TABLE access_grants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), grantor_id UUID NOT NULL REFERENCES agents(id), grantee_id UUID NOT NULL REFERENCES agents(id), resource_type TEXT NOT NULL CHECK (resource_type IN ('agent_traces', 'decision', 'run')), resource_id TEXT, permission TEXT NOT NULL CHECK (permission IN ('read', 'write')), granted_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ, org_id UUID NOT NULL REFERENCES organizations(id), UNIQUE(grantee_id, resource_type, resource_id, permission) ); -> CREATE INDEX idx_access_grants_grantee ON access_grants(grantee_id, resource_type); -> CREATE INDEX idx_access_grants_org ON access_grants (org_id, grantee_id); -> CREATE INDEX idx_access_grants_expires ON access_grants (expires_at) WHERE expires_at IS NOT NULL; -> CREATE TABLE search_outbox ( id BIGSERIAL PRIMARY KEY, decision_id UUID NOT NULL, org_id UUID NOT NULL, operation TEXT NOT NULL CHECK (operation IN ('upsert', 'delete')), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), attempts INT NOT NULL DEFAULT 0, last_error TEXT, locked_until TIMESTAMPTZ ); -> CREATE INDEX idx_search_outbox_pending ON search_outbox (created_at ASC) WHERE locked_until IS NULL; -> CREATE UNIQUE INDEX idx_search_outbox_decision_op ON search_outbox (decision_id, operation); -> CREATE INDEX idx_search_outbox_org ON search_outbox (org_id); -> CREATE TABLE integrity_proofs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES organizations(id), batch_start TIMESTAMPTZ NOT NULL, batch_end TIMESTAMPTZ NOT NULL, decision_count INTEGER NOT NULL, root_hash TEXT NOT NULL, previous_root TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE INDEX idx_integrity_proofs_org_time ON integrity_proofs(org_id, created_at DESC); -> CREATE VIEW current_decisions AS SELECT * FROM decisions WHERE valid_to IS NULL ORDER BY valid_from DESC; -> CREATE MATERIALIZED VIEW decision_conflicts AS SELECT d1.id AS decision_a_id, d2.id AS decision_b_id, d1.org_id, d1.agent_id AS agent_a, d2.agent_id AS agent_b, d1.run_id AS run_a, d2.run_id AS run_b, d1.decision_type, d1.outcome AS outcome_a, d2.outcome AS outcome_b, d1.confidence AS confidence_a, d2.confidence AS confidence_b, d1.valid_from AS decided_at_a, d2.valid_from AS decided_at_b, GREATEST(d1.valid_from, d2.valid_from) AS detected_at FROM decisions d1 JOIN decisions d2 ON d1.decision_type = d2.decision_type AND d1.org_id = d2.org_id AND d1.agent_id != d2.agent_id AND d1.outcome != d2.outcome AND d1.valid_to IS NULL AND d2.valid_to IS NULL AND d1.id < d2.id AND ABS(EXTRACT(EPOCH FROM (d1.valid_from - d2.valid_from))) < 3600 WITH DATA; -> CREATE UNIQUE INDEX idx_decision_conflicts_pair ON decision_conflicts(decision_a_id, decision_b_id); -> CREATE MATERIALIZED VIEW agent_current_state AS WITH latest_runs AS ( SELECT DISTINCT ON (agent_id, org_id) id, agent_id, org_id, status, started_at FROM agent_runs ORDER BY agent_id, org_id, started_at DESC ), decision_counts AS ( SELECT agent_id, org_id, COUNT(*) AS active_decisions FROM decisions WHERE valid_to IS NULL GROUP BY agent_id, org_id ), event_stats AS ( SELECT run_id, COUNT(id) AS event_count, MAX(occurred_at) AS last_activity FROM agent_events GROUP BY run_id ) SELECT lr.agent_id, lr.org_id, lr.id AS latest_run_id, lr.status AS run_status, lr.started_at, COALESCE(es.event_count, 0) AS event_count, es.last_activity, COALESCE(dc.active_decisions, 0) AS active_decisions FROM latest_runs lr LEFT JOIN event_stats es ON es.run_id = lr.id LEFT JOIN decision_counts dc ON dc.agent_id = lr.agent_id AND dc.org_id = lr.org_id WITH DATA; -> CREATE UNIQUE INDEX idx_agent_current_state_agent_org ON agent_current_state (agent_id, org_id); -- ok (223.844845ms) -- migrating version 022 -> ALTER TABLE decisions ADD COLUMN IF NOT EXISTS search_vector tsvector; -> CREATE OR REPLACE FUNCTION decisions_search_vector_update() RETURNS trigger AS $$ BEGIN NEW.search_vector := setweight(to_tsvector('english', COALESCE(NEW.outcome, '')), 'A') || setweight(to_tsvector('english', COALESCE(NEW.decision_type, '')), 'B') || setweight(to_tsvector('english', COALESCE(NEW.reasoning, '')), 'C'); RETURN NEW; END $$ LANGUAGE plpgsql; -> DROP TRIGGER IF EXISTS decisions_search_vector_trigger ON decisions; -> CREATE TRIGGER decisions_search_vector_trigger BEFORE INSERT OR UPDATE OF outcome, decision_type, reasoning ON decisions FOR EACH ROW EXECUTE FUNCTION decisions_search_vector_update(); -> DO $$ DECLARE batch_count int; BEGIN LOOP WITH batch AS ( SELECT id, outcome, decision_type, reasoning FROM decisions WHERE search_vector IS NULL LIMIT 10000 ) UPDATE decisions d SET search_vector = setweight(to_tsvector('english', COALESCE(b.outcome, '')), 'A') || setweight(to_tsvector('english', COALESCE(b.decision_type, '')), 'B') || setweight(to_tsvector('english', COALESCE(b.reasoning, '')), 'C') FROM batch b WHERE d.id = b.id; GET DIAGNOSTICS batch_count = ROW_COUNT; EXIT WHEN batch_count = 0; END LOOP; END $$; -> CREATE INDEX IF NOT EXISTS idx_decisions_search_vector ON decisions USING gin(search_vector); -- ok (15.487949ms) -- migrating version 023 -> DROP INDEX IF EXISTS idx_search_outbox_pending; -> CREATE INDEX idx_search_outbox_pending ON search_outbox (created_at ASC) WHERE attempts < 10; -- ok (4.669757ms) -- migrating version 024 -> ALTER TABLE decisions ADD COLUMN IF NOT EXISTS session_id UUID; -> ALTER TABLE decisions ADD COLUMN IF NOT EXISTS agent_context JSONB NOT NULL DEFAULT '{}'; -> CREATE INDEX idx_decisions_session ON decisions (session_id, valid_from DESC) WHERE session_id IS NOT NULL; -> CREATE INDEX idx_decisions_context_tool ON decisions USING btree ((agent_context->>'tool')) WHERE agent_context->>'tool' IS NOT NULL; -> CREATE INDEX idx_decisions_context_model ON decisions USING btree ((agent_context->>'model')) WHERE agent_context->>'model' IS NOT NULL; -> CREATE INDEX idx_decisions_context_repo ON decisions USING btree ((agent_context->>'repo')) WHERE agent_context->>'repo' IS NOT NULL; -- ok (11.181258ms) -- migrating version 025 -> CREATE INDEX IF NOT EXISTS idx_decisions_org_current_time ON decisions (org_id, valid_from DESC) WHERE valid_to IS NULL; -> CREATE INDEX IF NOT EXISTS idx_decisions_org_export ON decisions (org_id, valid_from ASC, id ASC) WHERE valid_to IS NULL; -> CREATE TABLE IF NOT EXISTS evidence_orphans ( LIKE evidence INCLUDING DEFAULTS INCLUDING GENERATED INCLUDING IDENTITY INCLUDING STORAGE INCLUDING COMMENTS, PRIMARY KEY (id), archived_at timestamptz NOT NULL DEFAULT now(), archive_reason text NOT NULL DEFAULT 'orphan org_id before fk_evidence_org validation' ); -> INSERT INTO evidence_orphans SELECT e.*, now() AS archived_at, 'orphan org_id before fk_evidence_org validation' AS archive_reason FROM evidence e LEFT JOIN organizations o ON o.id = e.org_id WHERE o.id IS NULL ON CONFLICT (id) DO NOTHING; -> DELETE FROM evidence e USING evidence_orphans a WHERE e.id = a.id; -> ALTER TABLE evidence ADD CONSTRAINT fk_evidence_org FOREIGN KEY (org_id) REFERENCES organizations(id) NOT VALID; -> ALTER TABLE evidence VALIDATE CONSTRAINT fk_evidence_org; -> COMMENT ON VIEW current_decisions IS 'Current (non-superseded) decisions. Order is not guaranteed when used in subqueries; add ORDER BY to the outer query if ordering is required.'; -- ok (17.637582ms) -- migrating version 026 -> DROP MATERIALIZED VIEW IF EXISTS decision_conflicts; -> CREATE MATERIALIZED VIEW decision_conflicts AS -- Cross-agent: different agents, same (normalized) type, different (normalized) outcomes, -- both current, within 1 hour. Time window keeps conflicts "in play" together. SELECT 'cross_agent'::TEXT AS conflict_kind, d1.id AS decision_a_id, d2.id AS decision_b_id, d1.org_id, d1.agent_id AS agent_a, d2.agent_id AS agent_b, d1.run_id AS run_a, d2.run_id AS run_b, d1.decision_type, d1.outcome AS outcome_a, d2.outcome AS outcome_b, d1.confidence AS confidence_a, d2.confidence AS confidence_b, d1.valid_from AS decided_at_a, d2.valid_from AS decided_at_b, GREATEST(d1.valid_from, d2.valid_from) AS detected_at FROM decisions d1 JOIN decisions d2 ON LOWER(TRIM(d1.decision_type)) = LOWER(TRIM(d2.decision_type)) AND d1.org_id = d2.org_id AND d1.agent_id != d2.agent_id AND LOWER(TRIM(d1.outcome)) != LOWER(TRIM(d2.outcome)) AND d1.valid_to IS NULL AND d2.valid_to IS NULL AND d1.id < d2.id AND ABS(EXTRACT(EPOCH FROM (d1.valid_from - d2.valid_from))) < 3600 UNION ALL -- Self-contradiction: same agent, same type, different outcomes, both current, -- within 7 days. Flags agents who said X then Y without revising. SELECT 'self_contradiction'::TEXT AS conflict_kind, d1.id AS decision_a_id, d2.id AS decision_b_id, d1.org_id, d1.agent_id AS agent_a, d2.agent_id AS agent_b, d1.run_id AS run_a, d2.run_id AS run_b, d1.decision_type, d1.outcome AS outcome_a, d2.outcome AS outcome_b, d1.confidence AS confidence_a, d2.confidence AS confidence_b, d1.valid_from AS decided_at_a, d2.valid_from AS decided_at_b, GREATEST(d1.valid_from, d2.valid_from) AS detected_at FROM decisions d1 JOIN decisions d2 ON LOWER(TRIM(d1.decision_type)) = LOWER(TRIM(d2.decision_type)) AND d1.org_id = d2.org_id AND d1.agent_id = d2.agent_id AND LOWER(TRIM(d1.outcome)) != LOWER(TRIM(d2.outcome)) AND d1.valid_to IS NULL AND d2.valid_to IS NULL AND d1.id < d2.id AND ABS(EXTRACT(EPOCH FROM (d1.valid_from - d2.valid_from))) < 604800 -- 7 days WITH DATA; -> CREATE UNIQUE INDEX idx_decision_conflicts_pair ON decision_conflicts(decision_a_id, decision_b_id); -- ok (18.310923ms) -- migrating version 027 -> ALTER TABLE decisions ADD COLUMN IF NOT EXISTS outcome_embedding vector(1024); -> CREATE INDEX IF NOT EXISTS idx_decisions_outcome_embedding ON decisions USING hnsw (outcome_embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64) WHERE outcome_embedding IS NOT NULL; -> CREATE TABLE IF NOT EXISTS scored_conflicts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), decision_a_id UUID NOT NULL REFERENCES decisions(id), decision_b_id UUID NOT NULL REFERENCES decisions(id), org_id UUID NOT NULL REFERENCES organizations(id), conflict_kind TEXT NOT NULL CHECK (conflict_kind IN ('cross_agent', 'self_contradiction')), agent_a TEXT NOT NULL, agent_b TEXT NOT NULL, decision_type_a TEXT NOT NULL, decision_type_b TEXT NOT NULL, outcome_a TEXT NOT NULL, outcome_b TEXT NOT NULL, topic_similarity REAL NOT NULL, outcome_divergence REAL NOT NULL, significance REAL NOT NULL, scoring_method TEXT NOT NULL DEFAULT 'embedding' CHECK (scoring_method IN ('embedding', 'text')), detected_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(decision_a_id, decision_b_id) ); -> CREATE INDEX IF NOT EXISTS idx_scored_conflicts_org_sig ON scored_conflicts(org_id, significance DESC); -> CREATE INDEX IF NOT EXISTS idx_scored_conflicts_detected ON scored_conflicts(org_id, detected_at DESC); -> CREATE INDEX IF NOT EXISTS idx_scored_conflicts_org ON scored_conflicts(org_id); -> DROP MATERIALIZED VIEW IF EXISTS decision_conflicts; -- ok (22.886602ms) -- migrating version 028 -> CREATE TABLE IF NOT EXISTS idempotency_keys ( org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, agent_id TEXT NOT NULL, endpoint TEXT NOT NULL, idempotency_key TEXT NOT NULL, request_hash TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('in_progress', 'completed')), status_code INTEGER, response_data JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (org_id, agent_id, endpoint, idempotency_key) ); -> CREATE INDEX IF NOT EXISTS idx_idempotency_keys_created_at ON idempotency_keys (created_at); -- ok (8.138914ms) -- migrating version 029 -> CREATE TABLE IF NOT EXISTS search_outbox_dead_letters ( outbox_id BIGINT PRIMARY KEY, decision_id UUID NOT NULL, org_id UUID NOT NULL, operation TEXT NOT NULL CHECK (operation IN ('upsert', 'delete')), attempts INT NOT NULL, last_error TEXT, created_at TIMESTAMPTZ NOT NULL, locked_until TIMESTAMPTZ, archived_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE INDEX IF NOT EXISTS idx_search_outbox_dead_letters_org_created ON search_outbox_dead_letters (org_id, created_at DESC); -> CREATE INDEX IF NOT EXISTS idx_search_outbox_dead_letters_archived ON search_outbox_dead_letters (archived_at DESC); -> CREATE TABLE IF NOT EXISTS deletion_audit_log ( id BIGSERIAL PRIMARY KEY, org_id UUID NOT NULL, agent_id TEXT NOT NULL, table_name TEXT NOT NULL, record_id TEXT NOT NULL, record_data JSONB NOT NULL, deleted_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE INDEX IF NOT EXISTS idx_deletion_audit_log_org_deleted ON deletion_audit_log (org_id, deleted_at DESC); -> CREATE INDEX IF NOT EXISTS idx_deletion_audit_log_agent_deleted ON deletion_audit_log (org_id, agent_id, deleted_at DESC); -> CREATE INDEX IF NOT EXISTS idx_deletion_audit_log_table ON deletion_audit_log (table_name, deleted_at DESC); -- ok (18.47486ms) -- migrating version 030 -> CREATE OR REPLACE FUNCTION check_agent_events_run_exists() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM agent_runs r WHERE r.id = NEW.run_id AND r.org_id = NEW.org_id AND r.agent_id = NEW.agent_id ) THEN RAISE EXCEPTION 'agent_events references missing run_id/org_id/agent_id (%/%/%)', NEW.run_id, NEW.org_id, NEW.agent_id; END IF; RETURN NEW; END; $$; -> DROP TRIGGER IF EXISTS trg_agent_events_run_exists ON agent_events; -> CREATE TRIGGER trg_agent_events_run_exists BEFORE INSERT ON agent_events FOR EACH ROW EXECUTE FUNCTION check_agent_events_run_exists(); -> CREATE OR REPLACE FUNCTION prevent_run_delete_with_events() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF EXISTS ( SELECT 1 FROM agent_events e WHERE e.run_id = OLD.id AND e.org_id = OLD.org_id ) THEN RAISE EXCEPTION 'cannot delete run % while agent_events still exist', OLD.id; END IF; RETURN OLD; END; $$; -> DROP TRIGGER IF EXISTS trg_prevent_run_delete_with_events ON agent_runs; -> CREATE TRIGGER trg_prevent_run_delete_with_events BEFORE DELETE ON agent_runs FOR EACH ROW EXECUTE FUNCTION prevent_run_delete_with_events(); -- ok (6.644552ms) -- migrating version 031 -> CREATE TABLE IF NOT EXISTS mutation_audit_log ( id BIGSERIAL PRIMARY KEY, occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), request_id TEXT NOT NULL, org_id UUID NOT NULL, actor_agent_id TEXT NOT NULL, actor_role TEXT NOT NULL, http_method TEXT NOT NULL, endpoint TEXT NOT NULL, operation TEXT NOT NULL, resource_type TEXT NOT NULL, resource_id TEXT NOT NULL, before_data JSONB, after_data JSONB, metadata JSONB NOT NULL DEFAULT '{}' ); -> CREATE INDEX IF NOT EXISTS idx_mutation_audit_log_org_time ON mutation_audit_log (org_id, occurred_at DESC); -> CREATE INDEX IF NOT EXISTS idx_mutation_audit_log_request ON mutation_audit_log (request_id); -> CREATE INDEX IF NOT EXISTS idx_mutation_audit_log_resource ON mutation_audit_log (resource_type, resource_id, occurred_at DESC); -> CREATE OR REPLACE FUNCTION mutation_audit_log_immutable_guard() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'mutation_audit_log is append-only'; END; $$; -> DROP TRIGGER IF EXISTS trg_mutation_audit_log_no_update ON mutation_audit_log; -> CREATE TRIGGER trg_mutation_audit_log_no_update BEFORE UPDATE ON mutation_audit_log FOR EACH ROW EXECUTE FUNCTION mutation_audit_log_immutable_guard(); -> DROP TRIGGER IF EXISTS trg_mutation_audit_log_no_delete ON mutation_audit_log; -> CREATE TRIGGER trg_mutation_audit_log_no_delete BEFORE DELETE ON mutation_audit_log FOR EACH ROW EXECUTE FUNCTION mutation_audit_log_immutable_guard(); -- ok (18.921593ms) -- migrating version 032 -> CREATE TABLE IF NOT EXISTS agent_events_archive ( id UUID NOT NULL, run_id UUID NOT NULL, event_type TEXT NOT NULL, sequence_num BIGINT NOT NULL, occurred_at TIMESTAMPTZ NOT NULL, agent_id TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}', org_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL, archived_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (id, occurred_at) ); -> CREATE INDEX IF NOT EXISTS idx_agent_events_archive_org_time ON agent_events_archive (org_id, occurred_at DESC); -> CREATE INDEX IF NOT EXISTS idx_agent_events_archive_run_seq ON agent_events_archive (run_id, sequence_num); -- ok (10.342068ms) -- migrating version 033 -> CREATE TABLE IF NOT EXISTS decision_claims ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), decision_id uuid NOT NULL, org_id uuid NOT NULL, claim_idx smallint NOT NULL, claim_text text NOT NULL, embedding vector(1024), created_at timestamptz NOT NULL DEFAULT now(), UNIQUE(decision_id, claim_idx) ); -> CREATE INDEX idx_decision_claims_decision ON decision_claims(decision_id); -> CREATE INDEX idx_decision_claims_org ON decision_claims(org_id); -> ALTER TABLE scored_conflicts DROP CONSTRAINT IF EXISTS scored_conflicts_scoring_method_check; -> ALTER TABLE scored_conflicts ADD CONSTRAINT scored_conflicts_scoring_method_check CHECK (scoring_method = ANY (ARRAY['embedding', 'text', 'claim'])); -- ok (13.808631ms) -- migrating version 034 -> ALTER TABLE scored_conflicts ADD COLUMN IF NOT EXISTS explanation TEXT; -> ALTER TABLE scored_conflicts DROP CONSTRAINT IF EXISTS scored_conflicts_scoring_method_check; -> ALTER TABLE scored_conflicts ADD CONSTRAINT scored_conflicts_scoring_method_check CHECK (scoring_method = ANY (ARRAY['embedding', 'text', 'claim', 'llm'])); -- ok (4.641733ms) -- migrating version 035 -> ALTER TABLE scored_conflicts ADD COLUMN IF NOT EXISTS category TEXT CHECK (category IN ('factual', 'assessment', 'strategic', 'temporal')), ADD COLUMN IF NOT EXISTS severity TEXT CHECK (severity IN ('critical', 'high', 'medium', 'low')), ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'acknowledged', 'resolved', 'wont_fix')), ADD COLUMN IF NOT EXISTS resolved_by TEXT, ADD COLUMN IF NOT EXISTS resolved_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS resolution_note TEXT; -> CREATE INDEX IF NOT EXISTS idx_scored_conflicts_status ON scored_conflicts(org_id, status) WHERE status = 'open'; -- ok (6.812015ms) -- migrating version 036 -> CREATE OR REPLACE FUNCTION decisions_immutable_guard() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN -- Check each immutable column. Use IS DISTINCT FROM to handle NULL correctly -- (NULL = NULL is NULL in SQL, but NULL IS NOT DISTINCT FROM NULL is TRUE). IF NEW.outcome IS DISTINCT FROM OLD.outcome THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify outcome (decision_id=%)', OLD.id; END IF; IF NEW.reasoning IS DISTINCT FROM OLD.reasoning THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify reasoning (decision_id=%)', OLD.id; END IF; IF NEW.confidence IS DISTINCT FROM OLD.confidence THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify confidence (decision_id=%)', OLD.id; END IF; IF NEW.decision_type IS DISTINCT FROM OLD.decision_type THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify decision_type (decision_id=%)', OLD.id; END IF; IF NEW.agent_id IS DISTINCT FROM OLD.agent_id THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify agent_id (decision_id=%)', OLD.id; END IF; IF NEW.run_id IS DISTINCT FROM OLD.run_id THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify run_id (decision_id=%)', OLD.id; END IF; IF NEW.org_id IS DISTINCT FROM OLD.org_id THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify org_id (decision_id=%)', OLD.id; END IF; IF NEW.content_hash IS DISTINCT FROM OLD.content_hash THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify content_hash (decision_id=%)', OLD.id; END IF; IF NEW.valid_from IS DISTINCT FROM OLD.valid_from THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify valid_from (decision_id=%)', OLD.id; END IF; IF NEW.created_at IS DISTINCT FROM OLD.created_at THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify created_at (decision_id=%)', OLD.id; END IF; IF NEW.transaction_time IS DISTINCT FROM OLD.transaction_time THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify transaction_time (decision_id=%)', OLD.id; END IF; RETURN NEW; END; $$; -> DROP TRIGGER IF EXISTS trg_decisions_immutable ON decisions; -> CREATE TRIGGER trg_decisions_immutable BEFORE UPDATE ON decisions FOR EACH ROW EXECUTE FUNCTION decisions_immutable_guard(); -- ok (5.155799ms) -- migrating version 037 -> ALTER TABLE decisions ADD COLUMN IF NOT EXISTS conflict_scored_at TIMESTAMPTZ; -> CREATE INDEX IF NOT EXISTS idx_decisions_unscored_conflicts ON decisions (valid_from ASC) WHERE valid_to IS NULL AND embedding IS NOT NULL AND outcome_embedding IS NOT NULL AND conflict_scored_at IS NULL; -- ok (4.254433ms) -- migrating version 038 -> ALTER TABLE scored_conflicts ADD COLUMN IF NOT EXISTS relationship TEXT; -> ALTER TABLE scored_conflicts ADD COLUMN IF NOT EXISTS confidence_weight DOUBLE PRECISION; -> ALTER TABLE scored_conflicts ADD COLUMN IF NOT EXISTS temporal_decay DOUBLE PRECISION; -> ALTER TABLE scored_conflicts ADD COLUMN IF NOT EXISTS resolution_decision_id UUID REFERENCES decisions(id); -> ALTER TABLE scored_conflicts DROP CONSTRAINT IF EXISTS scored_conflicts_scoring_method_check; -> ALTER TABLE scored_conflicts ADD CONSTRAINT scored_conflicts_scoring_method_check CHECK (scoring_method = ANY (ARRAY['embedding', 'text', 'claim', 'llm', 'llm_v2'])); -- ok (7.61651ms) -- migrating version 039 -> ALTER TABLE agents ADD COLUMN IF NOT EXISTS last_seen TIMESTAMPTZ; -- ok (2.347955ms) -- migrating version 040 -> DELETE FROM decision_claims WHERE decision_id NOT IN (SELECT id FROM decisions); -> ALTER TABLE decision_claims ADD CONSTRAINT fk_decision_claims_decision FOREIGN KEY (decision_id) REFERENCES decisions(id) ON DELETE CASCADE; -- ok (5.894292ms) -- migrating version 041 -> SELECT create_hypertable('agent_events_archive', 'occurred_at', chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE, migrate_data => TRUE ); -> ALTER TABLE agent_events_archive SET ( timescaledb.compress, timescaledb.compress_segmentby = 'org_id', timescaledb.compress_orderby = 'occurred_at DESC, sequence_num' ); -> SELECT add_compression_policy('agent_events_archive', INTERVAL '30 days', if_not_exists => TRUE); -> SELECT add_retention_policy('agent_events_archive', INTERVAL '365 days', if_not_exists => TRUE); -- ok (5.579295ms) -- migrating version 042 -> CREATE OR REPLACE FUNCTION prevent_deletion_audit_modify() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'deletion_audit_log is append-only'; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER deletion_audit_immutable_update BEFORE UPDATE ON deletion_audit_log FOR EACH ROW EXECUTE FUNCTION prevent_deletion_audit_modify(); -> CREATE TRIGGER deletion_audit_immutable_delete BEFORE DELETE ON deletion_audit_log FOR EACH ROW EXECUTE FUNCTION prevent_deletion_audit_modify(); -> CREATE OR REPLACE FUNCTION prevent_integrity_proofs_modify() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'integrity_proofs is append-only'; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER integrity_proofs_immutable_update BEFORE UPDATE ON integrity_proofs FOR EACH ROW EXECUTE FUNCTION prevent_integrity_proofs_modify(); -> CREATE TRIGGER integrity_proofs_immutable_delete BEFORE DELETE ON integrity_proofs FOR EACH ROW EXECUTE FUNCTION prevent_integrity_proofs_modify(); -> ALTER TABLE scored_conflicts ALTER COLUMN topic_similarity TYPE DOUBLE PRECISION, ALTER COLUMN outcome_divergence TYPE DOUBLE PRECISION, ALTER COLUMN significance TYPE DOUBLE PRECISION; -- ok (19.526694ms) -- migrating version 043 -> CREATE OR REPLACE FUNCTION prevent_outbox_dead_letters_modify() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'search_outbox_dead_letters is append-only'; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER outbox_dead_letters_immutable_update BEFORE UPDATE ON search_outbox_dead_letters FOR EACH ROW EXECUTE FUNCTION prevent_outbox_dead_letters_modify(); -> CREATE TRIGGER outbox_dead_letters_immutable_delete BEFORE DELETE ON search_outbox_dead_letters FOR EACH ROW EXECUTE FUNCTION prevent_outbox_dead_letters_modify(); -> CREATE OR REPLACE FUNCTION prevent_events_archive_modify() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'agent_events_archive is append-only'; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER events_archive_immutable_update BEFORE UPDATE ON agent_events_archive FOR EACH ROW EXECUTE FUNCTION prevent_events_archive_modify(); -> CREATE TRIGGER events_archive_immutable_delete BEFORE DELETE ON agent_events_archive FOR EACH ROW EXECUTE FUNCTION prevent_events_archive_modify(); -> ALTER TABLE scored_conflicts DROP CONSTRAINT IF EXISTS scored_conflicts_resolution_decision_id_fkey; -> ALTER TABLE scored_conflicts ADD CONSTRAINT scored_conflicts_resolution_decision_id_fkey FOREIGN KEY (resolution_decision_id) REFERENCES decisions(id) ON DELETE SET NULL; -- ok (15.784528ms) -- migrating version 044 -> CREATE TABLE api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), prefix TEXT NOT NULL, key_hash TEXT NOT NULL, agent_id TEXT NOT NULL, org_id UUID NOT NULL REFERENCES organizations(id), label TEXT NOT NULL DEFAULT '', created_by TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_used_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ ); -> CREATE INDEX idx_api_keys_org_agent ON api_keys(org_id, agent_id) WHERE revoked_at IS NULL; -> CREATE INDEX idx_api_keys_prefix ON api_keys(prefix); -> ALTER TABLE decisions ADD COLUMN api_key_id UUID REFERENCES api_keys(id) ON DELETE SET NULL; -> CREATE INDEX idx_decisions_api_key ON decisions(api_key_id) WHERE api_key_id IS NOT NULL; -- ok (15.466295ms) -- migrating version 045 -> CREATE INDEX idx_api_keys_prefix_agent ON api_keys(prefix, agent_id) WHERE revoked_at IS NULL; -> ALTER TABLE agents ADD CONSTRAINT uq_agents_org_agent UNIQUE USING INDEX idx_agents_org_agent; -> ALTER TABLE api_keys ADD CONSTRAINT fk_api_keys_agent FOREIGN KEY (org_id, agent_id) REFERENCES agents(org_id, agent_id) ON DELETE CASCADE; -- ok (9.731897ms) -- migrating version 046 -> ALTER TABLE scored_conflicts ADD COLUMN winning_decision_id UUID REFERENCES decisions(id) ON DELETE SET NULL; -> CREATE INDEX idx_scored_conflicts_winner ON scored_conflicts(winning_decision_id) WHERE winning_decision_id IS NOT NULL; -- ok (4.977237ms) -- migrating version 047 -> CREATE OR REPLACE FUNCTION api_keys_immutable_guard() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN IF NEW.id IS DISTINCT FROM OLD.id OR NEW.key_hash IS DISTINCT FROM OLD.key_hash OR NEW.prefix IS DISTINCT FROM OLD.prefix OR NEW.agent_id IS DISTINCT FROM OLD.agent_id OR NEW.org_id IS DISTINCT FROM OLD.org_id OR NEW.created_at IS DISTINCT FROM OLD.created_at OR NEW.created_by IS DISTINCT FROM OLD.created_by THEN RAISE EXCEPTION 'api_keys: immutable fields cannot be modified (id, key_hash, prefix, agent_id, org_id, created_at, created_by). Revoke and create a new key instead.'; END IF; RETURN NEW; END; $$; -> DROP TRIGGER IF EXISTS trg_api_keys_immutable ON api_keys; -> CREATE TRIGGER trg_api_keys_immutable BEFORE UPDATE ON api_keys FOR EACH ROW EXECUTE FUNCTION api_keys_immutable_guard(); -- ok (3.37057ms) -- migrating version 048 -> ALTER TABLE decisions ADD COLUMN IF NOT EXISTS tool TEXT GENERATED ALWAYS AS ( COALESCE( agent_context->'server'->>'tool', agent_context->>'tool' ) ) STORED, ADD COLUMN IF NOT EXISTS model TEXT GENERATED ALWAYS AS ( COALESCE( agent_context->'client'->>'model', agent_context->>'model' ) ) STORED, ADD COLUMN IF NOT EXISTS repo TEXT GENERATED ALWAYS AS ( COALESCE( agent_context->'server'->>'repo', agent_context->'server'->>'project', agent_context->'client'->>'repo', agent_context->>'repo' ) ) STORED; -> DROP INDEX IF EXISTS idx_decisions_context_tool; -> DROP INDEX IF EXISTS idx_decisions_context_model; -> DROP INDEX IF EXISTS idx_decisions_context_repo; -> CREATE INDEX IF NOT EXISTS idx_decisions_tool ON decisions (tool) WHERE tool IS NOT NULL; -> CREATE INDEX IF NOT EXISTS idx_decisions_model ON decisions (model) WHERE model IS NOT NULL; -> CREATE INDEX IF NOT EXISTS idx_decisions_repo ON decisions (repo) WHERE repo IS NOT NULL; -- ok (31.903954ms) -- migrating version 049 -> DROP INDEX IF EXISTS idx_decisions_embedding; -> DROP INDEX IF EXISTS idx_decisions_outcome_embedding; -> DROP INDEX IF EXISTS idx_evidence_embedding; -- ok (4.931006ms) -- migrating version 050 -> ALTER TABLE decisions RENAME COLUMN quality_score TO completeness_score; -- ok (1.951278ms) -- migrating version 051 -> CREATE TABLE decision_assessments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), decision_id UUID NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, org_id UUID NOT NULL, assessor_agent_id TEXT NOT NULL, outcome TEXT NOT NULL CHECK (outcome IN ('correct', 'incorrect', 'partially_correct')), notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -> CREATE INDEX idx_decision_assessments_decision_id ON decision_assessments (decision_id, assessor_agent_id, created_at DESC); -> CREATE INDEX idx_decision_assessments_org_created ON decision_assessments (org_id, created_at DESC); -> CREATE OR REPLACE FUNCTION prevent_assessment_mutation() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN IF TG_OP = 'DELETE' THEN RAISE EXCEPTION 'decision_assessments rows are immutable: a revised assessment is a new row (decision_id=%, assessor=%)', OLD.decision_id, OLD.assessor_agent_id; ELSIF TG_OP = 'UPDATE' THEN RAISE EXCEPTION 'decision_assessments rows are immutable: a revised assessment is a new row (decision_id=%, assessor=%)', OLD.decision_id, OLD.assessor_agent_id; END IF; RETURN OLD; END; $$; -> CREATE TRIGGER trg_prevent_assessment_delete BEFORE DELETE ON decision_assessments FOR EACH ROW EXECUTE FUNCTION prevent_assessment_mutation(); -> CREATE TRIGGER trg_prevent_assessment_update BEFORE UPDATE ON decision_assessments FOR EACH ROW EXECUTE FUNCTION prevent_assessment_mutation(); -- ok (13.734398ms) -- migrating version 052 -> ALTER TABLE decisions ADD COLUMN IF NOT EXISTS project TEXT GENERATED ALWAYS AS ( COALESCE( agent_context->'client'->>'project', agent_context->'server'->>'project', agent_context->>'project', agent_context->'server'->>'repo', agent_context->'client'->>'repo', agent_context->>'repo' ) ) STORED; -> CREATE INDEX IF NOT EXISTS idx_decisions_project ON decisions (project) WHERE project IS NOT NULL; -> DROP INDEX IF EXISTS idx_decisions_repo; -> ALTER TABLE decisions DROP COLUMN IF EXISTS repo; -- ok (24.452152ms) -- migrating version 053 -> ALTER TABLE organizations ADD COLUMN retention_days INTEGER, ADD COLUMN retention_exclude_types TEXT[]; -> CREATE TABLE retention_holds ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, reason TEXT NOT NULL, hold_from TIMESTAMPTZ NOT NULL, hold_to TIMESTAMPTZ NOT NULL, decision_types TEXT[], -- NULL = all types agent_ids TEXT[], -- NULL = all agents created_by TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), released_at TIMESTAMPTZ -- NULL = active ); -> CREATE INDEX idx_retention_holds_org ON retention_holds(org_id) WHERE released_at IS NULL; -> CREATE TABLE deletion_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, trigger TEXT NOT NULL CHECK (trigger IN ('policy', 'manual', 'gdpr')), initiated_by TEXT, -- agent_id for manual/gdpr, NULL for policy criteria JSONB NOT NULL, deleted_counts JSONB NOT NULL DEFAULT '{}', started_at TIMESTAMPTZ NOT NULL DEFAULT now(), completed_at TIMESTAMPTZ ); -> CREATE INDEX idx_deletion_log_org ON deletion_log(org_id, started_at DESC); -- ok (16.90922ms) -- migrating version 054 -> CREATE TABLE conflict_groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES organizations(id), -- Normalized agent pair: agent_a = LEAST, agent_b = GREATEST. -- For self-contradictions, both are the same agent ID. agent_a TEXT NOT NULL, agent_b TEXT NOT NULL, conflict_kind TEXT NOT NULL CHECK (conflict_kind IN ('cross_agent', 'self_contradiction')), decision_type TEXT NOT NULL, first_detected_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_detected_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(org_id, agent_a, agent_b, conflict_kind, decision_type) ); -> CREATE INDEX idx_conflict_groups_org ON conflict_groups(org_id, last_detected_at DESC); -> ALTER TABLE scored_conflicts ADD COLUMN group_id UUID REFERENCES conflict_groups(id) ON DELETE SET NULL; -> CREATE INDEX idx_scored_conflicts_group ON scored_conflicts(group_id) WHERE group_id IS NOT NULL; -> INSERT INTO conflict_groups (org_id, agent_a, agent_b, conflict_kind, decision_type, first_detected_at, last_detected_at) SELECT org_id, LEAST(agent_a, agent_b), GREATEST(agent_a, agent_b), conflict_kind, decision_type_a, MIN(detected_at), MAX(detected_at) FROM scored_conflicts GROUP BY org_id, LEAST(agent_a, agent_b), GREATEST(agent_a, agent_b), conflict_kind, decision_type_a ON CONFLICT DO NOTHING; -> UPDATE scored_conflicts sc SET group_id = cg.id FROM conflict_groups cg WHERE sc.org_id = cg.org_id AND LEAST(sc.agent_a, sc.agent_b) = cg.agent_a AND GREATEST(sc.agent_a, sc.agent_b) = cg.agent_b AND sc.conflict_kind = cg.conflict_kind AND sc.decision_type_a = cg.decision_type; -- ok (20.476033ms) -- migrating version 055 -> CREATE TABLE decision_erasures ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), decision_id UUID NOT NULL REFERENCES decisions(id), org_id UUID NOT NULL REFERENCES organizations(id), erased_by TEXT NOT NULL, original_hash TEXT NOT NULL, erased_hash TEXT NOT NULL, reason TEXT NOT NULL DEFAULT '', erased_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(decision_id) ); -> CREATE INDEX idx_decision_erasures_org ON decision_erasures(org_id, erased_at DESC); -> CREATE INDEX idx_decision_erasures_decision ON decision_erasures(decision_id); -> CREATE OR REPLACE FUNCTION decisions_immutable_guard() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN -- GDPR erasure bypass: when the session variable is set within a transaction -- via SET LOCAL, permit updates to the three PII/hash fields only. IF current_setting('akashi.erasure_in_progress', true) = 'true' THEN -- Even during erasure, protect structural fields that are NOT PII. IF NEW.decision_type IS DISTINCT FROM OLD.decision_type THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify decision_type (decision_id=%)', OLD.id; END IF; IF NEW.agent_id IS DISTINCT FROM OLD.agent_id THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify agent_id (decision_id=%)', OLD.id; END IF; IF NEW.run_id IS DISTINCT FROM OLD.run_id THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify run_id (decision_id=%)', OLD.id; END IF; IF NEW.org_id IS DISTINCT FROM OLD.org_id THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify org_id (decision_id=%)', OLD.id; END IF; IF NEW.valid_from IS DISTINCT FROM OLD.valid_from THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify valid_from (decision_id=%)', OLD.id; END IF; IF NEW.created_at IS DISTINCT FROM OLD.created_at THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify created_at (decision_id=%)', OLD.id; END IF; IF NEW.transaction_time IS DISTINCT FROM OLD.transaction_time THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify transaction_time (decision_id=%)', OLD.id; END IF; IF NEW.confidence IS DISTINCT FROM OLD.confidence THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify confidence (decision_id=%)', OLD.id; END IF; -- outcome, reasoning, content_hash are permitted during erasure. RETURN NEW; END IF; -- Normal path: full immutability enforcement (unchanged from migration 036). IF NEW.outcome IS DISTINCT FROM OLD.outcome THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify outcome (decision_id=%)', OLD.id; END IF; IF NEW.reasoning IS DISTINCT FROM OLD.reasoning THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify reasoning (decision_id=%)', OLD.id; END IF; IF NEW.confidence IS DISTINCT FROM OLD.confidence THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify confidence (decision_id=%)', OLD.id; END IF; IF NEW.decision_type IS DISTINCT FROM OLD.decision_type THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify decision_type (decision_id=%)', OLD.id; END IF; IF NEW.agent_id IS DISTINCT FROM OLD.agent_id THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify agent_id (decision_id=%)', OLD.id; END IF; IF NEW.run_id IS DISTINCT FROM OLD.run_id THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify run_id (decision_id=%)', OLD.id; END IF; IF NEW.org_id IS DISTINCT FROM OLD.org_id THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify org_id (decision_id=%)', OLD.id; END IF; IF NEW.content_hash IS DISTINCT FROM OLD.content_hash THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify content_hash (decision_id=%)', OLD.id; END IF; IF NEW.valid_from IS DISTINCT FROM OLD.valid_from THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify valid_from (decision_id=%)', OLD.id; END IF; IF NEW.created_at IS DISTINCT FROM OLD.created_at THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify created_at (decision_id=%)', OLD.id; END IF; IF NEW.transaction_time IS DISTINCT FROM OLD.transaction_time THEN RAISE EXCEPTION 'decisions row is immutable: cannot modify transaction_time (decision_id=%)', OLD.id; END IF; RETURN NEW; END; $$; -- ok (14.02281ms) -- migrating version 056 -> ALTER TABLE decisions ADD COLUMN IF NOT EXISTS claim_embeddings_failed_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS claim_embedding_attempts SMALLINT NOT NULL DEFAULT 0; -> CREATE INDEX IF NOT EXISTS idx_decisions_claim_retry ON decisions (claim_embeddings_failed_at ASC) WHERE claim_embeddings_failed_at IS NOT NULL AND valid_to IS NULL AND embedding IS NOT NULL; -- ok (5.417745ms) -- migrating version 057 -> ALTER TABLE agents ADD COLUMN email TEXT; -> CREATE UNIQUE INDEX idx_agents_email ON agents (email) WHERE email IS NOT NULL; -- ok (3.118604ms) -- migrating version 058 -> ALTER TABLE scored_conflicts ADD COLUMN claim_text_a TEXT, ADD COLUMN claim_text_b TEXT; -- ok (1.928217ms) -- migrating version 059 -> ALTER TABLE decisions ADD COLUMN outcome_score REAL; -- ok (1.99954ms) -- migrating version 060 -> ALTER TABLE decision_claims ADD COLUMN IF NOT EXISTS category TEXT; -> CREATE INDEX IF NOT EXISTS idx_decision_claims_category ON decision_claims(decision_id, category) WHERE category IN ('finding', 'assessment'); -- ok (3.471522ms) -- migrating version 061 -> CREATE TABLE conflict_labels ( scored_conflict_id uuid NOT NULL REFERENCES scored_conflicts(id) ON DELETE CASCADE, org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, label text NOT NULL CHECK (label IN ('genuine', 'related_not_contradicting', 'unrelated_false_positive')), labeled_by text NOT NULL, labeled_at timestamptz NOT NULL DEFAULT now(), notes text, PRIMARY KEY (scored_conflict_id) ); -> CREATE INDEX idx_conflict_labels_org ON conflict_labels (org_id); -> CREATE INDEX idx_conflict_labels_label ON conflict_labels (label); -> COMMENT ON TABLE conflict_labels IS 'Ground truth labels for conflict detection evaluation. Each row maps a scored_conflict to a human-verified label.'; -> COMMENT ON COLUMN conflict_labels.label IS 'genuine = true contradiction or supersession; related_not_contradicting = same topic, no real conflict; unrelated_false_positive = should not have been detected'; -- ok (13.016956ms) -- migrating version 062 -> ALTER TABLE scored_conflicts DROP CONSTRAINT IF EXISTS scored_conflicts_scoring_method_check; -> ALTER TABLE scored_conflicts ADD CONSTRAINT scored_conflicts_scoring_method_check CHECK (scoring_method = ANY (ARRAY['embedding', 'text', 'claim', 'llm', 'llm_v2', 'external'])); -- ok (3.560202ms) -- migrating version 063 -> CREATE TABLE project_links ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL, project_a TEXT NOT NULL, project_b TEXT NOT NULL, link_type TEXT NOT NULL DEFAULT 'conflict_scope', created_by TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (org_id, project_a, project_b, link_type) ); -> CREATE INDEX idx_project_links_org ON project_links (org_id); -> CREATE INDEX idx_project_links_lookup ON project_links (org_id, link_type) WHERE link_type = 'conflict_scope'; -- ok (9.579298ms) -- migrating version 064 -> ALTER TABLE conflict_groups ADD COLUMN group_topic TEXT; -> DO $$ DECLARE constraint_name TEXT; BEGIN SELECT tc.constraint_name INTO constraint_name FROM information_schema.table_constraints tc WHERE tc.table_name = 'conflict_groups' AND tc.constraint_type = 'UNIQUE' AND tc.constraint_name LIKE 'conflict_groups_org_id_agent_a_agent_b_conflict_kind_decisi%'; IF constraint_name IS NOT NULL THEN EXECUTE format('ALTER TABLE conflict_groups DROP CONSTRAINT %I', constraint_name); END IF; END $$; -> CREATE INDEX idx_conflict_groups_lookup ON conflict_groups (org_id, agent_a, agent_b, conflict_kind, decision_type); -> UPDATE conflict_groups cg SET group_topic = LEFT(sub.outcome_a, 120) FROM ( SELECT DISTINCT ON (sc.group_id) sc.group_id, sc.outcome_a FROM scored_conflicts sc WHERE sc.group_id IS NOT NULL ORDER BY sc.group_id, CASE WHEN sc.status IN ('open', 'acknowledged') THEN 0 ELSE 1 END, sc.significance DESC NULLS LAST, sc.detected_at DESC ) sub WHERE sub.group_id = cg.id; -- ok (11.122546ms) -- migrating version 065 -> CREATE TABLE IF NOT EXISTS org_settings ( org_id UUID PRIMARY KEY REFERENCES organizations(id) ON DELETE CASCADE, settings JSONB NOT NULL DEFAULT '{}', updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_by TEXT NOT NULL DEFAULT '' ); -- ok (5.550551ms) -- migrating version 066 -> CREATE INDEX IF NOT EXISTS idx_scored_conflicts_org_kind ON scored_conflicts(org_id, conflict_kind); -> CREATE INDEX IF NOT EXISTS idx_scored_conflicts_org_detected_status ON scored_conflicts(org_id, detected_at DESC, status); -> CREATE INDEX IF NOT EXISTS idx_scored_conflicts_org_resolved ON scored_conflicts(org_id, resolved_at DESC) WHERE resolved_at IS NOT NULL; -- ok (6.384042ms) -- migrating version 067 -> ALTER TABLE scored_conflicts ADD COLUMN reopens_resolution_id UUID NULL REFERENCES scored_conflicts(id); -> ALTER TABLE conflict_groups ADD COLUMN times_reopened INT NOT NULL DEFAULT 0; -> CREATE INDEX idx_scored_conflicts_reopens ON scored_conflicts (reopens_resolution_id) WHERE reopens_resolution_id IS NOT NULL; -- ok (6.050275ms) -- migrating version 068 -> ALTER TABLE alternatives DROP CONSTRAINT alternatives_decision_id_fkey, ADD CONSTRAINT alternatives_decision_id_fkey FOREIGN KEY (decision_id) REFERENCES decisions(id) ON DELETE CASCADE; -> ALTER TABLE evidence DROP CONSTRAINT evidence_decision_id_fkey, ADD CONSTRAINT evidence_decision_id_fkey FOREIGN KEY (decision_id) REFERENCES decisions(id) ON DELETE CASCADE; -- ok (13.1852ms) -- migrating version 069 -> UPDATE conflict_groups cg SET first_detected_at = sub.earliest_possible FROM ( SELECT sc.group_id, MIN(GREATEST(da.transaction_time, db.transaction_time)) AS earliest_possible FROM scored_conflicts sc JOIN decisions da ON da.id = sc.decision_a_id JOIN decisions db ON db.id = sc.decision_b_id WHERE sc.group_id IS NOT NULL GROUP BY sc.group_id ) sub WHERE cg.id = sub.group_id AND sub.earliest_possible < cg.first_detected_at; -- ok (4.543723ms) -- migrating version 070 -> ALTER TABLE decisions DROP COLUMN IF EXISTS model; -> ALTER TABLE decisions ADD COLUMN model TEXT GENERATED ALWAYS AS ( COALESCE( agent_context->'client'->>'model', agent_context->'server'->>'model', agent_context->>'model' ) ) STORED; -> CREATE INDEX IF NOT EXISTS idx_decisions_model ON decisions (model) WHERE model IS NOT NULL; -- ok (22.820722ms) -- migrating version 071 -> DROP INDEX IF EXISTS idx_alternatives_selected; -> ALTER TABLE alternatives DROP COLUMN IF EXISTS score; -> ALTER TABLE alternatives DROP COLUMN IF EXISTS selected; -- ok (4.571972ms) -- migrating version 072 -> ALTER TABLE evidence ADD COLUMN metrics JSONB; -> CREATE OR REPLACE FUNCTION jsonb_values_all_numeric(obj JSONB) RETURNS BOOLEAN LANGUAGE sql IMMUTABLE STRICT AS $$ SELECT NOT EXISTS ( SELECT 1 FROM jsonb_each(obj) AS kv WHERE jsonb_typeof(kv.value) <> 'number' ); $$; -> ALTER TABLE evidence ADD CONSTRAINT evidence_metrics_values_numeric CHECK (metrics IS NULL OR jsonb_values_all_numeric(metrics)); -> CREATE INDEX idx_evidence_metrics ON evidence USING gin (metrics) WHERE metrics IS NOT NULL; -- ok (5.140282ms) -- migrating version 073 -> ALTER TABLE decisions ADD COLUMN precedent_reason TEXT; -> COMMENT ON COLUMN decisions.precedent_reason IS 'Free-text explanation of why the precedent_ref decision was cited. NULL when no precedent is set or the agent did not provide a reason.'; -- ok (2.446534ms) -- migrating version 074 -> CREATE OR REPLACE FUNCTION queue_search_outbox_on_context_update() RETURNS trigger AS $$ BEGIN IF OLD.agent_context IS DISTINCT FROM NEW.agent_context THEN INSERT INTO search_outbox (decision_id, org_id, operation, created_at) VALUES (NEW.id, NEW.org_id, 'upsert', now()) ON CONFLICT (decision_id, operation) DO UPDATE SET created_at = EXCLUDED.created_at, attempts = 0, last_error = NULL, locked_until = NULL; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER trg_decisions_context_update_outbox AFTER UPDATE OF agent_context ON decisions FOR EACH ROW EXECUTE FUNCTION queue_search_outbox_on_context_update(); -- ok (3.246641ms) -- migrating version 075 -> ALTER TABLE scored_conflicts DROP CONSTRAINT scored_conflicts_status_check; -> UPDATE scored_conflicts SET status = 'open' WHERE status = 'acknowledged'; -> UPDATE scored_conflicts SET status = 'false_positive' WHERE status = 'wont_fix'; -> ALTER TABLE scored_conflicts ADD CONSTRAINT scored_conflicts_status_check CHECK (status IN ('open', 'resolved', 'false_positive')); -- ok (5.643352ms) -- migrating version 076 -> DELETE FROM decision_claims WHERE org_id NOT IN (SELECT id FROM organizations); -> ALTER TABLE decision_claims ADD CONSTRAINT fk_decision_claims_org FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE CASCADE; -- ok (4.583605ms) -- migrating version 077 -> CREATE TABLE integrity_audit_results ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, proof_id UUID NOT NULL REFERENCES integrity_proofs(id) ON DELETE CASCADE, check_type TEXT NOT NULL, -- 'merkle_root' or 'chain_linkage' passed BOOLEAN NOT NULL, sweep_type TEXT NOT NULL DEFAULT 'sample', -- 'sample' or 'full' detail TEXT, -- human-readable context on failure checked_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE INDEX idx_integrity_audit_results_org_checked ON integrity_audit_results (org_id, checked_at DESC); -> CREATE INDEX idx_integrity_audit_results_failed ON integrity_audit_results (org_id, checked_at DESC) WHERE NOT passed; -- ok (10.399219ms) -- migrating version 078 -> CREATE TABLE IF NOT EXISTS integrity_violations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, proof_id UUID NOT NULL REFERENCES integrity_proofs(id) ON DELETE CASCADE, violation_type TEXT NOT NULL CHECK (violation_type IN ( 'merkle_root_mismatch', 'chain_linkage_broken', 'chain_linkage_nil_previous' )), details JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE INDEX idx_integrity_violations_org_created ON integrity_violations (org_id, created_at DESC); -> COMMENT ON TABLE integrity_violations IS 'Append-only record of detected integrity proof failures. Written by the background audit loop.'; -- ok (10.673355ms) -- migrating version 079 -> CREATE OR REPLACE FUNCTION prevent_integrity_violations_modify() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'integrity_violations is append-only'; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER integrity_violations_immutable_update BEFORE UPDATE ON integrity_violations FOR EACH ROW EXECUTE FUNCTION prevent_integrity_violations_modify(); -> CREATE TRIGGER integrity_violations_immutable_delete BEFORE DELETE ON integrity_violations FOR EACH ROW EXECUTE FUNCTION prevent_integrity_violations_modify(); -> ALTER TABLE integrity_violations DROP CONSTRAINT integrity_violations_proof_id_fkey; -> ALTER TABLE integrity_violations ADD CONSTRAINT integrity_violations_proof_id_fkey FOREIGN KEY (proof_id) REFERENCES integrity_proofs(id) ON DELETE RESTRICT; -- ok (10.004231ms) -- migrating version 080 -> CREATE OR REPLACE FUNCTION prevent_integrity_audit_results_modify() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'integrity_audit_results is append-only'; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER integrity_audit_results_immutable_update BEFORE UPDATE ON integrity_audit_results FOR EACH ROW EXECUTE FUNCTION prevent_integrity_audit_results_modify(); -> CREATE TRIGGER integrity_audit_results_immutable_delete BEFORE DELETE ON integrity_audit_results FOR EACH ROW EXECUTE FUNCTION prevent_integrity_audit_results_modify(); -> ALTER TABLE integrity_audit_results DROP CONSTRAINT integrity_audit_results_proof_id_fkey; -> ALTER TABLE integrity_audit_results ADD CONSTRAINT integrity_audit_results_proof_id_fkey FOREIGN KEY (proof_id) REFERENCES integrity_proofs(id) ON DELETE RESTRICT; -- ok (10.38526ms) -- migrating version 081 -> ALTER TABLE scored_conflicts ADD COLUMN project_a TEXT; -> ALTER TABLE scored_conflicts ADD COLUMN project_b TEXT; -> UPDATE scored_conflicts sc SET project_a = da.project, project_b = db.project FROM decisions da, decisions db WHERE sc.decision_a_id = da.id AND sc.decision_b_id = db.id; -> CREATE INDEX idx_scored_conflicts_project_a ON scored_conflicts (project_a) WHERE status = 'open'; -> CREATE INDEX idx_scored_conflicts_project_b ON scored_conflicts (project_b) WHERE status = 'open'; -- ok (11.170696ms) -- migrating version 082 -> ALTER TABLE integrity_violations DROP CONSTRAINT integrity_violations_org_id_fkey; -> ALTER TABLE integrity_violations ADD CONSTRAINT integrity_violations_org_id_fkey FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE RESTRICT; -- ok (6.005669ms) -- migrating version 083 -> CREATE OR REPLACE FUNCTION prevent_decision_erasures_modify() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'decision_erasures is append-only'; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER decision_erasures_immutable_update BEFORE UPDATE ON decision_erasures FOR EACH ROW EXECUTE FUNCTION prevent_decision_erasures_modify(); -> CREATE TRIGGER decision_erasures_immutable_delete BEFORE DELETE ON decision_erasures FOR EACH ROW EXECUTE FUNCTION prevent_decision_erasures_modify(); -- ok (4.001928ms) -- migrating version 084 -> CREATE TABLE proof_leaves ( proof_id UUID NOT NULL REFERENCES integrity_proofs(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES organizations(id), leaf_hash TEXT NOT NULL, CONSTRAINT fk_proof_leaves_org FOREIGN KEY (org_id) REFERENCES organizations(id) ); -> CREATE INDEX idx_proof_leaves_proof_id ON proof_leaves (proof_id, leaf_hash); -> CREATE INDEX idx_proof_leaves_org_id ON proof_leaves (org_id); -- ok (9.468572ms) -- migrating version 085 -> CREATE TABLE IF NOT EXISTS conflict_resolutions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conflict_id UUID NOT NULL REFERENCES scored_conflicts(id) ON DELETE RESTRICT, org_id UUID NOT NULL REFERENCES organizations(id), resolved_by TEXT NOT NULL, resolved_at TIMESTAMPTZ NOT NULL, resolution_note TEXT, winning_decision_id UUID REFERENCES decisions(id) ON DELETE RESTRICT, archived_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -> CREATE INDEX idx_conflict_resolutions_conflict ON conflict_resolutions(conflict_id); -> CREATE INDEX idx_conflict_resolutions_org ON conflict_resolutions(org_id); -> CREATE OR REPLACE FUNCTION prevent_conflict_resolutions_modify() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'conflict_resolutions is append-only'; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER conflict_resolutions_immutable_update BEFORE UPDATE ON conflict_resolutions FOR EACH ROW EXECUTE FUNCTION prevent_conflict_resolutions_modify(); -> CREATE TRIGGER conflict_resolutions_immutable_delete BEFORE DELETE ON conflict_resolutions FOR EACH ROW EXECUTE FUNCTION prevent_conflict_resolutions_modify(); -- ok (16.990909ms) -- migrating version 086 -> ALTER TABLE decision_assessments DROP CONSTRAINT decision_assessments_decision_id_fkey, ADD CONSTRAINT decision_assessments_decision_id_fkey FOREIGN KEY (decision_id) REFERENCES decisions(id) ON DELETE RESTRICT; -> CREATE OR REPLACE FUNCTION prevent_assessment_mutation() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN IF TG_OP = 'DELETE' THEN IF current_setting('akashi.allow_assessment_delete', true) = 'true' THEN RETURN OLD; END IF; RAISE EXCEPTION 'decision_assessments rows are immutable: a revised assessment is a new row (decision_id=%, assessor=%)', OLD.decision_id, OLD.assessor_agent_id; ELSIF TG_OP = 'UPDATE' THEN RAISE EXCEPTION 'decision_assessments rows are immutable: a revised assessment is a new row (decision_id=%, assessor=%)', OLD.decision_id, OLD.assessor_agent_id; END IF; RETURN OLD; END; $$; -- ok (6.87271ms) -- migrating version 087 -> CREATE INDEX IF NOT EXISTS idx_project_links_alias ON project_links (org_id, project_a) WHERE link_type = 'alias'; -- ok (2.803694ms) -- migrating version 088 -> INSERT INTO mutation_audit_log ( request_id, org_id, actor_agent_id, actor_role, http_method, endpoint, operation, resource_type, resource_id, before_data, after_data, metadata ) SELECT 'migration-088', pl.org_id, 'system:migration', 'system', 'MIGRATION', 'migrations/088_unique_alias_per_project', 'delete', 'project_link', pl.id::text, jsonb_build_object( 'id', pl.id, 'org_id', pl.org_id, 'project_a', pl.project_a, 'project_b', pl.project_b, 'link_type', pl.link_type, 'created_by', pl.created_by, 'created_at', pl.created_at ), NULL, jsonb_build_object( 'reason', 'duplicate alias removed during uniqueness enforcement', 'kept_row', ( SELECT kept.id::text FROM project_links kept WHERE kept.org_id = pl.org_id AND kept.project_a = pl.project_a AND kept.link_type = 'alias' ORDER BY kept.created_at DESC LIMIT 1 ) ) FROM project_links pl WHERE pl.id IN ( SELECT id FROM ( SELECT id, ROW_NUMBER() OVER ( PARTITION BY org_id, project_a ORDER BY created_at DESC ) AS rn FROM project_links WHERE link_type = 'alias' ) ranked WHERE rn > 1 ); -> DELETE FROM project_links WHERE id IN ( SELECT id FROM ( SELECT id, ROW_NUMBER() OVER ( PARTITION BY org_id, project_a ORDER BY created_at DESC ) AS rn FROM project_links WHERE link_type = 'alias' ) ranked WHERE rn > 1 ); -> DROP INDEX IF EXISTS idx_project_links_alias; -> CREATE UNIQUE INDEX idx_project_links_alias_unique ON project_links (org_id, project_a) WHERE link_type = 'alias'; -- ok (9.052344ms) -- migrating version 089 -> CREATE OR REPLACE FUNCTION prevent_conflict_resolutions_modify() RETURNS TRIGGER AS $$ BEGIN -- UPDATE is never allowed — resolutions are append-only audit artifacts. IF TG_OP = 'UPDATE' THEN RAISE EXCEPTION 'conflict_resolutions is append-only'; END IF; -- DELETE is allowed only when the caller has set the session flag within -- a transaction (SET LOCAL). This restricts deletes to explicit -- application-layer operations like DeleteAgentData, preventing -- accidental or ad-hoc removal. IF TG_OP = 'DELETE' THEN IF current_setting('akashi.allow_conflict_resolution_delete', true) = 'true' THEN RETURN OLD; END IF; RAISE EXCEPTION 'conflict_resolutions is append-only'; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; -> ALTER TABLE proof_leaves DROP CONSTRAINT IF EXISTS fk_proof_leaves_org; -- ok (4.702039ms) -- migrating version 090 -> CREATE INDEX IF NOT EXISTS idx_decisions_org_created ON decisions (org_id, created_at); -- ok (3.074463ms) -- migrating version 091 -> CREATE TABLE decision_type_aliases ( alias TEXT NOT NULL, canonical TEXT NOT NULL, org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, created_by TEXT NOT NULL DEFAULT 'system', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (org_id, alias) ); -> INSERT INTO decision_type_aliases (alias, canonical, org_id, created_by) SELECT alias, canonical, id, 'system:migration-091' FROM organizations CROSS JOIN (VALUES ('refactoring', 'refactor'), ('test', 'testing'), ('review', 'code_review'), ('code_change', 'implementation'), ('positioning_recommendation', 'assessment'), ('positioning_analysis', 'assessment'), ('competitive_analysis', 'assessment'), ('documentation_consolidation','documentation') ) AS seed(alias, canonical); -- ok (8.186987ms) -- migrating version 092 -> UPDATE decisions d SET decision_type = a.canonical, metadata = d.metadata || jsonb_build_object('original_decision_type', d.decision_type) FROM decision_type_aliases a WHERE d.decision_type = a.alias AND d.org_id = a.org_id AND d.valid_to IS NULL; -- ok (2.875555ms) -- migrating version 093 -> ALTER TABLE decision_assessments ADD COLUMN source TEXT NOT NULL DEFAULT 'manual'; -> COMMENT ON COLUMN decision_assessments.source IS 'Origin of the assessment: manual (agent-submitted), supersession (decision was superseded), ' 'conflict (conflict resolution winner/loser), citation (precedent cited 3+ times).'; -- ok (3.237665ms) -- migrating version 094 -> CREATE UNIQUE INDEX IF NOT EXISTS idx_assessments_auto_unique ON decision_assessments (decision_id, org_id, source) WHERE source != 'manual'; -- ok (2.736827ms) -- migrating version 095 -> DELETE FROM project_links WHERE link_type = 'alias' AND created_by = 'system:auto-alias'; -> WITH canonical_projects AS ( -- Projects that appear in decisions without any project_submitted override -- are trustworthy — they were either server-verified from git or accepted -- without conflict. SELECT DISTINCT org_id, project FROM decisions WHERE valid_to IS NULL AND project IS NOT NULL AND agent_context->'client'->>'project_submitted' IS NULL ), fixable AS ( SELECT d.id, d.org_id, d.agent_context->'client'->>'project_submitted' AS correct_project, d.project AS wrong_project FROM decisions d JOIN canonical_projects cp ON cp.org_id = d.org_id AND cp.project = d.agent_context->'client'->>'project_submitted' WHERE d.valid_to IS NULL AND d.agent_context->'client'->>'project_submitted' IS NOT NULL -- Only fix rows where the current project differs from the submitted -- value (i.e., an override happened) AND the current project is NOT -- itself a canonical name (avoid breaking correct overrides). AND d.project != d.agent_context->'client'->>'project_submitted' AND NOT EXISTS ( SELECT 1 FROM canonical_projects cp2 WHERE cp2.org_id = d.org_id AND cp2.project = d.project ) ) UPDATE decisions d SET agent_context = jsonb_set( -- Set client.project to the correct value jsonb_set( -- Preserve the wrong value under project_submitted for the audit trail -- (it's already there, just swap the semantics) d.agent_context, '{client,project}', to_jsonb(f.correct_project) ), -- Update project_submitted to record what was wrong (the workspace name) '{client,project_submitted}', to_jsonb(f.wrong_project) ) FROM fixable f WHERE d.id = f.id AND d.org_id = f.org_id; -> UPDATE decisions d SET agent_context = jsonb_set( d.agent_context, '{server,project}', to_jsonb(d.agent_context->'client'->>'project') ) WHERE d.valid_to IS NULL AND d.agent_context->'server'->>'project' IS NOT NULL AND d.agent_context->'client'->>'project' IS NOT NULL AND d.agent_context->'server'->>'project' != d.agent_context->'client'->>'project' -- Only fix if server.project is NOT a canonical project (don't break -- rows where the server was correct and the client was wrong). AND NOT EXISTS ( SELECT 1 FROM decisions d2 WHERE d2.org_id = d.org_id AND d2.valid_to IS NULL AND d2.project = d.agent_context->'server'->>'project' AND d2.agent_context->'client'->>'project_submitted' IS NULL ); -- ok (7.997838ms) -- migrating version 096 -> ALTER TABLE decisions DISABLE TRIGGER trg_decisions_immutable; -> UPDATE decisions d SET decision_type = a.canonical, metadata = d.metadata || jsonb_build_object('original_decision_type', d.decision_type) FROM decision_type_aliases a WHERE d.decision_type = a.alias AND d.org_id = a.org_id AND d.valid_to IS NULL; -> ALTER TABLE decisions ENABLE TRIGGER trg_decisions_immutable; -- ok (4.56955ms) -- migrating version 097 -> ALTER TABLE project_links ADD CONSTRAINT fk_project_links_org FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE CASCADE NOT VALID; -> ALTER TABLE project_links VALIDATE CONSTRAINT fk_project_links_org; -- ok (4.68038ms) -- migrating version 098 -> UPDATE decisions SET agent_context = jsonb_set( jsonb_set( COALESCE(agent_context, '{}'::jsonb), '{client}', COALESCE(COALESCE(agent_context, '{}'::jsonb) -> 'client', '{}'::jsonb) ), '{client,project}', '"akashi"'::jsonb, true ) WHERE id IN ( '91631c33-2b51-4592-8691-a6f87e4b6489', -- project field required on trace entry points 'bbf9a4f8-d917-4ef2-a5d5-b4cd04d4889e', -- 6-axis audit of akashi codebase 'c1c5e13f-64a9-48d0-8643-8b693a0a9068', -- 15 missing soft-delete filters '85253ca8-dcbd-457a-b710-80bfc1897ba1', -- supersession_velocity JSON tag rename '34928025-e268-47d5-a824-7e031643b726', -- SDK types aligned to server model '197e3a6d-3101-41ac-b025-799add51dcb7', -- org_id added to 5 storage queries 'ca9f7d42-02a7-4654-8197-854ac424b49d', -- ErrCodeServiceUnavailable added 'a6d5f451-d2b7-4fce-9645-4737a96de062', -- golangci-lint version alignment 'ddcef804-a9ef-47c4-b8c3-b2b29c449d63' -- /v1 prefix on HTTP routes ) AND valid_to IS NULL; -> UPDATE decisions SET agent_context = jsonb_set( jsonb_set( COALESCE(agent_context, '{}'::jsonb), '{client}', COALESCE(COALESCE(agent_context, '{}'::jsonb) -> 'client', '{}'::jsonb) ), '{client,project}', '"tessera"'::jsonb, true ) WHERE id IN ( 'd4d2f8db-61b2-4d29-9cb0-0ed330ee81fb', -- Dockerfile node:20-slim frontend builder '1b441303-177e-4efc-9814-cda112040a0e' -- tessera emits to akashi integration design ) AND valid_to IS NULL; -> UPDATE decisions SET agent_context = jsonb_set( jsonb_set( COALESCE(agent_context, '{}'::jsonb), '{client}', COALESCE(COALESCE(agent_context, '{}'::jsonb) -> 'client', '{}'::jsonb) ), '{client,project}', '"akashi"'::jsonb, true ) WHERE id IN ( -- salvador: parallelized scoreForDecision '73b2c1d0-6cc0-4b4c-8f98-71e7d27486c6', -- boston: PRs #608 and #613 (akashi event buffer + flush atomicity) '94facf52-ec2b-411d-bfe9-69ef9866e8f6', 'a8a2a011-ba29-4d6f-a30c-33467ad69ac6', 'dcf00edb-fc92-4b86-9c98-8e910ac70ae5', '40280a05-aa2a-48ca-9f2f-6fb42677b17c', -- hanoi-v1: CLAUDE.md PR requirement format change '6767dd14-6163-44bc-99d8-99e508244728' ) AND valid_to IS NULL; -> UPDATE decisions SET agent_context = jsonb_set( jsonb_set( COALESCE(agent_context, '{}'::jsonb), '{client}', COALESCE(COALESCE(agent_context, '{}'::jsonb) -> 'client', '{}'::jsonb) ), '{client,project}', '"tessera"'::jsonb, true ) WHERE id IN ( -- palembang: repo sync worker (Python) '133c91ea-a834-4e5f-9963-b32f6d63354e', '2a2b6947-2c62-4907-adb5-819517fb2e7c', 'e515d092-f0b5-4764-9123-d008b69f9ba9', '5979e765-d2d4-4a0c-9c04-209b2ba9336b', 'd8e560e2-4da6-47b2-974b-485c9b6ba52c', -- valencia-v1: FQN injection fix in gRPC/GraphQL sync '28dff32a-e572-4d44-b8f0-ea3c9a3433fa', -- denpasar: force-publish endpoint 'f0221f95-0cb2-41eb-8547-d0e2b49c86d3', -- kathmandu-v2: graph API PR #430 '680313cb-e84c-4591-bce2-d9de558477c8', -- sao-paulo-v1: service dependency graph, error extraction, merge conflicts '4653a5b1-c918-471c-8e72-18ca8c0dc3f4', '1e5ee838-a6c3-44f3-b25f-ab96c81edf9f', '3ec31abd-95a0-4fd9-bcbc-afc4059debf0', 'dfffffa3-a367-4ed8-b890-15c0b558ec95', 'cacc3ba3-faf4-4987-85a3-db6793641ff0', -- winnipeg: OTEL dependency discovery 'f15a9f3b-fc7e-4e7b-9db6-7faa8464d71b', 'f01b0d08-88ee-447f-ac73-a62a48fa8584', -- manila-v1: Slack config CRUD '475b0f6b-5fa8-4538-b5d4-1a1fd8f0edac', -- lincoln: test cleanup '7c129790-e748-46c2-8df9-33ba481e5926', -- amman-v1: Repo CRUD API 'c3c6b2c6-91ca-4b18-853d-acca112bb88d', -- athens-v1: service contract pivot, frontend, specs, ADR-014 '86f56dae-f2d1-4925-9432-d045748405ef', '2491e4ab-a8db-41ea-88b6-fa3ba7f79f4e', '0b77a432-3ad2-46d7-8050-92c585b8b373', '2ec2f4d2-3394-4e2f-921e-6375c52e89a8', 'a4bdcfac-5d0c-4b95-8909-92a1982c8a67', -- montgomery: username auth '4a6ca482-1a05-4c13-a31c-3c4f95cf86fd', '21ed1539-e1cc-4497-a386-2d4b12d8cd96', -- delhi-v1: bulk endpoints (FastAPI) '49dad312-97d7-41de-856a-16016f46b23a', -- ev: tessera resume entry 'a200dcbb-7201-4ecd-8bc9-243e35bfd2c9' ) AND valid_to IS NULL; -> UPDATE decisions SET agent_context = jsonb_set( jsonb_set( COALESCE(agent_context, '{}'::jsonb), '{client}', COALESCE(COALESCE(agent_context, '{}'::jsonb) -> 'client', '{}'::jsonb) ), '{client,project}', '"mimir"'::jsonb, true ) WHERE id IN ( -- san-francisco: GitHub→provider-agnostic field renaming, semgrep '74a5e390-8689-4bd0-ac4f-1ff2b6ffaf0e', '07aeb2a3-0d5c-4708-877e-1020e2cc50ca', '5e473d33-c058-4bc0-b0d7-a944b78c2734', 'c6a19025-6e18-4807-a7bb-ca608d3ec9e7', -- san-antonio-v2: PR review for spec file renaming '518c4138-71ea-46c3-a6c3-bca9352c6e5f', -- abu-dhabi: PR review for spec genericization '74c0e842-af28-46cb-900c-3ebade7fc352' ) AND valid_to IS NULL; -> UPDATE decisions SET agent_context = jsonb_set( jsonb_set( COALESCE(agent_context, '{}'::jsonb), '{client}', COALESCE(COALESCE(agent_context, '{}'::jsonb) -> 'client', '{}'::jsonb) ), '{client,project}', '"ashita-ai"'::jsonb, true ) WHERE id IN ( -- daegu-v2: blog post outlines, writing, editorial review '9ae85d4a-e903-4768-9c86-561608d785c8', 'edbe0b6d-0c05-4270-8c12-ba8399b469df', 'ffbf2edd-b1f1-48c7-a8e3-a1fe71606de9', '7c19a273-a196-4c97-926a-c1c164fe2192', 'fabb40d5-10d9-4f41-9a44-778c36474bd2', '2eb8b6af-f4d5-4423-a867-fcd487863e30' ) AND valid_to IS NULL; -> UPDATE decisions SET valid_to = NOW() WHERE id IN ( '6c483e64-1456-4fd0-a93c-f2b66eb18e62', '96ab3ada-4d9f-4983-8947-11318cc80af6' ) AND valid_to IS NULL; -- ok (11.182629ms) -- migrating version 099 -> ALTER TABLE proof_leaves DROP CONSTRAINT proof_leaves_proof_id_fkey; -> ALTER TABLE proof_leaves ADD CONSTRAINT proof_leaves_proof_id_fkey FOREIGN KEY (proof_id) REFERENCES integrity_proofs(id) ON DELETE RESTRICT; -> ALTER TABLE proof_leaves DROP CONSTRAINT proof_leaves_org_id_fkey; -> ALTER TABLE proof_leaves ADD CONSTRAINT proof_leaves_org_id_fkey FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE RESTRICT; -> CREATE OR REPLACE FUNCTION prevent_proof_leaves_modify() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'proof_leaves is append-only'; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER proof_leaves_immutable_update BEFORE UPDATE ON proof_leaves FOR EACH ROW EXECUTE FUNCTION prevent_proof_leaves_modify(); -> CREATE TRIGGER proof_leaves_immutable_delete BEFORE DELETE ON proof_leaves FOR EACH ROW EXECUTE FUNCTION prevent_proof_leaves_modify(); -> ALTER TABLE decision_assessments ADD CONSTRAINT fk_decision_assessments_org FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE RESTRICT NOT VALID; -> ALTER TABLE decision_assessments VALIDATE CONSTRAINT fk_decision_assessments_org; -- ok (22.682544ms) -- migrating version 100 -> ALTER TABLE integrity_audit_results DROP CONSTRAINT integrity_audit_results_org_id_fkey; -> ALTER TABLE integrity_audit_results ADD CONSTRAINT integrity_audit_results_org_id_fkey FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE RESTRICT; -> ALTER TABLE deletion_log DROP CONSTRAINT deletion_log_org_id_fkey; -> ALTER TABLE deletion_log ADD CONSTRAINT deletion_log_org_id_fkey FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE RESTRICT; -> CREATE OR REPLACE FUNCTION deletion_log_immutable_guard() RETURNS TRIGGER AS $$ BEGIN IF NEW.id IS DISTINCT FROM OLD.id THEN RAISE EXCEPTION 'deletion_log: cannot modify id'; END IF; IF NEW.org_id IS DISTINCT FROM OLD.org_id THEN RAISE EXCEPTION 'deletion_log: cannot modify org_id'; END IF; IF NEW.trigger IS DISTINCT FROM OLD.trigger THEN RAISE EXCEPTION 'deletion_log: cannot modify trigger'; END IF; IF NEW.initiated_by IS DISTINCT FROM OLD.initiated_by THEN RAISE EXCEPTION 'deletion_log: cannot modify initiated_by'; END IF; IF NEW.criteria IS DISTINCT FROM OLD.criteria THEN RAISE EXCEPTION 'deletion_log: cannot modify criteria'; END IF; IF NEW.started_at IS DISTINCT FROM OLD.started_at THEN RAISE EXCEPTION 'deletion_log: cannot modify started_at'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER deletion_log_immutable_update BEFORE UPDATE ON deletion_log FOR EACH ROW EXECUTE FUNCTION deletion_log_immutable_guard(); -> CREATE OR REPLACE FUNCTION prevent_deletion_log_delete() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'deletion_log rows cannot be deleted'; END; $$ LANGUAGE plpgsql; -> CREATE TRIGGER deletion_log_immutable_delete BEFORE DELETE ON deletion_log FOR EACH ROW EXECUTE FUNCTION prevent_deletion_log_delete(); -> ALTER TABLE evidence ADD CONSTRAINT evidence_org_id_fkey FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE RESTRICT NOT VALID; -> ALTER TABLE evidence VALIDATE CONSTRAINT evidence_org_id_fkey; -> ALTER TABLE deletion_audit_log ADD CONSTRAINT deletion_audit_log_org_id_fkey FOREIGN KEY (org_id) REFERENCES organizations(id) ON DELETE RESTRICT NOT VALID; -> ALTER TABLE deletion_audit_log VALIDATE CONSTRAINT deletion_audit_log_org_id_fkey; -- ok (31.568251ms) -- migrating version 101 -> ALTER INDEX idx_decisions_quality RENAME TO idx_decisions_completeness; -> ALTER INDEX idx_decisions_type_quality RENAME TO idx_decisions_type_completeness; -> DROP INDEX idx_scored_conflicts_org; -- ok (3.681501ms) -- migrating version 102 -> DROP TABLE IF EXISTS evidence_orphans; -> DROP VIEW IF EXISTS current_decisions; -- ok (9.229307ms) -- migrating version 103 -> CREATE INDEX IF NOT EXISTS idx_decisions_git_branch ON decisions ((agent_context->'client'->>'git_branch')) WHERE agent_context->'client'->>'git_branch' IS NOT NULL; -- ok (2.61179ms) -- migrating version 104 -> CREATE TABLE decision_supersedes ( superseding_id UUID NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, superseded_id UUID NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, relationship TEXT NOT NULL DEFAULT 'supersedes', is_primary BOOLEAN NOT NULL DEFAULT FALSE, recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (superseding_id, superseded_id), CONSTRAINT decision_supersedes_not_self CHECK (superseding_id <> superseded_id), CONSTRAINT decision_supersedes_relationship_check CHECK (relationship IN ('supersedes', 'reconciles')) ); -> CREATE INDEX idx_decision_supersedes_superseded ON decision_supersedes(org_id, superseded_id); -> CREATE INDEX idx_decision_supersedes_superseding ON decision_supersedes(org_id, superseding_id); -> CREATE UNIQUE INDEX idx_decision_supersedes_primary ON decision_supersedes(superseding_id) WHERE is_primary; -> INSERT INTO decision_supersedes (superseding_id, superseded_id, org_id, relationship, is_primary) SELECT id, supersedes_id, org_id, 'supersedes', TRUE FROM decisions WHERE supersedes_id IS NOT NULL ON CONFLICT (superseding_id, superseded_id) DO NOTHING; -> CREATE OR REPLACE FUNCTION sync_decision_supersedes_primary() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.supersedes_id IS NULL THEN UPDATE decision_supersedes SET is_primary = FALSE WHERE superseding_id = NEW.id AND org_id = NEW.org_id AND is_primary; RETURN NEW; END IF; UPDATE decision_supersedes SET is_primary = FALSE WHERE superseding_id = NEW.id AND org_id = NEW.org_id AND superseded_id <> NEW.supersedes_id AND is_primary; INSERT INTO decision_supersedes (superseding_id, superseded_id, org_id, relationship, is_primary) VALUES (NEW.id, NEW.supersedes_id, NEW.org_id, 'supersedes', TRUE) ON CONFLICT (superseding_id, superseded_id) DO UPDATE SET is_primary = TRUE; RETURN NEW; END; $$; -> DROP TRIGGER IF EXISTS trg_decisions_sync_supersedes ON decisions; -> CREATE TRIGGER trg_decisions_sync_supersedes AFTER INSERT OR UPDATE OF supersedes_id ON decisions FOR EACH ROW EXECUTE FUNCTION sync_decision_supersedes_primary(); -- ok (18.755633ms) -- migrating version 105 -> ALTER TABLE decision_supersedes DROP CONSTRAINT decision_supersedes_relationship_check; -> ALTER TABLE decision_supersedes ADD CONSTRAINT decision_supersedes_relationship_check CHECK (relationship IN ('supersedes', 'reconciles', 'suggested')); -> ALTER TABLE decision_supersedes ADD COLUMN suggested_by TEXT, ADD COLUMN suggested_confidence REAL, ADD COLUMN suggested_reason TEXT; -> ALTER TABLE decision_supersedes ADD CONSTRAINT decision_supersedes_suggestion_fields_check CHECK ( (relationship = 'suggested' AND suggested_by IS NOT NULL) OR (relationship <> 'suggested' AND suggested_by IS NULL AND suggested_confidence IS NULL AND suggested_reason IS NULL) ); -> ALTER TABLE decision_supersedes ADD CONSTRAINT decision_supersedes_suggestion_not_primary_check CHECK (relationship <> 'suggested' OR is_primary = FALSE); -> CREATE INDEX idx_decision_supersedes_suggested ON decision_supersedes(org_id, superseding_id, recorded_at DESC) WHERE relationship = 'suggested'; -> CREATE INDEX idx_decision_supersedes_suggested_recorded_at ON decision_supersedes(recorded_at) WHERE relationship = 'suggested'; -- ok (12.756257ms) -- migrating version 106 -> CREATE OR REPLACE FUNCTION sync_decision_supersedes_primary() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.supersedes_id IS NULL THEN UPDATE decision_supersedes SET is_primary = FALSE WHERE superseding_id = NEW.id AND org_id = NEW.org_id AND is_primary; RETURN NEW; END IF; UPDATE decision_supersedes SET is_primary = FALSE WHERE superseding_id = NEW.id AND org_id = NEW.org_id AND superseded_id <> NEW.supersedes_id AND is_primary; INSERT INTO decision_supersedes (superseding_id, superseded_id, org_id, relationship, is_primary) VALUES (NEW.id, NEW.supersedes_id, NEW.org_id, 'supersedes', TRUE) ON CONFLICT (superseding_id, superseded_id) DO UPDATE SET is_primary = TRUE; -- Retire latent suggestions from the same agent that proposed the -- predecessor we just confirmed. The new confirmed row above is the -- canonical record — keeping the suggestion would surface a stale -- hint on every subsequent akashi_check call. DELETE FROM decision_supersedes WHERE relationship = 'suggested' AND org_id = NEW.org_id AND superseded_id = NEW.supersedes_id AND superseding_id IN ( SELECT id FROM decisions WHERE org_id = NEW.org_id AND agent_id = NEW.agent_id ); RETURN NEW; END; $$; -- ok (1.754604ms) -- migrating version 107 -> CREATE TABLE IF NOT EXISTS conflict_gold_labels ( scored_conflict_id UUID PRIMARY KEY REFERENCES scored_conflicts(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, label TEXT NOT NULL, rationale TEXT, -- Provenance of the label, e.g. 'blind_llm_fullcorpus_v1' or 'human_v1'. -- Required so a later run can weight or exclude a labeling method without -- guessing from timestamps, which is the failure that made conflict_labels -- unusable. method TEXT NOT NULL, labeled_by TEXT NOT NULL, labeled_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT conflict_gold_labels_label_check CHECK (label IN ('contradiction', 'supersession', 'related_not_contradicting', 'unrelated', 'insufficient')) ); -> CREATE INDEX IF NOT EXISTS idx_conflict_gold_labels_org_label ON conflict_gold_labels (org_id, label); -> CREATE INDEX IF NOT EXISTS idx_conflict_gold_labels_method ON conflict_gold_labels (org_id, method); -- ok (11.419923ms) -- migrating version 108 -> ALTER TABLE scored_conflicts ADD COLUMN IF NOT EXISTS disputed_question TEXT; -> COMMENT ON COLUMN scored_conflicts.disputed_question IS 'The single question both decisions answer differently, as named by the validator. Non-null only for contradictions.'; -- ok (3.294202ms) -- migrating version 109 -> CREATE TABLE IF NOT EXISTS decision_bindings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), decision_id UUID NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, -- parameter is what the agent wrote, preserved for display. parameter TEXT NOT NULL, -- parameter_key is the canonical form the join runs on. Canonicalization is -- deliberately shallow (case, surrounding whitespace, separator style): -- aggressive normalization would merge genuinely different parameters and -- manufacture conflicts, which is a worse failure than missing one. parameter_key TEXT NOT NULL, -- value as written, and its canonical form. Values are compared, never -- interpreted: "5m" and "300s" are different values here, because deciding -- they are the same requires knowing the parameter's type, which we do not. value TEXT NOT NULL, value_key TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT decision_bindings_parameter_not_blank CHECK (length(btrim(parameter)) > 0), CONSTRAINT decision_bindings_value_not_blank CHECK (length(btrim(value)) > 0), -- One decision cannot bind the same parameter twice; the later write would -- otherwise silently conflict with itself. CONSTRAINT decision_bindings_unique_per_decision UNIQUE (decision_id, parameter_key) ); -> CREATE INDEX IF NOT EXISTS idx_decision_bindings_lookup ON decision_bindings (org_id, parameter_key, value_key); -> CREATE INDEX IF NOT EXISTS idx_decision_bindings_decision ON decision_bindings (decision_id); -> COMMENT ON TABLE decision_bindings IS 'Named parameter a decision sets and the value it sets it to. Two decisions binding the same parameter_key to different value_keys are in exact conflict, detected by join rather than by the LLM judge.'; -- ok (15.712861ms) -- migrating version 110 -> ALTER TABLE scored_conflicts DROP CONSTRAINT IF EXISTS scored_conflicts_scoring_method_check; -> ALTER TABLE scored_conflicts ADD CONSTRAINT scored_conflicts_scoring_method_check CHECK (scoring_method = ANY (ARRAY['embedding', 'text', 'claim', 'llm', 'llm_v2', 'external', 'binding_v1'])); -- ok (4.121315ms) -- migrating version 111 -> CREATE TABLE IF NOT EXISTS suppressed_pair_samples ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, decision_a_id UUID NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, decision_b_id UUID NOT NULL REFERENCES decisions(id) ON DELETE CASCADE, rule TEXT NOT NULL, sampled_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- Label columns mirror conflict_gold_labels (107) so the same blind rating -- pass can be pointed at this table unchanged. NULL until rated. label TEXT, rationale TEXT, method TEXT, labeled_by TEXT, labeled_at TIMESTAMPTZ, CONSTRAINT suppressed_pair_samples_label_check CHECK (label IS NULL OR label IN ('contradiction', 'supersession', 'related_not_contradicting', 'unrelated', 'insufficient')), -- A label without provenance is the failure that made conflict_labels -- unusable; refuse the half-written row rather than accept it. CONSTRAINT suppressed_pair_samples_label_provenance CHECK ((label IS NULL AND method IS NULL AND labeled_by IS NULL AND labeled_at IS NULL) OR (label IS NOT NULL AND method IS NOT NULL AND labeled_by IS NOT NULL AND labeled_at IS NOT NULL)), -- Pair IDs are stored in canonical order so (a,b) and (b,a) are one row and -- the sampling hash is side-independent. CONSTRAINT suppressed_pair_samples_ordered CHECK (decision_a_id < decision_b_id), UNIQUE (decision_a_id, decision_b_id, rule) ); -> CREATE INDEX IF NOT EXISTS idx_suppressed_pair_samples_org_rule ON suppressed_pair_samples (org_id, rule); -> CREATE INDEX IF NOT EXISTS idx_suppressed_pair_samples_unlabeled ON suppressed_pair_samples (org_id) WHERE label IS NULL; -- ok (16.35986ms) ------------------------- -- 2.072990454s -- 91 migrations -- 399 sql statements