Skip to content

get_blocking/get_pg_replication_slots move reading guidance off tools/list (#3898) - #4108

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

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

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's full reading guidance before it asks a
single question. get_blocking and get_pg_replication_slots each carried a long description with the reading
guidance folded into the part every client always receives. This PR moves that guidance behind
get_tool_guide(tools[], topics[]), so a client only pays for it when it actually asks. Nothing is deleted.

What changes

Both tools convert to the head-plus-tail pattern. The served head is at most 620 characters, pointer included.
It carries the guardrail facts: the status vocabulary, what bounds the page or the scan, units, and the
empty/zero/null semantics. The full original prose moves, sentence for sentence, into the tail that
get_tool_guide serves. Neither tool has a Lite twin (git grep -n 'Name = "get_blocking"' -- Lite/Mcp and the
same search for get_pg_replication_slots both come back empty), so there is no D6 lockstep pair here.

Tool Darling before Darling after (served head)
get_blocking 1,645 chars 619 chars
get_pg_replication_slots 803 chars 618 chars

Neither tool is on Lite, so there is no Lite column.

Corrections

None. Both tools' original prose held up against the code. Nothing needed correcting.

D4 removals

get_blocking's dedup_key parameter read "Optional #1140 alert fingerprint...". The #1140 reference is gone
from the parameter's on-wire text. It was never copied into the tail either. Neither tool's main description
carried an issue reference.

Re-pointed pins

  • DarlingMcpBlockingToolsSurfaceAndSqlTests.ParamContract_DedupKeyDescription_AdvertisesItsScoping covers
    get_blocking, get_deadlocks, and get_deadlock_detail. Only get_blocking is mine. It requires the
    dedup_key parameter's own text to contain "Dedup Key" and "display name". Those two phrases stayed in the
    trimmed parameter text on the wire. They did not move to the tail. The test's own comment gives the reason. An
    agent picks arguments from a tool's own description. A caveat that is only in the tail is one that an agent
    never sees if it reads only the parameter.
  • McpPageContractTests.FingerprintReaders_NameTheScanCeilingFields("get_blocking") requires "rows_examined" and
    "scan_truncated" in the tool's own raw description. That is still true. Both are in the tail, which is part of
    the same attribute string. It also requires "BEFORE limit" in the dedup_key parameter's own text, kept in the
    guardrail text for the same reason as above.

get_blocking.dedup_key needed all three pinned phrases inside the 200-character parameter cap. It was 406
characters, and 400 once the #1140 reference is dropped. The trimmed text is 191 characters: "Optional alert
fingerprint (the alert's Dedup Key). The key is scoped to the server's display name and the incident's involved
objects. The fingerprint scan runs over the window BEFORE limit."

Two pieces move in full to the tool's own tail, under the label dedup_key:. The first is what supplying the key
returns, with the "paste it from a ticket" note. The second is the scan-ceiling detail, which the head already
covers.

D9 traps

  • Q: get_blocking returns status empty. Does that prove the collector ran in this window?
    Correct: No. not_collected already ruled out an incompatible engine. Empty still covers both "nothing
    happened" and "nothing was collected".
    Prevents it: "empty means none in the window, or none collected in it."
  • Q: get_blocking says truncated true at hours_back=24. Will hours_back=48 fix it?
    Correct: No. Widening the window can only add more candidate rows, never fewer. truncated is a row cap, not
    a time cap.
    Prevents it: "limit caps ROWS, not hours_back: truncated true means raise limit or narrow the window, not
    widen hours_back."
  • Q: A dedup_key call returns status empty with no events. Does that prove there was no blocking at all in the
    window?
    Correct: No. It proves no row matched that fingerprint. The window can still hold plenty of other blocking,
    all filtered out by the key.
    Prevents it: "a no-match answer is still empty."
  • Q: A caller sets limit to 5. Does the dedup_key fingerprint scan only look at those 5 newest rows?
    Correct: No. The fingerprint scan runs over the whole window before limit is applied, up to its own stated
    ceiling.
    Prevents it: "dedup_key scans the whole window before limit, up to a stated ceiling
    (rows_examined/scan_truncated)."
  • Q: blocked_last_tran_started reads earlier than event_time. Does that point at clock skew between the
    monitored server and the store?
    Correct: No. These timestamps are already de-skewed to the same clock frame as event_time, so the ordering
    is meaningful as read.
    Prevents it: "last_tran/last_batch stamps are de-skewed for direct comparison to event_time"
  • Q: wait_time_ms reads 500. Is that half a second, or 500 seconds?
    Correct: Milliseconds. 500 ms is half a second.
    Prevents it: "wait_time_ms is milliseconds."
  • Q: get_pg_replication_slots returns no_slots for a replica target. Does that prove the whole cluster has no
    abandoned slot?
    Correct: No. Replication slots live on the writer, so a replica's own no_slots says nothing about the
    writer's slots.
    Prevents it: "a replica's no_slots doesn't clear the writer."
  • Q: retained_wal_bytes is null for a slot. Does null mean the slot is retaining 0 bytes?
    Correct: No. Null means the collector's -1 sentinel applied: unmeasured or not applicable, not a measured
    zero.
    Prevents it: "null on the collector's -1 sentinel"
  • Q: has_configured_wal_ceiling is false for every slot on a server. Is that a sign of missing data?
    Correct: No. False is the default, normal state, because max_slot_wal_keep_size is -1 by default, so there
    is no configured ceiling. It is not a gap.
    Prevents it: "no ceiling by default isn't a gap."
  • Q: retained_wal_growth_bytes reads 0 for a slot. Is that the same as null, meaning growth was not measured?
    Correct: No. 0 is a real measurement: the slot is flat, no growth this window. Null means growth itself has
    no measurement at all.
    Prevents it: "retained_wal_growth_bytes is 0 when flat (measured), null when unmeasurable."
  • Q: worst_slot names a slot with a smaller retained_wal_bytes than another slot in the same payload. Is that
    a bug?
    Correct: No. worst_slot is chosen by severity classification, not by raw retained size. Size only breaks
    ties inside the same severity.
    Prevents it: "worst_slot ranks by severity, not size."
  • Q: get_pg_replication_slots is called against a SQL Server target and returns not_collected. Does that mean
    the collector is broken?
    Correct: No. not_collected here means the target's engine cannot run this collector at all. It says nothing
    about a working collector's health.
    Prevents it: "not_collected: engine is not PostgreSQL."
  • Q: The trimmed get_blocking.dedup_key parameter no longer spells out what supplying the key returns. Was the
    display-name scoping guarantee dropped from the tool?
    Correct: No. The parameter's own on-wire text still states the key is scoped to the server's display name
    and the incident's involved objects. Only the illustrative wording and the already-duplicated ceiling detail
    moved to the tail.
    Prevents it: "The key is scoped to the server's display name and the incident's involved objects." (in the
    dedup_key parameter's own served text)
  • Q: (added by the coordinator) get_pg_replication_slots answers status slots_present. Does that status tell
    you the slots are healthy?
    Correct: No. It only says slot rows were sampled in the window. Health is in worst_severity and each
    slot's severity.
    Prevents it: "slots_present: slots in window; read worst_severity."

Status routes

get_blocking (Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpBlockingTools.cs):

  • not_collected, line 90: rows.Count == 0 and the resolved server's engine cannot run the
    blocked_process_report collector.
  • empty (no rows), line 91: rows.Count == 0 and the engine check above did not fire.
  • empty (no match), line 112: a dedup_key was supplied and no row's fingerprint equals it, after scanning the
    window up to the fingerprint ceiling.
  • error, line 201: the read threw.

get_pg_replication_slots (Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpPgSlotTools.cs):

  • not_collected, line 104: rows.Count == 0 and the resolved server's engine cannot run the
    pg_replication_slots collector, for example a SQL Server target.
  • no_slots, line 115: rows.Count == 0 and the engine check above did not fire, on a real PostgreSQL target
    with no sampled slot row in the window.
  • slots_present, line 183: rows.Count > 0.
  • error, line 195: the read threw.

Test plan

  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug: Build succeeded, 0 Warning(s), 0
    Error(s).
  • dropcheck (python dropcheck.py origin/dev HEAD get_blocking get_pg_replication_slots) prints
    get_blocking [Darling] old=1645 new=2654 sentences=8 missing=0 and
    get_pg_replication_slots [Darling] old=803 new=1380 sentences=5 missing=0.
  • Red-watch: broke get_pg_replication_slots's "worst_slot ranks by severity, not size." head fact.
    Confirmed McpToolGuideHeadsBlockingSlotsTests.EveryConvertedHead_CarriesItsGuardrailFact_AndThePointer failed
    on it. Restored and confirmed green again.
  • Targeted classes (Darling.Tests.exe -class ...): DarlingMcpBlockingToolsSurfaceAndSqlTests (30 passed),
    McpToolsListBudgetTests + McpToolGuideTests + McpPageContractTests (58 passed),
    McpPayloadContractCensusTests + McpZeroIsAMeasurementTests + McpPayloadClockFrameDisciplineTests +
    ConsumedTimestampFrameDisciplineTests (113 passed), DarlingPgSlotReaderTests + DarlingBlockingStatsReadTests
    • DarlingBlockingTrendEmptyTests + DarlingContentiousObjectLabelTests + DarlingIncidentFingerprintTests
      (64 total, 4 skipped for no PostgreSQL rig, rest passed), and the new McpToolGuideHeadsBlockingSlotsTests
      (7 passed).
  • Full Darling.Tests suite, after git merge origin/dev: 13,315 total, 0 errors, 2 failed, 636 skipped (no
    PostgreSQL rig for this lane). The 2 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. This PR does not touch those files.
  • Lite suite: not run. This PR does not touch PerformanceMonitor.Common or any Lite file.
  • Plain-English checker on this PR body: body_4108.md: 29 hit(s), doc mode. The remaining hits are 28 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 for both tools. The parameter check shows dedup_key 406 -> 191. Its one "missing" sentence differs only by the #1140 reference that D4 removes. Everything else is verbatim in the tail under dedup_key:.
  • I checked every get_blocking head fact against the code. The window runs from as_of - hours_back to as_of, and rows sort by event_time DESC. No rows gives not_collected from the blocked_process_report engine check first, then empty. A dedup_key with no match gives empty. The fingerprint scan covers up to 2,000 rows of the window before limit. Every fact is correct.
  • I checked every get_pg_replication_slots head fact against the code. The collector reads the core catalog only, so not_collected means the engine is not PostgreSQL. No rows on a PostgreSQL target gives no_slots with the per-instance scope note. The -1 sentinel serves null on retained_wal_bytes, retained_wal_gb, safe_wal_size_bytes, xmin_age and catalog_xmin_age. Growth is null unless both ends were measured across a window that spans time. worst_slot is the highest severity rank, and size only breaks ties.
  • Head facts fixed: 1. slots_present is the normal case did not restate its route, which is "slot rows in the window" (rows.Count > 0). It also reads as "typical" or "healthy", but the code calls no_slots the common case, and a slots_present answer can carry a critical slot. The clause is now slots_present: slots in window; read worst_severity.
  • D9 round 1: a fresh haiku reader answered the 13 lane questions and 1 of mine (the last one above). It saw only the served heads and parameter text. Misreads: 0. The lane's Q9 came back as "can't tell", because the head does not name has_configured_wal_ceiling. My question got the right answer, but the reader took "the normal case" to mean "typical", which is the reading the code contradicts. I did not run a second round, because the edit changed only that one clause.
  • The served head of get_pg_replication_slots went from 599 to 618 characters. I updated its pin and budget line. The changelog buffer entry already said "fewer than 620 characters" and needed no change.
  • PR body: I fixed the 7 plain-English hits that the lane left (long sentences, semicolons, a 7-sentence paragraph and a present perfect). I replaced the checker line, which said every real hit was fixed. I also replaced the guess about the two DarlingInstallLocationTests failures with their known cause.
  • After the edit, the build has 0 warnings. Targeted classes: 474 tests, 0 failed, including every Darling test class that names either tool. Full suite: Darling ran 13,315 tests with 2 failed and 636 skipped. The 2 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. Lite needs no run, because this PR changes no Lite or Common file.

Handoff

🤖 Generated with Claude Code

https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC

erikdarlingdata and others added 4 commits September 23, 2026 22:08
…/list (#3898)

D3 Blocking+Slots: converts both tools to the head+tail MCP guide pattern.
Guardrail facts (status vocabulary, gates, units) move to a served head
under 620 chars; the rest of the original prose moves verbatim to the
tail get_tool_guide serves on demand. Neither tool has a Lite twin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
…t "the normal case" (#3898)

"slots_present is the normal case" did not restate its route (slot rows in
the window) and read as typical or healthy, while the code calls no_slots
the common case. The clause now says slots were seen in the window and
points at worst_severity for health. Served head 599 -> 618; pin 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:30
@erikdarlingdata
erikdarlingdata merged commit 39cd6fd into dev Sep 24, 2026
16 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/3898-content-blockingslots branch September 24, 2026 02:45
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
…ools/list (#3898) (#4123)

* get_deadlocks/get_blocked_process_reports move reading guidance off tools/list (#3898)

get_deadlocks is a twin: one byte-identical head (591 served chars) now covers both
Darling (was 822) and Lite (was 694). get_blocked_process_reports is Lite-only (was
1,340) and gets its own head (483 served), modeled on Darling's get_blocking (#4108).
Every original sentence moves to the tool's own get_tool_guide tail, unchanged.

The dedup_key parameter (Darling-only, get_deadlocks) was 406 chars, over the D2 200-char
cap; its "BEFORE limit" scan-order sentence stays on the parameter because an existing
pin (McpPageContractTests.FingerprintReaders_NameTheScanCeilingFields) reads it there,
and the other two original sentences moved verbatim into the tail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC

* Re-point two existing dedup_key pins found by the full suite (#3898)

McpPageContractTests.FingerprintReaders_NameTheScanCeilingFields and
DarlingMcpBlockingToolsSurfaceAndSqlTests.ParamContract_DedupKeyDescription_AdvertisesItsScoping
both read facts straight off the dedup_key parameter's own Description attribute. My
first trim moved one or the other fact to the tail and broke whichever pin it left
behind. get_blocking (#4108) already solved this in the same file: keep "BEFORE limit"
and "display name" in a 191-char parameter, and repeat the full original parameter text
verbatim in the tool's own tail under a "dedup_key:" label. get_deadlocks now matches
that shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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