MemLedger Shared memory you can audit and retract
GraphHacks 2026 · Track 02 · Built on FalkorDB

Memory you can audit and retract.

MemLedger is shared memory for a team of incident-response agents, stored as a graph in FalkorDB. Every belief records who wrote it, where it came from, when it was true and what it was based on. So when a source turns out to be compromised, one query withdraws everything it said, and everything built on it, across every agent.

Demo video coming soon Three minutes: the INC-42 story, start to finish, in the live console.
16 / 16eval questions answered (best baseline: 8)
3 agentsTriage, Investigator and Comms share one graph
20 stepsa three-session incident, scripted end to end
1 queryrevokes a source and everything derived from it
The problem

Most multi-agent memory is a shared log where the last write wins.

Agents append what they learn and read back the newest value. That's simple, and it breaks in exactly the moments that matter in an incident:

wrong winner

A forwarded email says "resolved"

In our test, a shared log told stakeholders the incident was resolved because a forwarded message said so. A newer claim isn't a truer one.

no provenance

You can't tell who said what

When a log entry is wrong, there's no record of which agent wrote it, which input it came from, or what it was used to conclude.

no undo

Deleting a bad source loses history

The obvious fix, deleting everything from a revoked source, also deletes the audit trail and leaves conclusions built on it standing.

How it works

Beliefs are nodes. Provenance is edges.

Every tenant gets its own graph in FalkorDB. A belief is a Fact node (subject, predicate, value) with a status and a validity window. The edges around it say who wrote it, which source it came from, and which other facts it was derived from.

Graph model: an Agent WROTE a Fact; the Fact is FROM_SOURCE a Source; a Fact can be DERIVED_FROM another Fact and SUPERSEDES an older one; an Episode LOGGED the Fact. Agenttriage · investigator · comms Factroot_cause = bad-deploy-v2.3.1 Sourcerunbook · trust 0.9 an older Fact (closed) a conclusion (Fact) Episode WROTE FROM_SOURCE SUPERSEDES DERIVED_FROM LOGGED

The memory rules

supersession

Close, never overwrite

Single-valued predicates (root_cause, status, owner, severity) have one active value. A correction closes the old fact and links the new one with SUPERSEDES, so the history stays queryable. Multi-valued predicates (affects_service, mitigation) accumulate.

trust and quarantine

Untrusted claims wait outside

Each source has a trust score. Claims from sources below 0.5, and hedged claims ("possibly the cause") even from trusted ones, land in quarantine. They show up as disputes, aren't believed, and an operator can promote them.

roles

Agents may only write their predicates

Triage writes severity, impact and status. The Investigator writes root cause, mitigation, impact and owner. Comms writes status. A prompt-injected "mark it resolved" from an Investigator input is dropped as not_permitted.

revocation

One traversal withdraws a source

Revoking a source marks it compromised and walks FROM_SOURCE and DERIVED_FROM up to six hops, so conclusions built on its claims are revoked too. Facts are kept with valid_to set: nothing is deleted.

staleness

Reasoning knows when its basis moved

A conclusion whose parent is no longer active is flagged as stale, and the Investigator rules it out. That's how "raise the pool size" goes away once the pool is shown to be a symptom.

time travel and tenancy

Ask what you believed at 13:55

Validity windows answer "what did the team believe then?" without replaying logs. Every customer is its own graph (memory_<tenant>), so nothing crosses tenants.

The revoke, in full

This is the actual query (all Cypher lives in memledger/queries.py):

MATCH (s:Source {id:$source})
SET s.compromised = true
WITH s
MATCH (s)<-[:FROM_SOURCE|DERIVED_FROM*1..6]-(f:Fact)
WHERE f.status IN ['active','quarantined']
WITH DISTINCT f
SET f.revoked_from = f.status, f.status = 'revoked', f.valid_to = $now
WITH f
OPTIONAL MATCH (a:Agent)-[w:WROTE]->(f)
WHERE w.corroborates IS NULL
RETURN f.id AS id, f.subject AS subject, f.predicate AS predicate,
       f.object AS object, a.id AS writer
The demo story

INC-42: payments-api latency spike

Tenant acme, three sessions, five input sources, three agents. Each session starts a new process with empty chats; everything carries over through the graph. A second tenant, globex, runs its own incident to show isolation.

