nwhitehouse teaches Mike to remember the conversation

The chatbot used to re-read your documents from scratch on every reply. Not anymore.

chat-uiinfrastructure

If you uploaded a contract and then asked three follow-up questions, the old behaviour was almost comical: the assistant would silently re-open the same document each time, because it had been explicitly told it couldn't hold onto anything between turns. nwhitehouse has now wired the chat history to persist not just what you and the assistant said, but every tool result the assistant pulled in along the way - so the second question genuinely builds on the first.

This is the first step of a planned memory stack. For now it's scoped tightly to a single conversation; remembering facts about you or your matter across separate chats is deliberately punted to a later piece of work. There was also a visible bug on the way in - every recalled tool result was rendering as an empty grey bubble - that's been cleaned up in the same pass.

So what If you've ever watched a legal AI chatbot 'forget' the document it's literally discussing, this is the unglamorous plumbing that fixes it.

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