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:
parent
2428ede03d
commit
ba9ef9741e
161 changed files with 17056 additions and 2379 deletions
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue