Parallel agents that think for forty seconds without corrupting each other.
An LLM agent reads shared memory, thinks for forty seconds, then acts. The world changed while it was thinking. Today you get two bad options: lock everything, or throw the thinking away. INTERLOCK adds a third.
Free, self-serve, no signup — the key is issued by the same cluster this page runs on.
Live mechanism
serializableOptimistic concurrency would discard all four steps and re-run the whole task. Locking would have made Triage wait forty seconds.
The finding that started this
Running AI agents in parallel today is slower than running them one at a time — and costs 83% more.
You pay nearly double to go backwards. That is not a tuning problem; it is the reason parallel agents do not scale.
Try it
Watch two agents collide
This runs against the production cluster, right now. Not a recording — real embeddings, a real serializable commit, a real ruling, and the cost of producing it.
Run a live conflict
Two agents, one queue. The Scheduler spends 12,500 tokens planning an overnight rebalance; Triage commits into the same queue while it is still thinking.
Nothing here is pre-recorded. Each run creates real rows, generates a real embedding, and calls a real model.
The problem
An agent's transaction is not a database transaction
A database transaction is measured in milliseconds and its read-set is knowable up front. An agent’s is measured in minutes of inference, and its read-set is broad and opaque. Classical concurrency control was not built for that shape.
- t = 0s
1. Read
The agent reads shared memory and forms a plan against that snapshot.
- t = 0–40sWorld changes
2. Think
Inference runs. Meanwhile another agent commits a write that touches the same state.
- t = 40sSilent corruption
3. Act
The agent acts on a world that no longer exists. Nothing raised an error.
Nothing in that sequence throws an exception. The agent did exactly what it decided to do, and what it decided to do was wrong. Monitoring built to catch crashes will report a zero error rate the whole way through.
What everyone does instead
The state of the art is a merge conflict
Parallel agents are shipping at scale right now — sub-agent teams, background agents, swarms coordinating a hundred agents on one job. The industry's answer to shared-state conflict is a git worktree.
Two-phase locking
Hold locks for the duration of the task.
An agent holds its locks for the entire minutes-long task, blocking every other agent that touches overlapping state.
0.81 deadlocks per trial · 1.04× speedup
Optimistic concurrency
Detect the conflict at commit and abort.
A single conflict discards the agent's entire reasoning — and re-running it costs more than the parallelism ever saved.
0.95 aborts per trial · 0.93× speedup at 1.83× cost
Fork and merge
Give every agent its own git worktree.
This is what mainstream systems actually ship. It offers only weak isolation, and if two agents edit the same file the merge simply fails.
Cannot prevent anomalies
The mechanism
Validation becomes reasoning; abort becomes repair
The insight is that most conflicts are semantically irrelevant. An agent can read the conflicting write and judge whether it actually breaks its plan. A real conflict needs repair of only the dependent steps — not the whole task.
- 1
Declare
Serializable write + vector columnBefore acting, an agent writes an intent: what it read, and what it plans to do — as text and as an embedding.
- 2
Watch
C-SPANN distributed vector indexWhen any agent commits, we find which in-flight agents are actually threatened — by meaning, not by row overlap.
- 3
Diff
AS OF SYSTEM TIMEShow the threatened agent exactly what changed in the world since the snapshot it read.
- 4
Adjudicate
Amazon BedrockA model reads the diff and rules: irrelevant, invalidating, or fatal. Most conflicts are irrelevant.
- 5
Commit
SERIALIZABLE by defaultThe final write lands as a genuine serializable transaction. Lost updates are impossible, not unlikely.
Our own audit feed disagrees with that — about seven in ten rulings on /v1/adjudications are invalidating. That feed is not production traffic: every row in it came from a demo, a test or the benchmark, and all three construct a real conflict on purpose. Nobody writes a demo where nothing happens. The claim rests on the benchmark workload, where writes are drawn realistically — and the endpoint returns this caveat next to the counts rather than waiting to be caught.
Why this database
Not convenient — load-bearing
Every one of these is doing work the system could not do without it. Swap the database and the mechanism stops functioning, rather than merely getting slower.
SERIALIZABLE by default
Two agents must never both win a write.
Correctness is inherited from the database rather than reimplemented in application code. PostgreSQL defaults to READ COMMITTED; most vector stores have no transactions at all.
MVCC + AS OF SYSTEM TIME
Diff the agent's snapshot against the present.
This is the mechanism, not a convenience. Without point-in-time reads there is no way to show an agent what changed under it, and no way to reconstruct a past decision during review.
C-SPANN vector index
Find semantically threatened agents.
Embeddings live in the same transactional database as the rows, so a similarity query and a graph query see identical state. A bolt-on vector store introduces exactly the consistency gap this system exists to close.
Multi-region survivability
Adjudication cannot stop when a region does.
Agents contend across regions. If the arbiter's memory is unavailable, every agent in the fleet is unsafe at once — so the memory has to survive a region loss without losing a decision.
Changefeeds
Notify without a second source of truth.
A changefeed on commit_log posts to Lambda, which publishes to EventBridge and on to an SQS worker. Publishing from the application instead would let 'the write succeeded' and 'the event was sent' disagree — a crash between them silently drops an adjudication. Reading the same durable log means an event exists if and only if the row does.
Benchmark
Where this pays, and where it doesn't
Every figure carries its provenance. Published means somebody else measured it and we cite them. Measured means our harness produced it on a live cluster, and you can re-run it.
Parallel vs serial speed
0.93×
Optimistic concurrency is slower than not parallelising at all
Token cost of parallelism
1.83×
83% more inference spend, for negative speedup
Failures from misalignment
>1/3
Share of multi-agent failures traced to inter-agent conflict
Tokens burned after the warning
58.1%
Spend that continues after a run is already doomed
Cost vs. optimistic concurrency
relative to serial execution · lower is better
Below roughly 12,000 tokens of reasoning per task, don’t use this — just retry. There is nothing expensive enough to be worth protecting. Above it, the saving grows with the cost of the task.
0 lost updates at every point on this curve, in every mode. Serializable isolation holds regardless of which approach is cheaper.
Reproduce this in fifteen seconds
Node only — no database, no AWS account, no configuration. It issues a throwaway key if you do not have one.
git clone https://github.com/usv240/interlock && cd interlock && npm install
npm run compare # two collisions, priced, ~15s
npm run compare -- --reasoning 400 # below the crossover — prints a lossThe second command is the one worth running. It sets the task below the crossover, where this approach costs more than simply retrying, and the output says so in red. A demo that can only produce good news is not evidence.
Head-to-head against both baselinesthroughput and token cost, at the top of the curve
Three ways to handle a conflict
Throughput
× serial execution speed
- Two-phase locking1.04×
- Optimistic concurrency0.93×
- INTERLOCK1.40×
Dashed line marks 1.0× — running the agents one at a time. Further right is better.
Inference cost
× serial execution token spend
- Two-phase locking1.00×
- Optimistic concurrency1.83×
- INTERLOCK1.28×
Dashed line marks 1.0× — running the agents one at a time. Further left is better.
The two baseline rows are measured numbers from CoAgent: Concurrency Control for Multi-Agent Systems (arXiv:2606.15376). The INTERLOCK row is our own measurement, produced by npm run bench on the live cluster. It is taken at the top of the crossover curve below, where tasks are expensive enough for the approach to pay — at smaller task sizes it loses, which the curve shows rather than hides.
Verified against the live cluster10 claims, each with the command that proves it
Every claim, and how to check it
| Claim | Value | How it was checked |
|---|---|---|
| Isolation | serializable | SHOW default_transaction_isolation |
| Regions | 3 (us-east-1, us-east-2, us-west-2) | SHOW REGIONS FROM DATABASE |
| Survival goal | region | SHOW DATABASES |
| Time-travel reach | 1–24h, bounded by the GC window | npm run continuity |
| Embedding dimensions | 1024 (Titan V2) | live Bedrock invocation |
| Vector index actually selected | yes, at ~1,700 in-flight plans | npm run ai:vector |
| Tenant isolation | 0.0000 cosine, still not matched across tenants | npm run test:isolation |
| Spend ceiling | enforced per tenant and service-wide | GET /v1/health |
| Lost updates | 0 across every benchmark mode | counter arithmetic |
| Exactly-once adjudication | held under repeated connection kills | npm run chaos |
Architecture
What each piece actually does
The hackathon asks for at least two CockroachDB tools and one AWS service. We use all four CockroachDB tools — and only the AWS services we genuinely run on, because claiming more than you use is worse than claiming less.
Two paths, separated on purpose
CockroachDB appears in both lanes because it is the only component that has to. It holds the state an agent commits, the provenance and vectors detection walks, the MVCC history the diff replays, and the ruling — and it is the thing that notifies, so the write and the notification cannot disagree.
What every service is doing, and why it is there4 CockroachDB tools · 7 AWS services
CockroachDB — all four tools
Distributed Vector Indexing
A C-SPANN index over intent embeddings finds which in-flight agents are semantically threatened by a commit — the ones that share meaning but no rows.
Managed MCP Server
Not a human console — a tool belt for the adjudicating agent. Before ruling, it may request one read-only lookup: has this resource been churning all morning, or is this the first change in an hour? Because the server is read-only by default, an agent investigating an incident is structurally incapable of altering it, and every lookup is audit-logged outside our own logging. A model that cannot look things up has to guess.
ccloud / Cloud control plane
A continuity agent that refuses to let adjudication run if resilience is not actually configured. It reads cluster state from the control plane, verifies three regions and a region-survival goal, and reports gc.ttlseconds per table — the real ceiling on how far back a diff can read, which turned out to be 75 minutes by default and is now a day on the decision tables.
Agent Skills Repo
We consume the published skills for schema and index design, and wrote one in return for serializable agent intents — skills/managing-long-running-agent-transactions. It ships in this repo and is not upstream; we would rather say that than imply a merge that has not happened.
AWS — only what we run on
Amazon Bedrock
Titan Text Embeddings V2 for the vector path, and Claude on two caller-selectable tiers — pass `adjudicator` on a commit, named by role rather than model id so your code survives the id moving. The cheap tier is the default because the provenance graph has already narrowed the question before a model sees it.
AWS Lambda
Two functions. The public API declares intents, commits and streams the demo. A separate SQS worker adjudicates off the queue with partial-batch failure reporting and its own concurrency.
Amazon EventBridge + SQS
A CockroachDB changefeed on commit_log posts to the API, which publishes to EventBridge; a rule routes to SQS, and the worker adjudicates in parallel. Commits return as soon as they are durable instead of blocking on everyone they threatened. Three retries, then a dead-letter queue.
Amazon S3 + CloudFront
Hosts this page as a static export with a private origin — CloudFront-only read via Origin Access Control, no public bucket policy.
Amazon CloudWatch
The logs, which caught a cold-start failure and a cross-region IAM denial during build.
AWS Budgets
A $20 monthly cap on the account, alerting at 50%, 80% and 100%. Alert-only by design, because Budgets can only email — the enforcement that actually refuses to spend sits in the API handler, ahead of every Bedrock call.
AWS IAM
A runtime role scoped to nine specific model and inference-profile ARNs rather than bedrock:*, with explicit denies on deleting evidence and altering model access. Nine rather than two because a us. inference profile dispatches across regions, so least privilege means naming the underlying model in every region it may route to.
Use it
A referee for your agents, not a place to run them
You keep your models, your prompts and your tools. INTERLOCK arbitrates the state two agents both touch — and nothing else.
You keep
- Your models, prompts and tools.
- Your framework — LangChain or none.
- Your data. We never see your agent’s reasoning.
You get
- A referee for state two agents both touch.
- A ruling naming which steps died, not just that something did.
- Serializable commits, an audit feed, and the bill for each decision.
You do not get
- Model access. This is not an inference proxy — you cannot send it a prompt.
- Somewhere to host or run your agents.
- A vector store you query directly.
Your agent today
// your agent, today
const state = await db.getQueue("support-eu"); // read
const plan = await llm.plan(state); // think, 40s
await db.setQueue("support-eu", plan.result); // actReads at t=0, acts at t=40s. If anything moved in between, this overwrites it and nothing raises an error.
The same agent, refereed
// the same agent, refereed
const q = await il.registerResource({ key: "support-eu" });
const plan = await llm.plan(q.body); // your model, untouched
const { intent } = await il.declare({ // + say what you're doing
agentId, statement: plan.summary,
reads: [{ resourceId: q.id, observedVersion: q.version }],
steps: plan.steps,
});
const { adjudications } = await il.commit({ // + commit through us
agentId, intentId: intent.id, resourceId: q.id,
expectedVersion: q.version, body: plan.result,
statement: "Rebalanced the overnight rota.",
});
for (const a of adjudications) { // + act on the ruling
if (a.verdict === "invalidating") redo(a.affectedSteps);
}The llm.plan call is still yours. Three additions: say what you are about to do, commit through us, act on the ruling.
Start here
The first three take about a minute and need nothing but a terminal.
See that it is real
No key needed. Returns the live topology, the survival goal, and how much quota is left today.
# no key needed curl -s https://wpvk3ox2bxo2w3zhxmx54ssjf40rakuz.lambda-url.us-east-1.on.aws/v1/healthWatch a real conflict get adjudicated
Creates real rows, embeds a real statement, and calls a real model. The response is the full trace, including what it cost.
curl -s -X POST https://wpvk3ox2bxo2w3zhxmx54ssjf40rakuz.lambda-url.us-east-1.on.aws/v1/demo \ -H 'content-type: application/json' -d '{}'Point your own agents at it
A key gives you an isolated tenant. No other caller’s commits can adjudicate your intents — the tenant filter sits inside the detection query, not around it.
Run the whole loop in one command
Two agents, one queue, a real ruling — declared, contended and adjudicated, printed step by step. Needs Node and nothing else: no database, no AWS account, no configuration. Pass your key, or leave it off and it issues a throwaway one.
git clone https://github.com/usv240/interlock && cd interlock && npm install npm run quickstart -- ilk_your_key_here
Already on LangChain? Add a callback.one wrapper, no rewrite
A guard, not a rewrite
import { InterlockCallback } from "interlock/langchain";
const guard = new InterlockCallback({ apiKey, agentId, resources });
await executor.invoke(input, { callbacks: [guard] });
// names which steps died, not just that something did
if (guard.wasInvalidated) redo(guard.stepsToRedo);Tool calls become plan steps as they happen, which is what lets a conflict be repaired at step granularity instead of throwing the task away.
Full API reference8 endpoints
Every endpoint
POST /v1/keysno quota
Self-serve. Creates an isolated tenant and returns a key, shown once.
GET /v1/healthno quota
Topology, survival goal, time-travel reach, and quota remaining.
POST /v1/agentsno quota
Register an agent. Idempotent by name, so a fleet can call it on every boot.
POST /v1/resourcesno quota
Register a piece of shared state for agents to contend over.
POST /v1/intents
Declare what an agent is about to do, and what it read, before it acts.
POST /v1/intents/stepsno quota
Add steps as a plan unfolds — for agents that discover their plan while working.
POST /v1/commits
Commit a change. Returns a ruling for every agent the commit threatened.
GET /v1/adjudicationsno quota
The audit feed — recent rulings, read-only.
Your agents keep their own reasoning and their own tools. INTERLOCK arbitrates only the shared state — declare an intent, commit through it, and act on the ruling.
Resilience
Kill a region mid-adjudication
If the memory arbitrating conflicts is unavailable, every agent in the fleet is unsafe at the same moment. So the test is not whether it works — it is whether it keeps working while a region is being taken away from it.
An agent whose memory goes offline does not degrade gracefully. It stops. That is the premise of this hackathon, and it is the one claim a demo can actually prove rather than assert.