feat(rag): pass chat history with prior thinking to the LLM
Build and Push Containers / build-and-push-app (push) Successful in 1m39s
Build and Push Containers / build-and-push-db (push) Successful in 11s

Phase 74 (TODO.md L4): a follow-up question now reaches the model WITH
the conversation so far — every prior user/brain turn and the prior
thinking blocks on brain turns (preserve-thinking) — while
POST /api/chat stays stateless (A10): the client provides the history
in the request body and the server stores nothing new.

Server (task 01):
- ChatRequest.history: optional list[HistoryTurn] (who: user|brain,
  text, optional thinking) — absent/empty keeps the request
  byte-identical to pre-phase-74 (the two-message [system, user]
  request; the kill-switch semantics are pinned in the integration
  suite).
- app.rag.prompts.history_to_messages: pure mapper — walks the turns
  newest-first against the settings budgets (history_max_turns=40 /
  history_max_chars=24000, BOR_HISTORY_MAX_TURNS /
  BOR_HISTORY_MAX_CHARS); a capped turn is dropped WHOLE (never cut
  mid-answer); the kept window is returned oldest-first; brain turns
  carry their thinking as reasoning_content (A4) only when
  non-empty.
- Both branches feed it: the deflected path splices it between the
  system prompt and the current user message (the phase-71 recovery
  still rebuilds from messages[1:]), the grounded agent receives
  run_agent(..., history=hist); llm.py's message params widen to
  list[dict[str, Any]] (string-only messages stay byte-identical on
  the wire — the SDK passes message dicts through verbatim).
- The per-turn log line (PLAN §9) gains history_msgs=N after
  kb_chars=N.
- Pins: tests/unit/test_history.py (mapper: mapping, reasoning
  gating, both budgets, drop-whole, ordering, empty default),
  tests/unit/test_config.py (the two settings + env overrides),
  tests/unit/test_agent.py (the history splice + the default),
  tests/integration/test_chat_api.py (deflected AND grounded forward
  the history incl. reasoning_content, no-history byte-identity, 422
  pins, the log field).

Client (task 02):
- runTurn — the single funnel for fresh send / phase-49 retry /
  phase-53 stale-regen — sends history = the conversation record
  minus the current question, with thinking only on brain records
  that streamed one (undefined drops the key from the JSON, the
  record's convention); the question is never duplicated into the
  history.

Wire proof (task 03):
- The mock's echo my history marker (HISTORY_TRIGGER) answers with
  the deterministic history echo — history: N prior messages; last
  answer tail: <last 24 chars>; thinking: yes|no — checked BEFORE
  the DEFLECT_MODE branch (like TABLE_TRIGGER), so it fires on both
  turn branches whatever the gate says; the module docstring records
  the user/assistant-only history invariant that keeps every
  existing (tool-result-classified) marker flow unaffected.
- tests/e2e/test_llm_history.py (isolated): a grounded follow-up and
  a deflected follow-up both receive history: 2 prior messages +
  thinking: yes + the byte-exact tail of turn 1's answer (derived
  from the persisted bor.chat.v1 record — the same array the client
  maps into the body); a cold start receives history: 0 prior
  messages / last answer tail: none / thinking: no.
- Regressions green in isolation: chat_rag, chat_history (phase 50),
  agent_document_tools, harness_aligned_tools, stop_generation,
  retry_answer, response_to_docs.
