diff --git a/components/DreamSymbolsSeerChatInterface.tsx b/components/DreamSymbolsSeerChatInterface.tsx index ffb711c..69700b8 100644 --- a/components/DreamSymbolsSeerChatInterface.tsx +++ b/components/DreamSymbolsSeerChatInterface.tsx @@ -40,8 +40,6 @@ const REGENERATE_MESSAGE = const UNAVAILABLE_MESSAGE = "The request didn't go through. Please try again in a moment."; -const RETRY_DELAY_MS = 1500; - const DREAM_SYMBOLS_STARTER_QUESTIONS = [ 'What does my dream about water mean?', 'Why do I keep dreaming about the same person or place?', @@ -152,16 +150,9 @@ export default function DreamSymbolsSeerChatInterface({ ); }; - const isRetryableStatus = (status: number) => - status === 500 || status >= 502; - try { - let response = await performFetch(); - - if (!response.ok && isRetryableStatus(response.status)) { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - response = await performFetch(); - } + // Do not auto-retry: enforceToolSeerGate debits before the LLM runs. + const response = await performFetch(); if (!response.ok) { setStreamingMessageId(null); @@ -206,34 +197,7 @@ export default function DreamSymbolsSeerChatInterface({ } catch (err) { devLog.error('Dream Symbols Seer error', err, 'DreamSymbolsSeerChatInterface'); setStreamingMessageId(null); - try { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - const retryResponse = await performFetch(); - if (!retryResponse.ok) { - setUnavailableMessage(); - return; - } - const reader = retryResponse.body?.getReader(); - const decoder = new TextDecoder(); - let accumulatedContent = ''; - if (reader) { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const chunk = decoder.decode(value); - accumulatedContent += chunk; - streamingLengthRef.current = accumulatedContent.length; - setMessages((prev) => - prev.map((msg) => - msg.id === aiMessageId ? { ...msg, content: stripAttributionForDisplay(accumulatedContent) } : msg - ) - ); - } - } - setStreamingMessageId(null); - } catch { - setUnavailableMessage(); - } + setUnavailableMessage(); } finally { setIsLoading(false); } diff --git a/components/FaceReadingSeerChatInterface.tsx b/components/FaceReadingSeerChatInterface.tsx index c679282..36c8910 100644 --- a/components/FaceReadingSeerChatInterface.tsx +++ b/components/FaceReadingSeerChatInterface.tsx @@ -39,8 +39,6 @@ const REGENERATE_MESSAGE = const UNAVAILABLE_MESSAGE = "The request didn't go through. Please try again in a moment."; -const RETRY_DELAY_MS = 1500; - const FACE_READING_STARTER_QUESTIONS = [ 'What does my face say about my personality?', 'What strengths or challenges does my face indicate?', @@ -144,16 +142,9 @@ export default function FaceReadingSeerChatInterface({ ); }; - const isRetryableStatus = (status: number) => - status === 500 || status >= 502; - try { - let response = await performFetch(); - - if (!response.ok && isRetryableStatus(response.status)) { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - response = await performFetch(); - } + // Do not auto-retry: enforceToolSeerGate debits before the LLM runs. + const response = await performFetch(); if (!response.ok) { setStreamingMessageId(null); @@ -198,34 +189,7 @@ export default function FaceReadingSeerChatInterface({ } catch (err) { devLog.error('Face Reading Seer error', err, 'FaceReadingSeerChatInterface'); setStreamingMessageId(null); - try { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - const retryResponse = await performFetch(); - if (!retryResponse.ok) { - setUnavailableMessage(); - return; - } - const reader = retryResponse.body?.getReader(); - const decoder = new TextDecoder(); - let accumulatedContent = ''; - if (reader) { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const chunk = decoder.decode(value); - accumulatedContent += chunk; - streamingLengthRef.current = accumulatedContent.length; - setMessages((prev) => - prev.map((msg) => - msg.id === aiMessageId ? { ...msg, content: stripAttributionForDisplay(accumulatedContent) } : msg - ) - ); - } - } - setStreamingMessageId(null); - } catch { - setUnavailableMessage(); - } + setUnavailableMessage(); } finally { setIsLoading(false); } diff --git a/components/FengShuiSeerChatInterface.tsx b/components/FengShuiSeerChatInterface.tsx index bb62eb0..45695ff 100644 --- a/components/FengShuiSeerChatInterface.tsx +++ b/components/FengShuiSeerChatInterface.tsx @@ -50,8 +50,6 @@ const REGENERATE_MESSAGE = const UNAVAILABLE_MESSAGE = "The request didn't go through. Please try again in a moment."; -const RETRY_DELAY_MS = 1500; - const FENGSHUI_STARTER_QUESTIONS = [ 'How can I support wealth and abundance energy in my home without overdoing symbols?', 'What commonly blocks qi at the entrance, and how do I fix it?', @@ -160,16 +158,9 @@ export default function FengShuiSeerChatInterface({ ); }; - const isRetryableStatus = (status: number) => - status === 500 || status >= 502; - try { - let response = await performFetch(); - - if (!response.ok && isRetryableStatus(response.status)) { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - response = await performFetch(); - } + // Do not auto-retry: enforceToolSeerGate debits before the LLM runs. + const response = await performFetch(); if (!response.ok) { setStreamingMessageId(null); @@ -214,34 +205,7 @@ export default function FengShuiSeerChatInterface({ } catch (err) { devLog.error('Feng Shui Seer error', err, 'FengShuiSeerChatInterface'); setStreamingMessageId(null); - try { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - const retryResponse = await performFetch(); - if (!retryResponse.ok) { - setUnavailableMessage(); - return; - } - const reader = retryResponse.body?.getReader(); - const decoder = new TextDecoder(); - let accumulatedContent = ''; - if (reader) { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const chunk = decoder.decode(value); - accumulatedContent += chunk; - streamingLengthRef.current = accumulatedContent.length; - setMessages((prev) => - prev.map((msg) => - msg.id === aiMessageId ? { ...msg, content: stripAttributionForDisplay(accumulatedContent) } : msg - ) - ); - } - } - setStreamingMessageId(null); - } catch { - setUnavailableMessage(); - } + setUnavailableMessage(); } finally { setIsLoading(false); } diff --git a/components/HorarySeerChatInterface.tsx b/components/HorarySeerChatInterface.tsx index df805d9..2ee38d9 100644 --- a/components/HorarySeerChatInterface.tsx +++ b/components/HorarySeerChatInterface.tsx @@ -39,8 +39,6 @@ const REGENERATE_MESSAGE = const UNAVAILABLE_MESSAGE = "The request didn't go through. Please try again in a moment."; -const RETRY_DELAY_MS = 1500; - const HORARY_STARTER_QUESTIONS = [ 'Will my app launch succeed if I release it now?', 'Will this partnership work out?', @@ -148,16 +146,9 @@ export default function HorarySeerChatInterface({ ); }; - const isRetryableStatus = (status: number) => - status === 500 || status >= 502; - try { - let response = await performFetch(); - - if (!response.ok && isRetryableStatus(response.status)) { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - response = await performFetch(); - } + // Do not auto-retry: enforceToolSeerGate debits before the LLM runs. + const response = await performFetch(); if (!response.ok) { setStreamingMessageId(null); @@ -203,34 +194,7 @@ export default function HorarySeerChatInterface({ } catch (err) { devLog.error('Horary Seer error', err, 'HorarySeerChatInterface'); setStreamingMessageId(null); - try { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - const retryResponse = await performFetch(); - if (!retryResponse.ok) { - setUnavailableMessage(); - return; - } - const reader = retryResponse.body?.getReader(); - const decoder = new TextDecoder(); - let accumulatedContent = ''; - if (reader) { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const chunk = decoder.decode(value); - accumulatedContent += chunk; - streamingLengthRef.current = accumulatedContent.length; - setMessages((prev) => - prev.map((msg) => - msg.id === aiMessageId ? { ...msg, content: stripAttributionForDisplay(accumulatedContent) } : msg - ) - ); - } - } - setStreamingMessageId(null); - } catch { - setUnavailableMessage(); - } + setUnavailableMessage(); } finally { setIsLoading(false); } diff --git a/components/KPSeerChatInterface.tsx b/components/KPSeerChatInterface.tsx index 333ae19..890c200 100644 --- a/components/KPSeerChatInterface.tsx +++ b/components/KPSeerChatInterface.tsx @@ -39,8 +39,6 @@ const REGENERATE_MESSAGE = const UNAVAILABLE_MESSAGE = "The request didn't go through. Please try again in a moment."; -const RETRY_DELAY_MS = 1500; - const KP_STARTER_QUESTIONS = [ 'Will my app launch succeed?', 'Will I get this job offer?', @@ -139,16 +137,9 @@ export default function KPSeerChatInterface({ ); }; - const isRetryableStatus = (status: number) => - status === 500 || status >= 502; - try { - let response = await performFetch(); - - if (!response.ok && isRetryableStatus(response.status)) { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - response = await performFetch(); - } + // Do not auto-retry: enforceToolSeerGate debits before the LLM runs. + const response = await performFetch(); if (!response.ok) { setStreamingMessageId(null); @@ -194,34 +185,7 @@ export default function KPSeerChatInterface({ } catch (err) { devLog.error('KP Seer error', err, 'KPSeerChatInterface'); setStreamingMessageId(null); - try { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - const retryResponse = await performFetch(); - if (!retryResponse.ok) { - setUnavailableMessage(); - return; - } - const reader = retryResponse.body?.getReader(); - const decoder = new TextDecoder(); - let accumulatedContent = ''; - if (reader) { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const chunk = decoder.decode(value); - accumulatedContent += chunk; - streamingLengthRef.current = accumulatedContent.length; - setMessages((prev) => - prev.map((msg) => - msg.id === aiMessageId ? { ...msg, content: stripAttributionForDisplay(accumulatedContent) } : msg - ) - ); - } - } - setStreamingMessageId(null); - } catch { - setUnavailableMessage(); - } + setUnavailableMessage(); } finally { setIsLoading(false); } diff --git a/components/RunesSeerChatInterface.tsx b/components/RunesSeerChatInterface.tsx index 0963229..93af2e3 100644 --- a/components/RunesSeerChatInterface.tsx +++ b/components/RunesSeerChatInterface.tsx @@ -39,8 +39,6 @@ const REGENERATE_MESSAGE = const UNAVAILABLE_MESSAGE = "The request didn't go through. Please try again in a moment."; -const RETRY_DELAY_MS = 1500; - const RUNES_STARTER_QUESTIONS = [ 'What do the runes say about my situation?', 'What energy surrounds this decision?', @@ -145,16 +143,9 @@ export default function RunesSeerChatInterface({ ); }; - const isRetryableStatus = (status: number) => - status === 500 || status >= 502; - try { - let response = await performFetch(); - - if (!response.ok && isRetryableStatus(response.status)) { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - response = await performFetch(); - } + // Do not auto-retry: enforceToolSeerGate debits before the LLM runs. + const response = await performFetch(); if (!response.ok) { setStreamingMessageId(null); @@ -199,34 +190,7 @@ export default function RunesSeerChatInterface({ } catch (err) { devLog.error('Runes Seer error', err, 'RunesSeerChatInterface'); setStreamingMessageId(null); - try { - await new Promise((r) => setTimeout(r, RETRY_DELAY_MS)); - const retryResponse = await performFetch(); - if (!retryResponse.ok) { - setUnavailableMessage(); - return; - } - const reader = retryResponse.body?.getReader(); - const decoder = new TextDecoder(); - let accumulatedContent = ''; - if (reader) { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const chunk = decoder.decode(value); - accumulatedContent += chunk; - streamingLengthRef.current = accumulatedContent.length; - setMessages((prev) => - prev.map((msg) => - msg.id === aiMessageId ? { ...msg, content: stripAttributionForDisplay(accumulatedContent) } : msg - ) - ); - } - } - setStreamingMessageId(null); - } catch { - setUnavailableMessage(); - } + setUnavailableMessage(); } finally { setIsLoading(false); } diff --git a/tests/unit/seerBilledRetry.test.ts b/tests/unit/seerBilledRetry.test.ts new file mode 100644 index 0000000..a1cd50f --- /dev/null +++ b/tests/unit/seerBilledRetry.test.ts @@ -0,0 +1,46 @@ +/** + * @jest-environment node + * + * Tool Ask-the-Seer POSTs debit in enforceToolSeerGate before the LLM runs. + * These chat UIs must not auto-retry 5xx or thrown fetches: a single send would + * consume the per-tool free instance and then charge PAYG credits. + */ +import fs from 'fs' + +const BILLED_SEER_CHAT_SOURCES: Array<{ rel: string; src: string }> = [ + { + rel: 'components/FaceReadingSeerChatInterface.tsx', + src: fs.readFileSync('components/FaceReadingSeerChatInterface.tsx', 'utf8'), + }, + { + rel: 'components/RunesSeerChatInterface.tsx', + src: fs.readFileSync('components/RunesSeerChatInterface.tsx', 'utf8'), + }, + { + rel: 'components/KPSeerChatInterface.tsx', + src: fs.readFileSync('components/KPSeerChatInterface.tsx', 'utf8'), + }, + { + rel: 'components/HorarySeerChatInterface.tsx', + src: fs.readFileSync('components/HorarySeerChatInterface.tsx', 'utf8'), + }, + { + rel: 'components/FengShuiSeerChatInterface.tsx', + src: fs.readFileSync('components/FengShuiSeerChatInterface.tsx', 'utf8'), + }, + { + rel: 'components/DreamSymbolsSeerChatInterface.tsx', + src: fs.readFileSync('components/DreamSymbolsSeerChatInterface.tsx', 'utf8'), + }, +] + +describe('billed tool Seer chat UIs do not auto-retry', () => { + it.each(BILLED_SEER_CHAT_SOURCES)('$rel posts the billed request once', ({ src }) => { + expect(src).not.toMatch(/RETRY_DELAY_MS/) + expect(src).not.toMatch(/isRetryableStatus/) + expect(src).not.toMatch(/retryResponse/) + const performFetchCalls = src.match(/await performFetch\(\)/g) ?? [] + expect(performFetchCalls).toHaveLength(1) + expect(src).toMatch(/Do not auto-retry/) + }) +})