Curriculum/Multi-Agent Systems
One Writer Per Record
Two roles mutating the same customer is a distributed race with a chat UI. Lock per record, or give writes to one apply role. Children never take the lock.
Single writer per resource: one role may call refund on a given customer_id in a job. Everyone else gets locked. Two roles mutating the same customer is a distributed race with a chat UI. Isolation by tool name is not enough: billing’s refund and loyalty’s credit are different names and the same ledger.
The failure-modes box set both flags true and called it bad. This lesson is the lock. The swarm write barrier said children never write. Here even two parent-level roles cannot both mutate c1 in one job. After an incident, add a fixture that replays two workers with the same customer id and asserts the lock. If your team says “that was a one-off,” it will recur with a new persona name.
Locks are not chats about “please let me finish.” Locks are a map customer_id → owner role on the job (or in the ledger service). The second caller fails closed. A prompt that says “if another agent is working, wait” is not a lock. Models do not wait. They write.
This is the same family as the swarm write barrier, aimed at roles instead of children. Children were never allowed write names. Here billing and loyalty are both “real” specialists with legal tools — and they still cannot share a row. If your org chart says they must both touch the customer, they propose on the blackboard and a single apply role writes. That is sequential subgraph plus one writer, not a mesh of courtesy.
What to lock
Lock the record, not the whole company. Billing may own c1 while loyalty writes c2. A global mutex would serialize unrelated customers and teach people to bypass the lock.
Lock writes, not reads. Docs may read policy while billing refunds. Reads still obey tenant ACL (evals track). This lesson is the double-spend / double-edit class.
Lock for the job (or for a TTL). After the job, a later job may assign a new owner. If two jobs overlap on c1, the ledger’s idempotency key or a longer-lived lock must exist — Tools track. Multi-agent adds role to that story.
| First writer | Second writer, same id | Result |
|---|---|---|
| billing refund c1 | loyalty credit c1 | locked, owner billing |
| billing refund c1 | billing email c1 | ok, same owner, again |
| loyalty credit c2 | (c1 still billing) | ok, different record |
Billing owns c1. Loyalty is locked. Different customers stay free.
One writer per recordSame owner writing again is allowed in the toy so billing can email after refund. If email should be a different apply step with its own key, still fine — owner matches. What must not happen is loyalty slipping in.
Files: one writer per path in a job. Two coders on main.py is the swarm-of-editors bug. Merge proposals in prefixes, parent applies one hunk stream. If you cannot merge hunks automatically, you do not have a swarm of editors. You have a sequential coder. Ship that.
HITL does not replace the lock. A human who approves two different roles’ writes on the same id still gets a race if those writes run concurrently. HITL then one apply is the shape. HITL then two applies is a slower race.
Walkthrough: c1 billing, loyalty denied; c2 free
Empty LOCKS. Billing refunds c1: ok, owner billing, map now holds c1.
Loyalty credits c1: locked, owner billing, role loyalty. World should not take the credit (the toy only returns an error; production dispatcher must not apply).
Billing emails c1: ok, again true, same owner.
Loyalty credits c2: ok, owner loyalty. Different customer is free.
Run to execute this in your browser. Nothing is sent to a server.
What printed: billing refund c1 ok. Loyalty credit c1 error locked. Billing email c1 ok again. Loyalty credit c2 ok. Loyalty cannot credit after billing owns c1. A different customer c2 is free. Billing may write again on c1.
The global LOCKS dict is a classroom stand-in. In a real worker, pass the lock map on the job so tests do not leak across tickets. Reset per fixture.
If you clear the lock when billing errors, loyalty might sneak in. Prefer: owner stays until job end, even after a failed write, unless a human releases. Failed refunds that release locks are a footgun.
Children and apply
Combine with the swarm write barrier: children never take the lock. They cannot call write names. The parent apply role is the owner. If reduce says “sev3 on c1,” apply runs as billing (or a dedicated apply role — even cleaner). Dedicated apply is one writer by construction. Then billing and loyalty both propose, apply decides. That is sequential subgraph plus a lock that is almost unused because only apply writes. Use it anyway: defense when someone adds a second apply.
Dashboards should fire on two writers to the same id in one trace — even if the second was denied. Denied means the prompt is trying. Granted twice means the lock is broken.
How agents use this
After an incident, add the two-worker fixture. Name it after the ticket. If the lock test is skipped because “we merged loyalty into billing,” that merge is the split-or-merge lesson winning. Good. Keep the fixture: it should still pass with one role.
Never fix two-writers with “the critic will catch the double credit.” Critics do not write, and they may run before the second write. Locks are runtime. Critics are packets. A critic that reads the ledger after the fact is an audit, not a mutex. You want both: lock so the second write never happens, eval so a lock bug still fails CI.
Name the resource in the lock key: customer:c1, path:src/app.py, ticket:T-9. Over-broad keys serialize unrelated work and get bypassed. Over-narrow keys (locking only the refund tool name) miss store credit. Test both mistakes.
When not to swarm, next: if items share a customer, you do not map-write. You map-score, reduce, one apply. That sentence is this lesson plus the write barrier.
Check your understanding