Repository navigation
Fix the /ai-debate demo's second turn and mid-sentence rebuttals - #301
Conversation
… full sentence Turn 2 failed with unknown_session because the function took the client IP from the right-most X-Forwarded-For entry. On hosted Supabase that entry is an internal hop that changes between requests, so turn 2 hashed to a different IP than turn 1 and the claim RPC found no session. The production logs for the 19:35 UTC failure show the same client IP on both requests. - Take the IP from CF-Connecting-IP, then X-Real-IP, and fall back to X-Forwarded-For last. - The first turn now returns a session token, an HMAC over the session id, its IP hash and an expiry, keyed with SPARRING_INTEGRITY_SECRET (or the service-role fallback). The follow-up must send it back and is charged to that IP hash, so a phone that changes networks can finish the round. The per-IP session limit applies when a session starts. Turnstile is still required for a first turn, and the 2-turn cap and DEMO_DAILY_LIMIT are unchanged. Rebuttals ended in "…" mid-sentence because capWords cut any reply past 150 words at word 150. The model's token limit and the client were not the cause. The prompt now asks for 90 to 130 words in complete sentences. A rebuttal past 170 words is cut at its last full sentence. A reply cut off at max_tokens is served up to its last full sentence, but only when its "allowed" flag arrived as true. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This PR changes |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 54 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
|
Codex review r1 (gpt-6.1-sol, xhigh) on Finding: on the local gateway, a caller-supplied Dismissed: production is not affected. On hosted Supabase, Cloudflare sets The review found no HMAC, replay, moderation-salvage, regex or client-state defects. The 7 Deno tests passed in the review snapshot. |
What broke
Turn 2 always failed with "Your connection changed, so this round cannot continue." The function took the caller's IP from the right-most
X-Forwarded-Forentry. On hosted Supabase that entry is an internal hop that changes between requests, so turn 2 hashed to a different IP than turn 1.claim_demo_round_turnmatches onip_hash, found no session, and returnedunknown_session. Production logs for the 2026-11-02 19:34:50 / 19:35:12 UTC round show the samecf-connecting-ipandx-real-ipon both requests. The IP never changed, so this hit every visitor. A reused Turnstile token was not the cause: turn 2 neither sends one nor checks one.Rebuttals ended mid-sentence ("…so it fails the very…").
capWordscut any rebuttal past 150 words at word 150 and appended "…". The model limit was not the cause: afinish_reason: lengthreply threw and returned a 502, so it never showed as text. The JSON schema has nomaxLength, and the client does not slice.Changes
getIpAddressreadsCF-Connecting-IP, thenX-Real-IP, then the right-mostX-Forwarded-Forentry. This also makes the 3-sessions-per-IP limit count real visitors; before, every session looked like a new IP.sessionToken, an HMAC-SHA256 over the session id, its IP hash and a 2-hour expiry, keyed withSPARRING_INTEGRITY_SECRET(the service-role key when that is unset, as the salt already does). The follow-up must send it back and is charged to the IP hash inside it, so a phone that changes networks mid-round can finish. A missing, forged or expired token is refused withunknown_sessionbefore the RPC or the model runs. No migration.DEMO_DAILY_LIMITare unchanged.max_tokensis repaired and served up to its last full sentence, but only whenallowedarrived astrue. The page hides the judge-note line when a repaired reply has none.REBUTTAL_MAX(the first rebuttal sent back with turn 2) goes from 1,200 to 2,000 characters, so a long Hungarian or Spanish rebuttal is no longer sliced out of the turn-2 context.unknown_sessionmessage now says the round expired.test:edgemay readDEMO_ENABLED,DEMO_DAILY_LIMITandTURNSTILE_SECRET_KEY, which the new Deno test sets.Verification
supabase/functions/demo-rebuttal/index_test.tsruns the real handler with Turnstile, the claim RPC and OpenRouter faked. Five of its seven tests failed onmainand pass here: turn 2 from a new IP, a forged or missing token, the proxy hop changing, a cut-off reply, and an over-long reply. The other two (a first turn needs Turnstile, and a cut-off reply withoutallowed: trueis not served) pass on both and guard the trust boundaries.e2e/ai-debate.spec.ts(mocked function) passes and now asserts that turn 2 sends the token.npm run verifypassed at this head (9 steps, 91 s).Deploy notes
Not low-risk (touches
supabase/andpackage.json), so this needs Adam's OK to merge. After merge, deploydemo-rebuttalonce the frontend is live: the new page still works against the old function, but the new function refuses follow-ups from old pages that do not send a token. No new secret and no migration.Known leftover: a round started just before 00:00 UTC still cannot take its second turn, because the RPC requires
usage_date = today.🤖 Generated with Claude Code