Skip to main content
Glama

C O R D O N

Fence the ownership. Keep the evidence.

Crash-safe shared memory for rescue-team agents, built on FalkorDB. A report from a team that is no longer in charge never changes the worksite, and it is never lost: it is kept, linked to what it contradicts, and put in front of a person.

FalkorDB Python FastAPI three.js React CI Stale writes accepted Reports of someone alive lost Exercise days Writes judged live demo video blog MCP licence

Graph Hacks · Track 2 · Revanth, team CMB

The swarm · Rewind · Crash test · Proof · Run it · MCP

▶ Demo video (2:53) · Live site · Blog post · Code · Use from your agent

🤖 Eight AI agents

Language-model rescue teams choose worksites at the same moment, without talking. The graph settles every collision: exactly one wins, the other remembers and chooses again.

⏪ Rewind the memory

Scrub the exercise day. See what the memory knew at 12:40, next to what was already true but had not arrived: a report seen at 10:30, learned at 14:10.

🔌 Pull the plug

Delete the live graph. Rebuild it from its own event log. Same digest, byte for byte. Devices resend: nothing counted twice.

Exercise. A fictional earthquake on real OpenStreetMap streets of Lajpat Nagar, New Delhi. Synthetic data, victim counts only, no names. Not for operational use. Not affiliated with INSARAG, UN OCHA, NDRF, NDMA or DDMA.

Try it

The API runs on a free instance, so the first request after it idles can take ~30–60 s to cold-start — give it a moment and retry.

Related MCP server: Agent Coherence — Stale Write Guard (FS)

The problem: the late message may be the only record that someone is alive

Rescue teams lose radio contact, collect reports offline and sync them hours later. By then the worksite may have been reassigned and closed. A shared memory for agents has three choices when such a report arrives:

  • overwrite it in: a team that is no longer in charge takes the site back;

  • reject it, the textbook fencing fix: the only record of the survivors is lost;

  • keep it and escalate, which is what Cordon does.

The result, measured on FalkorDB

At 14:10 team XSA11, silent since 10:20, syncs a report it wrote at 10:30: two confirmed live, NE quadrant. Since then a person confirmed a reassignment, XCF10 took the worksite at epoch 4, searched it and closed it. We send that same late write through three statements, on three copies of the same graph, and read the outcome back from the graph (cordon/exercise.py, checked by tests/test_fence.py):

Write policy

Statement

Worksite after the late write

Stale team took control

Report of two survivors kept

Escalated to a person

Overwrite (last writer wins)

Q_BASE_OVERWRITE

reopened as RESCUE under XSA11

yes

yes

no

Reject (textbook fencing)

Q_BASE_REJECT

CLOSED

no

lost

no

Cordon (fence and keep)

Q_REPORT

CLOSED under XCF10

no

yes

yes: CRITICAL re-triage, "targeted ASR4 + canine re-check, quadrant NE"

python -m cordon.exercise        # runs it on throwaway copies of the seed graph and prints this table

We tried to break it: 2,000 seeded exercise days

One scenario proves nothing, so bench/policies.py runs 2,000 seeded exercise days with 40 team agents. Leads lose radio contact at random and keep writing offline (some reports with confirmed live), the welfare sweep raises the alarm, a person decides each reassignment after a delay, the dark teams sync later, and devices resend some synced writes. Every one of the 127,890 writes (14,614 late, 14,484 stale, 4,342 stale reports of someone alive, 2,232 resent) is judged by the three policies against the true lease state:

Overwrite

Reject

Cordon

A team no longer in charge changed a worksite

3,730 (in 1,329 days)

0

0

A report of someone alive was lost

0

4,269 (in 1,213 days)

0

A report of someone alive reached nobody

4,269 (in 1,213 days)

4,269 (in 1,213 days)

0

A resent report of someone alive was counted twice

695 (in 506 days)

7 (in 7 days)

0

Cost: escalations a person must review

0

0

11,914

Reproduced on FalkorDB: the first 25 days were replayed through the real Cordon statements on FalkorDB, decision by decision: 9,734 decisions agree with the reference model, 0 disagree (resent reports included).

A life-safety report counts as lost or unescalated only when it adds something the current record lacks, for every policy (73 late reports repeated what the record already held: Cordon kept them without an escalation). Baseline harm is counted per write and not compounded, so their rows are a lower bound. The referee is the reference model (bench/model.py), which the oracle test checks against FalkorDB on random 40-agent sequences (2,000 in CI). Re-run: python -m bench.policies --seeds 2000 --falkordb 25; tests/test_policies.py.

How it works

