fix(qso-answerer): D-CALLER-018/016/019 — abort hard-stop, faster re-engage, dblclick cleanup#72
Open
frank001 wants to merge 1 commit into
Open
fix(qso-answerer): D-CALLER-018/016/019 — abort hard-stop, faster re-engage, dblclick cleanup#72frank001 wants to merge 1 commit into
frank001 wants to merge 1 commit into
Conversation
…engage, dead-code cleanup Abort must be a hard stop per the Captain's stated requirement: once clicked, nothing may re-engage TX until the operator explicitly requests it again. AbortAsync previously no-opped whenever _state was already Idle, so a pending CQ target or jump-in armed by AnswerCqAsync/ EngageAtAsync (which fire independently of _state while Idle) could not be cancelled — it fired regardless, up to ~30s later, no matter how many times Abort was clicked (14 no-op clicks observed in logs/openswfz-20260712T211150Z.log). AbortAsync now unconditionally clears the pending-target/jump-in fields before its Idle guard. Also raises MaxLateStartSeconds from 1.5s to 2.0s (D-CALLER-016): the 1.5s threshold left almost no realistic click budget after decode processing, so nearly every manual engagement missed its window and was deferred a full 30s. 2.0s still leaves a 0.36s safety margin against FT8 overrun while catching realistic latency. D-CALLER-019 removes web/js/main.js's redundant legacy CQ-row dblclick handler (predates D-CALLER-012's engage-decode handler, which already reproduces it plus does abort-first) — it always lost the race and produced a guaranteed spurious 409 on every double-click. Carried its stray-selection cleanup forward into the remaining handler. Verified live against a real isolated daemon over its real HTTP/WebSocket API (no mocks) — qa/d-caller-018-abort-hard-stop-live-verify/ — including a deliberate control run with the fix reverted that reproduces the exact production failure, and a real-browser double-click check proving D-CALLER-019 leaves exactly one outbound POST. Full suite 1176/1176. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements
dev-tasks/2026-07-12-answerer-abort-hard-stop-and-reengage-timing.mdin full (D-CALLER-018 critical, D-CALLER-016 high, D-CALLER-019 medium/cleanup).AbortAsyncpreviously returned immediately whenever_statewas alreadyIdle, which meant a pending CQ target (AnswerCqAsync) or jump-in target (EngageAtAsync) armed while Idle could not be cancelled by the operator — it fired regardless, up to ~30s later, no matter how many times Abort was clicked (14 no-op clicks observed inlogs/openswfz-20260712T211150Z.log).AbortAsyncnow unconditionally clears the pending-target/jump-in fields before its Idle guard, matching the same fieldsSafeAbortToIdleAsyncalready clears. Per the Captain's stated requirement: "Abort just means stop all and everything... Do not engage TX again unless the operator decides otherwise."MaxLateStartSecondsfrom 1.5s to 2.0s. The 1.5s threshold left ~0.8-1.0s of realistic click budget after decode processing (0.4-0.7s observed), so nearly every manual click missed its window and was deferred a full 30s (phase alternates every 15s, so the next matching-phase window is always two cycles away). 2.0s still leaves a 0.36s hard safety margin against FT8 overrun.web/js/main.js's redundant legacy CQ-row dblclick handler (predates D-CALLER-012'sengage-decodehandler, which already reproduces it exactly plus does abort-first). It always lost the race and produced a guaranteed spurious 409 on every double-click. Carried its stray-selection cleanup (window.getSelection()?.removeAllRanges()) forward into the remaining handler so that behavior isn't lost.Test plan
dotnet build OpenWSFZ.slnx -c Release— 0 errors, 0 warningsgit stashof justQsoAnswererService.cs) and pass against the fix — not just green, meaningfully greenqa/d-caller-018-abort-hard-stop-live-verify/live_verify_abort_hardstop.pydrives a real isolated daemon over its real HTTP/WebSocket API (noFakeTimeProvider, no mockedIPttController), reproducing the exact production sequence — arm a pending CQ target late, let the guard defer it, hammer Abort x14 while Idle, wait past the next matching-phase window, confirm no re-engagement. PASS with the fix; deliberately reverted the fix and reran — FAIL, reproducing the exact log line ("pending CQ target 'Q9BUG' ... answering at A phase"firing ~29s after the hammer) — proving the script is a real regression guard, not a false PASS. Seeqa/d-caller-018-abort-hard-stop-live-verify/README.mdfor the full report index.index.html, syntheticdecodeWS frame → real unmodifiedhandleDecodes()creates a real<tr>, real browser double-click → exactly onePOST /api/v1/tx/engage-decode, zeroPOST /api/v1/tx/answer-cq, zero console errors. (One-off Playwright driver not committed — repo has no Node/Playwright dependency elsewhere; result recorded in the README above.)🤖 Generated with Claude Code