Files
Dungeons-Ground/supabase/migrations/0013_memory_and_stability.sql
pavel444-byte 031d094867
Some checks failed
CI / validate (push) Has been cancelled
CI / validate (pull_request) Has been cancelled
feat(memory): implement stability stage
Co-authored-by: multica-agent <github@multica.ai>
2026-09-02 15:04:18 +05:00

544 lines
27 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Weeks 78: durable world projections, PostgreSQL context retrieval, quotas,
-- usage telemetry, atomic summaries, and owner-audited corrections.
alter table public.ai_usage add column if not exists usage_key text;
alter table public.ai_usage add column if not exists provider text not null default 'openrouter';
alter table public.ai_usage add column if not exists request_kind text not null default 'unknown';
update public.ai_usage set usage_key = id::text where usage_key is null;
alter table public.ai_usage alter column usage_key set not null;
create unique index if not exists ai_usage_usage_key_unique on public.ai_usage(usage_key);
create index if not exists ai_usage_user_created_idx on public.ai_usage(user_id, created_at desc);
create index if not exists ai_usage_campaign_created_idx on public.ai_usage(campaign_id, created_at desc);
create table if not exists public.ai_quota_events (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.profiles(id) on delete cascade,
campaign_id uuid references public.campaigns(id) on delete cascade,
request_kind text not null check (char_length(request_kind) between 1 and 80),
created_at timestamptz not null default now()
);
create index if not exists ai_quota_events_user_created_idx on public.ai_quota_events(user_id, created_at desc);
create index if not exists ai_quota_events_campaign_created_idx on public.ai_quota_events(campaign_id, created_at desc);
alter table public.ai_quota_events enable row level security;
drop policy if exists quota_events_self_read on public.ai_quota_events;
create policy quota_events_self_read on public.ai_quota_events for select using (user_id = auth.uid());
create table if not exists public.scene_states (
campaign_id uuid primary key references public.campaigns(id) on delete cascade,
location_id uuid references public.world_entities(id) on delete set null,
summary text not null check (char_length(summary) between 1 and 6000),
active_entity_ids uuid[] not null default '{}',
tags text[] not null default '{}',
through_round integer not null default 0 check (through_round >= 0),
updated_at timestamptz not null default now()
);
create table if not exists public.quest_states (
campaign_id uuid not null references public.campaigns(id) on delete cascade,
quest_entity_id uuid not null references public.world_entities(id) on delete cascade,
status text not null default 'hidden' check (status in ('hidden', 'active', 'completed', 'failed')),
summary text not null check (char_length(summary) between 1 and 1200),
tags text[] not null default '{}',
through_round integer not null default 0 check (through_round >= 0),
updated_at timestamptz not null default now(),
primary key (campaign_id, quest_entity_id)
);
create index if not exists quest_states_campaign_status_idx on public.quest_states(campaign_id, status, updated_at desc);
create or replace function public.stage_six_ensure_opening_quest()
returns trigger language plpgsql security definer set search_path = '' as $$
begin
if nullif(btrim(new.hook), '') is not null and not exists (
select 1 from public.world_entities where world_id = new.id and kind = 'quest'
) then
insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
values (new.id, 'quest', 'Opening objective', left(btrim(new.hook), 1200), array['opening', 'active'], '[]'::jsonb);
end if;
return new;
end;
$$;
drop trigger if exists stage_six_world_opening_quest on public.worlds;
create trigger stage_six_world_opening_quest
after insert or update of hook on public.worlds
for each row execute function public.stage_six_ensure_opening_quest();
insert into public.world_entities(world_id, kind, name, summary, tags, secrets)
select world.id, 'quest', 'Opening objective', left(btrim(world.hook), 1200), array['opening', 'active'], '[]'::jsonb
from public.worlds world
where nullif(btrim(world.hook), '') is not null
and not exists (select 1 from public.world_entities entity where entity.world_id = world.id and entity.kind = 'quest');
alter table public.scene_states enable row level security;
alter table public.quest_states enable row level security;
drop policy if exists scene_states_member_read on public.scene_states;
drop policy if exists quest_states_member_read on public.quest_states;
create policy scene_states_member_read on public.scene_states for select using (
public.is_campaign_member(campaign_id) or public.is_campaign_owner(campaign_id)
);
create policy quest_states_member_read on public.quest_states for select using (
public.is_campaign_member(campaign_id) or public.is_campaign_owner(campaign_id)
);
insert into public.scene_states(campaign_id, summary)
select id, current_scene from public.campaigns
on conflict (campaign_id) do nothing;
insert into public.quest_states(campaign_id, quest_entity_id, status, summary, tags)
select campaign.id, entity.id, case when 'opening' = any(entity.tags) then 'active' else 'hidden' end, entity.summary, entity.tags
from public.campaigns campaign
join public.world_entities entity on entity.world_id = campaign.world_id and entity.kind = 'quest'
on conflict (campaign_id, quest_entity_id) do nothing;
create or replace function public.stage_six_initialize_campaign_state()
returns trigger language plpgsql security definer set search_path = '' as $$
begin
insert into public.scene_states(campaign_id, summary)
values (new.id, new.current_scene)
on conflict (campaign_id) do nothing;
insert into public.quest_states(campaign_id, quest_entity_id, status, summary, tags)
select new.id, entity.id, case when 'opening' = any(entity.tags) then 'active' else 'hidden' end, entity.summary, entity.tags
from public.world_entities entity
where entity.world_id = new.world_id and entity.kind = 'quest'
on conflict (campaign_id, quest_entity_id) do nothing;
return new;
end;
$$;
drop trigger if exists stage_six_campaign_state on public.campaigns;
create trigger stage_six_campaign_state
after insert on public.campaigns
for each row execute function public.stage_six_initialize_campaign_state();
create or replace function public.stage_six_consume_ai_quota(
p_user_id uuid,
p_campaign_id uuid,
p_request_kind text
) returns jsonb language plpgsql security definer set search_path = '' as $$
declare
v_user_minute integer;
v_user_day integer;
v_campaign_day integer := 0;
v_tokens_day bigint;
begin
if p_user_id is null or not exists (select 1 from public.profiles where id = p_user_id) then
raise exception 'AI quota requires a valid user';
end if;
if p_campaign_id is not null and not exists (
select 1 from public.campaigns where id = p_campaign_id
and (owner_id = p_user_id or exists (
select 1 from public.campaign_members where campaign_id = p_campaign_id and user_id = p_user_id and active
))
) then
raise exception 'AI quota campaign access denied';
end if;
if p_request_kind is null or char_length(btrim(p_request_kind)) not between 1 and 80 then
raise exception 'AI request kind is required';
end if;
perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended('ai-quota:' || p_user_id::text, 0));
select count(*) into v_user_minute from public.ai_quota_events
where user_id = p_user_id and created_at >= now() - interval '1 minute';
select count(*) into v_user_day from public.ai_quota_events
where user_id = p_user_id and created_at >= date_trunc('day', now());
select coalesce(sum(input_tokens + output_tokens), 0) into v_tokens_day from public.ai_usage
where user_id = p_user_id and created_at >= date_trunc('day', now());
if p_campaign_id is not null then
select count(*) into v_campaign_day from public.ai_quota_events
where campaign_id = p_campaign_id and created_at >= date_trunc('day', now());
end if;
if v_user_minute >= 10 then raise exception 'AI rate limit reached; try again in a minute'; end if;
if v_user_day >= 60 then raise exception 'daily user AI quota reached'; end if;
if p_campaign_id is not null and v_campaign_day >= 40 then raise exception 'daily campaign AI quota reached'; end if;
if v_tokens_day >= 250000 then raise exception 'daily AI token quota reached'; end if;
insert into public.ai_quota_events(user_id, campaign_id, request_kind)
values (p_user_id, p_campaign_id, btrim(p_request_kind));
return jsonb_build_object(
'userRemaining', 59 - v_user_day,
'campaignRemaining', case when p_campaign_id is null then null else 39 - v_campaign_day end,
'tokenRemaining', greatest(0, 250000 - v_tokens_day)
);
end;
$$;
create or replace function public.enqueue_round_resolution(
p_round_id uuid,
p_forced_by uuid default null
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_round public.rounds%rowtype;
v_job_id uuid;
v_owner_id uuid;
begin
select * into v_round from public.rounds where id = p_round_id for update;
if not found then raise exception 'round not found'; end if;
select owner_id into v_owner_id from public.campaigns where id = v_round.campaign_id;
select id into v_job_id from public.ai_jobs
where job_type = 'resolve-round' and entity_id = p_round_id;
if v_round.status in ('queued', 'resolving', 'resolved') then
if v_job_id is null and v_round.status <> 'resolved' then raise exception 'round status and job outbox are inconsistent'; end if;
return v_job_id;
end if;
if v_round.status <> 'open' then raise exception 'round cannot be queued from status %', v_round.status; end if;
if p_forced_by is not null then
if p_forced_by <> v_owner_id then raise exception 'only the campaign owner can force a round'; end if;
elsif exists (
select 1 from public.campaign_members member
join public.characters character on character.campaign_id = member.campaign_id
and character.user_id = member.user_id and character.controller = 'human'::public.character_controller
where member.campaign_id = v_round.campaign_id and member.active
and not exists (
select 1 from public.player_intents intent
where intent.round_id = p_round_id and intent.member_id = member.id and intent.ready
)
) then
raise exception 'not all active players are ready';
end if;
perform public.stage_six_consume_ai_quota(v_owner_id, v_round.campaign_id, 'resolve-round');
v_job_id := gen_random_uuid();
insert into public.ai_jobs(id, job_type, entity_id, idempotency_key, status)
values (v_job_id, 'resolve-round', p_round_id, v_job_id::text, 'queued');
update public.rounds set status = 'queued', queued_at = now(), forced_by = p_forced_by, error = null
where id = p_round_id;
return v_job_id;
end;
$$;
create or replace function public.stage_two_enqueue_world_generation(
p_session_id uuid,
p_owner_id uuid
) returns uuid language plpgsql security definer set search_path = '' as $$
declare
v_session public.coauthor_sessions%rowtype;
v_job public.ai_jobs%rowtype;
v_existing boolean := false;
begin
select * into v_session from public.coauthor_sessions
where id = p_session_id and owner_id = p_owner_id for update;
if not found then raise exception 'coauthor session not found'; end if;
if v_session.status = 'confirmed' then raise exception 'coauthor session is already confirmed'; end if;
if jsonb_array_length(v_session.messages) < 1 then raise exception 'at least one message is required'; end if;
select * into v_job from public.ai_jobs
where job_type = 'generate-world' and entity_id = p_session_id for update;
v_existing := found;
if v_existing and v_job.status in ('queued', 'running') then return v_job.id; end if;
perform public.stage_six_consume_ai_quota(p_owner_id, null, 'generate-world');
if v_existing then
update public.ai_jobs set status = 'queued', attempts = 0, claimed_by = null,
lease_expires_at = null, available_at = now(), error = null, updated_at = now()
where id = v_job.id;
else
v_job.id := gen_random_uuid();
insert into public.ai_jobs(id, job_type, entity_id, idempotency_key, status)
values (v_job.id, 'generate-world', p_session_id, v_job.id::text, 'queued');
end if;
update public.coauthor_sessions set status = 'generating', generated_world = null, updated_at = now()
where id = p_session_id;
return v_job.id;
end;
$$;
create or replace function public.stage_six_retrieve_context(
p_campaign_id uuid,
p_search_text text,
p_limit integer default 8
) returns jsonb language plpgsql stable security definer set search_path = '' as $$
declare
v_world_id uuid;
v_query tsquery;
v_limit integer := greatest(1, least(coalesce(p_limit, 8), 12));
v_entities jsonb;
v_entity_ids uuid[];
begin
select world_id into v_world_id from public.campaigns where id = p_campaign_id;
if not found then raise exception 'campaign not found'; end if;
v_query := plainto_tsquery('english', left(coalesce(p_search_text, ''), 4000));
select coalesce(jsonb_agg(item.payload order by item.relevance desc, item.name), '[]'::jsonb),
coalesce(array_agg(item.id order by item.relevance desc, item.name), '{}')
into v_entities, v_entity_ids
from (
select entity.id, entity.name,
jsonb_build_object('id', entity.id, 'kind', entity.kind, 'name', entity.name, 'summary', entity.summary, 'tags', entity.tags) as payload,
case when numnode(v_query) > 0 then ts_rank_cd(entity.search_document, v_query) else 0 end
+ case when exists (select 1 from unnest(entity.tags) tag where position(lower(tag) in lower(coalesce(p_search_text, ''))) > 0) then 2 else 0 end
+ case when quest.status = 'active' then 3 else 0 end
+ case when scene.location_id = entity.id or entity.id = any(scene.active_entity_ids) then 4 else 0 end as relevance
from public.world_entities entity
left join public.quest_states quest on quest.campaign_id = p_campaign_id and quest.quest_entity_id = entity.id
left join public.scene_states scene on scene.campaign_id = p_campaign_id
where entity.world_id = v_world_id
order by relevance desc, entity.created_at
limit v_limit
) item;
return jsonb_build_object(
'entities', v_entities,
'memories', coalesce((
select jsonb_agg(memory_item.payload order by memory_item.relevance desc, memory_item.created_at desc)
from (
select memory.created_at,
jsonb_build_object('summary', memory.summary, 'importance', memory.importance, 'tags', memory.tags, 'entityIds', memory.entity_ids) as payload,
memory.importance
+ case when numnode(v_query) > 0 then ts_rank_cd(memory.search_document, v_query) * 10 else 0 end
+ case when memory.entity_ids && v_entity_ids then 5 else 0 end
+ case when exists (select 1 from unnest(memory.tags) tag where position(lower(tag) in lower(coalesce(p_search_text, ''))) > 0) then 4 else 0 end as relevance
from public.memories memory
where memory.campaign_id = p_campaign_id
order by relevance desc, memory.created_at desc
limit v_limit
) memory_item
), '[]'::jsonb),
'relationships', coalesce((
select jsonb_agg(jsonb_build_object(
'sourceEntityId', item.source_entity_id,
'targetEntityId', item.target_entity_id,
'score', item.score,
'notes', item.notes
) order by abs(item.score) desc)
from (
select relationship.* from public.relationships relationship
where relationship.campaign_id = p_campaign_id
and (
relationship.source_entity_id = any(v_entity_ids)
or relationship.target_entity_id = any(v_entity_ids)
or exists (
select 1 from public.characters character
where character.campaign_id = p_campaign_id
and character.id in (relationship.source_entity_id, relationship.target_entity_id)
)
)
order by abs(relationship.score) desc
limit 20
) item
), '[]'::jsonb),
'activeGoals', coalesce((
select jsonb_agg(jsonb_build_object(
'questEntityId', item.quest_entity_id,
'name', item.name,
'status', item.status,
'summary', item.summary,
'tags', item.tags
) order by item.updated_at desc)
from (
select quest.*, entity.name from public.quest_states quest
join public.world_entities entity on entity.id = quest.quest_entity_id
where quest.campaign_id = p_campaign_id and quest.status = 'active'
order by quest.updated_at desc
limit 8
) item
), '[]'::jsonb)
);
end;
$$;
create or replace function public.stage_six_apply_round_projections(
p_round_id uuid,
p_events jsonb
) returns void language plpgsql security definer set search_path = '' as $$
declare
v_round public.rounds%rowtype;
v_event jsonb;
v_actor uuid;
v_target uuid;
v_before jsonb;
v_after jsonb;
v_scene_projected boolean := false;
begin
select * into v_round from public.rounds where id = p_round_id;
if not found then raise exception 'round not found'; end if;
for v_event in select * from jsonb_array_elements(coalesce(p_events, '[]'::jsonb)) loop
v_actor := nullif(v_event->>'actorId', '')::uuid;
v_target := nullif(v_event->>'targetId', '')::uuid;
if v_event->>'type' = 'relationship' then
if v_actor is null or v_target is null or v_actor = v_target
or (v_event->>'value')::integer not between -20 and 20 then
raise exception 'invalid relationship projection';
end if;
select to_jsonb(relationship) into v_before from public.relationships relationship
where campaign_id = v_round.campaign_id and source_entity_id = v_actor and target_entity_id = v_target;
insert into public.relationships(campaign_id, source_entity_id, target_entity_id, score, notes)
values (v_round.campaign_id, v_actor, v_target, (v_event->>'value')::integer, v_event->>'description')
on conflict (campaign_id, source_entity_id, target_entity_id) do update
set score = greatest(-100, least(100, public.relationships.score + excluded.score)), notes = excluded.notes;
select to_jsonb(relationship) into v_after from public.relationships relationship
where campaign_id = v_round.campaign_id and source_entity_id = v_actor and target_entity_id = v_target;
insert into public.audit_entries(campaign_id, action, entity_type, entity_id, before_state, after_state)
values (v_round.campaign_id, 'project_relationship', 'relationship', v_target, v_before, v_after);
elsif v_event->>'type' = 'quest' then
if v_target is null or v_event->>'item' not in ('hidden', 'active', 'completed', 'failed') then
raise exception 'invalid quest projection';
end if;
select to_jsonb(quest) into v_before from public.quest_states quest
where campaign_id = v_round.campaign_id and quest_entity_id = v_target;
update public.quest_states set status = v_event->>'item', summary = v_event->>'description',
through_round = v_round.number, updated_at = now()
where campaign_id = v_round.campaign_id and quest_entity_id = v_target;
if not found then raise exception 'quest is outside the round campaign'; end if;
select to_jsonb(quest) into v_after from public.quest_states quest
where campaign_id = v_round.campaign_id and quest_entity_id = v_target;
insert into public.audit_entries(campaign_id, action, entity_type, entity_id, before_state, after_state)
values (v_round.campaign_id, 'project_quest', 'quest', v_target, v_before, v_after);
elsif v_event->>'type' = 'narrative' and v_event->>'item' = 'scene' then
v_scene_projected := true;
if v_target is not null and not exists (
select 1 from public.world_entities entity join public.campaigns campaign on campaign.world_id = entity.world_id
where campaign.id = v_round.campaign_id and entity.id = v_target and entity.kind = 'location'
) then raise exception 'scene location is outside the campaign world'; end if;
insert into public.scene_states(campaign_id, location_id, summary, active_entity_ids, through_round, updated_at)
values (
v_round.campaign_id, v_target, v_event->>'description',
array_remove(array[v_actor, v_target], null), v_round.number, now()
)
on conflict (campaign_id) do update set location_id = excluded.location_id,
summary = excluded.summary, active_entity_ids = excluded.active_entity_ids,
through_round = excluded.through_round, updated_at = excluded.updated_at;
update public.campaigns set current_scene = v_event->>'description', updated_at = now()
where id = v_round.campaign_id;
end if;
end loop;
if not v_scene_projected and nullif(btrim(v_round.narration), '') is not null then
insert into public.scene_states(campaign_id, summary, through_round, updated_at)
values (v_round.campaign_id, v_round.narration, v_round.number, now())
on conflict (campaign_id) do update set summary = excluded.summary,
through_round = excluded.through_round, updated_at = excluded.updated_at;
end if;
end;
$$;
create or replace function public.commit_stage_six_round_resolution(
p_round_id uuid,
p_narration text,
p_next_prompt text,
p_rolls jsonb,
p_events jsonb,
p_character_states jsonb,
p_memory jsonb,
p_story_summary text,
p_idempotency_key text
) returns void language plpgsql security definer set search_path = '' as $$
declare v_round public.rounds%rowtype;
begin
select * into v_round from public.rounds where id = p_round_id for update;
if not found then raise exception 'round not found'; end if;
if v_round.status = 'resolved' then return; end if;
perform public.commit_srd_round_resolution(
p_round_id, p_narration, p_next_prompt, p_rolls, p_events,
p_character_states, p_memory, p_idempotency_key
);
perform public.stage_six_apply_round_projections(p_round_id, p_events);
if p_story_summary is not null then
if v_round.number % 3 <> 0 or char_length(btrim(p_story_summary)) not between 20 and 6000 then
raise exception 'invalid scheduled story summary';
end if;
insert into public.story_summaries(campaign_id, through_round, summary)
values (v_round.campaign_id, v_round.number, btrim(p_story_summary))
on conflict (campaign_id, through_round) do update set summary = excluded.summary;
end if;
end;
$$;
create or replace function public.commit_claimed_stage_six_round_resolution(
p_round_id uuid,
p_narration text,
p_next_prompt text,
p_rolls jsonb,
p_events jsonb,
p_character_states jsonb,
p_memory jsonb,
p_story_summary text,
p_idempotency_key text,
p_worker_id text
) returns void language plpgsql security definer set search_path = '' as $$
declare v_round public.rounds%rowtype;
begin
select * into v_round from public.rounds where id = p_round_id for update;
if not found then raise exception 'round not found'; end if;
if v_round.status = 'resolved' then return; end if;
perform public.commit_claimed_srd_round_resolution(
p_round_id, p_narration, p_next_prompt, p_rolls, p_events,
p_character_states, p_memory, p_idempotency_key, p_worker_id
);
perform public.stage_six_apply_round_projections(p_round_id, p_events);
if p_story_summary is not null then
if v_round.number % 3 <> 0 or char_length(btrim(p_story_summary)) not between 20 and 6000 then
raise exception 'invalid scheduled story summary';
end if;
insert into public.story_summaries(campaign_id, through_round, summary)
values (v_round.campaign_id, v_round.number, btrim(p_story_summary))
on conflict (campaign_id, through_round) do update set summary = excluded.summary;
end if;
end;
$$;
create or replace function public.stage_six_adjust_character_state(
p_campaign_id uuid,
p_owner_id uuid,
p_character_id uuid,
p_patch jsonb,
p_reason text
) returns jsonb language plpgsql security definer set search_path = '' as $$
declare
v_character public.characters%rowtype;
v_before jsonb;
v_after jsonb;
v_hp integer;
begin
if not exists (select 1 from public.campaigns where id = p_campaign_id and owner_id = p_owner_id) then
raise exception 'only the campaign owner can adjust state';
end if;
if p_reason is null or char_length(btrim(p_reason)) not between 3 and 500 then raise exception 'an audit reason is required'; end if;
if coalesce(jsonb_typeof(p_patch), '') <> 'object' or p_patch - array['hp', 'inventory', 'statuses']::text[] <> '{}'::jsonb then
raise exception 'only hp, inventory, and statuses may be adjusted';
end if;
select * into v_character from public.characters
where id = p_character_id and campaign_id = p_campaign_id for update;
if not found then raise exception 'character not found'; end if;
v_before := jsonb_build_object('hp', v_character.hp, 'inventory', v_character.inventory, 'statuses', v_character.statuses);
v_hp := coalesce((p_patch->>'hp')::integer, v_character.hp);
if v_hp < 0 or v_hp > v_character.max_hp then raise exception 'hp must be between zero and max hp'; end if;
if p_patch ? 'inventory' and jsonb_typeof(p_patch->'inventory') <> 'array' then raise exception 'inventory must be an array'; end if;
if p_patch ? 'statuses' and jsonb_typeof(p_patch->'statuses') <> 'array' then raise exception 'statuses must be an array'; end if;
update public.characters set hp = v_hp,
inventory = coalesce(p_patch->'inventory', inventory),
statuses = coalesce(p_patch->'statuses', statuses)
where id = p_character_id
returning jsonb_build_object('hp', hp, 'inventory', inventory, 'statuses', statuses) into v_after;
insert into public.audit_entries(campaign_id, actor_id, action, entity_type, entity_id, before_state, after_state)
values (p_campaign_id, p_owner_id, 'manual_character_adjustment: ' || btrim(p_reason), 'character', p_character_id, v_before, v_after);
return v_after;
end;
$$;
create or replace function public.dng_schema_version()
returns integer language sql stable security definer set search_path = '' as $$
select 13;
$$;
revoke all on function public.stage_six_consume_ai_quota(uuid, uuid, text) from public, anon, authenticated;
revoke all on function public.stage_six_retrieve_context(uuid, text, integer) from public, anon, authenticated;
revoke all on function public.stage_six_apply_round_projections(uuid, jsonb) from public, anon, authenticated;
revoke all on function public.commit_stage_six_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text, text) from public, anon, authenticated;
revoke all on function public.commit_claimed_stage_six_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text, text, text) from public, anon, authenticated;
revoke all on function public.stage_six_adjust_character_state(uuid, uuid, uuid, jsonb, text) from public, anon, authenticated;
revoke all on function public.dng_schema_version() from public, anon, authenticated;
grant execute on function public.stage_six_consume_ai_quota(uuid, uuid, text) to service_role;
grant execute on function public.stage_six_retrieve_context(uuid, text, integer) to service_role;
grant execute on function public.commit_stage_six_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text, text) to service_role;
grant execute on function public.commit_claimed_stage_six_round_resolution(uuid, text, text, jsonb, jsonb, jsonb, jsonb, text, text, text) to service_role;
grant execute on function public.stage_six_adjust_character_state(uuid, uuid, uuid, jsonb, text) to service_role;
grant execute on function public.dng_schema_version() to service_role;
notify pgrst, 'reload schema';