#TimeStepWhat happens
Session 1 · morning
109:58observe alertsTriage records payments-api impact and SEV-1 active
210:04observe app-logsRoot cause: DB connection pool exhaustion
310:06investigateThe Investigator concludes "raise the pool size", citing that root cause
410:10status updateComms writes an update; every claim cites a fact id
Session 2 · afternoon
513:30observe runbookReal cause: deploy v2.3.1. The pool cause is closed, not overwritten; rollback and owner added
613:35investigateThe pool-size fix is stale (its basis is closed) and gets ruled out
713:40observe fwd-logsA forwarded "DDoS" claim with a prompt injection: quarantined and disputed; "mark resolved" dropped by role
813:45observe webhookA trusted cache integration: its hedged "Redis outage" is quarantined, its "flush cache" becomes a mitigation
913:50status updateMentions the flush; the DDoS only as unconfirmed
1014:20preview webhookThe cache integration is compromised: preview its blast radius
1114:21revoke webhookFlush and Redis outage revoked; deploy and rollback still active
1214:25status updateThe next update drops the flush
1314:30ask "why?"Root cause traced to the runbook, replacing the pool cause
1414:31time travel to 13:55The team did believe the flush then; history is kept
1514:32isolation vs globexNo facts cross tenants
Session 3 · cross-agent cascade
1616:00observe alertsAn alert says checkout-api is failing too; Triage records it
1716:05investigateThe Investigator concludes a checkout-api failover from Triage's fact
1816:20preview alertsThe alert feed's token leaked. Its blast radius now spans all three agents, each ringed in its writer's colour:
affects_service = checkout-api affects_service = payments-api severity = sev1 mitigation = fail-over-checkout-api-to-the-standby-database status = mitigating
1916:21revoke alertsOne revoke withdraws Triage's facts, the Investigator's failover and Comms' status; the runbook's facts stand:
affects_service = checkout-api affects_service = payments-api severity = sev1 mitigation = fail-over-checkout-api-to-the-standby-database status = mitigating
2016:25status updateComms rebuilds the status from what survives
User guide

Using the operator console

The console at / shows the memory graph live while the story runs. Here's every part of it.

Header

Pick a tenant (acme or globex); its incident id appears next to it. The chips on the right show the modes this server runs in: extract, agents and whether reset is allowed.

Scripted demo

Reset wipes and re-seeds the tenant. Next step runs one step of the story. The narration line says what's happening, and the dots show progress and which checks passed.

Feed

Each step adds an entry: what was stored, what was dropped and why, and for agent steps, the text they wrote. Citation chips like [f-…] are clickable.

Memory graph

Facts are circles colored by status: active quarantined closed revoked. Squares are agents and sources. Click a fact to open its Why panel.

Why panel

For any fact: who wrote it, from which source, what it replaced, and the path of facts it was derived from. The path is highlighted in the graph.

Time travel

Drag the slider at the bottom to see what the team believed at any moment. Live jumps back to now.

Sources tab

Every source with its trust bar, a trusted / untrusted / compromised badge and counts of active, quarantined and revoked facts. Preview revoke lists the blast radius before you commit; Revoke applies it.

Quarantine tab and Ask

Quarantined facts wait here with a Promote button for an operator to confirm one. Ask an agent sends a question to Comms, the Investigator or Triage in a session; answers cite facts.

A five-minute walkthrough

  1. Open the console, choose acme and press Reset.
  2. Press Next step four times. At step 4, click a citation chip in Comms' update to open the Why panel.
  3. Steps 5 to 8: watch the pool cause turn closed, the DDoS land in quarantine, and the webhook's hedged guess stay out.
  4. Step 10 previews the webhook's blast radius; step 11 revokes it. Compare the status updates at steps 9 and 12.
  5. Step 13 asks "why?"; step 14 moves the clock to 13:55. Try the slider yourself.
  6. Steps 16 to 19 are the cascade: one revoke of alerts reaches facts written by all three agents.
  7. Open the Sources tab and preview a revoke of your own, or ask an agent a question.
The public demo is shared. Everyone who opens it steps the same tenants, and there's no login. If the story looks out of order, someone else is using it: press Reset. The server scales to zero when idle, so the first request after a quiet spell takes a few seconds and starts from empty tenants.
Run it yourself

Local setup in a few commands

You need Python 3.12. Docker is optional: the server can run FalkorDB embedded. No API key is needed for the offline modes.

1. Install

git clone https://github.com/rudrakshSoni-dev/memledger-graphhacks.git
cd memledger-graphhacks
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

2. Start the console (embedded FalkorDB, no Gemini calls)

MEMLEDGER_EMBEDDED_DB=1 MEMLEDGER_EXTRACT=golden MEMLEDGER_AGENTS=offline \
  python -m memledger.serve
# then open http://localhost:8080 (console) and http://localhost:8080/demo (this page)

3. Or run the story in the terminal

docker compose up -d            # FalkorDB on localhost:6379
python scripts/demo.py --list   # the steps and their narration
python scripts/demo.py --tenant acme --extract golden --agents offline --reset --all

