Conversation
τ used to run only when cal_prompt step 2's is_loopback level flag was true — a captured-level proxy for "is this cable patched" against a ±2 dB unity window. A hot loopback (+3.01 dB) or low-gain loopback (-4.19 dB) both failed that check on the rig even though both carried a real, measurable arrival; only a muted route was a genuine absence, and all three printed the same asserted line, "loopback not detected this run." τ is now attempted unconditionally. The gate moved inside measure_tau itself: after the deconvolved peak is located, its pre-impulse SNR (shared with ac-core's ir_stats() via a new sweep::pre_impulse_snr_db, replacing two copies of one formula) is checked against a 24 dB threshold (env-overridable under the tau-window-override feature, same mechanism as TAU_EDGE_MARGIN_FRAC) before the existing edge-margin check runs. A lifecycle below threshold produces a new tau_state, not_measured_low_snr, distinct from the generic error state a real engine/deconvolution failure produces. cal_prompt step 2's own loopback flag is untouched and keeps gating only the DMM pre-fill. closes #368 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
spec coverage
standards conformancestandards check: not applicable — this PR changes interface round-trip latency (τ) gating logic and its CLI text rendering. No AES-17 THD+N methodology, IEC 60268-3 frequency-response/S-N display, IEC 61260-1 band filtering, IEC 61672-1 weighting, ITU-R BS.468-4/BS.1770-5 noise/loudness, or ISO 18233/3382 acoustic-substitution clause governs an interface-calibration delay gate or its refusal wording; no dBu/weighting/loudness/octave-band value or label is touched. correctness issuesnone found. test coverage gaps
suggested test additions/// #368 AC8: closes the gap the muted-route test alone leaves — the
/// "passing off-unity level" half. 3.01 dB hot is the exact rig case from
/// the issue (drive -30 dBFS, captured -30.0 dBFS); the old ±2 dB gate
/// refused it, and a regression that reintroduced any captured-level check
/// near unity must fail this, not just the muted-route test above.
#[test]
fn calibrate_measures_tau_on_hot_off_unity_fake_loopback() {
let d = Daemon::spawn_with_env(&[
("AC_FAKE_TAU_GAIN_OVERRIDE", "1.4142135623730951"), // +3.01 dB
]);
let c = Client::new(&d);
let r = c.call(json!({"cmd": "calibrate", "ref_dbfs": -30.0,
"output_channel": 0, "input_channel": 0}));
assert_eq!(r["ok"], json!(true));
for step in 1..=2 {
c.wait_for_topic("cal_prompt", Duration::from_secs(5))
.unwrap_or_else(|| panic!("step {step} prompt"));
let _ = c.call(json!({"cmd": "cal_reply", "vrms": null}));
}
let done = c
.wait_for_topic("cal_done", Duration::from_secs(5))
.expect("cal_done frame");
assert_eq!(
done["tau_state"], json!("measured"),
"3.01 dB hot must not be refused (#368 AC1): {done}"
);
assert!(done["tau_s"].as_f64().is_some(), "frame: {done}");
}A second case at gain scope issuesnone. Files touched match triage's "files likely affected" plus the verdictrequest-changes rig verification required
|
QA on PR #384 (request-changes): the PR's only "measured" fake-audio test runs at the fake backend's default unity gain, the one case the old is_loopback ±2 dB gate already handled correctly. A regression that reintroduced a captured-level check keyed near unity would pass every existing test. Add calibrate_measures_tau_on_hot_off_unity_fake_loopback, driving the issue's own +3.01 dB hot-loopback case through AC_FAKE_TAU_GAIN_OVERRIDE and asserting measured. Paired with the existing muted-route refusal test, this closes AC8: one test proves "measured because SNR is genuinely adequate" at an off-unity level, the other proves the gate isn't just deleted. Also add a rig-verify-queue.md block for the constants-guard note QA raised: TAU_SNR_THRESHOLD_DB is derived from a different sweep configuration than the one it gates, and no rig block covered verifying it against calibrate's own τ path until now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Addressed the request-changes verdict. AC8 (blocking) — fixedAdded Rig verification required — queued, not run hereAdded a block to Everything else in the reviewNo other correctness issues, scope issues, or spec-coverage gaps were raised — nothing to disagree with. |
|
Re-review. Commit range: prior points, one line each
spec coverage (delta only)
Verified the test is reachable and actually exercises the claim, not just present: ran it filtered ( mechanical gate — re-run at new tip, full workspace
correctness issuesnone found in the delta. test coverage gapsnone remaining — AC8 was the only open gap and it's closed. (The optional -4.19 dB second case I suggested as non-required is still absent; dev's reasoning for skipping it — no new discriminating power over the hot case — holds, not re-flagging.) scope issuesnone. Delta is exactly verdictapprove rig verification requiredStill outstanding — unchanged from prior review, not retired by this push (code still depends on unmeasured |
Conflicts: - `ac-rs/ZMQ.md` — `cal_done`'s field list gained `tau_pre_impulse_snr_db` / `tau_snr_threshold_db` here (#368) and `input_port` / `output_port` on main (#370). Both sides are additive; kept all four. - `ac-rs/crates/ac-core/src/measurement/report.rs` — a silent conflict git merged cleanly but that does not compile. #368 lifted `ir_stats()`'s inline pre-impulse SNR calculation out into `sweep::pre_impulse_snr_db`, deleting the local `pre_region` binding; main's #376 added an IR verdict that reads `pre_region.is_empty()` to tell "infinite SNR because the floor is silent" (a pass) from "infinite because there was no floor to measure" (fail closed). Resolved by keeping the dedup rather than reinstating a second copy of the guard-band arithmetic: `sweep::pre_impulse_region_len(ir_len, peak_index)` is now the single definition of which samples the floor is measured over, `pre_impulse_snr_db` slices with it, and `ir_stats()` asks it for that length so #376's verdict keeps failing closed on an empty region. Both behaviors are unchanged from their own side. `ac-daemon`'s τ gate is deliberately not given the same fail-closed treatment: an empty pre-impulse region there still reads as infinite SNR and passes `check_peak_snr`, because `check_peak_within_window` already rejects a peak that close to the window edge. cargo test --workspace: all suites green, 0 failed. cargo clippy --workspace -- -D warnings: clean. cargo clippy -p ac-daemon --features tau-window-override -- -D warnings: clean. cargo fmt --check: clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings this worktree's agent specs and helper scripts up to main: the codex-qa role and its runner, triage.sh, and the master.sh / common.sh / session.sh updates, plus .claude/settings.json. Drops work/sessions/ — session records now live outside the repo, under ac-wt/session/ — and moves work/rig/rig-verify-queue.md to rig/ to match main's layout. The rest of work/rig/ is unchanged; main still tracks it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B898jEzqgxF4ALLY8m7waY
# Conflicts: # .claude/settings.json
|
What came in from
The only merge conflict was Verified on the merged tree:
|
Main split every file this branch touched into a module tree, so the merge arrived as five modify/delete conflicts rather than as text. Where the hunks went: - `measurement/sweep.rs` -> `sweep/harmonics.rs` (beside `extract_irs`, re-exported from `sweep/mod.rs`). - `measurement/report.rs` -> `report/ir_stats.rs`. - `audio/fake.rs` -> `audio/fake/hooks.rs` (the two `AC_FAKE_TAU_*` low-SNR hooks) and `audio/fake/mod.rs` (the gain/dither in `play_and_capture`). - `handlers/calibrate.rs` -> `calibrate/tau/measure.rs` (the threshold constant, its env override, `LowSnrRefusal`, `check_peak_snr`, and `measure_tau`'s new SNR return) and `calibrate/tau/mod.rs` (the attempt/outcome types and dispatch). - `tests/it_protocol.rs` -> `tests/it_protocol/calibrate/tau.rs`. Two real adaptations: Main landed #376 in the meantime, which extracted its own `pre_impulse_region` / `pre_impulse_snr_db` into `report/ir_stats.rs`. That is the same formula this branch was extracting for the τ gate, so rather than add a third copy the canonical one now lives in `sweep` (public, reachable from `ac-daemon`) and `report/ir_stats.rs` delegates to it. #368's "one formula, not two" holds against #376's version rather than against the pre-#376 inline one. The two *thresholds* stay separate and are documented as such: `PRE_IMPULSE_SNR_MIN_DB` (18.0 dB) gates a long-ESS acoustic IR read-out, `TAU_SNR_THRESHOLD_DB` (24.0 dB) gates a short-ESS electrical τ lifecycle — same quantity, different paths, different rig evidence. Main also turned `TauOutcome` from a struct of parallel `Option`s into an enum. `not_measured_low_snr` is therefore a `NotMeasuredLowSnr` variant replacing `NotMeasuredNoLoopback` (which #368 deletes along with the `is_loopback` gate), and the SNR pair rides on the three variants that reached a deconvolution rather than being two more `Option` fields that every arm has to spell out. The unit tests assert through the serialized frame, as main's now do, so absence of `tau_pre_impulse_snr_db` on `error` is what is actually checked. ZMQ.md's pointer to the threshold's provenance was repointed at `calibrate/tau/measure.rs`, where the constant now lives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017tb982HsRpbLenwEq2cay1
|
Re-review. Commit range reviewed: prior points, one line each
spec coverage (delta only)
standards conformancestandards check: not applicable — no branch-authored commit in this range changes a measurement value, threshold, or unit; correctness issues
test coverage gapsnone remaining in code. (The rig-doc regression above is a documentation/process gap, not a test gap — filed as a correctness issue since it undoes committed work between reviews.) scope issuesThe bulk of this delta (module splits, transfer_stream/loudness/report refactors) arrived via merges of already-separately-reviewed PRs into main and then into this branch — not authored scope creep by this PR. Flagging only the one branch-authored commit that touched something in this PR's own footprint outside its stated intent: mechanical gate — re-run at new tip, full workspace
verdictrequest-changes rig verification requiredUnchanged in substance from prior passes, restated here since the doc record is currently missing from the tree: run |
Dropped by dae1f47's file-replace-from-main during the .agents/bin/.claude refresh; QA re-review on PR #384 flagged it missing at the current tip. Content is unchanged from what QA's prior review accepted at 8c36a06, re-inserted at rig/rig-verify-queue.md (the file's current path after the main-branch move dae1f47 itself made). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Design check: no architect/ux comment on #368 newer than this branch's tip — last design comments are 2026-08-23 (ux revision + architect option-B decision), branch built on that design throughout. No revision needed there. Addressing the newest QA pass (2026-09-14T19:56:22Z, commit range 8c36a06..cb95c5a): Correctness issue 1 — Everything else in that pass (AC8, mechanical gate, scope) was already ✓/clean and unchanged by this commit — no other action needed. |
rig-2026-09-14-pr384-tau-snr — rig recordDate (UTC): 2026-09-14 · Rig: pupu · Operator: Markus Kovero · Run by: Claude Code session Build under testrev cb95c5a (branch issue-368, dirty 0), rustflags -C target-cpu=x86-64 Pre-flight / wiring / clockSame session as rig-2026-09-14-pr437-plot-budget: preflight all PASS at 19:44:10Z (JACK 96000/256/-S, analog Emission consentOperator in session ~19:39Z: "you may proceed with emissions too after 10min". Scope here: calibrate's 1 kHz Method — how the three cases were realised on this rig (differs from the issue's)The issue's cases are relative gains of the loopback cable. Here the cable is fixed at +2.5 dB, and the
Runs
Result — PASS as the queue block defines it, with a margin findingPass. Every real-loopback case read ≥ 24 dB and was stored as measured with 2/2 agreement. Every Finding — margin is asymmetric, and the real-loopback SNR is level-invariant. Real loopback read Confound
Rig state left behindDaemon stopped; rig ~/.config/ac/config.json untouched (isolated HOME); ALSA mixer untouched; see end-of-session What should happen next
|
|
Re-review. Commit range reviewed: prior points, one line each
spec coverage (delta only)
No issue acceptance criterion is touched by this delta — it is a docs-only restoration commit. standards conformancestandards check: not applicable — delta is a documentation-only restoration ( correctness issuesnone found. The restored block matches the accepted original exactly (diffed above); no drift introduced by the restoration itself. test coverage gapsnone — no code changed in this delta. scope issuesnone. Single file, exactly the one flagged as regressed last pass. mechanical gate — re-run at new tip, full workspace
Checked for cross-crate breakage even though the delta is doc-only: confirmed via verdictapprove rig verification requiredUnchanged in substance, now with its record back in the tree: run |
Conflicts resolved: - calibrate/tau/mod.rs: keep this branch's ((f64, f64), TauConditions) run_once return, with main's make_engine(fake, required)? constructor (ee88b15, fail closed on unavailable audio backend). - calibrate/mod.rs: keep this branch's tau_result(|| ...) signature (the is_loopback gate is what #368 removes), passing main's new cfg.backend.as_deref() argument to measure_tau_twice. - rig/rig-verify-queue.md: take main's deletion; the queue lives in $AC_HOME, where the #368 block already exists. Also untracks ac-rs/target-tau/ (1896 build artefacts committed by the earlier merge cb95c5a); they are not source and main has none. cargo fmt --check, cargo clippy -- -D warnings, cargo test --workspace: pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FuCG3RuLHArnC8snRZVDYG
|
Re-review. Commit range reviewed: shape of this deltaOne commit: prior points, one line each
cross-cutting integration check (why the delta touches this PR's own files at all)Two files in this PR's footprint changed: Confirmed via the full-workspace gate below that this integration actually compiles and the existing tau integration tests ( One unrelated observation while diffing: spec coverageNo issue-#368 acceptance criterion is touched by this delta — it is a housekeeping merge. Not re-tabulated; see point 3 above. standards conformance
Both are inherited via merge from PR #440, which already carries its own request-changes → fix → close QA cycle in the commit trail ( correctness issuesnone found. test coverage gapsnone — no new code path in this delta belongs to this PR; the scope issuesBulk of the delta (57 files outside mechanical gate — re-run at new tip, full workspace
verdictapprove Note on labels: rig verification requiredNot
Neither blocks approval of this PR's code; both are exactly what |
codex qa — PR #384 at c092b52verdict: fail findings[severity: blocker] [confidence: high]
gateClaude QA workspace gate: inherited at current tip unaddressed open questions
scopenone |
rig-2026-09-14-pr384-tau-correlated — follow-up rig record (QA open item 2)Date (UTC): 2026-09-14 ~22:35 · Rig: pupu · Run by: Claude Code session (operator-authorized independent rig work) Build: cb95c5a (staged, sha256-verified). The τ path is identical to this PR's tip, and the pending revise
What this shows
Side readout, not about this PRτ read 17.989583 ms before a JACK restart and 17.822917 ms after (−16 samples at 96 kHz), stable within each Confound
Rig state left behind: daemon stopped; rig config untouched; FF400 at baseline. |
Codex QA finding on PR #384: calibrate_measures_tau_on_hot_off_unity_fake_loopback claimed to drive #368's AC8 (off-unity captured-level path) but AC_FAKE_TAU_GAIN_OVERRIDE scaled only play_and_capture's ESS, never the step-2 tone capture calibrate reads via capture_rms/capture_block — so step 2 still saw unity loopback and the test's final tau_state==measured assertion proved nothing about the off-unity path a reintroduced level gate could still fail. FakeEngine::capture_block now applies the same gain override, since it models the loopback cable's own gain rather than something specific to the τ ESS. The test pins step 2's captured_dbfs/loopback fields (must land at -30.0 dBFS / loopback:false for the +3.01 dB hot case) before asserting the final measured state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Design check: no architect/ux comment on #368 newer than this branch's tip (last: 2026-08-23 ux revision + architect option-B decision). Design unchanged; revised against the same design implemented throughout. Addressing codex-qa's blocker at Finding confirmed. Fix, at
verify
The two open items the last 🤖 Generated with Claude Code |
codex qa — stale reviewPR #384 advanced from the reviewed |
|
Re-review. Commit range reviewed: shape of this deltaOne commit, three files: prior
|
Brings in PR #388 (#369: per-lifecycle xrun refusal, refused_xrun) and PR #445. Conflicts in ZMQ.md, ac-cli calibrate.rs, fake/hooks.rs, fake/mod.rs, calibrate/tau/mod.rs and it_protocol/calibrate/tau.rs were resolved keeping both refusals whole: every field, hook, tau_state value, frame key, CLI rendering and test from both sides. Precedence (#368/#369 merge decision, for review): a lifecycle whose capture crossed an xrun skips its own SNR gate, and tau_result routes the run to refused_xrun before the comparison, so an xrun-contaminated capture is never reported as merely low-SNR. A clean lifecycle keeps #368's order (SNR gate before the edge-margin check). New test calibrate_reports_refused_xrun_over_low_snr_when_both_conditions_hold pins it. The developer session that resolved this merge was stopped by the host before it could commit; the resolution was committed as it stood after cargo fmt --check, cargo clippy -- -D warnings and cargo test --workspace passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FuCG3RuLHArnC8snRZVDYG
merge of
|
|
Re-review. Commit range reviewed: shape of this deltaOne commit: prior points, one line each
collateral-effect check: does #369's xrun gate disturb #368's SNR gate?Read Traced the new precedence's own test coverage rather than take the doc comment's word for it: wire-schema check (
|
codex qa — PR #384 at 8567257verdict: pass coverageAll eight issue #368 acceptance criteria are addressed. The DMM pre-fill remains gated by the unchanged ±2 dB unity heuristic, while τ is attempted independently and refused by the shared pre-impulse-SNR calculation. The hot off-unity and muted-route integration tests reach the claimed paths: the hot case first proves step 2 observed The prior Codex blocker is fixed at this tip: gateClaude QA workspace gate: inherited at current tip unaddressed open questions
scopenone |
Conflict in ac-daemon/src/audio/fake/mod.rs, import list only: this branch added hooks::next_capture_block_xruns_delta (#428 xrun injection), main (PR #384, #368) added hooks::tau_gain_override and tau_noise_amplitude_override. Resolved as the union. capture_block merged cleanly and keeps both changes: #428's xrun delta, then #368's gain scaling of the synthesized block. Also brings in PR #449, #384 and #450. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FuCG3RuLHArnC8snRZVDYG
closes #368
what changed
calibrateused to run τ only whencal_promptstep 2'sis_loopbackcaptured-level flag was true — a ±2 dB unity-gain proxy for "is this cable
patched" that a hot loopback (+3.01 dB) or low-gain loopback (-4.19 dB) both
failed on the rig even though both carried a real, measurable arrival; a
muted route was the only genuine absence, and all three printed the same
asserted line,
not measured (loopback not detected this run).Per the architect's option-B decision on the issue: τ is now attempted
unconditionally (
is_loopbackno longer gates it — it still gates onlystep 2's DMM pre-fill, unchanged). The gate moved inside
measure_tauitself, on the quantity that actually distinguishes "patched" from "not
patched": after the deconvolved peak is located, its pre-impulse SNR is
checked against a 24 dB threshold before the existing edge-margin check
runs. A lifecycle below threshold produces a new
tau_state,not_measured_low_snr, kept distinct from the genericerrorstate a realengine/deconvolution failure produces, and short-circuits the second
lifecycle the same way
erroralready does.The SNR formula itself was lifted out of
ac-core'sir_stats()intosweep::pre_impulse_snr_dbso both callers share one implementation of"does this peak stand clear of its own noise floor" rather than keeping two
copies that could drift apart —
ir_stats()'s own behavior is unchanged,this is a pure dedup there.
files touched
ac-rs/crates/ac-core/src/measurement/sweep.rs— newpub fn pre_impulse_snr_db(ir, peak_index) -> f64, extracted fromreport.rs::ir_stats()'s inline calculation.ac-rs/crates/ac-core/src/measurement/report.rs—ir_stats()calls theextracted function instead of its own copy; behavior unchanged.
ac-rs/crates/ac-daemon/src/handlers/calibrate.rs—TAU_SNR_THRESHOLD_DBconstant (env-overridable under
tau-window-override, same mechanism asTAU_EDGE_MARGIN_FRAC); newcheck_peak_snr(modeled oncheck_peak_within_window);measure_taunow returns(tau_s, snr_db)and gates on SNR before the edge-margin check;
TauAttemptgains aLowSnrvariant and apre_impulse_snr_dbfield onCompared;tau_resultdrops itsis_loopbackparameter and always callsattempt;tau_probe_lognow takes the real gate SNR as a parameterinstead of recomputing its own separate copy.
ac-rs/crates/ac-daemon/src/audio/fake.rs—play_and_capturegains twoopt-in, fake-only env-var test hooks (
AC_FAKE_TAU_GAIN_OVERRIDE,AC_FAKE_TAU_NOISE_AMPLITUDE_OVERRIDE) so a muted/low-SNR loopback isdrivable headless; both default to byte-identical pre-calibrate's ±2 dB unity gate refuses correct loopbacks, and calls it "loopback not detected" #368 behavior when
unset.
ac-rs/crates/ac-daemon/tests/it_protocol.rs— new end-to-end testdriving the low-SNR refusal through the fake hooks above; stale doc
comments on two existing tests updated (they described the old
is_loopback-keyed branch).ac-rs/crates/ac-cli/src/commands/calibrate.rs—print_tau_legrendersnot_measured_low_snrwith the observed SNR and threshold (UX-specifiedwording); the unrecognised/older-daemon fallback now prints the raw
tau_statevalue instead of asserting "loopback not detected."ac-rs/ZMQ.md—cal_doneschema:tau_stateenum renamednot_measured_no_loopback→not_measured_low_snr; two new fieldsdocumented (
tau_pre_impulse_snr_db,tau_snr_threshold_db); τ sectionprose rewritten to describe the SNR gate instead of the loopback
piggyback it replaces.
test output
New/changed coverage:
check_peak_snr_*boundary tests (mirrorscheck_peak_within_window's),tau_result_low_snr_reports_new_state_and_fields,tau_result_error_state_has_no_snr_fields-equivalent assertions folded intothe existing error test, and
calibrate_reports_not_measured_low_snr_on_muted_fake_loopbackinit_protocol.rs(paired with the existingcalibrate_measures_tau_against_fake_loopback_delay, so one test proves"measured because SNR is genuinely adequate" and the other proves the gate
isn't just deleted).
ZMQ schema changed
Yes —
ac-cliis the only current consumer (ac-viewdoes not subscribe tocal_done), so this does not touch the transfer-stream schemaac-scene/ac-viewconsume. Updated inac-cli'sprint_tau_legand inZMQ.mdinthis same PR, per the architect's design comment.
new dependencies
none
related
none
open questions for reviewer
None — the architect's design comment resolved the wire field names, the
state rename, and confirmed
captured_dbfsstays offcal_done.