Skip to content

get_active_queries moves reading guidance off tools/list (#3898) - #4107

Merged
erikdarlingdata merged 4 commits into
devfrom
feature/3898-content-activequeries
Sep 24, 2026
Merged

erikdarlingdata merged 4 commits into
devfrom
feature/3898-content-activequeries

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Refs #3898.

Why

get_active_queries put a 1,406-character description (1,421 on Lite) into every tools/list response on both
products. That text carries guardrail facts a model needs to read the answer correctly. It now moves to a short
head plus a get_tool_guide-served tail, per #3898.

What changes

  • Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpSessionTools.cs: get_active_queries's description
    split into a 616-character served head (585 chars plus the 31-character pointer) and a tail that keeps every
    original sentence.
  • Lite/Mcp/McpSessionTools.cs: the same split, byte-identical head, Lite's own tail (it names
    get_blocked_process_reports where Darling's names get_blocking, exactly as the original prose did on each
    product).
  • blocking_only's parameter description was 238 characters, over the 200-character cap for a converted tool. The
    guardrail clause stayed on the parameter, cut to 134 characters. It reads: "rows with blocking_session_id > 0,
    plus the head blockers those rows name in the same capture." The rest moved to the tool's tail: "Applied in SQL,
    so total_snapshots counts the blocking population and truncated is measured against it."
  • Budget files updated: Darling/Darling.Tests/McpToolsListBudget/DarlingMcpSessionTools.txt and
    Lite.Tests/McpToolsListBudget/McpSessionTools.txt.
  • New head-pin classes appended to the shared, multi-class McpToolGuideHeads.SqlCore.cs files. This family
    already held classes for other tools, including analyze_server, compare_analysis, and the daily-summary
    pair, from earlier The MCP surface costs ~100k tokens of context before the first call: 157 tools, 336 KB of tools/list, 86 KB of instructions #3898 lanes. The new class is McpToolGuideHeadsSqlCoreActiveQueriesTests, in both
    Darling/Darling.Tests/ and Lite.Tests/.
  • PerformanceMonitor.Common/Mcp/McpToolGuideTopics.SqlCore.cs was not touched. get_active_queries is the only
    tool in this lane. No prose repeats across two or more tools, so no topic was needed.

Served-chars table

Tool Darling before Darling after Lite before Lite after
get_active_queries 1406 616 1421 616

Corrections

None. The original prose matched the tool body on both products.

D4 removals

None. The original description carried no issue references and no anecdotes.

Re-pointed pins

None needed. Every test file that mentions get_active_queries pins something structural: the tool's name, its
parameter roster, or its page and filter behavior. None pins a substring of the description prose. The files are
DarlingMcpSessionToolsTests.cs, McpFilterSemanticsLivePostgresTests.cs, McpPageContractTests.cs on both
products, CrossAppMcpToolInventoryPinTests.cs, and McpMissMessageParityPinTests.cs. All pass unchanged.

Status routes

get_active_queries can answer empty, not_collected, or error. Darling and Lite route them the same way,
one server call apart:

Status Darling Lite Condition
empty (filtered miss) DarlingMcpSessionTools.cs:132 McpSessionTools.cs:46 rows.Count == 0 and database_name or blocking_only was set. Reported directly, with no check of whether the collector can run on this engine or has ever produced a row.
not_collected, else empty (fallback) DarlingMcpSessionTools.cs:139-140 McpSessionTools.cs:53-54 rows.Count == 0, no filter set. NotCollectedStatusAsync runs first. It answers not_collected only when the query_snapshots collector cannot run on this server's engine kind, for example a PostgreSQL target. Otherwise it falls through to empty.
error DarlingMcpSessionTools.cs:200 McpSessionTools.cs:113 Any exception during the read, via McpHelpers.FormatError.

The head's status sentence is Empty: no snapshot in the window, or none matching filters; not_collected (unfiltered only): engine can't run it. It restates both rows' conditions on both products. The filtered branch never runs the not_collected capability check that the unfiltered branch
runs. A target that can never run this collector still answers the ordinary filtered-miss empty message, not
not_collected.

