MemClaw · Long-Running Fleets

Stale Memory Is a Silent Failure Mode in Long-Running Agent Fleets.
Here’s How We Fixed It.

Eight days of a confirmed price, one change on Day 9, and a synthesis agent that would report the old number with total confidence — no error, no flag. The fix isn’t a smarter prompt. It’s a governed memory layer that retires the stale facts before the next recall runs.

Caura.AI · 10 min read

TL;DR

  • Stale memory is a silentfailure: when a fact changes, the old and new claims can be recalled side by side with no signal distinguishing current from superseded, and the synthesis agent produces a confidently wrong output — no exception, no flag.
  • Status-governed recall is the fix, not a prompt instruction. Because MemClaw’s status filter is an exact match, status:"active" excludes confirmedmemories too — it is not shorthand for “current.” The governed query is the default recall with no status filter.
  • An async contradiction detector resolves the Day-9 chain: it fires after the write commits, compares the new $349 claim via the single-value (RDF) path, and transitions all eight $299 memories to outdated. (The crystallizer is a separate dedup sweep — it never sets outdated.)
  • Separating write, verify, and synthesize into distinct agents with scoped recalls (filter_agent_id) is what makes contradiction detection robust. Self-verification in one context can’t catch pool-level contradictions.
  • Synthesis reports the suppression countalongside its output — turning a silent storage-layer operation into a visible audit signal.

A close cousin of this piece, told as a narrative rather than a walkthrough, is The Price Changed. Your Agents Didn’t Notice. This one goes to the storage layer.

What “stale memory” actually means

Memory is what separates an agent fleet from a stateless chatbot. When agents write what they learn and share it across sessions and teammates, knowledge compounds — a sourcing agent’s Monday finding becomes the foundation for a synthesis agent’s Thursday brief. The failure mode that comes with this is not amnesia. It is the absence of a mechanism to mark a fact as superseded when it changes.

When a fleet runs for days or weeks, the same fact gets written and reconfirmed daily. None of that repetition changes how retrieval ranks a query — memclaw_recallscores by semantic similarity (plus keyword and graph relevance), not by how many times a fact has been confirmed. There is no “confirmation weight” in the ranker. What repetition changes is what’s sitting in the pool waiting to be recalled together. By the time the fact changes, there are eight memories asserting the old value and one asserting the new one. Without a mechanism that marks the old entries as superseded before the next recall, all nine return in the same result set, with no structural signal indicating which is current. The system logs no exception. The agent reports no conflict. The output is confidently wrong.

The simulation: three agents, one fleet, 14 days

The memclaw-long-run-fleet repo models a competitive-intelligence team tracking a competitor’s pricing over 14 simulated days. Three agents share one MemClaw fleet, each with a single bounded role:

AgentRoleWhat it does each day
sourcing-agentWriterReads the pricing page, writes raw facts to shared memory
verification-agentVerifierIndependently confirms sourcing's facts, transitions them to confirmed
synthesis-agentConsumerReads only governed memory, produces the daily brief
Architecture diagram: a Sourcing, Verification, and Synthesis agent, each loading the memclaw-governance skill, connect to the MemClaw MCP server over one fleet-longrun-research pool; Days 1–8 hold eight $299 memories, Day 9 drift writes $349 and auto-marks the old ones outdated, and the Day 10 recall surfaces only $349; a pgvector store with an 8-status lifecycle sits underneath.
Fig. 1— The three-agent fleet over one shared pool. Day-9 drift auto-marks the eight $299 memories outdated; the Day-10 recall surfaces only $349.

Orchestration lives in simulate.py. Each day, Sourcing and Verification run in parallel threads; Synthesis runs after both complete. On Day 9 specifically, the script inserts one extra step between Verification and Synthesis: it waits for MemClaw’s async contradiction detector to finish resolving the Day-9 write before letting Synthesis read the pool. That wait is the fix, and the rest of this post is about what it does and why it’s necessary.

The role separation is foundational. Verification reads only what Sourcing produced, using filter_agent_id: "sourcing-agent"on its recall, so it compares against a known source rather than the full pool. Synthesis never writes pricing facts and never reads unverified output on its own initiative — it recalls the pool without a status filter, so what comes back is whatever MemClaw’s own lifecycle has left live, with anything already transitioned to outdated or conflicted excluded before a single token reaches the agent.

Days 1–8: confidence builds around one fact

For the first eight days, the competitor’s Pro plan costs $299/month. Each day follows the same lifecycle: Sourcing memclaw_writes the observed price (lands active); Verification confirms it and transitions it to confirmed via memclaw_manage (op:"transition"); Synthesis recalls with no status filter and reads back the live pool. By Day 8 the pool holds eight confirmed memories, all asserting the same fact:

DAILY INTELLIGENCE BRIEF — Day 8
Generated by: synthesis-agent
Memory pool: fleet-longrun-research

## Competitor Pricing (Governed Recall)
Current price: $299/month (source memory IDs: [list])
Status of recalled memories: confirmed
Suppressed memories: 0 outdated memories

