Files
brain-of-reese/.agents/phases/complete/20_sources_midstream_bug/00_phase.md
T
ducoterra dbf2af26c6 refactor(agents): migrate .agent/ planning tree to .agents/
Standardize on the .agents/ directory (shared with project skills):
phases/, user_stories/, reports/, screenshots/, validate.sh, and
phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves
history; runtime artifacts move alongside).

Updates every reference in AGENTS.md, README.md, .gitignore, app
docstrings, and test story headers. Historical KB content in data/
and the runtime pipeline.log transcript are left untouched.
2026-09-05 10:57:07 -04:00

6.3 KiB

Phase 20 — Sources Mid-Stream: an in-flight answer is not lost on navigation

Source: TODO.md L3 — "Clicking "sources" while chat is generating clears chat and result will never show up" Story: .agents/user_stories/sources-midstream.md (created by task 02) Context: frontend/assets/app.js — the phase-14 persistence block (STORAGE_KEY = "bor.chat.v1", conversation, saveConversation, rememberBrainTurn), the turn state machine (UI_STATE.thinking / .streaming), the streaming accumulators (acc / thinkingAcc / sawThinking), and the phase-14 restore path; frontend/index.html (#nav-sources link); frontend/assets/header.js (clearChatStorage — the deliberate New-Chat clear, NOT this bug).

Objective

When the user leaves the chat page (the Sources nav link, the document viewer, any link) while a turn is still in flight, the answer generated so far must not vanish. Today the brain message is persisted only on done, so navigating away aborts the stream and the partial answer is lost — the user returns to their own question with no result, ever. After this phase, returning to the chat shows the question and the partial answer that had already streamed (rendered like any brain message, thinking block restored if any).

Owner-confirmed (2026-08-24, roadmap A1)

  1. A partial answer is persisted as a plain brain message — no "(partial)" marker, no sources/suggestions (the turn is dead; the user can re-ask for the full answer).
  2. Navigation before the first answer token (pure thinking) persists nothing brain-side: the question is restored, no empty/partial bubble.
  3. The deliberate New Chat clearChatStorage() (sources/viewer pages) is untouched — that clear is by design (phase 14/19).
  4. No server-side resume (A10 stays stateless) and no "leave page?" confirmation dialog.

Design

  • app.js — one new pagehide handler (window.addEventListener( "pagehide", …) — fires on navigate-away and bfcache store):
    • Guard: only when a turn is in flight (current uiState is UI_STATE.thinking or UI_STATE.streaming) and acc is non-empty.
    • Action: rememberBrainTurn(acc, { thinking: thinkingAcc || undefined }) — reuse the existing save-point helper, so the partial text is stored raw (the restore path re-renders through the escape-first markdown renderer; the phase-17 thinking field restores the collapsed Thinking block).
    • Idempotency guard: a turn-local persistedOnLeave flag so a second pagehide (or bfcache store+restore churn) never appends the same partial message twice. The done save point is unaffected (navigation means the stream is dead; if the user returns via bfcache the turn is already aborted by the unloading page).
  • Restore path: unchanged — a stored partial message is a well-formed brain message and renders exactly like a completed one (minus sources/deflection, which it simply doesn't carry).
  • Non-goals: no resume of the SSE stream, no API changes, no changes to the New Chat buttons, sign-out, or the document-viewer back link.

Dependencies

  • 14_chat_persistence (complete) — bor.chat.v1 shape, save points, restore, and the rememberBrainTurn helper this phase reuses.
  • 17_thinking_display (complete) — the thinking field on persisted brain messages and the thinkingAcc accumulator.
  • 19_shared_header (complete) — the #nav-sources link (admin-only) the bug report clicks.
  • 18_follow_bottom_scroll (complete) — no overlap (scroll gating only).

Tasks

  1. 01_persist_inflight_turn.md — the pagehide partial-persistence handler in app.js + source-level unit pins.
  2. 02_e2e_story_suite_commit.md — tests/e2e/test_sources_midstream_bug.py (the story gate, isolated), regression suites, story file, final validation, the single atomic commit, phase move to complete/.

Locked decisions

  • A10 untouched — API stays stateless; no resume. A11 untouched — vanilla JS, no CDN. A16 honored — one new story E2E suite + adapted regressions. No anchor changed.

Testing & Quality

  • Unit (source-level, new tests/unit/test_sources_midstream.py, following the repo's source-pin pattern): app.js registers a pagehide listener; the guard references the in-flight uiState and a non-empty acc; the partial path calls rememberBrainTurn with thinking: thinkingAcc || undefined; a turn-local idempotency flag exists; STORAGE_KEY/save-point comments updated to list the new save point.
  • Integration: none (no app/ changes) — the uv run pytest --cov=app number must stay at today's.
  • Coverage: frontend-only; the >90% app/ gate is unaffected, re-run to prove it.
  • E2E: tests/e2e/test_sources_midstream_bug.py (task 02), green in isolation (prereq podman compose up -d db).
  • Lint/types: uv run ruff check . && uv run pyright clean.

Completion Criteria

  • Admin, mid-stream, clicks Sources → returns to /: the question and the already-streamed partial answer are both rendered; no error banner; bor.chat.v1 holds the partial brain message.
  • Navigate away before the first token → back: question restored, no empty/partial brain bubble.
  • A completed turn is persisted exactly as before (sources, deflection, suggestions intact).
  • New Chat from the sources page still clears the conversation.
  • uv run pytest green; uv run pytest --cov=app --cov-report=term-missing ≥ today's number.
  • uv run pytest tests/e2e/test_sources_midstream_bug.py -v --no-cov green in isolation; regressions green in isolation (one command each): test_chat_persistence.py, test_thinking_display.py, test_shared_header.py.
  • uv run ruff check . && uv run pyright clean.
  • UI Structure Check (AGENTS.md rule 5): no new UI surface — the restored partial renders through the existing bubble/thinking contract.
  • .agents/user_stories/sources-midstream.md exists.
  • One --no-gpg-sign commit (below); .agents/phases/todo/20_sources_midstream_bug/ moved to .agents/phases/complete/.

Commit

git add -A .agents/ frontend/ tests/ && git commit --no-gpg-sign -m "fix(chat): keep the in-flight answer when navigating away mid-turn — partial answer restored on return"