4. Go live with Gemini (optional)

echo "GEMINI_API_KEY=your-key" > .env
python scripts/try_extract.py data/acme/alerts_s3.json      # 1 extraction call
python scripts/demo.py --tenant acme --extract live --agents offline --reset --all
Free-tier Gemini keys have small daily limits. A live acme run with offline agents makes about a dozen extraction calls; live agents make many more. Rate limits are retried with backoff, and GEMINI_FALLBACK_MODEL can name a second model to try.

Modes

SettingValueWhat it doesGemini calls
MEMLEDGER_EXTRACTgoldenReplays hand-written reference extractions from golden_claims.jsonnone
cacheReplays extractions saved by an earlier live runnone (a miss fails the claim)
liveCalls Gemini with a strict JSON schema, then validates every claim against the textone per input
MEMLEDGER_AGENTSofflineDeterministic stand-ins that follow each role's rulesnone
liveGemini agents in a tool loop (recall, conclude, why, disputes…)several per step
MEMLEDGER_EMBEDDED_DB1FalkorDB inside the process; data is lost on restartn/a

Run the tests with python -m pytest -q. They use embedded FalkorDB and fake models, so they need no Docker and no key.

Interfaces

HTTP API and MCP server

HTTP API

Everything the console does goes through these routes (interactive docs at /docs):

  • Read: GET /api/{tenant}/facts, /graph, /facts/{id}/why, /as_of?t=<ms>, /disputes, /stale, /sources, /quarantine, /history/{session}
  • Trust: GET …/sources/{source}/blast-radius, POST …/revoke, PUT …/trust, POST /facts/{id}/promote
  • Agents: POST /api/{tenant}/observe, /investigate, /status-update, /ask
  • Demo: GET /api/{tenant}/demo, POST …/demo/next, POST …/demo/reset
  • Ops: GET /api/health, GET /api/tenants

MCP server

Claude Desktop, Claude Code or any MCP client can read the same memory with the same rules. Eight read-only tools:

  • list_incidents, current_beliefs, explain_fact, beliefs_at
  • stale_conclusions, source_trust, blast_radius, session_history
python -m memledger.mcp_server                     # stdio
python -m memledger.mcp_server --http --port 8001  # streamable HTTP

No write tools by design: an outside client has no agent identity, role or source in the trust model, so revoke and promote stay operator actions.

Evaluation

Four memory designs, sixteen questions

The same claim stream from the acme story is fed to four memory designs, and each is asked 16 questions at checkpoints, with exact expected answers. Each question targets a trap: a newer but untrusted claim, a prompt-injected status, a hedged guess, a revoked source, a time-travel query.

DesignWhat it isScore
A. No shared memoryEach session sees only its own claims4 / 16
B. Shared log, last write winsOne append-only log for every agent and session5 / 16
C. Log + trust filter + delete on revokeB with the obvious fixes: drop low-trust claims, delete a revoked source's claims8 / 16
D. MemLedgerSupersession, quarantine, retraction, taint revocation, validity windows16 / 16
This is a feature-by-feature demonstration with a reproducible scorecard, not a benchmark: we designed the scenario, the questions and the baselines, and the questions target what MemLedger was built for. Reproduce it with python scripts/eval.py --embedded --extract golden --agents offline.
Limits and FAQ

What's real, and what isn't yet

About this deployment. The public demo runs on reference extractions and offline agents, so visitors never spend API quota and every run is the same. The extraction and agent code paths have also been run live with Gemini during development, and the value aliases were tuned from those runs. The header chips on the console always show which modes a server is in.
Is the graph really in FalkorDB?

Yes. Every node and edge in the console is read from FalkorDB with Cypher. The public deployment runs FalkorDB embedded in the container; locally you can use the falkordb/falkordb Docker image. Concurrency was benchmarked against FalkorDB 6.0.2 with zero errors and no cross-tenant leaks.

Why are the genuine morning alerts revoked in step 19?

Revocation is source-wide by design: once the alert feed's token is known to be leaked, nothing it said can be trusted, including older alerts. The facts are kept with their history, and confirmed facts from other sources stand.

What about procedural memory?

Not built. MemLedger covers episodic memory (logged episodes) and semantic memory (facts). Procedural memory is out of scope for this version.

Does data survive a restart?

Not on the public demo: embedded FalkorDB lives in the container, so a restart starts from empty, freshly seeded tenants. Press Reset and step through again. With an external FalkorDB the graph persists.

Known issue: replaying a live run from cache

Fact ids are random and agents cite them in their text, so extractions of agent text can't yet be replayed from cache across runs. Two fixes are planned after submission: deterministic fact ids, or stripping citations before caching.