- 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>