nwhitehouse stops Mike's chatbot from forgetting what it just read

A fix so the legal assistant remembers the documents it pulled instead of re-reading them from scratch every single turn.

chat-uiworkflow

Until now, Mike's chat had a goldfish problem. Each time you sent a follow-up question, the assistant lost all memory of the documents it had fetched earlier in the same conversation, so it dutifully went back and re-read everything from the start. The system even carried an internal note admitting as much.

nwhitehouse rewires how the conversation is stored so the assistant now keeps a running record of what it looked up and what it found, and replays that history on every turn. A stray side effect - blank message bubbles cluttering the chat window - was caught and fixed in the same pass. The change is built to slot in cleanly on top of an existing setup without disturbing past conversations.

So what Worth a look if you run a fork where the assistant feels forgetful or sluggish mid-conversation - this is a contained, low-risk fix for exactly that.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

3 commits from nwhitehouse/mike, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
24e7b703 [feat-017] Add Sprint 3 plan + scope feat-017 to Tier 1 in-chat memory Nick Whitehouse 2026-05-07 ↗ GitHub
commit body
Documents the seven-story Sprint 3 (harness hardening + memory) and scopes
feat-017 to in-chat tool result replay only. Project-memory (Tier 2) moved
to feat-018 per user direction "I just want chat to remember what we're
talking about through the whole conversation at a minimum."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
563ffde8 [feat-017] Persist tool calls + results so chat remembers across turns Nick Whitehouse 2026-05-07 ↗ GitHub
commit body
Tier 1 of the memory hierarchy: in-chat tool result replay. Eliminates the
"finch loses context" failure where every new turn re-read the same
documents because the system prompt at chatTools.ts:730 told the model it
could not retain doc content between turns.

Schema (migration 003_memory_persistence.sql):
- chat_messages.tool_call_id, tool_name (for role='tool' rows)
- chat_messages.assistant_text (raw model output incl. <tool_call> markup)
- chat_messages.assistant_tool_calls jsonb (structured tool_calls per turn)
- All columns nullable; existing rows remain valid.

Persistence + replay flow:
- runLLMStream now takes chatId. Inside the runTools callback it inserts
  one role='tool' row per tool result and aggregates the structured
  tool_calls onto a turn-level array returned to the route handler.
- Route handlers (chat.ts, projectChat.ts) persist assistant_text=fullText
  and assistant_tool_calls on the assistant row at end-of-turn (alongside
  the existing events[] which the frontend still uses for display).
- Both routes now load conversation history from chat_messages directly
  rather than trusting the frontend payload, so prior tool results +
  assistant tool_calls flow into buildMessages on every turn.
- buildMessages emits role='tool' rows with tool_call_id, and
  role='assistant' rows with their tool_calls + assistant_text - the
  canonical OpenAI {assistant.tool_calls} → {tool.tool_call_id} pairing.
- LlmMessage type extended to support role='tool', tool_call_id, and
  tool_calls. Olava adapter passes them through; claude.ts and gemini.ts
  drop them (those paths are dead per coerceToOlava).

Removed enrichWithPriorEvents - it was a partial workaround that summarised
prior tool *names* into the last assistant message. Full tool result replay
makes it obsolete.

System prompt updated at chatTools.ts:752 - the "you do NOT retain document
content" warning is replaced with positive guidance pointing the model at
the tool history below.

Frontend (ChatView.tsx) filters role='tool' rows from the rendered chat;
visible UI is unchanged. MikeMessage type updated to admit the new role.

Tier 2 (per-project persistent facts across chats) is deferred to feat-018
per scope decision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d55d7575 [feat-017] Hide tool rows in getChat so they don't render as empty bubbles Nick Whitehouse 2026-05-07 ↗ GitHub
commit body
Bug from the first pass: ChatView's role!=='tool' filter never fired
because mikeApi.getChat() coerced every non-user row to role='assistant'.
Result: each persisted tool result row became an empty assistant bubble on
chat reload, crowding out the real tool cards (which render from the
adjacent assistant row's events[] array).

Fix: getChat now preserves role='tool' on the round-trip; ChatView's
existing filter handles it from there. ServerMessage.role widened to
include 'tool'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-117.md from inside the repo you want the changes in.

⬇ Download capture-thread-117.md