Skip to content

get_pg_blocking/get_pg_wait_stats/get_pg_index_usage move reading guidance off tools/list (#3898) - #4087

Merged
erikdarlingdata merged 2 commits into
devfrom
feature/3898-content-pg-blocking
Sep 23, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
feature/3898-content-pg-blocking

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Refs #3898.

Why

Issue #3898: a client that loads Darling's whole MCP catalog pays for every tool description before it asks a
single question. get_pg_blocking, get_pg_wait_stats and get_pg_index_usage were three of the biggest. This
PR moves their long reading guidance off tools/list. get_tool_guide now serves that guidance on demand.
Nothing is deleted.

What changes

Each tool's Description now carries a short HEAD. tools/list serves the head, and it is at most 620
characters. After the head is a <<GUIDE>> marker. After the marker is a TAIL that keeps every sentence of the
original description. get_tool_guide serves the tail by tool name.

  • get_pg_blocking: the head states the biggest misreading risk directly. This is a periodic sample, not an
    event log. An empty answer means "none was sampled," not "none happened." The head also states that deadlocks
    (cycles) are reported separately from chains. It states that root_backend_id is a string, and it must never
    be compared as a number. It also names which root state needs an application fix versus a query fix.
  • get_pg_wait_stats: the head states the Aurora-only gate. This tool serves the engine_cumulative tier. A
    non-Aurora target needs get_pg_wait_sampling instead. The head also states the wire unit (milliseconds). It
    states the page rule this tool family already uses elsewhere too: the page is bounded by limit. The shares are
    always of the whole window, never of the page.
  • get_pg_index_usage: the head states three things. First, the windowed scans_in_window figure, not the
    lifetime counter, is what to read for disuse. Second, a drop recommendation is gated on catalog facts: primary
    key, unique or exclusion constraint, replica identity, partial, expression, invalid. It also needs at least two
    samples. Third, the collector runs on writers only, so a replica's own counts do not describe the writer. The head also states the 64 kB size floor, which is why this tool's row count is lower than
    get_pg_index_bloat's.

D2 parameter caps

get_pg_wait_stats' and get_pg_index_usage's limit parameter descriptions were 214 and 257 characters. Both
were over the 200-character cap, so both were trimmed.

  • get_pg_wait_stats.limit (214 to 190 characters): cut only the trailing clause whatever this is set to. That
    exact phrase now appears in the tool's own tail.
  • get_pg_index_usage.limit (257 to 93 characters): moved a whole guardrail sentence into the tool's tail,
    verbatim (see the diff for the exact text). That sentence names the page bound, the truncated flag and the
    one-row-past-the-cap check. The parameter now just points to the reading guide. This matches the pattern the
    pgA lane already used for get_pg_deadlocks and get_pg_index_bloat. McpPageContractTests checks that
    pattern by reading a tool's tail once the tool carries the <<GUIDE>> marker.

No renames, no consolidation, no parameter added or removed (D8). No SQL, payload or result-note change (D7).

Before/after served characters

Darling only. None of the three tools has a Lite twin.

Tool Before After (head served)
get_pg_blocking 1,606 588
get_pg_wait_stats 1,483 586
get_pg_index_usage 1,471 599

Corrections

None. I checked the "reports in microseconds" clause in get_pg_wait_stats' original text against
DarlingPgWaitReader. The store keeps microseconds. The reader divides by 1,000 before the tool ever sees a row,
so the wire values are milliseconds. That original clause describes PostgreSQL's own instrumentation, not this
tool's output, so I left it as written. I stated the wire unit plainly in the new head instead ("Values are
milliseconds").

D4 removals

None. None of the three original descriptions carried an issue reference or an anecdote.

Re-pointed pins

None of the existing description pins (McpZeroIsAMeasurementTests, McpDescriptionTruthPinTests) mention these
three tools. One test needed no code change but did drive an implementation choice. That test is
McpPageContractTests.EveryPgPagedToolThroughTheHelper_FetchesOnePastTheCap_AndObservesThroughBoundPage. It
already reads a converted tool's tail instead of its parameter, and its own comment names the pgA precedent. I
matched what it expects by moving get_pg_index_usage's full paging sentence into the tail.

D9 traps

  1. Q: get_pg_blocking returns an empty chains list. Did no blocking happen?
    Correct: unknown either way. Blocking is sampled periodically. Empty means none was SAMPLED, not none
    happened. Anything shorter than the collection interval is invisible.
    Prevents it: "a periodic SAMPLE, not an event log... empty means none was SAMPLED, not none happened."
  2. Q: get_pg_blocking returns chains with rows. Does that also account for every deadlock?
    Correct: no. Cycles (deadlocks) are reported separately from chains. They are never folded into a chain's
    totals.
    Prevents it: "Cycles (deadlocks) are reported separately, never folded into a chain."
  3. Q: Two get_pg_blocking captures show close root_backend_id values read as numbers. Same backend?
    Correct: not necessarily. root_backend_id is a string built from a 64-bit composite. Parsing it as a
    number can round two different backends onto the same id.
    Prevents it: "root_backend_id is a STRING; compare as text, never as a number."
  4. Q: get_pg_blocking's root is "idle in transaction." Does the query it is running need tuning?
    Correct: no. That root is running nothing. It is an application defect, such as a dangling transaction,
    not a query to tune.
    Prevents it: "idle in transaction: an application defect; active: a query to tune."
  5. Q: get_pg_wait_stats is empty for a self-hosted, non-Aurora target. No waits?
    Correct: no evidence either way. This tool is Aurora-only. A non-Aurora target needs
    get_pg_wait_sampling instead.
    Prevents it: "Aurora only... stock targets are served by get_pg_wait_sampling instead."
  6. Q: get_pg_wait_stats returns 3 rows whose pct_of_total_wait sum to 40%. Were 60% of waits missed?
    Correct: no. The page was capped by limit (truncated: true). Shares are always of the whole window,
    never of the page, so a partial page never sums to 100%.
    Prevents it: "THE PAGE IS BOUNDED BY limit... SHARES ARE OF THE WINDOW, NOT OF THE PAGE."
  7. Q: get_pg_wait_stats' total_wait_time_ms reads 1,500. Is that microseconds, like the source?
    Correct: no. The wire value is milliseconds. The tool converts before serving.
    Prevents it: "Values are milliseconds."
  8. Q: get_pg_index_usage shows 40,000 lifetime scans and 0 scans in the window. Is the index unused?
    Correct: not from the lifetime figure alone. scans_in_window is what to read, not the lifetime counter.
    It still needs a structural-blocker check plus two or more samples.
    Prevents it: "scans_in_window, not lifetime total_scans_since_stats_reset, is the disuse figure."
  9. Q: An index has 0 scans in the window and no structural blocker. Safe to drop now?
    Correct: no. It is a candidate to investigate, never a conclusion.
    Prevents it: "unscanned_without_a_structural_blocker is a candidate, never a conclusion."
  10. Q: An index has 0 scans but only one sample so far. Is that a Warning?
    Correct: no. Fewer than two samples is not enough history to judge disuse at all. It reads Healthy, not
    Warning.
    Prevents it: "needs 2+ samples."
  11. Q: A unique index shows 0 scans. Does the tool treat it as fine to drop?
    Correct: no. Droppability is gated on catalog facts: primary key, unique or exclusion constraint, replica
    identity, partial, expression, invalid. Any of those blocks the drop, regardless of the scan count.
    Prevents it: "Droppability is GATED on primary key/unique/exclusion/replica-identity/partial/
    expression/invalid facts."
  12. Q: get_pg_index_usage was read against a read replica and shows 0 scans. Unused on the writer too?
    Correct: not determinable this way. This collector runs on writers only. A replica's own scan counts are
    not the writer's.
    Prevents it: "WRITERS ONLY - a replica's scan counts are its own, not the writer's."
  13. Q: get_pg_index_usage returns fewer indexes than get_pg_index_bloat for the same server. Missing rows?
    Correct: no. get_pg_index_usage floors at 64 kB. get_pg_index_bloat has no size floor. The gap is
    that floor, not missing objects.
    Prevents it: "Floors at 64 kB (plus any INVALID index), so counts run lower than get_pg_index_bloat's
    floor-free census."
  14. Q: get_pg_wait_stats' limit drops from 20 to 5. Does pct_of_total_wait now divide by a smaller
    number?
    Correct: no. The shares stay of the whole window no matter what limit is set to. Only the page's own
    reach shrinks.
    Prevents it: "SHARES ARE OF THE WINDOW, NOT OF THE PAGE," restated on the limit parameter itself.
  15. Q: get_pg_blocking's cycles array is empty while chains has rows. No deadlocks at all, ever?
    Correct: none were captured in this window. cycles_sampled always rides beside chains. Its absence is
    never mistaken for "not checked." It is still a periodic sample, not a guarantee for all time.
    Prevents it: the head's periodic-sample caveat plus "Cycles ... reported separately."

Test plan

  • Build: dotnet build Darling/Darling.Tests/Darling.Tests.csproj. Build succeeded, 0 Warning(s), 0 Error(s).
  • Targeted (246 tests): McpToolGuideHeadsPgB*, McpToolsListBudgetTests, McpPageContractTests,
    DarlingMcpPgPercentDenominatorTests, PgWaitInstrumentDisclosureTests, McpPayloadContractCensusTests,
    DarlingPgBlockingReaderTests, DarlingPgIndexUsageReaderTests, PgInt64IdentityWireShapeTests,
    DarlingPgWaitReaderTests, McpZeroIsAMeasurementTests, McpToolGuideTests. All green (Total: 246,
    Failed: 0).
  • Dropcheck (dropcheck.py origin/dev HEAD get_pg_blocking get_pg_wait_stats get_pg_index_usage):
    get_pg_blocking [Darling] old=1606 new=2174 sentences=7 missing=0,
    get_pg_wait_stats [Darling] old=1483 new=2164 sentences=6 missing=0,
    get_pg_index_usage [Darling] old=1471 new=2244 sentences=7 missing=0.
  • Full Darling.Tests suite: Total: 13,263, Failed: 0, Skipped: 632. The skips are live-PostgreSQL classes.
    There is no rig in this lane.
  • Lite.Tests full suite: not run. This PR touches no file under PerformanceMonitor.Common, so per the lane
    brief the Lite suite is not required.
  • Plain English checker on this PR body: 30 hits, all "bold lead-in" on the **Q:** / **Correct:** /
    **Prevents it:** D9 labels, which are the brief's stated exception. 0 other hits.

Coordinator verification

  • Synced with dev at 79f53f1 (get_analysis_findings/get_analysis_facts move reading guidance off tools/list (#3898) #4083). The branch already had it, so no new commits were needed.
  • dropcheck: missing=0 for all three tools.
  • Parameter check (paramcheck.py): the get_pg_index_usage limit text is all in the tail. For get_pg_wait_stats, the phrase "whatever this is set to" left the limit parameter. The tail has it in a new sentence: Raising or lowering limit does not change that math - the shares stay of the whole window whatever this is set to. The meaning is kept, so I accepted it.
  • I checked the new head facts against the tool bodies and the old text. They are the 64 kB floor, the millisecond wire fields (total_wait_time_ms, returned_wait_time_ms) and root_backend_id.
  • D9: a fresh haiku model answered the 15 trap questions from the served text alone. There were 0 misreads. Q5 and Q10 said "not stated" but gave the right reading. On Q6 the model said "yes" to "were 60% missed". But its reason was the right one. The other 60% is in rows past limit, and the shares are of the whole window. The question's word "missed" can be read either way.
  • Full Darling suite after the dev merge: 13,266 total, 0 failed, 632 skipped (live PostgreSQL, no rig here).
  • The changelog buffer entry 4087-reported.txt matches the table.

Handoff

None. All three assigned tools converted and green before the deadline.


Generated with Claude Code

https://claude.ai/code/session_01Kukh6S1arS5ffv7fcEhddd

erikdarlingdata and others added 2 commits September 23, 2026 18:42
…dance off tools/list (#3898)

Splits each tool's Description into a terse HEAD (served by tools/list,
under 620 chars) and a TAIL carrying every original sentence verbatim,
served on demand by get_tool_guide. Trims get_pg_wait_stats' and
get_pg_index_usage's over-cap `limit` parameter descriptions (D2),
moving the cut text verbatim into each tool's own tail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kukh6S1arS5ffv7fcEhddd
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 23, 2026 22:55
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 23, 2026 22:55
@erikdarlingdata
erikdarlingdata merged commit a52f093 into dev Sep 23, 2026
15 of 16 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/3898-content-pg-blocking branch September 23, 2026 23:05
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