Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/skills/verify-debatelab/features/practice.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Live practice modes. Sparring runs a multi-speech round against an AI opponent a
- `cross-ai-questions` reverses the roles: the AI asks (`mode-ai-questions-user`), the user answers, and the AI follows up.
- `cross-daily-limit` with no AI requests left, the first AI turn is refused: the daily-limit dialog opens and the page stays in setup with no clock and no `end-crossfire`.
- `cross-end-early` ending a round before any exchange returns to setup with no error.
- `spar-daily-limit` Sparring setup warns (test id `sparring-request-shortfall`) when today's remaining requests are fewer than the round needs.
- `spar-daily-limit` Sparring setup warns (test id `sparring-request-shortfall`) when today's remaining requests, as the server reports them, are fewer than the round costs. A round costs one use (by model weight) however many replies it takes; `session-cost` says so before Start.

## How to get to it (user POV)

Expand All @@ -30,7 +30,7 @@ Preconditions:
- **Start Crossfire.** On `/app/crossfire`, fill test id `crossfire-resolution` and `crossfire-argument`. Click `mode-question-ai` (or `mode-ai-questions-user`), then `start-crossfire`, then `chooseForMotion(page, "create")`.
- **Crossfire turn.** Fill `crossfire-input` and click `submit-turn`. The text `FAKE-PROVIDER crossfire answer` and `Direct answer` appear. `providerCall("crossfire")` and `providerCall("jev")` both fired.
- **Debrief.** Click `end-crossfire`. Test id `crossfire-debrief` is visible. `crossfires` for the user equals `[{title: <resolution>, debates: {motion: <resolution>}}]`.
- **Out of requests.** Through `admin`, set `user_settings.daily_limit_override` to N, upsert today's `daily_usage_counters` row (`quota_key: "ai"`, `request_count: N`; what the `ai` function checks), and insert N `ai_requests` rows (what the browser counts). Start Crossfire in `mode-ai-questions-user`: a `dialog` containing `N of N` opens and `start-crossfire` is back. On `/app/sparring`, `sparring-request-shortfall` says `You have no AI requests left today`.
- **Out of requests.** Through `admin`, set `user_settings.daily_limit_override` to N, upsert today's `daily_usage_counters` row (`quota_key: "ai"`, `request_count: N`; what the `ai` function checks), and insert N `ai_requests` rows (the fallback the Settings usage line counts against a database without `ai_usage_today`; Crossfire and Sparring ask the server). Start Crossfire in `mode-ai-questions-user`: a `dialog` containing `N of N` opens and `start-crossfire` is back. On `/app/sparring`, `sparring-request-shortfall` says `You have no AI requests left today`.

## Gotchas

Expand Down
14 changes: 10 additions & 4 deletions .claude/skills/verify-debatelab/features/usage-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,30 @@

The daily AI allowance counts units, not requests. An expensive model uses several units per request (Claude Opus 5.5 counts as 5, Sonnet 5.5 as 2, cheap models as 1), so a request that does not fit in what is left today is refused with a message that names the model's weight and the units left, while a lighter model still runs. Separately, once a user's AI spend for the current week (from Monday 00:00 UTC) reaches the weekly budget ($2 by default), every AI request is refused with its own dialog until Monday. Only `ai_requests.cap_cost_usd` counts: OpenRouter credit charges plus Exa and ElevenLabs; calls billed to our own BYOK key add only OpenRouter's fee. The weights, the budget and the tier defaults live in `supabase/functions/_shared/usage-limits.ts`; the reservation is `consume_ai_quota` in `supabase/migrations/20261022130001_weighted_quota_weekly_budget.sql`.

Crossfire and Sparring are charged per round, not per call: the round's first call reserves the model's units through `claim_ai_session_quota` (`supabase/migrations/20261022220000_ai_quota_sessions.sql`), and every later turn, reply, debrief and judge call in the same round is free. A round that passes its call cap or lifetime (`SESSION_QUOTA` in `usage-limits.ts`) pays again. Both setups ask the server what a round costs before Start (a `quotaCheck` request that reserves nothing) and show it in `session-cost`; a round the server would refuse never starts. A run that fails, is cancelled or is lost to a reload before the student saw `REFUND_MAX_DELIVERED_CHARS` of streamed output gets its units back, and the error says "You were not charged".

