1484 lines
56 KiB
PL/PgSQL
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;
|