Skip to content

MCP: Lite's system-health, default trace, wait stats and blocking stats tools say where their data starts (part of #4966) - #5172

Merged
erikdarlingdata merged 14 commits into
devfrom
fix/4966-mcp-lite-system-health
Oct 5, 2026
Merged

erikdarlingdata merged 14 commits into
devfrom
fix/4966-mcp-lite-system-health

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Thirteen more Lite MCP tools now say where their data starts. This PR also carries the default trace, wait stats and blocking stats work first opened as #5180, folded in so the two land together; #5180 is closed.

System health: the nine get_health_parser_* tools

What an MCP client sees

The nine get_health_parser_* tools (system_health, severe_errors, io_issues, scheduler_issues, memory_conditions, cpu_tasks, memory_broker, memory_node_oom, significant_waits) now say where this server's system_health data starts.

  • Data answers carry effective_start, window_truncated and truncation_note right after hours_back, always present. A covered window gives false, null and the asked start. effective_start is UTC with a Z. There is no effective_hours_back.
  • empty answers carry the same three keys under hints. This covers all three empty rungs of the empty ladder: events captured and gated out, a quiet window, and a session that is read but never recorded this type. A window the store holds nothing in gives effective_start: null and window_truncated: true, so an empty answer past coverage no longer reads as "nothing happened".
  • unavailable and not_collected answers are unchanged and carry no keys.
  • A window of 90 minutes or less with rows starts no probe. An empty answer is always probed.
  • Tool descriptions are unchanged.

