Files
brain-of-reese/.agents/user_stories/sources-midstream.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

3.7 KiB

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

Phase: 20_sources_midstream_bug · E2E: tests/e2e/test_sources_midstream_bug.py

Bug report (TODO.md L3, verbatim)

"Clicking "sources" while chat is generating clears chat and result will never show up"

Narrative

As a user with a question in flight, I want to click "Sources" while Brain is still generating without losing the answer — when I come back to the chat, what had already streamed should be waiting for me.

  • Given I asked a question and Brain's answer is still streaming
  • When I click the "Sources" nav link (or otherwise leave the chat page) mid-stream
  • Then the stream is aborted by the navigation (no server-side resume — A10 stays stateless), but returning to / shows my question and the partial answer that had already streamed, rendered exactly like any brain message (thinking block included, if any had streamed).

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.

Acceptance criteria

  1. pagehide (navigate-away / bfcache store) with an in-flight turn (thinking or streaming) and non-empty streamed text persists the partial raw answer via the existing rememberBrainTurn save-point helper (idempotent per turn — persistedOnLeave).
  2. Returning to / restores the question and the partial brain message through the existing restore path (escape-first markdown, optional collapsed thinking block) — no new UI surface, no error banner.
  3. Navigation during pure thinking (no answer tokens) persists nothing brain-side: exactly the user message is restored.
  4. A completed turn persists exactly as before (done metadata intact; no duplicate brain message).
  5. New Chat from the sources page still clears the conversation (clearChatStorage, phase 14/19 — untouched).
  6. Unit + integration green, app/ coverage ≥ today's number (>90% gate), story E2E green in isolation, one --no-gpg-sign commit.

Playwright Mapping Rule

Test Scenario → tests/e2e/test_sources_midstream_bug.py (mock LLM, seeded KB):

  1. test_partial_answer_survives_sources_nav_midstream — admin (login, next=/), send the ~9s long-answer question, wait for the first streamed frames to render, click #nav-sources (lands on /sources.html), page.goto("/") → question + partial answer rendered, no role="alert", bor.chat.v1 holds the partial brain message (starts with the first streamed chunk, shorter than the full answer, no done metadata).
  2. test_no_orphan_brain_message_when_navigated_before_first_token — the mock streams the phase-17 thinking scratchpad, then a 4s pre-token pause; navigate during the pause, return to / → exactly one user message, zero brain messages, in the DOM and in storage.
  3. test_completed_turn_unaffected — a fully settled turn → trip to Sources and back → full answer, source chip, and a byte-identical bor.chat.v1 payload (done metadata intact, no duplicate).
  4. test_new_chat_still_clears_conversation — completed turn → /sources.html → click the sources-page New Chat button → lands on / with the empty state and bor.chat.v1 removed.