Skip to content

Land howl_guard (#78) on main - #81

Merged
tap merged 2 commits into
mainfrom
feat/howl-guard-land
Oct 1, 2026
Merged

tap merged 2 commits into
mainfrom
feat/howl-guard-land

Conversation

@tap

@tap tap commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Lands #78 (tap::mu::howl_guard, PR B of the safety layer) on main.

#78 was stacked on #77's branch feat/howl-detector, and when it was merged GitHub merged it into that branch rather than into main (#77 had already landed on main by rebase). So main has the detector but not the guard. This PR is main + #78's two commits, cherry-picked unchanged (b805626 → 7b53fac, 9d088ea → 92dff1e): the guard header, the afc_chain hook, the tests and sweeps, docs/howl-guard.md, HANDOFF item 12. Everything measured and claimed is in #78's description and its CI run (green: 35 passed, 1 skip); nothing here is new.

The chain is bit-identical with no guard attached (test_afc_chain.cpp output and the 14 mutap_fingerprint lines unchanged, as #78 verified). Locally on this branch (macOS x86_64, AppleClang, Release): the build is clean on main's DspTap pin and the guard's state-machine suite passes; CI runs the host rows.

PR C (#80, the re-arm hold, soundcheck calibration and cap rule) is re-stacked on this branch and retargets main once this merges.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DDhgJqxUsdmWPKh6pKrV5A

tap and others added 2 commits October 1, 2026 11:11
…off (phase 2 item 6b, PR B)

The safety layer's gain policy over the canceller's verdict (D and A') and a
howl_detector on each residual: ARMING / OPEN / OPEN_CAPPED / DUCKED /
RELEASING / LATCHED, every transition in the header; TRIP = the level catch
always or growth while the verdict is not ok; LOST = an ok -> not-ok edge
held trip_hold_s, disarmed after a timer re-arm until ok is seen; strikes,
back-off and latch; dB-linear ramps that land exactly; per-mic attribution
with a duck-all fallback; a post-reverb bus stage. Real-time contract as
fd_kalman.h.

afc_chain::set_guard(): runs the guard per mic on e_k after forwarding A'
and D, before the bus sum; static_asserts the canceller's statistics and
refuses (returns false) a canceller without the shadow or a guard of the
wrong shape; reset() re-arms an attached guard. Without a guard the chain
is unchanged (the plain bus sum is the old code; fingerprints identical).

Tests: the float/double state-machine suite (both emulated selections run
the float half), the live loop on afc_chain (tests/support/guard_loop.h,
guard_runs.h) with guard-independent oracles, 13 gated host rows and the
MUTAP_SLOW sweep. docs/howl-guard.md has every table; HANDOFF item 12.

Measured (macOS 15.7 x86_64, AppleClang 17, Release, double): 0 howl blocks
in 108 gated and 620 sweep cold starts; at the canceller's limit + 6, 0
guarded against 575 / 1203 unguarded; 0 ducks off a loop-born burst in 180
audible-cost runs; cost 0.60 / 0.88 % of a canceller per mic. Open: F -> 2F
still cycles duck / open (47 LOST-ducks after a re-arm in 20 of 30 sweep
runs, 0 howl); no declaration without a backing track (the cap opens those
runs); A' plateaus near -15 dB in silence, so restart_a_db (a new field,
-1 dB) fires only on a canceller reset.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDhgJqxUsdmWPKh6pKrV5A
…unts with margin

guard_runs.h: the [from, to) helpers over run_trace (blocks_in, howl_blocks,
entries, longest_howl_s) walk iterators clamped to their own vector instead
of an index loop against min(to, size()). GCC 13's
-Waggressive-loop-optimizations bounded size() only by the pointer
difference and failed the Linux build ("iteration 4611686018427387903
invokes undefined behavior" in howl_blocks via cold_stats::add_unguarded).

HowlGuardHost.TwoMicsAttributeASingleMicHowl: the wrong-mic-alone count was
gated at 0 on 8 chaotic runs; macOS arm64 CI (run 36828198214) read 1 of 8.
Now gated at <= 2 of 8 (the sweep's rate is 1 of 20), plus what the claim is
about: howl blocks <= 24 over the 8 runs and no howl run of 0.1 s. Measured:
Intel 0 of 8 wrong, 1 howl block, longest 0.0013 s; arm64 1 of 8, 3 blocks,
longest one block. Header, doc and HANDOFF quote both hosts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDhgJqxUsdmWPKh6pKrV5A
@tap
tap merged commit 27c5300 into main Oct 1, 2026
64 of 66 checks passed
tap added a commit that referenced this pull request Oct 2, 2026
…nstead of apt gcc-arm-none-eabi

Ubuntu's gcc-arm-none-eabi pulls the 463 MB libstdc++-arm-none-eabi-newlib
package from the runner's Azure mirror. That fetch stalled past the
15-minute step limit three times on 2026-10-01 (#81 twice,
#80 once) after eating two 45-minute legs on #67. Both
Cortex legs now restore Arm's arm-gnu-toolchain-13.2.rel1-x86_64-arm-none-eabi
from the Actions cache, or download it (179 MB from Arm's CDN, curl with
retries, verified against Arm's published sha256) on a miss, and put its
bin on PATH; only qemu-system-arm still comes from apt. It is the same
upstream release Ubuntu noble packages (15:13.2.rel1-2), so the
fingerprint and icount gates on this PR's own run are the check that the
numerics did not move.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDhgJqxUsdmWPKh6pKrV5A
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant