STIGMERGY

CockroachDB × AWS — Build with Agentic Memory

Coordination without a coordinator. Verifiable without a central authority.


Tagline

Autonomous agents that coordinate through a verifiable CockroachDB memory — no coordinator, no message bus, and no way to lie about what happened.


Inspiration

Ants have no manager. No ant knows the plan, and no ant tells another ant what to do. An ant walks, finds food, and leaves a chemical trace on the way home. Another ant crosses that trace and is slightly more likely to follow it. Thousands of ants, each running a stupid local rule, and what emerges is a path — not just any path, close to the shortest one, rerouted automatically when you put a rock in the way. Biologists have a word for it: stigmergy. Coordination through traces left in a shared environment. The environment is the message.

Now look at how we usually build multi-agent systems: a controller process, or a message bus, or — increasingly — a language model in the middle deciding who does what next, in prose, non-deterministically, unaccountably. Agents are moving into production, and the hard part was never the model. It's the memory. Not just that it stays online, but that you can trust what it says. If an agent's memory can be quietly edited, every decision built on it is suspect.

We gave the agents a database instead, and took everything else away.

What it does

STIGMERGY is a multi-agent system where autonomous agents build a shared episodic memory and coordinate through it — without ever talking to each other. There is no orchestrator, no message bus, and no LLM anywhere in the decision path. The only channel between agents is CockroachDB, the way ants coordinate through pheromone trails left in a shared world.

Agents store memories and their embeddings, recall neighbours with a distributed vector search, reinforce what they find, and leave decaying recruitment signals when a memory sits far from where it belongs — a trace in a table that decays over time exactly like a pheromone. Other agents, pursuing their own local business, read those signals and vote with exact Fraction weights. When the weighted vote crosses quorum, the memory migrates to where it belongs, under a cooldown that is a SQL WHERE clause rather than an if statement. Nobody decided this. Nobody was in charge of it. The trail formed.

Here's the part we care about most: every state change that touches shared memory is sealed, as it happens, into a per-node SHA-256 hash chain, committed into a chained Merkle ledger — in the same transaction as the state change it records. "No unaudited transition" isn't a policy anyone has to remember to follow. It's true by construction. The whole run exports as a sealed evidence bundle that anyone can re-verify with no cluster at all, in Python or in a browser tab. You can edit the row. You cannot edit the evidence.

Watch it happen — no install, no account, no signup

Open demo/console.html in a browser. That's the entire setup — no server, no build step, no dependencies, works from file://.

Press Run. Three agents wake up in a field of thirty memories across three regions — cooking, astronomy, databases — with six memories deliberately filed in the wrong homes. Recruitment signals bloom and decay. Around eighteen seconds in, seven signals are live. Around twenty-four seconds, the first memory migrates. Around forty seconds, all six misplaced memories are home.

Then the scoreboard prints a line we did not script:

7 migrations so far, 1 of them a memory the corpus never marked as misplaced — the neighbourhood decides, not the label.

The field found a seventh memory that our own corpus labeled as correctly filed, disagreed with us, and moved it. The mechanism doesn't know what the answer key says.

This isn't a video or a mock-up. It's the real decision arithmetic, in BigInt rationals: reinforcement c' = c + (1−c)·1/5, hysteresis EMA with β=1/4 entering at 3/5 and exiting at 2/5, consensus at vigor_for ≥ 1/2 · live_signals. Same numbers, same closed forms, same exact boundaries as the Python that runs against the cluster. It builds the audit chain the same way too — canonical JSON, entry_hash = sha256(prev_hash ‖ envelope), SHA-256 implemented inline so it works offline.

So break it. Click any entry in any chain, edit the payload, press Forge, then press Verify. The page names the forgery: the sealed hash, the hash it recomputed, and the exact sequence number where the chain stops relinking. Then press Restore and watch it go green again.

CockroachDB as the memory layer — not an afterthought

Memory isn't a cache bolted onto STIGMERGY; it's the entire system. CockroachDB holds all of it as transactional data:

  • embeddings in a VECTOR(384) column, scoped by region_id, under a Distributed Vector Index — the substrate every recall runs on;
  • agent and region state, recruitment signals, and the roaming/dwelling controllers;
  • the audit itself — per-node hash chains and a Merkle ledger — written in the same transaction as the state change it records, so an unaudited transition is impossible by construction.

Every coordination decision is a read or a write against CockroachDB. Take the database away and there's no system left.

Where the floats went

