Files
Jyotisha/frontend/supabase/migrations/20260812010000_agentic_rectification_v9_runtime.sql

1484 lines
56 KiB
PL/PgSQL

-- V9 Agentic Rectification runtime: durable Case / Evidence / Turn / Receipt
-- state plus safe open/entry-summary/get/close/upgrade RPCs and the one-time
-- idempotent legacy Agentic backfill.
--
-- Business schema only. This migration belongs in frontend/supabase/migrations
-- and MUST NOT be duplicated into frontend/db/migrations (identity foundation;
-- see BUG-127 / BUG-144).
--
-- Security model (same as the existing agentic_rectification_results):
-- * all new tables enable RLS and grant table access to service_role only;
-- * all RPCs are SECURITY DEFINER with search_path = '', granted to
-- service_role only; the browser never passes userId, birth snapshot,
-- candidate range or permission decisions -- the server derives them.
begin;
-- ---------------------------------------------------------------------------
-- 1. Cases
-- ---------------------------------------------------------------------------
create table if not exists public.agentic_rectification_cases (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id) on delete cascade,
session_id uuid not null unique references public.chat_sessions(id) on delete cascade,
status text not null default 'draft' check (
status in (
'draft',
'collecting_evidence',
'candidate_ready',
'candidate_accepted',
'needs_rebaseline',
'paused',
'confirmed',
'closed',
'abandoned',
'superseded'
)
),
skill_name text not null default 'jyotish-birth-time-rectification'
check (length(btrim(skill_name)) > 0),
skill_version text not null check (length(btrim(skill_version)) > 0),
baseline_profile_fingerprint text not null check (length(btrim(baseline_profile_fingerprint)) > 0),
baseline_birth_snapshot jsonb not null check (jsonb_typeof(baseline_birth_snapshot) = 'object'),
candidate_range jsonb not null check (jsonb_typeof(candidate_range) = 'object'),
accepted_time time without time zone,
confirmed_time time without time zone,
created_at timestamptz not null default pg_catalog.now(),
updated_at timestamptz not null default pg_catalog.now(),
last_activity_at timestamptz not null default pg_catalog.now(),
completed_at timestamptz,
closed_reason text check (closed_reason is null or closed_reason in ('completed_by_user', 'abandoned_by_user', 'other')),
check (status not in ('confirmed', 'closed') or completed_at is not null)
);
-- Resumable statuses: draft, collecting_evidence, candidate_ready,
-- candidate_accepted, needs_rebaseline, paused.
-- Terminal statuses: confirmed, closed, abandoned, superseded.
-- At most ONE resumable case per user. This partial unique index is the
-- database enforcement point; the service layer must never bypass it.
create unique index if not exists agentic_rectification_cases_one_resumable_per_user
on public.agentic_rectification_cases (user_id)
where status in (
'draft', 'collecting_evidence', 'candidate_ready', 'candidate_accepted',
'needs_rebaseline', 'paused'
);
create index if not exists agentic_rectification_cases_user_activity_idx
on public.agentic_rectification_cases (user_id, last_activity_at desc);
-- ---------------------------------------------------------------------------
-- 2. Turns (raw user/assistant text + model version; reasoning is forbidden)
-- ---------------------------------------------------------------------------
create table if not exists public.agentic_rectification_turns (
id uuid primary key default gen_random_uuid(),
case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
user_message text,
assistant_message text,
status text not null check (status in ('pending', 'completed', 'failed', 'retryable')),
model_name text not null check (length(btrim(model_name)) > 0),
model_version text,
created_at timestamptz not null default pg_catalog.now(),
completed_at timestamptz,
updated_at timestamptz not null default pg_catalog.now(),
check (status <> 'completed' or (assistant_message is not null and length(btrim(assistant_message)) > 0))
);
create index if not exists agentic_rectification_turns_case_idx
on public.agentic_rectification_turns (case_id, created_at);
-- ---------------------------------------------------------------------------
-- 3. Evidence (append-only revision lineage, server-owned IDs)
-- ---------------------------------------------------------------------------
create table if not exists public.agentic_rectification_evidence (
id uuid primary key default gen_random_uuid(),
case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
source_turn_id uuid not null references public.agentic_rectification_turns(id) on delete cascade,
source_message_id uuid,
user_quote text not null check (length(btrim(user_quote)) > 0),
subject text not null check (subject in ('self', 'family', 'other')),
event_kind text not null check (
event_kind in (
'education_start', 'education_completion', 'education_interruption',
'career_entry', 'career_change', 'promotion', 'career_pressure', 'career_exit',
'relationship_start', 'relationship_commitment', 'relationship_separation',
'relocation', 'finance_gain', 'finance_loss',
'self_health_event', 'family_event', 'other'
)
),
domain text not null check (
domain in ('education', 'career', 'relationship', 'relocation', 'finance', 'health', 'family', 'other')
),
occurred_from date,
occurred_to date,
date_precision text not null check (date_precision in ('year', 'month', 'day', 'range', 'unknown')),
summary text not null check (length(btrim(summary)) > 0),
status text not null default 'draft' check (
status in ('draft', 'pending_confirmation', 'confirmed', 'superseded', 'rejected')
),
supersedes_evidence_id uuid references public.agentic_rectification_evidence(id) on delete set null,
created_at timestamptz not null default pg_catalog.now(),
confirmed_at timestamptz,
updated_at timestamptz not null default pg_catalog.now(),
check (status <> 'confirmed' or confirmed_at is not null),
check (supersedes_evidence_id is null or supersedes_evidence_id <> id)
);
create index if not exists agentic_rectification_evidence_case_idx
on public.agentic_rectification_evidence (case_id, created_at);
create index if not exists agentic_rectification_evidence_lineage_idx
on public.agentic_rectification_evidence (supersedes_evidence_id)
where supersedes_evidence_id is not null;
-- ---------------------------------------------------------------------------
-- 4. Tool receipts (fingerprints/phase/tool/status only; no payload/reasoning)
-- ---------------------------------------------------------------------------
create table if not exists public.agentic_rectification_tool_receipts (
id uuid primary key default gen_random_uuid(),
case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
turn_id uuid not null references public.agentic_rectification_turns(id) on delete cascade,
tool_name text not null check (
tool_name in (
'rectification-read-case',
'rectification-propose-evidence',
'rectification-confirm-evidence',
'rectification-revise-evidence',
'rectification-compare-candidates',
'rectification-read-diagnostics',
'rectification-offer-candidates',
'rectification-accept-candidate',
'rectification-confirm-birth-time',
'rectification-close-case'
)
),
public_phase text not null check (
public_phase in (
'run.started', 'skill.started', 'skill.loaded', 'case.loaded',
'evidence.proposed', 'evidence.confirmed', 'candidates.comparing', 'candidates.updated',
'diagnostics.completed', 'candidate.accepted', 'birth_time.confirmed',
'answer.delta', 'run.completed', 'run.failed'
)
),
started_at timestamptz not null default pg_catalog.now(),
completed_at timestamptz,
status text not null check (status in ('started', 'completed', 'failed', 'skipped')),
input_fingerprint text,
result_fingerprint text,
engine_version text,
safe_error_code text
);
create index if not exists agentic_rectification_tool_receipts_turn_idx
on public.agentic_rectification_tool_receipts (case_id, turn_id);
-- ---------------------------------------------------------------------------
-- 5. Open request idempotency ledger (double-click / multi-tab safe)
-- ---------------------------------------------------------------------------
create table if not exists public.agentic_rectification_open_ledger (
request_id uuid not null,
user_id uuid not null references auth.users(id) on delete cascade,
case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
session_id uuid not null references public.chat_sessions(id) on delete cascade,
intent text not null check (intent in ('homepage', 'session', 'new')),
created_at timestamptz not null default pg_catalog.now(),
primary key (user_id, request_id)
);
-- ---------------------------------------------------------------------------
-- 6. Forward extension of agentic_rectification_results to case_id semantics
-- ---------------------------------------------------------------------------
alter table public.agentic_rectification_results
add column if not exists case_id uuid
references public.agentic_rectification_cases(id) on delete set null,
add column if not exists evidence_ledger_fingerprint text,
add column if not exists candidate_range_fingerprint text,
add column if not exists skill_version text;
create index if not exists agentic_rectification_results_case_idx
on public.agentic_rectification_results (case_id)
where case_id is not null;
-- ---------------------------------------------------------------------------
-- 7. Chat session reverse pointer (one-to-one, bidirectional consistency)
-- ---------------------------------------------------------------------------
alter table public.chat_sessions
add column if not exists agentic_rectification_case_id uuid
references public.agentic_rectification_cases(id) on delete set null;
create unique index if not exists chat_sessions_agentic_rectification_case_unique
on public.chat_sessions (agentic_rectification_case_id)
where agentic_rectification_case_id is not null;
-- ---------------------------------------------------------------------------
-- 8. RLS + grants (service-role only; browser never touches these tables)
-- ---------------------------------------------------------------------------
alter table public.agentic_rectification_cases enable row level security;
alter table public.agentic_rectification_turns enable row level security;
alter table public.agentic_rectification_evidence enable row level security;
alter table public.agentic_rectification_tool_receipts enable row level security;
alter table public.agentic_rectification_open_ledger enable row level security;
revoke all on table public.agentic_rectification_cases from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_turns from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_evidence from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_tool_receipts from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_open_ledger from public, anon, authenticated, service_role;
grant all on table public.agentic_rectification_cases to service_role;
grant all on table public.agentic_rectification_turns to service_role;
grant all on table public.agentic_rectification_evidence to service_role;
grant all on table public.agentic_rectification_tool_receipts to service_role;
grant all on table public.agentic_rectification_open_ledger to service_role;
-- ---------------------------------------------------------------------------
-- 9. Bidirectional Case <-> Session consistency triggers
-- ---------------------------------------------------------------------------
create or replace function public.agentic_rectification_cases_sync_session()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
if new.session_id is distinct from old.session_id then
update public.chat_sessions
set agentic_rectification_case_id = null
where agentic_rectification_case_id = old.id;
end if;
update public.chat_sessions
set agentic_rectification_case_id = new.id,
session_type = 'birth_time_rectification'
where id = new.session_id;
return new;
end;
$$;
revoke all on function public.agentic_rectification_cases_sync_session()
from public, anon, authenticated;
drop trigger if exists agentic_rectification_cases_sync_session_trigger
on public.agentic_rectification_cases;
create trigger agentic_rectification_cases_sync_session_trigger
after insert or update of session_id on public.agentic_rectification_cases
for each row execute function public.agentic_rectification_cases_sync_session();
create or replace function public.agentic_rectification_chat_sessions_case_guard()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
begin
if new.agentic_rectification_case_id is not null then
select * into v_case
from public.agentic_rectification_cases
where id = new.agentic_rectification_case_id;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
if v_case.session_id is distinct from new.id then
raise exception 'agentic_rectification_case_session_mismatch' using errcode = 'P0001';
end if;
if v_case.user_id is distinct from new.user_id then
raise exception 'agentic_rectification_case_owner_mismatch' using errcode = 'P0001';
end if;
end if;
return new;
end;
$$;
revoke all on function public.agentic_rectification_chat_sessions_case_guard()
from public, anon, authenticated;
drop trigger if exists agentic_rectification_chat_sessions_case_guard_trigger
on public.chat_sessions;
create trigger agentic_rectification_chat_sessions_case_guard_trigger
before update of agentic_rectification_case_id on public.chat_sessions
for each row execute function public.agentic_rectification_chat_sessions_case_guard();
-- ---------------------------------------------------------------------------
-- 10. Shared helpers used by RPCs
-- ---------------------------------------------------------------------------
create or replace function public.agentic_rectification_normalize_quote(p_value text)
returns text
language sql
immutable
as $$
select regexp_replace(
lower(coalesce(p_value, '')),
'[\s\u3000,。!?、;:“”‘’()《》·—…]',
'',
'g'
)
$$;
revoke all on function public.agentic_rectification_normalize_quote(text)
from public, anon, authenticated;
create or replace function public.agentic_rectification_resumable_statuses()
returns text[]
language sql
immutable
as $$
select array[
'draft', 'collecting_evidence', 'candidate_ready', 'candidate_accepted',
'needs_rebaseline', 'paused'
]::text[]
$$;
revoke all on function public.agentic_rectification_resumable_statuses()
from public, anon, authenticated;
create or replace function public.agentic_rectification_legacy_fingerprint(
p_user_id uuid,
p_session_id uuid,
p_status text
)
returns text
language sql
immutable
as $$
select encode(
public.digest(
convert_to(
p_user_id::text || ':' || p_session_id::text || ':' || coalesce(p_status, ''),
'utf8'
),
'sha256'
),
'hex'
)
$$;
revoke all on function public.agentic_rectification_legacy_fingerprint(uuid, uuid, text)
from public, anon, authenticated;
-- ---------------------------------------------------------------------------
-- 11. Open Case RPC (idempotent, same-user serialized, atomic case+session)
-- ---------------------------------------------------------------------------
create or replace function public.open_agentic_rectification_case(
p_user_id uuid,
p_request_id uuid,
p_intent text,
p_session_id uuid,
p_skill_name text,
p_skill_version text,
p_baseline_profile_fingerprint text,
p_baseline_birth_snapshot jsonb,
p_candidate_range jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_session public.chat_sessions%rowtype;
v_session_id uuid;
v_turn_count bigint;
begin
if p_user_id is null or p_request_id is null then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
if p_intent not in ('homepage', 'session', 'new') then
raise exception 'agentic_rectification_invalid_intent' using errcode = 'P0001';
end if;
if length(btrim(p_skill_name)) = 0 or length(btrim(p_skill_version)) = 0
or length(btrim(coalesce(p_baseline_profile_fingerprint, ''))) = 0 then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
-- Serialize concurrent open/create for the same user (double-click, two
-- tabs, retries). The advisory lock is released when this transaction ends.
perform pg_catalog.pg_advisory_xact_lock(
pg_catalog.hashtext('agentic_rectification_open:' || p_user_id::text)
);
-- Idempotency: an earlier open with the same requestId wins.
select c.* into v_case
from public.agentic_rectification_cases c
join public.agentic_rectification_open_ledger l
on l.case_id = c.id and l.session_id = c.session_id
where l.user_id = p_user_id and l.request_id = p_request_id
limit 1;
if found then
return jsonb_build_object(
'disposition', case
when v_case.status = any (public.agentic_rectification_resumable_statuses()) then 'resumed'
else 'readonly'
end,
'case_id', v_case.id,
'session_id', v_case.session_id,
'status', v_case.status,
'should_start_opening', false,
'skill_version', v_case.skill_version
);
end if;
if p_intent = 'session' then
if p_session_id is null then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
select * into v_session
from public.chat_sessions
where id = p_session_id and user_id = p_user_id;
if not found then
raise exception 'agentic_rectification_session_not_found' using errcode = 'P0001';
end if;
if v_session.session_type <> 'birth_time_rectification' then
raise exception 'agentic_rectification_session_not_rectification' using errcode = 'P0001';
end if;
select * into v_case
from public.agentic_rectification_cases
where session_id = p_session_id;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
return jsonb_build_object(
'disposition', case
when v_case.status = any (public.agentic_rectification_resumable_statuses()) then 'resumed'
else 'readonly'
end,
'case_id', v_case.id,
'session_id', v_case.session_id,
'status', v_case.status,
'should_start_opening', false,
'skill_version', v_case.skill_version
);
end if;
if p_intent = 'homepage' then
select * into v_case
from public.agentic_rectification_cases
where user_id = p_user_id
and status = any (public.agentic_rectification_resumable_statuses())
order by last_activity_at desc, id
limit 1;
if found then
select count(*) into v_turn_count
from public.agentic_rectification_turns
where case_id = v_case.id;
return jsonb_build_object(
'disposition', 'resumed',
'case_id', v_case.id,
'session_id', v_case.session_id,
'status', v_case.status,
'should_start_opening', false,
'skill_version', v_case.skill_version
);
end if;
end if;
if p_intent = 'new' then
select * into v_case
from public.agentic_rectification_cases
where user_id = p_user_id
and status = any (public.agentic_rectification_resumable_statuses())
order by last_activity_at desc, id
limit 1;
if found then
-- supersedeActive=true is forbidden: restarting with an active case
-- must surface a safe conflict, never silently abandon.
raise exception 'agentic_rectification_active_case_conflict' using errcode = 'P0001';
end if;
end if;
-- Create a new case + a new session atomically. The inner exception block
-- rolls back both inserts together if a concurrent request already created
-- a resumable case for this user. Profile snapshot and candidate range are
-- validated here because only this path persists them.
begin
if p_baseline_birth_snapshot is null or jsonb_typeof(p_baseline_birth_snapshot) <> 'object'
or p_baseline_birth_snapshot ->> 'birth_date' is null
or p_baseline_birth_snapshot ->> 'latitude' is null
or p_baseline_birth_snapshot ->> 'longitude' is null
or p_baseline_birth_snapshot ->> 'timezone_offset' is null
or length(btrim(coalesce(p_baseline_birth_snapshot ->> 'birth_time_source', ''))) = 0 then
raise exception 'agentic_rectification_profile_incomplete' using errcode = 'P0001';
end if;
if p_candidate_range is null or jsonb_typeof(p_candidate_range) <> 'object'
or not (p_candidate_range ->> 'start_time') ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
or not (p_candidate_range ->> 'end_time') ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
raise exception 'agentic_rectification_invalid_range' using errcode = 'P0001';
end if;
insert into public.chat_sessions (user_id, title, theme, session_type, messages)
values (p_user_id, '生时校正', 'general', 'birth_time_rectification', '[]'::jsonb)
returning id into v_session_id;
insert into public.agentic_rectification_cases (
user_id, session_id, status, skill_name, skill_version,
baseline_profile_fingerprint, baseline_birth_snapshot, candidate_range
) values (
p_user_id, v_session_id, 'draft', p_skill_name, p_skill_version,
p_baseline_profile_fingerprint, p_baseline_birth_snapshot, p_candidate_range
) returning id into v_case.id;
insert into public.agentic_rectification_open_ledger (
request_id, user_id, case_id, session_id, intent
) values (
p_request_id, p_user_id, v_case.id, v_session_id, p_intent
);
exception when unique_violation then
select * into v_case
from public.agentic_rectification_cases
where user_id = p_user_id
and status = any (public.agentic_rectification_resumable_statuses())
order by last_activity_at desc, id
limit 1;
if not found then
raise;
end if;
insert into public.agentic_rectification_open_ledger (
request_id, user_id, case_id, session_id, intent
) values (
p_request_id, p_user_id, v_case.id, v_case.session_id, p_intent
);
return jsonb_build_object(
'disposition', 'resumed',
'case_id', v_case.id,
'session_id', v_case.session_id,
'status', v_case.status,
'should_start_opening', false,
'skill_version', v_case.skill_version
);
end;
return jsonb_build_object(
'disposition', 'created',
'case_id', v_case.id,
'session_id', v_session_id,
'status', 'draft',
'should_start_opening', true,
'skill_version', p_skill_version
);
end;
$$;
revoke all on function public.open_agentic_rectification_case(uuid, uuid, text, uuid, text, text, text, jsonb, jsonb)
from public, anon, authenticated;
grant execute on function public.open_agentic_rectification_case(uuid, uuid, text, uuid, text, text, text, jsonb, jsonb)
to service_role;
-- ---------------------------------------------------------------------------
-- 12. Entry summary RPC (homepage CTA truth)
-- ---------------------------------------------------------------------------
create or replace function public.get_agentic_rectification_entry_summary(
p_user_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_resumable public.agentic_rectification_cases%rowtype;
v_terminal public.agentic_rectification_cases%rowtype;
begin
if p_user_id is null then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
select * into v_resumable
from public.agentic_rectification_cases
where user_id = p_user_id
and status = any (public.agentic_rectification_resumable_statuses())
order by last_activity_at desc, id
limit 1;
select * into v_terminal
from public.agentic_rectification_cases
where user_id = p_user_id
and status in ('confirmed', 'closed', 'abandoned', 'superseded')
order by last_activity_at desc, id
limit 1;
return jsonb_build_object(
'has_resumable_case', v_resumable.id is not null,
'has_terminal_case_with_time', v_terminal.id is not null
and (v_terminal.accepted_time is not null or v_terminal.confirmed_time is not null),
'latest_resumable', case
when v_resumable.id is null then null
else jsonb_build_object(
'case_id', v_resumable.id,
'status', v_resumable.status,
'last_activity_at', v_resumable.last_activity_at
)
end,
'latest_terminal', case
when v_terminal.id is null then null
else jsonb_build_object(
'case_id', v_terminal.id,
'status', v_terminal.status,
'has_usable_time', v_terminal.accepted_time is not null or v_terminal.confirmed_time is not null
)
end
);
end;
$$;
revoke all on function public.get_agentic_rectification_entry_summary(uuid)
from public, anon, authenticated;
grant execute on function public.get_agentic_rectification_entry_summary(uuid)
to service_role;
-- ---------------------------------------------------------------------------
-- 13. Case read RPC (sanitized projection; never the birth snapshot)
-- ---------------------------------------------------------------------------
create or replace function public.get_agentic_rectification_case(
p_user_id uuid,
p_case_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_result public.agentic_rectification_results%rowtype;
v_evidence_count bigint;
v_turn_count bigint;
begin
if p_user_id is null or p_case_id is null then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
select * into v_case
from public.agentic_rectification_cases
where id = p_case_id and user_id = p_user_id;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
select count(*) into v_evidence_count
from public.agentic_rectification_evidence
where case_id = v_case.id;
select count(*) into v_turn_count
from public.agentic_rectification_turns
where case_id = v_case.id;
select * into v_result
from public.agentic_rectification_results
where case_id = v_case.id
and invalidated_at is null
order by created_at desc
limit 1;
return jsonb_build_object(
'case_id', v_case.id,
'session_id', v_case.session_id,
'status', v_case.status,
'skill_name', v_case.skill_name,
'skill_version', v_case.skill_version,
'candidate_range', v_case.candidate_range,
'accepted_time', v_case.accepted_time,
'confirmed_time', v_case.confirmed_time,
'created_at', v_case.created_at,
'last_activity_at', v_case.last_activity_at,
'completed_at', v_case.completed_at,
'closed_reason', v_case.closed_reason,
'evidence_count', v_evidence_count,
'turn_count', v_turn_count,
'latest_result', case
when v_result.id is null then null
else jsonb_build_object(
'result_id', v_result.id,
'candidates', v_result.candidates,
'overall_confidence', v_result.overall_confidence,
'selection_allowed', v_result.selection_allowed,
'confirmation_allowed', v_result.confirmation_allowed,
'representative_time', v_result.representative_time,
'selected_time', v_result.selected_time,
'selection_kind', v_result.selection_kind,
'created_at', v_result.created_at
)
end
);
end;
$$;
revoke all on function public.get_agentic_rectification_case(uuid, uuid)
from public, anon, authenticated;
grant execute on function public.get_agentic_rectification_case(uuid, uuid)
to service_role;
-- ---------------------------------------------------------------------------
-- 14. Close Case RPC (terminal; never wraps as engine confirmed)
-- ---------------------------------------------------------------------------
create or replace function public.close_agentic_rectification_case(
p_user_id uuid,
p_case_id uuid,
p_reason text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
begin
if p_user_id is null or p_case_id is null
or coalesce(p_reason, '') not in ('completed_by_user', 'abandoned_by_user', 'other') then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
select * into v_case
from public.agentic_rectification_cases
where id = p_case_id and user_id = p_user_id;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
if v_case.status <> 'closed' then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
return jsonb_build_object(
'success', true, 'idempotent', true,
'case_id', v_case.id, 'status', v_case.status
);
end if;
update public.agentic_rectification_cases
set status = 'closed',
closed_reason = p_reason,
completed_at = pg_catalog.now(),
last_activity_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = v_case.id;
return jsonb_build_object(
'success', true, 'idempotent', false,
'case_id', v_case.id, 'status', 'closed'
);
end;
$$;
revoke all on function public.close_agentic_rectification_case(uuid, uuid, text)
from public, anon, authenticated;
grant execute on function public.close_agentic_rectification_case(uuid, uuid, text)
to service_role;
-- ---------------------------------------------------------------------------
-- 15. Upgrade Skill RPC (explicit migration; running cases pin their version)
-- ---------------------------------------------------------------------------
create or replace function public.upgrade_agentic_rectification_skill(
p_user_id uuid,
p_case_id uuid,
p_skill_version text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_previous text;
begin
if p_user_id is null or p_case_id is null
or length(btrim(coalesce(p_skill_version, ''))) = 0 then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
select * into v_case
from public.agentic_rectification_cases
where id = p_case_id and user_id = p_user_id;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
v_previous := v_case.skill_version;
update public.agentic_rectification_cases
set skill_version = p_skill_version,
updated_at = pg_catalog.now()
where id = v_case.id;
return jsonb_build_object(
'success', true,
'case_id', v_case.id,
'previous_skill_version', v_previous,
'skill_version', p_skill_version,
'idempotent', v_previous = p_skill_version
);
end;
$$;
revoke all on function public.upgrade_agentic_rectification_skill(uuid, uuid, text)
from public, anon, authenticated;
grant execute on function public.upgrade_agentic_rectification_skill(uuid, uuid, text)
to service_role;
-- ---------------------------------------------------------------------------
-- 16. Turn append RPC (server-owned; no reasoning ever persisted)
-- ---------------------------------------------------------------------------
create or replace function public.append_agentic_rectification_turn(
p_user_id uuid,
p_case_id uuid,
p_user_message text,
p_assistant_message text,
p_model_name text,
p_model_version text,
p_status text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_turn_id uuid;
begin
if p_user_id is null or p_case_id is null
or length(btrim(coalesce(p_model_name, ''))) = 0
or p_status not in ('pending', 'completed', 'failed', 'retryable') then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
if p_status = 'completed' and (p_assistant_message is null or length(btrim(p_assistant_message)) = 0) then
raise exception 'agentic_rectification_turn_incomplete' using errcode = 'P0001';
end if;
select * into v_case
from public.agentic_rectification_cases
where id = p_case_id and user_id = p_user_id;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
insert into public.agentic_rectification_turns (
case_id, user_message, assistant_message, status, model_name, model_version, completed_at
) values (
p_case_id, p_user_message, p_assistant_message, p_status, p_model_name, p_model_version,
case when p_status = 'completed' then pg_catalog.now() else null end
) returning id into v_turn_id;
update public.agentic_rectification_cases
set last_activity_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = p_case_id;
return jsonb_build_object('turn_id', v_turn_id);
end;
$$;
revoke all on function public.append_agentic_rectification_turn(uuid, uuid, text, text, text, text, text)
from public, anon, authenticated;
grant execute on function public.append_agentic_rectification_turn(uuid, uuid, text, text, text, text, text)
to service_role;
-- ---------------------------------------------------------------------------
-- 17. Tool receipt insert RPC (fingerprints only)
-- ---------------------------------------------------------------------------
create or replace function public.insert_agentic_rectification_tool_receipt(
p_user_id uuid,
p_case_id uuid,
p_turn_id uuid,
p_tool_name text,
p_public_phase text,
p_status text,
p_input_fingerprint text,
p_result_fingerprint text,
p_engine_version text,
p_safe_error_code text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_receipt_id uuid;
v_turn_count bigint;
begin
if p_user_id is null or p_case_id is null or p_turn_id is null then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
if not exists (
select 1 from public.agentic_rectification_cases
where id = p_case_id and user_id = p_user_id
) then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
select count(*) into v_turn_count
from public.agentic_rectification_turns
where id = p_turn_id and case_id = p_case_id;
if v_turn_count = 0 then
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
end if;
insert into public.agentic_rectification_tool_receipts (
case_id, turn_id, tool_name, public_phase, status,
input_fingerprint, result_fingerprint, engine_version, safe_error_code,
completed_at
) values (
p_case_id, p_turn_id, p_tool_name, p_public_phase, p_status,
p_input_fingerprint, p_result_fingerprint, p_engine_version, p_safe_error_code,
case when p_status = 'completed' then pg_catalog.now() else null end
) returning id into v_receipt_id;
return jsonb_build_object('receipt_id', v_receipt_id);
end;
$$;
revoke all on function public.insert_agentic_rectification_tool_receipt(uuid, uuid, uuid, text, text, text, text, text, text, text)
from public, anon, authenticated;
grant execute on function public.insert_agentic_rectification_tool_receipt(uuid, uuid, uuid, text, text, text, text, text, text, text)
to service_role;
-- ---------------------------------------------------------------------------
-- 18. Evidence proposal / confirm / revise RPCs
-- ---------------------------------------------------------------------------
create or replace function public.propose_agentic_rectification_evidence(
p_user_id uuid,
p_case_id uuid,
p_source_turn_id uuid,
p_user_quote text,
p_subject text,
p_event_kind text,
p_domain text,
p_occurred_from date,
p_occurred_to date,
p_date_precision text,
p_summary text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_turn public.agentic_rectification_turns%rowtype;
v_evidence_id uuid;
v_existing_id uuid;
begin
if p_user_id is null or p_case_id is null or p_source_turn_id is null
or length(btrim(coalesce(p_user_quote, ''))) = 0
or length(btrim(coalesce(p_summary, ''))) = 0
or p_subject not in ('self', 'family', 'other')
or p_date_precision not in ('year', 'month', 'day', 'range', 'unknown') then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
select * into v_case
from public.agentic_rectification_cases
where id = p_case_id and user_id = p_user_id;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
select * into v_turn
from public.agentic_rectification_turns
where id = p_source_turn_id and case_id = p_case_id;
if not found then
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
end if;
if v_turn.user_message is null
or position(
public.agentic_rectification_normalize_quote(p_user_quote)
in public.agentic_rectification_normalize_quote(v_turn.user_message)
) = 0 then
raise exception 'agentic_rectification_quote_not_grounded' using errcode = 'P0001';
end if;
-- Idempotency: replaying the same proposal returns the existing draft.
select id into v_existing_id
from public.agentic_rectification_evidence
where case_id = p_case_id
and source_turn_id = p_source_turn_id
and user_quote = p_user_quote
and event_kind = p_event_kind
and summary = p_summary
and status in ('draft', 'pending_confirmation')
limit 1;
if v_existing_id is not null then
return jsonb_build_object('evidence_id', v_existing_id, 'idempotent', true);
end if;
insert into public.agentic_rectification_evidence (
case_id, source_turn_id, user_quote, subject, event_kind, domain,
occurred_from, occurred_to, date_precision, summary, status
) values (
p_case_id, p_source_turn_id, p_user_quote, p_subject, p_event_kind, p_domain,
p_occurred_from, p_occurred_to, p_date_precision, p_summary, 'draft'
) returning id into v_evidence_id;
update public.agentic_rectification_cases
set last_activity_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = p_case_id;
return jsonb_build_object('evidence_id', v_evidence_id, 'idempotent', false);
end;
$$;
revoke all on function public.propose_agentic_rectification_evidence(uuid, uuid, uuid, text, text, text, text, date, date, text, text)
from public, anon, authenticated;
grant execute on function public.propose_agentic_rectification_evidence(uuid, uuid, uuid, text, text, text, text, date, date, text, text)
to service_role;
create or replace function public.confirm_agentic_rectification_evidence(
p_user_id uuid,
p_case_id uuid,
p_evidence_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_evidence public.agentic_rectification_evidence%rowtype;
begin
if p_user_id is null or p_case_id is null or p_evidence_id is null then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
select * into v_case
from public.agentic_rectification_cases
where id = p_case_id and user_id = p_user_id;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
select * into v_evidence
from public.agentic_rectification_evidence
where id = p_evidence_id and case_id = p_case_id;
if not found then
raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
end if;
if v_evidence.status = 'confirmed' then
return jsonb_build_object('evidence_id', v_evidence.id, 'status', 'confirmed', 'idempotent', true);
end if;
if v_evidence.status not in ('draft', 'pending_confirmation') then
raise exception 'agentic_rectification_evidence_not_confirmable' using errcode = 'P0001';
end if;
update public.agentic_rectification_evidence
set status = 'confirmed',
confirmed_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = v_evidence.id;
return jsonb_build_object('evidence_id', v_evidence.id, 'status', 'confirmed', 'idempotent', false);
end;
$$;
revoke all on function public.confirm_agentic_rectification_evidence(uuid, uuid, uuid)
from public, anon, authenticated;
grant execute on function public.confirm_agentic_rectification_evidence(uuid, uuid, uuid)
to service_role;
create or replace function public.revise_agentic_rectification_evidence(
p_user_id uuid,
p_case_id uuid,
p_evidence_id uuid,
p_user_quote text,
p_occurred_from date,
p_occurred_to date,
p_date_precision text,
p_summary text
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_target public.agentic_rectification_evidence%rowtype;
v_new_id uuid;
begin
if p_user_id is null or p_case_id is null or p_evidence_id is null
or length(btrim(coalesce(p_user_quote, ''))) = 0
or length(btrim(coalesce(p_summary, ''))) = 0
or p_date_precision not in ('year', 'month', 'day', 'range', 'unknown') then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
select * into v_case
from public.agentic_rectification_cases
where id = p_case_id and user_id = p_user_id;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
if v_case.status in ('confirmed', 'closed', 'abandoned', 'superseded') then
raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001';
end if;
select * into v_target
from public.agentic_rectification_evidence
where id = p_evidence_id and case_id = p_case_id;
if not found then
raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
end if;
if v_target.status not in ('confirmed', 'pending_confirmation') then
raise exception 'agentic_rectification_evidence_not_revisable' using errcode = 'P0001';
end if;
-- Append-only lineage: the old row is superseded, never overwritten.
update public.agentic_rectification_evidence
set status = 'superseded',
updated_at = pg_catalog.now()
where id = v_target.id;
insert into public.agentic_rectification_evidence (
case_id, source_turn_id, user_quote, subject, event_kind, domain,
occurred_from, occurred_to, date_precision, summary, status, supersedes_evidence_id
) values (
v_target.case_id, v_target.source_turn_id, p_user_quote, v_target.subject,
v_target.event_kind, v_target.domain,
p_occurred_from, p_occurred_to, p_date_precision, p_summary,
'pending_confirmation', v_target.id
) returning id into v_new_id;
update public.agentic_rectification_cases
set last_activity_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = p_case_id;
return jsonb_build_object(
'evidence_id', v_new_id,
'supersedes_evidence_id', v_target.id,
'idempotent', false
);
end;
$$;
revoke all on function public.revise_agentic_rectification_evidence(uuid, uuid, uuid, text, date, date, text, text)
from public, anon, authenticated;
grant execute on function public.revise_agentic_rectification_evidence(uuid, uuid, uuid, text, date, date, text, text)
to service_role;
-- ---------------------------------------------------------------------------
-- 19. Legacy Agentic backfill (one-time, idempotent)
--
-- Mapping rules:
-- * engine_confirmed result -> confirmed (terminal)
-- * user_accepted result only -> candidate_accepted (resumable)
-- * messages present, no selection -> collecting_evidence, or
-- candidate_ready when results exist
-- * empty session -> draft for the newest empty per user
-- only; older empties -> abandoned
-- * per user, at most one resumable -> latest activity wins; the rest of
-- the resumable candidates are
-- superseded (read-only history kept)
-- * no confirmed evidence is ever generated from chat text
-- * agentic_rectification_results rows are mapped to the backfilled case_id
-- ---------------------------------------------------------------------------
create or replace function public.backfill_agentic_rectification_legacy_cases()
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_row record;
v_case_id uuid;
v_fingerprint text;
v_snapshot jsonb;
v_range jsonb;
v_profile public.profiles%rowtype;
v_selected_time time without time zone;
v_result_range jsonb;
v_base_birth_date date;
v_base_lat double precision;
v_base_lon double precision;
v_base_tz double precision;
v_base_source text;
v_accepted_time time without time zone;
v_confirmed_time time without time zone;
v_legacy_sessions integer := 0;
v_created integer := 0;
v_mapped_results integer := 0;
v_confirmed_count integer := 0;
v_candidate_accepted_count integer := 0;
v_candidate_ready_count integer := 0;
v_collecting_count integer := 0;
v_draft_count integer := 0;
v_abandoned_count integer := 0;
v_superseded_count integer := 0;
begin
for v_row in
with candidate as (
select
s.id as session_id,
s.user_id,
s.updated_at,
(jsonb_typeof(coalesce(s.messages, '[]'::jsonb)) = 'array'
and jsonb_array_length(coalesce(s.messages, '[]'::jsonb)) > 0) as has_messages,
exists (
select 1 from public.agentic_rectification_results r where r.session_id = s.id
) as has_results,
(select r.selection_kind
from public.agentic_rectification_results r
where r.session_id = s.id
and r.selection_kind in ('engine_confirmed', 'user_accepted')
order by r.created_at desc
limit 1) as result_kind,
(select count(*) from public.agentic_rectification_results r where r.session_id = s.id) as result_count
from public.chat_sessions s
where s.session_type = 'birth_time_rectification'
and not exists (
select 1 from public.agentic_rectification_cases c where c.session_id = s.id
)
),
based as (
select candidate.*,
case
when result_kind = 'engine_confirmed' then 'confirmed'
when result_kind = 'user_accepted' then 'candidate_accepted'
when has_messages then case when has_results then 'candidate_ready' else 'collecting_evidence' end
else 'abandoned'
end as base_status
from candidate
)
select ranked.*,
case
when base_status = any (public.agentic_rectification_resumable_statuses())
and resumable_rank > 1 then 'superseded'
when base_status = 'abandoned'
and resumable_rank = 1 and activity_rank = 1 then 'draft'
else base_status
end as final_status
from (
select based.*,
row_number() over (
partition by user_id
order by (base_status = any (public.agentic_rectification_resumable_statuses())) desc,
updated_at desc, session_id
) as resumable_rank,
row_number() over (
partition by user_id order by updated_at desc, session_id
) as activity_rank
from based
) ranked
order by user_id, updated_at desc, session_id
loop
v_legacy_sessions := v_legacy_sessions + 1;
v_range := null;
v_accepted_time := null;
v_confirmed_time := null;
v_selected_time := null;
v_result_range := null;
v_base_birth_date := null;
v_base_lat := null;
v_base_lon := null;
v_base_tz := null;
v_base_source := null;
select r.selected_time, r.candidate_range,
r.baseline_birth_date, r.baseline_latitude, r.baseline_longitude,
r.baseline_timezone_offset, r.baseline_birth_time_source
into v_selected_time, v_result_range,
v_base_birth_date, v_base_lat, v_base_lon, v_base_tz, v_base_source
from public.agentic_rectification_results r
where r.session_id = v_row.session_id
order by r.created_at desc
limit 1;
if v_result_range is not null and jsonb_typeof(v_result_range) = 'object'
and (v_result_range ->> 'start_time') ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$'
and (v_result_range ->> 'end_time') ~ '^([01][0-9]|2[0-3]):[0-5][0-9]$' then
v_range := v_result_range;
end if;
if v_row.final_status = 'confirmed' then
v_confirmed_time := v_selected_time;
elsif v_row.final_status = 'candidate_accepted' then
v_accepted_time := v_selected_time;
end if;
select * into v_profile from public.profiles where id = v_row.user_id;
if v_range is null then
if v_profile.id is not null
and (v_profile.active_birth_time is not null or v_profile.reported_birth_time is not null) then
v_range := jsonb_build_object(
'start_time', coalesce(to_char(v_profile.active_birth_time, 'HH24:MI'), to_char(v_profile.reported_birth_time, 'HH24:MI')),
'end_time', coalesce(to_char(v_profile.active_birth_time, 'HH24:MI'), to_char(v_profile.reported_birth_time, 'HH24:MI'))
);
else
v_range := jsonb_build_object('start_time', '00:00', 'end_time', '23:59');
end if;
end if;
v_snapshot := jsonb_build_object(
'birth_date', to_char(coalesce(v_profile.birth_date, v_base_birth_date), 'YYYY-MM-DD'),
'latitude', coalesce(v_profile.latitude, v_base_lat),
'longitude', coalesce(v_profile.longitude, v_base_lon),
'timezone_offset', coalesce(v_profile.timezone_offset, v_base_tz),
'birth_time_source', coalesce(v_profile.birth_time_source, v_base_source, 'legacy_import')
);
v_fingerprint := public.agentic_rectification_legacy_fingerprint(
v_row.user_id, v_row.session_id, v_row.final_status
);
insert into public.agentic_rectification_cases (
user_id, session_id, status, skill_name, skill_version,
baseline_profile_fingerprint, baseline_birth_snapshot, candidate_range,
accepted_time, confirmed_time, last_activity_at, completed_at
) values (
v_row.user_id, v_row.session_id, v_row.final_status,
'jyotish-birth-time-rectification', '9.0.0',
v_fingerprint, v_snapshot, v_range,
v_accepted_time, v_confirmed_time, v_row.updated_at,
case when v_row.final_status in ('confirmed', 'closed') then v_row.updated_at else null end
) returning id into v_case_id;
update public.agentic_rectification_results
set case_id = v_case_id
where session_id = v_row.session_id and case_id is null;
v_mapped_results := v_mapped_results + v_row.result_count;
v_created := v_created + 1;
if v_row.final_status = 'confirmed' then
v_confirmed_count := v_confirmed_count + 1;
elsif v_row.final_status = 'candidate_accepted' then
v_candidate_accepted_count := v_candidate_accepted_count + 1;
elsif v_row.final_status = 'candidate_ready' then
v_candidate_ready_count := v_candidate_ready_count + 1;
elsif v_row.final_status = 'collecting_evidence' then
v_collecting_count := v_collecting_count + 1;
elsif v_row.final_status = 'draft' then
v_draft_count := v_draft_count + 1;
elsif v_row.final_status = 'abandoned' then
v_abandoned_count := v_abandoned_count + 1;
elsif v_row.final_status = 'superseded' then
v_superseded_count := v_superseded_count + 1;
end if;
end loop;
return jsonb_build_object(
'legacy_sessions_scanned', v_legacy_sessions,
'cases_created', v_created,
'results_mapped', v_mapped_results,
'confirmed', v_confirmed_count,
'candidate_accepted', v_candidate_accepted_count,
'candidate_ready', v_candidate_ready_count,
'collecting_evidence', v_collecting_count,
'draft', v_draft_count,
'abandoned', v_abandoned_count,
'superseded', v_superseded_count
);
end;
$$;
revoke all on function public.backfill_agentic_rectification_legacy_cases()
from public, anon, authenticated;
grant execute on function public.backfill_agentic_rectification_legacy_cases()
to service_role;
-- ---------------------------------------------------------------------------
-- 20. Backfill verification (idempotent; can be re-run anytime)
-- ---------------------------------------------------------------------------
create or replace function public.verify_agentic_rectification_backfill()
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_total_cases bigint;
v_resumable bigint;
v_terminal bigint;
v_unmapped_results bigint;
v_legacy_without_case bigint;
v_orphan_cases bigint;
v_conflicts bigint;
begin
select count(*) into v_total_cases from public.agentic_rectification_cases;
select count(*) into v_resumable
from public.agentic_rectification_cases
where status = any (public.agentic_rectification_resumable_statuses());
select count(*) into v_terminal
from public.agentic_rectification_cases
where status in ('confirmed', 'closed', 'abandoned', 'superseded');
select count(*) into v_unmapped_results
from public.agentic_rectification_results
where case_id is null;
select count(*) into v_legacy_without_case
from public.chat_sessions s
where s.session_type = 'birth_time_rectification'
and not exists (
select 1 from public.agentic_rectification_cases c where c.session_id = s.id
);
select count(*) into v_orphan_cases
from public.agentic_rectification_cases c
left join public.chat_sessions s on s.id = c.session_id
where s.id is null;
select count(*) into v_conflicts
from (
select user_id
from public.agentic_rectification_cases
where status = any (public.agentic_rectification_resumable_statuses())
group by user_id
having count(*) > 1
) conflicts;
return jsonb_build_object(
'total_cases', v_total_cases,
'resumable_cases', v_resumable,
'terminal_cases', v_terminal,
'unmapped_results', v_unmapped_results,
'legacy_sessions_without_case', v_legacy_without_case,
'orphan_cases', v_orphan_cases,
'resumable_conflicts', v_conflicts
);
end;
$$;
revoke all on function public.verify_agentic_rectification_backfill()
from public, anon, authenticated;
grant execute on function public.verify_agentic_rectification_backfill()
to service_role;
-- ---------------------------------------------------------------------------
-- 21. Run the one-time backfill inside this migration (apply). The app never
-- runs bulk legacy migration at startup.
-- ---------------------------------------------------------------------------
do $$
declare
v_result jsonb;
begin
select public.backfill_agentic_rectification_legacy_cases() into v_result;
raise notice 'agentic_rectification_v9 backfill: %', v_result;
end;
$$;
commit;