Files
Jyotisha/frontend/supabase/migrations/20260814020000_rectification_v10_runtime.sql

2056 lines
76 KiB
PL/PgSQL

-- Rectification V10 durable conversation/runtime contract.
-- Additive only: V9 RPC signatures remain available for older runners.
begin;
-- ---------------------------------------------------------------------------
-- 1. Durable run attempts and receipt ownership
-- ---------------------------------------------------------------------------
create table if not exists public.agentic_rectification_run_attempts (
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,
attempt_number integer not null check (attempt_number > 0),
status text not null default 'started' check (
status in ('started', 'completed', 'failed', 'retryable', 'aborted')
),
error_code text,
usage jsonb not null default '{}'::jsonb check (jsonb_typeof(usage) = 'object'),
started_at timestamptz not null default pg_catalog.now(),
completed_at timestamptz,
updated_at timestamptz not null default pg_catalog.now(),
unique (turn_id, attempt_number),
unique (id, case_id, turn_id),
check (status = 'started' or completed_at is not null)
);
create index if not exists agentic_rectification_run_attempts_turn_idx
on public.agentic_rectification_run_attempts (case_id, turn_id, attempt_number desc);
create index if not exists agentic_rectification_run_attempts_success_idx
on public.agentic_rectification_run_attempts (turn_id, completed_at desc)
where status = 'completed';
alter table public.agentic_rectification_run_phases
add column if not exists attempt_id uuid references public.agentic_rectification_run_attempts(id) on delete cascade;
alter table public.agentic_rectification_tool_receipts
add column if not exists attempt_id uuid references public.agentic_rectification_run_attempts(id) on delete cascade;
alter table public.agentic_rectification_turns
add column if not exists successful_attempt_id uuid references public.agentic_rectification_run_attempts(id) on delete set null;
alter table public.agentic_rectification_turns
add column if not exists request_id uuid;
create unique index if not exists agentic_rectification_turns_case_request_idx
on public.agentic_rectification_turns (case_id, request_id)
where request_id is not null;
-- Keep the storage-layer allowlists aligned with the V10 RPC allowlists. The
-- original V9 CHECK constraints would otherwise reject valid V10 receipts.
alter table public.agentic_rectification_run_phases
drop constraint if exists agentic_rectification_run_phases_phase_check,
add constraint agentic_rectification_run_phases_phase_check check (
phase in (
'run.started', 'skill.started', 'skill.loaded', 'skill.bound', 'case.loaded',
'intent.classified', 'evidence.proposed', 'evidence.confirmed',
'candidates.comparing', 'candidates.updated', 'diagnostics.completed',
'candidate.accepted', 'birth_time.confirmed', 'answer.composed',
'billing.settled', 'answer.delta', 'run.completed', 'run.failed'
)
);
alter table public.agentic_rectification_tool_receipts
drop constraint if exists agentic_rectification_tool_receipts_tool_name_check,
add constraint agentic_rectification_tool_receipts_tool_name_check check (
tool_name in (
'rectification-read-case', 'rectification-set-focus',
'rectification-resolve-focus', 'rectification-record-evidence-batch',
'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'
)
),
drop constraint if exists agentic_rectification_tool_receipts_public_phase_check,
add constraint agentic_rectification_tool_receipts_public_phase_check check (
public_phase in (
'run.started', 'skill.started', 'skill.loaded', 'skill.bound', 'case.loaded',
'intent.classified', 'evidence.proposed', 'evidence.confirmed',
'candidates.comparing', 'candidates.updated', 'diagnostics.completed',
'candidate.accepted', 'birth_time.confirmed', 'answer.composed',
'billing.settled', 'answer.delta', 'run.completed', 'run.failed'
)
);
-- Enforce Case/Turn/Attempt scope below the SECURITY DEFINER RPC layer.
alter table public.agentic_rectification_turns
drop constraint if exists agentic_rectification_turns_id_case_id_key,
add constraint agentic_rectification_turns_id_case_id_key unique (id, case_id);
alter table public.agentic_rectification_run_attempts
drop constraint if exists agentic_rectification_run_attempts_turn_case_fkey,
add constraint agentic_rectification_run_attempts_turn_case_fkey
foreign key (turn_id, case_id)
references public.agentic_rectification_turns(id, case_id) on delete cascade;
alter table public.agentic_rectification_run_phases
drop constraint if exists agentic_rectification_run_phases_attempt_scope_fkey,
add constraint agentic_rectification_run_phases_attempt_scope_fkey
foreign key (attempt_id, case_id, turn_id)
references public.agentic_rectification_run_attempts(id, case_id, turn_id) on delete cascade;
alter table public.agentic_rectification_tool_receipts
drop constraint if exists agentic_rectification_tool_receipts_attempt_scope_fkey,
add constraint agentic_rectification_tool_receipts_attempt_scope_fkey
foreign key (attempt_id, case_id, turn_id)
references public.agentic_rectification_run_attempts(id, case_id, turn_id) on delete cascade;
create index if not exists agentic_rectification_run_phases_attempt_idx
on public.agentic_rectification_run_phases (attempt_id, sequence);
create index if not exists agentic_rectification_tool_receipts_attempt_idx
on public.agentic_rectification_tool_receipts (attempt_id, started_at);
-- ---------------------------------------------------------------------------
-- 2. ConversationFocus and server-owned CaseConversationSummary
-- ---------------------------------------------------------------------------
create table if not exists public.agentic_rectification_conversation_focuses (
id uuid primary key default gen_random_uuid(),
case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade,
question_id text not null check (length(btrim(question_id)) > 0),
intent text not null check (length(btrim(intent)) > 0),
target_evidence_id uuid references public.agentic_rectification_evidence(id) on delete set null,
target_domain text check (
target_domain is null or target_domain in (
'education', 'career', 'relationship', 'relocation', 'finance', 'health', 'family', 'other'
)
),
target_kind text check (
target_kind is null or target_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'
)
),
expected_answer_schema jsonb not null default '{}'::jsonb
check (jsonb_typeof(expected_answer_schema) = 'object'),
status text not null default 'active' check (
status in ('active', 'resolved', 'declined', 'skipped', 'superseded')
),
asked_at timestamptz not null default pg_catalog.now(),
resolved_at timestamptz,
created_at timestamptz not null default pg_catalog.now(),
updated_at timestamptz not null default pg_catalog.now(),
unique (case_id, question_id),
check (
(status = 'active' and resolved_at is null)
or (status <> 'active' and resolved_at is not null)
)
);
create unique index if not exists agentic_rectification_one_active_focus_per_case
on public.agentic_rectification_conversation_focuses (case_id)
where status = 'active';
create index if not exists agentic_rectification_focus_case_history_idx
on public.agentic_rectification_conversation_focuses (case_id, asked_at desc);
create index if not exists agentic_rectification_focus_evidence_idx
on public.agentic_rectification_conversation_focuses (target_evidence_id)
where target_evidence_id is not null;
create table if not exists public.agentic_rectification_case_conversation_summaries (
case_id uuid primary key references public.agentic_rectification_cases(id) on delete cascade,
confirmed_evidence_summary jsonb not null default '[]'::jsonb
check (jsonb_typeof(confirmed_evidence_summary) = 'array'),
pending_revisions jsonb not null default '[]'::jsonb
check (jsonb_typeof(pending_revisions) = 'array'),
active_focus jsonb,
declined_skipped_topics jsonb not null default '[]'::jsonb
check (jsonb_typeof(declined_skipped_topics) = 'array'),
candidate_divergence_summary jsonb,
missing_evidence_categories jsonb not null default '[]'::jsonb
check (jsonb_typeof(missing_evidence_categories) = 'array'),
last_result_policy jsonb,
summary_version integer not null default 1 check (summary_version > 0),
updated_at timestamptz not null default pg_catalog.now(),
check (active_focus is null or jsonb_typeof(active_focus) = 'object'),
check (candidate_divergence_summary is null or jsonb_typeof(candidate_divergence_summary) = 'object'),
check (last_result_policy is null or jsonb_typeof(last_result_policy) = 'object')
);
-- Batch idempotency is per Case and per independently parsed event.
alter table public.agentic_rectification_evidence
add column if not exists idempotency_key text;
create unique index if not exists agentic_rectification_evidence_idempotency_idx
on public.agentic_rectification_evidence (case_id, idempotency_key)
where idempotency_key is not null;
-- ---------------------------------------------------------------------------
-- 3. RLS and grants: service role owns writes; browser receives projections.
-- ---------------------------------------------------------------------------
alter table public.agentic_rectification_run_attempts enable row level security;
alter table public.agentic_rectification_conversation_focuses enable row level security;
alter table public.agentic_rectification_case_conversation_summaries enable row level security;
revoke all on table public.agentic_rectification_run_attempts from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_conversation_focuses from public, anon, authenticated, service_role;
revoke all on table public.agentic_rectification_case_conversation_summaries from public, anon, authenticated, service_role;
-- No direct table DML is granted. Runtime access is only through the scoped
-- SECURITY DEFINER RPCs below. Revoke inherited V9 DML on receipt tables too;
-- their legacy and V10 write paths are SECURITY DEFINER functions.
revoke insert, update, delete, truncate
on table public.agentic_rectification_run_phases,
public.agentic_rectification_tool_receipts
from service_role;
-- ---------------------------------------------------------------------------
-- 4. Server summary rebuild. No raw quote, birth snapshot, score or reasoning.
-- ---------------------------------------------------------------------------
create or replace function public.refresh_agentic_rectification_case_conversation_summary(
p_case_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_confirmed jsonb;
v_pending jsonb;
v_active_focus jsonb;
v_declined_skipped jsonb;
v_candidate_divergence jsonb;
v_missing jsonb;
v_last_policy jsonb;
v_result public.agentic_rectification_results%rowtype;
v_summary jsonb;
begin
if p_case_id is null or not exists (
select 1 from public.agentic_rectification_cases where id = p_case_id
) then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
select coalesce(jsonb_agg(jsonb_build_object(
'evidence_id', e.id,
'summary', e.summary,
'event_kind', e.event_kind,
'domain', e.domain,
'date_precision', e.date_precision,
'occurred_from', e.occurred_from,
'occurred_to', e.occurred_to
) order by e.created_at, e.id), '[]'::jsonb)
into v_confirmed
from public.agentic_rectification_evidence e
where e.case_id = p_case_id and e.status = 'confirmed';
select coalesce(jsonb_agg(jsonb_build_object(
'evidence_id', e.id,
'supersedes_evidence_id', e.supersedes_evidence_id,
'summary', e.summary,
'event_kind', e.event_kind,
'domain', e.domain,
'date_precision', e.date_precision,
'occurred_from', e.occurred_from,
'occurred_to', e.occurred_to
) order by e.created_at, e.id), '[]'::jsonb)
into v_pending
from public.agentic_rectification_evidence e
where e.case_id = p_case_id
and e.status in ('draft', 'pending_confirmation');
select jsonb_build_object(
'id', f.id,
'case_id', f.case_id,
'question_id', f.question_id,
'intent', f.intent,
'target_evidence_id', f.target_evidence_id,
'target_domain', f.target_domain,
'target_kind', f.target_kind,
'expected_answer_schema', f.expected_answer_schema,
'status', f.status,
'asked_at', f.asked_at,
'resolved_at', f.resolved_at
) into v_active_focus
from public.agentic_rectification_conversation_focuses f
where f.case_id = p_case_id and f.status = 'active'
order by f.asked_at desc, f.id
limit 1;
select coalesce(jsonb_agg(jsonb_build_object(
'focus_id', f.id,
'intent', f.intent,
'target_evidence_id', f.target_evidence_id,
'target_domain', f.target_domain,
'target_kind', f.target_kind,
'status', f.status,
'asked_at', f.asked_at,
'resolved_at', f.resolved_at
) order by f.resolved_at, f.id), '[]'::jsonb)
into v_declined_skipped
from public.agentic_rectification_conversation_focuses f
where f.case_id = p_case_id and f.status in ('declined', 'skipped');
select * into v_result
from public.agentic_rectification_results
where case_id = p_case_id and invalidated_at is null
order by created_at desc, id desc
limit 1;
if v_result.id is null then
v_candidate_divergence := null;
v_last_policy := null;
else
v_candidate_divergence := jsonb_build_object(
'result_id', v_result.id,
'overall_confidence', v_result.overall_confidence,
'margin_percent', v_result.margin_percent,
'candidate_count', jsonb_array_length(coalesce(v_result.candidates, '[]'::jsonb)),
'tied_candidate_count', (
select count(*)
from jsonb_array_elements(coalesce(v_result.candidates, '[]'::jsonb)) candidate
where coalesce((candidate->>'tied_minute_count')::integer, 0) > 1
)
);
v_last_policy := jsonb_build_object(
'result_id', v_result.id,
'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,
'algorithm_version', v_result.algorithm_version
);
end if;
select coalesce(jsonb_agg(domain order by ordinal), '[]'::jsonb)
into v_missing
from unnest(array[
'education', 'career', 'relationship', 'relocation',
'finance', 'health', 'family'
]::text[]) with ordinality as required(domain, ordinal)
where not exists (
select 1
from public.agentic_rectification_evidence e
where e.case_id = p_case_id and e.status = 'confirmed' and e.domain = required.domain
);
insert into public.agentic_rectification_case_conversation_summaries (
case_id,
confirmed_evidence_summary,
pending_revisions,
active_focus,
declined_skipped_topics,
candidate_divergence_summary,
missing_evidence_categories,
last_result_policy,
summary_version,
updated_at
) values (
p_case_id,
v_confirmed,
v_pending,
v_active_focus,
v_declined_skipped,
v_candidate_divergence,
v_missing,
v_last_policy,
1,
pg_catalog.now()
)
on conflict (case_id) do update set
confirmed_evidence_summary = excluded.confirmed_evidence_summary,
pending_revisions = excluded.pending_revisions,
active_focus = excluded.active_focus,
declined_skipped_topics = excluded.declined_skipped_topics,
candidate_divergence_summary = excluded.candidate_divergence_summary,
missing_evidence_categories = excluded.missing_evidence_categories,
last_result_policy = excluded.last_result_policy,
summary_version = excluded.summary_version,
updated_at = excluded.updated_at;
v_summary := jsonb_build_object(
'confirmed_evidence_summary', v_confirmed,
'pending_revisions', v_pending,
'active_focus', v_active_focus,
'declined_skipped_topics', v_declined_skipped,
'candidate_divergence_summary', v_candidate_divergence,
'missing_evidence_categories', v_missing,
'last_result_policy', v_last_policy,
'summary_version', 1
);
return v_summary;
end;
$$;
revoke all on function public.refresh_agentic_rectification_case_conversation_summary(uuid)
from public, anon, authenticated;
grant execute on function public.refresh_agentic_rectification_case_conversation_summary(uuid)
to service_role;
create or replace function public.agentic_rectification_refresh_conversation_summary_trigger()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
declare
v_old_case_id uuid := case when tg_op in ('UPDATE', 'DELETE') then old.case_id else null end;
v_new_case_id uuid := case when tg_op in ('INSERT', 'UPDATE') then new.case_id else null end;
begin
if v_old_case_id is not null and exists (
select 1 from public.agentic_rectification_cases where id = v_old_case_id
) then
perform public.refresh_agentic_rectification_case_conversation_summary(v_old_case_id);
end if;
if v_new_case_id is not null
and v_new_case_id is distinct from v_old_case_id
and exists (
select 1 from public.agentic_rectification_cases where id = v_new_case_id
) then
perform public.refresh_agentic_rectification_case_conversation_summary(v_new_case_id);
end if;
return case when tg_op = 'DELETE' then old else new end;
end;
$$;
revoke all on function public.agentic_rectification_refresh_conversation_summary_trigger()
from public, anon, authenticated;
drop trigger if exists agentic_rectification_evidence_refresh_summary
on public.agentic_rectification_evidence;
create trigger agentic_rectification_evidence_refresh_summary
after insert or update or delete on public.agentic_rectification_evidence
for each row execute function public.agentic_rectification_refresh_conversation_summary_trigger();
drop trigger if exists agentic_rectification_focus_refresh_summary
on public.agentic_rectification_conversation_focuses;
create trigger agentic_rectification_focus_refresh_summary
after insert or update or delete on public.agentic_rectification_conversation_focuses
for each row execute function public.agentic_rectification_refresh_conversation_summary_trigger();
drop trigger if exists agentic_rectification_result_refresh_summary
on public.agentic_rectification_results;
create trigger agentic_rectification_result_refresh_summary
after insert or update or delete on public.agentic_rectification_results
for each row execute function public.agentic_rectification_refresh_conversation_summary_trigger();
-- Terminal cases cannot retain an active question. Evidence/focus RPCs below
-- also reject all new writes once the Case is terminal.
create or replace function public.agentic_rectification_terminal_focus_guard()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
if new.status in ('confirmed', 'closed', 'abandoned', 'superseded')
and old.status is distinct from new.status then
update public.agentic_rectification_conversation_focuses
set status = 'superseded',
resolved_at = coalesce(resolved_at, pg_catalog.now()),
updated_at = pg_catalog.now()
where case_id = new.id and status = 'active';
end if;
return new;
end;
$$;
revoke all on function public.agentic_rectification_terminal_focus_guard()
from public, anon, authenticated;
drop trigger if exists agentic_rectification_cases_terminal_focus_guard
on public.agentic_rectification_cases;
create trigger agentic_rectification_cases_terminal_focus_guard
after update of status on public.agentic_rectification_cases
for each row execute function public.agentic_rectification_terminal_focus_guard();
-- Existing cases receive a durable summary before the new dossier is used.
select public.refresh_agentic_rectification_case_conversation_summary(id)
from public.agentic_rectification_cases;
-- ---------------------------------------------------------------------------
-- 5. Focus RPCs. The server, not the model, validates the active target.
-- ---------------------------------------------------------------------------
create or replace function public.set_agentic_rectification_conversation_focus(
p_user_id uuid,
p_case_id uuid,
p_question_id text,
p_intent text,
p_target_evidence_id uuid,
p_target_domain text,
p_target_kind text,
p_expected_answer_schema jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_focus public.agentic_rectification_conversation_focuses%rowtype;
begin
if p_user_id is null or p_case_id is null
or length(btrim(coalesce(p_question_id, ''))) = 0
or length(btrim(coalesce(p_intent, ''))) = 0
or p_expected_answer_schema is null
or jsonb_typeof(p_expected_answer_schema) <> 'object' 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
for update;
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;
if p_target_evidence_id is not null and not exists (
select 1 from public.agentic_rectification_evidence
where id = p_target_evidence_id and case_id = p_case_id
) then
raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
end if;
select * into v_focus
from public.agentic_rectification_conversation_focuses
where case_id = p_case_id and question_id = p_question_id;
if found then
if v_focus.status = 'active'
and v_focus.intent = p_intent
and v_focus.target_evidence_id is not distinct from p_target_evidence_id
and v_focus.target_domain is not distinct from p_target_domain
and v_focus.target_kind is not distinct from p_target_kind
and v_focus.expected_answer_schema = p_expected_answer_schema then
return jsonb_build_object(
'focus_id', v_focus.id, 'status', v_focus.status, 'idempotent', true
);
end if;
raise exception 'agentic_rectification_focus_idempotency_conflict' using errcode = 'P0001';
end if;
update public.agentic_rectification_conversation_focuses
set status = 'superseded',
resolved_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where case_id = p_case_id and status = 'active';
insert into public.agentic_rectification_conversation_focuses (
case_id, question_id, intent, target_evidence_id, target_domain, target_kind,
expected_answer_schema, status, asked_at
) values (
p_case_id, p_question_id, p_intent, p_target_evidence_id, p_target_domain, p_target_kind,
p_expected_answer_schema, 'active', pg_catalog.now()
) returning * into v_focus;
return jsonb_build_object(
'focus_id', v_focus.id,
'status', v_focus.status,
'idempotent', false
);
end;
$$;
revoke all on function public.set_agentic_rectification_conversation_focus(
uuid, uuid, text, text, uuid, text, text, jsonb
) from public, anon, authenticated;
grant execute on function public.set_agentic_rectification_conversation_focus(
uuid, uuid, text, text, uuid, text, text, jsonb
) to service_role;
create or replace function public.resolve_agentic_rectification_conversation_focus(
p_user_id uuid,
p_case_id uuid,
p_focus_id uuid,
p_status text,
p_evidence_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_focus public.agentic_rectification_conversation_focuses%rowtype;
begin
if p_user_id is null or p_case_id is null or p_focus_id is null
or p_status not in ('resolved', 'declined', 'skipped', 'superseded') 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_focus
from public.agentic_rectification_conversation_focuses
where id = p_focus_id and case_id = p_case_id
for update;
if not found then
raise exception 'agentic_rectification_focus_not_found' using errcode = 'P0001';
end if;
if p_evidence_id is not null and not exists (
select 1 from public.agentic_rectification_evidence
where id = p_evidence_id and case_id = p_case_id
) then
raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
end if;
if v_focus.target_evidence_id is not null
and p_evidence_id is distinct from v_focus.target_evidence_id then
raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
end if;
if v_focus.status <> 'active' then
if v_focus.status = p_status
and (v_focus.target_evidence_id is null or v_focus.target_evidence_id is not distinct from p_evidence_id) then
return jsonb_build_object(
'focus_id', v_focus.id, 'status', v_focus.status,
'evidence_id', v_focus.target_evidence_id, 'idempotent', true
);
end if;
raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
end if;
update public.agentic_rectification_conversation_focuses
set status = p_status,
target_evidence_id = coalesce(target_evidence_id, p_evidence_id),
resolved_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = v_focus.id;
return jsonb_build_object(
'focus_id', v_focus.id,
'status', p_status,
'evidence_id', coalesce(v_focus.target_evidence_id, p_evidence_id),
'idempotent', false
);
end;
$$;
revoke all on function public.resolve_agentic_rectification_conversation_focus(
uuid, uuid, uuid, text, uuid
) from public, anon, authenticated;
grant execute on function public.resolve_agentic_rectification_conversation_focus(
uuid, uuid, uuid, text, uuid
) to service_role;
create or replace function public.confirm_agentic_rectification_evidence_v10(
p_user_id uuid,
p_case_id uuid,
p_focus_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_focus public.agentic_rectification_conversation_focuses%rowtype;
v_result jsonb;
begin
if p_user_id is null or p_case_id is null or p_focus_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
for update;
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;
if not exists (
select 1 from public.agentic_rectification_evidence
where id = p_evidence_id and case_id = p_case_id
) then
raise exception 'agentic_rectification_evidence_not_found' using errcode = 'P0001';
end if;
select * into v_focus
from public.agentic_rectification_conversation_focuses
where id = p_focus_id and case_id = p_case_id
for update;
if not found then
raise exception 'agentic_rectification_focus_not_found' using errcode = 'P0001';
end if;
if v_focus.target_evidence_id is not null
and v_focus.target_evidence_id <> p_evidence_id then
raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
end if;
if v_focus.status not in ('active', 'resolved') then
raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
end if;
if v_focus.status = 'resolved'
and v_focus.target_evidence_id is distinct from p_evidence_id then
raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
end if;
v_result := public.confirm_agentic_rectification_evidence(
p_user_id, p_case_id, p_evidence_id
);
if v_focus.status = 'active' then
perform public.resolve_agentic_rectification_conversation_focus(
p_user_id, p_case_id, p_focus_id, 'resolved', p_evidence_id
);
end if;
return v_result || jsonb_build_object('focus_id', p_focus_id);
end;
$$;
revoke all on function public.confirm_agentic_rectification_evidence_v10(uuid, uuid, uuid, uuid)
from public, anon, authenticated;
grant execute on function public.confirm_agentic_rectification_evidence_v10(uuid, uuid, uuid, uuid)
to service_role;
create or replace function public.revise_agentic_rectification_evidence_v10(
p_user_id uuid,
p_case_id uuid,
p_focus_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_focus public.agentic_rectification_conversation_focuses%rowtype;
v_existing public.agentic_rectification_evidence%rowtype;
v_result jsonb;
begin
if p_user_id is null or p_case_id is null or p_focus_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
for update;
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_focus
from public.agentic_rectification_conversation_focuses
where id = p_focus_id and case_id = p_case_id
for update;
if not found then
raise exception 'agentic_rectification_focus_not_found' using errcode = 'P0001';
end if;
if v_focus.target_evidence_id is not null
and v_focus.target_evidence_id <> p_evidence_id then
raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
end if;
if v_focus.status not in ('active', 'resolved') then
raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
end if;
if v_focus.status = 'resolved' then
if v_focus.target_evidence_id is distinct from p_evidence_id then
raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
end if;
select * into v_existing
from public.agentic_rectification_evidence
where case_id = p_case_id
and supersedes_evidence_id = p_evidence_id
and user_quote = p_user_quote
and occurred_from is not distinct from p_occurred_from
and occurred_to is not distinct from p_occurred_to
and date_precision = p_date_precision
and summary = p_summary
order by created_at desc, id desc
limit 1;
if not found then
raise exception 'agentic_rectification_focus_idempotency_conflict' using errcode = 'P0001';
end if;
return jsonb_build_object(
'evidence_id', v_existing.id,
'supersedes_evidence_id', p_evidence_id,
'focus_id', p_focus_id,
'idempotent', true
);
end if;
v_result := public.revise_agentic_rectification_evidence(
p_user_id, p_case_id, p_evidence_id, p_user_quote,
p_occurred_from, p_occurred_to, p_date_precision, p_summary
);
perform public.resolve_agentic_rectification_conversation_focus(
p_user_id, p_case_id, p_focus_id, 'resolved', p_evidence_id
);
return v_result || jsonb_build_object('focus_id', p_focus_id);
end;
$$;
revoke all on function public.revise_agentic_rectification_evidence_v10(
uuid, uuid, uuid, uuid, text, date, date, text, text
) from public, anon, authenticated;
grant execute on function public.revise_agentic_rectification_evidence_v10(
uuid, uuid, uuid, uuid, text, date, date, text, text
) to service_role;
-- ---------------------------------------------------------------------------
-- 6. Batch evidence: per-item validation/result and transactional idempotency.
-- ---------------------------------------------------------------------------
create or replace function public.record_agentic_rectification_evidence_batch(
p_user_id uuid,
p_case_id uuid,
p_source_turn_id uuid,
p_focus_id uuid,
p_items jsonb
)
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_focus public.agentic_rectification_conversation_focuses%rowtype;
v_focus_replay boolean := false;
v_focus_match_id uuid;
v_focus_match_count integer := 0;
v_focus_resolution text := 'not_requested';
v_item jsonb;
v_index bigint;
v_item_key text;
v_quote text;
v_subject text;
v_kind text;
v_domain text;
v_precision text;
v_summary text;
v_from date;
v_to date;
v_status text;
v_outcome text;
v_error text;
v_clarification jsonb;
v_existing public.agentic_rectification_evidence%rowtype;
v_evidence_id uuid;
v_idempotent boolean;
v_results jsonb := '[]'::jsonb;
v_accepted integer := 0;
v_needs integer := 0;
v_rejected integer := 0;
begin
if p_user_id is null or p_case_id is null or p_source_turn_id is null
or p_items is null or jsonb_typeof(p_items) <> 'array'
or jsonb_array_length(p_items) = 0 or jsonb_array_length(p_items) > 12 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
for update;
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 then
raise exception 'agentic_rectification_quote_not_grounded' using errcode = 'P0001';
end if;
if p_focus_id is not null then
select * into v_focus
from public.agentic_rectification_conversation_focuses
where id = p_focus_id and case_id = p_case_id
for update;
if not found or v_focus.status not in ('active', 'resolved') then
raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
end if;
v_focus_replay := v_focus.status = 'resolved';
if not v_focus_replay and v_focus.target_evidence_id is not null then
raise exception 'agentic_rectification_focus_target_mismatch' using errcode = 'P0001';
end if;
v_focus_resolution := case when v_focus_replay then 'idempotent' else 'active' end;
if v_focus_replay and exists (
select 1
from jsonb_array_elements(p_items) item
where length(btrim(coalesce(item->>'idempotency_key', ''))) = 0
or not exists (
select 1 from public.agentic_rectification_evidence e
where e.case_id = p_case_id
and e.idempotency_key = btrim(item->>'idempotency_key')
)
) then
raise exception 'agentic_rectification_focus_not_active' using errcode = 'P0001';
end if;
end if;
for v_item, v_index in
select value, ordinality
from jsonb_array_elements(p_items) with ordinality
loop
v_item_key := btrim(coalesce(v_item->>'idempotency_key', ''));
v_quote := btrim(coalesce(v_item->>'quote', ''));
v_subject := coalesce(v_item->>'subject', '');
v_kind := coalesce(v_item->>'event_kind', '');
v_domain := coalesce(v_item->>'domain', '');
v_precision := coalesce(v_item->>'date_precision', '');
v_summary := btrim(coalesce(v_item->>'summary', ''));
v_from := null;
v_to := null;
v_error := null;
v_clarification := '[]'::jsonb;
v_evidence_id := null;
v_idempotent := false;
begin
if nullif(v_item->>'occurred_from', '') is not null then
v_from := (v_item->>'occurred_from')::date;
end if;
if nullif(v_item->>'occurred_to', '') is not null then
v_to := (v_item->>'occurred_to')::date;
end if;
exception when others then
v_error := 'invalid_date';
end;
if v_error is null and (
length(v_item_key) = 0 or length(v_item_key) > 160
or length(v_quote) = 0 or length(v_summary) = 0
or v_subject not in ('self', 'family', 'other')
or v_kind not 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'
)
or v_domain not in ('education', 'career', 'relationship', 'relocation', 'finance', 'health', 'family', 'other')
or v_precision not in ('year', 'month', 'day', 'range', 'unknown')
) then
v_error := 'invalid_item';
end if;
if v_error is null and position(
public.agentic_rectification_normalize_quote(v_quote)
in public.agentic_rectification_normalize_quote(v_turn.user_message)
) = 0 then
v_error := 'quote_not_grounded';
end if;
if v_error is null and v_precision = 'range'
and (v_from is null or v_to is null or v_from > v_to) then
v_error := 'invalid_range';
end if;
if v_error is not null then
v_outcome := 'rejected';
v_status := 'rejected';
v_rejected := v_rejected + 1;
else
select * into v_existing
from public.agentic_rectification_evidence
where case_id = p_case_id and idempotency_key = v_item_key;
if found then
if v_existing.source_turn_id is distinct from p_source_turn_id
or v_existing.user_quote is distinct from v_quote
or v_existing.subject is distinct from v_subject
or v_existing.event_kind is distinct from v_kind
or v_existing.domain is distinct from v_domain
or v_existing.occurred_from is distinct from v_from
or v_existing.occurred_to is distinct from v_to
or v_existing.date_precision is distinct from v_precision
or v_existing.summary is distinct from v_summary then
v_outcome := 'rejected';
v_status := 'rejected';
v_error := 'idempotency_conflict';
v_rejected := v_rejected + 1;
else
v_evidence_id := v_existing.id;
v_idempotent := true;
if v_existing.status = 'confirmed' then
v_outcome := 'accepted';
v_status := 'confirmed';
v_accepted := v_accepted + 1;
else
v_outcome := 'needs_clarification';
v_status := v_existing.status;
v_clarification := case
when v_existing.date_precision = 'unknown' then '["date"]'::jsonb
else '[]'::jsonb
end;
v_needs := v_needs + 1;
end if;
end if;
else
if v_precision = 'unknown' or v_from is null then
v_outcome := 'needs_clarification';
v_status := 'draft';
v_clarification := '["date"]'::jsonb;
v_needs := v_needs + 1;
else
v_outcome := 'accepted';
v_status := 'confirmed';
v_accepted := v_accepted + 1;
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,
confirmed_at, idempotency_key
) values (
p_case_id, p_source_turn_id, v_quote, v_subject, v_kind, v_domain,
v_from, v_to, v_precision, v_summary, v_status,
case when v_status = 'confirmed' then pg_catalog.now() else null end,
v_item_key
) returning id into v_evidence_id;
end if;
end if;
if p_focus_id is not null
and v_outcome = 'accepted'
and v_evidence_id is not null
and (v_focus.target_domain is null or v_domain = v_focus.target_domain)
and (v_focus.target_kind is null or v_kind = v_focus.target_kind) then
v_focus_match_count := v_focus_match_count + 1;
if v_focus_match_id is null then
v_focus_match_id := v_evidence_id;
end if;
end if;
v_results := v_results || jsonb_build_array(jsonb_build_object(
'index', v_index - 1,
'idempotency_key', nullif(v_item_key, ''),
'outcome', v_outcome,
'evidence_id', v_evidence_id,
'status', v_status,
'idempotent', v_idempotent,
'clarification_fields', v_clarification,
'error_code', v_error
));
end loop;
update public.agentic_rectification_cases
set status = case when status = 'draft' and v_accepted > 0 then 'collecting_evidence' else status end,
last_activity_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = p_case_id;
if p_focus_id is not null then
if v_focus_replay then
if v_focus_match_count <> 1
or v_focus_match_id is distinct from v_focus.target_evidence_id then
raise exception 'agentic_rectification_focus_idempotency_conflict' using errcode = 'P0001';
end if;
v_focus_resolution := 'idempotent';
elsif v_focus_match_count = 1 then
perform public.resolve_agentic_rectification_conversation_focus(
p_user_id, p_case_id, p_focus_id, 'resolved', v_focus_match_id
);
v_focus_resolution := 'resolved';
elsif v_focus_match_count > 1 then
-- Do not guess which evidence answered the question. Keep the focus active
-- and make the ambiguity explicit to the caller.
v_focus_resolution := 'ambiguous';
else
v_focus_resolution := 'unmatched';
end if;
end if;
return jsonb_build_object(
'items', v_results,
'accepted_count', v_accepted,
'needs_clarification_count', v_needs,
'rejected_count', v_rejected,
'focus_id', p_focus_id,
'focus_evidence_id', v_focus_match_id,
'focus_resolution', v_focus_resolution
);
end;
$$;
revoke all on function public.record_agentic_rectification_evidence_batch(
uuid, uuid, uuid, uuid, jsonb
) from public, anon, authenticated;
grant execute on function public.record_agentic_rectification_evidence_batch(
uuid, uuid, uuid, uuid, jsonb
) to service_role;
-- ---------------------------------------------------------------------------
-- 7. Attempt lifecycle and attempt-aware receipt insertions.
-- ---------------------------------------------------------------------------
-- Request-aware turn append. The seven-argument V9 function remains available.
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,
p_request_id uuid
)
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;
begin
if p_user_id is null or p_case_id is null or p_request_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
for update;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
select * into v_turn
from public.agentic_rectification_turns
where case_id = p_case_id and request_id = p_request_id
for update;
if found then
if v_turn.user_message is distinct from p_user_message
or v_turn.model_name is distinct from p_model_name
or v_turn.model_version is distinct from p_model_version then
raise exception 'agentic_rectification_request_mismatch' using errcode = 'P0001';
end if;
return jsonb_build_object(
'turn_id', v_turn.id,
'status', v_turn.status,
'assistant_message', v_turn.assistant_message,
'successful_attempt_id', v_turn.successful_attempt_id,
'should_execute', false,
'already_in_progress', v_turn.status = 'pending',
'idempotent', true
);
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, request_id, user_message, assistant_message, status,
model_name, model_version, completed_at
) values (
p_case_id, p_request_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 * into v_turn;
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,
'status', v_turn.status,
'assistant_message', v_turn.assistant_message,
'successful_attempt_id', v_turn.successful_attempt_id,
'should_execute', true,
'already_in_progress', false,
'idempotent', false
);
end;
$$;
revoke all on function public.append_agentic_rectification_turn(
uuid, uuid, text, text, text, text, text, uuid
) from public, anon, authenticated;
grant execute on function public.append_agentic_rectification_turn(
uuid, uuid, text, text, text, text, text, uuid
) to service_role;
-- V10 request idempotency is mandatory for every runtime Turn write. Keep the
-- V9 overload for migration compatibility, but remove it from service runtime.
revoke execute on function public.append_agentic_rectification_turn(
uuid, uuid, text, text, text, text, text
) from service_role;
create or replace function public.create_agentic_rectification_run_attempt(
p_user_id uuid,
p_case_id uuid,
p_turn_id uuid,
p_attempt_number integer
)
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_attempt public.agentic_rectification_run_attempts%rowtype;
v_inserted boolean := false;
begin
if p_user_id is null or p_case_id is null or p_turn_id is null
or coalesce(p_attempt_number, 0) <= 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
for update;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
select * into v_turn
from public.agentic_rectification_turns
where id = p_turn_id and case_id = p_case_id
for update;
if not found then
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
end if;
-- A terminal Case must not create a new attempt, but a worker recovering
-- after a confirm/close action still needs the identity of the attempt that
-- already owned execution so it can finish the durable Turn projection.
select * into v_attempt
from public.agentic_rectification_run_attempts
where turn_id = p_turn_id and attempt_number = p_attempt_number
for update;
if found then
return jsonb_build_object(
'attempt_id', v_attempt.id,
'attempt_number', v_attempt.attempt_number,
'status', v_attempt.status,
'should_execute', false,
'already_in_progress', v_attempt.status = 'started',
'idempotent', true
);
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_run_attempts (
case_id, turn_id, attempt_number, status
) values (
p_case_id, p_turn_id, p_attempt_number, 'started'
)
on conflict (turn_id, attempt_number) do nothing
returning * into v_attempt;
v_inserted := found;
if not v_inserted then
select * into v_attempt
from public.agentic_rectification_run_attempts
where turn_id = p_turn_id and attempt_number = p_attempt_number
for update;
end if;
return jsonb_build_object(
'attempt_id', v_attempt.id,
'attempt_number', v_attempt.attempt_number,
'status', v_attempt.status,
'should_execute', v_inserted,
'already_in_progress', not v_inserted and v_attempt.status = 'started',
'idempotent', not v_inserted
);
end;
$$;
revoke all on function public.create_agentic_rectification_run_attempt(uuid, uuid, uuid, integer)
from public, anon, authenticated;
grant execute on function public.create_agentic_rectification_run_attempt(uuid, uuid, uuid, integer)
to service_role;
create or replace function public.finalize_agentic_rectification_run_attempt(
p_user_id uuid,
p_case_id uuid,
p_turn_id uuid,
p_attempt_id uuid,
p_status text,
p_error_code text,
p_usage jsonb
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_case public.agentic_rectification_cases%rowtype;
v_attempt public.agentic_rectification_run_attempts%rowtype;
v_error_code text := nullif(btrim(coalesce(p_error_code, '')), '');
begin
if p_user_id is null or p_case_id is null or p_turn_id is null or p_attempt_id is null
or p_status not in ('completed', 'failed', 'retryable', 'aborted')
or p_usage is null or jsonb_typeof(p_usage) <> 'object' 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
for update;
if not found then
raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001';
end if;
select * into v_attempt
from public.agentic_rectification_run_attempts
where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id
for update;
if not found then
raise exception 'agentic_rectification_attempt_not_found' using errcode = 'P0001';
end if;
if v_attempt.status <> 'started' then
if v_attempt.status = p_status
and v_attempt.error_code is not distinct from v_error_code
and v_attempt.usage = p_usage then
return jsonb_build_object(
'attempt_id', v_attempt.id, 'status', v_attempt.status, 'idempotent', true
);
end if;
raise exception 'agentic_rectification_attempt_already_finalized' using errcode = 'P0001';
end if;
if p_status = 'completed' then
if not exists (
select 1 from public.agentic_rectification_run_phases
where attempt_id = p_attempt_id
and case_id = p_case_id
and turn_id = p_turn_id
and phase = 'billing.settled'
) or not exists (
select 1 from public.agentic_rectification_run_phases
where attempt_id = p_attempt_id
and case_id = p_case_id
and turn_id = p_turn_id
and phase = 'run.completed'
) then
raise exception 'agentic_rectification_attempt_completion_receipt_missing' using errcode = 'P0001';
end if;
if not exists (
select 1
from public.agentic_rectification_skill_run_receipts receipt
where receipt.case_id = p_case_id
and receipt.turn_id = p_turn_id
and receipt.request_id = p_attempt_id
and receipt.run_kind = 'turn'
and receipt.user_id = p_user_id
and receipt.skill_name = v_case.skill_name
and receipt.skill_version = v_case.skill_version
and receipt.skill_sha256 = v_case.skill_sha256
and receipt.source_commit is not distinct from v_case.skill_source_commit
) then
raise exception 'agentic_rectification_skill_receipt_missing' using errcode = 'P0001';
end if;
end if;
update public.agentic_rectification_run_attempts
set status = p_status,
error_code = v_error_code,
usage = p_usage,
completed_at = pg_catalog.now(),
updated_at = pg_catalog.now()
where id = p_attempt_id;
return jsonb_build_object(
'attempt_id', p_attempt_id, 'status', p_status, 'idempotent', false
);
end;
$$;
revoke all on function public.finalize_agentic_rectification_run_attempt(
uuid, uuid, uuid, uuid, text, text, jsonb
) from public, anon, authenticated;
grant execute on function public.finalize_agentic_rectification_run_attempt(
uuid, uuid, uuid, uuid, text, text, jsonb
) to service_role;
-- New overload. The six-argument V9 function remains untouched.
create or replace function public.insert_agentic_rectification_run_phase(
p_user_id uuid,
p_case_id uuid,
p_turn_id uuid,
p_phase text,
p_tool_name text,
p_sequence integer,
p_attempt_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_attempt public.agentic_rectification_run_attempts%rowtype;
v_phase_id uuid;
begin
if p_user_id is null or p_case_id is null or p_turn_id is null or p_attempt_id is null
or p_phase not in (
'run.started', 'skill.started', 'skill.loaded', 'skill.bound', 'case.loaded',
'intent.classified', 'evidence.proposed', 'evidence.confirmed',
'candidates.comparing', 'candidates.updated', 'diagnostics.completed',
'candidate.accepted', 'birth_time.confirmed', 'answer.composed',
'billing.settled', 'answer.delta', 'run.completed', 'run.failed'
) 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;
if not exists (
select 1 from public.agentic_rectification_turns
where id = p_turn_id and case_id = p_case_id
) then
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
end if;
select * into v_attempt
from public.agentic_rectification_run_attempts
where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id
for update;
if not found then
raise exception 'agentic_rectification_attempt_not_found' using errcode = 'P0001';
end if;
if v_attempt.status <> 'started' then
raise exception 'agentic_rectification_attempt_not_started' using errcode = 'P0001';
end if;
insert into public.agentic_rectification_run_phases (
case_id, turn_id, attempt_id, phase, tool_name, sequence
) values (
p_case_id, p_turn_id, p_attempt_id, p_phase, p_tool_name, coalesce(p_sequence, 0)
) returning id into v_phase_id;
return jsonb_build_object('phase_id', v_phase_id, 'attempt_id', p_attempt_id);
end;
$$;
revoke all on function public.insert_agentic_rectification_run_phase(
uuid, uuid, uuid, text, text, integer, uuid
) from public, anon, authenticated;
grant execute on function public.insert_agentic_rectification_run_phase(
uuid, uuid, uuid, text, text, integer, uuid
) to service_role;
-- New overload. The eleven-argument V9 function remains untouched.
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,
p_executed_methods jsonb,
p_attempt_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_attempt public.agentic_rectification_run_attempts%rowtype;
v_receipt_id uuid;
begin
if p_user_id is null or p_case_id is null or p_turn_id is null or p_attempt_id is null
or p_tool_name not in (
'rectification-read-case', 'rectification-set-focus',
'rectification-resolve-focus', 'rectification-record-evidence-batch',
'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'
)
or p_public_phase not in (
'run.started', 'skill.started', 'skill.loaded', 'skill.bound', 'case.loaded',
'intent.classified', 'evidence.proposed', 'evidence.confirmed',
'candidates.comparing', 'candidates.updated', 'diagnostics.completed',
'candidate.accepted', 'birth_time.confirmed', 'answer.composed',
'billing.settled', 'answer.delta', 'run.completed', 'run.failed'
)
or p_status not in ('started', 'completed', 'failed', 'skipped')
or p_executed_methods is null or jsonb_typeof(p_executed_methods) <> 'array'
or exists (
select 1 from jsonb_array_elements_text(p_executed_methods) method(value)
where method.value not in (
'd1-rashi', 'd2-hora', 'd4-chaturthamsha', 'd9-navamsa',
'd10-dashamsa', 'd11-labhamsha', 'd24-chaturvimshamsha',
'd30-trimshamsha', 'vimshottari-dasha', 'narayana-dasha',
'gochara', 'ashtakavarga', 'shadbala', 'arudha-pada',
'functional-benefic-malefic'
)
) 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;
if not exists (
select 1 from public.agentic_rectification_turns
where id = p_turn_id and case_id = p_case_id
) then
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
end if;
select * into v_attempt
from public.agentic_rectification_run_attempts
where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id
for update;
if not found then
raise exception 'agentic_rectification_attempt_not_found' using errcode = 'P0001';
end if;
if v_attempt.status <> 'started' then
raise exception 'agentic_rectification_attempt_not_started' using errcode = 'P0001';
end if;
insert into public.agentic_rectification_tool_receipts (
case_id, turn_id, attempt_id, tool_name, public_phase, status,
input_fingerprint, result_fingerprint, engine_version, safe_error_code,
executed_methods, completed_at
) values (
p_case_id, p_turn_id, p_attempt_id, p_tool_name, p_public_phase, p_status,
p_input_fingerprint, p_result_fingerprint, p_engine_version, p_safe_error_code,
p_executed_methods,
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, 'attempt_id', p_attempt_id);
end;
$$;
revoke all on function public.insert_agentic_rectification_tool_receipt(
uuid, uuid, uuid, text, text, text, text, text, text, text, jsonb, uuid
) from public, anon, authenticated;
grant execute on function public.insert_agentic_rectification_tool_receipt(
uuid, uuid, uuid, text, text, text, text, text, text, text, jsonb, uuid
) to service_role;
-- Attempt-aware finalization. The five-argument V9 function remains available.
create or replace function public.finalize_agentic_rectification_turn(
p_user_id uuid,
p_case_id uuid,
p_turn_id uuid,
p_status text,
p_assistant_message text,
p_successful_attempt_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_turn public.agentic_rectification_turns%rowtype;
v_attempt public.agentic_rectification_run_attempts%rowtype;
begin
if p_user_id is null or p_case_id is null or p_turn_id is null
or p_status not in ('completed', 'failed', 'retryable') 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 * into v_turn
from public.agentic_rectification_turns
where id = p_turn_id and case_id = p_case_id
for update;
if not found then
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
end if;
if v_turn.status = 'completed' then
if p_status = 'completed'
and v_turn.successful_attempt_id is not distinct from p_successful_attempt_id
and v_turn.assistant_message is not distinct from p_assistant_message then
return jsonb_build_object(
'turn_id', p_turn_id,
'status', v_turn.status,
'successful_attempt_id', v_turn.successful_attempt_id,
'idempotent', true
);
end if;
raise exception 'agentic_rectification_turn_already_completed' using errcode = 'P0001';
end if;
if p_status = 'completed' then
if p_successful_attempt_id is null
or p_assistant_message is null
or length(btrim(p_assistant_message)) = 0 then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
select * into v_attempt
from public.agentic_rectification_run_attempts
where id = p_successful_attempt_id
and case_id = p_case_id
and turn_id = p_turn_id
and status = 'completed';
if not found then
raise exception 'agentic_rectification_attempt_not_successful' using errcode = 'P0001';
end if;
elsif p_successful_attempt_id is not null or p_assistant_message is not null then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
update public.agentic_rectification_turns
set status = p_status,
assistant_message = case when p_status = 'completed' then p_assistant_message else null end,
successful_attempt_id = case when p_status = 'completed' then p_successful_attempt_id else null end,
completed_at = case when p_status = 'completed' then pg_catalog.now() else null end,
updated_at = pg_catalog.now()
where id = p_turn_id and status <> 'completed';
return jsonb_build_object(
'turn_id', p_turn_id,
'status', p_status,
'successful_attempt_id', case when p_status = 'completed' then p_successful_attempt_id else null end,
'idempotent', false
);
end;
$$;
revoke all on function public.finalize_agentic_rectification_turn(
uuid, uuid, uuid, text, text, uuid
) from public, anon, authenticated;
-- Legacy finalizers cannot prove latest-attempt ownership. Retain their
-- definitions for additive migration compatibility, but make the V10 runtime
-- use only the attempt-aware overload below.
revoke execute on function public.finalize_agentic_rectification_turn(
uuid, uuid, uuid, text, text
) from service_role;
revoke execute on function public.finalize_agentic_rectification_turn(
uuid, uuid, uuid, text, text, uuid
) from service_role;
-- Attempt-aware monotonic finalization used by the V10 runner.
create or replace function public.finalize_agentic_rectification_turn(
p_user_id uuid,
p_case_id uuid,
p_turn_id uuid,
p_attempt_id uuid,
p_status text,
p_assistant_message text,
p_successful_attempt_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_turn public.agentic_rectification_turns%rowtype;
v_attempt public.agentic_rectification_run_attempts%rowtype;
v_latest_attempt_id uuid;
begin
if p_user_id is null or p_case_id is null or p_turn_id is null or p_attempt_id is null
or p_status not in ('completed', 'failed', 'retryable') 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 * into v_turn
from public.agentic_rectification_turns
where id = p_turn_id and case_id = p_case_id
for update;
if not found then
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
end if;
if v_turn.status = 'completed' then
if p_status = 'completed'
and p_attempt_id = p_successful_attempt_id
and v_turn.successful_attempt_id = p_successful_attempt_id
and v_turn.assistant_message is not distinct from p_assistant_message then
return jsonb_build_object(
'turn_id', p_turn_id,
'status', v_turn.status,
'successful_attempt_id', v_turn.successful_attempt_id,
'idempotent', true
);
end if;
raise exception 'agentic_rectification_turn_already_completed' using errcode = 'P0001';
end if;
select * into v_attempt
from public.agentic_rectification_run_attempts
where id = p_attempt_id and case_id = p_case_id and turn_id = p_turn_id
for update;
if not found then
raise exception 'agentic_rectification_attempt_not_found' using errcode = 'P0001';
end if;
select id into v_latest_attempt_id
from public.agentic_rectification_run_attempts
where case_id = p_case_id and turn_id = p_turn_id
order by attempt_number desc
limit 1;
if v_latest_attempt_id is distinct from p_attempt_id then
raise exception 'agentic_rectification_attempt_superseded' using errcode = 'P0001';
end if;
if p_status = 'completed' then
if p_successful_attempt_id is distinct from p_attempt_id
or p_assistant_message is null
or length(btrim(p_assistant_message)) = 0
or v_attempt.status <> 'completed' then
raise exception 'agentic_rectification_attempt_not_successful' using errcode = 'P0001';
end if;
else
if p_successful_attempt_id is not null or p_assistant_message is not null
or v_attempt.status <> p_status then
raise exception 'agentic_rectification_invalid_input' using errcode = 'P0001';
end if;
if v_turn.status = p_status then
return jsonb_build_object(
'turn_id', p_turn_id,
'status', v_turn.status,
'successful_attempt_id', null,
'idempotent', true
);
end if;
end if;
update public.agentic_rectification_turns
set status = p_status,
assistant_message = case when p_status = 'completed' then p_assistant_message else null end,
successful_attempt_id = case when p_status = 'completed' then p_successful_attempt_id else null end,
completed_at = case when p_status = 'completed' then pg_catalog.now() else null end,
updated_at = pg_catalog.now()
where id = p_turn_id and status <> 'completed';
return jsonb_build_object(
'turn_id', p_turn_id,
'status', p_status,
'successful_attempt_id', case when p_status = 'completed' then p_successful_attempt_id else null end,
'idempotent', false
);
end;
$$;
revoke all on function public.finalize_agentic_rectification_turn(
uuid, uuid, uuid, uuid, text, text, uuid
) from public, anon, authenticated;
grant execute on function public.finalize_agentic_rectification_turn(
uuid, uuid, uuid, uuid, text, text, uuid
) to service_role;
-- ---------------------------------------------------------------------------
-- 8. Attempt-filtered turn receipt and bounded dossier with durable summary.
-- ---------------------------------------------------------------------------
create or replace function public.get_agentic_rectification_turn_receipt(
p_user_id uuid,
p_case_id uuid,
p_turn_id uuid
)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_turn public.agentic_rectification_turns%rowtype;
v_case public.agentic_rectification_cases%rowtype;
v_attempt_id uuid;
v_phases jsonb;
v_tools jsonb;
v_methods jsonb;
v_engine_version text;
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;
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 * into v_turn
from public.agentic_rectification_turns
where id = p_turn_id and case_id = p_case_id;
if not found then
raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001';
end if;
v_attempt_id := v_turn.successful_attempt_id;
if v_attempt_id is null then
select id into v_attempt_id
from public.agentic_rectification_run_attempts
where turn_id = p_turn_id and status = 'completed'
order by completed_at desc, attempt_number desc
limit 1;
end if;
select coalesce(jsonb_agg(
jsonb_build_object('phase', rp.phase, 'tool', rp.tool_name)
order by rp.sequence, rp.created_at, rp.id
), '[]'::jsonb) into v_phases
from public.agentic_rectification_run_phases rp
where rp.turn_id = p_turn_id
and (
(v_attempt_id is not null and rp.attempt_id = v_attempt_id)
or (v_attempt_id is null and rp.attempt_id is null)
);
select coalesce(jsonb_agg(tool_name order by tool_name), '[]'::jsonb) into v_tools
from (
select distinct tr.tool_name
from public.agentic_rectification_tool_receipts tr
where tr.turn_id = p_turn_id and tr.status = 'completed'
and (
(v_attempt_id is not null and tr.attempt_id = v_attempt_id)
or (v_attempt_id is null and tr.attempt_id is null)
)
) tools;
select coalesce(jsonb_agg(method order by method), '[]'::jsonb) into v_methods
from (
select distinct jsonb_array_elements_text(tr.executed_methods) as method
from public.agentic_rectification_tool_receipts tr
where tr.turn_id = p_turn_id and tr.status = 'completed'
and (
(v_attempt_id is not null and tr.attempt_id = v_attempt_id)
or (v_attempt_id is null and tr.attempt_id is null)
)
) methods;
select max(tr.engine_version) into v_engine_version
from public.agentic_rectification_tool_receipts tr
where tr.turn_id = p_turn_id and tr.engine_version is not null
and (
(v_attempt_id is not null and tr.attempt_id = v_attempt_id)
or (v_attempt_id is null and tr.attempt_id is null)
);
return jsonb_build_object(
'turn_id', v_turn.id,
'attempt_id', v_attempt_id,
'status', v_turn.status,
'skill_name', v_case.skill_name,
'skill_version', v_case.skill_version,
'engine_version', v_engine_version,
'phases', v_phases,
'tools', v_tools,
'methods', v_methods,
'started_at', v_turn.created_at,
'completed_at', v_turn.completed_at
);
end;
$$;
revoke all on function public.get_agentic_rectification_turn_receipt(uuid, uuid, uuid)
from public, anon, authenticated;
grant execute on function public.get_agentic_rectification_turn_receipt(uuid, uuid, uuid)
to service_role;
create or replace function public.get_agentic_rectification_case_dossier(
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_turns jsonb;
v_evidence jsonb;
v_summary public.agentic_rectification_case_conversation_summaries%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 coalesce(jsonb_agg(jsonb_build_object(
'id', recent.id,
'role', message.role,
'text', message.text,
'status', recent.status,
'created_at', recent.created_at,
'completed_at', recent.completed_at
) order by recent.created_at, recent.id, message.ordinal), '[]'::jsonb)
into v_turns
from (
select t.*
from public.agentic_rectification_turns t
where t.case_id = v_case.id
order by t.created_at desc, t.id desc
limit 50
) recent
cross join lateral (
values
(1, 'user'::text, recent.user_message),
(2, 'assistant'::text, recent.assistant_message)
) as message(ordinal, role, text)
where message.text is not null;
select coalesce(jsonb_agg(jsonb_build_object(
'id', e.id,
'source_turn_id', e.source_turn_id,
'subject', e.subject,
'event_kind', e.event_kind,
'domain', e.domain,
'occurred_from', e.occurred_from,
'occurred_to', e.occurred_to,
'date_precision', e.date_precision,
'summary', e.summary,
'status', e.status,
'supersedes_evidence_id', e.supersedes_evidence_id,
'created_at', e.created_at
) order by e.created_at, e.id), '[]'::jsonb)
into v_evidence
from public.agentic_rectification_evidence e
where e.case_id = v_case.id;
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_summary
from public.agentic_rectification_case_conversation_summaries
where case_id = v_case.id;
if not found then
perform public.refresh_agentic_rectification_case_conversation_summary(v_case.id);
select * into v_summary
from public.agentic_rectification_case_conversation_summaries
where case_id = v_case.id;
end if;
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, id desc
limit 1;
return jsonb_build_object(
'case', 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,
'completed_at', v_case.completed_at,
'closed_reason', v_case.closed_reason,
'last_activity_at', v_case.last_activity_at,
'evidence_count', v_evidence_count,
'turn_count', v_turn_count
),
'turns', v_turns,
'evidence', v_evidence,
'conversation_summary', jsonb_build_object(
'confirmed_evidence_summary', v_summary.confirmed_evidence_summary,
'pending_revisions', v_summary.pending_revisions,
'active_focus', v_summary.active_focus,
'declined_skipped_topics', v_summary.declined_skipped_topics,
'candidate_divergence_summary', v_summary.candidate_divergence_summary,
'missing_evidence_categories', v_summary.missing_evidence_categories,
'last_result_policy', v_summary.last_result_policy,
'summary_version', v_summary.summary_version,
'updated_at', v_summary.updated_at
),
'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,
'evidence_ledger_fingerprint', v_result.evidence_ledger_fingerprint,
'candidate_range_fingerprint', v_result.candidate_range_fingerprint,
'skill_version', v_result.skill_version,
'algorithm_version', v_result.algorithm_version,
'created_at', v_result.created_at,
'invalidated_at', v_result.invalidated_at
) end
);
end;
$$;
revoke all on function public.get_agentic_rectification_case_dossier(uuid, uuid)
from public, anon, authenticated;
grant execute on function public.get_agentic_rectification_case_dossier(uuid, uuid)
to service_role;
commit;