Nowhere near a decision. Every value that decides anything is an exact Fraction, quantized to DECIMAL(11,10) only at the SQL and audit boundary, so a hash is always taken over a canonical decimal string and never over a float. Two floating-point exceptions exist in the entire system, both documented, both outside every decision path: signal decay (strength * exp(-rate * Δt), gates liveness, never votes) and retry backoff jitter (decides nothing, is never hashed). Nothing is ever deleted, either — FORGOTTEN and ORPHANED are states, and rediscovery is an audited event. The ledger only grows, which we list as an accepted cost rather than pretend is free.

Two verifiers that do not trust each other

A sealed evidence bundle is one JSON file containing everything needed to re-examine a run without the cluster that produced it. Six checks run against it: the bundle as shipped is the bundle as sealed; every chain relinks; every entry hash recomputes; the content shipped is the content born; declared state reproduces from replaying the chains; every Merkle snapshot recomputes. That fifth one is the one that matters — edit a memory's region in the file to something the chain never authorized, and replay stops matching.

Those six checks are implemented twice — once in audit/bundle.py, once in the JavaScript inside demo/console.html — by two implementations that share no code. Destroy your cluster, open the bundle in a browser tab with nothing installed, and the page re-runs all six. If the two ever disagree about a bundle, one of them is wrong, and we'd rather find out than not. To prove the canonicalizers agree byte-for-byte, we ship a fixture sealed by the Python side over deliberately hostile content — accents, CJK, a quote, a backslash, a tab, a newline, an astral-plane emoji — and it passes in the browser too. The round trip closes in both directions.

Authority: hash chains do not authorize anybody

This was the correction that came out of red-teaming. A hash chain proves what happened. It says nothing about whether the writer was allowed to do it. So every mutable node_id is bound to an authenticated CockroachDB database principal, and every operation requires an explicit capability for the region it touches. A revoked node can't keep storing, reinforcing, signalling, resolving, or sweeping merely by continuing to name its old node id — one principal per node, per agent, per Lambda, per seeder, never one connection wearing many hats. tools/run_authority_integration.sh starts a disposable cluster and proves impersonation, cross-agent mutation, and revoked writes are all rejected.

The boundary we did not solve, stated plainly: a stolen database credential or a superuser is outside an application-level guarantee, and this is not Byzantine consensus. Consensus dilution raises the cost of cherry-picking a target — that's a robustness property, not a Sybil defense.

How we built it

CockroachDB Cloud (Serverless, on AWS) as the system of record, with Distributed Vector Indexing for recall; agents as Python processes, each with its own authenticated principal; two reactors deployed as AWS Lambdas — a changefeed resolver and an EventBridge-scheduled sweeper — with per-function IAM roles and Secrets Manager DSNs; the cluster managed through the agent-ready ccloud CLI. Audit is real SHA-256 over canonical JSON; embeddings are all-MiniLM-L6-v2. The same handlers deploy unchanged to GCP Cloud Run, which is how we know the identity contract is substrate-agnostic.

Serializable transactions. Every module takes a live cursor and never commits — the caller owns the transaction, so a state change and its audit entry are atomic together. FOR UPDATE on the memory row is what makes "two agents cannot double-migrate" a property of the database rather than a hope.

Changefeeds. A recruitment write becomes a webhook event that triggers the resolver. The deployed path reacts; it doesn't poll.

Challenges we ran into

CockroachDB Cloud signs with a private CA, so sslmode=verify-full from a container or Lambda needs the cluster root explicitly, not the system store — worth a note in the connect docs.

Making the field converge honestly took a correction, not a shortcut: with evidence from the database, we found the demo harness was missing the sweeper the deployed system already has (the anti-flood keeps one pending signal per memory, so a weak early vote blocked stronger later ones). We fixed it by running the real sweep — never by touching the consensus math or lowering a threshold.

And the one we found while writing this submission. The browser console has no Python and can't run a transformer, and the temptation was to invent a similarity function and narrate convergence anyway. Instead it uses a corpus where lexical distance is a truthful signal, labels its provider on its face, and offers a bake step to swap in the pinned model's real vectors. That bake step is where we caught ourselves. Testing the documented path, we found three defects stacked in it: the page had no <script> tag for the vector table at all, so the README's claim that it "picks it up automatically" was simply false; fixing that exposed a loader that fed a metadata string to atob(), which throws and would have killed the page; and fixing that revealed the tool had baked the cluster corpus while the console seeds its own — one text out of thirty-nine overlapped. Every other lookup would have missed, recall would have silently collapsed to insertion order, and the page would have relabeled itself as the pinned semantic model anyway. That's the exact failure our own is_semantic flag exists to prevent, arriving through a build artifact instead of a provider. The fix wasn't just to bake both corpora — the console now refuses a table that doesn't cover its corpus and says so, with the count of missing texts, because a partial table is worse than none.

Accomplishments we're proud of

Coordination that emerges with no coordinator, and a memory where every claim is independently verifiable — two implementations, Python and a browser tab, agree on a sealed run byte-for-byte, deployed on AWS Lambda against a real CockroachDB cluster.

What we learned

That "it works" and "it proves it worked" are different bars, and the second one is the one that matters for agentic memory. Run the cluster demo with the non-semantic development provider and the report exercises every mechanism, then declines to tell you the field converged — because with is_semantic = False, distance-based claims would be fabricated. It would have been three lines to print a convergence story anyway. Nobody would have checked. The demo obeys its own philosophy instead, and that's the reason we trust the parts that do make claims. A limitation named is a decision. A limitation hidden is a bug waiting for a better moment.

What's next

Actual multi-region deployment (the schema is already region-partitioned); wiring the CockroachDB Cloud Managed MCP Server for read-only agent inspection; a supersede() flow for memory revision under the same custody guarantees; region split/merge once we've designed the CAS serialization it needs (deliberately not shipped yet); and bounding transitive taint propagation with a fixpoint before quarantine ever auto-flags anything.

What we did not do (honest scope)

KNOWN_LIMITATIONS.md is a real file in the repo. Short version: no CockroachDB Cloud cluster and no deployed Lambdas have been exercised end-to-end — infra/template.json is a reviewable SAM scaffold with static contract coverage, not a deployment, and we won't call it one until there's pasted output to back it up. The local demo's resolver is --local-resolver, a labeled polling stand-in for the changefeed Lambda so the demo runs on a laptop. demo/field_viewer.html is a staged, hand-authored replay of custody and taint mechanics that audit/custody.py really implements — it visualizes them, it doesn't call them. A bundle proves what happened, not that the writer was authorized, and dropping a whole node's chain leaves an internally consistent file unless a published Merkle snapshot already committed to that node — so publish your roots. Bundle v1 doesn't yet seal the per-memory custody chains.

The demo corpus is deliberately compact — thirty memories, six misplaced — chosen so a judge can verify the entire run on camera, end to end. The design (region partitioning, the vector index, bounded sweeps) is built for scale; this submission optimizes for provability over raw volume.

Which CockroachDB tools did we use, and how?

Two of the four, used meaningfully:

  • Distributed Vector Indexing — the memories table stores VECTOR(384) embeddings with a VECTOR INDEX (region_id, embedding). Each agent's recall() is a region-scoped nearest-neighbour vector search over the pinned all-MiniLM-L6-v2 embeddings. This is how an agent finds a memory's resonant neighbourhood and detects one sitting away from it — the signal that drives every migration. The entire coordination loop runs on this index; without it there is no recall.
  • ccloud CLI (Agent-Ready) — the terminal-native, JSON-friendly, service-account control-plane surface used to inspect and manage the live cluster (ccloud cluster list / info / user / sql). It's the agent-ready path for cluster and SQL-principal management in our workflow.

Not used, disclosed honestly: Cloud Managed MCP Server, Agent Skills Repo.

Which AWS services did we use, and how?

  • AWS Lambda — two deployed reactors: a changefeed-driven resolver (public Function URL) that migrates memories by consensus, and an EventBridge-scheduled sweeper that expires stale recruitment signals. On every invocation, each proves that its authenticated CockroachDB principal owns its declared node before doing any work.
  • AWS Secrets Manager — each Lambda reads only its own CockroachDB DSN secret.
  • Amazon EventBridge — schedules the sweeper tick, the cron half of the system.
  • AWS IAM — one least-privilege execution role per function.
  • (Amazon S3 also backs the SAM deployment artifacts.)

Meaningful integration

CockroachDB is the agents' only shared memory and their only communication channel. Distributed Vector Indexing is the recall substrate — operational data and vectors live in the same transactional store, so there's no consistency gap between an agent's memory and its embeddings. The audit trail is written in the same transaction as the state it records. On AWS, the resolver and sweeper Lambdas are the deployed reactors of that memory: each has its own IAM role, reads its own DSN from Secrets Manager, and — via EventBridge and the changefeed — reacts to the database rather than being told what to do. The integration is load-bearing: remove the vector index and there is no recall; remove the Lambda resolver and sweeper and the field cannot converge.

Try it

  • demo/console.html — open the file. Run the field, forge an entry, watch verification catch you. Zero setup.
  • for t in tests/test_*_pure.py; do python3 "$t"; done — 230 assertions, no database required.
  • tools/run_secure_demo_local.sh — one command: disposable CockroachDB node, one authenticated principal per node, a real run, a sealed bundle, verified, then the cluster is deleted and the evidence is not.
  • Load that bundle back into the console after the cluster is gone. Then open it in a text editor, change one row, and watch check five refuse it.

Apache 2.0. The schema is the real spec — most invariants are constraints, not conventions.

Built With

Share this project:

Updates

Submission history