phase: 120_failed_turn_retry
All verification complete. Final report: **Phase 120 — Failed-turn retry: verification pass (all 3 tasks were done; final verification + 1 regression fix)** **Verified:** `ChatMessage.failed`/`error` (≤500, `extra="forbid"` intact); `finalizeFailedTurn` funnel on the 3 failure paths (catch-else, stream-drop guard, zero-frame fallback) with `failed: true` + capped detail + `markLastRetryable`; `appendFailedNote` restore branch (Save-as-doc/Tune excluded); `showErrorBanner`/`retryLastTurn` byte-pinned untouched; only the three paths persist `failed: true` (grep + unit pin); no test asserts the old broken behavior. **Defect found & fixed (rule 7):** a real navigate-away mid-turn let the browser's teardown fetch rejection (TypeError, not AbortError) leak into the failed funnel, persisting a phantom failed brain record — `test_sources_midstream_bug.py::test_no_orphan_brain_message_when_navigated_before_first_token` failed (2 `.msg` after reload) and violated the phase-20 navigate-away convention. Fixed: turn-scoped `leftThePage` flag (set unconditionally on `pagehide`, reset in `runTurn`) skips the funnel in the catch-else branch; pinned by new unit test `test_navigate_away_is_not_a_failed_turn`. No phase-overview/PLAN/todo/complete files touched; no commits made. **Gates (exact):** - `uv run pytest` → 2577 passed - `uv run pytest --cov=app --cov-report=term-missing` → TOTAL 4271 stmts, 99% (>90%) - `uv run pytest tests/e2e/test_failed_turn_retry.py -v --no-cov` → 4 passed (isolated) - `uv run ruff check . && uv run pyright` → clean (0 errors) - Regression E2E, isolated: `test_sources_midstream_bug.py` 6/6 (was 5/6); `test_llm_retry`/`test_tool_scaffolding_guardrails`/`test_stop_generation`/`test_navbar_refresh` 17/17 **Completion criteria:** (1) network error → banner + in-bubble Retry, re-ask without re-typing ✅ (E2E A); (2) refresh restores failed bubble + working Retry, no "new chat" ✅ (E2E C); (3) stopped/successful turns byte-identical ✅ (negative E2E, stop suite, byte-identity units); (4) pytest/coverage/lint/types ✅; (5) commit + phase move — left to the harness per pass rules. **Notable:** deviation = the regression fix above (a navigation is not a failed turn; phase-20 partial-persist convention restored). Next pending phase: `121_git_source_tokens`.
This commit is contained in:
+19
-2
@@ -744,8 +744,8 @@ class ChatMessage(BaseModel):
|
||||
(phase 14) — the stored ``messages`` payload of a saved chat (phase 50).
|
||||
|
||||
``{who, text, sources?, related?, deflected?, suggestions?,
|
||||
thinking?, tools?, stopped?}`` — raw text, never HTML, so a saved
|
||||
chat restores pixel-identical through the existing
|
||||
thinking?, tools?, stopped?, failed?, error?}`` — raw text, never
|
||||
HTML, so a saved chat restores pixel-identical through the existing
|
||||
``renderStoredMessage`` path.
|
||||
``extra="forbid"`` rejects unknown keys (a corrupted or HTML-shaped
|
||||
payload, e.g. a stray ``<b>``-ish extra key) at the boundary with a
|
||||
@@ -774,6 +774,19 @@ class ChatMessage(BaseModel):
|
||||
shape; ``suggestions`` ≤ 50 chips of ≤ 200 chars; ``tools``
|
||||
≤ 50 — one entry per tool call, the round cap is 10). Only value
|
||||
bounds were added — the accepted/rejected KEYS are unchanged.
|
||||
|
||||
Phase 120 (task 01, locked A1): a FAILED turn (network error, SSE
|
||||
``error`` frame, stream drop — the client-side failure paths) stores
|
||||
``{who: "brain", text: <detail or fallback>, failed: true,
|
||||
error: <detail>}`` — the phase-48 ``stopped`` precedent: a marker +
|
||||
the capped detail on the brain record itself, no separate error
|
||||
table, no new API (``retryLastTurn``'s pop-the-last-brain-record
|
||||
logic works on a failed record UNCHANGED — the question's user
|
||||
record immediately precedes it). ``error`` is the persisted banner
|
||||
detail, capped at 500 (the phase-83 value-bounds style). No
|
||||
serializer change: ``None`` values flow as absent/None exactly like
|
||||
``stopped`` today — the phase-50 byte-identical round-trip contract
|
||||
covers the new keys automatically.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
@@ -787,6 +800,10 @@ class ChatMessage(BaseModel):
|
||||
thinking: str | None = Field(default=None, max_length=32_000)
|
||||
tools: list[ToolCall] | None = Field(default=None, max_length=50)
|
||||
stopped: bool | None = None
|
||||
# Phase 120 (task 01, locked A1): the failed-turn marker + the
|
||||
# persisted error detail (the phase-48 ``stopped`` precedent).
|
||||
failed: bool | None = None
|
||||
error: str | None = Field(default=None, max_length=500)
|
||||
|
||||
|
||||
class SavedChatCreate(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user