Files
brain-of-reese/.agent/user_stories/sources-midstream.md
T
ducoterra 4971e2859d
Build and Push Containers / build-and-push-app (push) Successful in 12s
Build and Push Containers / build-and-push-db (push) Successful in 10s
chore(agent): track .agent/ planning tree in git
Remove the blanket .agent/ gitignore so the phase roadmap, user
stories, reports, and PLAN.md are versioned with the code. Only
runtime artifacts (.agent/phase-sessions/, .agent/pipeline.log)
remain ignored. Update AGENTS.md git protocol rule to match.
2026-09-01 10:18:22 -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.