Repository navigation
get_plan_corrections moves reading guidance off tools/list (#3898) - #4105
Merged
Merged
Conversation
Head-pin test classes for get_plan_corrections on both products, and the McpToolsListBudget ceiling update to the new served head length (598). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
…ing stamp (#3898) The D9 reader took "as_of says when" to mean the request parameter. The head now says the latest snapshot's own per-row as_of field carries the time. Served head 598 -> 611 on both products; pins and budget lines follow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K
erikdarlingdata
marked this pull request as ready for review
September 24, 2026 02:17
erikdarlingdata
enabled auto-merge (squash)
September 24, 2026 02:17
9 tasks done
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #3898.
Why
Issue #3898:
get_plan_correctionsput its full 1,672-character description into everytools/listreply onboth products. That text carries reading guidance a model only needs when it actually calls the tool. It covers
what bounds the page, when
emptybeatsnot_collected, and why timestamps line up againstget_query_store_regressions. Nothing is deleted. The guidance moves into a per-tool reading guide thatget_tool_guide(tools[], topics[])serves on demand, and a short guardrail head stays intools/list.What changes
get_plan_correctionsconverts on both products (D6 twin, byte-identical head):Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpPlanCorrectionTools.csand
Lite/Mcp/McpPlanCorrectionTools.cs.McpToolGuideHeadsSqlTailPlanCorrectionsTests, appended toDarling/Darling.Tests/McpToolGuideHeads.SqlTail.csandLite.Tests/McpToolGuideHeads.SqlTail.cs.Darling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanCorrectionTools.txtandLite.Tests/McpToolsListBudget/McpPlanCorrectionTools.txt. Only thetoolline changed. No parameter was over200 chars, so none needed a cut.
Served characters
Corrections
None. The original description matched the tool body on both products (verified against
DarlingMcpPlanCorrectionTools.csandMcpPlanCorrectionTools.cs, and their shared status routes below).D4 removals
None. The original text carried no issue-number references and no anecdote.
Re-pointed pins
None needed.
git grep -l get_plan_corrections -- Darling/Darling.Tests Lite.Testsfound five files beyond thisPR's own new tests:
DarlingMcpPlanCorrectionToolsTests.cs,McpLatestSnapshotStampTests.cs,McpPageContractTests.cs,PlanCorrectionFrameLiveTests.csandRepoFileAdoptionTests.cs. None of them pinsdescription prose that moved.
McpPageContractTestsreads the rawDescriptionAttribute.Description, which still holds the head, the<<GUIDE>>marker and the full tail together. It only asserts the text contains"truncated"and"limit".Both are still true.
McpLatestSnapshotStampTestsandRepoFileAdoptionTestsreference the tool name incomments and rosters, not its text.
PlanCorrectionFrameLiveTestsnever readsDescription.Status routes (for the D9 traps below)
DarlingMcpPlanCorrectionTools.cs:61-63andMcpPlanCorrectionTools.cs:56-58. Thecondition is
tuning.Count == 0 && rows.Count == 0, AND the engine-capability probe finds this server's enginecan never produce
plan_correctionrows, for example a PostgreSQL target. The probe isDarlingEngineCapability.NotCollectedStatusAsync(DarlingEngineCapability.cs:62-82) on Darling. Lite's twinis
McpEngineCapability.NotCollectedStatusAsync(McpEngineCapability.cs:43).CAN collect the data. There is just nothing collected or open right now.
recommendations[]but an emptyautomatic_tuning[], or the reverse, is a normal 200 payload with oneside as an empty array. Neither status word fires unless BOTH are empty.
DarlingMcpPlanCorrectionTools.cs:123/McpPlanCorrectionTools.cs:124, the catch block'sMcpHelpers.FormatError, an unhandled exception during the read.D9 traps
get_plan_correctionsreturnsrecommendations_returned: 40. Does that mean 40 distinctFORCE_LAST_GOOD_PLANrecommendations fired in the window?Correct: No. The collector re-captures every still-open recommendation on every cycle, so one
recommendation that stays open counts once per capture.
Prevents it: "Rows recur per capture, not per distinct recommendation."
recommendations[]share the samequery_idandregressed_plan_id. Is that two separatefindings, or one?
Correct: Almost certainly one. They are most likely the same recommendation, captured on two different
cycles while it stayed open. Rows are not unique findings.
Prevents it: "Rows recur per capture, not per distinct recommendation."
hours_back=168(a week) was requested,truncatedcame backtrue, andoldest_returned_collection_timeis only 6 hours before
newest_returned_collection_time. Is the reader broken?Correct: No. The page is bounded by
limit, not byhours_back. A server with several open recommendationscan fill the page in hours. Raise
limitor narrowhours_backto reach further back.Prevents it:
THE PAGE IS BOUNDED BY limit, NOT hours_back: truncated means more rows existed; oldest/newest_returned_collection_time bound the page.hours_back=1was requested, butautomatic_tuning[]lists a database whose snapshot was captured threedays ago. Is that a bug?
Correct: No.
automatic_tuningignoreshours_backandas_ofcompletely. It is always the latestcaptured snapshot, however old.
Prevents it:
automatic_tuning ignores the window: the latest snapshot; each row's as_of says when.as_ofparameter. Doesautomatic_tuning[]'s ownas_offield then comeback null?
Correct: No. The response field
as_of(per row, when that snapshot was captured) is a different thingfrom the request parameter
as_of(the window anchor). The field is populated whenever a tuning row exists,whether or not the caller passed the parameter.
Prevents it:
automatic_tuning ignores the window: the latest snapshot; each row's as_of says when.recommendations[]came back empty butautomatic_tuning[]has three rows. Does the payload carrystatus: "empty"?Correct: No.
emptyonly fires when both are empty. Here the call returns an ordinary payload withrecommendations: []and the populatedautomatic_tuningarray, no status wrapper at all.Prevents it: "No rows and no automatic_tuning: empty (not_collected checked first)."
recommendations[]andautomatic_tuning[]came back empty. Is the status always"empty"?Correct: No.
not_collectedis checked first. On an engine that can never produce this data, for example aPostgreSQL target, the answer is
not_collected, notempty.Prevents it: "No rows and no automatic_tuning: empty (not_collected checked first)."
status: "error". Does that mean this server's engine cannot collect plan corrections?Correct: No.
erroronly comes from an unhandled exception during the read. An engine that cannot collectthis data answers
not_collectedearlier in the function, on a path that never reaches the code that raisesthat exception.
Prevents it: "No rows and no automatic_tuning: empty (not_collected checked first)" (states the
not_collected-first order the error path sits outside of).
valid_since,last_refresh,execute_action_initiated_timeorrevert_action_initiated_timein themonitored server's local time zone, the way some other DMV-sourced columns are?
Correct: No. All four, like every timestamp on this payload, are UTC, stored and returned unconverted. They
compare directly against
collection_time, with no offset math.Prevents it: "All timestamps are UTC."
recommendations[]beread in that order directly?
Correct: No. The array is served newest capture first (
order: "collection_time_desc"). A chronologicaltimeline has to reverse it, or re-sort by
collection_timeascending.Prevents it: "newest capture first" (head's opening clause).
Test plan
dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug- Build succeeded, 0 Warning(s), 0 Error(s).dotnet build Lite.Tests/Lite.Tests.csproj -c Debug- Build succeeded, 0 Warning(s), 0 Error(s).get_plan_corrections old=1672 new=2250 sentences=7 missing=0.get_plan_corrections old=1672 new=2250 sentences=7 missing=0.McpToolGuideHeadsSqlTailPlanCorrectionsTests.EveryConvertedHead_CarriesItsGuardrailFact_AndThePointerfailed(Total: 2, Failed: 1), restored, confirmed green again.
McpToolGuideHeadsSqlTail*,McpToolsListBudget*,McpPageContract*,McpLatestSnapshotStamp*,DarlingMcpPlanCorrectionToolsTests,McpToolGuideTests,McpDescriptionTruthPinTests,McpZeroIsAMeasurementTests,McpPayloadContractCensusTests,MeasurementContractCensusTests- Total: 217, Failed: 0 (across the two runs).McpToolGuideHeadsSqlTail*,McpToolsListBudget*,McpPageContract*,McpToolGuideTests,McpDescriptionTruthPinTests,McpZeroIsAMeasurementTests,McpPayloadContractCensusTests,MeasurementContractCensusTests- Total: 64, Failed: 0 (across the two runs).body_4105.md: 21 hit(s), doc mode(20 D9 labels and 1 emoji). The remaining hits are the D9 labels and the attribution emoji, which are allowed.Coordinator verification
origin/dev(c51706c9) into the branch. The dropped-sentence check showsmissing=0on both products. No parameter description changed.collection_time >= start AND collection_time <= as_of. Rows sort bycollection_time DESC, score DESC.truncatedcomes from alimit + 1fetch.automatic_tuningreads the server's newest capture (MAX(collection_time)) with no time filter.emptycomes back only when both lists are empty, and only afterNotCollectedStatusAsyncreturns null. Every head fact is correct. Head facts fixed: 0.as_ofto mean the request parameter. So I changeda latest snapshot, as_of says whentothe latest snapshot; each row's as_of says whenon both products. I updated the two head pins and the two budget lines. The served head went from 598 to 611 characters.DarlingInstallLocationTestspair 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
None. One tool, fully converted, both products in lockstep, no unconverted work left for this lane.
🤖 Generated with Claude Code
https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC