genSquawk() drew a decimal number in 1000-8999, so clearances could contain
the digits 8 or 9 — codes no transponder can dial. Replace it with a shared
generateSquawk() that draws four octal digits and re-rolls the reserved codes
(7500/7600/7700, 7000, 2000, 1200, 0000).
shared/learn/scenario.ts had its own octal generator that could still draw an
emergency code; it now uses the same helper.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Neutralize the local engine's autonomous auto-advance (evaluateAutoTransitions/
evaluateSimpleAutoFlow) — the Python backend now drives state exclusively via
moveToSilent; the old walker could self-answer pilot states and race the
backend, producing loops.
- Dedupe applyBackendDecision's ATC log entry + TTS: moveToSilent no longer logs
say_tpl for backend-driven auto-advanced states (suppressSay), and a
lastAppliedSay guard drops a repeated decision from overlapping sources
(transmit reply, telemetry tick, silence timeout).
- Cap consecutive silence-timeout re-fires on the same state at 2 instead of
re-arming forever.
- Pause telemetry forwarding while a pilot transmission is in flight, and guard
against overlapping telemetry POSTs.
- Add request timeouts to TTS (20s) and backend transmit/telemetry/timeout
(30s)/createSession (60s) calls so a hung request can no longer freeze the
whole session.
Verified live against the Python backend: a full clearance→taxi chain now logs
each ATC line exactly once instead of 2-3x.
Implements the ai-traffic roadmap item per
docs/plans/2026-07-14-ai-traffic-architecture-design.md.
Simulated other aircraft on the user's frequency — callsigns, ATC
instructions, readbacks in their own stable voice, handovers — as pure
scenery. It never touches radioBackend: the Python backend keeps owning
the dialogue *with* the user, useAiTraffic owns the radio *around* the
user. The two share only the speech queue (arbitration) and the log.
Rules live as pure, seeded, framework-free modules under
shared/utils/aiTraffic/ so they run in tsx --test without a browser:
callsign collision rules, wake/in-trail separation, runway slots, the
speed ladder, direct validation, the §3 decision table, and the gating
chain. app/composables/useAiTraffic.ts wires them to Vue (1 Hz tick,
spawner, scheduler).
Gating is evaluated twice — before enqueue and again at playback, since
seconds pass in between. Traffic never keys up while the user holds PTT,
while their transmission is out at the backend, or inside the fresh
readback window. Off by default; the toggle surfaces the feature's v1
limitations rather than burying them in a doc.
Zero LLM calls: variance comes from seeded RNG over template variants.
Deviations from the design, both documented in the design doc:
- Adds SimAircraft.quietUntilSec. The design's rule table says "first
matching row per tick" but never says an instruction must be allowed to
take effect before the next one. Without it the planner re-derives the
same unresolved condition every second and nags one aircraft with the
same vector: 624 calls/30min measured, vs 90 with the cooldown.
- Airline pool limited to the 14 designators DEFAULT_AIRLINE_TELEPHONY
already knows; UAE/AUA/WZZ from the design would be spelled out letter
by letter instead of spoken as airline names.
Verified: 406 tests pass (176 new), no new typecheck errors, /live-atc
compiles and serves. The manual in-session walkthrough (audible traffic,
toggle mid-session) is NOT verified — it needs a login and the Python
backend. The 30-minute deterministic integration run stands in for it and
caught two of the three bugs found during development.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- SID basenames and 5-letter waypoints (ANEKI 7S, SULUS) are pronounceable
by design and are now spoken as words instead of letter-by-letter phonetics
- skip acronyms (ATIS, RNAV, MAIN, ...) when spelling 4-letter ICAO codes
- expand stand/gate designators, ATIS information letter, and surface wind
groups for TTS
- normalizeATCText now runs full client-side radiotelephony expansion
(callsigns, airports) since preNormalized texts skip the server normalizer
Note: tests/radioSpeech "normalizes SID suffix and METAR data" still expects
the old spelled-out SID behavior and fails until updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scenario picker & completion:
- Login → scenario selection screen with complete chains + individual phases
- Completion screen with "fly again / try opposite / back to scenarios"
- Scenario.airport ('dep'|'arr') drives which airport frequencies to fetch;
arrival scenarios (vfr-arrival, circuit-landing, taxi-in) use arr ICAO
Backend session integration:
- createSession forwards no_chain; response carries active_flow + session_complete
- Pass all six airport frequency variables to session so every chained flow
has the real airport values from creation
- fetchAirportFrequencies now runs before session creation so resolved
frequencies are included in backendVariables
Wrong-frequency check:
- airportFreqMap computed (from airportFrequencies, always up-to-date)
used as primary source in expectedFrequencyForState — immune to flow
snapshot switches
- setActiveFlow called when response.active_flow changes so local engine
cursor moves to the correct flow's states after a chain
- Wrong-freq ATC reply appended to communication log (offSchema entry)
Engine fixes (communicationsEngine.ts):
- patchVariables / patchFlags: write directly to the internal reactive
store, bypassing readonly(ref) which silently blocked all (vars as any)
.value[k] = v mutations
- appendLogEntry: push ATC speech (and wrong-freq replies) into comm log
- ATC controller_say_rendered appended to comm log after every transmission
- Pass as top-level field in PTT requests so Whisper STT
results are linked to the correct Python backend session
- Add namespaced helper in pm.vue (info/warn/error/debug/group)
controlled by localStorage PM_DEBUG flag; logs transmit/response
cycles, TTS calls, flag/variable syncs, and fallback warnings
- Log backend session creation context (flow, start state, vars, flags)
in startMonitoring
- Fix typo in text input hint: STT fails not PTT fails
and
fix: sync backend variables to frontend after each transmission
The ATC say template was rendered using the frontend engine's local
variable defaults (squawk '1234', hardcoded SID, etc.) instead of
the authoritative values from the Python backend session. This caused
the spoken clearance and the readback prompt to show different squawk
codes.
- After each backend transmission response, sync all response.variables
into vars.value (same pattern already used for flags)
- Prefer controller_say_rendered (pre-rendered by backend) over the raw
template for TTS scheduling, eliminating any remaining dependency on
local variable state for the ATC speech text
Replace the LLM-per-request flow in /pm with a stateful Python backend
(OpenSquawk-LiveATC-api). The backend owns session state, does regex-first
routing with readback evaluation, and returns the next state + ATC speech.
The frontend keeps its local cursor (communicationsEngine) for TTS and
monitoring UI, but no longer calls /api/llm/decide.
Changes:
app/composables/useRadioBackend.ts (new)
Typed Nuxt composable wrapping the Python REST API:
createSession, transmit, deleteSession, fetchFlows.
Base URL read from NUXT_PUBLIC_RADIO_BACKEND_URL (default 127.0.0.1:8000).
nuxt.config.ts
Expose radioBackendUrl as a public runtime config key so the composable
and communicationsEngine can both reach the Python backend.
shared/utils/communicationsEngine.ts
- fetchRuntimeTree now accepts an optional baseUrl so it fetches from the
Python backend instead of the Nuxt server when a URL is provided.
- renderTpl handles both {var} (old MongoDB schema) and {{var}} (new YAML
schema) — double-brace matched first to avoid partial matches.
- stateSayTpl / stateUtteranceTpl helpers unify say_tpl|say_template and
utterance_tpl|expected_pilot_template across both schema versions.
- auto_transitions from the new YAML schema are included when collecting
eligible transitions in collectAtcStatesUntilPilotTurn.
shared/types/decision.ts
RuntimeDecisionState extended with say_template and expected_pilot_template
fields (new YAML schema field names alongside the existing legacy names).
app/pages/pm.vue
- startMonitoring: loads tree from Python backend, then creates a backend
session (backendSessionId). Cursor synced to session.current_state.
- handlePilotTransmission: calls radioBackend.transmit instead of
/api/llm/decide. Applies auto_advanced_states via moveToSilent, then
the final state. Speaks controller_say_template via TTS.
- Both fetchRuntimeTree calls now pass radioBackendUrl so they hit the
Python backend, not the Nuxt flow-from-MongoDB path.
AGENTS.md (new)
Project guide updated to document the new two-backend architecture,
the Python backend session lifecycle, and the dual template schema.
docs/plans/2026-05-06-pm-python-runtime-contract.md (new)
Implementation plan and API contract written before the work started.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>