From bbe656929c96120c238a13a623fb80f5c224c126 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 11 Mar 2026 00:13:31 +0000 Subject: [PATCH] another fix for human choices --- frontend/src/composables/useFightPolling.ts | 1 + frontend/src/composables/useHumanChallenge.ts | 14 +++++++++++++- frontend/src/pages/FightPage.vue | 18 +++++------------- frontend/src/pages/HumanFightPage.vue | 10 +++++++++- server/src/app.ts | 4 ++-- server/src/engine/human-responses.ts | 13 +++++++++---- server/src/engine/orchestrator.ts | 4 +++- 7 files changed, 42 insertions(+), 22 deletions(-) diff --git a/frontend/src/composables/useFightPolling.ts b/frontend/src/composables/useFightPolling.ts index 6fd1076..7a6e179 100644 --- a/frontend/src/composables/useFightPolling.ts +++ b/frontend/src/composables/useFightPolling.ts @@ -42,6 +42,7 @@ export function useFightPolling(fightId: Ref) { async function loadFight(): Promise { try { const res = await fetch(`/api/fights/${fightId.value}`) + if (res.status === 429) return null // Rate limited, skip this poll if (res.ok) { const data = await res.json() liveRounds.value = data.rounds?.length || 0 diff --git a/frontend/src/composables/useHumanChallenge.ts b/frontend/src/composables/useHumanChallenge.ts index 7c376be..3415e38 100644 --- a/frontend/src/composables/useHumanChallenge.ts +++ b/frontend/src/composables/useHumanChallenge.ts @@ -97,11 +97,19 @@ export function useHumanChallenge( } } + let pollBackoff = 0 + async function pollForChallenge() { if (!myBotId.value) return try { const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBotId.value}`) + if (res.status === 429) { + // Back off on rate limit — skip next few polls + pollBackoff = Math.min(pollBackoff + 2, 8) + return + } if (!res.ok) return + pollBackoff = Math.max(0, pollBackoff - 1) // Recover gradually const data = await res.json() if (data.pending) { @@ -130,8 +138,12 @@ export function useHumanChallenge( function startHumanPolling() { if (!myBotId.value) return + pollBackoff = 0 void pollForChallenge() - humanPollHandle = setInterval(() => { void pollForChallenge() }, 400) + humanPollHandle = setInterval(() => { + if (pollBackoff > 0) { pollBackoff--; return } + void pollForChallenge() + }, 800) } function stopHumanPolling() { diff --git a/frontend/src/pages/FightPage.vue b/frontend/src/pages/FightPage.vue index 9a703f4..de7bec9 100644 --- a/frontend/src/pages/FightPage.vue +++ b/frontend/src/pages/FightPage.vue @@ -197,19 +197,8 @@ function wireSSE() { }) scrollLiveLog() }, - async onHumanChallenge(sseData) { - if (myBot.value) { - try { - const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBot.value.id}`) - if (res.ok) { - const fullData = await res.json() - if (fullData.pending) { - handleSSEChallenge(fullData) - return - } - } - } catch { /* fall through */ } - } + onHumanChallenge(sseData) { + // SSE now includes choices directly — no secondary fetch needed handleSSEChallenge(sseData) }, onRoundEnd(data) { @@ -539,6 +528,9 @@ async function fightAgain(botId: string) { liveHpA.value = 100 liveHpB.value = 100 liveCurrentRound.value = 0 + // Stop ALL old polling/connections before starting new fight + stopHumanPolling() + stopPolling() disconnectSSE() try { diff --git a/frontend/src/pages/HumanFightPage.vue b/frontend/src/pages/HumanFightPage.vue index dc28d44..464ba7d 100644 --- a/frontend/src/pages/HumanFightPage.vue +++ b/frontend/src/pages/HumanFightPage.vue @@ -123,9 +123,15 @@ function handleKeyboard(e: KeyboardEvent) { } } +let pollBackoff = 0 + function startPolling() { + pollBackoff = 0 pollForChallenge() - pollHandle = setInterval(pollForChallenge, 600) + pollHandle = setInterval(() => { + if (pollBackoff > 0) { pollBackoff--; return } + pollForChallenge() + }, 800) } function stopPolling() { @@ -172,7 +178,9 @@ async function pollForChallenge() { try { const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBotId.value}`) + if (res.status === 429) { pollBackoff = Math.min(pollBackoff + 2, 8); return } if (!res.ok) return + pollBackoff = Math.max(0, pollBackoff - 1) const data = await res.json() // Reset error tracking on success diff --git a/server/src/app.ts b/server/src/app.ts index 7cae4ab..82a1ed6 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -57,7 +57,7 @@ app.use('*', secureHeaders({ scriptSrc: ["'self'", 'blob:'], styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], imgSrc: ["'self'", 'data:', 'blob:'], - connectSrc: ["'self'", 'https://huggingface.co', 'https://*.huggingface.co'], + connectSrc: ["'self'", 'https://huggingface.co', 'https://*.huggingface.co', 'https://*.hf.co'], fontSrc: ["'self'", 'https://fonts.gstatic.com'], workerSrc: ["'self'", 'blob:'], } : undefined, @@ -67,7 +67,7 @@ app.use('*', secureHeaders({ app.use('/api/*', bodyLimit({ maxSize: 256 * 1024 })) // Global rate limit (120/min per IP — generous for polling + signup flows) -app.use('/api/*', rateLimit(60_000, 120)) +app.use('/api/*', rateLimit(60_000, 300)) // API cache headers app.use('/api/*', async (c, next) => { diff --git a/server/src/engine/human-responses.ts b/server/src/engine/human-responses.ts index e1e00d2..091330e 100644 --- a/server/src/engine/human-responses.ts +++ b/server/src/engine/human-responses.ts @@ -190,6 +190,11 @@ const CREATIVE_POOLS: Record= 2) { + return shuffle([...challenge.choices]) + } + if (challenge.scoring === 'factual' && challenge.answers && challenge.answers.length > 0) { const correct = challenge.answers[0] const num = Number(correct) @@ -237,8 +242,9 @@ export function waitForHumanResponse( botId: string, challenge: Challenge, roundNumber: number, -): Promise<{ answer: string | null; trashTalk?: string; timedOut: boolean }> { - return new Promise((resolve) => { +): { promise: Promise<{ answer: string | null; trashTalk?: string; timedOut: boolean }>; choices: string[] } { + const choices = generateChoices(challenge) + const promise = new Promise<{ answer: string | null; trashTalk?: string; timedOut: boolean }>((resolve) => { const key = `${fightId}:${botId}` const timeoutMs = challenge.timeout_ms || DEFAULT_HUMAN_TIMEOUT_MS @@ -248,8 +254,6 @@ export function waitForHumanResponse( resolve({ answer: null, timedOut: true }) }, timeoutMs) - const choices = generateChoices(challenge) - pending.set(key, { fightId, botId, @@ -267,6 +271,7 @@ export function waitForHumanResponse( logger.info('human', `waiting: ${key} round=${roundNumber} type=${challenge.type} choices=${choices.length}`) }) + return { promise, choices } } export function submitHumanResponse( diff --git a/server/src/engine/orchestrator.ts b/server/src/engine/orchestrator.ts index da03e85..5d0cba5 100644 --- a/server/src/engine/orchestrator.ts +++ b/server/src/engine/orchestrator.ts @@ -269,7 +269,8 @@ async function getBotResponse( // before the SSE event tells the frontend to fetch it (fixes race condition // where frontend fetched before pending was set → no choices shown) const start = Date.now() - const resultPromise = waitForHumanResponse(fightId, bot.id, challenge, roundNumber) + const { promise: resultPromise, choices } = waitForHumanResponse(fightId, bot.id, challenge, roundNumber) + // Include choices directly in SSE so frontend never depends on a secondary fetch emit(fightId, 'human_challenge', { botId: bot.id, round: roundNumber, @@ -278,6 +279,7 @@ async function getBotResponse( prompt: challenge.prompt, timeoutMs: challenge.timeout_ms, scoring: challenge.scoring, + choices, }) const result = await resultPromise const elapsed = Date.now() - start