fix(chat): raise pre-token guard from 120 s to 300 s
The client-side TURN_TIMEOUT_MS was the binding constraint: turns with slow prompt processing (no first SSE frame within 120 s of visible time) errored with the 'stuck' copy even though nginx (300 s) and BOR_LLM_TIMEOUT (300 s) would have let them run. Raise the guard to 300 s so the upstream timeouts are reachable, and re-pin the tests: the unit constant pins and the fake-clock E2E timeline (295 s hidden + 290 s after the re-arm = 585 s: past the original 300 s deadline, short of the re-armed 595 s deadline). Verified: tests/unit (full, 100% pass), tests/e2e/test_hidden_tab_stream.py and tests/e2e/test_loading_feedback.py in isolation.
This commit is contained in:
+11
-11
@@ -22,7 +22,7 @@
|
||||
* SSE events), the live collapsible Thinking block IS the
|
||||
* visible feedback (it replaces the typing dots; the UI
|
||||
* state stays "thinking" — the button stays the "Stop"
|
||||
* control) and the 120s guard clears on the first
|
||||
* control) and the 300s guard clears on the first
|
||||
* thinking *or* delta event.
|
||||
* • streaming — the first delta removes the dots and appends live into
|
||||
* the answer bubble (auto-collapsing the Thinking block,
|
||||
@@ -39,7 +39,7 @@
|
||||
* SAME finally path — no error banner, no scroll
|
||||
* (phase 42).
|
||||
* • error — red banner (role="alert") with an actionable retry hint;
|
||||
* the 120s guard (TURN_TIMEOUT_MS) catches hung pre-token
|
||||
* the 300s guard (TURN_TIMEOUT_MS) catches hung pre-token
|
||||
* streams and the sawDone guard (phase 17) catches a
|
||||
* stream that dies after frames but before `done`, so the
|
||||
* button can never sit zombified.
|
||||
@@ -85,7 +85,7 @@
|
||||
* request (up to BOR_LLM_RETRIES retries, BOR_LLM_RETRY_DELAY seconds
|
||||
* apart — a flat delay, no backoff, owner-locked A3) and streams one
|
||||
* `retry` SSE frame per wait. The handler treats it with the same
|
||||
* status pattern as the `tool` frames: the 120s guard clears (a frame
|
||||
* status pattern as the `tool` frames: the 300s guard clears (a frame
|
||||
* arrived) and the EXISTING #send-status live region + the
|
||||
* typing-indicator aria-label read the owner-locked copy
|
||||
* "Communication interrupted — retrying (n of N)…" (n = the attempt
|
||||
@@ -255,7 +255,7 @@
|
||||
* question even on browsers that fire pagehide on a merely-hidden tab
|
||||
* (e.g. Safari; Chromium fires only visibilitychange — task 01); a REAL
|
||||
* navigation never runs a settle, so the leave-save is unchanged there.
|
||||
* (2) The 120s pre-token guard counts only VISIBLE time: when the tab
|
||||
* (2) The 300s pre-token guard counts only VISIBLE time: when the tab
|
||||
* returns to visible with the guard still armed (it clears on the first
|
||||
* thinking/delta/retry frame), it re-arms with a fresh TURN_TIMEOUT_MS —
|
||||
* a slow first frame arriving while the tab was hidden past the deadline
|
||||
@@ -305,7 +305,7 @@ const brand = () => window.BOR_BRAND || "Brain of Reese";
|
||||
* cleared on the first delta (entering "streaming") and on every
|
||||
* terminal transition. Exported so the constant is testable (tests/unit/
|
||||
* test_frontend_feedback.py). */
|
||||
export const TURN_TIMEOUT_MS = 120_000;
|
||||
export const TURN_TIMEOUT_MS = 300_000;
|
||||
|
||||
/* Phase 87 (TODO.md L5): the latest tool line's visible "processing"
|
||||
* threshold (A5): below 5s a frameless gap reads as normal latency;
|
||||
@@ -1023,7 +1023,7 @@ async function loadHealth() {
|
||||
let uiState = UI_STATE.idle;
|
||||
let thinkingClock = 0; // setInterval id — elapsed-seconds hint
|
||||
let thinkingStart = 0; // Date.now() when "thinking" began
|
||||
let turnTimeout = 0; // setTimeout id — 120s pre-token guard
|
||||
let turnTimeout = 0; // setTimeout id — 300s pre-token guard
|
||||
let turnTimeoutCb = null; // the guard's callback — lets visibilitychange re-arm it (phase 73)
|
||||
/* Phase 87 (TODO.md L5): the per-tool-line elapsed clock — one clock per
|
||||
* turn, re-armed per `tool` frame: the baseline (toolLineStart) resets on
|
||||
@@ -1057,7 +1057,7 @@ let persistedOnLeave = false; // pagehide partial-persist at most once
|
||||
let leavePartialIndex = -1; // index of this turn's pagehide partial (-1 = none)
|
||||
/* Phase 48 (2026-08-29, TODO.md L3): the user-stop machinery.
|
||||
* `turnAbort` owns the in-flight fetch (the Stop button aborts it; the
|
||||
* 120s guard aborts the same controller as its backstop); `stoppedByUser`
|
||||
* 300s guard aborts the same controller as its backstop); `stoppedByUser`
|
||||
* marks a turn the Stop button took, so the catch's AbortError can tell a
|
||||
* user stop from the guard's own abort (the guard sets `aborted` first).
|
||||
* Both are turn-scoped: created/reset at the top of handleSend, cleared
|
||||
@@ -1159,9 +1159,9 @@ function stopToolLineClock() {
|
||||
}
|
||||
|
||||
/* Phase 73 (task 02; task 01 C2 — confirmed): a merely-HIDDEN tab must
|
||||
* never stop a turn, but the 120s pre-token guard is a plain setTimeout,
|
||||
* never stop a turn, but the 300s pre-token guard is a plain setTimeout,
|
||||
* so hidden time counted toward it: a turn whose first frame lands while
|
||||
* the tab is hidden past the 120s mark errored with the "stuck" copy
|
||||
* the tab is hidden past the 300s mark errored with the "stuck" copy
|
||||
* (the user perceives "switching tabs killed the answer"). When the tab
|
||||
* returns to VISIBLE with the guard still armed (it is only armed in the
|
||||
* pre-token window — cleared on the first thinking/delta/retry frame and
|
||||
@@ -2089,7 +2089,7 @@ async function runTurn(text, { reask = false } = {}) {
|
||||
|
||||
let wrap = null;
|
||||
let res = null;
|
||||
let aborted = false; // the 120s guard already took the turn to error
|
||||
let aborted = false; // the 300s guard already took the turn to error
|
||||
// Phase 20: acc / thinkingAcc / persistedOnLeave live at module scope
|
||||
// (the pagehide handler reads them) but reset here, so they stay
|
||||
// turn-scoped exactly like the other turn locals. Phase 73:
|
||||
@@ -2339,7 +2339,7 @@ async function runTurn(text, { reask = false } = {}) {
|
||||
}
|
||||
} catch (err) {
|
||||
if (aborted) {
|
||||
// The 120s guard already took the turn to the error state — its
|
||||
// The 300s guard already took the turn to the error state — its
|
||||
// own abort surfaces here, and it is never read as a user stop.
|
||||
} else if (stoppedByUser || err?.name === "AbortError") {
|
||||
// Phase 48 (owner-locked): the STOP path — no error banner. When
|
||||
|
||||
Reference in New Issue
Block a user