tinyhumansai/openhuman · error

{tool}: set_user_state failed: {e:#}

Error message

{tool}: set_user_state failed: {e:#}

What it means

Thrown from the shared set_pin helper used by both learning_pin_facet and learning_unpin_facet ({tool} interpolates to one of those names) when FacetCache::set_user_state() returns an Err. set_user_state is a single SQL UPDATE on user_profile; an Err means the UPDATE itself failed (locked DB, I/O), as opposed to returning Ok(false) which is the 'facet not found' case.

Source

Thrown at src/openhuman/agent/learning/tools.rs:273

        Ok(ToolResult::success(serde_json::to_string(&json!({
            "facet": facet_to_json(&facet),
        }))?))
    }
}

/// Set/clear a facet's pin via `set_user_state`. Shared by pin/unpin.
async fn set_pin(
    args: serde_json::Value,
    tool: &str,
    state: UserState,
) -> anyhow::Result<ToolResult> {
    let class_str = read_required_str(&args, "class")?;
    let key_suffix = read_required_str(&args, "key")?;
    let fk = full_key(&class_str, &key_suffix);
    let cache = get_cache()?;
    let updated = cache
        .set_user_state(&fk, state)
        .map_err(|e| anyhow::anyhow!("{tool}: set_user_state failed: {e:#}"))?;
    if !updated {
        return Err(anyhow::anyhow!("{tool}: facet not found: {fk}"));
    }
    let facet = cache
        .get(&fk)
        .map_err(|e| anyhow::anyhow!("{tool}: re-read failed: {e:#}"))?;
    Ok(ToolResult::success(serde_json::to_string(&json!({
        "facet": facet.as_ref().map(facet_to_json),
    }))?))
}

/// Pin a facet. Default-OFF.
pub struct LearningPinFacetTool;

#[async_trait]
impl Tool for LearningPinFacetTool {
    fn name(&self) -> &str {
        "learning_pin_facet"

View on GitHub (pinned to a221052e0d)

Solutions

  1. Retry the pin/unpin after the lock clears — the UPDATE is idempotent
  2. Confirm the key exists with learning_get_facet to rule out the not-found variant
  3. Check for concurrent learning_rebuild_cache / learning_reset_cache calls and serialize them
  4. Inspect the error chain for rusqlite specifics (busy vs corrupt vs disk I/O)
Defensive patterns

Strategy: retry

Try / catch

match tool_exec(&tool_name, args).await { // learning_pin_facet | learning_unpin_facet
    Err(e) if e.to_string().contains("set_user_state failed") => {
        tokio::time::sleep(Duration::from_millis(300)).await;
        tool_exec(&tool_name, args).await // UPDATE is idempotent
    }
    other => other,
}

Prevention

When it happens

Trigger: Invoking learning_pin_facet / learning_unpin_facet with {class, key} while the SQLite DB is locked by a rebuild cycle, reset, or reflection persist; storage I/O failure. Distinguished from a wrong key by the error chain carrying a store error rather than the not-found message.

Common situations: Agent pins a facet immediately after heavy transcript ingest triggered the stability cycle; workspace DB on a flaky disk; concurrent UI RPC calls into the learning namespace.

Related errors


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