D9 traps

  • Q: get_active_queries returns granted_query_memory_gb: 2.5 on a row. Is that value in megabytes or
    gigabytes?
    Correct: Gigabytes.
    Prevents it: "memory grant GB"
  • Q: A call with hours_back=6 and as_of set to a past timestamp. Does the window run forward from as_of,
    or end at it?
    Correct: It ends at as_of. The six hours are the six hours immediately before that timestamp.
    Prevents it: "over a window ending at as_of"
  • Q: A call with database_name="Sales" returns total_snapshots: 40. Is 40 the count for the whole server's
    window, or only for Sales?
    Correct: Only for Sales. The filter runs in SQL before the count is taken.
    Prevents it: "database_name/blocking_only filter IN SQL: total_snapshots is the filtered count."
  • Q: truncated: true with total_snapshots: 80 and the default limit of 50. Does truncated describe the
    whole hours_back window, or the filtered, paged count?
    Correct: The filtered count: 80 filtered rows exceed a limit of 50, whatever the raw window held.
    Prevents it: "truncated means over limit."
  • Q: A row shows wait_time_ms: null while wait_type is populated. Does the null mean this field failed to
    collect?
    Correct: Not necessarily. wait_time_ms is null both for a true zero and for "not applicable."
    Prevents it: "wait_time_ms, dop, granted_query_memory_gb, open_transaction_count: null = zero or not applicable."
  • Q: granted_query_memory_gb: null on a row that shows a memory-grant wait type. Does that prove no memory
    was granted?
    Correct: No, the same field collapses a genuine zero grant and "not applicable" to the same null.
    Prevents it: "null = zero or not applicable."
  • Q: open_transaction_count: null. Does that guarantee zero open transactions right now?
    Correct: Not from that field alone. It is null both for zero and for not-applicable.
    Prevents it: "null = zero or not applicable."
  • Q: A row is a bare WAITFOR shell with no blocking activity of its own. Does it get filtered out as noise?
    Correct: No. If another row in the same capture names it as the blocker, it stays on the page whatever its
    own text is.
    Prevents it: "Head blockers are never stripped (is_head_blocker)."
  • Q: blocker_not_shown: "filtered" on a victim row. Does that mean the victim row itself was excluded by
    your database_name filter?
    Correct: No, the victim row is on the page. "filtered" means its blocker, a different row, was excluded.
    Prevents it: "a victim's blocker_not_shown is not_captured, filtered, or past_page."
  • Q: blocker_not_shown: "past_page". Does that mean the blocker was never captured by the collector?
    Correct: No. "past_page" means the blocker passed every filter and is in the population, just beyond
    limit. "not_captured" is the separate code for a blocker that held no running request at all.
    Prevents it: "a victim's blocker_not_shown is not_captured, filtered, or past_page."
  • Q: blocker_not_shown is absent on a row. Does that always mean the row is not blocked?
    Correct: Not by itself. It is also absent when the row is blocked but its blocker IS on the page. Check
    blocking_session_id to tell the two apart.
    Prevents it: "a victim's blocker_not_shown is not_captured, filtered, or past_page."
  • Q: blocking_only=true returns status: "empty". Does that prove this server's active-query collector has
    never captured anything, ever?
    Correct: No. The filtered branch answers empty purely from what matched blocking_only this call. It
    never checks whether the collector has ever produced a row, or whether this engine can run it at all.
    Prevents it: "Empty: no snapshot in the window, or none matching filters"
  • Q: Narrowing database_name on a later call. Does total_snapshots stay computed on the same unfiltered
    population while only the returned rows shrink?
    Correct: No, both total_snapshots and truncated are computed on the filtered population together, so
    narrowing the filter changes both.
    Prevents it: "database_name/blocking_only filter IN SQL: total_snapshots is the filtered count."
  • Q: (added by the coordinator) A call with NO filters answers status: "empty". Does that mean this server's
    active-query collector has never captured anything?
    Correct: No. It means no snapshot in the window. The unfiltered branch checks the engine first and answers
    not_collected when this engine cannot run the collector.
    Prevents it: "Empty: no snapshot in the window, or none matching filters"
  • Q: (added by the coordinator) Can this tool answer a status other than empty when it finds no rows?
    Correct: Yes. An unfiltered call on an engine that cannot run the collector answers not_collected.
    Prevents it: "not_collected (unfiltered only): engine can't run it."

