Skip to content

Fix the /ai-debate demo's second turn and mid-sentence rebuttals - #301

Merged
Programcode merged 1 commit into
mainfrom
fix/demo-round-session-token
Nov 2, 2026
Merged

Programcode merged 1 commit into
mainfrom
fix/demo-round-session-token

Conversation

@Programcode

Copy link
Copy Markdown
Owner

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-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. claim_demo_round_turn matches on ip_hash, found no session, and returned unknown_session. Production logs for the 2026-11-02 19:34:50 / 19:35:12 UTC round show the same cf-connecting-ip and x-real-ip on 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…"). capWords cut any rebuttal past 150 words at word 150 and appended "…". The model limit was not the cause: a finish_reason: length reply threw and returned a 502, so it never showed as text. The JSON schema has no maxLength, and the client does not slice.

Changes

  • getIpAddress reads CF-Connecting-IP, then X-Real-IP, then the right-most X-Forwarded-For entry. This also makes the 3-sessions-per-IP limit count real visitors; before, every session looked like a new IP.
  • The first turn returns a sessionToken, an HMAC-SHA256 over the session id, its IP hash and a 2-hour expiry, keyed with SPARRING_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 with unknown_session before the RPC or the model runs. No migration.
  • Turnstile is still required for every first turn. The 2-turn cap and DEMO_DAILY_LIMIT are unchanged.
  • The prompt 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 repaired and served up to its last full sentence, but only when allowed arrived as true. 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.
  • The unknown_session message now says the round expired. test:edge may read DEMO_ENABLED, DEMO_DAILY_LIMIT and TURNSTILE_SECRET_KEY, which the new Deno test sets.

Verification

  • New supabase/functions/demo-rebuttal/index_test.ts runs the real handler with Turnstile, the claim RPC and OpenRouter faked. Five of its seven tests failed on main and 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 without allowed: true is 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 verify passed at this head (9 steps, 91 s).
  • Not run: a live round against Claude and real Turnstile. The function is not deployed.

Deploy notes

Not low-risk (touches supabase/ and package.json), so this needs Adam's OK to merge. After merge, deploy demo-rebuttal once 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

… 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>
Copilot AI balanced review requested due to automatic review settings November 2, 2026 19:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

This PR changes supabase/. Its debatelab-staging preview talks to staging, which runs main's migrations and Edge Functions, so this PR's backend changes are not there yet. The VPS staging sync applies them within about 5 minutes of the merge. To try them before merging, use the verify-debatelab skill against a local stack. See docs/staging.md.

@coderabbitai

coderabbitai Bot commented Nov 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 1549030 and 99135ca.

Files selected for processing: 7
  • e2e/ai-debate.spec.ts
  • package.json
  • src/components/ai-debate/MiniRound.tsx
  • src/lib/ai-debate-demo.ts
  • supabase/README.md
  • supabase/functions/demo-rebuttal/index.ts
  • supabase/functions/demo-rebuttal/index_test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Share

@Programcode

Copy link
Copy Markdown
Owner Author

Codex review r1 (gpt-6.1-sol, xhigh) on 99135cae: one low finding, dismissed.

Finding: on the local gateway, a caller-supplied CF-Connecting-IP overrides X-Real-IP, so forged values could spread first turns across IP hashes and get past the 3-sessions-per-IP limit.

Dismissed: production is not affected. On hosted Supabase, Cloudflare sets CF-Connecting-IP and overwrites any value the client sends. That is why the function reads it first: the right-most X-Forwarded-For entry it used before was the internal hop that broke turn 2. The local stack was never a trust boundary for these headers. Its gateway passes client-set X-Forwarded-For/X-Real-IP through on the experimental stack (supabase/cli#7275), and local runs use test Turnstile keys. Even if someone did get past the per-IP limit, a first turn still needs a passing Turnstile check, and DEMO_DAILY_LIMIT caps model calls globally.

The review found no HMAC, replay, moderation-salvage, regex or client-state defects. The 7 Deno tests passed in the review snapshot.

@Programcode
Programcode merged commit a9f69b3 into main Nov 2, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants