Skip to content

Lite's MCP reports every alert group Darling's does - #2410

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/2394-lite-alert-settings-parity
Aug 21, 2026
Merged

Lite's MCP reports every alert group Darling's does#2410
erikdarlingdata merged 1 commit into
devfrom
fix/2394-lite-alert-settings-parity

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Closes #2394 — scope 1 only, read-only parity.

Lite's get_alert_settings reported four groups (cpu, blocking, deadlocks, smtp) where Darling's reports nineteen. The issue's framing holds up completely against the source: nothing about Lite's alerting is narrower. PerformanceMonitor.Alerting evaluates the same alerts on both SKUs against IAlertEngineSettings, and AppAlertEngineSettings already projects every one of Lite's App.Alert* statics onto that interface. So an operator could have tempdb-space, low-disk, PVS, file-growth, long-running-query, long-running-job, failed-job, database-state and analysis alerting all live, and an agent triaging that instance could not read whether any of it was even switched on. The gap was purely the MCP surface, and this closes it.

Thirteen groups are added — poison_wait, long_running_query, tempdb_space, low_disk, pvs, file_growth, long_running_job, failed_job, database_state, cooldown_minutes, excluded_databases, delivery, analysis — spelled Darling's way down to the last key, in Darling's order, because one MCP schema across both apps is what #1839 and #1911 were for.

The two spellings that are easy to get wrong were checked against Darling's emitted shape rather than guessed. low_disk nests the #1136 CRITICAL floors as critical_free_percent / critical_free_gb, dropping the "disk" that the App.AlertDiskCriticalFree* statics carry — naming them after the statics would have read as correct and been a fifth key-level mismatch. And delivery.mode is a value-level alignment like cpu.mode, though it is the one place ToString() is right rather than a mapping: AlertNotificationMode is the shared enum both SKUs run on and Darling's store holds literally a.DeliveryMode.ToString(), so the two cannot drift the way Lite's app-local CpuAlertMode could.

The one group not added

self_alerts — Darling's own store volume, collection staleness/failure counts, and store-job cadence. This is a genuine Darling-only concept rather than an omission of convenience. AppAlertEngineSettings returns shipped constants (10, 30, 10) for three of its four members precisely because a single-instance WPF app has no headless store volume and no fleet collection loop to self-monitor, and Lite has no member at all for the fourth, store_job_cadence_warn_percent. Reporting constants under names that read as knobs would tell an agent it can tune something Lite cannot, and an admitted gap beats an overstated capability. The omission is asserted as a decision, not left to look like an oversight.

smtp stays as Lite's one addition — Lite delivers its own email, where Darling manages delivery credentials outside the settings row and reports no smtp group at all.

Why there is no update_alert_settings

Out of scope, and the issue is right that it needs a design decision first. Darling's tools write config_alert_settings and the service picks the change up through StoreConfigProvider.ApplyToConfig — a live reload with no restart. Lite has no equivalent plane: its settings are App.Alert* statics that the Settings window writes to settings.json, and SaveAlertSettings reads its controls, not the statics — so an open Settings window would silently overwrite an MCP-applied change on the next Save. That wants a change-notification seam (the issue's scope 3), not a tool bolted on next to the hazard.

Tests

McpAlertSettingsKeyTests is extended, not rewritten — its existing assertions are all still there. It grows from pinning three renamed keys to pinning the whole shape, which is the lesson of this issue: the drift it was written to catch had already happened at a scale no per-key assertion would notice.

The new parity assertion derives Darling's shape from Darling's source (BuildAlertSettingsPayload) rather than transcribing it. A hand-copied list of nineteen groups is exactly the artifact that stays green on the day a twentieth arrives. Both directions are asserted, so a key Lite emits that Darling does not fails too — two spellings of one setting across the two apps is the same class of bug as a missing one — and smtp is pinned as the exact set of legitimate Lite-only additions, so a second one cannot appear without someone justifying it here.