The envelope is built in one place: a private WindowNoticeAsync helper in McpHealthParserTools, and WitnessStatus (the empty ladder's one builder) takes an optional hints. The probe is GetQueryWindowFloorAsync(SystemHealthEvents, ...), the same source the nine reads window on (v_system_health_events, event_time). It counts events of every type, because one collector stores them all, so it says where the session's capture starts, not where one category's first hit sits. The code comment says so.

The event-time decision

EventWindowNoticeAsync is not used. A first run of this collector can store events from before itself (up to the 10-minute event fallback window), but the probe already reads event_time, the same column the reads window on. So it names such an event itself, and no row on a page can be older than the probe's floor. A test seeds a first run 100 minutes into the window with an event from 30 minutes in, and checks that all nine tools name the event and report window_truncated: false.

Test plan

  • New Lite.Tests/McpWindowNoticeSystemHealthToolTests.cs, a theory over the nine tools with a fixed as_of: a notice when collection starts inside the window; none on a quiet start; the key order; a short window starts no probe; a capped page (limit: 1) leaves the notice unchanged; the first-run backfill; empty past coverage carries hints; empty with nothing held; empty but covered; the gated-out rung; an empty short window is probed; a never-captured server stays bare.
  • Lite.Tests builds with --no-incremental: 0 warnings, 0 errors.
  • Windows VM, one pre-ready run (second run after fixing a helper bug in the new test; the first run's only failure was in the new class): McpWindowNoticeSystemHealthToolTests + McpWindowNoticeToolTests: Total: 135, Failed: 0.
  • Mac: McpPayloadContractCensusTests 68/0, EffectiveStartUtcTests 15/0 (a new pin counts nine effective_start writes and three hints calls, and bans an EventWindowNoticeAsync( call in the file). The census roster lists McpHealthParserTools.cs for window_truncated and truncation_note.
  • Guard family on the Mac: 956 total, 1 failed, the allowed LivePostgresCollectionHygieneTests (WPF-only).
  • RED: by construction, not run. Before the change, the keys are absent, so every GetProperty throws.
  • The full suite was not run; CI runs it.

Review additions, at b0285c739:

  • Sparse event types read as covered: an event of another type from before the window, no runs before the window, and the tool's own first event a day back, across all nine tools.
  • The collection-off answer: tested on significant_waits as well as the memory broker.
  • On Windows: McpWindowNoticeSystemHealthToolTests, 97 tests, 0 failed.

Default trace, wait stats and blocking stats

Three more Lite MCP tools now say where their data starts, with the contract the other Lite window-floor tools use (#5004, #5015, #5127). get_daily_summary_range needs no notice, and the reason is below.

What an MCP client sees

On a data answer, get_default_trace_events, get_wait_stats and get_blocking_stats write three keys right after hours_back, in this order:

  • effective_start: UTC with a trailing Z. It is the start that was asked for when the store held the whole window.
  • window_truncated: true when the store's data starts more than 90 minutes after the asked start.
  • truncation_note: the usual sentence naming the table, or null when covered.

Notes on each tool:

  • get_default_trace_events names default_trace_events. The Default Trace stores its times in the monitored server's wall clock. The probe converts each candidate row to UTC through that server's own clock (the read does the same), so effective_start is UTC like every other. The probe reads event_time, the column the read windows on, so no row on a page is older than its floor and the event-time form is not needed.
  • get_wait_stats names wait_stats. The keys ride on the data answer only. The no-rows answer is unavailable and stays bare, as the contract says.
  • get_blocking_stats returns two separate series in one answer, blocking and deadlocks, and writes ONE notice for the payload, at the LATER of the two series' start points. Inside the blocking series, the blocked process reports and the always-on DMV blocking snapshots stand in for each other, so the earlier of the two is the blocking start (the same also-covered rule the Blocked Process Reports grid uses). Between the two series the later start wins: a series whose collection began inside the window has an empty head, and that head would read as "no deadlocks" (or "no blocking") next to the other series' data. When both series are covered from before the window, there is no notice. The note names blocked_process_report and deadlocks. Neither probe needs the event-time form: each reads the column its series windows on (event_time, deadlock_time).
  • Empty answers. get_default_trace_events and get_blocking_stats carry the same three keys under hints on an empty answer, and always probe, whatever the window's length. not_collected and unavailable keep their shape.
  • Short windows. A data answer over a window of 90 minutes or less starts no probe and is covered at the asked start. For get_blocking_stats that skips both probes.
  • No effective_hours_back is written: get_wait_stats and get_default_trace_events carry a page truncated/shown, and the census holds that key apart.
  • No tool description changed.

Sparse tables: none of these probes names a sparse table's oldest row. GetQueryWindowFloorAsync counts the collector's runs for all four relations (default_trace_events, wait_stats, blocked_process_report, deadlocks; dmv_blocking_snapshot for the DMV arm), as the collector map in QueryWindowRelationCollector shows. A quiet server with the collector running from before the window reads covered. There are tests for that for each tool.

get_daily_summary_range: no notice

The reader (LocalDataService.GetDailySummaryRangeAsync, Lite/Services/LocalDataService.DailySummary.cs:173) returns a row for each day in its day spine, and each row carries its own state. ReadDailySummaryRow (:~270-293) sets DataState from the day, its collector runs, its signal presence and the retention horizon, and stamps RetentionHorizon on the row. The tool (Lite/Mcp/McpHealthTools.cs) serialises both: retention_horizon, days_before_horizon, purged_day_count, collected_day_count, and per day data_state and data_note. A day before the horizon says purged or past_horizon with health_band NoData, never Healthy. A day the spine does not hold is absent, and the tool's description and its empty answer both call that a collection gap, not a quiet day. The desktop viewer's Daily Summary says the same through the same two fields (DataState, RetentionHorizon). A notice would repeat that, so I added none. A comment at the top of the tool's body says so, and DailySummaryRange_CarriesItsOwnDataState_AndNoWindowKeys pins both halves.

Test plan

  • Lite.Tests compiles on the Mac with --no-incremental: 0 warnings, 0 errors.
  • McpPayloadContractCensusTests: 68 total, 0 failed (rosters now list McpDefaultTraceTools.cs; the window_truncated and truncation_note prose names the three tools).
  • EffectiveStartUtcTests: 16 total, 0 failed. The shared-notice pin now covers McpDefaultTraceTools.cs, McpWaitTools.cs and the second write in McpHealthTools.cs.
  • Guard family (80 classes, 956 cases): the only failure is LivePostgresCollectionHygieneTests, which cannot run on the Mac.
  • On Windows 11: McpWindowNoticeAggregateToolTests and McpWindowNoticeToolTests, 77 tests, 0 failed. After the review fixes (the blocking stats rule, three new cases, the daily-summary guard), McpWindowNoticeAggregateToolTests again: 32 tests, 0 failed.
  • New class Lite.Tests/McpWindowNoticeAggregateToolTests.cs, fixed as_of anchor. Per tool: a notice when collection starts inside the window; none on a quiet start; the key order; a short window starts no probe; empty or unavailable as the contract says; for get_default_trace_events the floor converted from the server's wall clock; for get_wait_stats a capped page next to a floor, and the bare unavailable; for get_blocking_stats the earlier coverage wins in both orders, one source covering from before the window makes it covered in both orders, the DMV collector counts, and one notice only (none inside either series).
  • RED: by construction, not run. Every assertion reads a key with GetProperty, which throws when the key is absent.

Checks on the combined head

  • Lite.Tests and Darling.Tests build with 0 warnings.
  • McpPayloadContractCensusTests, EffectiveStartUtcTests and McpMissMessageParityPinTests: 85 tests, 0 failed, with both parts' roster rows.
  • Every Darling.Tests class that reads repository source: every failure is one that fails only on macOS.

CHANGELOG

SECTION: Fixed
ENTRY: - Lite's system-health MCP tools say where their data starts ([#5172]) - the nine get_health_parser_* tools now return effective_start, window_truncated and truncation_note after hours_back, and the same keys under hints on an empty answer, so an empty answer over a window the store does not reach back to is no longer read as "nothing happened".
ENTRY: - Lite's default trace, wait stats and blocking stats MCP tools say where their data starts ([#5172]) - get_default_trace_events, get_wait_stats and get_blocking_stats now write effective_start (UTC), window_truncated and truncation_note after hours_back, so a window the store never covered no longer reads as a quiet one. get_blocking_stats gives one notice from the later of its blocking and deadlock start points, so an uncovered deadlock half no longer reads as "no deadlocks". An empty default trace or blocking answer carries the same keys under hints.
REF: [#5172]: #5172

Part of #4966

erikdarlingdata and others added 13 commits October 4, 2026 20:54
…ere their data starts (part of #4966)

get_default_trace_events, get_wait_stats and get_blocking_stats now write effective_start, window_truncated and truncation_note right after hours_back on a data answer, with the contract the other Lite window-floor tools use; an empty answer carries the same keys under hints. get_blocking_stats reads two collectors and says one thing, from the earlier of the blocked process report coverage (with the DMV snapshots beside it) and the deadlock coverage. get_wait_stats keys on the data answer only; its no-rows unavailable answer stays bare. get_daily_summary_range needs none: each day it returns carries its own data_state and the payload names the retention_horizon.

Part of #4966
…t the new class's doc at an existing sibling (part of #4966)

Part of #4966
… blocking and deadlock floors, and pin both series (part of #4966)
…to HEAD

# Conflicts:
#	Darling/Darling.Tests/McpPayloadContractCensusTests.cs
@erikdarlingdata erikdarlingdata changed the title MCP: Lite's system-health tools say where their data starts (part of #4966) MCP: Lite's system-health, default trace, wait stats and blocking stats tools say where their data starts (part of #4966) Oct 5, 2026
# Conflicts:
#	Darling/Darling.Tests/McpPayloadContractCensusTests.cs
@erikdarlingdata erikdarlingdata added the merge-train In a merge train: its CI ran on the train PR label Oct 5, 2026
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review October 5, 2026 06:33
@erikdarlingdata
erikdarlingdata merged commit 3a242a0 into dev Oct 5, 2026
12 of 15 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4966-mcp-lite-system-health branch October 5, 2026 06:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-train In a merge train: its CI ran on the train PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant