-- Question-in-message: persist the asked turn and selected option on each -- conversation focus so GET can rebuild the same assistant bubble after refresh. -- New columns are nullable so existing cases remain readable as plain text. begin; alter table public.agentic_rectification_conversation_focuses add column if not exists asked_turn_id uuid references public.agentic_rectification_turns(id) on delete set null, add column if not exists answer_option text check (answer_option is null or answer_option in ('A', 'B', 'C', 'D', 'stop')); create index if not exists agentic_rectification_focus_asked_turn_idx on public.agentic_rectification_conversation_focuses (asked_turn_id) where asked_turn_id is not null; drop function if exists public.set_agentic_rectification_conversation_focus( uuid, uuid, text, text, uuid, text, text, jsonb ); create 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, p_asked_turn_id uuid default null ) 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_identity jsonb; v_incoming_identity jsonb; 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; if p_asked_turn_id is not null and not exists ( select 1 from public.agentic_rectification_turns where id = p_asked_turn_id and case_id = p_case_id ) then raise exception 'agentic_rectification_turn_not_found' 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 then v_existing_identity := (v_focus.expected_answer_schema - 'prompt'); v_incoming_identity := (p_expected_answer_schema - 'prompt'); if jsonb_typeof(v_existing_identity -> 'choice') = 'object' then v_existing_identity := jsonb_set( v_existing_identity, '{choice}', (v_existing_identity -> 'choice') - 'prompt' ); end if; if jsonb_typeof(v_incoming_identity -> 'choice') = 'object' then v_incoming_identity := jsonb_set( v_incoming_identity, '{choice}', (v_incoming_identity -> 'choice') - 'prompt' ); end if; if v_existing_identity = v_incoming_identity then update public.agentic_rectification_conversation_focuses set expected_answer_schema = p_expected_answer_schema, asked_turn_id = coalesce(asked_turn_id, p_asked_turn_id), updated_at = pg_catalog.now() where id = v_focus.id returning * into v_focus; return jsonb_build_object( 'focus', to_jsonb(v_focus), 'idempotent', true ); end if; 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, asked_turn_id ) 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(), p_asked_turn_id ) returning * into v_focus; return jsonb_build_object( 'focus', to_jsonb(v_focus), 'idempotent', false ); end; $$; revoke all on function public.set_agentic_rectification_conversation_focus( uuid, uuid, text, text, uuid, text, text, jsonb, uuid ) from public, anon, authenticated; grant execute on function public.set_agentic_rectification_conversation_focus( uuid, uuid, text, text, uuid, text, text, jsonb, uuid ) to service_role; drop function if exists public.resolve_agentic_rectification_conversation_focus( uuid, uuid, uuid, text, uuid ); create 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, p_answer_option text default null ) 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; if p_answer_option is not null and p_answer_option not in ('A', 'B', 'C', 'D', 'stop') 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) and v_focus.answer_option is not distinct from p_answer_option then return jsonb_build_object( 'focus_id', v_focus.id, 'status', v_focus.status, 'evidence_id', v_focus.target_evidence_id, 'idempotent', true, 'answer_option', to_jsonb(v_focus.answer_option) ); 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), answer_option = coalesce(p_answer_option, answer_option), resolved_at = pg_catalog.now(), updated_at = pg_catalog.now() where id = v_focus.id returning * into v_focus; 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, 'answer_option', to_jsonb(v_focus.answer_option) ); end; $$; revoke all on function public.resolve_agentic_rectification_conversation_focus( uuid, uuid, uuid, text, uuid, text ) from public, anon, authenticated; grant execute on function public.resolve_agentic_rectification_conversation_focus( uuid, uuid, uuid, text, uuid, text ) to service_role; create or replace function public.list_agentic_rectification_conversation_focuses( 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_focuses jsonb; 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(to_jsonb(f) order by f.asked_at, f.created_at, f.id), '[]'::jsonb) into v_focuses from public.agentic_rectification_conversation_focuses f where f.case_id = p_case_id; return v_focuses; end; $$; revoke all on function public.list_agentic_rectification_conversation_focuses(uuid, uuid) from public, anon, authenticated; grant execute on function public.list_agentic_rectification_conversation_focuses(uuid, uuid) to service_role; create or replace function public.apply_agentic_rectification_choice_action( p_user_id uuid, p_case_id uuid, p_action_id uuid, p_question_id text, p_option_id text, p_expected_revision integer, p_focus_id uuid, p_focus_status text, p_source_quote text, p_derived_context jsonb, p_narration text, p_inference 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; v_existing public.agentic_rectification_choice_actions%rowtype; v_transition jsonb; v_focus_result jsonb; v_receipt jsonb; v_id uuid; v_probe_id text; v_question_id text; begin if p_user_id is null or p_case_id is null or p_action_id is null or p_option_id is null or p_option_id not in ('A', 'B', 'C', 'D', 'stop') or p_expected_revision is null or p_expected_revision < 0 or p_focus_id is null or p_focus_status is null or p_focus_status not in ('resolved', 'declined', 'skipped') or p_derived_context is null or jsonb_typeof(p_derived_context) <> 'object' or (p_inference is not null and jsonb_typeof(p_inference) <> '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; select * into v_existing from public.agentic_rectification_choice_actions where case_id = p_case_id and action_id = p_action_id; if found then return jsonb_set(v_existing.receipt, '{idempotent}', 'true'::jsonb, true); 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.status is distinct from 'active' then raise exception 'agentic_rectification_stale_question' using errcode = 'P0001'; end if; v_question_id := coalesce(nullif(btrim(coalesce(p_question_id, '')), ''), v_focus.question_id); insert into public.agentic_rectification_choice_actions ( case_id, action_id, question_id, probe_id, option_id, expected_revision, status, answer_class, source_quote, derived_context, focus_id, narration, receipt ) values ( p_case_id, p_action_id, v_question_id, nullif(btrim(coalesce(p_inference ->> 'probe_id', p_derived_context ->> 'derivedFromProbeId', '')), ''), p_option_id, p_expected_revision, 'received', nullif(btrim(coalesce(p_inference ->> 'answer_class', p_derived_context ->> 'answerClass', '')), ''), nullif(btrim(coalesce(p_source_quote, '')), ''), p_derived_context, p_focus_id, nullif(btrim(coalesce(p_narration, '')), ''), jsonb_build_object('status', 'received') ) returning * into v_existing; v_transition := null; if p_inference is not null then v_transition := public.append_agentic_rectification_inference_transition( p_user_id, p_case_id, coalesce((p_inference ->> 'expected_revision')::integer, p_expected_revision), p_inference ->> 'probe_id', p_inference ->> 'open_probe_id', p_inference ->> 'semantic_key', p_inference ->> 'candidate_split_hash', p_inference ->> 'answer_class', p_inference ->> 'raw_answer', p_inference -> 'inference_state', coalesce(p_inference -> 'posterior_before', '{}'::jsonb), coalesce(p_inference -> 'posterior_after', '{}'::jsonb), coalesce(p_inference -> 'score_deltas', '{}'::jsonb), p_inference ->> 'decision_state_fingerprint', p_inference ->> 'reason', p_inference ->> 'idempotency_key', p_inference ->> 'candidate_set_id' ); end if; v_focus_result := public.resolve_agentic_rectification_conversation_focus( p_user_id, p_case_id, p_focus_id, p_focus_status, null, p_option_id ); v_probe_id := coalesce( v_transition ->> 'probe_id', p_inference ->> 'probe_id', p_derived_context ->> 'derivedFromProbeId' ); v_receipt := jsonb_build_object( 'action_id', p_action_id, 'status', 'applied', 'idempotent', false, 'question_id', v_question_id, 'option_id', p_option_id, 'probe_id', to_jsonb(v_probe_id), 'revision', coalesce((v_transition ->> 'revision')::integer, p_expected_revision), 'focus_id', v_focus_result -> 'focus_id', 'focus_status', v_focus_result -> 'status', 'transition_id', v_transition -> 'transition_id', 'source_quote', to_jsonb(nullif(btrim(coalesce(p_source_quote, '')), '')), 'derived_context', p_derived_context, 'narration', to_jsonb(nullif(btrim(coalesce(p_narration, '')), '')) ); update public.agentic_rectification_choice_actions set status = 'applied', probe_id = v_probe_id, transition_id = nullif(v_transition ->> 'transition_id', '')::uuid, receipt = v_receipt, updated_at = pg_catalog.now() where id = v_existing.id returning id into v_id; update public.agentic_rectification_cases set last_activity_at = pg_catalog.now(), updated_at = pg_catalog.now() where id = p_case_id; return v_receipt; exception when unique_violation then select * into v_existing from public.agentic_rectification_choice_actions where case_id = p_case_id and action_id = p_action_id; if found then return jsonb_set(v_existing.receipt, '{idempotent}', 'true'::jsonb, true); end if; raise; end; $$; revoke all on function public.apply_agentic_rectification_choice_action( uuid, uuid, uuid, text, text, integer, uuid, text, text, jsonb, text, jsonb ) from public, anon, authenticated; grant execute on function public.apply_agentic_rectification_choice_action( uuid, uuid, uuid, text, text, integer, uuid, text, text, jsonb, text, jsonb ) to service_role; commit;