Legacy State Writer Inventory¶
Status: MOR-407 final cleanup status, updated for MOR-437 dead-code/mirror cleanup
Date: 2026-06-03
Spec: docs/superpowers/specs/2026-06-02-radio-state-pipeline-design.md
This inventory started as the MOR-335 baseline audit. MOR-407 resolves the
remaining RadioState, StateCache, and legacy revision overlap into explicit
cleanup decisions so reviewers can tell which legacy paths were deleted, which
now project through StateStore, which remain protocol-local, and which are
compatibility-only surfaces.
MOR-407 classification:
- required compatibility mirror fed from StateStore/observations:
compatibility_onlyrows whose replacement/guard namesStateStore, observations, or explicit compatibility ingress. - protocol-local state that is not radio state:
protocol_local_keep. - removable legacy writer:
deleted. - bug requiring separate fix:
deferred_follow_upconstraints. No new out-of-scope bug was found during the MOR-407 audit.
Decision statuses:
deleted: removed in MOR-347 or an earlier milestone.migrated: normal delivery now uses observations,StateStore, orStateStoresnapshots.protocol_local_keep: local protocol/session state, not semantic radio state.compatibility_only: retained only for public API, Web schema, CLI/config, Hamlib wire behavior, or legacy test/backend compatibility.executor_cache_keep: private command/acquisition timeout, pacing, dedupe, or restore cache; it must not be treated as fresher consumer state.deferred_follow_up: intentionally left for a later issue because removing it requires a broader field-family migration or public compatibility decision.
Icom Runtime and CI-V¶
| Path | MOR-407 status | Current replacement / guard | Remaining constraint |
|---|---|---|---|
runtime/_civ_rx.py::_update_state_cache_from_frame |
migrated plus executor_cache_keep |
Supported CI-V frames call _apply_state_store_observations(...) before legacy mirrors; private _state_cache updates remain executor fallback. Covered by tests/test_civ_rx_coverage.py, Web meter/freshness regressions, and state pipeline diagnostics. |
Keep cache reads private to executors; do not expose _state_cache as consumer delivery truth. |
runtime/_civ_rx.py::_RADIO_STATE_HANDLERS |
migrated plus compatibility_only |
MOR-437 made the in-scope Icom field families observation-backed: _observations_from_frame(...) emits StateStore observations for freq/mode, att (0x11), filter_width/agc_time_constant/data_mode (0x1A 03/04/06), power/ptt/tuner/tx-monitor (0x18/0x1C), active/dual-watch/split (0x07/0x0F), RIT (0x21), the 0x14 levels in _CMD14_OBSERVATION_BACKED_SUBS (af/rf-gain/squelch/nr/nb/mic/comp/monitor levels + cw_pitch), the 0x15 meters, and the 0x16 toggles/values in _CMD16_OBSERVATION_BACKED_SUBS/_CMD16_OBSERVATION_BACKED_VALUE_SUBS (nb/nr/auto-notch/manual-notch/comp-on/monitor-on/vox-on/preamp/agc). _handle_* skips the redundant RadioState mirror for those subs. |
deferred_follow_up: handlers still mirror into RadioState for field families with no observation emitter yet (e.g. pbt_inner/pbt_outer 0x14 07/08, antenna 0x12, repeater tone/tsql, digisel, twin-peak, ipplus, apf, filter_shape, dial_lock, break_in, ssb_tx_bandwidth, tuning_step, scan, scope controls). Delete those mirrors only after each gains a documented snapshot projection and compatibility tests. |
runtime/_civ_rx.py::_notify_change |
compatibility_only |
state_store_changed carries canonical revision/freshness delivery; legacy event names remain for Web event notifications and older callback consumers. |
Do not use notify events to produce Web state revisions. MOR-347 static tests reject reintroducing Web poller revision bumps. |
runtime/_civ_rx.py::_publish_scope_frame and _scope_frame_queue |
protocol_local_keep |
Scope sample streaming remains a separate sample protocol, not semantic radio state. | Scope controls are state fields; scope samples are not. |
runtime/_dual_rx_runtime.py main getters/setters |
executor_cache_keep plus compatibility_only |
VFO switch/restore sequencing and _last_* cache use remain private executor behavior; confirmed values are also represented by StateStore where supported. |
deferred_follow_up: remove direct active-slot RadioState writes after dual-RX slot projections cover all public consumers. |
runtime/radio_initial_state.py |
migrated |
Initial sweeps flow through runtime receive/observation paths and seed StateStore for supported fields. |
Keep hardware-dependent validation manual where profiles require it. |
runtime/radio_state_snapshot.py |
executor_cache_keep |
Best-effort restore fallback may use _last_* caches but does not define consumer freshness. |
No Web/rigctld delivery path may prefer this cache over StateStore. |
core/_state_cache.py and re-export _state_cache.py |
executor_cache_keep plus compatibility_only |
Retained for runtime timeout fallback and import compatibility. | Public state_cache exposure is compatibility-only; new delivery code must use snapshots/projections. |
backends/icom7610/drivers/serial_stub.py |
migrated plus compatibility_only |
MOR-437: the fake serial backend is now ObservationPollable — SerialMockRadio.create_observation_poller(...) feeds the provider observation pipeline, so production reads no longer depend on StateStore bulk sync from the stub. The internal RadioState/StateCache mirrors remain only to drive the stub's deterministic CI-V responses and legacy radio_state/state_cache test accessors. |
Keep the internal mirrors scoped to CI-V simulation/legacy accessors; new stub state must be observable via the observation poller, not consumed from the mutable mirrors. |
Web Runtime, Revisions, and Frontend Store¶
| Path | MOR-407 status | Current replacement / guard | Remaining constraint |
|---|---|---|---|
web/radio_poller.py::_revision / bump_revision |
deleted |
Removed in MOR-347. Web state revisions come from StateStore.snapshot().state_revision; tests/test_state_pipeline_contracts.py rejects reintroducing poller revision API or server callback bumps. |
Public Web payload still includes legacy revision, but it aliases canonical stateRevision. |
web/radio_poller.py::_execute command ACK paths |
deleted no-op plus migrated/compatibility_only mirrors |
MOR-437 deleted the pure no-op _apply_command_response_observation(...) and every call site (~15): setter success was already lifecycle-only, so the no-op carried no behavior. The per-family legacy RadioState mirrors for now observation-backed families were also removed — att, preamp, agc, rf_gain, squelch, nr/nb levels, mic_gain, compressor/monitor level+on, vox_on, split, tuner_status, filter_width, data_mode, agc_time_constant, auto/manual notch. Read-after-write for these is owned by CommandService scoped pending overlays plus the _civ_rx.py observation emitters. BSR keeps its real _apply_bsr_readback_observations(...) poll_response emitter (the BAND audit relies on it). |
Setter success must never confirm StateStore. deferred_follow_up: the mirrors listed in "MOR-437 intentionally-kept compatibility mirrors" below remain because those families have no observation emitter yet; migrate them field-family by field-family. |
web/radio_poller.py::_last_polled and _send_query |
executor_cache_keep |
Poll cadence is acquisition/runtime local. Meter and state query observations feed StateStore where supported. |
Do not use poll cadence markers as delivery freshness for Web consumers. |
web/radio_poller.py::_poll_unselected_slot / host _vfo_slot_override |
migrated plus protocol_local_keep |
VFO swap mechanics remain protocol-local; returned values are slot-scoped observations where supported. | Keep swap/restore invisible to consumer state except via confirmed observations. |
web/server.py::_radio_state and build_public_state |
migrated plus compatibility_only |
HTTP and WS state build from command_state_store.snapshot(). sync_state_store_from_radio_state(...) is a compatibility ingress for legacy state-poller snapshots and startup composition points. HTTP power keeps only a legacy public mirror; it does not confirm StateStore. |
Normal delivery must not call compatibility sync or read backend/poller/legacy state to build radio values. Legacy ingress is allowed only at explicit startup/poller callback boundaries. |
web/server.py::_health_revision |
compatibility_only |
Public health transitions are separate from semantic stateRevision and included in ETags. |
Keep healthRevision additive/backward compatible until a public freshness schema replacement exists. |
web/server.py::_broadcast_state_update |
migrated |
Broadcast encodes snapshots from StateStore and suppresses duplicate state keys using state/freshness/health revisions. Audio FFT scope metadata is derived from the same snapshot before broadcast. |
Delivery triggers may broadcast events, but they must not mutate confirmed state or ingest legacy state. |
web/server.py::_serve_state |
migrated |
HTTP ETag uses stateRevision-freshnessRevision-healthRevision; body revision aliases stateRevision. |
Preserve legacy revision key for existing clients. |
web/_delta_encoder.py |
migrated |
MOR-347 splits transport-local transportSeq from canonical revision/stateRevision. tests/test_delta_encoder.py covers the split. |
revision remains a legacy alias for canonical state revision when supplied; no frontend should treat transportSeq as state freshness. |
web/handlers/control.py initial state |
migrated |
Initial WS state calls server build_state_update_envelope(force_full=True) so HTTP and WS share the same snapshot revision/freshness. |
Fallback build_public_state_payload(... revision=0) remains compatibility-only for handler tests without a server. |
web/web_startup.py poller startup |
migrated plus compatibility_only |
Observation-capable providers use create_observation_poller(...) and apply typed observations to StateStore before broadcast. The legacy StatePollable/sync_state_store_from_radio_state(...) branch remains only for providers that have not opted into observations. |
New production providers must not claim observation readiness while relying on legacy bulk sync. |
frontend/src/lib/transport/http-client.ts |
migrated |
Frontend gates updates on canonical stateRevision plus freshnessRevision, falling back to legacy revision for compatibility. |
Legacy revision fallback remains public Web compatibility. |
frontend/src/lib/transport/ws-client.ts |
migrated |
WS full/delta envelopes preserve stateRevision and freshnessRevision; MOR-347 adds backend transportSeq without requiring frontend changes. |
Future frontend tests may consume transportSeq for ordering only. |
frontend/src/lib/stores/radio.svelte.ts |
migrated plus compatibility_only |
Store stale rejection uses canonical state/freshness revision helpers with legacy fallback. Optimistic maps remain local pending overlays. | Keep restart handling compatible with legacy low revision resets. |
rigctld Server and Hamlib Compatibility¶
| Path | MOR-407 status | Current replacement / guard | Remaining constraint |
|---|---|---|---|
rigctld/handler.py::_PendingRigState |
compatibility_only |
CommandService pending overlays and StateStore projections are canonical where present; local pending state preserves Hamlib read-after-write behavior. |
Keep scoped to Hamlib commands; do not share as generic radio state. |
rigctld/handler.py::_FallbackRigState |
compatibility_only |
GET paths prefer StateStore projections, then public RadioState compatibility, then fallback cache where needed for Hamlib wire stability. |
deferred_follow_up: remove individual fallback fields once projections cover every GET and compatibility tests prove behavior. |
rigctld/handler.py::_split_tx_vfo |
protocol_local_keep |
Hamlib TX VFO label/session behavior is protocol state, not confirmed radio state. | Preserve wire output. |
rigctld/handler.py GET paths |
migrated plus compatibility_only |
Frequency, mode, PTT, levels, split, functions, and dual-RX reads project StateStore first when available. |
Compatibility fallbacks may not appear fresher than a present projection. |
rigctld/routing.py level/func routing |
migrated plus compatibility_only |
Routing reads can update fallback cache, but handler projections prefer StateStore. |
Backend reads should continue moving into observation adapters. |
rigctld/poller.py |
executor_cache_keep |
Background cache maintenance remains private telemetry plumbing. | deferred_follow_up: delete as consumer source after rigctld fake-radio tests cover all projection reads. |
rigctld/server.py session vfo_mode |
protocol_local_keep |
Per-client chk_vfo state is Hamlib session state. |
Preserve Hamlib protocol behavior. |
rigctld/contract.py and dump-state constants |
compatibility_only |
Positional dump-state and command response text remain public Hamlib wire compatibility. | State migration must not alter text output without explicit compatibility callout. |
MOR-422 active receiver/RIT cleanup: rigctld get_vfo formats active VFO only
from fresh StateStore projection (global.slow_state.active for dual-RX,
receiver.<rx>.vfo.active_slot for single-RX); missing or stale projection
returns Hamlib EIO instead of legacy RadioState/default values. get_rit
and get_xit both project the shared Icom CI-V offset register from
global.operator_controls.rit_freq; missing/stale projection performs
backend readback, records a hamlib_response observation, and then responds
from Store, or returns EIO when readback is unavailable.
Yaesu and External rigctld Client Backends¶
| Path | MOR-407 status | Current replacement / guard | Remaining constraint |
|---|---|---|---|
backends/yaesu_cat/radio.py::_state |
migrated plus compatibility_only |
backends/yaesu_cat/observations.py provides provider observations for polling reads; mutable _state remains public backend compatibility. |
deferred_follow_up: remove direct setter echo mirrors after pending overlays cover Yaesu SET commands. |
backends/yaesu_cat/poller.py |
migrated plus compatibility_only |
Web startup uses Yaesu observation poller ingress for the fields that the FTX-1 [state_acquisition] profile (rigs/ftx1.toml:59-92) declares and that backends/yaesu_cat/observations.py emits: main+sub freq/mode, PTT, main+sub S-meter, global power/SWR meters, and main+sub AF/RF/squelch controls. Legacy RadioState callbacks remain for compatibility poller callers only. |
The remaining Yaesu fields are read only into legacy RadioState in poller.py::_poll_slow/_poll_fast with no [state_acquisition] capability and no observation adapter lane — they are explicit gaps, not silent MOR-401 production claims: ATT/preamp, AGC, NB/NR levels, auto/manual notch, filter_width, IF-shift, TX power-level setpoint, mic_gain, compressor on/level, split + FR/FT routing, vfo_select active slot, VOX, tuner, RIT/XIT clarifier, CW (keyer speed/pitch/break-in/spot/delay), narrow, dial_lock, tone/CTCSS, contour/APF, and the ALC meter. monitor is correctly excluded (FTX-1 has no CAT monitor — ML is rejected, EX-menu only; rigs/ftx1.toml:39,481-485), not a gap. deferred_follow_up: these Yaesu gaps are tracked under MOR-424 and are not release-gating — no current release claims FTX-1 backend-neutral control parity. They should be promoted to a release gate (as MOR-437 was for Icom) only when an FTX-1 release is scheduled. |
backends/yaesu_cat/poller.py EMA S-meter state |
executor_cache_keep |
Local smoothing memory is acquisition-local; observation mode emits the resulting S-meter samples only for the declared receiver.*.meters.s_meter paths. |
Do not add new smoothed meter paths without explicit acquisition/profile policy. |
backends/rigctld_client/radio.py::_state |
migrated plus compatibility_only |
backends/rigctld_client/observations.py adapts external Hamlib responses and command responses to observations; RigctldClientRadio.create_observation_poller(...) wires adapter-covered production reads into Web startup. After MOR-437 the rigctld-client is otherwise at the desired end-state: freq/mode, PTT, rf_gain, af_level, preamp, att, nb, nr, and filter_width are all observation-backed (filter_width via get_mode polling readback in read_freq_mode_controls, observations.py:90-99,203). |
The only genuine acquisition-profile gaps are the two explicit FieldAvailability.UNSUPPORTED declarations in observations.py:142-162: global.tx_state.power_on (external rigctld exposes no power state) and the active VFO slot on rigs without VFO support (receiver.main.vfo.active_slot). Direct SET echoes remain compatibility until command pending overlays cover all fields. |
backends/rigctld_client/radio.py::_vfo_supported |
executor_cache_keep |
Capability probe result remains a backend capability cache. | Not radio state. |
core.radio_protocol.StateCacheCapable |
compatibility_only |
Protocol remains to preserve public API/backend compatibility. New consumers should prefer state_store/snapshot projections when available. |
Public removal/deprecation requires a separate compatibility issue. |
Profile and Schema Gaps¶
| Gap | MOR-347 status | Current replacement / guard | Remaining constraint |
|---|---|---|---|
| Meter push/support metadata | migrated plus deferred_follow_up |
Acquisition scheduler/coalescer and profile capability metadata now cover current meter freshness behavior. | Extend profile metadata before adding model-specific meter push policy. |
| VFO path precision | migrated plus protocol_local_keep |
FieldPath includes receiver and active/fixed slot dimensions; Hamlib VFO labels remain protocol-local. |
Continue migrating dual-RX public fields to slot-aware projections. |
| Hamlib wire assumptions | protocol_local_keep plus compatibility_only |
chk_vfo, split TX labels, dump-state constants, and text formatting remain Hamlib compatibility. |
Preserve wire behavior while values move to StateStore projections. |
MOR-437 Dead-Code and Migrated-Mirror Cleanup¶
MOR-437 (acceptance criterion #4) removed the now-dead command-response no-op
and the legacy RadioState mirror writes in web/radio_poller.py for the Icom
field families that earlier lanes made observation-backed in
runtime/_civ_rx.py.
Removed:
web/radio_poller.py::_apply_command_response_observation— a pure no-op (itdel'd its arguments). Deleted with all ~15 call sites.web/radio_poller.py::_apply_att_compatibility_mirror/_apply_preamp_compatibility_mirror— the per-family mirror helpers for the migrated att/preamp families.- The inline
RadioStatemirror writes in_execute(...)for the migrated families: att (0x11), preamp (0x16 02), agc (0x16 12), nr_level (0x14 06), nb_level (0x14 12), auto_notch (0x16 41), manual_notch (0x16 48), agc_time_constant (0x1A 04), data_mode (0x1A 06), mic_gain (0x14 0B), vox_on (0x16 46), compressor_level (0x14 0E), monitor_on (0x16 45), monitor_gain (0x14 15), filter_width (0x1A 03), split (0x0F), nb (0x16 22), nr (0x16 40), tuner_status (0x1C 01). rf_gain/squelch had only the no-op and no mirror.
Read-after-write for the removed families is guaranteed by two layers, with no
poller-side RadioState write: (1) CommandService records a scoped pending
overlay carrying the written value on execute(intent); (2) the next poll
readback produces a typed StateStore observation from _civ_rx.py that
reconciles the overlay. tests/test_radio_poller_coverage.py::
test_compatibility_mirror_commands_do_not_confirm_state_without_readback proves
the overlay still carries the value after the poller executes the command and
that the legacy mirror is no longer written for migrated families.
Kept (real emitter — must NOT be removed):
web/radio_poller.py::_apply_bsr_readback_observations— emits explicitpoll_responsefreq/mode observations from the BSR register readback; the BAND audit relies on it.
MOR-437 intentionally-kept compatibility mirrors (deferred_follow_up, not yet
observation-backed — leave until each family gains an observation emitter):
- pbt_inner / pbt_outer (0x14 07/08 — emit-capable in
_civ_rx.pybut still on the_handle_14mirror path, not the mirror-skip set; mirror kept via_apply_compatibility_mirror). - notch_filter, if_shift, filter_shape (0x16 56), cw_pitch (0x14 09 mirror in
poller kept although observation-backed — not in this lane's family list),
dial_lock (0x16 50), break_in (0x16 47), drive_gain, ref_adjust,
tx_freq_monitor (0x1C 03), af_mute, tuning_step (0x10), scan state
(scanning/scan_type/scan_resume_mode, 0x0E), main_sub_tracking,
ssb_tx_bandwidth (0x16 58), repeater_tone/tsql (0x16 42/43) and tone/tsql
freq (0x1B), antenna (0x12: tx_antenna/rx_antenna_1/2), apf (0x16 32),
twin_peak_filter (0x16 4F), digisel, ipplus (0x16 65), vox_gain/anti_vox_gain/
vox_delay, nb_depth/nb_width, dash_ratio, RIT toggles/freq poller mirrors,
scope controls (0x27 sub-commands), and the optimistic
power_onwrite inSetPowerstat(radio stops answering polls when off). The active-slot mirror inSelectVfo(receiver.0.vfo.active_slot) is also kept — the 0x07 0xD2 observation writesglobal.slow_state.active, a different path. - Private
_state_cacheexecutor fallbacks remainexecutor_cache_keep.
MOR-407 Cleanup Guards and Regression Coverage¶
tests/test_state_pipeline_contracts.pykeeps a narrow public API guard for Web poller revision methods, then uses poisoned legacyRadioStateand poller revision paths to prove Web HTTP/WS/callback delivery reads the canonicalStateStoresnapshot without delivery-time legacy sync or directStateStoremutation.tests/test_radio_poller_coverage.pyverifies Web setter success does not confirm StateStore fields without readback. After MOR-437 it also asserts the migrated families no longer write the legacyRadioStatemirror while scoped pending overlays still carry the written value for read-after-write, and that the keptdeferred_follow_upmirrors (pbt_inner/pbt_outer) plus the BSR readback emitter remain.tests/test_state_pipeline_contracts.py:: test_web_poller_command_response_no_op_remains_removedis the MOR-437 static guard: it rejects reintroducing_apply_command_response_observationor the_apply_att_compatibility_mirror/_apply_preamp_compatibility_mirrorhelpers, and asserts_apply_bsr_readback_observationsand the generic_apply_compatibility_mirrorhelper are still present.tests/test_web_server_coverage.pyverifies observation-capable startup uses observation pollers instead of legacy bulk sync, and HTTP power does not seed confirmed StateStore state from a command ACK.tests/test_rigctld_handler.pyverifies rigctld SET commands enter CommandService without command-ACK confirmation, and GET paths project StateStore values ahead of RadioState/fallback compatibility paths.tests/test_delta_encoder.pyverifies canonicalrevision/stateRevisionare split from transport-localtransportSeq.- Web regression tests cover meter-only semantic changes, freshness-only
updates, HTTP/WS snapshot agreement, ETag state/freshness/health behavior,
and legacy
revisionaliasing. - rigctld tests cover
StateStoreprojection precedence and Hamlib compatibility fallbacks.
Public Compatibility Callouts¶
- Web state payloads still include legacy
revision; it is now explicitly an alias for canonicalstateRevisionwhenever the backend supplies a canonical revision. - WebSocket full/delta envelopes now include additive
transportSeq. Existing clients may ignore it. It must be treated as ordering metadata only, never as semantic state freshness. - Public
RadioState,StateCacheCapable, CLI/config behavior, and Hamlib rigctld wire text remain compatible in MOR-347.