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:
2026-09-02 10:52:38 -04:00
parent f04ddbe1f8
commit 88293ed02f
44 changed files with 2488 additions and 56 deletions
+39
View File
@@ -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);