{"record":{"id":"4f3c8f21daf9dd42","repo":"zeroclaw-labs/zeroclaw","slug":"memory-consolidation-write-denied-by-policy-e","errorCode":null,"errorMessage":"memory consolidation write denied by policy: {e}","messagePattern":"memory consolidation write denied by policy: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-memory/src/consolidation.rs","lineNumber":178,"sourceCode":"    {\n        let mem_key = format!(\"core_{}\", uuid::Uuid::new_v4());\n\n        // Compute importance score heuristically.\n        let imp = importance::compute_importance(update, &MemoryCategory::Core);\n\n        // A: fail-closed policy write-gate on the autonomous consolidation path.\n        let policy = PolicyEnforcer::new(&memory_config.policy);\n        if let Err(e) =\n            policy_gate::validate_store(memory, &policy, \"default\", &MemoryCategory::Core).await\n        {\n            ::zeroclaw_log::record!(\n                WARN,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                    .with_attrs(::serde_json::json!({\"error\": e.to_string()})),\n                \"memory consolidation write denied by policy\"\n            );\n            anyhow::bail!(\"memory consolidation write denied by policy: {e}\");\n        }\n\n        // A: write-time near-duplicate detection.\n        let candidates = memory.recall(update, 10, None, None, None).await?;\n        let candidates = dedup::core_candidates(candidates);\n        match dedup::dedup_gate(&candidates, update, memory_config) {\n            DedupAction::Insert => {}\n            DedupAction::Reject { dup_of } => {\n                ::zeroclaw_log::record!(\n                    DEBUG,\n                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                        .with_attrs(::serde_json::json!({\"duplicate_of\": dup_of})),\n                    \"memory consolidation skipped duplicate core update\"\n                );\n                return Ok(());\n            }\n            DedupAction::Merge { into } => {\n                if let Some(survivor) = candidates.iter().find(|entry| entry.id == into) {","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-memory/src/consolidation.rs#L160-L196","documentation":"During consolidate_turn, before the LLM-extracted memory update is written to the Core category, a fail-closed policy gate (policy_gate::validate_store with PolicyEnforcer built from memory_config.policy) must pass for namespace \"default\". The gate fails on three conditions: \"default\" is listed in policy.read_only_namespaces (ReadOnlyNamespace), the namespace row count reached max_entries_per_namespace (NamespaceQuotaExceeded), or the Core category count reached max_entries_per_category (CategoryQuotaExceeded). The underlying PolicyViolation text is appended to the bail message, so {e} tells you which rule fired.","triggerScenarios":"Running consolidate_turn (directly or via run_consolidation/run) with a non-empty result.memory_update while memory config has policy.read_only_namespaces = [\"default\"], or while count_in_scope reports counts at/above max_entries_per_namespace or max_entries_per_category. Subtly, the gate also fails closed when memory.count_in_scope itself errors (it is unwrap_or(usize::MAX)), so a broken counting path masquerades as a quota violation.","commonSituations":"A config hardens \"default\" as read-only to protect production memories but consolidation still runs on every turn; low quota values hit after long-running installs accumulate Core entries; a backend downgrade or permission error breaks count_in_scope and every consolidation turn then fails; tests that deliberately set read_only_namespaces to exercise the gate.","solutions":["If consolidation should write Core memories, remove \"default\" from memory.policy.read_only_namespaces in the runtime memory config that feeds MemoryConfig.","If a quota fired ({e} says quota exceeded), raise max_entries_per_namespace / max_entries_per_category or prune old entries (memory hygiene / retention), then retry the turn.","If the namespace is intentionally read-only, stop running consolidation against it — disable the consolidation pipeline or point it at a writable namespace so the settings agree.","If {e} claims a quota but counts look small, suspect count_in_scope failing on the backend (the gate treats an errored count as usize::MAX) and fix that backend/counting error first."],"exampleFix":"# before: policy protects the very namespace consolidation writes to\n[policy]\nread_only_namespaces = [\"default\"]\n\n# after: keep protection only where nothing autonomous writes\n[policy]\nread_only_namespaces = [\"archive\"]","handlingStrategy":"validation","validationCode":"// Pre-flight the exact gate consolidate_turn will run\nuse zeroclaw_memory::policy::PolicyEnforcer;\nuse zeroclaw_memory::policy_gate;\n\nlet policy = PolicyEnforcer::new(&memory_config.policy);\nif let Err(violation) = policy_gate::validate_store(memory, &policy, \"default\", &MemoryCategory::Core).await {\n    // fix config (read_only_namespaces / quotas) BEFORE running consolidation\n    return Err(anyhow::anyhow!(\"consolidation would be denied: {violation}\"));\n}\nconsolidation::consolidate_turn(memory, memory_config, &result).await?;","typeGuard":null,"tryCatchPattern":"// At the turn boundary: deny -> log and keep the conversation alive\nif let Err(e) = consolidation::consolidate_turn(memory, cfg, &result).await {\n    if e.to_string().contains(\"denied by policy\") {\n        tracing::warn!(error = %e, \"consolidation blocked by policy; skipping memory write\");\n    } else {\n        return Err(e);\n    }\n}","preventionTips":["Keep policy.read_only_namespaces and the consolidation pipeline mutually exclusive in config — add a startup check that fails fast on the conflict.","Set max_entries_per_namespace / max_entries_per_category with headroom above steady-state Core growth, and monitor counts via count_in_scope.","Remember count_in_scope errors read as 'quota exceeded' (fail-closed) — alert on backend count failures, not just the denial message.","Re-run the pre-flight gate after any memory policy config change, in config tests."],"tags":["rust","zeroclaw","memory","consolidation","policy","read-only","quota"],"backgroundTag":"policy-write-denied","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}