zeroclaw-labs/zeroclaw · error

memory consolidation write denied by policy: {e}

Error message

memory consolidation write denied by policy: {e}

What it means

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.

Source

Thrown at crates/zeroclaw-memory/src/consolidation.rs:178

    {
        let mem_key = format!("core_{}", uuid::Uuid::new_v4());

        // Compute importance score heuristically.
        let imp = importance::compute_importance(update, &MemoryCategory::Core);

        // A: fail-closed policy write-gate on the autonomous consolidation path.
        let policy = PolicyEnforcer::new(&memory_config.policy);
        if let Err(e) =
            policy_gate::validate_store(memory, &policy, "default", &MemoryCategory::Core).await
        {
            ::zeroclaw_log::record!(
                WARN,
                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                    .with_attrs(::serde_json::json!({"error": e.to_string()})),
                "memory consolidation write denied by policy"
            );
            anyhow::bail!("memory consolidation write denied by policy: {e}");
        }

        // A: write-time near-duplicate detection.
        let candidates = memory.recall(update, 10, None, None, None).await?;
        let candidates = dedup::core_candidates(candidates);
        match dedup::dedup_gate(&candidates, update, memory_config) {
            DedupAction::Insert => {}
            DedupAction::Reject { dup_of } => {
                ::zeroclaw_log::record!(
                    DEBUG,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                        .with_attrs(::serde_json::json!({"duplicate_of": dup_of})),
                    "memory consolidation skipped duplicate core update"
                );
                return Ok(());
            }
            DedupAction::Merge { into } => {
                if let Some(survivor) = candidates.iter().find(|entry| entry.id == into) {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. If consolidation should write Core memories, remove "default" from memory.policy.read_only_namespaces in the runtime memory config that feeds MemoryConfig.
  2. 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.
  3. 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.
  4. 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.

Example fix

# before: policy protects the very namespace consolidation writes to
[policy]
read_only_namespaces = ["default"]

# after: keep protection only where nothing autonomous writes
[policy]
read_only_namespaces = ["archive"]
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight the exact gate consolidate_turn will run
use zeroclaw_memory::policy::PolicyEnforcer;
use zeroclaw_memory::policy_gate;

let policy = PolicyEnforcer::new(&memory_config.policy);
if let Err(violation) = policy_gate::validate_store(memory, &policy, "default", &MemoryCategory::Core).await {
    // fix config (read_only_namespaces / quotas) BEFORE running consolidation
    return Err(anyhow::anyhow!("consolidation would be denied: {violation}"));
}
consolidation::consolidate_turn(memory, memory_config, &result).await?;

Try / catch

// At the turn boundary: deny -> log and keep the conversation alive
if let Err(e) = consolidation::consolidate_turn(memory, cfg, &result).await {
    if e.to_string().contains("denied by policy") {
        tracing::warn!(error = %e, "consolidation blocked by policy; skipping memory write");
    } else {
        return Err(e);
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/4f3c8f21daf9dd42. Report an issue: GitHub.