fix(model): ownership-gated auto-floor, stale-session detection + eviction, get clients verb (#3977, #3951)#3981
Conversation
… falloff, plateau-gated smoothing Fixes the two-release trace-quality regression chain behind aethersdr#3967 (and aethersdr#3932), root-caused with per-commit builds and live same-band A/B grabs: 1. aethersdr#3836 (shipped 26.6.5) applied buildFftDisplayTrace's 5-tap spatial smooth at an unconditional 0.75 blend. It exists to melt the radio's RBW stair-steps when zoomed in, but at wide spans it low-passes everything: narrow carriers lose height and the noise floor defocuses ("out of focus", aethersdr#3932). The blend is now gated on the measured plateau fraction (adjacent equal-pixel bins): zoomed-in plateau runs still get the full blend, busy wide spans get none, with a ramp between so zooming never visibly mode-flips. 2. aethersdr#3958 (shipped 26.7.1) drew the per-pixel stroke from the distance to the segment's INFINITE line (dFdx-derived slope). On steep segments that line passes near the whole pixel column, so the stroke shot full-height spikes above and below the trace, and the fixed ±0.75 px smoothstep AA band read as a soft "blurred" stroke at 1x DPI. The stroke now uses the true distance to the three adjacent polyline segments (endpoint-clamped, matching the old geometry quads' coverage) with the old falloff: solid to halfWidth−1, 1 px fade, 1 px feather annulus at low alpha. Also folds in PR aethersdr#3968's R16F-unconditional column format (float32 linear filtering is optional on GLES and undetectable via isTextureFormatSupported; half precision is ample for normalized 0..1 amplitudes) — though note that could not have caused aethersdr#3967: the reporter's renderer string shows D3D11 on Intel HD 520 (FL 12.1), where R32F filtering is mandatory and hardware-accelerated. Live-proven on FLEX-8400M against the AM broadcast band: 4-way same-band comparison (pre-aethersdr#3836 / post-aethersdr#3836 / 26.7.1 / this fix) shows the fix restores the pre-aethersdr#3836 look; panstats confirms frame-prep cost unchanged (~120 µs/frame at 58 presents/s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wnership gate, zombie detection + eviction, get clients verb Fixes aethersdr#3977 (root cause of aethersdr#3951: a still-alive earlier AetherSDR session keeps auto-floor-adjusting the pan a newer session reclaimed, bouncing the new session's trace 0.1 dB at a time). Three layers plus test tooling: - Auto-floor discipline (radio-authoritative): PanadapterModel now parses client_handle from pan status, and the dBm-range sender drops-and-warns when the pan's radio-reported owner is not us — a superseded session goes quiet the moment the radio broadcasts the new owner. - Reclaim eviction: when reconnect-reclaim adopts a staged pan, the predecessor handle recorded on it gets a `client disconnect` (SmartSDR's takeover behavior); disconnecting an already-dead handle is a harmless error reply. Live-proven: a radio-initiated bounce mid-test produced a reconnect+reclaim that evicted the prior handle. - Evidence-based zombie eviction: the S<handle>| status source (parsed but previously discarded at the connection signal boundary) is now plumbed through; foreign writes to OUR pans' dBm range are counted per handle and warned (1st + every 25th). Three strikes AND radio-roster identity match (program AetherSDR + our own radio-reported station) → evict. Anything else — SmartSDR, other stations, unidentifiable non-GUI writers — is logged, never touched. - `get clients` bridge verb (documented): radio client roster, per-pan clientHandle/ownedByUs (also added to get pan/pans), foreign-write counters, evicted handles. `get pans` could only show minDbm drifting; this shows the culprit. - tools/zombie_session_sim.py: raw-TCP zombie simulator (registers as AetherSDR with the victim's station, replays display pan set min_dbm writes) used for the live proofs. Live findings on FLEX-8400M / 4.2.18: detection attributed and counted every simulated zombie; radio firmware self-heals the same-client_id topology via duplicate_client_id disconnects (both sides bounced — the client-side eviction is the backstop for pre-4.2 firmware like aethersdr#3951's 4.1.3); victim survived the hostile bounce with a clean reconnect+reclaim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…andle Found during the aethersdr#3984 defensive review: pan status parsing (client_handle, added in this PR) can legitimately reassign a staged pan's owner to another live client before we disconnect (ownership transfer between two sessions sharing a client_id). Reclaim eviction then read that handle as "our predecessor" and force-disconnected a healthy session — observed live as a forced=1 bounce loop between this Mac and the test Mac's instance. Stash our own handle at stage time and evict only when the staged pan still records exactly that handle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the two PR-specific commits (597ff84c, b4ce5766); the third commit 1c40d965 and its files (panscope.frag, SpectrumWidget.cpp) are the stacked #3975 and out of scope here. CI is green across build/macOS/Windows/CodeQL.
Overall
This is a well-architected fix. The layering is the right call, and the load-bearing layer is the cheapest one: because the radio broadcasts the reassigned client_handle to every connected client, the superseded session's own PanadapterModel::applyPanStatus flips ownedByClient() false and the ownership gate in wirePanadapter silences it — no timers, no eviction round-trip needed. Eviction is correctly positioned as the pre-4.2-firmware backstop. Conventions are respected (AppSettings via ourStationName(), RAII, QStringView/std::clamp), and the fail-open bias in ownedByClient (unknown/unparsable/0 → treated as ours) is the right default — a parse glitch degrades to "send anyway," never to "silently muted forever."
Signal-signature propagation checks out: both RadioConnection/WanConnection emit sites pass msg.handle (populated for S<handle>| status at CommandParser.cpp:77), both connect() calls use member-pointer syntax to the new 3-arg slot, and the public RadioModel::statusReceived relay stays 2-arg so TciServer/MemoryDialog are untouched.
Minor (non-blocking)
-
Duplicated hex-handle parsing. The
0x-strip +toUInt(&ok, 16)dance is now open-coded inPanadapterModel::ownedByClientandapplyPanStatus. RadioModel already has aparseClientHandle()free helper doing exactly this (RadioModel.cpp:130) — worth a smallstaticequivalent onPanadapterModelso the two copies can't drift (e.g. one treating0as "ours" and the other not). -
ForeignPanWritepan-id parse.noteForeignPanWriteIfAnyhand-parses the id withobject.mid(12).section(' ', 0, 0)while the main status dispatcher uses thepanReregex. It happens to be correct (and"display panafall "is excluded by the trailing space), but keying off the same regex/normalizePanadapterIdpath everything else uses would be more robust to future object-format tweaks.
One thing to confirm
The 3-strike foreign-write path force-disconnects another client — outward-facing and hard to reverse. The gate (≥3 writes to a pan we still own and radio-reported program==AetherSDR and matching radio-reported station) is tight, and you've documented that this exact path couldn't be provoked on 4.2.18. The residual risk is two legitimate same-station AetherSDR sessions, but since a pan carries a single client_handle, a foreign write to a pan the radio still says we own is genuinely anomalous — so the classification looks sound. Flagging only because it's the one branch that acts on another session and wasn't hardware-exercised end-to-end.
Nice work on the get clients verb + zombie_session_sim.py harness — turning "minDbm drifts between polls" into an attributable culprit roster is a real diagnostic upgrade. Thanks @jensenpat.
🤖 aethersdr-agent · cost: $3.5363 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Review: request changes
Deep pass over the eviction/ownership work (multi-agent finders + adversarial verify; the SpectrumWidget/panscope hunks are identical to #3975 and were reviewed there, not here). The get clients verb and the foreign-write detection/forensics are genuinely useful and I'd like to see them land. But two of the three defense layers are currently dead code, and the one live layer can force-disconnect a healthy client — so I'm requesting changes on the mechanism, not just lines. Each inline comment carries a fix (apply-able suggestion where clean, a code sketch where the fix is structural).
Dead / non-functional:
- Reclaim eviction never fires — the guard added in
b4ce5766readsm_staleSessionOwnHandle, which is captured after the new handle lands, making it always-false (RadioModel.cpp:2106 / :3946). This is why the "reclaim eviction: live-proven" note no longer holds. - Ownership-transfer tracking never runs —
classifyOwnedStatusdrops a reclaimed pan's foreign-owner status beforeapplyPanStatussees it, soownedByClient(ours)stays true and the Layer-1 gate is a no-op (PanadapterModel.cpp:71).
Unsafe (the one live layer):
3. A healthy second operator can be force-disconnected — the identity gate is station-name-only with no liveness check; two same-hostname operators collide (RadioModel.cpp:4082). Recommend gating the disconnect behind an opt-in (default off) per AGENTS.md, plus a recency window.
4. Fail-open ownership as eviction evidence — writes to a not-yet-attributed pan are tallied as writes to ours (RadioModel.cpp:4050). The eviction tally must fail closed.
5. Eviction state is never reset — recycled FLEX handles then suppress legitimate evictions and mislabel get clients (folded into comment 1).
Minor: get pan emits "0x" for an unset handle; braces-on-control-flow; and three hand-rolled copies of a handle parser that gates a disconnect — reuse the existing helpers.
Threading is clean — mutator (onStatusReceived) and reader (doGet) both run on the main event loop, no race.
Suggested path: land the detection + get clients half; hold the automatic eviction until findings 1-4 are addressed and you've decided (finding 3) whether client eviction should ship at all or stay opt-in. Independent of the rest of the queue, so nothing else is blocked.
Resolves the two textual conflicts against main's panstats/wavestats additions by union: the get-verb hint string lists both clients and wavestats, and the docs verb table keeps both rows. Merge (not rebase) per CONTRIBUTING.md stale-base policy.
…ciple II. Addresses both review passes on aethersdr#3981 (8 inline threads + 10 findings): Ownership (radio-authoritative, Principle II): - Stamp the radio-reported owner onto a held pan in the foreign-owner Ignore branch instead of only warning — Layer 1 now actually flips ownedByClient() for superseded sessions, and the rightful new owner's own echoes stop counting as zombie evidence (mutual-eviction loop). - Central pan-set gate in RadioModel::sendCommand covers every writer (band restore, center/bandwidth/zoom/fps), not just the dBm lambda. - dBm handlers refuse the whole operation before committing the local stream decoder, so a superseded session's display can't diverge. Eviction (opt-in, confirmed, evidence fail-closed): - 3-strike eviction now requires AppSettings["StaleSessionDefense"] {"EvictionEnabled": true} (default OFF — detection/forensics always run). fw 4.2+ self-heals duplicate client_ids; S<handle> originator semantics are unverified on the 4.1.x firmware this backstops. - evictStaleSession(): handles enter evictedPredecessorHandles only on a radio-acknowledged disconnect; refused disconnects stay retryable. - Evidence gate uses the new fail-closed PanadapterModel::ownerHandle() (unattributed pans can no longer frame a legitimate peer). - Reclaim-eviction guard fixed: our own handle is captured at registration and consumed at stage time (was captured post-reconnect, making the guard dead code). - Strike tallies + eviction marks reset on disconnect (handles are radio-boot-scoped and recycled). Plumbing and cleanups: - Drop the statusReceived signature widening in RadioConnection and WanConnection: messageReceived(ParsedMessage) already carries the S<handle> source; the tally hooks into onMessageReceived instead. - Fix dangling QStringView over the by-value temporary from const QMap::operator[] in applyPanStatus (UB); parse the owner once into a cached quint32. - panSnapshot(): empty clientHandle serializes as "" (not "0x"); ownedByUs folded into the snapshot; braces per style guide. - get clients: report evictionEnabled; document confirmed-eviction and per-connection-reset semantics, and the non-GUI-zombie roster limit. - zombie_session_sim.py: treat ConnectionResetError/BrokenPipeError as the eviction succeeding (was an unhandled traceback exiting 1 on the success path). - Rebased onto main; get-clients verb merged with wavestats additions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed maintainer fixes on top of your branch (with How the fixes map to the review threads:
Not addressed here (follow-up material, not blockers): unifying the pan-owner record with the #3856 The detection layer + 🤖 Pushed with Claude Code on behalf of KK7GWY |
ten9876
left a comment
There was a problem hiding this comment.
Approving — the review-fixes commit (679d4282) addresses all 10 findings from the two passes. Spot-verified the load-bearing ones in the code (not an exhaustive re-review of the +425-line delta): reclaim-eviction guard now captures the pre-reconnect handle at registration (no longer dead code); the foreign-owner Ignore branch stamps the new owner so Layer-1 ownedByClient() actually flips; the 3-strike eviction is gated behind StaleSessionDefense.EvictionEnabled (default OFF — detection/forensics still run); evidence uses the fail-closed ownerHandle(); per-disconnect state reset; and the applyPanStatus dangling-QStringView UB is fixed. The opt-in default-off eviction is the right call given the force-disconnect risk. Nice thorough remediation.
Summary
Fixes #3977 (the root cause of #3951). Stacked on #3975 (first commit here is that PR's fix; review the second commit).
A still-alive earlier AetherSDR session keeps running its auto-floor tracker against the pan a newer session has reclaimed, ratcheting
min_dbm/max_dbmin 0.1 dB steps and visibly bouncing the new session's trace every ~1–1.5 s (evidence: #3951's support bundle — ~250 foreign-handle status echoes, zero outbound dBm commands from the victim).The three layers
PanadapterModelnow parsesclient_handlefrom pan status — previously unparsed; ownership was assumed at creation. The dBm-range sender drops-and-warns when the radio-reported owner isn't us, so a superseded session goes quiet the moment the radio broadcasts the new owner. No timers, no new state machine.client disconnect 0x<handle>(SmartSDR's takeover behavior, reusing the existingdisconnectClientHandlesThen). Disconnecting a dead handle is a harmless error reply.S<handle>|status source — parsed byCommandParserbut discarded at thestatusReceivedsignal boundary — is now plumbed through (RadioConnection/WanConnectionsignature; RadioModel's public relay unchanged). Foreign writes to our pans' dBm range are tallied per handle and warned (first + every 25th). Three strikes and a radio-roster identity match (programAetherSDR+ our own radio-reported station — not local settings, which can differ) → evict. Anything else — SmartSDR, a different station, an unidentifiable non-GUI writer — is logged forensics only, never touched.Per review discussion: no display-side smoothing of foreign echoes — a foreign write to our pan is an anomaly to eliminate, not render gracefully, and smoothing would have masked #3951 instead of surfacing it.
get clientsbridge verb (+ pan snapshot fields)get panscould only show the symptom (minDbmdrifting between polls). The new verb shows the culprit: radio client roster (handle/station/program/isUs), per-handleforeignPanWritescounters with last pan + timestamp, andevictedHandles.get pan/panssnapshots now carryclientHandle+ownedByUs. Documented indocs/automation-bridge.md, withtools/zombie_session_sim.py(raw-TCP zombie: registers as AetherSDR with the victim's station, replaysdisplay pan set … min_dbm=…) as the repro/regression harness.Live proofs (FLEX-8400M, fw 4.2.18)
get clients.evictedHandlespopulated). The victim recovered fully — new handle, pan reclaimed, clean roster.client guiUUID made 4.2.18 fire its ownduplicate_client_iddisconnects — modern firmware self-heals the exact weird behaviour in the spectrum of v2665 #3951 topology. The client-side eviction is the backstop for pre-4.2 firmware (the reporter's 8600 runs 4.1.3, where the zombie demonstrably survived).TX safety
No TX-path changes; the simulator and all tests are RX/display-only (
display pan set min_dbmexclusively).💻 Generated with Claude Code (claude-fable-5 7/2/26) with architecture by @jensenpat
🤖 Generated with Claude Code