Test plan

  • Build Darling/Darling.Tests/Darling.Tests.csproj: Build succeeded, 0 Warning(s), 0 Error(s).
  • Build Lite.Tests/Lite.Tests.csproj: Build succeeded, 0 Warning(s), 0 Error(s).
  • Targeted Darling classes (McpToolGuide*, McpToolsListBudget*, McpDescription*, McpZeroIsAMeasurement*,
    McpPayloadContractCensus*, DarlingMcpSessionToolsTests, McpFilterSemanticsLivePostgresTests,
    McpPageContractTests): Total 249, Failed 0, Skipped 8 (live-PostgreSQL, no rig).
  • Targeted Lite classes (the same families plus CrossAppMcpToolInventoryPinTests,
    McpMissMessageParityPinTests): Total 154, Failed 0, Skipped 0.
  • Red-watch: broke the head's last sentence, watched McpToolGuideHeadsSqlCoreActiveQueriesTests fail, then
    restored it and watched it pass again.
  • dropcheck (Darling): get_active_queries old=1406 new=2103 sentences=6 missing=0.
  • dropcheck (Lite): get_active_queries old=1421 new=2118 sentences=6 missing=0.
  • git merge origin/dev: clean, no conflicts (Darling install-script files, unrelated to this change).
  • Full Lite.Tests suite: Total 5264, Failed 0, Skipped 0.
  • Full Darling.Tests suite: Total 13311, Failed 2, Skipped 636. The 2 failures are
    DarlingInstallLocationTests.TheInstallTreeLock_ClosesTheInheritedGrant_KeepsProtectedFiles_AndReportsWhatItCannotClose
    and ThePreLockWritableExtractionCheck_TrustsTheAccountTheLockItselfGranted_OnAnAlreadyLockedTree. Both are
    pre-existing and unrelated to this PR. They live in a file this PR never touches. git log shows they came
    from the origin/dev merge commit c51706c9 (Install scripts: the service account gets Read & Execute on the install root and Modify only where it writes (#4052) #4090, an install-script permissions change). Re-run alone,
    in isolation, they fail the same way, so this is not a suite-ordering artifact of this branch.
  • Plain-English checker on this PR body: body_4107.md: 31 hit(s), doc mode. The remaining hits are 30 D9 labels
    and the attribution emoji, which are allowed.

Coordinator verification

  • I merged origin/dev (bbd7eec6) into the branch. The dropped-sentence check shows missing=0 on both products. The parameter check shows blocking_only 238 -> 134 with missing=0, so the text it lost is verbatim in the tail.
  • Both McpToolGuideHeads.SqlCore.cs files are a pure append: 64 and 65 added lines, 0 removed, in one hunk at the end of each file.
  • I checked every head fact against the code on both products. The window runs from as_of - hours_back to as_of. Both filters run inside the SQL, and total_snapshots is that statement's COUNT(*) OVER (). truncated comes from a limit + 1 fetch. The four named fields serve x > 0 ? x : null. The reader maps a database NULL to 0 first, so a null covers a zero and a missing value. blocker_not_shown is set on the victim's row.
  • Head facts fixed: 1. The status clause named only the filtered empty, and only in the negative. The unfiltered empty (no snapshot in the window) and not_collected (an unfiltered call on an engine that cannot run the collector) were missing. The new clause is Empty: no snapshot in the window, or none matching filters; not_collected (unfiltered only): engine can't run it.
  • To fit that clause under 620, I made three wording edits with no change of fact. a hidden blocker's blocker_not_shown became a victim's blocker_not_shown, because the field sits on the victim's row. The null sentence became wait_time_ms, dop, granted_query_memory_gb, open_transaction_count: null = zero or not applicable. The opening now says (sys.dm_exec_requests) in a window.
  • D9 round 1: a fresh haiku reader answered the 13 lane questions and 2 of mine (the last two above). It saw only the served head and parameter text. Misreads: 0. The lane's Q8, Q10 and Q11 came back as "can't tell". My two questions came back as "can't tell" too, and that showed the gap above. Round 2, after the edit: 0 misreads, and both of my questions were answered correctly.
  • The served head went from 613 to 616 characters on both products. I updated the pins and the budget lines. The changelog buffer entry now says "fewer than 620 characters".
  • After the edit, both builds have 0 warnings. Targeted classes: Darling 277 tests and Lite 161 tests, 0 failed. Full suites: Darling ran 13,311 tests with 2 failed and 636 skipped. Lite ran 5,264 tests with 0 failed. The 2 Darling failures are the known DarlingInstallLocationTests pair from Install scripts: the service account gets Read & Execute on the install root and Modify only where it writes (#4052) #4090. They fail on any non-elevated local run, and CI runs elevated.

Handoff

Nothing deferred. get_active_queries is the only tool in this lane's brief and it is fully converted, green, and
committed on both products. The 2 pre-existing DarlingInstallLocationTests failures above are outside this PR's
scope. They belong to already-merged PR #4090. The #3898 coordinator reported them to Erik as a known local test bug.

One process note for the coordinator. McpToolGuideHeads.SqlCore.cs is a shared file. It already held classes for
other #3898 lanes' tools before this PR touched it: analyze_server, compare_analysis, get_analysis_facts,
get_analysis_findings, audit_config, mute_analysis_finding, get_daily_summary, and get_daily_summary_range.
This PR appends a new class, McpToolGuideHeadsSqlCoreActiveQueriesTests, after the last one. It does not modify
any existing class. Worth calling out in future sqlCore-family briefs so a lane reads the file before writing to
it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC

erikdarlingdata and others added 4 commits September 23, 2026 22:06
Both SKUs' get_active_queries carried a 1.4k-character description in
tools/list. The head now states the guardrail facts (SQL-side filters,
null-for-zero numeric fields, the blocker_not_shown codes, and that a
filtered empty does not prove nothing was ever collected) in 613 served
characters; get_tool_guide serves the rest, verbatim, on demand.

blocking_only's parameter description was trimmed from 238 to 134 chars
to clear the 200-char cap; the elaboration it lost moved verbatim into
the tool's own tail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
…eir window scope (#3898)

The head named only the filtered empty, in the negative. It now says what
an empty answer means (no snapshot in the window, or none matching the
filters) and that not_collected answers only unfiltered calls on an engine
that can't run the collector. blocker_not_shown is named as the victim's
field. Served head 613 -> 616 on both products; pins and budget follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 24, 2026 02:29
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 24, 2026 02:29
@erikdarlingdata
erikdarlingdata merged commit a6c56ae into dev Sep 24, 2026
16 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/3898-content-activequeries branch September 24, 2026 02: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