Skip to content

get_pg_server_config counted the page and called it the server: non_default_count is now the snapshot's, computed above the cap, truncated is observed off a limit + 1 fetch, and its three sibling pages stop inferring truncation from a full page (#3653 A3 residue) - #3679

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/3653-pg-server-config-page
Sep 19, 2026

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Part of #3653 (the get_pg_server_config A3 residue, from #3541). Does not close it.

The lie

get_pg_server_config published two numbers about the server that were numbers about the page.

non_default_count was rows.Count(r => !r.IsDefault) — a count over the rows the reader had FETCHED, which the caller's limit had already cut. At limit = 25 against a server with forty chosen settings it said 25, and an agent that cannot see the code read "25 settings differ from default" as a fact about the snapshot. The number moved with the page size, which is the one thing a count of the server must never do.

truncated was rows.Count >= limit — the inference #3594 named and every other honest page in the repository has since replaced: a page that happens to be exactly limit long cannot tell "that was all of them" from "there was at least one more", and the flag said more either way. On this tool the flag was worse than uninformative, because the default view fetched the WHOLE snapshot at limit and filtered the defaults out in C# afterwards. A server has several hundred parameters and the default limit is 100, so truncated was true on nearly every default-view call even when every chosen setting was on the page. A flag that is always up says nothing.

Three sibling tools in the same file — get_pg_extensions (~175), get_pg_lock_stats (~423), get_pg_server_config_changes (~716) — carried the same >= limit inference and published it. get_pg_extensions and get_pg_lock_stats also WITHHOLD their state totals when truncated is true, so on a window holding exactly limit groups they withheld figures that were complete.

The fix

The reader carries the snapshot's count on the row statement (DarlingPgServerConfigReader.CurrentConfigSql): COUNT(*) FILTER (WHERE coalesce(c.source, 'default') <> 'default') OVER ()::int AS snapshot_non_default_count — the #3613 idiom. A window aggregate over the filtered result, which PostgreSQL evaluates before ORDER BY and LIMIT, so it is the same figure at limit = 5 and limit = 500, and the same figure whether or not defaults are included (the FILTER counts only non-default rows, and every non-default row is in both populations). The FILTER's predicate is the SAME expression the row's is_default column is read from, so the count is provably of the flag the rows report. A new PgConfigPage(Rows, SnapshotNonDefaultCount) record and GetCurrentConfigPageAsync carry it; GetCurrentConfigAsync (the Viewer's grid) is unchanged in signature and returns .Rows with every setting, as before.

The include_defaults filter moved into the statement ($2::boolean), so the cap cuts the population the caller asked for and truncated means "that population held more than limit". A pending-restart row stays in the default view whatever its source: it is the row that says the file and the running server disagree, the ordering already put it first for that reason, and the old shape counted it in pending_restart_settings while hiding it from settings. That is the one visible behaviour change beyond the counts: a pending-restart setting whose running value is still the default now appears in the default-view settings list with is_default = true, pending_restart = true. It is not counted in non_default_count — it is not a chosen setting yet.