None of this changes how the next recall ranks a query. It just means that by Day 9 there are eight memories that agree with each other and nothing yet to tell a future recall that any of them are stale. That absence of a “this is superseded” signal is what makes the next section a problem.

Day 9: the price changes and the pool breaks

On Day 9 the Pro plan moves to $349/month. Sourcing does exactly what it should: memclaw_writewith the new price. The write path returns immediately — the new $349 entry commits active with no supersedes_id yet, because contradiction detection runs after the commit, asynchronously, to keep the write fast. For a brief window the pool holds:

  • 8 confirmed memories: “$299/month” (Days 1–8)
  • 1 active memory: “$349/month” (Day 9), not yet linked to anything

If Synthesis ran in that window, it would recall all nine. There is no confirmation-weight scoring pulling the eight $299 memories to the top — recall ranks by similarity, and all nine are about the same subject, so all nine come back. The risk isn’t that retrieval favors the majority; it’s that the agent has no structural signalfor which of nine near-identical claims is current — and with eight in agreement, an ungoverned synthesis is far more likely to default to the old number. The pipeline logs no exception; the brief arrives with the same confidence as every correct one before it. That is context-rot: the signal that something is wrong is absent, not subtle.

Why prompt-level instructions can’t solve this

The instinct is to tell Synthesis: “if you see conflicting memories, prefer the most recent.” It fails for three reasons. Context contamination:once stale data enters the context window through retrieval, it influences the output regardless of the instruction — the agent is reading and weighing all nine. No recency signal: recall ranks by similarity, not timestamp or confirmation count, so the instruction has nothing reliable to act on. Probabilistic, not deterministic: across long contexts, under pressure, or across sessions, instruction-following drifts. Enforcing correct recall requires a mechanism that runs before the agent sees any data— which puts the fix at the storage layer. (The same lesson, applied to access control, drives fleet-partitioned memory.)

The fix: status as a storage-layer query predicate

MemClaw’s memory lifecycle has eight states. Three govern the Day-9 scenario — and the key subtlety is that status on memclaw_recall is an exact-match filter, not an “or.” Passing status:"active" excludes confirmedmemories just as surely as it excludes outdated ones — so a synthesis agent that filtered on status:"active" would get zero results by Day 8, because every $299 memory is already confirmed.

StatusMeaningReturned by status:"active"?Returned by default recall?
activeWritten, not yet verifiedYesYes
confirmedIndependently verified as accurateNoYes
outdatedSuperseded via the single-value contradiction pathNoNo

The governed recall for a “what’s current” query is the default recall with no status filter at all. A default recall excludes whatever MemClaw has retired — anything marked outdated or conflicted (and deleted rows) is filtered out at the query layer before it reaches the agent’s context. That exclusion is a predicate, not something the agent reasons about: once transitioned, the eight $299 entries simply do not exist in the result set. A prompt instructs the agent to make a good decision using all the data in front of it; a status-governed default recall changes what data the agent is allowed to see in the first place. For a problem rooted in an unmarked-supersession pool, only the second approach addresses the failure at its source.

How the contradiction detector resolves the Day-9 chain

MemClaw detects the contradiction the moment Sourcing writes $349, but resolution isn’t part of the write’s request/response cycle. Contradiction detection, entity extraction, and re-embedding all run as fire-and-forget background work scheduled after the write commits. When that pass runs, it compares the new $349 memory against the pool through the single-value (RDF) contradiction path— the same subject, the same predicate (price), a new object value — flips each superseded row’s status to outdated, and sets the new memory’s supersedes_id to point back at the row it replaces.

This is a different mechanism from the crystallizer (POST /crystallize), a separate maintenance job that clusters near-duplicate memories and archives the sources under one new confirmed canonical. It has nothing to do with resolving a factual contradiction, and it never sets a memory to outdated. Confusing the two is easy — both get described as “cleaning up the pool” — but they solve different problems and leave different status trails.

Because detection is asynchronous, there’s a real race on Day 9. If Synthesis’s recall runs before the background pass completes, the pool still shows all nine as live. The sequencing in simulate.py closes that window: after Sourcing and Verification finish, wait_for_contradiction_detection() polls the write’s own memory ID until detection reports complete, and only then does Synthesis run.

# Day 9 only — before Synthesis reads, wait for the async
# contradiction detector to finish resolving the new $349 write.
# simulate.py polls up to ~60s (12 × 5s):

GET {MEMCLAW_API_URL}/memories/{memory_id}/contradictions
#  → {"detection_status": "completed", "contradictions": [ ... ]}

# MEMCLAW_API_URL defaults to https://memclaw.net/api/v1
# (or http://localhost:8000/api/v1 self-hosted)

This poll isn’t just defensive programming. It is the mechanism that makes the status-governed recall actually work. Without waiting for detection to complete, the eight $299 memories are still confirmedwhen Synthesis queries — there is nothing yet for a default recall to exclude.

