Audit, 29 July 2026
Thirty findings, each verified against the source and put to a skeptic told to refute it.
Marshall audit, 29 July 2026
Type: HISTORICAL DEBRIEF
Validated against: 29 July 2026, tree at 482a71b
A dated audit. Its findings are issues now — do not work from this file.
Two things before you fly
1. The paper nav log is 5.74 degrees out on every leg. Today. route.py knows
about grid convergence — grid_convergence_deg = 5.74 — and applies it on the radar
side (agent_atc.py:780). bearing_distance (route.py:1235) does not: it takes
atan2(dz, dx) straight off the sim's grid coordinates, and solve_route labels
that as TRUE course and converts it to magnetic for the kneeboard. Measured on the
legs actually on the chart:
| leg | chart says | should be | cross-track error |
|---|---|---|---|
| KOBULETI to INITIAL | 236 M | 242 M | 2.39 nm over 23.9 nm |
| INITIAL to BATUMI | 125 M | 131 M | 1.50 nm over 15.0 nm |
The identical bug in the radar half already cost a night, and the comment at
route.py:1158 documents that fix — it was applied to one of the two consumers.
2. A wingman has no radar position, and that one is mine, from last night.
identity.flatten_formation, added hours before this audit ran to make formations
parseable at all, strips the wingmen out of a formation line before the position
regexes see it. So radar_fix_by_track returns None for anyone who is not the
formation lead:
lead fix: Position(range_nm=13.5, radial_deg=307.0, ...)
wingman fix: None
The window is narrow — a man who has verbally broken out but is still inside the
2 nm cluster threshold — which is exactly the case the flight model was built for,
and exactly what a two-pilot session flies. The honest fix is for _render to print
each wingman's own range and radial so flatten_formation can expand a formation
into real contact lines; substituting the lead's position would be up to 2 nm wrong.
I have fixed neither. The audit's own central finding is that fixes get applied where the bug was found and not at the sibling call sites. Changing navigation maths and the scope contract unsupervised at five in the morning is that mistake wearing a different hat. Both are yours to call.
Is the naming family really the root?
Partly. The evidence supports a weaker and more useful statement than "naming was the root cause".
Eight findings are literally a name-keyed lookup asking the wrong question, and four of the six worst are in that group: release_stale comparing a spoken callsign to a player name (agent_atc.py:460), separation_context asking the scope by callsign five lines after the comment explaining why that finds nobody (agent_atc.py:1763,1771), flatten_formation deleting the wingmen so no formation member ever has a radar position, and flights.track_name holding a label while four SQL consumers join on the unit name. That is not a story the team is telling itself. It is in the code, and it is where the severity is.
But naming is the material, not the mechanism. The recurring shape across all twenty-seven is narrower and more actionable: a fix gets applied where the bug was found and not at the sibling call sites, and nothing catches the misses. flatten_formation is applied at five scope readers and forgotten at the sixth. The by-track conversion landed at agent_atc.py:1755 and :3167 and :3440 and was skipped at :1763, :1771 and :3234 — with the comment at :3437 saying "By track, for the same reason everything else is", which the author believed. Migration 013 diagnosed the label-vs-name split perfectly and patched one of five consumers. radar_live has fallen three features behind _render. That pattern accounts for more of the list than naming does, and it has a single cause: _run_srs — 1,167 lines, every ordering decision in the bridge — is executed by zero tests (coverage reports 2484-3651 missing). The helpers are individually well tested and green; the wiring that calls them is not tested at all, and every one of the top findings is in the wiring.
So: the naming diagnosis is correct about what the bugs are made of. It is incomplete about why they survive. Fixing the names without putting a test on the receive loop buys one clean sortie.
1. Which name is this lookup keyed on
The dominant theme, and where the severity is.
1.1 release_stale's radar test can never fire, and the one evidence path left is latched — so a departed pilot's board entry is immortal
src/marshall/atc/agent_atc.py:460,467, :2666 (caller, every 2 s); controller.py:320-330, :111.
here = {_key_name(u.name) for u in identity.units_on(scope)} — u.name is the printed radar name, a player name or DCS callsign. cs is a board key: a spoken callsign, a handle, a flight name. Run on 362nd_sockeye [Pony 1-1] (P-51D, manned): 8.0 nm ..., here == {'362ndsockeye'} and both _key_name('Pony 1-1') and _key_name('Sockeye') are absent — while identity.handle('362nd_sockeye') == 'sockeye' and agent_atc._track_of:511-516 already does exactly that conversion for the same job. The radar clause is dead. That leaves ac.radar_identified, which is written only at :1761 for the callsign on the current transmission, so a leftover entry is frozen at whatever its last call set. radar_identified or ... then refreshes _seen_at[cs] on every 2-second tick and the 8-minute timer never expires.
Cost: the function exists for one documented outage — a pilot flew as Falcon 1-1, left, came back as Pony 1-1, and the leftover entry made the engine engage ("assigned ten thousand, held at five, banished to Kobuleti, all over the top of correct vectors"). That entry was radar-identified while he was flying, so this cannot remove the entry it was written for. It also removes the only escape from 1.2 below. The test that appears to guard it (tests/test_events.py:288-294) hand-writes a scope line whose printed name is the callsign, which production never produces.
Skeptic narrowed: entries whose last transmission was not radar-correlated do still age out. The immortality bites exactly the aircraft that were being worked properly.
Fix: build here as {_key_name(identity.handle(u.name))} ∪ {_key_name(u.callsign)}, and stop treating a stale radar_identified as evidence.
1.2 separation_context asks the scope by callsign for the on-final seed — the seed the stacking bug was written for
src/marshall/atc/agent_atc.py:1763,1771; contrast :1755-1761, which was fixed; track is a parameter of this same function (:1609).
The comment at :1747 states the rule — "BY TRACK. This asked the scope for the CALLSIGN, and the picture labels a manned contact by player name — so unless something had already tagged the line, radar_fix found nobody." It was applied to the radar-contact flag only. Two lines later aircraft_type_on_scope(scope, intent.callsign) and radar_fix(scope, intent.callsign, ctl.profile) still take the callsign, and both _TYPE (:733) and _FIX (:724) open with \[([^\]]+)\]. The bracket is emitted only for a contact the agent has already bound via identify (tracks.py:496), so an untagged manned contact — every pilot from check-in until the agent's first successful identify() — is invisible to both.
Cost: fix is None, so Controller.seen_on_final never runs, and an aircraft radar shows established on the approach is filed as a fresh arrival and stacked. That is verbatim the incident in migrations/004_flights.sql's header: "a flight established on final at ten miles checked in, was filed as a fresh arrival, and told to climb to five thousand and hold while the other half was talking it down." The window is widest at the moment the pilot arrives, and one AI blip on the scope is enough to have the engine engaged.
Skeptic corrected: the equipment half's cost is not "assume modern". controller._hold_phrase:423-430 treats kit is None as ABLE on purpose at a beacon field. The real consequence of the missing note_equipment is the inverse — an aircraft that cannot receive the navaid is offered "hold at BATUMI as published". Severity is carried by the seed, not the equipment call.
Fix: radar_fix_by_track(scope, track, ...) or radar_fix(scope, intent.callsign, ...), the idiom already used at :1309 and :3167.
1.3 The track-first radar fix is computed, then thrown away and recomputed by callsign
src/marshall/atc/agent_atc.py:3167 (correct), :3234 (rebind), :3246, :3252, :3256, :3641-3646.
At :3167 the loop does the right thing with the comment saying why: "BY TRACK FIRST. The callsign is a label and can be stale; the track is the sim's own name for the aeroplane this radio is sitting in." That value is used only for the record() line. Sixty lines later the same name is rebound to radar_fix(scope, known, profile) if known else None — callsign only. Everything on the guidance path reads the second one: _g at :3252, reconcile(...) at :3256, and the _g at :3641 handed to hush_a_second_talkdown.
Cost: with a stale or unapplied label, _g is None, so reconcile returns at :1432 with everything unchanged and both the holding directive and the ASR talkdown are appended to the prompt — precisely the failure reconcile exists to prevent ("told, in one transmission, that he was on final AND to climb to five thousand and hold"). guide=None also disables hush_a_second_talkdown (:1215), so with a stale tag the metronome flies "[Falcon 1-1]" while the agent answers "Pony 1-1" — identity.py:379's "two brains working two different aeroplanes".
Skeptic corrected two overreaches: the handoff path at :3440 reads the correct track-first value, so the defect is confined to guidance/reconcile/hush/track-bind. And the callsign-only lookup gating track_name at :3246 is deliberate (:3231-3233: binding a guess "would attach one aeroplane's history to another's"); the narrower defect there is that the gate demands confirmation from the label, which this codebase has already decided is the unreliable half, when a by-track hit is stronger confirmation.
Fix: hoist one track-first _fix/_g pair and pass it to the record, the bind, reconcile and interact.
1.4 flatten_formation deletes the wingmen, so no formation member ever has a radar position
src/marshall/atc/identity.py:89-106; agent_atc.py:785-825, :1758-1761, :3218; director/tools/tracks.py:440-462, :496.
_FORM_SPAN.sub(": ", scope) removes everything from "IN FORMATION with" to "lead ", which is where the wingmen's names are. Executed:
radar_fix_by_track(scope, 'Enfield11') -> Position(8.2, 332, 6004, ...)
radar_fix_by_track(scope, 'Enfield12') -> None
identity.units_on(scope) -> [Unit('Enfield11'), Unit('Enfield12')] # identity CAN see him
Two producer gaps behind it: _other_ship never consults bindings, so a wingman line can never carry the [callsign] tag _FIX requires; and it prints only name/type/manned/on-ground/offset — no radial, altitude or heading — so even an un-flattened parser could not build a Position for him.
Cost: DESIGN.md:143 says "any member who transmits is the flight talking", and identity resolves that radio to the member's own unit name, which :3218 hands to separation_context, which does seen = radar_fix_by_track(scope, track) is not None → False → note_radar_contact(cs, False). Whenever a wingman keys the mic, the deterministic engine is told the aircraft is not radar contact; the lead's next call flips it back. The recorded symptom is already in the comment at :1754: "im not sure batumi approach ever REALLY had me on the approach until the very end."
Skeptic corrected: radar_fix handing a joined wingman the lead's geometry is intended (:876-887) and is not part of this. Cost lands only on vectored approaches (may_be_sequenced returns True unconditionally when not vectored), and the real bite is on members checking in individually right after _break_up asks them to, while they are still inside FORM_NM/FORM_FT and radar is still collapsing them.
Fix: have _render emit one line per aircraft plus an explicit formation: <lead> field, and collapse the controller-facing prose separately from the machine-facing picture. Short term, have radar_fix_by_track fall back to lead position plus offset, the way _scope_geometry already does.
1.5 radar_range_for reads a wingman's offset as the flight's range, and matches on flight rather than aeroplane
src/marshall/atc/agent_atc.py:717 (_RANGE), :1374-1388, :1726-1734 (only caller).
Two bugs, one function, both proved by execution against a legal _render line:
radar_range_for(scope, 'Pony 1-1') -> 0.3 # the wingman's formation offset
radar_fix(scope, 'Pony 1-1').range_nm -> 8.2 # the lead's real range
radar_range_for(scope, 'Falcon 1-2') -> 12.4 # his flight-mate's range
It is the only radar reader in the file that never calls identity.flatten_formation (the other five are at :576, :744, :817, :888, :933), and :1384 matches on C.parse(cs).flight — the exact word radar_fix's comment block was corrected away from ("THE AEROPLANE, NOT THE FLIGHT. Falcon 1-1 and Falcon 1-2 ARE THE SAME FLIGHT").
Cost — latent, not live. The sole consumer is the station-passage guard at :1728, gated on beacon_flown = not profile.vectored, and the shipped default is BATUMI_ASR (kind="asr", vectored). So on the current profile the call is pure waste. On any beacon-letdown profile — the 1944 flavour, which is where DESIGN.md:171-174's "a flight called 'over the beacon' at eight miles" incident happened — the guard silently passes for a tagged formation at any distance, and the engine acts on the report.
tests/test_bridge.py:190-197 does not guard it: its wingman carries no (...) parenthetical, so there is no earlier "nm" to steal. The renderer grew that parenthetical (tracks.py:461) and the fixture was never updated.
Fix: identity.flatten_formation(scope) in the findall, copy radar_fix's exact-canonical-first matching, and update the fixture so it can fail. Better: delegate — f = radar_fix(scope, cs); return f.range_nm if f else None.
1.6 flights.track_name holds a radar LABEL; four consumers still join on tracks.name — migration 013 fixed one of five
director/migrations/004_flights.sql:159, 007_tasking.sql:101, 013_join_a_track_by_either_name.sql:40; director/tools/clearance.py:171-176, events.py:180; tracks.py:163; identity.py:446; agent_atc.py:3246.
Chain proved end to end: _upsert sets label = u.player_name or u.callsign or u.name; _render prints the label; units_on parses it into Unit.name; resolve() builds Identity(label, u.name, ...); :3246 writes it into flights.track_name. So for a human, track_name is 362nd_sockeye and tracks.name is Testbed 1-1. Migration 013 diagnoses this in the team's own words and widens flight_airspace to ON t.name = f.track_name OR t.label = f.track_name. It did not touch the others.
Cost — one live, three latent. Live: clearance.aircraft_type (:171-176) does SELECT type FROM tracks WHERE name=%s with the label, returns None for every manned aircraft, and plans.nav_of(None) returns 'dr' — so flight_plan_help tells the controller an F-16 pilot needs dead-reckoning help, position reports and vectors. Latent: flight_state (004:159) leaves radar_identified, observed_alt_ft, geog and alt_error_ft NULL for every manned flight — but nothing reads those columns (flight_strip deliberately carries no position, and tests/test_flight_state.py:63-70 pins that). events._forget_slot matches nothing, but the track row is gone by then, so the residual harm is a flights row keeping a person-stable track_name. Refuted: tasking_state is unaffected — target_alive keys on target_name, and neither taskings nor tasking_state has any Python reader in the repo.
Fix: one widening, WHERE name = %s OR label = %s, applied in the same three places, or a single tracks_by_either_name view everything routes through. Grep = f.track_name and WHERE name= over tracks before closing.
1.7 asr_monitor passes a spoken callsign where is_on_the_ground() expects a sim track name
src/marshall/atc/agent_atc.py:2809, :2783, :923-939, :1964-1979; tests/test_events.py:230-259.
radar_fixes() yields _FIX group 1 — the bracketed tag, "Pony 1-1". asr_monitor binds it to cs and passes it as the track argument. is_on_the_ground matches _key_name(u.name), the printed unit name (362nd_sockeye). "pony11" never equals "362ndsockeye", so the sim's land/takeoff event branch cannot fire. The other two call sites are right (:1332 passes track or cs, :3462 passes _ident.track), and the function's docstring says it exists as "one function so the two callers cannot drift".
Cost: the sim's authoritative land signal is dead in the one thread that says goodbye. Skeptic corrected the headline — the 19-second-early goodbye does not return, because the geometry fallback now needs alt<200 AND speed<60 together. The accurate cost: the goodbye waits for the aircraft to decelerate through 60 kt instead of firing on touchdown, and during roll-out grounded is never latched so the map/missed branches at :2919-2932 stay live — the "Batumi Tower thinks I'm on the missed approach" shape the block at :2786 was written to end. The whole [ARCH-3]/#41 event work is inert on this path while a green suite says otherwise. Note it still works for AI contacts, whose printed label and binding key coincide; this is a manned-aircraft failure, i.e. the pilots.
Fix: have radar_fixes return the unit name alongside the tag and pass the track. The callsign stays what is spoken.
1.8 The identify → contacts → render loop compares model-typed strings byte-for-byte, and in_formation matches a label the renderer may have replaced
director/tools/identify.py:58-72, :101-120, :135-177; director/tools/tracks.py:377-413 (esp. :409), :416-437, :478-479, :496.
Two unconverted comparisons on one key. (a) _render prints naming.get(name, label) — on a label collision (_unique_labels' whole reason to exist: "two separate groups both came up 'Enfield11'") the picture shows the sim unit name. The agent passes that to identify(), which forwards it to in_formation(), which compares against the raw t.label column and matches nothing — so the guard at identify.py:168 ("A FLIGHT name may only be bound to a track that is ALONE") silently passes. The neighbouring _resolve in the same file (tracks.py:250-254) already does lower(label) = %s OR lower(name) = %s, so the file knows the two-key rule and this is the one place that forgot. (b) There is no normalisation anywhere on the loop: bind stores the model's string verbatim, bindings_for returns it verbatim, _render:496 does exact label in bindings — while identity.py:241-248 exists on the bridge side for exactly this ("case, spaces, underscores and squadron numbers are decoration that DCS and SRS decorate differently").
Cost: (a) is bypassed only on an actual duplicate label, which by construction is AI traffic, so the human wingman-answered-as-lead scenario quoted in identify.py's comment cannot reach it — call that half low. (b) a single case or whitespace variance makes bind() succeed and answer "Radar identified: ..." while the bracket never appears, and nothing is logged; the failure is indistinguishable from "the agent never identified him". Blast radius is the same surface as 4.3 — the call sites that still pass a callsign — not an independent one.
Related (noted, not ranked): in_formation's query has no ORDER BY while radar_cached has ORDER BY nm, and _clusters is single-linkage with an early break, so the same world can partition two ways in the same second. Narrow window; the cost is speculative.
Fix: resolve the model's contact through the same de-collided naming _render used, case-insensitively, before both in_formation() and bind().
2. The live loop: nets that never fire, two threads on one outbox
2.1 The bridge never calls ctl.tick() — every time-based safety net in the separation engine is dead code
src/marshall/atc/controller.py:1080-1111, :45-47, :1031-1037; agent_atc.py:2536, :1591-1606.
grep -rn "tick(" over src/, tools/ and tests/ returns three hits: the definition, the __main__ demo driver at controller.py:1171, and tests/test_controller.py:146/156. The bridge constructs Controller(profile) and never advances its clock, so ctl.t stays 0.0 for the whole sortie. Unreachable in production: the timed missed-approach-point backup (the only caller of _station_passage), the CLEARANCE_TIMEOUT_SEC deadlock breaker, and the REPORT_OVERDUE_SEC position prompt. CLEARANCE_TIMEOUT_SEC, REPORT_OVERDUE_SEC, map_t and _letdown_since are referenced nowhere outside controller.py. The module docstring and DESIGN.md:96-98 both advertise the timeout as the thing that stops a silent aircraft deadlocking the stack. It has never run.
Cost: an aircraft cleared for the approach that goes quiet — disconnect, crash, forgets to call — holds _letdown forever. _try_clear returns at its first line for every subsequent arrival, and may_be_vectored refuses to vector or make a mile call for anyone who is not owns_the_approach(). The field silently closes; the cockpit symptom is a controller that stopped talking.
Skeptic narrowed: the block is only permanent when the ghost is also radar-latched — release_stale → ctl.release does free _letdown. Finding 1.1 is what removes that escape for the common case. Also, the dead timed MAP is not a total loss on radar profiles; asr_monitor computes it geometrically. The clearance-timeout and overdue nets have no equivalent anywhere.
Fix: drive ctl.tick() from the scheduler thread (it already wakes every HOOK_POLL_SEC) with elapsed wall time, and route ctl.out through the same transmit path as the goodbye.
2.2 seen_on_final seizes _letdown from whoever is currently in it
src/marshall/atc/controller.py:611-638 (esp. :632, :637); agent_atc.py:1771-1779.
It guards only the calling aircraft's phase — if ac.phase in (CLEARED, LANDED): return False — then unconditionally does self._letdown, self._letdown_since = ac.callsign, self.t. It never asks whether _letdown is already somebody else. The contrast is in the same file: request_visual (:925) does guard. The caller fires for any transmitting aircraft whose fix reads established, and Guidance.established is phase in ("final","map"), which asr.py:553-588 reaches for anything within 60 degrees of the final course, inside the cross-track tolerance and inside final_intercept_nm (11 nm) — no altitude term at all. So aircraft B in trail at ten miles, or a holder on an inbound leg, takes the letdown from aircraft A at three miles.
Cost: A is left in Phase.CLEARED — nothing demotes him, and per :632 he can never re-seize it — so two aircraft are in the letdown block, which is the one thing the engine exists to prevent, and no state records the conflict because the invariant is a single string. A loses the metronome mid-approach: may_be_vectored now returns True only for B, so A's mile calls and descent instructions stop dead at three miles in cloud while B is vectored onto the same final. tests/test_controller.py:408-445 covers seeding twice for the same aircraft and a second aircraft still being held, never the case where the seeder is not the owner.
Skeptic narrowed: the lost-talkdown symptom is asr_monitor's and so is confined to vectored profiles. The state corruption happens on any profile where a radar fix exists.
Fix: if self._letdown not in (None, ac.callsign): return False.
2.3 The Controller is shared across three threads with no lock, and ctl.out is join-and-cleared by two of them
src/marshall/atc/agent_atc.py:2544 (radio_lock guards the radio only), :2988-2990, :1783-1785, :2841-2844, :2666-2670; controller.py:879-904.
One lock exists and it wraps client.transmit. ctl is mutated from the RX loop (separation_context → intents.dispatch), from asr_monitor (ctl.report_down), and from scheduler (release_stale → ctl.release). Both the RX loop and asr_monitor treat ctl.out as theirs alone: each does " ".join(...) then ctl.out.clear().
The deterministic half needs no race to be wrong. report_down ends in _try_clear() (controller.py:904), which composes the next aircraft's full approach clearance and possibly _step_down calls into ctl.out; :2842 joins all of it into bye, transmits it on final_hz regardless of the Tx.freq_mhz each clearance carries, and records it as kind="atc/landed", callsign=<the aircraft that just landed>. The frequency mismatch is real: say stamps each Tx with profile.station() — TOWER 118.000 on this profile — while final_hz is APPROACH 124.000.
Cost: on any multi-ship ASR landing, another aircraft's clearance and step-downs are spoken as a tail on the landing aircraft's taxi instruction, and the flight recorder attributes the whole thing to the wrong callsign — which makes the record useless for debugging exactly this. Under the race, asr_monitor's clear() between dispatch and its join deletes a clearance the engine has already committed: the pilot is CLEARED and owns the letdown but was never told. ctl.out.clear() sits inside the try at :2843, so an exception in for_voice leaves the queue dirty for the next landing.
Skeptic narrowed: the race is proved possible by construction, not observed, and the window is small. The deterministic half is the part to act on.
Fix: one lock around every ctl mutation plus the drain; have dispatch/report_down return what they produced instead of reading a shared outbox; transmit each Tx on its own freq_mhz.
2.4 The canned simple_response short-circuit runs before the engineering, ship-to-ship and flight-verdict gates
src/marshall/atc/agent_atc.py:3200-3216, :3294-3370, :3389-3393, :3406-3413, :89-115, :1855-1863.
canned = simple_response(transcript) transmits and continues at :3216. Every "is this transmission even for the controller?" test is below that line: the engineering summons handling, the ship-to-ship guard, the debug-note drain, and the challenge guard explicitly written to "NEVER SWALLOW A DECISION" — which already carries a not _flight_say check, proof the author cared about this ordering one gate lower. _ENG_CALL matches "engineering, radio check"; so does _CHECK (:84). I ran it: both return "Station calling, loud and clear."
Cost: a pilot's first act on the engineering channel is normally "engineering, radio check". He gets an affirmative in the controller's voice, on the controller's channel; engineering_line is never opened, nothing is written to debug-notes.md, and engineering_ack never runs — the failure it was written to kill ("a pilot transmitting into what he thinks is a live channel"), except worse, because the affirmative makes it look like it worked. simple_response also picks the first callsign-shaped token, so "Pony one two, Pony one one, radio check" answers the addressee — contradicting the "identified by his radio, not by the words" doctrine at :3608.
Skeptic added the precondition: the branch only fires when not engaged, i.e. a single-contact scope or a radar-off setup. That is exactly when a pilot is on the ramp making his first engineering call, so the window is the common case for this channel.
Fix: move the short-circuit below the engineering/ship-to-ship/debug gates, add the same not _flight_say guard, and make simple_response address known rather than the first name in the sentence.
3. State nothing clears
3.1 Nothing clears per-mission state on a mission change
director/tools/flights.py:261-267, :120-138; director/app.py:285-288; director/tools/events.py:70, :160-172; tracks.py:487-489; agent_atc.py:1983-2014, :3483-3488.
clear_mission exists and its docstring is right ("stale aircraft from the last sortie are worse than none"). DELETE /flights has no caller in src/marshall/ — only tools/handoff_check.py:149 and tools/plan_assign_check.py:138, both scoped to their own mission names. The live default mission is never cleared.
Cost, corrected by the skeptic — the stale rows do not reach the separation engine (that reads the bridge's in-memory Controller, fresh per process). The real cost is stronger: bind → _all_matching matches on track_name, which holds the player-derived label and is therefore stable across nights, so a returning pilot folds into last sortie's row, and flight_strip re-injects its cleared, assigned_ft and promised into the prompt with "do not ask him again for anything in it."
The stronger half: events._on_ground is in-memory, keyed on the raw sim unit name — which the mission builder reuses ("Testbed 1-1", "Pony 1-1") — and cleared only per-unit on player_leave_unit, never on mission load. The routine operation here is a gRPC hot-load with the director left up. A slot that landed last run can respawn airborne still flagged down; _render annotates "on the ground" and handoff_on_the_event hands an approach-controlled pilot to Tower on that annotation. SUSPECTED, not proved — it turns on whether DCS raises player_leave_unit when a hot-load evicts a client, which is not answerable from this source.
Two sub-claims were refuted and should be dropped: contacts.srs_name being write-only is by design (identity.py:441-443 re-runs the physical chain every transmission on purpose), and the per-channel session id scoping contacts by a 2-hour TTL is the documented, deliberate answer at identify.py:84-100.
Fix: one "mission loaded" entry point on the director that calls clear_mission and clears _on_ground, hit by the deploy script on every load.
3.2 The mission key is configurable on only one side of the seam, and the director's board has no expiry
director/app.py:112; director/tools/clearance.py:259-277, :316; flights.py:244-258; agent_atc.py:251, :319.
clearance_tools() is constructed with no argument, so mission is baked to "default" in every closure. The bridge writes rows with MISSION = os.environ.get("MARSHALL_MISSION", "default") and sends it on /flights/bind. Nothing in the repo sets that env var today, so this is a latent trap — but total and silent when sprung: every clearance request is answered not_on_the_board, and because F.callsigns("default") also returns nothing, the controller reads out "On the board: nobody — the board is empty" to a pilot who checked in thirty seconds ago. An env var that looks like a harmless label.
Secondary and real: flights.callsigns() is an unbounded SELECT DISTINCT ON (callsign) ... WHERE mission = %s, so the closed set not_on_the_board reads out loud grows across sorties. (Skeptic corrected: working() already excludes landed/filed, and clear_mission is documented in WIRING.md:267 as a between-sorties operation, not an unimplemented intention. The "two boards with two lifetimes" framing is also loose — the bridge's 8-minute board is the separation stack, flights stores what was agreed.)
Fix: pass the mission through to clearance_tools() from the same env var, and bound callsigns on updated_at.
3.3 _missed_count is a module global that is never cleared
src/marshall/atc/agent_atc.py:1456-1492, :472; controller.py:293-318.
_missed_count[key] = ac.approaches latches go-arounds per callsign and nothing ever removes the entry — not release_stale (which pops only _seen_at), not Controller.release, whose own docstring makes the point that the returning aeroplane is a different aeroplane. One prior go-around is enough: a new pilot flying as Pony 1-1 an hour later starts at approaches = 1, 1 > 1 is false, and the latch never sets.
Skeptic corrected the mechanism and most of the cost: note_missed is not restricted to on_missed-derived phases — asr.py:653 has a second out("missed", ...) in the "past the field and low" branch that takes no flag, and guide's docstring says so ("Geometry still starts it... The flag is only what makes him STAY on it"). So the quoted cockpit regression does not follow. What a stale count actually disables is the other entry path: the one that fires when the pilot says he is going around. That is the only path available in the window between his radio call and the geometry catching him, and the only path at all on non-vectored profiles, where asr_monitor returns immediately. Degraded latch, not absent. Same never-cleared pattern applies to _flying_missed.
Fix: clear both wherever the board entry goes away, or key on the flight row id rather than the bare callsign.
4. The radar picture: two producers, and what the parsers drop
4.1 The picture handed to the model includes tanks, trucks and ships — and that flips the separation engine on permanently
director/tools/tracks.py:231-234, :135-146, :162-185, :293-305, :326-374; director/tools/dcs.py:109, :148-161, :164-176, :203-207; agent_atc.py:683-698, :3195, :3477.
start_streamer deliberately subscribes AIRPLANE, HELICOPTER, GROUND and SHIP so the overlord can ask "is the target still alive" — the comment justifies it well. But _upsert does not persist which category the row arrived under and the DDL has no column for it, so the discriminator is discarded at the door, and radar_cached filters on last_seen alone. radar_live filters to _AIR. Primary path and fallback disagree about what a radar contact is, and the cache path is the one that runs.
Cost: count_contacts counts every | segment and N per "N ships", so engaged = ... or n_contacts >= 2 goes true with a single aeroplane airborne. That routes every transmission through separation_context → bedrock_intent.classify, a second Sonnet call benched at 2.2 s, and kills the canned short-circuit — defeating the design note at :3192 ("a single ship stays pure rich Sonnet ... no classify on the path") and the assumption written into bedrock_intent.py:26-28. A spawn_ground group of four vehicles 60 m apart also satisfies FORM_NM/FORM_FT/FORM_HDG and renders as one "IN FORMATION with ... — 4 ships", which count_contacts reads as four.
Skeptic corrected several overstatements. The invariant is not at risk: Controller.aircraft is populated from transcripts, never from the scope, and identity-by-elimination filters on manned, so unmanned rows cannot steal an identification — engaging early is the conservative direction. Token bloat is minor once clustering collapses them; the count inflation is the harm. Nothing in mission/build.py places ground or ship units, so this needs spawn_ground or a hand-built mission — a trap that springs on the first CAS sortie, not a bill being paid now. And whether a parked vehicle stays inside FRESH_SEC = 15 depends on DCS-gRPC re-emitting unchanged units, which is not verifiable from this repo — if it does not, only moving ground and naval units contaminate.
Fix: ALTER TABLE tracks ADD COLUMN IF NOT EXISTS category INT, set in _upsert from the streaming thread that already has it, filter radar_cached and _clusters to air. Keep the ground rows for the overlord's target lookups, which is what they were added for.
4.2 radar_live and radar_cached produce structurally different strings on the same wire, and nothing says which wrote the line
director/tools/dcs.py:148-176; director/tools/tracks.py:465-523, :416-437.
tracks._render emits the manned marker, on the ground, groundspeed, the IN FORMATION ... — N ships, lead collapse and _unique_labels de-duplication. radar_live emits none of them. In the fallback, identity.units_on sets manned=False for every contact, so Registry.by_elimination can never fire — rung 1b of the identity ladder, the one that exists so a visiting pilot needs nothing set up in advance, is dead with no log line. _unique_labels is gone, so two AI groups both called "Enfield11" are back on the scope under one name — the collision tracks.py:416-437 says is "almost certainly what put 'Pony one one' on an AI unit".
Skeptic refuted the claimed trigger, which was most of the claimed cost: radar_picture falls back only when radar_cached raises, not on a cache miss. An empty or fully-stale table renders "no contacts", which is the documented and correct freshness behaviour. The fallback needs a PostGIS/pool exception, at which point identify, flights and clearance are failing too. So this is a latent divergence: two producers of one wire format with nothing pinning them together, radar_live silently three features behind, and no marker saying which wrote the line.
Fix: render radar_live through the same formatter, or delete the fallback and let a cold cache read as no-contact. At minimum, tag the picture with its producer so a degraded mode is visible.
4.3 The airframe is dropped on both radar paths the metronome uses — every vectored aircraft gets the 250 kt unknown-type floor
src/marshall/atc/agent_atc.py:932-938, :824, :736-761, :1276-1277, :2907; equipment.py:102-154.
radar_fixes builds asr.Position(...) with no type=, defaulting to "" — even though radar_fix two functions earlier does pass type=aircraft_type_on_scope(scope, cs). radar_fixes is the only source of pos in asr_monitor, and pos feeds vector_call → speed_instruction(..., aircraft_type=getattr(pos, "type", "") or ""). Empty type floors at ASSUMED_MIN_KT = 250.0. Separately, radar_fix_by_track asks aircraft_type_on_scope(scope, ""), and C.parse("").canonical is "", so neither branch can hit and it returns "" unconditionally.
Proved by running the real BATUMI_ASR profile against a scope line for an F-16 at 300 kt: via radar_fixes the metronome emits "...reduce speed to two five zero knots"; via radar_fix it correctly says nothing. That is below MIN_VECTOR_KT for the type, and the comment there says "the one time this was set low, the aeroplane it hit was the one flying". In the other direction a warbird can never trigger speed control at all, so the feature is dead for the aircraft it was written for. The whole equipment.MIN_VECTOR_KT table and the pilot-reported fix documented at equipment.py:102-113 are inert in production. Coverage confirms vector_call's body is never executed by any test, which is why test_speed.py passes while the live path is typeless.
Skeptic narrowed: pos.type has exactly one consumer, speed_instruction; the equipment/receivers path at :1763 is unaffected. radar_fix_by_track's blank type is latent today but goes live the moment 1.3 is fixed — fix the two together.
Fix: type=aircraft_type_on_scope(scope, tag) in radar_fixes, and pass the matched tag in radar_fix_by_track.
4.4 flight_airspace reads tracks with no last_seen filter
director/migrations/013_join_a_track_by_either_name.sql:19-42; director/app.py:359-368; agent_atc.py:3468, :2079-2084.
DESIGN.md:74-76 states the rule: every row carries last_seen, reads filter on it, and a stale track reads as no-contact — never confidently-wrong. radar_cached and _resolve honour it. flight_airspace does ST_Intersects(s.volume, t.geog) with no predicate on t.last_seen, and exposes neither last_seen nor observed_at, so no caller can compensate. Rows leave tracks only on an explicit gone; a sim pause, a LoadMission hot-swap or a gRPC reconnect freezes the last position in place.
Skeptic refuted the claimed cost: leaving_my_airspace hands down the ladder only (order = {center:0, approach:1, tower:2}, returns None when order[role] >= order[me.role]), so a frozen track at the field cannot hand an airborne pilot to Tower. The reachable harm is the opposite direction — a frozen position far out resolves to Center and offers an approach-controlled pilot to Georgia Center, i.e. a re-run of the bug 013 was written to fix — and even that is gated by under_our_vectors.
Fix: AND t.last_seen > now() - interval '15 seconds' in the join, share the constant with FRESH_SEC, and surface t.last_seen as a column. Index tracks(label) while you are in there.
5. Numbers and frames
5.1 bearing_distance returns DCS GRID bearings but is labelled "true", and magnetic() subtracts MAGVAR from them
src/marshall/core/route.py:1235-1239, :1261-1262; contradicted by route.py:1155-1184 and agent_atc.py:766-780.
route.py:1237 computes atan2(dz, dx) on DCS terrain metres — the file header says "x increases north, z increases east", so that is a grid bearing — and the docstring calls it "True course in degrees". :1262 then does magnetic(true) = true - MAGVAR. Eighty lines earlier the same file establishes grid_convergence_deg = 5.74 and spells out that grid north is 5.74 degrees off true at Batumi. agent_atc.py:780 applies exactly this correction to radar headings: true = grid + grid_convergence_deg. ISSUES.md:887 records that this frame error "cost a night". The correction reached the radar half and not the dead-reckoning half.
Measured, projecting the sortie fixes through pydcs's Caucasus transform: convergence on the five legs is 5.59, 5.76, 5.76, 6.61, 6.48 degrees. So the error is 5.6–6.6 degrees per leg, not perfectly uniform, and the route does not exactly close. On the 52 nm FEET WET→INGRESS leg that is ~5 nm of cross-track at the turn; on the 64 nm run east, ~6 nm.
This is the pilot's only navigation on this sortie. The consumers are navlog.py:107/213, routemap.py:88, brief.py:45, briefing.py:218 (the agent's own copy) and build.py:680/954. INGRESS, TSUTSNVATI and EGRESS carry freq_mhz=None and navaid="" — there is nothing to home. I also checked the mitigation: the player group's waypoints come from R.FIXES[1:] = [INITIAL, BATUMI], the letdown only, so the sortie steerpoints are not in the aircraft's route and the paper nav log really is the navigation.
Not documented anywhere — WIRING.md:1969 lists these functions with no frame note, and the frame scar tissue at WIRING.md:2025-2037 covers only the radar half. Batumi's convergence (5.74) almost exactly equals MAGVAR (6.0), which is why every runway-based cross-check in the file agrees with both hypotheses.
Fix: have bearing_distance return the grid bearing under its real name and convert once — true = grid + convergence, magnetic = true - magvar. Both are properties of a place; the profile already carries grid_convergence_deg and magvar_deg.
5.2 Beyond 25 nm the MVA lookup returns the highest altitude in the whole table regardless of bearing
src/marshall/core/route.py:378-403 (esp. :402), :704-709, :552-561; asr.py:171-190; agent_atc.py:1370, :3224.
mva_for walks MVA_CELLS for the smallest ring containing the range; the surveyed table stops at 25 nm. When nothing matches, :402 falls back to max(a for *_, a in cells) — the largest altitude anywhere in the table, from the mountain cells on the 120–150 radial — with no reference to the bearing asked about. Reproduced:
24.9 nm on the 270 radial (open sea, west): min_safe_ft = 2000
25.1 nm on the 270 radial (open sea, west): min_safe_ft = 12000
guide() at 40 nm, 270 radial, 3000 ft -> phase vector, assign 12000
asr.safe_alt makes it a hard floor via max(msa, ...), overriding the "never above where he already is" rule two lines above it, and asr_context speaks it. There is no range gate at the call site. The cliff is a factor of six at a routine range: approach_hands_over_nm = 25.0, the EGRESS→BATUMI return leg is 55 nm. It also contradicts the profile — hold_top_ft = 10000, commented "the only genuine ceiling here is OXYGEN: a P-51D holding for a long recovery has no business above 10,000 ft."
Cost: a pilot checking in inbound over open water is told to climb to 12,000 ft, then the floor collapses to 2,000 at ~25.6 nm and he is handed 10,000 ft of descent inside the vectoring segment. That is route.py:296-307's complaint against the old four-quadrant table ("seven thousand feet of climb for nothing"), reproduced outside the surveyed ring. This is guidance, not separation, so the invariant is not at risk, and the agent may soften the number. tests/test_asr.py:522-598 only exercises this at 15–20 nm.
Fix: when no ring matches, fall back to the outermost ring for that bearing.
5.3 The default approach's MDA sits 332 ft above the briefed cloud base — every --hard mission is a guaranteed missed approach
src/marshall/core/route.py:860-864, :902-910, :1188-1221; mission/build.py:404, :421-424; briefing.py:130.
route.py:860 states the invariant: "MDA must sit just below the briefed cloud base so that levelling at minimums actually reveals the runway. Ceiling and MDA move together." When BATUMI_ASR raised min_hat_ft to 700 (correctly, per the chart's 687 ft OCA), ceiling_ft was left at 400. Result: mda_ft = max(32+700, 400-100) = 732 against a --hard base of 400.
Already documented — WIRING.md:1999-2004 states it verbatim ("a guaranteed missed approach that looks like a controller error and is a weather-build error. Use --ceiling/--tops rather than --hard") and lists it as leak #1. Scope is narrower than it first looks: --hard is not the default (build.py:927 defaults to light, base 1900, which breaks out 1,168 ft above MDA).
The residual defect worth fixing is the two-profile split: build.py:404 rebinds P = R.BATUMI_APPROACH inside build() purely for weather while the plate, the ATC and the MDA come from BATUMI_ASR. They share ceiling_ft = 400 today, so the disagreement is invisible — the invariant "ceiling and MDA move together" is now held only by coincidence between two objects.
Fix: delete the rebind, derive the base from profile.mda_ft + profile.breakout_ft, and add a test asserting mda_ft < ceiling_ft for every profile.
5.4 The radar origin is a hand-typed lat/lon in the director, and MAGVAR is declared twice
director/tools/dcs.py:90; director/tools/tracks.py:35, :52, :296-305, :390-397; route.py:21, :124, :725; agent_atc.py:2405-2479.
BATUMI_LAT, BATUMI_LON = 41.6103, 41.5997 is the fixed origin for every range and radial the ASR geometry consumes, used directly by both PostGIS queries and the gRPC fallback. push_fixes already projects every route.py fix through the sim's own coord.LOtoLL — precisely because "a flat-earth offset was 1.2 miles wrong at the coast and 7.6 miles wrong at the target area" — but set_fixes writes only the fixes table and _FIXES, never the origin. Worse than reported: _FIXES is seeded with the literal at tracks.py:52, so the push silently overwrites the vector tool's idea of Batumi with route.py's projected point while the radar origin keeps the literal. Two answers to "where is the field" in one module, and nothing compares them. _MAGVAR = 6.0 at tracks.py:35 is a second copy of route.MAGVAR with no link.
Skeptic measured the divergence: projecting route.py's BATUMI through pydcs gives 41.60959, 41.60023 — 0.049 nm, about 300 ft, so ~12 ft of altitude error at the ASR's 235 ft/nm, invisible against whole-mile range calls and the 0.559 nm touchdown offset. The ARP and the runway centre at Batumi are the same place, so route.py:609-623's premise stands. What survives is maintainability and portability: an unreconciled duplicate constant with no test, in a file explicitly designed to be pointed at another field. Same for magvar — route.py:722-730 already documents that variation belongs to a place, which makes the constant in tracks.py the one that will be forgotten.
Fix: read the origin from the pushed fixes row, falling back to the literal only on a cold table and logging that it did. Push magvar_deg rather than re-declaring it.
5.5 altimeter_datum = "QFE" is briefed to the pilot and applied to zero altitudes
route.py:646-667, :1221; briefing.py:97-106, :139-143; kneeboard/brief.py:127; kneeboard/comms.py:75, :110; build.py:736.
Every use of altimeter_datum and qfe_inhg across src/, director/, tools/ and tests/ is string formatting. No arithmetic anywhere. Meanwhile every altitude is MSL: mda_ft = field_elev + HAT, MVA cells are land.getHeight plus clearance, radar alt is position.alt, on_the_ground compares against field_elev_ft + 200. briefing.py:141 hands the agent the QFE setting with "Every height you read him is measured against this datum", which is false for every height the geometry computes; briefing.py:130 prints both frames side by side without noticing ("MDA 732 (700 ft above the field)").
Cost today: 32 ft. The field is a parameter and the error equals its elevation, so a QFE field at 2,000 ft flies the whole approach 2,000 ft low — which route.py:664-666 predicts in its own comment. Latent portability gap, not a live defect. The author has written down both the fact and the risk; what is missing is the arithmetic and any assertion that the two are consistent.
Fix: make the field load-bearing with one conversion at the speech boundary, or delete it and state in one place that this system works MSL and briefs QNH. A dead datum field is worse than none, because briefing.py:141 tells the agent to trust it.
6. The prompt seam, and what it costs
6.1 The /atc agent is cached forever and never rebuilt — every plate push is a no-op on the only agent that flies
director/app.py:150, :176-180, :78-98; director/strands_pg/api.py:144, :170, :280-291; agent_atc.py:39, :410, :2490.
Two auditors found this from opposite ends; it is one finding. _atc_agents: dict[str, Agent] is declared at app.py:150, written only at :179, and cleared nowhere — grep -rn "_atc_agents" director/ returns exactly three lines. The invalidation that exists lives in a different closure: PUT /prompts/{name} calls invalidate_agents(), which clears agents (api.py:144), the dict behind /chat and /chat/stream. The bridge never calls /chat; AGENT_URL = BASE_URL + "/atc". build_agent captures the system prompt by value at construction. The session key is stable across restarts: f"batumi-approach:{freq_mhz:.3f}". So the first transmission of the director's lifetime freezes soul+plate+rules for that session for as long as the container lives.
Cost: change the runway, MDA, approach type, beacon frequency or station list in route.py, restart the bridge — the controller keeps voicing the previous mission's plate. Silent from both ends: the PUT returns 200, the bridge prints "pushed plate ... to the director", and the plate in Postgres is correct. Meanwhile the bridge's own geometry uses the new profile it re-read from /approaches. That is the two brains quoting different numbers on the same frequency. WIRING.md:276 calls a stale plate "the one that produces confidently-wrong altitudes", and WIRING.md's honest list of where the cannot-disagree guarantee leaks does not contain this — so it is undocumented, not a known scar.
Skeptic corrected one claimed cost: tools/atc_dryrun.py is not affected — it uses f"dryrun-{name}-{stamp}", a fresh session per invocation, so it always validates the current plate. The divergence runs the other way: the dry run shows the new plate while the live controller voices the old one. Also: a docker compose restart silently repairs it, so the window is "director uptime spanning a plate change" — which is exactly the mid-sortie bridge-restart workflow the ops notes describe.
Fix: register a callback so invalidate_agents() also drops _atc_agents, or compare a hash of prompts.assemble(...) against the cached agent's system_prompt on each /atc call.
6.2 One session id stands in for four station identities, and ~550 tokens of static instruction ride in the user message
src/marshall/atc/agent_atc.py:2490, :2517-2535, :2626, :2687-2695, :3475-3638, :258-303; director/app.py:130; DESIGN.md:49-50.
session_id is set once per bridge process and interact() passes it unchanged for every channel — but the bridge deliberately monitors all of the approach's stations. Center, Approach, Tower and Overlord share one conversation, contradicting DESIGN.md:49-50 and WIRING.md:640 ("one session per channel per mission instance"). SlidingWindowConversationManager(window_size=16) retains ~8 prior user turns verbatim, each with its own YOU ARE: <station> on <freq> line and its own stale RADAR: line. OVERLORD_BRIEF is 3,074 chars and is a role reversal — "you do not own runways, approaches, separation or the holding stack, and you must never issue an approach clearance or vector anyone onto a final" — so one overlord exchange leaves that instruction sitting in the window for the next several approach turns.
Skeptic corrected: an ordinary turn restates identity three times in the current message (TRANSMITTER, YOU ARE, THIS TRANSMISSION IS FROM), so a wrong self-identification is possible but not likely. The unmitigated case is the hook re-invocation at :2687-2695, which sends only EVENT ... RADAR: ... with no YOU ARE and no TRANSMITTER — a callback promised on Center is composed against whatever station identity the shared window happens to hold, and that path already has a scar for going out on the wrong frequency (comment at :2678). Lead with that.
The token half: ~2,150 chars / ~550 tokens of byte-identical instruction per turn, retained ~8 times, ~4,400 tokens of literal duplicate per request, sitting below any cache point and therefore uncacheable where it is. Same money as 6.3 — do not double-count it.
Fix: give each station its own session_id (<station>:<freq>), which is what DESIGN.md already specifies. Move everything invariant (VISUAL APPROACHES, the TRANSMITTER wording, DEPARTURE FREQUENCY, the RADIO contract) into the system prompt. Keep only per-call facts in the user message. Put YOU ARE on the hook path.
6.3 No prompt caching anywhere
director/app.py:87-92, :104-124; director/prompts/*.md; requirements.txt:4.
_bedrock() passes only model_id and additional_request_fields={"thinking": {"type": "disabled"}}. grep for cache_prompt|cache_tools|cachePoint across director/ returns nothing. Measured: soul 620 + plate 1,575 + rules 18,185 = 20,380 chars (~5,100 tokens), plus ten tool schemas (~1,200-1,500). Byte-stable for the life of the cached agent, which per 6.1 is very long indeed. ~6,500 uncached tokens × ~45 transmissions ≈ 290k tokens ≈ $0.88/sortie at $3/M, billable at ~10% with a cache point.
Skeptic corrected the latency argument down: a cache hit saves prefill on ~6.5k tokens, order of a few hundred ms — real, but not comparable to the 2.2 s classify call. And the claim that the 2-second hook poll keeps sessions warm is false: fetch_due is a plain GET that reads Postgres and makes no Bedrock call, so it does nothing for a 5-minute TTL. SUSPECTED, not proved: that strands-agents>=1.13 exposes cache_prompt/cache_tools — the package is not installed on this box. Worth doing, not urgent, and it only pays off once 6.2 moves the boilerplate above the cache point.
6.4 rules.md names a radar tool that is not registered, and two memory tools are registered that no prompt describes
director/prompts/rules.md:253-254, :139; director/app.py:39-42, :99-124; director/tools/dcs.py:279-284; strands_pg/memory_tools.py:22-24, :84-107.
rules.md says "when you use a tool (set_hook, radar, identify), call it FIRST". radar is an @tool in dcs.py but app.py imports only spawn_ground, radar_picture and deliberately does not register it — the comment at :99-100 says why ("Radar is injected into every call, so it's not here"). Every other tool rules.md names exists. In the other direction, memory_tools(namespace=session_id) registers remember and recall with generic descriptions, and memory_tools.py:22-24 says "When rolling out, update rules.md to reference the right tool names", which was not done.
Skeptic largely refuted the claimed cost: a Bedrock tool_use can only name a tool in the request's toolConfig, so the "unknown-tool error costing a 2-4 s round-trip" is speculative and I found no path producing it. The realistic cost is that the prompt tells the controller it has a way to fetch the scope on demand when the scope is already injected — a hedge or a stall. And rules.md does mention memory once, at :139 ("Never search your memory for a plan"), the only mention, pointing away from the tools. So the two registered tools are ~150 tokens of schema per call carrying an instruction that only tells the model not to use them.
Prompt hygiene, not a latency bug. Delete radar from that bullet; drop memory_tools from the ATC agent or give rules.md one line about what the controller remembers.
7. What is not tested
7.1 No test executes any of _run_srs, and the dry run that claims to mirror it is a hand copy that has drifted
Coverage of src/marshall/atc/agent_atc.py (missing 2484-3651); tools/atc_dryrun.py:3, :169-189; CLAUDE.md testing section.
coverage run --include=src/marshall/atc/agent_atc.py -m pytest tests reports 48% and lists 2484-3651 unexecuted — _run_srs in its entirety, plus vector_call at 1269-1282. 725 tests pass in 0.93 s and none touch the receive loop. Every module-level helper is ~82% covered, which is why every finding in sections 1 and 2 lives in the wiring: the helpers are right and the wiring is untested.
atc_dryrun.py:3 claims it "Mirrors exactly what agent_atc._run_srs assembles per transmission" and CLAUDE.md sells it as how the two-brain seam is "testable in seconds". It is an independent re-implementation missing flight_strip, the FLIGHT verdict, THIS TRANSMISSION IS FROM, READ-BACK EXPECTED, READ-BACK CORRECT, VISUAL APPROACHES, DEPARTURE FREQUENCY and TOWER RELAY, with a one-line YOU ARE against the live 26-line block.
Skeptic sharpened this and cut half of it: the radar-dependent omissions are not real drift, because run() defaults to scope="" and those blocks would be empty in the live loop too. The damning drift is in the non-radar blocks the dry run's own scripts exist to test. The READBACK script's comment says it exists because "a correct read-back must be ANSWERED" and that rule "competes with the airborne one" — while the live bridge resolves that competition deterministically with the READ-BACK EXPECTED / READ-BACK CORRECT blocks the dry run never sends. run()'s comment names "a clearance with no departure frequency in it" as the bug it guards, and the dry run omits the DEPARTURE FREQUENCY block that fixes it. The two scripts most worth running are graded against a harder prompt than production ever sees: a failure there is not a production bug, and a pass says nothing about the real assembly.
Fix: extract the prompt assembly (:3475-3646) into a pure function over already-computed inputs; have both _run_srs and atc_dryrun call it; unit-test it plus the receive-loop ordering with a fake SRS client.
7.2 The schema cannot be rebuilt from migrations/
director/entrypoint.sh:30; director/strands_pg/migrate.py:66-72; director/migrations/004_flights.sql:159, 005:91, 007:101, 008:54, 009:24-47, 011, 012, 013:40; director/tools/tracks.py:135, approaches.py:30.
tracks, fixes, approaches and flight_plans are created by CREATE TABLE IF NOT EXISTS inside tools/*.py, lazily, at first use, in the running agent. They appear in no migration. But entrypoint.sh:30 runs python -m strands_pg.migrate as a separate process before the app starts, and that process imports only psycopg and _pool — no tools.* — so no runtime DDL fires. Postgres resolves relations at CREATE VIEW time, apply() has no try/except, and set -euo pipefail is on. Against an empty volume the container dies at boot.
Skeptic corrected the first failure: 004, not 005 — 004_flights.sql:159 already does LEFT JOIN tracks t. The dependent set is 004, 005, 007, 008, 009, 011, 012, 013 on four tables. The only reason this has never been seen is that the live volume predates the migrations. The author already treats replayability as the contract — 004:143-146 and 007:92-95 both carry "Migrations have to be replayable from nothing in sequence, and this one was not."
Cost: no disaster recovery, no clean dev/test database, no way to stand up a second instance to test a schema change. The failure is loud rather than silent, which is why it is medium.
Fix: a 000_runtime_tables.sql creating those four ahead of 004 (they are already IF NOT EXISTS, so it is a no-op on the live volume), then a CI step that runs the migrator against a throwaway Postgres.
WHAT I WOULD DO FIRST
- Finish the by-track sweep in one commit.
agent_atc.py:3234,:1763,:1771,radar_range_for:1374,radar_fixes:932andradar_fix_by_track:824. These are five instances of a conversion the team has already decided on and applied at three other sites — the comment at:3437says "for the same reason everything else is", believing it was done. It closes 1.2, 1.3, 1.5 and 4.3, and it is the cheapest work on the list because the correct idiom is already in the file. release_stale+ctl.tick()together. Separately each looks survivable; together they mean a departed pilot can hold the letdown until the bridge is restarted, and the field closes with no symptom but silence.release_staleis the escape hatch for the dead clock, and it cannot fire. Do not fix one and call it done.- Invalidate
_atc_agentson a prompt push. Three lines. It is the only finding where the two brains can voice different numbers on the same frequency with a green log on both sides, andWIRING.md's own leak list does not know about it. Do it before the next mission edit, not after. bearing_distance's frame. The paper nav log is the pilot's whole navigation on this sortie and it is 5.6–6.6 degrees wrong on every leg, today, with nothing in the repo that will catch it — the identical bug in the radar half already cost a night. It is a one-place fix with a clear correct answer.- Extract the prompt assembly and put a fake-SRS test on the receive loop. Last because it fixes nothing today, first in importance because every item above landed in code no test executes, and
atc_dryrun— the thing sold as the guard — is grading against a prompt production never sends. Until this exists, the next sweep will be forgotten at the sixth call site too.