Repository navigation
Charge Crossfire and Sparring per round, refund runs that produce nothing (batch T5) - #201
Merged
Merged
Conversation
…model, usage from the counter
…er refund, quota check times out
…k without the ledger, refunds stay on their own day
|
Warning Review limit reachedEnable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Next included review available in 44 minutes. View limit details
Note 🎁 Summarized by CodeRabbit Free
Comment |
This was referenced Oct 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Batch T5 from the 22 Oct 2026 QA report: Crossfire and Sparring are charged once per round, and a run that produces nothing is refunded.
Per issue
QB-01 (High), Sparring needed all 5 trial uses. A Sparring round (every opponent reply and the judge) now costs one use, weighted by model. The server enforces this through a new session ledger, keyed by the round's validated
sessionId. Before Start, the setup asks the server what the round costs and how many uses are left ("This round costs 1 AI request, however many turns it takes. You have N left today."). A round the server would refuse never opens, so a speech can't be written into it. Verified: theusage-session-sparringrecipe starts with 4 of 5 used on Claude Haiku 5.5, runs two opponent replies and the judge to a verdict, and the counter goes from 4 to 5 once (ai_quota_sessions.calls_used = 3).QB-05 (Medium), Crossfire charged per answer. A Crossfire round (opening turn, answers, debrief) is charged once. The page sends its round id as
quotaSessionbesideinput, so an older server ignores it and charges per call. Verified:usage-session-crossfiremakes four calls and one charge.MD-02 (Medium), Crossfire started the clock when the opening turn was refused. The same pre-start quota check runs first. A refused round opens the limit dialog and keeps the setup. The old fallback (reset to setup when the first turn is refused) stays. Verified: unit test
crossfire-daily-limit.test.tsxand thecross-daily-limitrecipe.MD-03 (Low), empty-state text next to T1. The live transcript no longer shows the setup hint. Verified in
usage-session-crossfire.MD-01 (Medium), Ask lost the message on the limit modal. The question stays in the chat under "Not sent: daily limit reached" with Retry, in EN, ES and HU. A streamed 402 no longer falls back to a second request. Retry uses whatever model is selected when you press it. Verified:
usage-ask-not-senton both viewports. The question stays, Retry sends it once, and it appears once.QB-04 (Medium) and MD-13 (Medium), failed, aborted and lost runs were charged.
aiandai-streamgive the reserved units back when a run fails, times out, or the browser leaves (reload, closed tab, Stop) before it shows 200 characters of streamed output and before anything was saved. Abuse guards:finish_reasonexceptlength) is never refunded, however the request ended afterwards, even when the usage trailer was lost.charge_id), so a failed call can't give back a later call's charge.The error carries
charged: false, and the client adds "You were not charged for this request." Verified: Deno tests inai/quota-sessions_test.tsandprovider-errors_test.ts, plus theusage-refund-reloadrecipe (reload while Brainstorm streams: rowcancelled/aborted, counter back to 0). For MD-13 I implemented the minimum (refund). Brainstorm does not keep running server-side after a reload the way Generate does.SC-07 (Low), raw provider text in
ai_requests.error_code.logUsagekeeps short snake_case codes and maps anything else toaborted,timeout,rate_limitedorprovider_error. The raw text goes only to the function log, truncated.aiandai-streamnow writeprovider_error,timeout,abortedorclaude_unavailable.Also changed
SparringPagememoized its model config on a function that never changes, so a model picked in setup was ignored until the page remounted, and the round ran on the old model. It now follows the store.ai_usage_today(), the caller's charged units, instead of countingai_requestsrows, which overstated usage once rounds are charged once. Against a database without the function it falls back to counting rows. QB-03's wording and display are untouched.usage-weightedfailed because demo users now default to Sonnet. It now picks Opus itself, and its dialogs close with Escape.Review rounds
01574a5b), five findings, all fixed in9af788f1:9af788f1), three findings, all fixed with tests:009a0c1c.3c4914b6:checkRateLimitnow falls back to per-callconsume_ai_quotawhen the ledger functions are missing.[DONE]without afinish_reasondidn't count as finished. It now does.ai-stream's "OpenRouter is not configured" exit didn't refund. It now does.3c4914b6. They hold, with one Low regression: an empty reply counted as finished. Fixed with a test in28678c80.Migrations
supabase/migrations/20261022220000_ai_quota_sessions.sql, additive only, nothing destructive:ai_quota_sessions(RLS on, no client grants, cascades withauth.users);claim_ai_session_quota,refund_ai_session_quotaandpeek_ai_quota(service role only);ai_usage_today()(signed-in users, their own row only; anon revoked).It drops, rewrites or deletes nothing, and no existing function changes. Its version sorts after main's
20261022210100. Nothing has been applied anywhere.Needs applying or deploying (not done)
20261022220000_ai_quota_sessions.sql, staging then production.aiandai-stream.Deploy order: any order is safe. Migration first is still preferred.
quotaSession.ai/ai-streambefore the migration: Sparring and Crossfire fall back to per-call charging, and the quota check answers 500, treated as unknown. Nothing breaks, but per-round charging starts only once the migration is applied.Verification
npm run verifyon28678c80: pass, exit 0. This is a local pass, not a CI pass; Actions minutes are used up, so CI won't run.@smokee2e testsai/quota-sessions_test.ts, 10 tests: session charging, refund rules, the trailer lost to a disconnect, the upstream read failing after finish, empty replies, the missing-ledger fallback, the quota check.provider-errors_test.ts.supabase/tests/ai_quota_sessions_test.sql, 22 pgTAP assertions: grants, charge once, the call cap and lifetime, heavier models, races serialized withFOR UPDATE, refunds bycharge_id, midnight,ai_usage_today../.superset/db-gate.sh --db-only): the last full run passed (26 files, 492 assertions, plus the concurrency scripts). It ran this migration's exact SQL, then numbered20261022190000. Since then the migration was only renamed and main's two migrations were merged in. The rerun on the merged order could not run, because another worktree's verify stack held the shared Supabase lock for over an hour. Rerun before applying.verify-debatelab, fake provider, desktop and mobile Chromium, all green before the review fixes):recipes/usage-limits(6 tests:usage-weighted,usage-weekly-sparring,usage-session-sparring,usage-session-crossfire,usage-refund-reload,usage-ask-not-sent) andrecipes/practice(5 tests). I did not re-drive them after the review fixes. The unit, edge and e2e tests above cover those changes.crossfire,sparring,voice-rounds) passed in full on desktop and mobile after I added a quota-check answer to their mocks (e2e/helpers/quota.ts).Still to check by hand
errorrow suggests the function sees it there.--live) was not run. The change doesn't touch prompts or model output.🤖 Generated with Claude Code