feat(rag): retry a failed LLM request before the first token lands — BOR_LLM_RETRIES/BOR_LLM_RETRY_DELAY with a live 'retrying' status
This commit is contained in:
@@ -75,6 +75,24 @@
|
||||
* optional `tools: [{name, argument}]` array next to `thinking` and
|
||||
* restore re-renders the lines (phase 14 convention).
|
||||
*
|
||||
* LLM retry status (phase 67, TODO.md L3): if the endpoint dies BEFORE
|
||||
* the first frame of an LLM request lands, the server restarts that
|
||||
* 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
|
||||
* 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
|
||||
* about to be tried, N = the configured total). Nothing else changes —
|
||||
* no bubble, no tool line, no banner, no UI-state change: it is a
|
||||
* transient status that the next thinking/tool/delta frame replaces
|
||||
* through its own branch. A retry arrives only before the request's
|
||||
* first piece (locked A2), so it never races a partial answer; the
|
||||
* status gate still covers BOTH live states (thinking AND streaming)
|
||||
* because a LATER agent round may restart while an earlier round
|
||||
* already emitted content (the rare content+tool-call stream).
|
||||
*
|
||||
* Steering notes (phase 15) let the owner tune how Brain answers: a
|
||||
* "Tune" button under every completed brain bubble (deflected included)
|
||||
* opens an inline form → POST /api/steering → the note is stored in
|
||||
@@ -1927,6 +1945,27 @@ async function runTurn(text, { reask = false } = {}) {
|
||||
}
|
||||
appendToolLine(wrap, name, argument);
|
||||
// No page scroll (phase 42): tool lines never yank the viewport.
|
||||
} else if (ev.type === "retry") {
|
||||
// Phase 67 (owner-locked A4): the server restarted the LLM
|
||||
// request before ANY frame of it reached the client (locked
|
||||
// A2) — say exactly what is happening on the existing status
|
||||
// line. Transient status only: no bubble, no tool line, no
|
||||
// banner, no UI-state change — the next thinking/tool/delta
|
||||
// frame replaces it through its own branch. The gate covers
|
||||
// BOTH live states: a LATER agent round may restart while the
|
||||
// UI already streams (a content+tool-call stream from an
|
||||
// earlier round).
|
||||
clearTurnTimeout(); // the stream is alive — the server is restarting the LLM request
|
||||
const attempt = Number(ev.attempt) || 1;
|
||||
const max = Number(ev.max_attempts) || 1;
|
||||
const retryStatus =
|
||||
`Communication interrupted — retrying (${attempt} of ${max})…`;
|
||||
if (uiState === UI_STATE.thinking || uiState === UI_STATE.streaming) {
|
||||
sendStatus.textContent = retryStatus;
|
||||
document
|
||||
.querySelector("#typing-indicator .bubble")
|
||||
?.setAttribute("aria-label", retryStatus);
|
||||
}
|
||||
} else if (ev.type === "delta") {
|
||||
acc += ev.text || "";
|
||||
if (uiState === UI_STATE.thinking) setUiState(UI_STATE.streaming);
|
||||
|
||||
Reference in New Issue
Block a user