fix: red-team round 3 hardening across desktop, mobile, core and server

Batch of red-team r3 fixes that were in the working tree before the
2026-09-28 design overhaul, committed as one unit with their tests.

- desktop main: STT timeouts and sidecar, voice recording store, sync
  (credentials, audio, knowledge reindex, push gates), runtime
  provisioner, update policy, AltGr keybindings, voice-command policy,
  dictionary file codec/limits, meeting transcript condensing and a
  local recording ledger so interrupted-session recovery only closes
  meetings this device recorded (a phone's live meeting is left alone).
- mobile: login CSRF via implicit token callbacks rejected, account
  deletion/retention, durable queue retention, knowledge realtime
  without unfiltered DELETE, meeting re-record failure paths, cloud STT
  client, preferences store/resync.
- core: text chunking splits long unbroken transcripts to fit, template
  field policy, dictionary limits, meeting markdown inline handling.
- server: payple webhook policy and cancellation order scope, meeting
  document generation quota, team RPC null-role guard, unified LLM
  quota in-flight accounting, knowledge chunk vector index, meeting
  re-record failure paths (migrations 20260929*).
- ci: portable/runtime feed gates, update-policy schema, Forgejo file
  delete and alias planning.

Four older tests are updated to the new contracts rather than the old
behavior: token-pair auth callbacks are rejected, knowledge realtime no
longer subscribes to DELETE, long transcript lines are split, and
meeting recovery requires the local recording ledger for empty rows.
This commit is contained in:
Yun Chan 2026-09-28 20:45:52 +09:00
parent 2428ede03d
commit ba9ef9741e
161 changed files with 17056 additions and 2379 deletions

View file

@ -0,0 +1,830 @@
-- ============================================================================
-- 20260929020000_unify_llm_quota_inflight.sql
--
-- One LLM quota ledger and one lock for every path that spends LLM allowance.
--
-- Bug
-- Meeting-document generation held its in-flight unit in a different ledger
-- and under a different lock than llm-proxy / consume_quota:
-- * claim_meeting_document_generation_v1 (20260928000037) locked
-- hashtextextended(user:feature, 0) and counted daily_usage PLUS
-- meeting_document_generation_requests rows in status 'processing';
-- it wrote nothing to daily_usage;
-- * reserve_llm_quota / finalize_llm_quota (20260928020800) and
-- consume_quota (20260928000131) locked hashtextextended(user:feature,
-- 20260928) and counted daily_usage only.
-- So an in-flight document unit was invisible to llm-proxy. With one Opus
-- unit left, a document claim passed (49 < 50), a Talk/command request then
-- reserved the same unit through llm-proxy (daily_usage 49 -> 50), and
-- commit_meeting_document_generation_v1 re-checked the allowance, saw
-- 50 >= 50 and raised generation_quota_exceeded: the paid Opus generation
-- was thrown away (or, with overage credits, a credit the claim promised
-- was covered by the base allowance was spent silently). The two paths also
-- never serialised against each other because the lock keys differed.
--
-- Fix (single source of truth for LLM quota state)
-- 1. public.daily_usage_lock_v1(user, feature) is the only place that knows
-- the per-user/per-feature advisory-lock key for the daily_usage ledger.
-- reserve_llm_quota, finalize_llm_quota, consume_quota and the three
-- meeting-document RPCs all take the lock through it.
-- 2. A meeting-document claim now reserves its unit with reserve_llm_quota
-- (a fresh server-generated reservation id, stored on the request row in
-- llm_reservation_id). The unit is in daily_usage from the moment of the
-- claim, so llm-proxy and consume_quota see it.
-- commit -> finalize_llm_quota(id, true) (the unit stays spent;
-- consumedFrom comes from the reservation)
-- fail -> finalize_llm_quota(id, false) (daily_usage -1, overage
-- credit refunded when the unit came from overage)
-- The separate 'processing'-row in-flight count and commit's allowance
-- re-check / daily_usage insert are gone, so usage is counted once.
-- 3. Crashed workers: the reservation lease (10 minutes) is reclaimed by the
-- next reserve for that user/feature, exactly like llm-proxy. That
-- replaces the old "processing rows older than 10 minutes stop counting"
-- rule.
--
-- Single-path behaviour is unchanged: "allowed" is still
-- usage + in-flight < base limit + overage, because daily_usage now includes
-- in-flight units. What moves: document usage is recorded at claim instead of
-- commit, and an overage credit is taken at claim and refunded on failure
-- (the llm-proxy model).
--
-- Compatibility
-- * RPC signatures and return shapes are unchanged.
-- * Request rows that were already 'processing' when this migration ran have
-- llm_reservation_id NULL. commit charges them through the same ledger at
-- commit time (the previous behaviour), and fail has nothing to release.
-- The same path covers a reservation whose lease expired and was
-- reclaimed before commit arrived.
-- * The reservation id is generated server-side (gen_random_uuid), never the
-- client's idempotency key: idempotency keys are unique only per user,
-- llm_quota_reservations.id is a global primary key.
--
-- Only service_role may call the quota functions. Local verification:
-- tests/meeting-document-generation-quota.integration.sql.
-- ============================================================================
-- ----------------------------------------------------------------------------
-- 1. The single lock for the daily_usage quota ledger.
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.daily_usage_lock_v1(
p_user_id uuid,
p_feature text
) RETURNS void
LANGUAGE plpgsql
SET search_path = pg_catalog, public
AS $$
BEGIN
IF p_user_id IS NULL OR p_feature IS NULL THEN
RAISE EXCEPTION 'invalid_daily_usage_lock' USING ERRCODE = '22023';
END IF;
-- Seed 20260928 is the key consume_quota and reserve_llm_quota already used,
-- so existing sessions of those functions keep serialising with new ones.
PERFORM pg_advisory_xact_lock(hashtextextended(p_user_id::text || ':' || p_feature, 20260928));
END;
$$;
REVOKE ALL ON FUNCTION public.daily_usage_lock_v1(uuid, text) FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.daily_usage_lock_v1(uuid, text) TO service_role;
COMMENT ON FUNCTION public.daily_usage_lock_v1(uuid, text) IS
'Transaction advisory lock guarding the daily_usage quota ledger for one user/feature. Every quota path (consume_quota, reserve/finalize_llm_quota, meeting-document claim/commit/fail) must take it through this function.';
-- ----------------------------------------------------------------------------
-- 2. Request rows point at the reservation that holds their unit.
-- ----------------------------------------------------------------------------
ALTER TABLE public.meeting_document_generation_requests
ADD COLUMN IF NOT EXISTS llm_reservation_id uuid
REFERENCES public.llm_quota_reservations(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS meeting_document_generation_llm_reservation_idx
ON public.meeting_document_generation_requests(llm_reservation_id)
WHERE llm_reservation_id IS NOT NULL;
-- The 'processing'-row in-flight count is replaced by the reservation ledger.
DROP INDEX IF EXISTS public.meeting_document_generation_in_flight_idx;
-- ----------------------------------------------------------------------------
-- 3. reserve_llm_quota / finalize_llm_quota / consume_quota: same bodies as
-- 20260928020800 and 20260928000131, lock taken through the helper.
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.reserve_llm_quota(
p_user_id uuid,
p_reservation_id uuid,
p_feature text,
p_base_limit integer,
p_period text
) RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
DECLARE
existing public.llm_quota_reservations%ROWTYPE;
expired public.llm_quota_reservations%ROWTYPE;
subscription_tier text := 'free';
overage integer := 0;
new_overage integer;
current_count integer := 0;
consumed_from text;
BEGIN
IF p_user_id IS NULL
OR p_reservation_id IS NULL
OR p_feature IS NULL
OR p_feature NOT IN ('llm_haiku', 'llm_sonnet', 'llm_opus')
OR p_base_limit IS NULL
OR p_base_limit < -1
OR p_period IS NULL
OR p_period NOT IN ('daily', 'weekly') THEN
RAISE EXCEPTION 'invalid_llm_quota_reservation' USING ERRCODE = '22023';
END IF;
PERFORM public.daily_usage_lock_v1(p_user_id, p_feature);
SELECT * INTO existing
FROM public.llm_quota_reservations
WHERE id = p_reservation_id
FOR UPDATE;
IF FOUND THEN
IF existing.user_id <> p_user_id OR existing.feature <> p_feature THEN
RAISE EXCEPTION 'llm_quota_reservation_conflict' USING ERRCODE = 'PT409';
END IF;
RETURN jsonb_build_object(
'allowed', existing.status IN ('reserved', 'completed'),
'reservation_id', existing.id,
'status', existing.status,
'current', existing.current_count,
'limit', existing.quota_limit,
'period', existing.quota_period,
'tier', existing.tier,
'overage_credits', existing.overage_after,
'consumed_from', existing.consumed_from
);
END IF;
-- Reclaim crashed requests before calculating the next allowance.
FOR expired IN
SELECT *
FROM public.llm_quota_reservations
WHERE user_id = p_user_id
AND feature = p_feature
AND status = 'reserved'
AND lease_expires_at <= now()
FOR UPDATE
LOOP
UPDATE public.daily_usage
SET count = greatest(count - 1, 0)
WHERE user_id = expired.user_id
AND date = expired.usage_date
AND feature = expired.feature;
IF expired.consumed_from = 'overage' THEN
UPDATE public.subscriptions
SET overage_credits = overage_credits + 1,
updated_at = now()
WHERE user_id = expired.user_id;
END IF;
UPDATE public.llm_quota_reservations
SET status = 'released', finalized_at = now(), release_reason = 'lease_expired'
WHERE id = expired.id;
END LOOP;
SELECT coalesce(tier, 'free'), coalesce(overage_credits, 0)
INTO subscription_tier, overage
FROM public.subscriptions
WHERE user_id = p_user_id
FOR UPDATE;
IF NOT FOUND THEN
subscription_tier := 'free';
overage := 0;
END IF;
-- Not available: never spend credits on a model the tier does not include.
IF p_base_limit = 0 THEN
RETURN jsonb_build_object(
'allowed', false,
'reservation_id', NULL,
'status', 'denied',
'current', 0,
'limit', 0,
'period', p_period,
'tier', subscription_tier,
'overage_credits', overage,
'consumed_from', 'none'
);
END IF;
-- daily_usage already includes units held by in-flight reservations
-- (llm-proxy requests and meeting-document claims alike).
SELECT coalesce(sum(count), 0)::integer INTO current_count
FROM public.daily_usage
WHERE user_id = p_user_id
AND feature = p_feature
AND date >= CASE WHEN p_period = 'weekly' THEN CURRENT_DATE - 6 ELSE CURRENT_DATE END
AND date <= CURRENT_DATE;
IF p_base_limit = -1 THEN
consumed_from := 'unlimited';
ELSIF current_count < p_base_limit THEN
consumed_from := 'base';
ELSE
UPDATE public.subscriptions
SET overage_credits = overage_credits - 1,
updated_at = now()
WHERE user_id = p_user_id
AND overage_credits > 0
RETURNING overage_credits INTO new_overage;
IF NOT FOUND THEN
RETURN jsonb_build_object(
'allowed', false,
'reservation_id', NULL,
'status', 'denied',
'current', current_count,
'limit', p_base_limit,
'period', p_period,
'tier', subscription_tier,
'overage_credits', 0,
'consumed_from', 'none'
);
END IF;
overage := new_overage;
consumed_from := 'overage';
END IF;
INSERT INTO public.daily_usage(user_id, date, feature, count)
VALUES (p_user_id, CURRENT_DATE, p_feature, 1)
ON CONFLICT (user_id, date, feature)
DO UPDATE SET count = public.daily_usage.count + 1;
current_count := current_count + 1;
INSERT INTO public.llm_quota_reservations(
id, user_id, feature, consumed_from, tier, quota_period, quota_limit,
current_count, overage_after, lease_expires_at
) VALUES (
p_reservation_id, p_user_id, p_feature, consumed_from, subscription_tier, p_period, p_base_limit,
current_count, overage, now() + interval '10 minutes'
);
RETURN jsonb_build_object(
'allowed', true,
'reservation_id', p_reservation_id,
'status', 'reserved',
'current', current_count,
'limit', p_base_limit,
'period', p_period,
'tier', subscription_tier,
'overage_credits', overage,
'consumed_from', consumed_from
);
END;
$$;
CREATE OR REPLACE FUNCTION public.finalize_llm_quota(
p_reservation_id uuid,
p_succeeded boolean
) RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
DECLARE
reservation public.llm_quota_reservations%ROWTYPE;
final_status text;
BEGIN
IF p_reservation_id IS NULL OR p_succeeded IS NULL THEN
RAISE EXCEPTION 'invalid_llm_quota_finalize' USING ERRCODE = '22023';
END IF;
SELECT * INTO reservation
FROM public.llm_quota_reservations
WHERE id = p_reservation_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'llm_quota_reservation_not_found' USING ERRCODE = 'P0002';
END IF;
PERFORM public.daily_usage_lock_v1(reservation.user_id, reservation.feature);
SELECT * INTO reservation
FROM public.llm_quota_reservations
WHERE id = p_reservation_id
FOR UPDATE;
IF reservation.status <> 'reserved' THEN
RETURN jsonb_build_object('reservation_id', reservation.id, 'status', reservation.status);
END IF;
IF p_succeeded THEN
final_status := 'completed';
ELSE
UPDATE public.daily_usage
SET count = greatest(count - 1, 0)
WHERE user_id = reservation.user_id
AND date = reservation.usage_date
AND feature = reservation.feature;
IF reservation.consumed_from = 'overage' THEN
UPDATE public.subscriptions
SET overage_credits = overage_credits + 1,
updated_at = now()
WHERE user_id = reservation.user_id;
END IF;
final_status := 'released';
END IF;
UPDATE public.llm_quota_reservations
SET status = final_status,
finalized_at = now(),
release_reason = CASE WHEN p_succeeded THEN NULL ELSE 'provider_failed' END
WHERE id = reservation.id;
RETURN jsonb_build_object('reservation_id', reservation.id, 'status', final_status);
END;
$$;
REVOKE ALL ON FUNCTION public.reserve_llm_quota(uuid, uuid, text, integer, text) FROM PUBLIC, anon, authenticated;
REVOKE ALL ON FUNCTION public.finalize_llm_quota(uuid, boolean) FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.reserve_llm_quota(uuid, uuid, text, integer, text) TO service_role;
GRANT EXECUTE ON FUNCTION public.finalize_llm_quota(uuid, boolean) TO service_role;
CREATE OR REPLACE FUNCTION public.consume_quota(
p_user_id uuid,
p_feature text,
p_base_limit integer,
p_period text DEFAULT 'daily'
) RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
DECLARE
v_window_start date;
v_current integer := 0;
v_overage integer := 0;
v_new_overage integer;
v_consumed_from text;
BEGIN
IF p_user_id IS NULL
OR p_feature IS NULL
OR p_base_limit IS NULL
OR p_base_limit < -1
OR p_period IS NULL
OR p_period NOT IN ('daily', 'weekly') THEN
RAISE EXCEPTION 'invalid_quota_consumption' USING ERRCODE = '22023';
END IF;
-- Serialise read-then-increment for this user/feature.
PERFORM public.daily_usage_lock_v1(p_user_id, p_feature);
SELECT coalesce(overage_credits, 0) INTO v_overage
FROM public.subscriptions
WHERE user_id = p_user_id
FOR UPDATE;
v_overage := coalesce(v_overage, 0);
-- Not available: never spend credits on a feature the tier does not include.
IF p_base_limit = 0 THEN
RETURN jsonb_build_object(
'allowed', false,
'current', 0,
'limit', 0,
'overage_credits', v_overage,
'consumed_from', 'none'
);
END IF;
v_window_start := CASE WHEN p_period = 'weekly' THEN CURRENT_DATE - 6 ELSE CURRENT_DATE END;
SELECT coalesce(sum(count), 0)::integer INTO v_current
FROM public.daily_usage
WHERE user_id = p_user_id
AND feature = p_feature
AND date >= v_window_start
AND date <= CURRENT_DATE;
IF p_base_limit = -1 THEN
v_consumed_from := 'unlimited';
ELSIF v_current < p_base_limit THEN
v_consumed_from := 'base';
ELSE
UPDATE public.subscriptions
SET overage_credits = overage_credits - 1,
updated_at = now()
WHERE user_id = p_user_id
AND overage_credits > 0
RETURNING overage_credits INTO v_new_overage;
IF NOT FOUND THEN
RETURN jsonb_build_object(
'allowed', false,
'current', v_current,
'limit', p_base_limit,
'overage_credits', 0,
'consumed_from', 'none'
);
END IF;
v_overage := v_new_overage;
v_consumed_from := 'overage';
END IF;
INSERT INTO public.daily_usage (user_id, date, feature, count)
VALUES (p_user_id, CURRENT_DATE, p_feature, 1)
ON CONFLICT (user_id, date, feature) DO UPDATE
SET count = public.daily_usage.count + 1;
RETURN jsonb_build_object(
'allowed', true,
'current', v_current + 1,
'limit', p_base_limit,
'overage_credits', v_overage,
'consumed_from', v_consumed_from
);
END;
$$;
REVOKE ALL ON FUNCTION public.consume_quota(uuid, text, integer, text) FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.consume_quota(uuid, text, integer, text) TO service_role;
-- ----------------------------------------------------------------------------
-- 4. Meeting-document claim: reserve the unit in the shared ledger.
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.claim_meeting_document_generation_v1(
p_actor_id uuid,
p_idempotency_key uuid,
p_meeting_id uuid,
p_template_id uuid,
p_title text,
p_model text
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, public, auth, extensions
AS $$
DECLARE
meeting_row public.meetings;
template_row public.user_templates;
transcript_value text;
transcript_digest text;
request_digest text;
request_row public.meeting_document_generation_requests;
inserted boolean := false;
tier_value text := 'free';
quota_feature_value text;
quota_limit_value integer;
quota_period_value text;
reservation_id_value uuid;
reservation jsonb;
is_replay boolean := false;
safe_title text := trim(p_title);
BEGIN
IF p_actor_id IS NULL OR p_idempotency_key IS NULL OR p_meeting_id IS NULL OR p_template_id IS NULL THEN
RAISE EXCEPTION 'generation_identifiers_required' USING ERRCODE = '22023';
END IF;
IF char_length(safe_title) NOT BETWEEN 1 AND 160 THEN
RAISE EXCEPTION 'invalid_document_title' USING ERRCODE = '22023';
END IF;
IF p_model NOT IN ('claude-haiku-4-5-20251001', 'claude-sonnet-4-6', 'claude-opus-4-6') THEN
RAISE EXCEPTION 'invalid_generation_model' USING ERRCODE = '22023';
END IF;
SELECT * INTO meeting_row FROM public.meetings WHERE id = p_meeting_id;
IF meeting_row.id IS NULL THEN
RAISE EXCEPTION 'meeting_not_found' USING ERRCODE = 'P0002';
END IF;
IF meeting_row.user_id <> p_actor_id AND NOT (
meeting_row.team_id IS NOT NULL
AND (
EXISTS (
SELECT 1 FROM public.teams
WHERE id = meeting_row.team_id AND owner_id = p_actor_id
)
OR EXISTS (
SELECT 1 FROM public.team_members
WHERE team_id = meeting_row.team_id
AND user_id = p_actor_id
AND role IN ('owner', 'admin')
)
)
) THEN
RAISE EXCEPTION 'meeting_generation_forbidden' USING ERRCODE = '42501';
END IF;
SELECT * INTO template_row
FROM public.user_templates
WHERE id = p_template_id
AND user_id = p_actor_id
AND template_kind = 'meeting_document';
IF template_row.id IS NULL THEN
RAISE EXCEPTION 'meeting_template_not_found' USING ERRCODE = 'P0002';
END IF;
SELECT nullif(string_agg(
CASE WHEN nullif(trim(transcript.speaker), '') IS NULL
THEN transcript.text
ELSE trim(transcript.speaker) || ': ' || transcript.text
END,
E'\n' ORDER BY transcript.segment_index
), '')
INTO transcript_value
FROM public.transcripts AS transcript
WHERE transcript.meeting_id = meeting_row.id;
transcript_value := coalesce(
transcript_value,
nullif(trim(meeting_row.edited_transcript), ''),
nullif(trim(meeting_row.raw_transcript), '')
);
IF transcript_value IS NULL THEN
RAISE EXCEPTION 'meeting_transcript_required' USING ERRCODE = '22023';
END IF;
IF char_length(transcript_value) > 48000 THEN
RAISE EXCEPTION 'meeting_transcript_too_large' USING ERRCODE = '22023';
END IF;
SELECT coalesce(subscription.tier, 'free')
INTO tier_value
FROM public.subscriptions AS subscription
WHERE subscription.user_id = p_actor_id;
tier_value := coalesce(tier_value, 'free');
IF tier_value = 'free' AND p_model <> 'claude-haiku-4-5-20251001' THEN
RAISE EXCEPTION 'generation_model_not_allowed' USING ERRCODE = '42501';
END IF;
quota_feature_value := CASE
WHEN p_model LIKE '%sonnet%' THEN 'llm_sonnet'
WHEN p_model LIKE '%opus%' THEN 'llm_opus'
ELSE 'llm_haiku'
END;
quota_limit_value := CASE
WHEN tier_value = 'free' AND quota_feature_value = 'llm_haiku' THEN 250
WHEN tier_value = 'free' THEN 0
WHEN tier_value = 'pro' AND quota_feature_value = 'llm_haiku' THEN 1500
WHEN tier_value = 'pro' AND quota_feature_value = 'llm_sonnet' THEN 300
WHEN tier_value = 'pro' AND quota_feature_value = 'llm_opus' THEN 50
WHEN tier_value = 'pro_plus' AND quota_feature_value = 'llm_haiku' THEN -1
WHEN tier_value = 'pro_plus' AND quota_feature_value = 'llm_sonnet' THEN 1500
WHEN tier_value = 'pro_plus' AND quota_feature_value = 'llm_opus' THEN 300
WHEN tier_value = 'team' AND quota_feature_value = 'llm_haiku' THEN -1
WHEN tier_value = 'team' AND quota_feature_value = 'llm_sonnet' THEN 3000
WHEN tier_value = 'team' AND quota_feature_value = 'llm_opus' THEN 600
WHEN tier_value = 'enterprise' THEN -1
ELSE 0
END;
quota_period_value := CASE WHEN tier_value = 'free' THEN 'weekly' ELSE 'daily' END;
IF quota_limit_value = 0 THEN
RAISE EXCEPTION 'generation_quota_exceeded' USING ERRCODE = 'P0001';
END IF;
-- The shared ledger lock: claims, commits, fails, llm-proxy reservations and
-- consume_quota for one user/feature all serialise here.
PERFORM public.daily_usage_lock_v1(p_actor_id, quota_feature_value);
SELECT EXISTS (
SELECT 1 FROM public.meeting_document_generation_requests
WHERE user_id = p_actor_id AND idempotency_key = p_idempotency_key
) INTO is_replay;
-- A replay never starts provider work, so it takes no unit.
IF NOT is_replay THEN
reservation_id_value := gen_random_uuid();
reservation := public.reserve_llm_quota(
p_actor_id, reservation_id_value, quota_feature_value, quota_limit_value, quota_period_value
);
IF (reservation->>'allowed')::boolean IS NOT TRUE THEN
RAISE EXCEPTION 'generation_quota_exceeded' USING ERRCODE = 'P0001';
END IF;
END IF;
transcript_digest := encode(extensions.digest(transcript_value, 'sha256'), 'hex');
request_digest := encode(extensions.digest(
jsonb_build_object(
'meeting_id', meeting_row.id,
'template_id', template_row.id,
'template_revision', template_row.revision,
'transcript_hash', transcript_digest,
'title', safe_title,
'model', p_model
)::text,
'sha256'
), 'hex');
INSERT INTO public.meeting_document_generation_requests(
user_id, idempotency_key, meeting_id, template_id, request_hash,
document_title, model, quota_feature, quota_limit, quota_period,
template_revision, template_type, transcript_hash, status, llm_reservation_id
)
VALUES (
p_actor_id, p_idempotency_key, meeting_row.id, template_row.id, request_digest,
safe_title, p_model, quota_feature_value, quota_limit_value, quota_period_value,
template_row.revision, template_row.template_type, transcript_digest, 'processing',
reservation_id_value
)
ON CONFLICT DO NOTHING
RETURNING true INTO inserted;
SELECT * INTO request_row
FROM public.meeting_document_generation_requests
WHERE user_id = p_actor_id AND idempotency_key = p_idempotency_key;
IF NOT coalesce(inserted, false) THEN
IF request_row.request_hash <> request_digest THEN
RAISE EXCEPTION 'generation_idempotency_conflict' USING ERRCODE = '22023';
END IF;
-- A concurrent claim for the same key won the insert; give our unit back.
IF reservation_id_value IS NOT NULL THEN
PERFORM public.finalize_llm_quota(reservation_id_value, false);
END IF;
END IF;
RETURN jsonb_build_object(
'claimed', coalesce(inserted, false),
'status', request_row.status,
'documentId', request_row.document_id,
'meetingTitle', coalesce(meeting_row.title, 'Meeting'),
'documentTitle', request_row.document_title,
'templateType', request_row.template_type,
'systemPrompt', CASE WHEN coalesce(inserted, false) THEN template_row.system_prompt ELSE NULL END,
'transcript', CASE WHEN coalesce(inserted, false) THEN transcript_value ELSE NULL END,
'model', request_row.model
);
END;
$$;
REVOKE ALL ON FUNCTION public.claim_meeting_document_generation_v1(uuid, uuid, uuid, uuid, text, text)
FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.claim_meeting_document_generation_v1(uuid, uuid, uuid, uuid, text, text)
TO service_role;
-- ----------------------------------------------------------------------------
-- 5. Meeting-document fail: release the reserved unit.
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.fail_meeting_document_generation_v1(
p_actor_id uuid,
p_idempotency_key uuid,
p_error_code text
)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, public
AS $$
DECLARE
changed integer;
held_reservation uuid;
BEGIN
IF p_error_code NOT IN (
'provider_unavailable', 'provider_timeout', 'provider_request_failed',
'provider_invalid_response', 'quota_exceeded', 'commit_failed'
) THEN
RAISE EXCEPTION 'invalid_generation_error_code' USING ERRCODE = '22023';
END IF;
UPDATE public.meeting_document_generation_requests
SET status = 'failed', error_code = p_error_code, completed_at = now()
WHERE user_id = p_actor_id
AND idempotency_key = p_idempotency_key
AND status = 'processing'
RETURNING llm_reservation_id INTO held_reservation;
GET DIAGNOSTICS changed = ROW_COUNT;
-- Rows claimed before 20260929020000 hold no reservation: nothing to release.
-- finalize is a no-op for a reservation whose lease was already reclaimed.
IF changed > 0 AND held_reservation IS NOT NULL THEN
PERFORM public.finalize_llm_quota(held_reservation, false);
END IF;
RETURN changed > 0;
END;
$$;
REVOKE ALL ON FUNCTION public.fail_meeting_document_generation_v1(uuid, uuid, text)
FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.fail_meeting_document_generation_v1(uuid, uuid, text)
TO service_role;
-- ----------------------------------------------------------------------------
-- 6. Meeting-document commit: settle the reserved unit, never re-evaluate it.
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.commit_meeting_document_generation_v1(
p_actor_id uuid,
p_idempotency_key uuid,
p_content text,
p_latency_ms integer,
p_input_tokens integer DEFAULT NULL,
p_output_tokens integer DEFAULT NULL
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, public
AS $$
DECLARE
request_row public.meeting_document_generation_requests;
document_row public.meeting_documents;
settled jsonb;
reservation jsonb;
charged_reservation uuid;
consumed_from text;
BEGIN
IF char_length(trim(p_content)) NOT BETWEEN 1 AND 100000
OR p_latency_ms NOT BETWEEN 0 AND 600000
OR (p_input_tokens IS NOT NULL AND p_input_tokens < 0)
OR (p_output_tokens IS NOT NULL AND p_output_tokens < 0) THEN
RAISE EXCEPTION 'invalid_generation_result' USING ERRCODE = '22023';
END IF;
SELECT * INTO request_row
FROM public.meeting_document_generation_requests
WHERE user_id = p_actor_id AND idempotency_key = p_idempotency_key
FOR UPDATE;
IF request_row.user_id IS NULL THEN
RAISE EXCEPTION 'generation_request_not_found' USING ERRCODE = 'P0002';
END IF;
IF request_row.status = 'succeeded' THEN
SELECT * INTO document_row FROM public.meeting_documents WHERE id = request_row.document_id;
RETURN jsonb_build_object('idempotent', true, 'document', to_jsonb(document_row));
END IF;
IF request_row.status <> 'processing' THEN
RAISE EXCEPTION 'generation_request_not_committable' USING ERRCODE = '55000';
END IF;
PERFORM public.daily_usage_lock_v1(p_actor_id, request_row.quota_feature);
charged_reservation := request_row.llm_reservation_id;
IF charged_reservation IS NOT NULL THEN
-- The claim already holds the unit: spend it (no allowance re-check).
settled := public.finalize_llm_quota(charged_reservation, true);
END IF;
IF settled->>'status' = 'completed' THEN
SELECT reservation_row.consumed_from
INTO consumed_from
FROM public.llm_quota_reservations AS reservation_row
WHERE reservation_row.id = charged_reservation;
ELSE
-- No unit is held: a row claimed before 20260929020000, or a reservation
-- whose lease expired and was reclaimed before commit arrived. Charge one
-- unit now through the same ledger (the previous commit-time rule).
charged_reservation := gen_random_uuid();
reservation := public.reserve_llm_quota(
p_actor_id, charged_reservation, request_row.quota_feature,
request_row.quota_limit, request_row.quota_period
);
IF (reservation->>'allowed')::boolean IS NOT TRUE THEN
RAISE EXCEPTION 'generation_quota_exceeded' USING ERRCODE = 'P0001';
END IF;
PERFORM public.finalize_llm_quota(charged_reservation, true);
consumed_from := reservation->>'consumed_from';
END IF;
INSERT INTO public.meeting_documents(
meeting_id, user_id, template_type, title, content, prompt_used,
llm_model, llm_latency_ms, template_id, generation_idempotency_key
)
VALUES (
request_row.meeting_id,
p_actor_id,
request_row.template_type,
request_row.document_title,
trim(p_content),
'template:' || request_row.template_id::text || '@' || request_row.template_revision::text,
request_row.model,
p_latency_ms,
request_row.template_id,
p_idempotency_key
)
RETURNING * INTO document_row;
INSERT INTO public.meeting_document_generation_audit(
user_id, meeting_id, template_id, document_id, idempotency_key,
model, template_revision, transcript_hash, input_tokens, output_tokens, latency_ms
)
VALUES (
p_actor_id, request_row.meeting_id, request_row.template_id, document_row.id,
p_idempotency_key, request_row.model, request_row.template_revision,
request_row.transcript_hash, p_input_tokens, p_output_tokens, p_latency_ms
);
UPDATE public.meeting_document_generation_requests
SET status = 'succeeded',
document_id = document_row.id,
llm_reservation_id = charged_reservation,
error_code = NULL,
completed_at = now()
WHERE user_id = p_actor_id AND idempotency_key = p_idempotency_key;
RETURN jsonb_build_object(
'idempotent', false,
'consumedFrom', consumed_from,
'document', to_jsonb(document_row)
);
END;
$$;
REVOKE ALL ON FUNCTION public.commit_meeting_document_generation_v1(uuid, uuid, text, integer, integer, integer)
FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.commit_meeting_document_generation_v1(uuid, uuid, text, integer, integer, integer)
TO service_role;