Hmbown/CodeWhale · warning · anyhow::Error

At least one thread field is required

Error message

At least one thread field is required

What it means

update_thread was called with an UpdateThreadRequest in which every optional field (archived, allow_shell, trust_mode, auto_approve, model, mode, permission_posture, title, system_prompt, workspace) is None. The API rejects no-op patches instead of performing a load-and-save cycle that would emit an empty change record.

Source

Thrown at crates/tui/src/runtime_threads.rs:4639

        self.flush_recovery_receipts_for_thread(id).await?;
        self.store
            .load_thread(id)
            .with_context(|| format!("Thread not found: {id}"))
    }

    pub async fn update_thread(&self, id: &str, req: UpdateThreadRequest) -> Result<ThreadRecord> {
        if req.archived.is_none()
            && req.allow_shell.is_none()
            && req.trust_mode.is_none()
            && req.auto_approve.is_none()
            && req.model.is_none()
            && req.mode.is_none()
            && req.permission_posture.is_none()
            && req.title.is_none()
            && req.system_prompt.is_none()
            && req.workspace.is_none()
        {
            bail!("At least one thread field is required");
        }

        if let Some(model) = req.model.as_ref()
            && model.trim().is_empty()
        {
            bail!("model must not be empty");
        }
        if let Some(mode) = req.mode.as_ref()
            && mode.trim().is_empty()
        {
            bail!("mode must not be empty");
        }
        if let Some(permission_posture) = req.permission_posture.as_ref()
            && permission_posture.trim().is_empty()
        {
            bail!("permission_posture must not be empty");
        }
        if let Some(workspace) = req.workspace.as_ref()

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Only send fields that actually changed - diff against the thread record client-side first
  2. Skip the update call entirely when the change set is empty
  3. If using PATCH-style JSON, ensure at least one key is present before sending
  4. Check the struct after deserialization and reject empty bodies at your own API boundary with a clearer message

Example fix

// before
let req = UpdateThreadRequest::default();
manager.update_thread(id, req).await?; // bails: no fields

// after
let mut req = UpdateThreadRequest::default();
req.title = Some("New title".to_string());
manager.update_thread(id, req).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Diff before patching: skip empty requests.
fn has_any_field(req: &UpdateThreadRequest) -> bool {
    req.archived.is_some() || req.allow_shell.is_some() || req.trust_mode.is_some()
        || req.auto_approve.is_some() || req.model.is_some() || req.mode.is_some()
        || req.permission_posture.is_some() || req.title.is_some()
        || req.system_prompt.is_some() || req.workspace.is_some()
}
if !has_any_field(&req) {
    return Ok(current_record); // no-op: nothing to update
}
manager.update_thread(id, req).await?;

Type guard

fn is_empty_update(req: &UpdateThreadRequest) -> bool {
    req.archived.is_none() && req.allow_shell.is_none() && req.trust_mode.is_none()
        && req.auto_approve.is_none() && req.model.is_none() && req.mode.is_none()
        && req.permission_posture.is_none() && req.title.is_none()
        && req.system_prompt.is_none() && req.workspace.is_none()
}

Prevention

When it happens

Trigger: Constructing UpdateThreadRequest::default() (or an all-None deserialization from an empty JSON PATCH body) and calling update_thread. Check at runtime_threads.rs:4630-4640.

Common situations: A client builds the request from a diff object that came up empty; serde deserializes {} into all-None; a UI 'save' button fires with no changed fields.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/871baa7f90f12691. Report an issue: GitHub.