Separation is two air-traffic controllers issuing orders that are safe alone but deadly together. Both clearances are held in an airlock while still forming. FLOW can object mid-turn and APPROACH’s descent is rewritten before it executes.




What it does
Two agents write disjoint state, safe alone, together breaking an invariant neither touched. Databases named this write skew in 1995; serializable snapshot isolation catches it. Agent runtimes don't: validating at commit can't see a decision that isn't written yet. SEPARATION is that hazard in a Mozaik TRACON sector: a descent moves only the vertical, a turn only the lateral; together, 2.3511 NM, 397 ft, 34.54 s of lost separation. Serializing misses the window both ways: 9446 and 8770 ms. An airlock holds both half-formed clearances mid-turn and rewrites one argument: 4000→7000 ft. 600 runs: 100 losses in both control arms, 0 with the interlock; 100/100 hazardous seeds altered, 0/100 safe.
How the agents run concurrently
The airlock is a Mozaik InterceptionHandler on commit_clearance calls. Mozaik awaits interceptionHandler.handle() inside the agent loop, so awaiting suspends that agent alone, mid-turn — the only interval in which a peer's half-formed intent can change its mind. Concurrently: three LLM controllers with incommensurable objectives (APPROACH, DEPARTURE, FLOW), ten LLM pilots with private constraint sheets, non-LLM TCAS reflexes at 20 Hz, and a world that gates no one, absent from the model registry. Both controller turns stay open 57.78 s (fixtures/trace.json); when FLOW's objection lands, TurnScheduler.inflight() holds both turnIds — asserted at tests/live/mind-change.test.ts:153, printed as INFLIGHT AT THIS MOMENT: 2. The sequential ablation arm shows content-differs 0 by construction: no peer intent exists while a decision forms. Shared state, on Mozaik's RuntimeState: a VersionedCell per aircraft's standing, and the desk's PendingSet — the airlock, the only genuinely multi-writer cell. Standing is multiply held by design: APPROACH holds AAL221/runway-sequence while FLOW holds AAL221/metering-interval, at once, legally. Every mutation goes through casWrite(cell, expectedToken, mutator, …); a stale write is rejected and announced as cas.rejected. What is not shared is context: each turn owns a fresh ModelContext, because addContextItems mutates in place and one Memory across two live turns hard-400s the provider. FunctionCallItem.rehydrate replaces the held call's args — a narrowing, not a veto — so it executes and lands in APPROACH's own memory: targetAltFt 4000→7000, "that descent crosses my metering block at CARDL".
Newsletter
For developers who want to learn how to build self-organizing agents.
3alet Hackathon · 30–31 October, Novi Sad + online. Registration is open. 60 seats on site.
Register