The legal agent that gave wrong advice
A contract management agent is asked: "What are the current terms of MSA-7 and what governing law applies?"
The facts in the system look like this:
- March 2025 — MSA-7 signed. Contract value: $2.5M. Governing law: Delaware.
- July 2025 — Amendment 1 executed. Contract value revised to $3.2M.
- November 2025 — Governing law changed to California via side letter.
- January 2026 — Litigation hold placed on all MSA-7 documents.
The agent queries its vector store. Semantic search returns the original MSA, the amendment, and both governing law records — all ranked roughly equally by embedding similarity to the query. The LLM receives all of them. It produces an answer that hedges between $2.5M and $3.2M and references Delaware law, which was superseded eight months ago.
The agent didn't hallucinate. It faithfully summarised what it was given. The problem is what it was given — contradictory facts with no signal about which version is current.
| Plain RAG | Ashnode | |
|---|---|---|
| Contract value | $2.5M and $3.2M returned together — LLM hedges between both | Only $3.2M (Amendment 1). Original superseded and hidden. |
| Governing law | Delaware and California both returned with no conflict signal | Only California. Delaware superseded in November 2025. |
| Litigation hold | May or may not appear depending on embedding similarity rank | Always included — semantically relevant to the query |
| Contradictions | Passed silently to the LLM | Detected and flagged in the packet before the LLM sees them |
| Audit trail | None — no record of what was retrieved or when | store_revision + policy_version stamped on every packet |
This is not an edge case. It is the default behavior of plain vector RAG applied to facts that evolve over time.
In legal, compliance, healthcare, and finance — exactly the domains where agents are being deployed — the cost of this failure is not a wrong chatbot response. It's liability.
Why RAG doesn't know what "current" means
Vector search was designed for static document retrieval. You have a corpus, you embed it, you find the nearest neighbours to a query. It does that job well.
What it was never designed to do:
- Track supersession — it has no concept that MSA Amendment 1 replaces the original value, not supplements it
- Surface contradictions — it returns "Delaware" and "California" with equal confidence and no flag that they conflict
- Decay relevance over time — a fact from 18 months ago ranks the same as a fact from last week if the embeddings are similar
- Produce a reproducible packet — run the same query twice after a new document is ingested and you get different results with no record of what changed
These aren't missing features you can prompt-engineer around. They're structural properties of the retrieval architecture. You cannot fix a supersession problem with a better system prompt.
What an epistemic memory layer does differently
An epistemic memory layer sits between your data pipeline and your LLM. Its job is not storage or retrieval — it's the retrieval contract.
Every query returns a bounded, inspectable context packet with four guarantees that plain RAG cannot provide:
- Supersession enforced — when a newer fact replaces an older one, the old version is hidden by default. The agent always sees the current state.
- Contradictions surfaced — tensions between retrieved facts are detected and attached to the packet, not silently passed to the LLM.
- Freshness scored — every item carries its age, decay factor, and supersession status. The agent knows this fact is three days old, that one is six months old.
- Deterministic and replayable — the same query against the same memory state always returns an identical packet. If an agent makes a wrong decision, you can reconstruct exactly what it knew.
This is Ashnode. It runs locally, CPU-only, with ~3ms p95 recall latency. No external dependencies. No API keys required for the default setup.
How Ashnode works
There are three components:
1. The store and index
Claims are stored in SQLite (WAL mode, durable across restarts) and indexed in an HNSW graph for approximate nearest-neighbour search. HNSW gives O(log N) query time — latency grows at 0.12x for a 100x corpus increase, keeping p95 recall around 3ms in production.
2. The background brain
A daemon thread that runs NLI (Natural Language Inference) contradiction detection asynchronously after every ingest. It scans each new claim against its nearest neighbours using a cross-encoder model, stores contradiction records when found, and never blocks the ingest path. Your agent sees contradiction flags on the next recall after detection — typically within milliseconds on modern hardware.
3. The recall contract
recall(query, policy) returns a structured context packet. The policy controls how many items to return (k), how many contradictions to surface (c), decay half-life (tau), and whether superseded history is included. Every packet carries a store_revision — a monotonic write counter that pins the exact memory state that produced it.
The legal agent, rebuilt with Ashnode
Let's walk through the same contract lifecycle, this time with Ashnode as the memory layer.
Ingestion
Each fact is stored with a claim_key — a stable, human-readable identifier for that specific claim (e.g. "contract.MSA-7.governing_law"). When a newer version of the same fact arrives, Ashnode automatically supersedes the old one. Your agent always gets the current truth, never the full revision history unless you explicitly ask for it.
from ashnode import Ashnode
memory = Ashnode(db_path="legal-memory.db")
# March 2025 — original MSA
memory.ingest(
"MSA-7 contract value is $2.5M. Governing law: Delaware.",
source="contracts/msa-7-original.pdf",
claim_key="contract.MSA-7.value",
)
memory.ingest(
"MSA-7 governing law is Delaware.",
source="contracts/msa-7-original.pdf",
claim_key="contract.MSA-7.governing_law",
)
# July 2025 — amendment supersedes value
memory.ingest(
"MSA-7 contract value revised to $3.2M per Amendment 1.",
source="contracts/msa-7-amendment-1.pdf",
claim_key="contract.MSA-7.value", # same key — auto-supersedes the original
)
# November 2025 — governing law change
memory.ingest(
"MSA-7 governing law changed to California via side letter dated Nov 2025.",
source="contracts/msa-7-side-letter-nov25.pdf",
claim_key="contract.MSA-7.governing_law", # supersedes Delaware
)
# January 2026 — litigation hold
memory.ingest(
"Litigation hold placed on all MSA-7 documents effective Jan 2026.",
source="legal/litigation-hold-jan26.pdf",
claim_key="contract.MSA-7.litigation_hold",
)
The claim_key parameter is the key design decision. When two claims share the same key, the newer one automatically supersedes the older one. The old version is preserved in the store for audit purposes but hidden from default recall. No manual ID tracking. No delete operations.
Recall
packet = memory.recall("MSA-7 current terms, governing law, and any active holds")
# What the agent receives:
for item in packet.items:
fi = packet.freshness[item.item_id]
print(f"[{item.source}] {item.content}")
print(f" age={fi.age_seconds/86400:.0f} days superseded={fi.is_superseded}")
# Check for any surfaced contradictions
if packet.contradictions:
print("Contradictions detected — review before proceeding")
# Always know if context was capped
if packet.completeness.any_truncated():
print("Context was capped by policy — consider raising k")
The agent receives exactly three items: the current contract value ($3.2M), the current governing law (California), and the active litigation hold.
The $2.5M original and the Delaware governing law record are in the store — available for audit — but not returned by default recall. The LLM cannot see them unless you explicitly request superseded history.
The agent produces a correct, unambiguous answer: MSA-7 is currently valued at $3.2M, governed by California law, with an active litigation hold from January 2026. Zero contradictions passed to the LLM. Full audit trail available if needed.
Audit query — when you need the history
from ashnode.models import Policy
from ashnode import register_policy
audit_policy = Policy(
policy_version="audit-v1",
k=50,
include_superseded=True, # show the full history
)
register_policy(audit_policy)
history = memory.recall(
"MSA-7 full contract history",
policy_version="audit-v1",
)
# Returns all versions — original, amendment, side letter — with provenance
The same store serves both queries. Default recall returns only current state. Audit recall returns the full chain. The policy controls which view you get — and the policy version is stamped on every packet so you always know which view produced which output.
What's in every context packet
| Field | What it gives your agent |
|---|---|
| items | Top-k claims, semantically ranked and freshness-weighted. Superseded versions excluded by default. |
| contradictions | Detected tensions between retrieved items, keyed by item_id. Flagged before they reach the LLM. |
| freshness | Per-item: age in seconds, decay factor (e^−age/tau), supersession status, and the item that superseded it. |
| completeness | Explicit flags for each cap: items_truncated, contradictions_truncated. No silent gaps. |
| store_revision | Monotonic write counter. Same revision + same query + same policy = identical packet. Enables replay. |
| policy_version | Pins which policy produced this packet. Every decision is auditable to its exact retrieval configuration. |
Performance
Ashnode is designed to run in production without infrastructure overhead:
- p95 recall latency: ~3ms (O(log N) scaling with corpus size)
- Sublinear scaling: 0.12x latency growth for a 100x corpus increase (HNSW)
- Determinism: zero failures across all test runs — same inputs, same packet
- Contradiction detection: async in background brain, never adds to ingest latency
- CPU-only: no GPU required, no cloud dependency for the default configuration
Who should use this
Ashnode is the right layer if your agent meets any of these conditions:
- It runs for more than one session — facts it holds can change between runs
- It operates in a domain where a wrong answer has a real cost (legal, healthcare, compliance, finance)
- You need to be able to audit or replay what the agent knew when it made a decision
- You're already using a vector store but have no mechanism for supersession or contradiction detection
If your agent is stateless — each conversation starts fresh with no persistent memory — you don't need this. Ashnode is infrastructure for agents that accumulate knowledge over time and need that knowledge to stay correct.
Getting started
pip install git+https://github.com/itachi-hue/ashnode.git
from ashnode import Ashnode
memory = Ashnode(db_path="agent.db")
memory.ingest("Your first claim.", source="your-source")
packet = memory.recall("your query")
for item in packet.items:
print(item.content)
print(packet.freshness[item.item_id])
That's the full integration. NLI contradiction detection runs automatically in the background. SQLite persists everything across restarts. No configuration required to get started.
Try Ashnode with your agent
Early access is available to teams building long-lived agents in production. Full technical documentation and benchmark results shared after access is granted.
Request Access →