The tool fetches limit + 1 and observes. truncated = page.Rows.Count > limit, the page is Take(limit), and the payload publishes three counts with their denominator in their names: settings_returned (the page), non_default_count (the snapshot), non_default_returned (the snapshot's chosen settings on THIS page — when it equals non_default_count, every chosen setting is here whatever truncated says about the rest). The description states which denominator each uses, in the A7 vocabulary: "THE PAGE IS BOUNDED BY limit … COUNTS ARE OF THE SNAPSHOT, NOT OF THE PAGE".

The three siblings fetch limit + 1 and observe. Their readers already bound the cap as a parameter, so it is the tool-side change only; every downstream consumer (the extension tool's PgCappedRead.Classify, per-database completeness grouping, the lock tool's ungranted count) reads the trimmed page as before.

What it does NOT do

  • It does not rename extension_count / lock_count / change_count to *_returned. They are page counts under a neutral noun, not total_*, and server-tabs.js may read them; renaming is a web-tile change outside this lane.
  • It does not touch PgCappedRead's ReturnedRows < Limit → Complete test. That classifier is documented as deliberately pessimistic ("the direction to be wrong in") and has its own two test files; a page of exactly limit rows with nothing behind it still classifies Partial there. Noted, not changed.
  • Three MORE Darling tools in OTHER files carry the same truncated = rows.Count >= limit: DarlingMcpPgDeadlockTools.cs:82, DarlingMcpPgPlanTools.cs:210 (inside BuildReadinessJson, which has its own wire-shape tests), DarlingMcpPgIndexTools.cs:150 (which also hands rows.Count to the coverage verdict). Each needs its own diff against its own pins; reported on Brains-review campaign: deferred structural residue (from #3538 / #3539 / #3540 / #3541) #3653 rather than folded in here.
  • No settings_in_snapshot total. The FILTERed count is the figure the finding named; a second window count is a separate decision.

Tests

McpPageContractTests gains a Darling-only section (Lite has no PostgreSQL tools, so — like the A7 roster — no twin-parity arm):

  • EveryPgServerStatePage_ObservesTruncation_AndNeverInfersIt — the four bodies never match TruncationInferred, each matches an observed form and asks for limit + 1; and the WHOLE file is swept, so a fifth paged tool added to it inherits the rule.
  • EveryPgServerStateRead_BindsItsCapAsAParameter_NeverALiteral — the four reader consts end in a parameterised LIMIT.
  • PgServerConfig_CountsTheSnapshotOnTheRowStatement_AndFiltersInTheQuery — the COUNT(*) FILTER … OVER () column sits above the ORDER BY, its predicate is the is_default expression, $2::boolean and the pending-restart keep are in the statement, the body reads page.SnapshotNonDefaultCount, and no .Where( remains over the rows beyond the pending-restart name list.
  • PgServerConfig_NamesItsDenominators_InItsDescription.
  • McpPageContractLivePostgresTests.PgServerConfig_CountsTheSnapshotNotThePage_AgainstDevPostgres — seeds one snapshot (four chosen, two default, one session-scoped row) plus an OLDER snapshot with eight chosen settings the newest-snapshot anchor must ignore. Default view, population 4: limit = 3 truncated with non_default_count = 4 (the old shape said 3), limit = 4 NOT truncated (the >= limit case). include_defaults, population 6: limit = 5 truncated, limit = 6 not, non_default_count = 4 on both. A pending-restart default-source row: population 5, snapshot count holds at 4, the row leads the page.

Executed on this Mac via the mactest harness (net10.0 xunit.v3, AssemblyName=Darling.Tests) against timescale/timescaledb:2.28.1-pg18: McpPageContractTests, PgServerConfigTests, McpPageContractLivePostgresTests, McpLatestSnapshotStampTests, DarlingPgReadSqlParsesLiveTests (parse analysis of every shipped reader SQL — the $2::boolean bind is what this catches), PgCappedReadSurfaceTests, DarlingCappedReadLivePostgresTests — 92 tests, 0 failed, 0 skipped. Positive control: with the tool's truncated reverted to >= limit the live test fails on the limit = 4 page (Assert.False … Actual: True). Service, Storage, Viewer and Darling.Tests build -c Release -p:EnableWindowsTargeting=true with 0 warnings.

…efault_count was rows.Count(!IsDefault) over the rows fetched, and truncated was rows.Count >= limit, so a page exactly limit long read as truncated and a limit below the number of chosen settings reported the limit as a fact about the snapshot. The count is now the snapshot's, computed on the row statement above the LIMIT; the page's own count is non_default_returned; the include_defaults filter moved into the statement so truncated is about the population asked for; and the three sibling pages in the same file (extensions, lock stats, config changes) fetch limit + 1 and observe truncation instead of inferring it (#3653)
@erikdarlingdata
erikdarlingdata merged commit f255873 into dev Sep 19, 2026
7 of 8 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3653-pg-server-config-page branch September 19, 2026 02:13
erikdarlingdata added a commit that referenced this pull request Sep 19, 2026
…le, Opus full tier (#3710) (#3718)

* REVIEW-TRAPS.md: the repository's recurring defect shapes, one line each with the PR that bit (#3710)

Seven layers (SQL, collectors and deltas, analysis, alerting and notifications,
storage and MCP payloads, tests and censuses, workflows and CI, repository
mechanics), fifty-odd lines, every one citing the pull request or issue that
verified it: the >= limit truncation inference (#3594/#3679/#3699), ORDER BY
3 + 4 folding to a constant (#3613), MAX(max_dop) across plans (#3648), integer
division in cntr_value_per_second (#3653 Q9), per-collection alert firing
against a shorter cooldown (#3628/#3636), restart zeros in deltas and baselines
(#3595/#3698/#3705/#3708), ScoreAll skipping amplifiers on a zero base and the
non-virtual GetActiveEdges (#3688), the PgTargetFactKeys censuses (#3688),
pg_total_relation_size vs the heap and the two TimescaleDB views that hide
materializations and history (#3610/#3585), the -infinity crash backoff (#3629),
the Slack block budget and the surrogate-pair cut (#3612/#3618/#3625/#3644),
LCK_M_SCH_M folding into LCK so no SCH_M fact exists (#3709), one wait profile
per run (#3709), and the CRLF round-trip. Each citation was read before it was
written down; a wrong citation is worse than none.

The review workflow reads this file before the PR body and must say, per trap
the diff is near, whether it applies here and why.

* Claude review: blind read before the body, callers rule, structured verdict with a ledger, and an Opus/Sonnet tier by changed path (#3710)

The prompt runs in two phases. Phase 1 forbids gh pr view / git log / git show
until the reviewer has read the diff, every changed region in context, the
callers of every changed public symbol (Grep, both SKUs and both test projects)
and .github/REVIEW-TRAPS.md, and written its own account. Phase 2 reads the
body and grades every claim VERIFIED / UNVERIFIED / REFUTED, then reports the
DIVERGENCE between the two accounts. The verdict is a fixed block -- Claims,
Riskiest lines with the pinning test or UNPINNED, Traps near this diff,
Divergence, Reviewer checklist -- ending in one machine-readable ledger line,
posted through gh pr review --body-file - with a quoted heredoc (no Write tool;
the allowlist is unchanged and still read-only).

The #3650 verdict check now fetches the newest verdict in the window by id and
fails the job when the body lacks the "## Verdict:" header or the ledger, or
when the ledger's verdict disagrees with the review's API state (a
"CHANGES REQUESTED" body submitted with --comment is the shape the guard would
wave through). The guard's own arm is untouched and still keys on the state.

A dorny/paths-filter@v4 step (the repo's pin style) routes changes under the
shared libraries, Darling Storage/Analysis/Service, Lite Services/Mcp/Database,
install/ and every .sql file to the full tier -- claude-opus-5[1m], 60 turns --
and everything else to claude-sonnet-5 at 30 turns; one decision step emits
tier/model/max_turns, one review step consumes them (two steps would double the
"review posted" accounting the guard does), and the prompt carries the tier and
model into the ledger. A filter that dies degrades to FULL, not light.

Step name "Claude review" and the ALWAYS POST sentinel are unchanged: both are
read by claude-review-guard.yml.

* Review tier: Alerting and Notifications read at the full tier (#3710)
erikdarlingdata added a commit that referenced this pull request Sep 23, 2026
…idance off tools/list (#3898) (#4068)

Head/tail split for the pgA family (Darling only, no Lite twins). Heads carry the gating,
empty/null and sighting-count guardrails; full original prose moves to each tool's
get_tool_guide tail. D2: min_severity, get_pg_index_bloat.limit/answered_only and
get_pg_deadlocks.limit were over the 200-char parameter cap, trimmed to a guardrail
sentence with the overflow moved verbatim into the tool's tail.

Re-points McpPageContractTests' page-contract pin (the #3679 "This is what bounds the
page" / truncated wording) to read the tail when the tool is #3898-converted, since the
wording moved off the now-shortened limit parameter for get_pg_deadlocks and
get_pg_index_bloat.


Claude-Session: https://claude.ai/code/session_01M7r1CXmEcBmsrTV34CCLfF

Co-authored-by: Claude Sonnet 5 <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