The parser proves itself before it is trusted: it asserts the parse found a plausible number of groups and that known anchors (cpu, analysis, self_alerts, and cpu's three nested keys) are present, because a parse that quietly returned nothing would make every assertion built on it vacuously true.

Verification, and what it does and does not cover

Lite.Tests targets net10.0-windows, so it builds on macOS but cannot run; CI is the arbiter. McpAlertSettingsKeyTests is a runtime test, so rather than assert it passes, the logic was verified three ways against the real files:

  1. The shipped parser loop, run for real. The DarlingPayloadShape scanning loop was copied verbatim into a throwaway net10.0 harness and run against the actual DarlingMcpAlertTools.cs and McpAlertTools.cs. It reads 19 groups from Darling with correct nested keys, and the parity comparison passes group-for-group and key-for-key.
  2. It fails without the fix. The same comparison against dev's pre-fix McpAlertTools.cs reports 6 groups and 13 parity problems — every group this PR adds. A check that passed both before and after would be worse than none.
  3. The source-to-wire link. The whole key-name argument rests on the C# identifier being the wire key, so that was verified rather than assumed: a harness reads the real McpHelpers.JsonOptions by reflection (it is internal) and confirms it carries no naming policy, that snake_case identifiers survive verbatim at both nesting levels, that List<string> serializes as an array under its identifier, and that AlertNotificationMode.Summary/PerEvent.ToString() are byte-for-byte the two values Darling's update_alert_settings accepts for delivery.mode.

What that does not cover: the App.Alert* static reads themselves, which need a WPF assembly and therefore CI.

Notes for review

No CHANGELOG entry, deliberately. #2395 is cutting [3.5.1] right now and dev has no [Unreleased] section, so an entry here would either collide with that diff or wrongly claim to be in a release this landed after. Happy to fold one into the next section once the cut lands.

One correction rode along: the class doc-comment claimed the payload is serialized "with a naming policy in McpHelpers.JsonOptions". There is no naming policy — the options object is new() { WriteIndented = false }. The reason the test is runtime rather than source-parsing is still sound (that object owns the mapping and could acquire a policy tomorrow, changing every key without this payload being touched), so the reasoning is kept and the false fact is fixed.

Two things worth separate issues, found while matching the shape — neither touched here:

  • Darling's AlertSettingsReadRow reads notify_connection_down_at_startup, connection_refire_minutes, notify_ag_health, ag_lag_alert_seconds, ag_redo_queue_alert_kb and ag_disconnect_refire_minutes, and BuildAlertSettingsPayload reports none of them. The reader's own comment says it was widened so "an MCP client could not see the V33 connection opt-ins or the V35 Availability Group family at all" — the reader was widened and the payload was not, so neither SKU reports the AG family. Lite has the matching statics (App.NotifyAgHealth, AgLagAlertSeconds, AgRedoQueueAlertKb), so this is a both-SKUs gap, and it is Darling's payload that has to move first for parity to mean anything.
  • Darling's get_alert_settings emits blocking.wait_threshold_seconds, but update_alert_settings' blocking group dispatches only on enabled and count_threshold — so feeding a full get_alert_settings payload back to it is rejected with Unknown field 'blocking.wait_threshold_seconds'. BuildAlertSettingsPayload's doc comment promises "the same field names update_alert_settings accepts on the way in, so a read → modify → write round-trips", and for that one key it does not.

🤖 Generated with Claude Code

get_alert_settings reported four groups -- cpu, blocking, deadlocks, smtp --
where Darling reports nineteen, and the gap was never that Lite's alerting is
narrower. The engine is shared: PerformanceMonitor.Alerting evaluates the same
alerts on both SKUs against IAlertEngineSettings, and AppAlertEngineSettings has
projected every one of Lite's App.Alert* statics onto that interface since the
Phase-5 forwarding. So an operator could have tempdb-space, low-disk, PVS,
file-growth, long-running-query, long-running-job, failed-job, database-state
and analysis alerting all running, and an agent triaging that instance could not
read whether any of it was even switched on.

Thirteen groups are added, spelled Darling's way down to the last key, because
one MCP schema across both apps is what #1839 and #1911 were for. The two that
are easy to get wrong were checked against Darling's emitted shape rather than
guessed: low_disk nests the #1136 CRITICAL floors as critical_free_percent and
critical_free_gb, dropping the "disk" the App statics carry, and delivery.mode
is a value-level alignment like cpu.mode -- though it is the one place
ToString() is right rather than a mapping, because AlertNotificationMode is the
shared enum both SKUs run on and Darling's store holds literally its ToString().

self_alerts is the one group deliberately left out. AppAlertEngineSettings
returns shipped constants for three of its four members precisely because a
single-instance WPF app has no headless store volume and no fleet collection
loop to self-monitor, and Lite has no concept at all of the fourth,
store_job_cadence_warn_percent. Reporting constants under names that read as
knobs would tell an agent it can tune something Lite cannot, and an admitted gap
beats an overstated capability.

McpAlertSettingsKeyTests grows from pinning three renamed keys to pinning the
whole shape, and it DERIVES Darling's shape from Darling's source rather than
transcribing it -- a hand-copied list of nineteen groups is exactly the artifact
that stays green on the day a twentieth arrives, which is the drift this turned
out to be. Both directions are asserted, so a key Lite emits that Darling does
not is a failure too, and smtp is pinned as its one legitimate addition.

No update_alert_settings here. Darling's writes land in config_alert_settings
and the service reloads through StoreConfigProvider.ApplyToConfig, whereas
Lite's settings are App.Alert* statics the Settings window owns, and
SaveAlertSettings reads its CONTROLS rather than those statics -- so an open
Settings window would silently overwrite anything an MCP write applied. That
wants a change-notification seam, not a tool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Review

Verified this line-by-line against Darling's source of truth (DarlingMcpAlertTools.cs / BuildAlertSettingsPayload) rather than trusting the diff at face value:

  • Key-for-key parity: every group and nested key Lite now emits (poison_wait, long_running_query, tempdb_space, low_disk incl. critical_free_percent/critical_free_gb, pvs, file_growth, long_running_job, failed_job, database_state, cooldown_minutes, excluded_databases, delivery, analysis) matches Darling's spelling and document order exactly, with self_alerts correctly omitted and smtp correctly the sole Lite-only addition.
  • All referenced App.Alert*/App.Analysis* statics exist in Lite/App.xaml.cs with the expected types (confirmed via grep, since this can't be built on this box — Lite.Tests targets net10.0-windows).
  • The "already wired" claim checks out: Lite/Services/AppAlertEngineSettings.cs already forwards every one of these statics into IAlertEngineSettings, so this is genuinely a read-surface-only change, not new alerting behavior.
  • No secrets leak: smtp still reports password_configured as a bool, never the credential itself — unchanged.
  • Lite/Darling parity: this is a Lite-catches-up-to-Darling fix; Darling's side is untouched and was already correct, so no drift introduced in the other direction.

No correctness, security, or parity issues found. One non-blocking observation: DarlingPayloadShape()'s regex/brace-depth parser in the test file is intricate (comment-stripping + depth-tracked identifier scanning), but it validates itself before use (Assert.InRange, known-anchor checks) and the PR description documents it was run for real against both pre- and post-fix source, so I don't consider it a risk.

Nice work on the self-verification methodology described in the PR body (running the shipped parser against real files, confirming the pre-fix failure mode, and confirming JsonOptions carries no naming policy via reflection) — that's exactly the kind of proof this class of "two apps must agree on wire spelling" bug needs.

@erikdarlingdata
erikdarlingdata merged commit 58cb8d9 into dev Aug 21, 2026
6 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/2394-lite-alert-settings-parity branch August 21, 2026 12:18
erikdarlingdata added a commit that referenced this pull request Aug 21, 2026
#2410 landed on dev between this branch's base and its first CI run, and it
changed the rules: GetAlertSettings_ReportsEveryGroupDarlingDoes_SpelledDarlingsWay
parses Darling's BuildAlertSettingsPayload and requires Lite's payload to carry
every group and key it finds. So Darling's half of #2417 could not merge alone
after all -- the Lite build went red naming exactly the three additions,
missing group 'notify_connection_down_at_startup', 'connection_refire_minutes'
and 'ag'. That test is doing precisely what it was written to do, and the
answer is to make Lite follow now rather than to weaken it.

Lite needs no new plumbing for six of the seven. App.NotifyConnectionDownAtStartup
and App.ConnectionRefireMinutes have driven the connection-alert path since
#1659, and App.NotifyAgHealth, App.AgLagAlertSeconds and App.AgRedoQueueAlertKb
have driven all four AG conditions since #1726. Only the MCP surface was
narrow, which is the same shape of gap #2394 fixed for the other nine groups.

The seventh is a real asymmetry rather than an oversight, so it is exempted by
name instead of faked. Darling's ag_disconnect_refire_minutes is #1696 / store
V37, and Lite has no AG disconnect re-fire at ALL -- no static, no settings.json
key, and no edge state that could re-announce a still-disconnected replica,
because #1726 mirrored three of those four knobs. Emitting it as a constant 0
would tell an agent it can tune something Lite cannot, which is the reasoning
the file already applies to self_alerts, here at member granularity instead of
group. LiteOmittedMembers holds the one entry, the comparison skips exactly
that key, and a new test asserts BOTH halves still hold -- that Darling still
emits it, so the exemption cannot become dead weight hiding a Darling
regression, and that Lite still does not, so it cannot mask a key that has
since arrived. The set is pinned exactly, so a second exemption has to be
justified in that test the way a second Lite-only group has to be justified in
the smtp assertion.

Lite.Tests cannot run on macOS, so the parity test's parser was ported to
Python and validated against the test's OWN self-checks before being trusted --
the 15-40 group count, cpu/analysis/self_alerts present, smtp absent, and cpu's
members exactly enabled/threshold_percent/mode. Run against dev's Lite payload
it reproduces CI's failure message byte for byte; against this branch's it
passes; and with the exemption removed it fails on ag.disconnect_refire_minutes
and nothing else, which is what shows the exemption is both load-bearing and
minimal. Darling's own round-trip invariant re-ran green after the merge.

The gap itself is worth closing and is not closed here: a replica disconnected
for a week is announced once on Lite where Darling can re-announce it. That is
#1696's Lite half, a feature with its own static, settings row, box and edge
state, and it does not belong in a payload-symmetry change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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