Repository navigation
analyze_procedure_plan moves reading guidance off tools/list (#3898) - #4117
Merged
Merged
Conversation
Darling and Lite both serve a 608-char head (was 926/912) plus a get_tool_guide pointer; the moved detail (operator ranking fields, the missing-index caveat's exact wording) lands verbatim in each product's tail. The differing key parameter (sql_handle on Darling, plan_handle on Lite) is named in one head clause per D6. Re-points Lite.Tests/McpDescriptionTruthPinTests's shared plan-tools Description() helper from head-only to head+tail, since the fragments it pins are guardrails against the #3696 suppression regression coming back, not a rule that they must live in the head specifically. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
10 tasks done
…e truth-pin helper stays head-only (#3898) The Lite truth pin reads the served head on purpose (#3898 D3), so this PR no longer re-points it to head plus tail. The head now carries the pinned missing-index and operator-cut fragments in 587 characters (served 618). 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 03:32
erikdarlingdata
enabled auto-merge (squash)
September 24, 2026 03:32
erikdarlingdata
added a commit
that referenced
this pull request
Sep 24, 2026
…3898 D9) The three plan tools (analyze_plan_xml #4115, analyze_procedure_plan #4117, analyze_query_plan here) now share one guardrail block verbatim in their heads. Replaces lane e1's own head wording with the ruled block: per statement warnings, missing_indexes/impact_basis/create_statement caveats, parameters, memory_grant, top_operators. Adds the null-not-zero actual_* sentence the tool's head needs on top of that shared block. Verified against the code on both products before writing the head: - "latest plan" is collection_time DESC LIMIT 1 on both Lite (LocalDataService.QueryStats.cs) and Darling (DarlingStoredPlanReader.QueryStatsPlanXmlByHashSql). - Both route not_collected before falling back to unavailable (McpPlanTools.cs / DarlingMcpPlanTools.cs AnalyzeQueryPlan). - actual_rows/actual_elapsed_ms/actual_cpu_ms are (long?)null, not 0, without HasActualStats (McpPlanAnalysisFormatter.cs), shared by both products. Tails are untouched: both already carried every pre-#3898 sentence verbatim, so dropcheck reports missing=0 without any tail edit. Updates McpToolGuideHeads.AnalyzeQueryPlan.cs (both products) to pin the new head's guardrail facts instead of the old ones, and the tools/list budget lines (613 -> 616, the new served length) in both products' McpToolsListBudget files. Does not touch Lite.Tests/McpDescriptionTruthPinTests.cs or Darling/Darling.Tests/DarlingMcpPlanToolsTests.cs -- their existing plan-tool pins already match this head's fragments verbatim. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K
7 of 8 tasks
erikdarlingdata
added a commit
that referenced
this pull request
Sep 24, 2026
* analyze_query_plan moves reading guidance off tools/list (#3898) Splits analyze_query_plan's served tools/list description into a terse head (<=620 served chars) plus a get_tool_guide tail carrying the full original prose, on both Darling and Lite. Adds McpToolGuideHeads.AnalyzeQueryPlan.cs on both products and updates each product's DarlingMcpPlanTools.txt / McpPlanTools.txt budget for analyze_query_plan only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC * Revert McpDescriptionTruthPinTests.cs to dev: the plan pins read the head via the shared helper (#4115) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K * analyze_query_plan head takes the shared plan-tool guardrail block (#3898 D9) The three plan tools (analyze_plan_xml #4115, analyze_procedure_plan #4117, analyze_query_plan here) now share one guardrail block verbatim in their heads. Replaces lane e1's own head wording with the ruled block: per statement warnings, missing_indexes/impact_basis/create_statement caveats, parameters, memory_grant, top_operators. Adds the null-not-zero actual_* sentence the tool's head needs on top of that shared block. Verified against the code on both products before writing the head: - "latest plan" is collection_time DESC LIMIT 1 on both Lite (LocalDataService.QueryStats.cs) and Darling (DarlingStoredPlanReader.QueryStatsPlanXmlByHashSql). - Both route not_collected before falling back to unavailable (McpPlanTools.cs / DarlingMcpPlanTools.cs AnalyzeQueryPlan). - actual_rows/actual_elapsed_ms/actual_cpu_ms are (long?)null, not 0, without HasActualStats (McpPlanAnalysisFormatter.cs), shared by both products. Tails are untouched: both already carried every pre-#3898 sentence verbatim, so dropcheck reports missing=0 without any tail edit. Updates McpToolGuideHeads.AnalyzeQueryPlan.cs (both products) to pin the new head's guardrail facts instead of the old ones, and the tools/list budget lines (613 -> 616, the new served length) in both products' McpToolsListBudget files. Does not touch Lite.Tests/McpDescriptionTruthPinTests.cs or Darling/Darling.Tests/DarlingMcpPlanToolsTests.cs -- their existing plan-tool pins already match this head's fragments verbatim. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K --------- 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>
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
analyze_procedure_planserved 926 characters on Darling and 912 on Lite in everytools/listcall. Most of that text is reading guidance: how to interpretimpact_basis, whycreate_statementis corroboration and not a diagnosis. It also covers howtop_operatorsis capped and ranked. A model needs that guidance once it already has a result in hand, not before it decides whether to call the tool. This PR moves that guidance toget_tool_guide, the same way earlier #3898 lanes converted the HealthParser and Data families.What changes
Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpPlanTools.cs:analyze_procedure_plan'sDescriptionnow carries a terse head, the<<GUIDE>>marker, then the full original text as the tail.Lite/Mcp/McpPlanTools.cs: the same split, with Lite's own first sentence (it keys the lookup byplan_handle, notsql_handle) in its own tail.Darling/Darling.Tests/McpToolGuideHeads.AnalyzeProcedurePlan.cs,Lite.Tests/McpToolGuideHeads.AnalyzeProcedurePlan.cs.analyze_procedure_planinDarling/Darling.Tests/McpToolsListBudget/DarlingMcpPlanTools.txtandLite.Tests/McpToolsListBudget/McpPlanTools.txt.Before/after served characters
Both products now serve the identical 618-character head, which satisfies the D6 lockstep rule. The two products key the lookup by a different parameter:
sql_handleon Darling,plan_handleon Lite. The head states both in one clause rather than picking one product's word. D6 allows this when the underlying fact itself differs by product.D6 note: the tool body, not just the description
Darling reads a stored plan out of its own Postgres store (
DarlingStoredPlanReader.GetProcedurePlanXmlBySqlHandleAsync, keyed bysql_handle, theprocedure_stats.sql_handlehex string). Lite reads a cached plan out of its local DuckDBv_query_statsview (LocalDataService.GetCachedProcedurePlanAsync, keyed byplan_handle). This parameter difference is real, not a naming inconsistency to paper over, so the head names both explicitly. Neither product's identifier works against the other product's tool.Status routes (every status word the head states)
GetProcedurePlanXmlBySqlHandleAsyncreturned null/empty AND this server's probed engine cannot runprocedure_statscollection at allprocedure_stats: no stored plan exists for thissql_handle, not captured or aged out of the storeMcpHelpers.FormatError, statuserroron both products)GetCachedProcedurePlanAsyncreturned null/empty AND the engine cannot runprocedure_statscollectionplan_handle, never captured or evicted from the plan cache since the last collectionMcpHelpers.FormatError)not_collectedis checked beforeunavailableon both products. The capability probe runs first on every miss. So anunavailableanswer is never proof the engine lacks this capability. It only means this specific handle has no stored plan right now.Corrections
None. The original text's behavioral claims all matched the tool body. This includes the key parameter names,
create_statementdescribed as corroboration,impact_basisdescribed as a per-statement estimate, and the operator ranking switch. Each also matched the published constants inPerformanceMonitor.PlanAnalysis/McpPlanAnalysisFormatter.csverbatim.D4 removals
None. The original description carried no issue references and no anecdotes.
Re-pointed pins
None in the final version. The lane first changed the shared
Description()helper inLite.Tests/McpDescriptionTruthPinTests.csto read head plus tail. The coordinator pass reverted that, so this PR no longer touches the file. The helper reads the served head on purpose (#3898 D3). A model needs these caveats fromtools/listbefore it acts on a missing-index row.The head now carries every fragment that
PlanTools_DescribeTheStatedOperatorCut_AndTheCreateStatementpins, in 587 characters:labelled impact_basis,create_statement — the optimizer's suggested CREATE INDEX for this statement,corroboration for a statement already measured slow, never a diagnosis,every row carries the fixed caveat,regression risk for other plans,operators_ranked_byandoperators_returned / total_operators / truncated. #4115 also makes the pin'sDoesNotContainchecks read head plus tail. That covers the lane's concern about the #3696 suppression sentence coming back in the tail.D9 traps
analyze_procedure_planreturns statusunavailable. Does that mean the plan collector is broken?Correct: Not always. A missing plan can mean the handle was never captured, or it aged out of the store.
Prevents it: "No plan: not_collected if the engine can't collect procedure_stats, else unavailable."
not_collected. Is a stopped collector the cause?Correct: No.
not_collectedmeans this engine cannot produceprocedure_statsplans at all. A capable engine's stopped collector is a different case, and it reportsunavailableinstead.Prevents it: "No plan: not_collected if the engine can't collect procedure_stats, else unavailable."
unavailableandnot_collectedboth mean no plan came back. Are they the same signal for deciding whether to escalate?Correct: No.
not_collectedleaves nothing to escalate, because this engine never produces one.unavailablemeans the engine can produce one, and this one is missing right now. That is worth checking against the collector.Prevents it: "not_collected if the engine can't collect procedure_stats, else unavailable" gives each status its own condition.
Correct: It depends on the product. Darling takes
sql_handle. Lite takesplan_handle. No single name works on both.Prevents it: "by sql_handle (Darling) or plan_handle (Lite)"
get_top_procedures_by_cpu. Does it work if I pass it to Lite'sanalyze_procedure_plan, since both tools share a name and parameter shape?Correct: No. Each product's
get_top_procedures_by_cpureturns the identifier in its own product's shape. The two are not interchangeable, even though the tool name matches.Prevents it: "by sql_handle (Darling) or plan_handle (Lite)". Each product's parameter text names
get_top_procedures_by_cpuas the source.missing_indexes[].create_statementgives runnable CREATE INDEX DDL. Is that reason enough to run it?Correct: No. It is corroboration for a statement already measured slow, not a standalone diagnosis. It also carries write cost and regression risk for other plans.
Prevents it: "corroboration for a statement already measured slow, never a diagnosis"
missing_indexes[].impact_basisis a percent. Does a bigger number mean more total server-wide benefit if I add several suggested indexes?Correct: No. It is one statement's own estimated cost reduction, taken alone. It does not add up across statements or rows, and it does not measure server load.
Prevents it: "missing_indexes labelled impact_basis". Each row's
impact_basisstring says the estimate is statement-scoped and not additive.missing_indexesrows on the same statement carry differentimpact_basisvalues. Does the higher one alone tell me which index to build first?Correct: No. Neither row is a diagnosis by itself. Both are corroboration for a statement independently measured slow, so the choice needs other evidence, not just a bigger percent.
Prevents it: "corroboration for a statement already measured slow, never a diagnosis"
top_operatorslists some rows. Is that the complete operator tree for the statement?Correct: No. It is a fixed cut of the 10 most expensive operators per statement. The rest of the plan is not in this list.
Prevents it: "with operators_returned / total_operators / truncated"
top_operatorsrank the same way on every call, so two payloads' top rows compare directly?Correct: No. It ranks by measured
actual_elapsed_mswhen the plan has runtime statistics. Without runtime statistics, it ranks by the optimizer'scost_percentestimate instead. One payload's ranking can be a measurement, and another's a guess.Prevents it: "top_operators by operators_ranked_by". Each payload states its own ranking basis.
top_operatorstogether. Does the fixed 10-row cut apply to the warnings list too?Correct: No. The cut and its ranking basis are stated for
top_operatorsalone.warningsis a separate list with no cap.Prevents it: "top_operators by operators_ranked_by, with operators_returned / total_operators / truncated" names the cut on
top_operatorsalone. Nothing in the head caps warnings.analyze_procedure_plana live query against the monitored server, so the result reflects what the server is doing right now?Correct: No. It analyzes a stored plan. Darling reads its own Postgres store, and Lite reads its local DuckDB cache. Neither is a live fetch.
Prevents it: "Analyzes a procedure's stored plan"
Test plan
Darling.Tests: 0 Warning(s), 0 Error(s).Lite.Tests: 0 Warning(s), 0 Error(s).McpToolGuide*,McpToolsListBudget*,McpDescription*,McpZeroIsAMeasurement*,McpPayloadContractCensus*,DarlingMcpPlanTools*,DarlingWebEndpoints*. Total 295, 0 failed.McpToolGuide*,McpToolsListBudget*,McpDescription*,McpZeroIsAMeasurement*,McpPayloadContractCensus*. Total 59, 0 failed after the re-point above. Before that re-point, 1 failed.Head_CarriesEveryGuardrailFact_AndStaysUnderTheCapfailed with "Not found". Restored the text and confirmed green again.dropcheck.py origin/dev HEAD analyze_procedure_plan:missing=0on both Darling and Lite.git merge origin/dev(clean, no conflicts).AnalysisPassTokenThreadingTests.TheReadLockWaitIsAbandonableWhileAWriterHoldsIt, a lock-abandon concurrency test. It has no connection to this PR's files (MCP plan-tool descriptions and one test helper). It passed cleanly when re-run alone. That makes it a timing flake in the full-suite run, not a regression from this change.Coordinator verification
origin/dev(90d6af4a) into the branch. The dropped-sentence check showsmissing=0on both products. No parameter description changed.DarlingStoredPlanReaderbysql_handle. Lite readsv_query_statsin its DuckDB store byplan_handle, newest capture first. On a miss, both callNotCollectedStatusAsyncforprocedure_statsfirst and returnunavailableonly when it returns null. Every head fact was correct. Head facts fixed: 0.analyze_plan_xml's head (analyze_plan_xml moves reading guidance off tools/list (#3898) #4115), so the plan tools describe the missing-index fields in one voice. The served size went from 608 to 618 on both products. I updated both head-pin files and both budget lines.impact_basisis not additive. The head names the label, and each row'simpact_basisstring says "statement-scoped, not additive".Handoff
Nothing deferred. The one tool in this lane's budget is fully converted, green, and pinned on both products.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K