{"record":{"id":"14f6789e1c12a2fd","repo":"tinyhumansai/openhuman","slug":"learning-update-facet-e","errorCode":null,"errorMessage":"learning_update_facet: {e:#}","messagePattern":"learning_update_facet: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/openhuman/agent/learning/tools.rs","lineNumber":248,"sourceCode":"            },\n            \"required\": [\"class\", \"key\", \"value\"]\n        })\n    }\n\n    fn permission_level(&self) -> PermissionLevel {\n        PermissionLevel::Write\n    }\n\n    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {\n        log::debug!(\"[tool][learning] update_facet invoked\");\n        let class_str = read_required_str(&args, \"class\")?;\n        let key_suffix = read_required_str(&args, \"key\")?;\n        let value = read_required_str(&args, \"value\")?;\n        let fk = full_key(&class_str, &key_suffix);\n        let cache = get_cache()?;\n        let mut facet = cache\n            .get(&fk)\n            .map_err(|e| anyhow::anyhow!(\"learning_update_facet: {e:#}\"))?\n            .ok_or_else(|| anyhow::anyhow!(\"learning_update_facet: facet not found: {fk}\"))?;\n        facet.value = value;\n        facet.user_state = UserState::Pinned;\n        cache\n            .upsert(&facet)\n            .map_err(|e| anyhow::anyhow!(\"learning_update_facet: upsert failed: {e:#}\"))?;\n        Ok(ToolResult::success(serde_json::to_string(&json!({\n            \"facet\": facet_to_json(&facet),\n        }))?))\n    }\n}\n\n/// Set/clear a facet's pin via `set_user_state`. Shared by pin/unpin.\nasync fn set_pin(\n    args: serde_json::Value,\n    tool: &str,\n    state: UserState,\n) -> anyhow::Result<ToolResult> {","sourceCodeStart":230,"sourceCodeEnd":266,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/src/openhuman/agent/learning/tools.rs#L230-L266","documentation":"Thrown by the learning_update_facet agent tool when FacetCache::get() fails while looking up the facet by full key 'class/key'. FacetCache is a thin wrapper over ProfileStore, which owns the SQL against the user_profile table in the memory store's SQLite database, so this is a storage-layer failure (lock contention, I/O error, corrupted DB) — not a 'wrong key' condition (that is a separate 'facet not found' error). The {e:#} formatting prints the full anyhow error chain, e.g. the underlying rusqlite error.","triggerScenarios":"Calling learning_update_facet {class, key, value} while another writer holds the SQLite write lock — the stability-detector rebuild cycle (learning_rebuild_cache), reflection persistence, or a concurrent RPC mutation from learning::schemas handlers. Also fires on disk-full, the workspace DB being deleted/moved under a running core, or a corrupted user_profile table.","commonSituations":"Agent pinning/updating a facet right after a transcript-ingest or scheduled stability rebuild started; two tool calls racing (tool_tracker allows parallel tool execution); workspace on a network/full disk; DB left locked after a crash.","solutions":["Retry the tool call after the concurrent rebuild/reflection cycle finishes — SQLITE_BUSY is transient","Reproduce the read failure with learning_get_facet (same class/key) to confirm it is the store, not the key","Check disk space and that the workspace_dir memory DB file is present and writable","If persistent, inspect the core log for the underlying rusqlite error chain and run the memory doctor / restart the core to reopen the DB"],"exampleFix":"// before: update raced a rebuild cycle\ntool: learning_update_facet {\"class\":\"style\",\"key\":\"verbosity\",\"value\":\"concise\"} // -> learning_update_facet: database is locked\n// after: wait for rebuild, then retry\ntool: learning_get_facet {\"class\":\"style\",\"key\":\"verbosity\"}   // confirm readable\ntool: learning_update_facet {\"class\":\"style\",\"key\":\"verbosity\",\"value\":\"concise\"} // ok","handlingStrategy":"retry","validationCode":"// Cheap read-only probe of the same table before mutating:\n// via RPC: learning_get_facet {\"class\":c,\"key\":k} -> if it errors, the store is unhealthy;\n// if it returns null, fix the key instead of retrying.\n// In-process equivalent:\nlet cache = FacetCache::new(memory::global::client_if_ready().unwrap().profile_store());\nassert!(cache.get(&format!(\"{class}/{key}\")).is_ok(), \"store unhealthy — defer update\");","typeGuard":null,"tryCatchPattern":"match tool_exec(\"learning_update_facet\", args).await {\n    Ok(res) => res,\n    Err(e) if e.chain().any(|c| c.to_string().contains(\"database is locked\")) => {\n        tokio::time::sleep(Duration::from_millis(250)).await;\n        tool_exec(\"learning_update_facet\", args).await? // single bounded retry\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Serialize learning mutator tools (update/pin/forget/rebuild/reset) within one agent turn instead of issuing them in parallel","Keep one core process per workspace_dir to avoid cross-process SQLite contention","Monitor for 'database is locked' in core logs as an early signal of write contention"],"tags":["rust","sqlite","learning","agent-tools","concurrency"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}