DEV Community

Cover image for Cordon: the late message may be the only record that someone is alive
crsvt
crsvt

Posted on

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

Revanth, team CMB · Graph Hacks (WeMakeDevs × FalkorDB) · Track 2

Live demo: https://cordon-crsvt.vercel.app

Code: https://github.com/crsvt/Cordon

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

The problem nobody fences correctly

When an earthquake hits, rescue teams lose their radios, but their tablets keep writing. Reports pile up offline and sync hours later. By then, the worksite they describe may have been handed to another team.

Picture it. At 10:30, team XSA11 finds two people alive under a slab at worksite C-5 and logs it on a tablet with no signal. At 12:40, XSA11 has been silent for two hours, so a coordinator reassigns C-5 to XCF10. At 14:10, XSA11's tablet finally syncs: "RESCUE, two confirmed live, north-east corner." It still claims the lease it held at 10:30.

A shared memory for agents has three choices:

Write policy What happens to C-5 Stale team took control Report of two survivors
Overwrite (last writer wins) reopened under XSA11 yes kept, but nobody is told
Reject (textbook fencing) stays with XCF10 no lost
Cordon (fence and keep) stays with XCF10 no kept and escalated to a person

The textbook fix for stale writers, fencing tokens, solves the first problem by creating the second: the only record that two people are alive gets thrown away. Cordon keeps both truths.

One atomic Cypher statement

Every coordination decision in Cordon is a single Cypher statement on FalkorDB that checks, decides, writes, audits and appends its own outbox event, all at once. Here is the heart of Q_REPORT:

// The fence: is this writer still the lead,
// at the worksite's current epoch?
CASE WHEN l IS NULL THEN 'UNKNOWN_LEASE'
     WHEN l.state <> 'ACTIVE'
       OR l.epoch <> w.epoch
       OR l.epoch <> $epoch THEN 'STALE_EPOCH'
     WHEN l.expires_at < now THEN 'LEASE_EXPIRED'
     WHEN l.role <> 'lead' THEN 'NOT_LEAD'
     ELSE null END AS fence_fail
...
// 1. The report is always kept
CREATE (t)-[:REPORTED]->(o:Observation {...})-[:ABOUT]->(w)

// 2. Only the current holder can move the status
FOREACH (_ IN CASE WHEN ctl_ok THEN [1] ELSE [] END |
  SET w.status = $status)

// 3. A stale status change is refused, on the record
FOREACH (_ IN CASE WHEN $status IS NOT NULL AND NOT ctl_ok
               THEN [1] ELSE [] END |
  CREATE (o)-[:CAUSED]->(:RejectedWrite {reason: fence_fail})
             -[:AGAINST]->(w))

// 4. New information reaches a person
FOREACH (_ IN CASE WHEN escalate THEN [1] ELSE [] END |
  MERGE (a:Alert {key: 'RETRIAGE:' + w.id})
  CREATE (a)-[:EVIDENCE]->(o))
Enter fullscreen mode Exit fullscreen mode

Three rules come out of it:

  1. Fence the control write. A status change counts only if the writer's lease is active, unexpired, the lead, and at the worksite's current epoch. Otherwise a RejectedWrite is recorded against the worksite.
  2. Keep the evidence. The observation is always stored. If it contradicts the current fact, it's linked CONFLICTS_WITH and raised as a re-triage alert for a person.
  3. Silence is a welfare event, never a silent reassignment. A quiet team goes amber, then red. A reassignment waits in an approval queue for a human.

This is why a graph fits: the proof of every decision is a path. Q_WHY walks from the rejected observation through the fenced lease to the current holder in five hops, so the "why" is a query, not a log search.

The fence in 3D on the real Lajpat Nagar streets

We tried to break it: 2,000 exercise days

One scenario proves nothing. The benchmark runs 2,000 seeded exercise days with 40 team agents: radios fail at random, teams keep writing offline, a person decides each reassignment after a delay, dark teams sync later, and devices resend. Every one of the 127,890 writes is judged by all three policies against the true lease state:

Overwrite Reject Cordon
A team no longer in charge changed a worksite 3,730 0 0
A report of someone alive was lost 0 4,269 0
A report of someone alive reached nobody 4,269 4,269 0
A resent report was counted twice 695 7 0

