Skip to content

get_query_store_duration_trend/get_perfmon_trend move reading guidance off tools/list (#3898) - #4101

Merged
erikdarlingdata merged 3 commits into
devfrom
feature/3898-content-trend
Sep 24, 2026
Merged

erikdarlingdata merged 3 commits into
devfrom
feature/3898-content-trend

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Refs #3898.

Why

A client that loads Darling's whole MCP catalog pays for every tool description before it asks its first
question. get_query_store_duration_trend and get_perfmon_trend carried long reading guidance in their
tools/list descriptions. This PR moves that guidance off tools/list and into get_tool_guide, which serves
it on demand by tool name. Nothing is deleted.

What changes

Both tools now carry a short head (served by tools/list, at most 620 characters including the Reading guide: get_tool_guide. pointer) and a full tail (the original description, unchanged, served only by get_tool_guide).
get_query_store_duration_trend has a Lite twin in Lite/Mcp/McpQueryTools.cs. get_perfmon_trend has a Lite
twin in Lite/Mcp/McpPerfmonTools.cs. Both twins convert in this PR with byte-identical heads, per D6.

Files touched:

  • Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpTrendTools.cs
  • Lite/Mcp/McpQueryTools.cs
  • Lite/Mcp/McpPerfmonTools.cs
  • Darling/Darling.Tests/McpToolGuideHeads.Trend.cs (new head pins)
  • Lite.Tests/McpToolGuideHeads.Trend.cs (new head pins)
  • Darling/Darling.Tests/McpToolsListBudget/DarlingMcpTrendTools.txt
  • Lite.Tests/McpToolsListBudget/McpQueryTools.txt
  • Lite.Tests/McpToolsListBudget/McpPerfmonTools.txt

No topic file was needed: nothing new is duplicated between these two tools' heads or tails.

The first draft of get_perfmon_trend's head wrote the rate formula as "delta_value / sample_interval_seconds".
The full Lite suite caught MeasurementContractCensusTests.NoReaderDividesByTheStoredIntervalBare. This census
reads every string literal containing the word "interval" as SQL. It flags a bare, unguarded division by
sample_interval_seconds. The literal slash in the head's prose matched that pattern, on both products. The
tail's own original prose spells the same fact as "divided by", so the head now does too, on both products.

Served-chars table

Tool Darling before Darling after Lite before Lite after
get_query_store_duration_trend 1537 616 1276 616
get_perfmon_trend 1057 615 1057 615

Numbers are from McpToolsListBudgetTests on each product. The "after" number includes the Reading guide: get_tool_guide. pointer.

Corrections

None. Both tools' original prose held up against their bodies.

D4 removals

None. Neither tool's original description carried an issue reference or an anecdote.

Re-pointed pins

McpPayloadContractCensusTests.EveryWindowFloorTool_CarriesTheSharedClause_AndNoOtherToolDoes (Darling) checks
every tool on its WindowFloorTools roster by reflection. get_query_store_duration_trend is on that roster on
both products. A converted head must still state the window_truncated key. It must also carry the words "not
a page cut". My first draft head said "window_truncated marks the retention floor" without that exact phrase,
and the pin caught it. I added ", not a page cut" to the head. Served length went from 583 to 599 characters.
The pin checks the converted head's own wording, not text that moved elsewhere, and it passed once the head
carried the phrase the pin requires.

Status routes

get_perfmon_trend. Same branches, same order, on both products. It never returns empty:

Status Condition Darling Lite
not_collected Engine cannot run perfmon collection, checked first DarlingMcpTrendTools.cs:203 McpPerfmonTools.cs:96
not_collected Counter is Page Life Expectancy (intentionally uncollected) DarlingMcpTrendTools.cs:216-217 McpPerfmonTools.cs:108-109
unavailable No perfmon counters at all collected for this server in the window DarlingMcpTrendTools.cs:225-226 McpPerfmonTools.cs:117-118
not_collected Other counters exist in the window, but not the one asked for DarlingMcpTrendTools.cs:233 McpPerfmonTools.cs:125

get_query_store_duration_trend:

Status Condition Darling Lite
not_collected Engine cannot run Query Store, checked first DarlingMcpTrendTools.cs:658 McpQueryTools.cs:788
empty Whole window sits before the corrected rollup's oldest materialized hour (coverage gap) DarlingMcpTrendTools.cs:670-676 not applicable, Lite has no rollup tier
unavailable Server has never been sampled for Query Store DarlingMcpTrendTools.cs:678-681 McpQueryTools.cs:794, EmptyTrendAsync at :959-962
empty Window's head sits before the rollup floor; the served tail is quiet (partial coverage gap) DarlingMcpTrendTools.cs:694-696 not applicable
empty Sampled before, genuinely quiet window DarlingMcpTrendTools.cs:702 McpQueryTools.cs:794, EmptyTrendAsync at :955-958

On both tools, the engine gate (not_collected) is checked first. No empty or unavailable determination happens
before it. So "no points" never by itself proves a quiet server or a missing collector.

D9 traps

  • Q: Does get_perfmon_trend answer status empty when a counter has no points, the way its trend siblings do?
    Correct: No. This tool never returns status empty. A miss is always not_collected or unavailable.
    Prevents it: No points never returns empty: not_collected covers a gated engine, Page Life Expectancy, or an unknown counter name; unavailable means nothing was collected here yet.
  • Q: Does get_perfmon_trend's unavailable status for a counter name mean the name is wrong?
    Correct: No. unavailable means no counters at all were collected in the window. A wrong name, when other
    counters were collected, returns not_collected instead, with the real names listed.
    Prevents it: not_collected covers a gated engine, Page Life Expectancy, or an unknown counter name; unavailable means nothing was collected here yet.
  • Q: For a 'rate' counter_kind point, is delta_value alone the per-second figure?
    Correct: No. Divide by sample_interval_seconds. Never divide when that interval is 0.
    Prevents it: rate - per-second is delta_value divided by sample_interval_seconds, never delta_value alone or when the interval is 0.
  • Q: A gauge point's delta_value is null. Is that a sign of a collection problem?
    Correct: No. A level never carries a delta. Null delta_value and null sample_interval_seconds are the
    gauge's normal shape.
    Prevents it: gauge - value is the bucket average, delta_value and sample_interval_seconds null (no delta for a level).
  • Q: counter_kind comes back null for a point. Is that point bad data to discard?
    Correct: No. Classify it by name instead. A name ending in /sec is a rate. Null just means the row
    predates the stored type, or the instances mix types.
    Prevents it: null - classify by name (ends in /sec = rate).
  • Q: A point's counter_kind is 'other'. Can its delta_value be treated as a rate, like a 'rate' point's?
    Correct: No. 'other' is a non-rate change, never divided by sample_interval_seconds.
    Prevents it: other - delta_value is a non-rate change.
  • Q: Does get_query_store_duration_trend's empty status on Darling always mean the server was genuinely
    quiet?
    Correct: Not necessarily. On Darling, empty can also mean the window is older than the corrected rollup's
    covered range. That is a coverage gap needing a backfill, not a quiet server.
    Prevents it: empty: quiet on Lite always; on Darling, empty can also be a rollup coverage gap (window predates the corrected rollup, run --backfill-rollups).
  • Q: The same tool answers empty on Lite. Is that also a possible rollup coverage gap?
    Correct: No. Lite has no rollup tier, so its empty is always a genuinely quiet window.
    Prevents it: empty: quiet on Lite always; on Darling, empty can also be a rollup coverage gap...
  • Q: Does get_query_store_duration_trend's unavailable status mean Query Store is off right now?
    Correct: Not exactly. unavailable means this server has never been sampled at all. Query Store being off
    everywhere is the likely cause, but the status itself marks "never sampled," not "currently off."
    Prevents it: unavailable: never sampled here.
  • Q: get_query_store_duration_trend has zero points and the target is PostgreSQL. Does it still choose
    between empty and unavailable?
    Correct: No. not_collected is checked first, before any empty or unavailable determination.
    Prevents it: not_collected: engine cannot run Query Store.
  • Q: A point has a null rate. Does averaging the series read that null as a 0?
    Correct: No. Null rates are never reported as 0. unrated_points counts them, and unrated_note says why.
    Prevents it: A point with no earlier point to rate against carries null rates, never 0 (unrated_points, unrated_note says why).
  • Q: get_query_store_duration_trend's window_truncated is true. Does raising a limit or paging further fix
    it?
    Correct: No. window_truncated marks a retention-floor start, not a page cut. No limit changes it.
    Prevents it: window_truncated marks the retention floor, not a page cut; effective_start says where the answer begins.
  • Q: Are get_query_store_duration_trend's points sized by a bucket_minutes choice, the way get_perfmon_trend's
    are?
    Correct: No. This tool has no bucket_minutes parameter. Every point is one Query Store interval, one point
    per hour worked.
    Prevents it: one point per hour worked.
  • Q: get_query_store_duration_trend answers empty for a quiet server. Does get_perfmon_trend also answer
    empty for that same quiet server and window?
    Correct: No. get_perfmon_trend never returns status empty for any reason. Its zero-point answer is always
    not_collected or unavailable, regardless of what the paired trend tool says.
    Prevents it: No points never returns empty (get_perfmon_trend) versus empty: appearing in
    get_query_store_duration_trend's own head.

Test plan

  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj: Build succeeded, 0 Warning(s), 0 Error(s).
  • dotnet build Lite.Tests/Lite.Tests.csproj: Build succeeded, 0 Warning(s), 0 Error(s).
  • Targeted Darling classes: McpToolsListBudgetTests, McpToolGuideTests, McpToolGuideHeadsTrendTests,
    McpDescription*, McpZeroIsAMeasurement*, McpPayloadContractCensusTests, plus every class git grep found
    mentioning either tool. Result: 110 total, 0 failed, 4 skipped (live PostgreSQL, no rig here).
  • Targeted Lite classes: the same families, plus BaselineDiscontinuityRenderCensusTests,
    PerfmonCounterTypeTests, PerformanceTrendsToolTests. Result: 67 total, 0 failed, 0 skipped.
  • Red-watch: broke get_perfmon_trend's head fact, confirmed McpToolGuideHeadsTrendTests failed on both its
    assertions, restored the text, confirmed green again.
  • Dropcheck (dropcheck.py origin/dev HEAD get_query_store_duration_trend get_perfmon_trend): all four rows
    (Darling and Lite, both tools) report missing=0.
  • Full Darling.Tests suite, first pass: 13,295 total, 0 failed, 632 skipped (no PostgreSQL rig here). This
    pass caught nothing new. The regression below only showed up on Lite's copy of the same census.
  • Full Lite.Tests suite, first pass: 5,255 total, 1 failed
    (MeasurementContractCensusTests.NoReaderDividesByTheStoredIntervalBare, flagging both products' new
    get_perfmon_trend head), 0 skipped. Fixed by spelling the rate formula as "divided by" instead of "/".
  • Full Darling.Tests suite, after the fix: 13,295 total, 0 failed, 632 skipped (no PostgreSQL rig here).
  • Full Lite.Tests suite, after the fix: 5,255 total, 0 failed, 0 skipped.
  • Dropcheck re-run after the fix: all four rows still report missing=0.
  • Plain-English checker on this PR body: pr_body_trend.md: 38 hit(s), doc mode. Every remaining hit is an
    allowed exception: a D9 label, a semicolon or dash inside a quoted literal head phrase (code span) or a
    table cell, or the attribution line's emoji.

Coordinator verification

  • Synced with dev with no conflicts. dropcheck: missing=0 in all four rows. paramcheck: no parameter changed.
  • I checked every head fact against the code on both products. Two were wrong:
    • get_perfmon_trend said that unavailable means "nothing was collected here yet". The code returns
      unavailable when no counter at all was collected in the window (collected.Count == 0,
      DarlingMcpTrendTools.cs:225-226 and McpPerfmonTools.cs:117-118). A server with history before the
      window gets the same answer. The head now says "no counter at all collected in the window", which matches
      the Status routes table above.
    • get_query_store_duration_trend said "one point per hour worked". The original text says that each interval
      is counted once, at the hour the work ran. Lite rates one point per collection, and Darling's raw points are
      Query Store intervals, so a point is not always an hour. The head now says "each interval counted once, at
      the hour it ran".
    • To stay under the 620-character cap, "carries null rates" became "has null rates".
  • New pins cover both corrected facts on both products. Budget: get_query_store_duration_trend goes from 599
    to 616, and get_perfmon_trend from 609 to 615, on both products.
  • D9 on the fixed heads (the lane's 14 questions plus one about unavailable): 0 misreads. Six answers were
    "not stated", which is not a misread.
  • Tests on the final build: the targeted classes pass, McpPayloadContractCensus and MeasurementContractCensus
    included. Darling: 327 total, 0 failed, 6 skipped. Lite: 98 total, 0 failed. The lane's full suites ran
    before these text edits, and CI runs both suites again.

Handoff

None. Both tools in this lane's brief are converted and green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC

erikdarlingdata and others added 3 commits September 23, 2026 21:09
…e off tools/list (#3898)

Splits both tools' descriptions into a short head (served by tools/list)
and a full reading-guide tail (served by get_tool_guide), per #3898.
Nothing is deleted: the original prose moves to the tail verbatim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
The head used a literal "/" right before sample_interval_seconds
("delta_value / sample_interval_seconds"), which MeasurementContractCensusTests'
BareIntervalDenominator regex reads as an unguarded SQL division, since the
scanner treats any string literal containing "interval" as SQL to check.
Spelled out as "divided by", matching the tail's own prose, on both products.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
get_perfmon_trend: unavailable means no counter at all was collected in the
window, not "nothing was collected here yet" (collected.Count == 0 over the
window, both products). get_query_store_duration_trend: each interval is
counted once, at the hour it ran; a point is not always an hour (Lite rates
per collection, Darling raw points are Query Store intervals). Both facts
pinned on both products; budgets 599 -> 616 and 609 -> 615.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 24, 2026 01:31
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 24, 2026 01:31
@erikdarlingdata
erikdarlingdata merged commit b74cb70 into dev Sep 24, 2026
16 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/3898-content-trend branch September 24, 2026 01:40
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
* Add 48 buffered CHANGELOG entries in one splice

Adds 48 entries and 48 link refs (#4057, #4077, #4079, #4081, #4082, #4083, #4084, #4085, #4086, #4087, #4088, #4089, #4090, #4091, #4092, #4093, #4095, #4096, #4099, #4100, #4101, #4103, #4105, #4106, #4107, #4108, #4109, #4110, #4111, #4113, #4114, #4115, #4116, #4117, #4118, #4119, #4120, #4121, #4122, #4123, #4124, #4125, #4126, #4127, #4131, #4133, #4136, #4141). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran.

Entries found under a bare section line (none unless the old script ran again) move into the matching ### section, below the new entries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC

* Changelog splice: add #4137's entry, which merged before the splice but was missed

#4137 (pg_plan_capture reads csvlog) merged at 05:57Z with a CHANGELOG entry section in its
body. It was neither spliced nor listed as needing no entry. Its entry goes under Fixed, right
after #4136's, and #4136's last sentence now points to it instead of saying plan capture is
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K

---------

Co-authored-by: Claude Opus 5.5 (1M context) <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