tinyhumansai/openhuman · warning

missing `cards` for op=replace

Error message

missing `cards` for op=replace

What it means

The `todo` tool dispatched op=replace but args has no `cards` key (todo.rs:156). replace swaps the entire board for the provided card list, so cards is mandatory for that op — clearing the board is a separate op=clear, not an empty replace.

Source

Thrown at src/openhuman/agent/tools/todo.rs:156

                let id = required_string(&args, "id")?;
                let mut patch = patch_from_args(&args)?;
                patch.content = optional_string(&args, "content");
                ops::edit(&location, &id, patch).await
            }
            "update_status" => {
                let id = required_string(&args, "id")?;
                let status = required_string(&args, "status")?;
                let status = ops::parse_status(&status).map_err(anyhow::Error::msg)?;
                ops::update_status(&location, &id, status).await
            }
            "remove" => {
                let id = required_string(&args, "id")?;
                ops::remove(&location, &id).await
            }
            "replace" => {
                let cards = args
                    .get("cards")
                    .ok_or_else(|| anyhow::anyhow!("missing `cards` for op=replace"))?;
                let cards: Vec<TaskBoardCard> = serde_json::from_value(cards.clone())
                    .map_err(|e| anyhow::anyhow!("invalid `cards`: {e}"))?;
                ops::replace(&location, cards).await
            }
            "clear" => ops::clear(&location).await,
            "list" => ops::list(&location).await,
            other => {
                return Ok(ToolResult::error(format!(
                "unknown op '{other}' (expected add|edit|update_status|remove|replace|clear|list)"
            )))
            }
        };

        match result {
            Ok(snap) => {
                let payload = json!({
                    "threadId": snap.thread_id,
                    "cards": snap.cards,

View on GitHub (pinned to a221052e0d)

Solutions

  1. Include "cards" as a JSON array of card objects for op=replace
  2. Use op=clear when the intent is an empty board
  3. Validate that op=replace calls carry an array cards field before dispatch

Example fix

// before
{ "op": "replace" }

// after
{ "op": "replace", "cards": [{ "id": "t1", "content": "Ship the fix" }] }
Defensive patterns

Strategy: validation

Validate before calling

const a = args as Record<string, unknown>;
if (a?.op === "replace" && !Array.isArray(a.cards)) {
  throw new Error("todo replace: 'cards' array is required (use op=clear to empty the board)");
}

Type guard

function isReplaceArgs(a: unknown): a is { op: "replace"; cards: unknown[] } {
  const v = a as Record<string, unknown>;
  return v?.op === "replace" && Array.isArray(v.cards);
}

Try / catch

if e.to_string().contains("missing `cards` for op=replace") {
    return Ok(ToolResult::error("replace needs the full 'cards' array; use op=clear for an empty board"));
}

Prevention

When it happens

Trigger: Model sends { "op": "replace" } with no cards; wrapper mapping a board update to replace without including the new card array; intending to clear but choosing the wrong op.

Common situations: Board-sync logic that falls back to replace without a payload; models conflating replace and clear.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/9ad79b00ba779ccc. Report an issue: GitHub.