fix(chat): skip empty-content turns so no empty text block reaches the model

🟢 open · #404 · open-legal-products/mike ← Marioario/mike · opened 5d ago by Marioario · +27 across 2 files · ↗ on GitHub

From the PR description

Summary

buildMessages forwards every stored turn to the model, including turns whose content is empty. An empty string becomes an empty text content block, and Anthropic rejects the entire request:

400 invalid_request_error
messages: text content blocks must be non-empty

The turn fails before any tokens are generated, and the user sees "Sorry, something went wrong."

What changed

backend/src/lib/chat/contextBuilders.ts - skip turns whose content is empty or whitespace-only instead of pushing an empty text block:

if (typeof content === "string" && !content.trim()) continue;

Plus a regression test in backend/src/lib/__tests__/documentContext.test.ts, added to the existing null-content assistant reservations block since that is where the empty-content case is already exercised.

Why it changed

An assistant turn stores empty content when it opens directly with a tool call and no preamble text. That makes the failure intermittent and confusing: the first turn of a chat always works, and whether a later turn breaks depends on whether the model happened to narrate before calling a tool.

Observed on a self-hosted instance against claude-opus-5: a turn that began "I'll read the NDA now." replayed fine, while the next one, which went straight to the document lookup, poisoned every subsequent turn in that chat. Two of five follow-up turns failed this way.

Backend logs for the failing request:

[chat/stream] starting LLM stream { apiMessageCount: 6, docCount: 1, workflowCount: 0 }
APICallError [AI_APICallError]: messages: text content blocks must be non-empty
[chat/stream] model stream failed Error: messages: text content blocks must be non-empty
[chat/stream] error: AssistantStreamError: The response could not be completed. Please try again.

This is distinct from #379, which handles the opposite direction (a provider returning an empty completion). Here the empty content is outbound, in the request we construct.

Testing performed

  • Deterministic repro, before and after: POST /chat with a history containing an { role: "assistant", content: "" } message. Fails with the 400 above on main; returns a normal streamed response with this change.
  • New regression test fails on main (expected [ ...(4) ] to have a length of 2 but got 4) and passes with the fix.
  • npm test --prefix backend: 68 files passed, 4 skipped, 840 tests passed, 0 failures.
  • Multi-turn context is preserved - a follow-up still resolves references to earlier turns.

Formatting note: both touched files already differ from a default Prettier run on main, so I left their existing style alone rather than reformatting and inflating the diff.

🤖 Generated with Claude Code

https://claude.ai/code/session_016SMoPAqimiGANmUQjd7ihr

Our analysis

Skip empty turns in chat context — read the full analysis →

Think the analysis missed something the PR description covers?

Capture this PR into my fork

Download a Markdown prompt that tells Claude how to port every commit in this PR into your working tree. Run it via claude -p < capture-pull-404.md from inside the repo you want the changes in.

⬇ Download capture-pull-404.md