## Sub-features

- `usage-weighted`: with 4 units left, Opus is refused (`Daily Access Paused`, "This model counts as 5 uses, and you have 4 left today.") without a provider call or a counter change; switching to Claude Haiku 5.5 runs and uses 1 unit.
- `usage-weekly`: with `ai_requests.cap_cost_usd` for this week at or above the budget, the next request opens `Weekly AI Allowance Used` with an `OK` button and no upgrade offer, makes no provider call, and leaves the daily counter alone.

- `usage-weekly-inline`: Sparring shows refusals inline rather than in the dialog. With the budget reached, the opponent's turn shows the translated weekly message from the catalog, not the server's English reason, and no provider call is made.
- `usage-weekly-sparring`: with the budget reached, the Sparring setup's `session-cost` shows the translated weekly message from the catalog (not the server's English reason) before Start, and Start opens `Weekly AI Allowance Used` without opening a round or calling the provider.
- `usage-session-sparring` (QB-01): with 1 request left of 5, a Lincoln-Douglas round on Claude Haiku 5.5 runs both opponent replies and the judge; the counter moves 4 to 5 once and `ai_quota_sessions` shows `calls_used: 3`.
- `usage-session-crossfire` (QB-05, MD-03): an AI-led Crossfire round with two answers and the debrief makes four calls and one charge; the setup hint is gone once the round runs.
- `usage-refund-reload` (QB-04, MD-13): reloading while Brainstorm streams logs the run `cancelled`/`aborted` and gives its units back. Fake mode only.
- `usage-ask-not-sent` (MD-01): an Ask question refused by the daily limit stays in the chat under a `Not sent: daily limit reached` alert with Retry; after the limit is raised, Retry sends it once.

## How to get to it (user POV)

- Any AI tool, for example `/app/generate`, once the day's allowance or the week's budget is used up. The dialog opens on submit.

## Driving it with verify.sh + Playwright

- `bin/verify.sh drive recipes/usage-limits` covers the dialog sub-features in one test and the Sparring inline message in a second.
- `bin/verify.sh drive recipes/usage-limits` covers each sub-feature in its own test.
- The recipe sets `daily_limit_override: 7` on the demo user and inserts a `daily_usage_counters` row (`quota_key: "ai"`, today's UTC date, `request_count: 3`), then inserts an `ai_requests` row with `cap_cost_usd: 2` for the weekly step.
- End states are read from `daily_usage_counters.request_count` and from the recorded provider calls.
- End states are read from `daily_usage_counters.request_count`, `ai_quota_sessions`, `ai_requests` and the recorded provider calls.

## Gotchas

- Demo users are Pro with Claude Opus 5.5 as the default model, so every Generate in other recipes uses 5 units of their 999.
- Demo users are Pro with Claude Sonnet 5.5 as the default model, so every Generate in other recipes uses 2 units of their 999. The weighted step picks Opus itself.
- The weekly sum counts `cap_cost_usd` of every `ai_requests` row since Monday 00:00 UTC, including voice and Jev routing rows; NULL (unknown or logged before the column existed) counts as nothing, and `cost_usd` alone never counts. Cost is logged after the provider answers, so the cap can be passed by about one request.
5 changes: 3 additions & 2 deletions .claude/skills/verify-debatelab/recipes/practice.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ test("cross-ai-questions: the AI questions the user, answers are judged, and the

/**
* Today's AI requests are used up: the server's counter (what the ai function
* checks) and the usage rows (what the browser counts). A refused call and a
* Jev helper row are on the list too, and must not count as used.
* checks, and what Crossfire and Sparring ask it for before a round) and the
* usage rows (the Settings usage line's fallback on an older database). A refused call and a Jev
* helper row are on the list too, and must not count as used.
*/
async function useUpDailyRequests(userId: string, limit: number) {
await admin.from("user_settings").update({ daily_limit_override: limit }).eq("user_id", userId);
Expand Down
Loading
Loading