[feat-017] Persist tool calls + results so chat remembers across turns

↗ view on GitHub · Nick Whitehouse · 2026-05-07 · 563ffde8

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>
Repository nwhitehouse/mike
Author Nick Whitehouse <nick.whitehouse@mccarthyfinch.com>
Authored
Parents 24e7b703
Stats 10 files changed , +429 , -158
Part of In-chat memory: persist tool calls + results across turns (feat-017)

Capture this commit into my fork

Download a Markdown prompt that tells Claude how to port this exact commit into your working tree. Run it via claude -p < capture-commit-563ffde8.md from inside the repo you want the change in.

⬇ Download capture-commit-563ffde8.md