zeroclaw-labs/zeroclaw · error · anyhow::Error

refusing to dispatch to provider: prepared history has no us

Error message

refusing to dispatch to provider: prepared history has no user turn (system-only after leading-turn-order sanitize)

What it means

prepare_messages_for_iteration enforces the universal leading-turn-order invariant before any provider dispatch: strict providers reject a first non-system turn that is not user, so leading assistant/tool-call turns are sanitized away. If after sanitization no user message remains at all (system-only history), the runtime fails closed — it refuses the provider call rather than sending a request the provider will reject opaquely.

Source

Thrown at crates/zeroclaw-runtime/src/agent/turn/vision_route.rs:130

        None
    };

    Ok((vision_model_provider, degrade_strip_images))
}

pub(crate) async fn prepare_messages_for_iteration(
    history: &[ChatMessage],
    multimodal_config: &MultimodalConfig,
    degrade_strip_images: bool,
    image_cache: Option<&mut multimodal::LocalImageCache>,
) -> Result<multimodal::PreparedMessages> {
    // Enforce the universal leading-turn-order invariant before any provider
    // sees the history: strict providers reject a first non-system turn that is
    // not `user`, which context trims and session restores can produce.
    let mut sanitized = history.to_vec();
    ChatMessage::sanitize_leading_turn_order(&mut sanitized);
    if !sanitized.iter().any(ChatMessage::is_user) {
        anyhow::bail!(
            "refusing to dispatch to provider: prepared history has no user turn \
             (system-only after leading-turn-order sanitize)"
        );
    }
    let history = sanitized.as_slice();
    if degrade_strip_images {
        // Text-only fallback: replace every media marker with a
        // `[media attachment]` placeholder so no filesystem path or data
        // URI reaches the text-only provider, while surrounding text
        // (captions, tool metadata) survives.
        let stripped: Vec<ChatMessage> = history
            .iter()
            .map(|m| ChatMessage {
                role: m.role.clone(),
                content: multimodal::strip_media_markers(&m.content),
            })
            .collect();
        match image_cache {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Replay the prepared history and confirm at least one ChatMessage::is_user survives trimming
  2. Adjust the trim policy to always retain the original (or a synthetic) user turn
  3. When restoring sessions, re-seed a user message summarizing the request before resuming the loop
  4. Fix upstream builders that construct system-only histories

Example fix

// before: trim policy can drop the only user turn
history.retain(|m| !m.is_old(cutoff));

// after: always keep the initial user turn
history.retain(|m| !m.is_old(cutoff) || m.is_initial_user_turn());
Defensive patterns

Strategy: validation

Validate before calling

let mut history = build_history(session);
if !history.iter().any(ChatMessage::is_user) {
    history.push(ChatMessage::user(session.request_summary())); // re-seed a user turn
}
let resp = agent.run_tool_call_loop(history, ...).await?;

Type guard

fn history_has_user_turn(history: &[ChatMessage]) -> bool {
    history.iter().any(ChatMessage::is_user)
}

Try / catch

match agent.run_turn(req).await {
    Err(ref e) if e.to_string().contains("no user turn") => {
        // history is structurally invalid: re-seed a user turn and rebuild the session
    }
    other => other,
}

Prevention

When it happens

Trigger: Context trimming drops the last remaining user turn; a restored session contains only system + assistant/tool messages; histories built upstream without any user role. Tests prepare_fails_closed_when_no_user_turn_survives and prepare_strips_leading_assistant_tool_call pin exactly these shapes.

Common situations: Aggressive context-window trimming on long sessions; hand-built or migrated histories missing user turns; session restore after crashes; summarization pipelines that replace the user turn with a system summary and trim the original.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/d0c38cb985115370. Report an issue: GitHub.