What Synthesis sees on Day 10

With detection completed, the pool entering Day 10 is resolved. The eight $299 memories are outdatedand excluded by the query predicate on a default recall. The $349 memory is the only valid entry, so Synthesis’s recall for “competitor pricing current status” returns exactly one result:

DAILY INTELLIGENCE BRIEF — Day 10
Generated by: synthesis-agent
Memory pool: fleet-longrun-research

## Competitor Pricing (Governed Recall)
Current price: $349/month (source memory IDs: [list])
Status of recalled memories: active / confirmed
Suppressed memories: 8 outdated $299 memories from Days 1–8

The suppression count is a deliberate design choice in the synthesis agent’s AGENTS.md: it makes a second, separate recall with status:"outdated"and surfaces the count. The difference between “we have one confirmed price” and “we have one confirmed price and suppressed eight outdated entries” is the difference between a governed brief and a black box.

The three-agent architecture that makes governance enforceable

The governance model depends on role separation encoded in each agent’s configuration, not enforced at runtime through prompts. Three files define each agent: SOUL.md sets disposition, AGENTS.md defines the daily workflow, and the shared memclaw-research-fleet.mdskill establishes the common memory rules — the fleet_id, visibility scope, contradiction-handling rules, and recall protocol — in one place, so updating it propagates to all three agents.

Every agent stamps agent_idon every write and recall. That’s what lets Verification filter by filter_agent_id: "sourcing-agent", gives the audit trail provenance for every transition, and keeps any agent from acting on another’s scope. The reason this matters: self-verification is a structuralfailure, not just bad practice. If Sourcing confirmed its own writes, it would never catch a new write contradicting eight prior confirmed entries, because the comparison would happen in the same context that produced the write. Independent verification — a separate agent, its own identity, a scoped recall — is the only thing that makes contradiction detection robust.

Applying this to your own long-running fleet

The Day-9 scenario isn’t unique to pricing. Any fleet that ingests facts from external sources over time faces the same structure: the pool accumulates confirmations, and when the fact changes, the old entries stay until something marks them superseded. Five decisions, all demonstrated in the repo, prevent context-rot from compounding silently:

  • Separate write, verify, and synthesizeinto distinct agents. Scope the verifier’s recall to the writer’s output with filter_agent_id.
  • Don’t filter consumer recalls on status:"active"— use the default, unfiltered recall. Exact-match status silently drops everything confirmed.
  • Write new facts; never delete old ones. Let the async detector transition superseded entries to outdated— deletion removes provenance; transitioning preserves the audit trail.
  • Wait for detection to complete before a downstream read. For fleets with predictable update points, poll GET /memories/{id}/contradictions until detection_status is completed.
  • Surface suppression countsin synthesis output — a silent suppression becomes a visible audit signal.
Managed vs. OSS: the repo defaults to managed memclaw.net (/api/v1), with the async contradiction detector and the Prism dashboard as managed features; the MemClaw core is Apache-2.0 and self-hostable at localhost:8000/api/v1.

The core principle: trust the storage layer, not the context window

Long-running fleets fail silently not because agents reason poorly, but because memory accumulates faster than it is audited. The fix wasn’t a smarter prompt or a more capable model. It was infrastructure: an async contradiction detector that resolves the chain at write time, a default recall that excludes superseded entries before any agent sees them, and an orchestration step that waits for that resolution before the next recall. If you’re building a fleet that runs over days or weeks against data that can change, this is the reference architecture to start from.

FAQ

How is a default recall different from a prompt instruction to ignore outdated memories?

A default memclaw_recall (no status filter) is enforced at the database query layer: anything transitioned to outdated or conflicted (and deleted rows) is excluded from the result set before the agent sees it. A prompt instruction operates afterretrieval — the stale data is already in context, already influencing the synthesis.

When should a downstream agent wait for the contradiction detector rather than read immediately?

Detection fires on every write but runs asynchronously after the commit. For any fleet with predictable drift events (pricing, config, regulatory updates) where a downstream agent runs right after a write cycle, poll GET /memories/{id}/contradictions for the new write until detection_status returns completed, then let the downstream agent recall.

What's the difference between the contradiction detector and the crystallizer?

The contradiction detector resolves factual conflicts — on a single-value change it marks the superseded rows outdated and sets supersedes_id. The crystallizer (POST /crystallize) is a separate hygiene job that merges near-duplicate memories into one confirmed canonical and archives the sources. It never sets outdated.

Why can't a single agent write and self-confirm its own facts?

The contradiction comparison would happen in the same context that produced the write, so a new write contradicting eight prior confirmed entries would never be caught. Independent verification — a separate agent with its own identity and a recall scoped via filter_agent_id— is what makes detection robust.

Run the Day-9 scenario yourself.

python simulate.py --start 9 --end 10 jumps straight to the contradiction.

MemClaw is governed shared memory for AI agent fleets — multi-agent, multi-fleet, multi-tenant, with permissions and audit trails. Built by Caura.ai.