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
- Remove "default" from memory.policy.read_only_namespaces if autonomous Core fact writes are acceptable.
- Raise max_entries_per_namespace / max_entries_per_category or prune old Core entries so facts fit under quota.
- If you do not actually need atomic fact extraction, set consolidation_extract_facts = false — store_typed_facts (and this gate) is then skipped entirely.
- 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
- Add a config-lint rule: consolidation_extract_facts = true requires "default" not in read_only_namespaces and quota headroom in the Core category.
- Treat fact extraction as optional telemetry — decide up front whether its failure should fail the turn, and encode that in the catch.
- Watch Core category counts when facts are on: each turn can add up to MAX_TYPED_FACTS_PER_TURN entries.
- Cover this gate in tests by setting read_only_namespaces = ["default"] like the repo's own test does.
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
- memory consolidation write denied by policy: {e}
- AgentScopedMemory refuses purge_namespace: cross-agent bulk
- Embedding API error {status}: {text}
- Unable to allocate non-conflicting key for '{base}'
- QQ channel requires the `channel-qq` feature
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/eb847e6c336cfbf5.
Report an issue: GitHub.