Skip to content

skip tool + branch + depth limit corrupts conversation history (400: text content blocks must be non-empty) #201

@indexedfit

Description

@indexedfit

Summary

When the agent calls branch then skip and hits the conversation depth limit (3/5), the turn ends with no text output. This empty assistant message gets persisted to conversation history. Since the Anthropic API rejects empty text content blocks, every subsequent message fails permanently with a 400 error. The bot goes silent with no user-facing error and no self-recovery.

100% reproducible on a completely fresh install (empty data dir, fresh config).

Steps to reproduce

  1. Fresh install: ghcr.io/spacedriveapp/spacebot:latest (v0.1.15), Telegram channel, anthropic/claude-opus-4-6 for all routing
  2. Send any first message — bot replies successfully
  3. Send a message that triggers a branch for memory recall (e.g. "how many memories do you have?")
  4. Bot goes silent. All further messages fail.

Log trace

Message 1 — works fine:

09:55:53 handle_message message_id=28
09:55:57 reply tool called content_len=272

Message 2 — triggers the bug:

09:56:12 handle_message message_id=30
09:56:16 executed tool: branch (memory recall)     ← depth 2/5
09:56:20 executed tool: skip ("Waiting for branch") ← depth 3/5
09:56:22 Depth reached: 3/5                         ← turn ends, no text produced

Branch completes, retrigger fires — already broken:

09:56:28 branch.run Depth reached: 2/50             ← branch finishes
09:56:28 handle_message (retrigger)
09:56:29 ERROR: Anthropic API error (400): messages: text content blocks must be non-empty

All subsequent messages — permanently broken:

09:56:30 handle_message message_id=31
09:56:31 ERROR: Anthropic API error (400): messages: text content blocks must be non-empty

Root cause analysis

We traced the full code path through spacebot and rig-core 0.30.0:

Bug 1: Empty/whitespace text blocks poison conversation history

Path: rig-core/src/agent/prompt_request/mod.rs line ~436 → channel.rs:apply_history_after_turn

  1. LLM returns response with tool calls (branch + skip) at depth 2/5
  2. Tools execute, results pushed to history as User message
  3. Loop continues, depth becomes 3/5
  4. LLM returns a response with no tool calls — rig pushes Message::Assistant { content: [Text("")] } to history
  5. "Depth reached: 3/5" fires, rig returns MaxTurnsError
  6. apply_history_after_turn (channel.rs line ~2175) writes history back on MaxTurnsError without sanitizing
  7. History now contains an assistant message with empty text content block
  8. Anthropic provider serializes via TryFrom<message::AssistantContent> for Content (anthropic/completion.rs ~line 429) which passes empty string through with no filtering
  9. Next API call → Anthropic rejects: "text content blocks must be non-empty"

Note: Anthropic also rejects whitespace-only text (" ", "\n", etc.) with: "text content blocks must contain non-whitespace text". Both empty and whitespace-only must be filtered.

Fix location: apply_history_after_turn in src/agent/channel.rs — filter out AssistantContent::Text entries where text.trim().is_empty() before writing history back. Drop assistant messages that become entirely empty after filtering. This is ~15 lines of code.

Bug 2: Thinking-only API responses cause hard errors

Path: src/llm/model.rs:parse_anthropic_response (~line 1172)

When Anthropic returns a response containing only thinking blocks (no text, no tool calls), parse_anthropic_response discards all thinking blocks, leaving assistant_content empty. It then fails with CompletionError::ResponseError("empty response from Anthropic") and retries 3 times — all fail identically.

This happens after skip — the model has nothing to say, so it returns only a thinking block.

Fix location: parse_anthropic_response in src/llm/model.rs — when all content blocks are thinking/unknown (no text or tool calls), synthesize a valid placeholder response instead of erroring. The placeholder must then be stripped by the history sanitizer in Bug 1's fix to avoid poisoning.

Bug 3: Retrigger turns don't produce a reply

Even with bugs 1 and 2 fixed, the retrigger mechanism doesn't reliably deliver replies:

  1. Branch completes → results incorporated → debounced retrigger fires
  2. New turn starts at depth 1/5
  3. Model returns text without calling the reply tool → rig terminates the turn ("Depth reached: 1/5" on first depth)
  4. User never gets a response

The model doesn't understand it needs to call reply on retrigger turns. The text it returns goes nowhere — spacebot only sends messages to Telegram via the reply tool.

Fix: Either auto-send text returned on retrigger turns when no reply was called, or make the retrigger prompt explicitly instruct the model to call reply.

Expected behavior

  • Don't persist assistant messages with empty/whitespace-only text content to conversation history
  • Handle thinking-only API responses gracefully instead of erroring
  • Retrigger turns should reliably deliver a reply to the user

Environment

  • Version: 0.1.15
  • Image: ghcr.io/spacedriveapp/spacebot:latest
  • Platform: x86_64, Docker, Linux
  • LLM provider: Anthropic (claude-opus-4-6)
  • Channel: Telegram

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions