The setup: a demo repo that hangs
kvstore is a small demo repo the OmniRush team keeps for exactly this kind of test. It is a toy key-value store that spreads accounts across buckets, with one lock per bucket. A method called transfer() moves a balance from one account to another, and total() adds everything up.
Next to it sits stress.py, a script that starts several workers, has each of them make random transfers, then checks the total at the end. Most of the time it finishes. Sometimes it just stops, with nothing on the screen and nothing in a log.
That is the classic shape of a deadlock. Nothing crashes, so there is no traceback to paste into a chat. Whoever debugs it has to work out which threads are waiting on what, which is slow work for a person and a good test of whether an agent really reads code or just pattern-matches.
Turn one: diagnosis only, in the terminal
We started in the OmniRush CLI, in the kvstore folder, and asked for a diagnosis before any fix. That is a habit worth copying: a fix for a bug nobody has explained is a guess, even when the tests pass afterwards.

The answer opens with the location: the deadlock is in Store.transfer() at store.py:25-26. transfer() acquires the source bucket's lock first, then the destination bucket's lock, so two transfers in opposite directions take the same two locks in opposite order.
Then it shows the interleaving as a table, which is how you would draw it on a whiteboard:
| Step | Thread A: bucket 0 → 1 | Thread B: bucket 1 → 0 |
|---|---|---|
| 1 | Acquires lock 0 | |
| 2 | Acquires lock 1 | |
| 3 | Waits for lock 1 | |
| 4 | Waits for lock 0 |
Neither thread can release its first lock, so both wait forever.
A fix for a bug nobody has explained is a guess, even when the tests pass afterwards.
It proved the theory instead of asserting it
The part we liked most is the next line. The agent did not stop at a plausible story: it forced this exact interleaving with a barrier, so both threads were guaranteed to take their first lock before either tried the second. Both thread stacks were then blocked at store.py:26.
That turns “this could deadlock” into “this does deadlock, here”. A random stress run might need hundreds of tries to hang. A barrier makes it hang every time, which is also what a good regression test needs.
It explained the symptom, too
The agent connected the cause back to what we saw. stress.py's workers transfer between buckets in both directions at random, and they do not need to touch the same accounts to collide, only the same bucket pair. That is why the hang was rare and random, and why it looked unrelated to any particular account.
It found a second path to the same bug
Then it flagged something we had not asked about: a transfer() and total() variant. total() acquires locks in ascending bucket order (store.py:33-34), so it can hold lock 0 while waiting for lock 1 just as a transfer holds lock 1 and waits for lock 0.
It was also careful to say this was not the cause in this stress script, since stress.py calls total() only after all the workers finish. Being precise about what is and is not the cause is exactly what you want before anyone touches the code.
The fix: one lock order everywhere
The proposed fix is the textbook one, applied carefully: acquire the two bucket locks in ascending bucket order everywhere, while keeping the original a and b for reading and updating the data. The screenshot shows the new code; the before side is our sketch of the original, from the agent's description:
a, b = self._bucket(src), self._bucket(dst)
with self._locks[a]:
with self._locks[b] if a != b else _null():
# balance checks and updates using a and ba, b = self._bucket(src), self._bucket(dst)
first, second = sorted((a, b))
with self._locks[first]:
with self._locks[second] if first != second else _null():
# Existing balance checks and updates, still using a and b.Because every caller now takes locks in the same global order, the cycle in the table above cannot form, and transfer() agrees with the order total() already used.
One more detail shows the agent read the code rather than a textbook. It pointed out that the existing same-bucket guard is correct and must remain, because these locks are non-reentrant: a transfer between two accounts in the same bucket would otherwise try to take the same lock twice and block on itself.
Turn two: a swarm, in the desktop app
With the cause agreed, we opened the same kvstore project in the OmniRush desktop app and asked for the fix as a swarm, with each agent given one job:
Run a swarm on this: one agent reproduces the deadlock, one audits every
lock acquisition, and one writes the fix with a regression test. Then give
me one summary.
The turn worked for 10m 57s. Three sub-agents ran under it: Reproduce deadlock (7m 50s), Audit lock acquisitions (4m 21s) and Implement fix and test (8m 36s). Their times add up to almost 21 minutes inside an 11-minute turn, because they ran side by side.
The summary is short enough to read in one go:
- Root cause: transfer() acquired source then destination locks in arbitrary order, so opposite transfers could deadlock; total() already used ascending lock order.
- Fix: acquire transfer locks in sorted bucket order.
- Regression test: a deterministic lock-order and balance test in test_store.py.
- Verified: python -m unittest -v, 1 passed; pytest -q, 1 passed; python stress.py completed with total = 50000.
- Changed files: store.py and test_store.py, left uncommitted.
$ python -m unittest -v # 1 passed
$ pytest -q # 1 passed
$ python stress.py # completed, total = 50000Note the last line of the summary. The swarm left the changes uncommitted, so the diff was ours to read and commit. That is the right default for any agent: it does the work, and you decide what goes into history.
It does the work, and you decide what goes into history.
Why the swarm split works for concurrency bugs
Each of the three jobs checks the others. The reproducer gives the fixer something to run. The auditor looks at every lock, not just the one in the bug report, which is how a second path like the total() variant gets caught. The fixer has to make the reproducer's case pass.
With one agent, the same work happens in sequence, and the audit is the step most likely to be skipped once a fix looks done. Splitting it makes the audit a job of its own. You can ask for a swarm in any task you describe in the desktop app.
How to prompt an agent for a hang or a race
None of this needs a special tool, only the right order of questions. What worked here, and what we now do for every concurrency bug:
- Give the symptom and the command: “stress.py sometimes hangs; run it with a timeout to see it.” Name the file if you know it.
- Ask for a diagnosis only, with proof: the exact lines, the interleaving, and a way to force it (a barrier, an event, a sleep in the right place).
- Read the explanation. If it does not account for the symptom you saw, push back before any code changes.
- Ask for the fix with a deterministic regression test, not just a passing stress run. Random runs pass by luck.
- Ask it to audit every other place that takes the same locks. The second path is often where the next hang lives.
- Review the diff and commit it yourself.
What the agent did not do
It is worth being clear about the edges of this demo. kvstore is small, the bug is a known pattern, and the fix is one idea. Real codebases have locks hidden in libraries, async code and database transactions, and an agent can miss those like anyone else.
What made the difference here was not magic but method: name the lines, prove the interleaving, explain the symptom, test deterministically. You can hold any agent to that standard, and you should.
Clients and availability
Both clients run the same agent. The CLI installs with npm i -g omnirush@latest (Node.js 22.19 or newer) on Windows, macOS and Linux; the desktop app, with swarms, is for Windows (x64), macOS (Apple silicon) and Linux (x64 and arm64, as .deb, AppImage or Arch package). Models are GPT 6 Astra (the default), GPT 6.1 Sol and GPT 6 Sol, included within a daily allowance, and sign-up needs a GitHub account at least 7 days old.
OmniRush is funded by brand ads, and data use described in its privacy policy. Use it on personal and open-source code, and keep secrets, customer data and anyone else's confidential code out of the project folder.
Questions
Can an AI coding agent debug a deadlock?
In this demo, yes: it named the lines, forced the interleaving with a barrier to prove it, and a swarm then wrote a fix with a deterministic regression test. Ask for a diagnosis with proof first, and always review the diff.
What is a swarm in OmniRush?
Several agents working on one task side by side in the desktop app, each with its own job, handing back one summary. Ask for one in the prompt, as in “run a swarm on this”.
Is kvstore a real project?
It is a small demo repo the OmniRush team keeps for testing. The screenshots in this post are from real runs on it.