The first 25 days were replayed through the real statements on FalkorDB: 9,734 decisions agree with a pure-Python reference model, and 0 disagree.

Eight AI agents. No messages. One memory.

Then we gave every free rescue team a language model. Each step, the agents read the shared memory and choose the worksite they'll lead, at the same moment, without talking to each other, weighing collapse type, building use, size and travel. When two choose the same building, the lease grant settles it: exactly one wins. The loser is told who got there first, remembers it in its own session in the graph, and chooses again.

Round 08:50: each arc is one agent's choice; a red ring marks the building two agents wanted

In the run above, XSA10 and XIB10 both chose A-2 ("4-story apartments, pancake collapse, high life-safety priority"). XIB10 got the lease; XSA10 took A-1 one step later. The models choose what to write; the database decides whether it counts.

Agent cards: each team's model, its reason in its own words, and what the graph decided

Bring your own agent. Cordon serves MCP over streamable HTTP. Point a desktop assistant, an IDE agent or your own script at /mcp/, call join_exercise, and it becomes a rescue team, fenced exactly like everyone else.

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). Scrub the exercise day and Cordon shows the memory as it stood at that minute, next to what was already true but had not arrived yet.

At 12:40, while a person is deciding whether to reassign C-5, the memory shows XSA11 silent. The report of two people alive, seen at 10:30, only reaches it at 14:10: 220 minutes during which the memory could not have known.

Rewind at 12:40: already true, not yet known

Pull the plug. Lose nothing.

Every write commits exactly one event in the same atomic statement: query id, parameters, the server's now, a gapless sequence number. So the log is the memory.

The crash test copies the outbox, deletes the live graph, rebuilds it by replaying every event through the same named statements, and compares digests. On a full exercise day, 493 events replayed and the rebuilt memory was byte-for-byte identical to the one that was lost. Then the devices that missed the crash resend their last reports: each comes back as a retry, and nothing is counted twice.

Crash test: copy the outbox, pull the plug, replay, compare, resend

Try to break it yourself

The terminal on the site sends real attacks through the same fenced statement a team's tablet uses, on the running exercise:

  • attack stale: a team that lost a worksite sends "CLOSED" with its old epoch → STALE_EPOCH, refused, report kept
  • attack intruder: a forged lease → UNKNOWN_LEASE, refused, kept, escalated
  • attack replay: the same report twice → stored once, the second send is a retry
  • attack silence: a team goes dark → nothing is reassigned until a person decides

Then the storm: 120 hostile writes in about three seconds. Not one gets through, and FalkorDB answers each in milliseconds (the fenced report statement runs at about 2.5 ms p95 inside FalkorDB).

Memory that outlives the exercise

When an exercise ends, Cordon distils what it taught into a separate memory graph that no reset touches: how long a silent team's worksite waited for a person, and how late the report arrived. The lessons are derived from the graph's own record, never written by a model, and the agents recall them over MCP.

How it's built

  • FalkorDB 4.22. Every statement is a named Cypher constant that passes a lint: writes anchored on indexed keys, exactly one clock bump and one event per write, time only from the server clock.
  • Python + FastAPI for the API, with MCP tools for agents. No agent has a tool to approve a reassignment; that stays with a person.
  • Groq models (openai/gpt-oss-120b, qwen/qwen3.8-27b) for the field agents, the swarm and the coordinator's assistant.
  • React + three.js for the site: the fence in 3D on the real district, the swarm's arcs, and the crash test.
  • Real map data: 3,068 OpenStreetMap buildings of Lajpat Nagar, with fictional damage.

Try it

{
  "mcpServers": {
    "cordon": { "url": "https://cordon-api-ukja.onrender.com/mcp/" }
  }
}
Enter fullscreen mode Exit fullscreen mode

The API runs on a free instance, so the first request after a quiet spell can take up to a minute to wake it.

Rescue coordination is one place where "last writer wins" and "reject the stale writer" are both wrong. The late message may be the only record that someone is alive. Cordon fences the ownership and keeps the evidence.

Built for Graph Hacks by WeMakeDevs × FalkorDB.

Top comments (0)