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
- 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.
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
- 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.
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
- memory fact 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/4f3c8f21daf9dd42.
Report an issue: GitHub.