zeroclaw-labs/zeroclaw · error

memory fact write denied by policy: {e}

Error message

memory fact write denied by policy: {e}

What it means

When memory_config.consolidation_extract_facts is enabled, consolidate_turn calls store_typed_facts, which writes each extracted atomic fact as an individual Core memory. Every fact write passes the same fail-closed policy gate as the primary update (policy_gate::validate_store on namespace "default", category Core), and any PolicyViolation aborts the whole consolidation with this message. The gate fails on read_only_namespaces containing "default", on max_entries_per_namespace, or on max_entries_per_category being reached (and it fails closed to 'quota exceeded' if count_in_scope errors).

Source

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

                fact_overlaps_primary_update(trimmed, primary, memory_config)
            });
            (!trimmed.is_empty() && !overlaps_primary).then_some(trimmed)
        })
        .take(MAX_TYPED_FACTS_PER_TURN)
    {
        // Same fail-closed policy write-gate as the primary core update; each
        // fact is an autonomous Core write.
        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 fact write denied by policy"
            );
            anyhow::bail!("memory fact write denied by policy: {e}");
        }

        let mem_key = format!("core_fact_{}", uuid::Uuid::new_v4());
        let imp = importance::compute_importance(fact, &MemoryCategory::Core);

        let candidates = memory.recall(fact, 10, None, None, None).await?;
        let candidates = dedup::core_candidates(candidates);
        match dedup::dedup_gate(&candidates, fact, 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 fact skipped as duplicate"
                );
                continue;
            }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Remove "default" from memory.policy.read_only_namespaces if autonomous Core fact writes are acceptable.
  2. Raise max_entries_per_namespace / max_entries_per_category or prune old Core entries so facts fit under quota.
  3. If you do not actually need atomic fact extraction, set consolidation_extract_facts = false — store_typed_facts (and this gate) is then skipped entirely.
  4. If facts are optional for you, catch this error at the consolidation boundary and log-and-continue the turn instead of failing the whole consolidation.

Example fix

# before: facts enabled but namespace protected
consolidation_extract_facts = true
[policy]
read_only_namespaces = ["default"]

# after: pick one coherent stance
consolidation_extract_facts = false   # or drop "default" from read_only_namespaces
Defensive patterns

Strategy: validation

Validate before calling

// Only enable fact extraction when the policy actually allows Core writes
let policy = PolicyEnforcer::new(&memory_config.policy);
let facts_allowed = policy_gate::validate_store(memory, &policy, "default", &MemoryCategory::Core).await.is_ok();
let mut cfg = memory_config.clone();
if !facts_allowed {
    cfg.consolidation_extract_facts = false; // skip store_typed_facts instead of failing the turn
}

Try / catch

// Facts are additive: isolate their failure from the primary update
if let Err(e) = consolidation::consolidate_turn(memory, cfg, &result).await {
    if e.to_string().starts_with("memory fact write denied by policy") {
        tracing::warn!(error = %e, "typed facts skipped: policy denies Core writes");
    } else {
        return Err(e);
    }
}

Prevention

When it happens

Trigger: Turning on consolidation_extract_facts while policy.read_only_namespaces includes "default", or letting namespace/category entry counts reach the configured max_entries_per_namespace / max_entries_per_category before a turn with extracted facts is consolidated. Called from consolidate_turn, so it surfaces through run_consolidation/run just like the primary-write denial.

Common situations: Enabling fact extraction experimentally on a config that was hardened read-only for safety; long-lived installs whose Core category fills to its quota so every subsequent turn fails consolidation; treating fact extraction as free and enabling it on agents whose memory namespace is protected.

Related errors


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