phase: 114_embed_question_length
All verification passes complete — the phase was already fully implemented in the working tree, and every gate is green. No defects found; no code changes were needed.
**Final verification pass — Phase 114 (embed question length):**
- Verified truncation: `chat.py:459` embeds `request.message[:settings.embed_question_max_chars]` (default 1200, `BOR_EMBED_QUESTION_MAX_CHARS`, `>0` validator); full question still reaches the LLM prompt/log.
- Verified error mapping: `EmbeddingInputTooLargeError(EmbeddingError)` (byte-identical message) caught **before** `EmbeddingError` → no retry, terminal frame `detail="Question too long — trim it and re-ask."` + reachability-fine hint; `ChatErrorEvent.hint` additive.
- Verified frontend chain: frame `hint` → `err.hint` → `setUiState(error, …, {hint})` → `showErrorBanner(…, {retryable: true})` — hint replaces default `ERROR_HINT`, phase-111 `#banner-retry` button revealed. 4,000-char clamp untouched (A2).
- `uv run pytest tests/unit/test_embed_question_length.py -v --no-cov` → 21 passed
- `uv run pytest tests/e2e/test_embed_question_length.py -v --no-cov` (isolation, DB up) → 1 passed (4,000-char question → done, no banner)
- Regression: `test_llm_retry.py` 4 passed · `test_oneshot_llm_retry.py` 2 passed · `test_chip_sizing_question_cap.py` 6 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 2444 passed, TOTAL **99%** (>90% gate)
- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings
**Completion criteria:** (1) 4,000-char question embeds prefix + full prompt ✅ · (2) too-large → accurate frame + hint + Retry button ✅ · (3) reachability failure byte-identical (retries + old copy) ✅ · (4) all gates green ✅ · (5) commit/phase-move → left to the harness per instructions (no `git add`/`commit` run).
**Deviations:** none. **Next pending phase:** `115_doc_draft_discard`.
This commit is contained in:
+22
-5
@@ -1251,7 +1251,7 @@ document.addEventListener("visibilitychange", () => {
|
||||
* turn is in flight (click or Enter aborts it). It is never disabled
|
||||
* anymore, and the spinner never shows: the "Stop" label + the .is-stop
|
||||
* treatment carry the in-flight state. */
|
||||
export function setUiState(state, errorDetail = "") {
|
||||
export function setUiState(state, errorDetail = "", opts = {}) {
|
||||
uiState = state;
|
||||
stopThinkingClock();
|
||||
clearTurnTimeout(); // the guard only owns the pre-token window
|
||||
@@ -1278,7 +1278,11 @@ export function setUiState(state, errorDetail = "") {
|
||||
} else {
|
||||
removeTyping();
|
||||
}
|
||||
if (state === UI_STATE.error) showErrorBanner(errorDetail, { retryable: true });
|
||||
// Phase 114 (TODO L6): turn-error opts (the SSE error frame's optional
|
||||
// hint) merge into the banner call — the frame's hint replaces the
|
||||
// default reachability hint when present (showErrorBanner's opts.hint).
|
||||
if (state === UI_STATE.error)
|
||||
showErrorBanner(errorDetail, { retryable: true, ...opts });
|
||||
}
|
||||
|
||||
/* Phase 104 (owner 2026-09-12, A3/A4): the visible question-length cap.
|
||||
@@ -2135,7 +2139,12 @@ function showErrorBanner(detail, opts = {}) {
|
||||
banner.hidden = false;
|
||||
banner.classList.add("is-error");
|
||||
banner.setAttribute("role", "alert");
|
||||
bannerText.textContent = detail ? `${detail} ${ERROR_HINT}` : ERROR_HINT;
|
||||
// Phase 114 (TODO L6): a frame-carried hint (the "question too long"
|
||||
// case — reachability is fine, only the length is the problem) replaces
|
||||
// the default reachability hint when present.
|
||||
bannerText.textContent = detail
|
||||
? `${detail} ${opts.hint ?? ERROR_HINT}`
|
||||
: (opts.hint ?? ERROR_HINT);
|
||||
// Phase 111 (task 01): reveal the banner Retry button only for failed
|
||||
// chat turns (opts.retryable) AND when a retryable bubble exists.
|
||||
if (opts.retryable) {
|
||||
@@ -2558,7 +2567,12 @@ async function runTurn(text, { reask = false } = {}) {
|
||||
}
|
||||
}
|
||||
} else if (ev.type === "error") {
|
||||
throw new Error(ev.detail || "Something went wrong on my side.");
|
||||
// Phase 114 (TODO L6): carry the frame's optional hint (the
|
||||
// "question too long" frame has one) through the throw — the
|
||||
// banner shows it in place of the default reachability hint.
|
||||
const err = new Error(ev.detail || "Something went wrong on my side.");
|
||||
err.hint = ev.hint;
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
// Stream-drop guard (phase 17): frames arrived but no `done` event —
|
||||
@@ -2619,7 +2633,10 @@ async function runTurn(text, { reask = false } = {}) {
|
||||
err instanceof Error && err.message
|
||||
? err.message
|
||||
: "Something went wrong on my side.";
|
||||
setUiState(UI_STATE.error, detail);
|
||||
// Phase 114 (TODO L6): the SSE error frame's optional hint flows to
|
||||
// the banner (setUiState → showErrorBanner's opts.hint); the
|
||||
// phase-111 Retry button rides along on the same turn-error path.
|
||||
setUiState(UI_STATE.error, detail, err instanceof Error ? { hint: err.hint } : {});
|
||||
}
|
||||
} finally {
|
||||
// done | error | stop → idle: always settle, always focus back.
|
||||
|
||||
Reference in New Issue
Block a user