Every coordination decision is one atomic Cypher statement that checks, decides, writes, audits and appends its own outbox event (cordon/queries.py). In Q_REPORT:

  1. Fence the control write. The status change is accepted only if the writer's lease exists, is active and unexpired, is the lead, and its epoch equals both the claimed epoch and the worksite's epoch. Otherwise a (:RejectedWrite {reason:'STALE_EPOCH'}) is written against the worksite.

  2. Keep the evidence. The (:Observation) is always stored. If it disagrees with the current (:Fact) it is linked -[:CONFLICTS_WITH]-> it and a (:Alert {kind:'RETRIAGE'}) is raised with the observation as evidence.

  3. Silence is a welfare event, never a silent reassignment. A team that misses its check-ins goes amber, then red (WELFARE_UNKNOWN), and an (:Approval {kind:'REASSIGN'}) waits for a person. Only then can another team take the worksite, at a new epoch (cordon/graph/lease.py).

  4. Explain it. Q_WHY returns the proof path from the rejected observation through the fenced lease to the current holder: five hops, with the chain of epochs.

The safety argument (assumptions, five lemmas, where the guarantees stop) is in proofs/PROOF.md. A pure-Python reference model (bench/model.py) and an oracle test drive the same random sequences of 40 team agents through FalkorDB and the model and require identical decisions.

Three things to try first

The swarm: eight AI agents, no messages, one memory

Every free rescue team is a language-model agent (cordon/swarm.py, POST /v1/live/swarm/start, POST /v1/live/swarm/step). On each exercise step the agents read the shared memory and each chooses the worksite it will lead, at the same moment and without talking to each other, weighing collapse type, building use, size and travel. Their writes reach FalkorDB in any order, and the lease grant decides every collision: when two agents choose the same building, exactly one gets the lease and the other is told who holds it and chooses again on the next step. In the run pictured, XSA10 and XIB10 both chose A-2 ("4-story apartments, pancake collapse, high life-safety priority"); the lease went to XIB10, and XSA10 took A-1 one step later. Coordination through the memory alone. (tests/test_swarm.py forces every agent onto the same site and checks there is exactly one winner.)

Rewind: what did the memory know, and when?

Every observation carries two times: when it was seen (observed_at) and when the memory learned it (received_at). GET /v1/live/rewind?t= returns the memory as it stood at any step of the day (kept per step) next to what was already true at that minute but had not arrived (Q_OBS_UPTO, a bi-temporal read). At 12:40, when a person is deciding whether to reassign C-5, the memory shows XSA11 silent and C-5 in SEARCH; the report of two people alive, seen at 10:30, only reaches it at 14:10, 220 minutes later.

Crash test: pull the plug, lose nothing

Every coordination write commits exactly one event in the same atomic statement (query id, parameters, the server's now, gapless sequence), so the log is the memory (cordon/recover.py, POST /v1/live/crash, tests/test_recover.py). The crash test copies the outbox, deletes the live graph, rebuilds it by replaying every event through the same named statements, and compares: on a full exercise day, 493 events replayed in about 12 s and the rebuilt memory had the same digest as the one lost (worksites, leases and epochs, approvals, fenced writes, every event). Devices that missed the crash then resend their last reports: each comes back as a retry, nothing is counted twice.

See it

Six pages. Every live view starts the exercise on first visit and runs it to 12:40, when XSA11 has been silent for two hours and a reassignment is waiting for a person, so nothing opens empty.

Page

What happens there

The story (#/)

A scroll film (generated background shots, labelled as dramatisation), then the fence in 3D on the real Lajpat Nagar streets (three.js): worksites rise as columns, C-5's lease moves to epoch 2 behind a fence, and the late report flies in as a packet of light that splits into a refused control write, a kept observation and an escalation. Then the result, the field agents, the lessons, the proof path and a terminal

Live ops (#/live)

The exercise on a 3D map of the district (columns by status, rings for held leases, silent teams flashing), the event rail, the assistant, and tabs: Decisions (the reassignment waits for you), Teams, Handover and Terminal

Swarm (#/swarm)

The language-model team agents choosing at once, their reasons, and every collision the graph settled

Rewind (#/rewind)

Scrub the day; the memory as it stood, next to what was true but not yet known

Crash test (#/crash)

Delete the live memory, rebuild it from its own log, compare digests

Proof (#/proof)

The 2,000-day benchmark as a race between three write policies, the FalkorDB replay, and how it works

The story page also works offline from the run recorded by python -m cordon.exercise --write-fixture (web/public/replays/fence.json), and its terminal replays a recorded run.

Try to break it

The terminal (on the story page and in Live ops) sends real writes through the same fenced statement a team's device uses, on the running exercise, and prints what FalkorDB answered (cordon/attack.py, POST /v1/live/attack/{kind}, tests/test_attack.py):

Command

The attack

What the graph answers

attack stale

a team that lost a worksite sends "CLOSED, 1 confirmed live" with its old lease and epoch

STALE_EPOCH: status change refused, report kept, escalated to a person

attack intruder

a team that never led the worksite closes it with a forged lease id

UNKNOWN_LEASE: refused, kept, escalated

attack replay

the network resends a report with the same id

stored once; the second send returns the stored row as a retry

attack silence

a working team's radio goes dark now

nothing is reassigned; the welfare sweep raises the alarm and a reassignment waits for a person

Agents that write, a graph that decides

At the two moments where a team turns what it saw or read into a write, a language model acts for the team through Cordon's field tools over MCP (an in-process server, never exposed over HTTP), and the graph decides whether the write counts (cordon/field_agent.py, tests/test_field_agent.py):

  • 10:30, radio down. XSA11's tablet turns the lead's dictated note ("Voices under the slab in the north-east corner. Two people…") into a structured report against the lease it last knew, epoch 1, and queues it. At 14:10 the device syncs it through submit_report: the graph fences the stale status change and keeps the report, escalated.

  • 15:00, handover. The incoming team's agent calls read_handover, then ack_handover with its read-back of the counts, open items, top hazard and digest. The lease moves to a new epoch only if every field matches; the test makes the model misread one field, and the lease stays where it was.

The model chooses what to write; the database decides whether it counts. Without a model key (as in CI) the scripted values are used, and every step says which wrote it.

Memory that outlives the exercise

When an exercise ends (16:00, or the website starts a new one), it is distilled into lessons in a separate graph, {prefix}_memory, that no reset touches (cordon/lessons.py, tests/test_lessons.py): how late the report from a silent team arrived and what the fence did with it, how long the reassignment waited for a person, the building reported under two ids, whether the handover read-back matched. Each lesson is derived from the graph's own record, never written by a model, and counts the exercises that taught it with the value each one measured; distilling the same exercise twice changes nothing. The assistant recalls them over MCP (lessons); GET /v1/lessons.

The coordinator's assistant, with a memory that knows when it is out of date

On the Live exercise page, a language model briefs the coordinator from the shared memory ("What needs my attention right now?", "Why was XSA11's report refused, and what should I do?"). It reaches the memory only through Cordon's MCP tools (an in-process MCP client), so it sees exactly what any other agent would, and it has no write tool: a reassignment is always a person's decision in the Inbox. Every worksite id, team code, number and time in its answer must appear in the tool results, or the answer is rebuilt from a template that reads the board.

Its own memory lives in the same graph: every question and answer is a (:Turn) of a (:Session), and each answer CITES the worksites it names. When a cited worksite changes afterwards (its version moves past the turn's), the answer is marked out of date, with what changed; asked again, the assistant reads its earlier turns from the graph and says its last brief no longer holds. (cordon/agent.py, tests/test_agent.py; Groq openai/gpt-oss-120b, then qwen/qwen3.8-27b; POST /v1/ask, GET /v1/memory/{sid}.)

Measured in production terms

GET /v1/metrics reports what the shared memory decided, counted from its own outbox events (writes, accepted, fenced, kept and escalated, waited for a person), and how long FalkorDB took for each statement, measured where it runs: round trip and server-side, p50 and p95. On a full exercise day against FalkorDB Cloud, the fenced report statement (Q_REPORT) took about 2.5 ms at p95 inside FalkorDB and about 20 ms round trip. The Live page shows the same strip.

Use it from your own agent

Cordon serves MCP over streamable HTTP at /mcp/:

{ "mcpServers": { "cordon": { "url": "http://localhost:8000/mcp/" } } }

Tool

What it does

fence_demo

Runs the late report under the three policies on throwaway graphs and returns the measured outcomes

explain_fence

The proof path for a fenced write

worksite_dossier

The worksite's full record: status, leases and epochs, every observation, conflicts, alerts, rejected writes

exercise_timeline

Every coordination write as an outbox event (gapless sequence, query id, outcome)

live_board

The live exercise now: clock, sectors, worksites, teams, alerts and approvals

live_brief

What needs a person now, compactly: pending decisions, critical alerts, silent teams, active worksites

live_dossier

A live worksite's full record

lessons

What earlier exercises taught, kept in the memory graph across resets

No agent has a tool to approve a reassignment: that decision stays with a person, on the website.

REST: GET /v1/fence, GET /v1/why/{oid}, GET /v1/dossier/{wid}, POST /v1/live/reset, POST /v1/live/tick?minutes=10, GET /v1/live/state, GET /v1/live/events, GET /v1/live/dossier/{wid}, POST /v1/approvals/{key} (a person's decision), POST /v1/ask (the assistant), GET /v1/memory/{sid}, POST /v1/live/attack/{kind}, POST /v1/live/swarm/start, POST /v1/live/swarm/step, GET /v1/live/rewind?t=, POST /v1/live/crash, GET /v1/lessons, POST /v1/lessons/distill, GET /v1/metrics, GET /v1/bench, GET /v1/health.

The graph data model

Node

Key

What it is

Building

osm_id

An OpenStreetMap building (3,068), IN_SECTOR a Sector, ADJOINS its neighbours

Worksite

id

An INSARAG-style worksite (C-5), with status, epoch, holder, on a building

Team

code

A rescue team agent (XSA11, XCF10, ...) of an Agency

Lease

lid

(:Team)-[:HOLDS]->(:Lease {epoch, state, expires_at})-[:ON]->(:Worksite); a new lead lease FENCES the old one

Observation

oid

A team's report, REPORTED by a team, ABOUT a worksite, always stored

Fact

fid

The current, bi-temporal record (valid_from, recorded_at), SUPERSEDES the previous one

RejectedWrite

rwid

A refused control write, with the reason and the epochs, AGAINST the worksite

Alert, Approval

key

Welfare and re-triage alerts; human decisions on reassignments

Event

seq

The outbox: one per coordination write, gapless

Every statement is a named constant in cordon/queries.py and passes a lint (cordon/query_lint.py): writes anchored on indexed keys, exactly one clock bump and one event per write, no LIMIT after an update, no path-enumeration procedures, time only from the server clock.

Run it yourself

Requirements: Python 3.14, Node 24, FalkorDB 4.22 (Docker falkordb/falkordb-server:v4.22.0 or FalkorDB Cloud).

python -m venv .venv
.venv/bin/python -m pip install -r requirements/dev.lock      # Windows: .venv\Scripts\python
.venv/bin/python -m cordon.seed --all --verify                 # load the Lajpat Nagar seed into FalkorDB
.venv/bin/python -m cordon.exercise                            # the three-policy result
.venv/bin/python -m uvicorn cordon.main:app --port 8000        # API, MCP at /mcp/
cd web && npm ci && npm run dev                                # website (reads /v1 through the dev proxy)
.venv/bin/python -m pytest                                     # the tests (FalkorDB needed for most)

Configuration comes from environment variables or a .env file one level above the repository; secrets are never committed.

What is built, and what is not yet

Built and tested: the seed (OSM extract, sectors, damage, injects), atomic worksite allocation, leases and fencing epochs, the fenced report, welfare sweeps and human approvals, handover packets with a checked read-back, the reference model and oracle, the proof argument, the three-policy exercise, the 2,000-day benchmark replayed on FalkorDB, the live exercise with eight team agents and a person in the loop (tests/test_live.py), the field agents with a model in the loop, the swarm of language-model team agents, the lessons kept across exercises, the attacks, rewind, the crash test, the assistant with its memory, the API and MCP tools, and the website.

Not built yet (planned in the project's master plan): five policies (this benchmark has three) with injected faults, the crash matrix, the Neon event log replayed on boot, the TLA+ model check, the device page and the radio net. This README claims only what the tests above check.

Data, licence and attribution

Code: MIT. Map data © OpenStreetMap contributors, available under the Open Database Licence (ODbL); see THIRD_PARTY_NOTICES.md. The earthquake, the teams and every count are fictional.

AI assistance

Built with the help of an AI coding assistant. Design decisions and the final review are the author's own. The three background shots in the website's opening film were generated with Kling and are labelled on the page as dramatisation; every number, map and verdict on the site comes from the project's own data and graph.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Persistent, consensus-validated institutional memory for AI agents. Gives LLMs real memory that survives across sessions - validated through BFT consensus, not just dumped to a file.
    38
    10 npm
    255
    Apache 2.0
  • A
    license
    A
    quality
    A
    maintenance
    Coherence guard for shared files: when two agents write the same file, the stale writer is denied instead of silently overwriting, then reacquires and retries. Single-host, TLA+-verified.
    10
    218 PyPI
    11
    Apache 2.0
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to safely write to and read from persistent memory by validating every write through three independent consensus nodes, storing memories only on 2/3 agreement and quarantining poisoning attempts.
    Apache 2.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables hierarchical coding agents to record typed decisions into a shared append-only log and have contradicting decisions refused by deterministic comparison, with conflicts escalated up the hierarchy and a report of how few reached the human.
    MIT