This commit is contained in:
2026-09-05 16:04:40 -04:00
parent a16130c71d
commit 055c0b5d85
29 changed files with 1418 additions and 18 deletions
@@ -0,0 +1,18 @@
All criteria verified — no defects found, no fixes needed. Final report:
**Phase 73 (hidden_tab_stream) — final verification pass: ALL GREEN**
- Verified C1 fix: `pagehide` partial is correlated via `leavePartialIndex`; done/fallback/stop settles *replace* it in place (identity-guarded `rememberBrainTurn` in-place mode) — `frontend/assets/app.js`
- Verified C2 fix: `visibilitychange` re-arms the still-armed 120s pre-token guard on return to visible (hidden time no longer counts)
- Verified phase-48 teardown untouched: Stop / tab close / navigation still abort (`turnAbort`, `cancelStream`)
- Commit `a16130c fix(chat): keep generating while the tab is hidden`: unsigned (`--no-gpg-sign` ✓), body carries the root-cause line ("none of C1-C3 — Chromium 151 real mode: hidden tab neither stops the stream nor fires pagehide; C1 latent via synthetic pagehide; C2 confirmed to fire while hidden")
**Test / lint / coverage (exact commands):**
- `uv run pytest tests/e2e/test_hidden_tab_stream.py -v --no-cov` → 4 passed (36.8s)
- Isolation regressions: `test_stop_generation` 3 passed · `test_chat_persistence` 4 passed · `test_chat_rag` 3 passed · `test_chat_history` 5 passed · `test_retry_answer` 4 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 1519 passed, `app/` coverage **99%** (>90% ✓)
- `uv run ruff check . && uv run pyright` → clean (0 errors)
**Completion criteria:** root-cause line in commit ✓ · phase E2E green in isolation (exactly-one-brain-turn pin) ✓ · real departures unchanged (stop/persistence green) ✓ · full suite + >90% + ruff/pyright ✓ · atomic unsigned Conventional-Commits commit; task files 01–03 in `complete/` (00_phase.md move is the harness's on success) ✓
**Deviations:** none. **Next pending phase:** `74_llm_chat_history`.
@@ -0,0 +1,78 @@
........................................................................ [ 4%]
........................................................................ [ 9%]
........................................................................ [ 14%]
........................................................................ [ 18%]
........................................................................ [ 23%]
........................................................................ [ 28%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 42%]
........................................................................ [ 47%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 61%]
........................................................................ [ 66%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 80%]
........................................................................ [ 85%]
........................................................................ [ 90%]
........................................................................ [ 94%]
........................................................................ [ 99%]
....... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
-----------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 22 0 100%
app/api/chat.py 176 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 8 0 100%
app/api/sync.py 101 0 100%
app/config.py 126 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 20 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/db.py 21 0 100%
app/main.py 52 0 100%
app/models.py 86 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 65 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 205 0 100%
-----------------------------------------------
TOTAL 2946 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,18 @@
Task 03 complete — all gates green, phase committed.
**Implemented**
- `tests/e2e/test_hidden_tab_stream.py` (4 tests): synthetic `pagehide`+hidden `visibilityState` mid-stream → answer completes exactly once, `bor.chat.v1` holds exactly ONE brain turn (partial read pinned pre-settle, full byte-exact post-settle), auto-saved `saved_chats` row carries the same single turn (admin context), `query_log==1` (no phase-48 cancel); reload restores one brain bubble; no-event baseline; fake-clock pre-token guard re-arm test.
- Both pins verified discriminating: with the C1 replace disabled the main test catches `['user','brain','brain']`; with the re-arm disabled the C2 test catches the "stuck" banner. Fixtures restored byte-identical afterwards.
**Results (exact commands)**
- `uv run pytest tests/e2e/test_hidden_tab_stream.py -v --no-cov` → 4 passed
- Regressions in isolation: `test_chat_rag.py` 3✓, `test_chat_persistence.py` 4✓, `test_chat_history.py` 5✓, `test_stop_generation.py` 3✓, `test_retry_answer.py` 4✓
- `uv run pytest --cov=app --cov-report=term-missing` → 1519 passed, app/ 99% (>90%)
- `uv run ruff check . && uv run pyright` → clean, 0 errors
- Committed `a16130c` `fix(chat): keep generating while the tab is hidden` (`--no-gpg-sign`), task-01 root-cause line in the body; recorded the harness's 01/02 move + task-01 evidence.
**Decisions**
- C2 test uses `pretend to think slowly` (pure 3s pre-token silence) + Playwright fake clock: the task's suggested `think out loud then hesitate` streams scratchpad first, clearing the guard before its 4s pause — it cannot exercise the re-arm.
- Did not move `todo/73_hidden_tab_stream/` (harness rule; it holds `00_phase.md` + this task file for the harness to move on success).
**Next pending task:** `.agents/phases/todo/74_llm_chat_history/01_server_history_ingestion.md`
@@ -0,0 +1,78 @@
........................................................................ [ 4%]
........................................................................ [ 9%]
........................................................................ [ 14%]
........................................................................ [ 18%]
........................................................................ [ 23%]
........................................................................ [ 28%]
........................................................................ [ 33%]
........................................................................ [ 37%]
........................................................................ [ 42%]
........................................................................ [ 47%]
........................................................................ [ 52%]
........................................................................ [ 56%]
........................................................................ [ 61%]
........................................................................ [ 66%]
........................................................................ [ 71%]
........................................................................ [ 75%]
........................................................................ [ 80%]
........................................................................ [ 85%]
........................................................................ [ 90%]
........................................................................ [ 94%]
........................................................................ [ 99%]
....... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
-----------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 22 0 100%
app/api/chat.py 176 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 8 0 100%
app/api/sync.py 101 0 100%
app/config.py 126 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 20 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/db.py 21 0 100%
app/main.py 52 0 100%
app/models.py 86 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 65 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 205 0 100%
-----------------------------------------------
TOTAL 2946 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,16 @@
All done — full gate green. Final report:
**Implemented (phase 74, task 01 — server-side history ingestion):**
- `app/config.py`: `history_max_turns=40` / `history_max_chars=24000` (`BOR_`-overridable, fail-loud validators on negatives, house pattern)
- `app/schemas.py`: `HistoryTurn` + `ChatRequest.history` (default `[]`, schema ceiling 100)
- `app/rag/prompts.py`: pure `history_to_messages()` — newest-first walk, both budgets, drop-whole (contiguous newest window), chronological output, `reasoning_content` only when thinking non-empty
- `app/api/chat.py`: `hist` mapped once per turn; deflected branch splices it into `messages`; grounded passes `history=hist` to `run_agent`; log line gains `history_msgs=N` after `kb_chars=N`
- `app/rag/agent.py`: `run_agent(..., history=())` spliced between system and current user; obsolete `cast`s dropped; tool rounds / recovery / retries unchanged
- `app/rag/llm.py`: `chat`/`chat_stream`/`chat_stream_retried` widened to `list[dict[str, Any]]` (no transport change)
- Tests: 16 new in `tests/unit/test_history.py`, 4 in `test_config.py`, 3 in `test_agent.py`, 5 in `tests/integration/test_chat_api.py` (deflected + grounded forwarding incl. `reasoning_content`, no-history byte-identical pin, 422s, caplog `history_msgs`)
**Results:** `uv run pytest` → 1547 passed (baseline 1519); `uv run pytest --cov=app` → TOTAL 99% (>90%, all touched files 100%); `uv run ruff check .` → clean; `uv run pyright` → 0 errors; `uv run .agents/validate.sh` → exit 0.
**Notes:** `.agents/PLAN.md` is absent from the repo — worked from the phase overview + task spec (A2/A3/A4 assumptions). Trim semantics implemented as contiguous newest window (walk stops at first non-fitting turn). No commit, per task (task 03 commits).
**Next pending task:** `.agents/phases/todo/74_llm_chat_history/02_client_sends_history.md`
@@ -0,0 +1,78 @@
........................................................................ [ 4%]
........................................................................ [ 9%]
........................................................................ [ 13%]
........................................................................ [ 18%]
........................................................................ [ 23%]
........................................................................ [ 27%]
........................................................................ [ 32%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 51%]
........................................................................ [ 55%]
........................................................................ [ 60%]
........................................................................ [ 65%]
........................................................................ [ 69%]
........................................................................ [ 74%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 93%]
........................................................................ [ 97%]
................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
-----------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 22 0 100%
app/api/chat.py 177 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 8 0 100%
app/api/sync.py 101 0 100%
app/config.py 140 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 20 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/db.py 21 0 100%
app/main.py 52 0 100%
app/models.py 86 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 210 0 100%
-----------------------------------------------
TOTAL 2989 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK
@@ -0,0 +1,19 @@
All gates green and the diff is exactly the specified change. Final report:
## Task 02 complete — client sends conversation history (with thinking)
**Implemented**
- `frontend/assets/app.js` `runTurn` (single funnel for fresh send / phase-49 retry / phase-53 stale-regen): builds `history = conversation.slice(0, -1).map(...)` carrying exactly `{who, text, thinking}` (`thinking` only on brain records that streamed one — `undefined` drops the key) and sends `body: JSON.stringify({ message: text, history })`.
- Verified the invariant: in all three paths the sent question is the LAST `conversation` entry at fetch time (fresh send pushes it; retry/stale-regen pop the old answer and keep the question last).
- No UI change; no other files touched by this task (task 01's server work remains uncommitted in-tree for task 03's phase commit).
**Verification**
- Wire check (throwaway Playwright script, run against real app + deterministic mock, then deleted): 10/10 PASS — turn 1 `history: []`; turn 2 history = first Q/A pair with 2.7k-char `thinking` on the brain entry and no extra keys; retry's history byte-identical to turn 2 (popped answer excluded, question not duplicated). Server logs show `history_msgs=0 → 2 → 2`.
- `uv run pytest` → 1547 passed
- `uv run pytest --cov=app --cov-report=term-missing` → 99% (>90%)
- `uv run ruff check . && uv run pyright` → clean (0 errors)
- E2E regressions in isolation: `test_chat_rag.py` 3 passed, `test_retry_answer.py` 4 passed, `test_thinking_display.py` 5 passed
**Decisions** — none; change matches the task spec verbatim. No commit (task 03 owns the phase commit).
**Next pending task:** `74_llm_chat_history/03_mock_marker_e2e.md` (mock `echo my history` marker + story E2E + commit).
@@ -0,0 +1,78 @@
........................................................................ [ 4%]
........................................................................ [ 9%]
........................................................................ [ 13%]
........................................................................ [ 18%]
........................................................................ [ 23%]
........................................................................ [ 27%]
........................................................................ [ 32%]
........................................................................ [ 37%]
........................................................................ [ 41%]
........................................................................ [ 46%]
........................................................................ [ 51%]
........................................................................ [ 55%]
........................................................................ [ 60%]
........................................................................ [ 65%]
........................................................................ [ 69%]
........................................................................ [ 74%]
........................................................................ [ 79%]
........................................................................ [ 83%]
........................................................................ [ 88%]
........................................................................ [ 93%]
........................................................................ [ 97%]
................................... [100%]
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
/var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.13.13-final-0 _______________
Name Stmts Miss Cover
-----------------------------------------------
app/__init__.py 1 0 100%
app/api/__init__.py 0 0 100%
app/api/auth.py 22 0 100%
app/api/chat.py 177 0 100%
app/api/chats.py 110 0 100%
app/api/config.py 7 0 100%
app/api/doc_drafts.py 93 0 100%
app/api/docs.py 50 0 100%
app/api/git_sources.py 212 0 100%
app/api/health.py 10 0 100%
app/api/steering.py 42 0 100%
app/api/suggestions.py 8 0 100%
app/api/sync.py 101 0 100%
app/config.py 140 0 100%
app/core/__init__.py 0 0 100%
app/core/auth.py 20 0 100%
app/core/caching.py 108 0 100%
app/core/debugging.py 29 2 93%
app/core/docs_push.py 39 0 100%
app/core/logging.py 13 0 100%
app/db.py 21 0 100%
app/main.py 52 0 100%
app/models.py 86 0 100%
app/rag/__init__.py 0 0 100%
app/rag/agent.py 222 0 100%
app/rag/archive_upload.py 128 0 100%
app/rag/chunker.py 206 4 98%
app/rag/git_sources.py 14 0 100%
app/rag/importer.py 180 3 98%
app/rag/llm.py 216 0 100%
app/rag/overview.py 71 0 100%
app/rag/prompts.py 88 0 100%
app/rag/retriever.py 150 3 98%
app/rag/scaffolding.py 55 0 100%
app/rag/source_removal.py 41 0 100%
app/rag/sources_meta.py 16 0 100%
app/rag/suggestions.py 27 0 100%
app/rag/summarizer.py 24 0 100%
app/schemas.py 210 0 100%
-----------------------------------------------
TOTAL 2989 12 99%
coverage gate: app/ 99% (>90%) OK
All checks passed!
0 errors, 0 warnings, 0 informations
validation OK