Cordon
by crsvt
README.md
<div align="center">
<img src="docs/img/fence.jpg" alt="The fence in 3D on the real Lajpat Nagar streets: a stale status change is refused, the report is kept and linked as conflicting" width="100%">
# C O R D O N
### Fence the ownership. Keep the evidence.
**Crash-safe shared memory for rescue-team agents, built on FalkorDB.**<br>
A report from a team that is no longer in charge never changes the worksite, and it is never lost:<br>
it is kept, linked to what it contradicts, and put in front of a person.
<br>





<br>





<br>
[](https://cordon-crsvt.vercel.app)
[](https://youtu.be/PexaFdd-GFI)
[](https://dev.to/crsvt_7bcbef1ef88d38a72b2/cordon-the-late-message-may-be-the-only-record-that-someone-is-alive-k79)
[](#use-it-from-your-own-agent)
[](LICENSE)
**Graph Hacks · Track 2 · Revanth, team CMB**
[**The swarm**](#the-swarm-eight-ai-agents-no-messages-one-memory) ·
[**Rewind**](#rewind-what-did-the-memory-know-and-when) ·
[**Crash test**](#crash-test-pull-the-plug-lose-nothing) ·
[**Proof**](#we-tried-to-break-it-2000-seeded-exercise-days) ·
[**Run it**](#run-it-yourself) ·
[**MCP**](#use-it-from-your-own-agent)
<br>
**[▶ Demo video (2:53)](https://youtu.be/PexaFdd-GFI)** · **[Live site](https://cordon-crsvt.vercel.app)** · **[Blog post](https://dev.to/crsvt_7bcbef1ef88d38a72b2/cordon-the-late-message-may-be-the-only-record-that-someone-is-alive-k79)** · **[Code](https://github.com/crsvt/Cordon)** · **[Use from your agent](#use-it-from-your-own-agent)**
</div>
<br>
<table>
<tr>
<td width="33%" valign="top">
### 🤖 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.
</td>
<td width="33%" valign="top">
### ⏪ 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**.
</td>
<td width="33%" valign="top">
### 🔌 Pull the plug
Delete the live graph. Rebuild it from its own event log. **Same digest, byte for byte.** Devices resend: nothing counted twice.
</td>
</tr>
</table>
> **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
- **Live:** https://cordon-crsvt.vercel.app — start with **Swarm**, then **Rewind**, then **Crash test**.
- **Demo video:** ▶ https://youtu.be/PexaFdd-GFI (2:53)
- **Code:** https://github.com/crsvt/Cordon (MIT)
- **Blog post:** https://dev.to/crsvt_7bcbef1ef88d38a72b2/cordon-the-late-message-may-be-the-only-record-that-someone-is-alive-k79
- **MCP** (streamable HTTP — drop into your own agent):
```json
{ "mcpServers": { "cordon": { "url": "https://cordon-api-ukja.onrender.com/mcp/" } } }
```
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.
## The problem: the late message may be the only record that someone is alive
<img src="docs/img/late-report.jpg" alt="14:10, a report from four hours ago: XSA11's tablet syncs what it wrote at 10:30 while XCF10 now holds C-5 at epoch 2" width="100%">
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`](cordon/exercise.py), checked by [`tests/test_fence.py`](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" |
```sh
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`](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`](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`](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`](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`](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`](proofs/PROOF.md).
A pure-Python reference model ([`bench/model.py`](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
<img src="docs/img/swarm-map.jpg" alt="Round 08:50 on the 3D district: each arc is one agent's choice; a red ring marks the building two agents wanted" width="100%">
<img src="docs/img/swarm-agents.jpg" alt="Agent cards: each team's model, its reason in its own words, and what the graph decided (XSA10: chose A-2, XIB10 got it first)" width="100%">
Every free rescue team is a language-model agent ([`cordon/swarm.py`](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`](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?
<img src="docs/img/rewind.jpg" alt="Rewind at 12:40: the memory knew 26 worksites and one silent team; already true but not yet known: C-5, 2 confirmed alive, seen 10:30, learned 14:10" width="100%">
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
<img src="docs/img/crash.jpg" alt="Crash test: copy the outbox, pull the plug, replay 440 events, compare (identical), devices resend (8 of 8 came back as retries, 0 counted twice)" width="100%">
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`](cordon/recover.py), `POST /v1/live/crash`,
[`tests/test_recover.py`](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
<table>
<tr>
<td width="50%"><img src="docs/img/bento.jpg" alt="The story page: the three headline features as cards"></td>
<td width="50%"><img src="docs/img/swarm-hero.jpg" alt="The Swarm page: eight AI agents, no messages, one memory"></td>
</tr>
</table>
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`](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`](cordon/attack.py),
`POST /v1/live/attack/{kind}`, [`tests/test_attack.py`](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`](cordon/field_agent.py), [`tests/test_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`](cordon/lessons.py),
[`tests/test_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`](cordon/agent.py), [`tests/test_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/`:
```json
{ "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`](cordon/queries.py) and passes a lint
([`cordon/query_lint.py`](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).
```sh
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`](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`](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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues