diff --git a/plugins/desktop-notification/.claude-plugin/plugin.json b/plugins/desktop-notification/.claude-plugin/plugin.json index 4b6ff36cfc..675821ca32 100644 --- a/plugins/desktop-notification/.claude-plugin/plugin.json +++ b/plugins/desktop-notification/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "desktop-notification", - "version": "0.5.2", + "version": "0.5.3", "description": "Alert you when Claude Code needs input — an audible terminal bell, an OSC 9 terminal notification, and an OS-native toast (macOS/Linux) on permission and idle prompts.", "author": { "name": "Melodic Software", diff --git a/plugins/desktop-notification/CHANGELOG.md b/plugins/desktop-notification/CHANGELOG.md index b81c5ad3ea..bb50e6e929 100644 --- a/plugins/desktop-notification/CHANGELOG.md +++ b/plugins/desktop-notification/CHANGELOG.md @@ -3,6 +3,29 @@ All notable changes to the `desktop-notification` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.5.3] + +### Fixed + +- C1 fd1-leak detector in `desktop-notification.test.sh`: the threshold that was + supposed to widen the slow-sink margin (`#751`, closing `#448`) could not actually + widen it — `THRESHOLD_MS` was derived as `SINK_SLEEP * 1000 / 2`, so widening + `SINK_SLEEP` widened the threshold by the same ratio and left the margin unchanged + by construction. `#448` was reopened after this reproduced on clean `main` + (delta=3697ms false-fail, no leak present). `THRESHOLD_MS` now asserts the real + invariant directly — sink-sleep-minus-a-safety-margin, not half the sleep — and + `SINK_SLEEP` is widened from 6s to 8s (still comfortably under the 10s ceiling + documented against EXIT-cleanup file-locking on Windows) for more absolute + separation between ambient noise and the leak signal. The safety margin is sized so + BOTH sides of the threshold clear the 2150ms of worst observed no-leak noise, not + just the noise side: a threshold too close to the leak signal lets a load shift that + inflates every baseline sample and then subsides before the slow run subtract real + leak signal out of the delta, and the detector reports no leak. At `SINK_SLEEP`=8s + and a 3000ms margin the threshold sits at 5000ms — 2850ms of noise-side margin, + 3000ms of leak-side margin. Verified on Windows Git Bash: 10 consecutive clean runs, + 40 runs under heavy concurrent load (worst observed no-leak delta ~1590ms), and a + deliberately reintroduced fd1 leak still fails the case (observed delta ~8065ms). + ## [0.5.2] ### Fixed diff --git a/plugins/desktop-notification/hooks/desktop-notification.test.sh b/plugins/desktop-notification/hooks/desktop-notification.test.sh index 052d1a1b3a..b149221240 100755 --- a/plugins/desktop-notification/hooks/desktop-notification.test.sh +++ b/plugins/desktop-notification/hooks/desktop-notification.test.sh @@ -226,8 +226,32 @@ rm -f "$TEL" # locks its stub file on Windows. A baseline run (fast sink, no sleep) captures # the SAME ambient overhead as the slow run under the SAME capture; subtracting # it cancels the overhead and isolates the leak signal. Under a leak the delta -# is ~SINK_SLEEP; with no leak it is ~0 (± scheduling jitter). Threshold is half -# the sleep: comfortably above jitter, comfortably below the leak signal. +# is ~SINK_SLEEP; with no leak it is ~0 (± scheduling jitter). +# +# THRESHOLD_MS asserts the actual invariant — "did not wait for the sink" — +# as sleep-minus-a-safety-margin, NOT half the sleep. Halving discarded margin +# for no gain and was proven too tight empirically (#448 reopen): a clean-main +# false-fail measured delta=3697ms against the old 3000ms/half-sleep threshold, +# and load-generated measurements up to ~2150ms (30 concurrent suite runs on +# Windows Git Bash) confirm noise alone can approach that old threshold. +# +# The threshold has TWO margins and both must clear that 2150ms worst observed +# noise, because the detector can fail in either direction: +# +# noise side threshold - max_noise a no-leak run must stay BELOW it +# leak side leak_signal - threshold a leaking run must stay ABOVE it +# +# The leak side is the one a too-HIGH threshold breaks, and it is not covered by +# the min-of-N baseline below: that protects against ONE unlucky baseline sample, +# not against a load shift that inflates ALL of them and then subsides before the +# slow run. Baselines 2s+ slower than the slow run's own overhead subtract real +# leak signal out of the delta, so a leak whose delta lands between the threshold +# and SINK_SLEEP passes silently. SINK_SLEEP=8s with SAFETY_MARGIN_MS=3000 puts +# the threshold at 5000ms, roughly midway between the signal and the noise: +# 2850ms of noise-side margin and 3000ms of leak-side margin, both above the +# 2150ms worst case measured. Recorded measurements bracket it — a deliberately +# reintroduced leak measured delta=8065ms (detected), while 40 runs under 30x +# concurrent load peaked at ~1590ms with no leak (passed). # # The baseline is the MINIMUM of several fast runs, not a single sample. A lone # baseline that happened to be descheduled longer than the slow run would shrink @@ -238,7 +262,8 @@ rm -f "$TEL" # unlucky sample. The opposite error — an inflated slow sample with no leak — # only re-fails a green run (fail-safe, re-runnable), so just the baseline needs # min-of-N; the slow run stays single (each slow sample costs SINK_SLEEP). -SINK_SLEEP=6 +SINK_SLEEP=8 +SAFETY_MARGIN_MS=3000 BASE_SAMPLES=3 BASE_SINK="$(make_sink "cat >/dev/null")" SLOW_SINK="$(make_sink "cat >/dev/null; sleep $SINK_SLEEP")" @@ -261,7 +286,7 @@ _TS1=$EPOCHREALTIME SLOW_MS=$(epoch_delta_ms "$_TS0" "$_TS1") DELTA_MS=$((SLOW_MS - BASE_MS)) -THRESHOLD_MS=$((SINK_SLEEP * 1000 / 2)) +THRESHOLD_MS=$((SINK_SLEEP * 1000 - SAFETY_MARGIN_MS)) echo " (C1 fd1-leak: base=${BASE_MS}ms (min of ${BASE_SAMPLES}) slow=${SLOW_MS}ms delta=${DELTA_MS}ms, threshold <${THRESHOLD_MS}ms, sink sleeps ${SINK_SLEEP}s)" if [[ $RC_SLOW -eq 0 ]]; then ok "telemetry/slow-sink: hook exit 0"; else fail "telemetry/slow-sink: hook exit $RC_SLOW"; fi if [[ $DELTA_MS -lt $THRESHOLD_MS ]]; then diff --git a/plugins/markdown-format/.claude-plugin/plugin.json b/plugins/markdown-format/.claude-plugin/plugin.json index 80593823b4..50f9a20670 100644 --- a/plugins/markdown-format/.claude-plugin/plugin.json +++ b/plugins/markdown-format/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "markdown-format", - "version": "0.6.4", + "version": "0.6.5", "description": "Auto-format and lint Markdown on edit via markdownlint-cli2, using the consuming repo's own markdownlint config.", "author": { "name": "Melodic Software", diff --git a/plugins/markdown-format/CHANGELOG.md b/plugins/markdown-format/CHANGELOG.md index 7065b5fc99..c0d36b2ee6 100644 --- a/plugins/markdown-format/CHANGELOG.md +++ b/plugins/markdown-format/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to the `markdown-format` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.6.5] + +### Fixed + +- C1 fd1-leak detector in the hook contract test: the differential threshold + introduced in `0.6.2` (ported from `desktop-notification` `#751`) carried the same + latent defect — `THRESHOLD_MS` was derived as `SINK_SLEEP * 1000 / 2`, so widening + `SINK_SLEEP` widened the threshold proportionally and left the margin unchanged by + construction (`#448`, reopened after reproducing on clean `main`: delta=3697ms + false-fail with no leak present, in the `desktop-notification` copy this test was + ported from). `THRESHOLD_MS` now asserts the real invariant directly — + sink-sleep-minus-a-safety-margin, not half the sleep — and `SINK_SLEEP` widens from + 6s to 8s (still under the 10s ceiling documented against EXIT-cleanup file-locking + on Windows) for more absolute separation between ambient noise and the leak signal. + The safety margin is sized so BOTH sides of the threshold clear the 2150ms of worst + observed no-leak noise, not just the noise side: a threshold too close to the leak + signal lets a load shift that inflates every baseline sample and then subsides + before the slow run subtract real leak signal out of the delta, and the detector + reports no leak. At `SINK_SLEEP`=8s and a 3000ms margin the threshold sits at + 5000ms — 2850ms of noise-side margin, 3000ms of leak-side margin. + No behavior change for this plugin — the hook is untouched; test-only. + ## [0.6.4] ### Fixed diff --git a/plugins/markdown-format/hooks/markdown-format.test.sh b/plugins/markdown-format/hooks/markdown-format.test.sh index 6c607f7b03..4e42c83af7 100755 --- a/plugins/markdown-format/hooks/markdown-format.test.sh +++ b/plugins/markdown-format/hooks/markdown-format.test.sh @@ -789,8 +789,32 @@ rm -f "$FAIL_SINK_FILE" # file on Windows. A baseline run (fast sink, no sleep) captures the SAME ambient # overhead as the slow run under the SAME capture; subtracting it cancels the # overhead and isolates the leak signal. Under a leak the delta is ~SINK_SLEEP; -# with no leak it is ~0 (± scheduling jitter). Threshold is half the sleep: -# comfortably above jitter, comfortably below the leak signal. +# with no leak it is ~0 (± scheduling jitter). +# +# THRESHOLD_MS asserts the actual invariant — "did not wait for the sink" — +# as sleep-minus-a-safety-margin, NOT half the sleep. Halving discarded margin +# for no gain and was proven too tight empirically (#448 reopen): a clean-main +# false-fail measured delta=3697ms against the old 3000ms/half-sleep threshold, +# and load-generated measurements up to ~2150ms (30 concurrent suite runs on +# Windows Git Bash) confirm noise alone can approach that old threshold. +# +# The threshold has TWO margins and both must clear that 2150ms worst observed +# noise, because the detector can fail in either direction: +# +# noise side threshold - max_noise a no-leak run must stay BELOW it +# leak side leak_signal - threshold a leaking run must stay ABOVE it +# +# The leak side is the one a too-HIGH threshold breaks, and it is not covered by +# the min-of-N baseline below: that protects against ONE unlucky baseline sample, +# not against a load shift that inflates ALL of them and then subsides before the +# slow run. Baselines 2s+ slower than the slow run's own overhead subtract real +# leak signal out of the delta, so a leak whose delta lands between the threshold +# and SINK_SLEEP passes silently. SINK_SLEEP=8s with SAFETY_MARGIN_MS=3000 puts +# the threshold at 5000ms, roughly midway between the signal and the noise: +# 2850ms of noise-side margin and 3000ms of leak-side margin, both above the +# 2150ms worst case measured. Recorded measurements bracket it — a deliberately +# reintroduced leak measured delta=8065ms (detected), while 40 runs under 30x +# concurrent load peaked at ~1555ms with no leak (passed). # # The baseline is the MINIMUM of several fast runs, not a single sample. A lone # baseline that happened to be descheduled longer than the slow run would shrink @@ -801,7 +825,8 @@ rm -f "$FAIL_SINK_FILE" # sample with no leak — only re-fails a green run (fail-safe, re-runnable), so # just the baseline needs min-of-N; the slow run stays single (each slow sample # costs SINK_SLEEP). -SINK_SLEEP=6 +SINK_SLEEP=8 +SAFETY_MARGIN_MS=3000 BASE_SAMPLES=3 BASE_SINK="$(make_sink "cat >/dev/null")" SLOW_SINK="$(make_sink "cat >/dev/null; sleep $SINK_SLEEP")" @@ -832,7 +857,7 @@ _TS1=$EPOCHREALTIME SLOW_MS=$(epoch_delta_ms "$_TS0" "$_TS1") DELTA_MS=$((SLOW_MS - BASE_MS)) -THRESHOLD_MS=$((SINK_SLEEP * 1000 / 2)) +THRESHOLD_MS=$((SINK_SLEEP * 1000 - SAFETY_MARGIN_MS)) echo " (C1 fd1-leak: base=${BASE_MS}ms (min of ${BASE_SAMPLES}) slow=${SLOW_MS}ms delta=${DELTA_MS}ms, threshold <${THRESHOLD_MS}ms, sink sleeps ${SINK_SLEEP}s)" if [[ $RC_SLOW -eq 0 ]]; then ok "telemetry/slow-sink: hook exit 0"; else fail "telemetry/slow-sink: hook exit $RC_SLOW"; fi if [[ $DELTA_MS -lt $THRESHOLD_MS ]]; then