@herbertgao/pi-extensions 2026.8.15 → 2026.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
- package/node_modules/@czottmann/pi-automode/README.md +20 -0
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +20 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +46 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +6 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +34 -30
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +5 -1
- package/node_modules/@narumitw/pi-btw/dist/index.ts +143 -14
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +158 -7
- package/node_modules/@narumitw/pi-btw/src/text.ts +18 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +5 -16
- package/node_modules/@narumitw/pi-caffeinate/LICENSE +21 -0
- package/node_modules/@narumitw/pi-caffeinate/README.md +224 -0
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +1112 -0
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-caffeinate/package.json +53 -0
- package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +811 -0
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +153 -0
- package/node_modules/@narumitw/pi-caffeinate/src/index.ts +1 -0
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +43 -0
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +167 -0
- package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +202 -0
- package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
- package/node_modules/@tifan/pi-handoff/README.md +6 -3
- package/node_modules/@tifan/pi-handoff/package.json +1 -1
- package/node_modules/@tifan/pi-handoff/src/index.ts +8 -3
- package/node_modules/pi-lens/CHANGELOG.md +988 -0
- package/node_modules/pi-lens/README.md +3 -0
- package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +1 -3
- package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -4
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
- package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
- package/node_modules/pi-lens/dist/clients/cache-observability.js +44 -18
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
- package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
- package/node_modules/pi-lens/dist/clients/config-core/index.js +47 -0
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +171 -0
- package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +167 -0
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
- package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
- package/node_modules/pi-lens/dist/clients/config-schema.js +197 -0
- package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
- package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
- package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +14 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -18
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +380 -26
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +13 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
- package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
- package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
- package/node_modules/pi-lens/dist/clients/file-time.js +6 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +112 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
- package/node_modules/pi-lens/dist/clients/format-service.js +6 -1
- package/node_modules/pi-lens/dist/clients/formatters.js +276 -102
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +114 -19
- package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
- package/node_modules/pi-lens/dist/clients/git-guard.js +1 -1
- package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +71 -52
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +104 -230
- package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
- package/node_modules/pi-lens/dist/clients/language-registry.js +595 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +160 -2
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +13 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +141 -30
- package/node_modules/pi-lens/dist/clients/lens-engine.js +30 -37
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +59 -6
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +168 -30
- package/node_modules/pi-lens/dist/clients/lsp/config.js +483 -158
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +15 -2
- package/node_modules/pi-lens/dist/clients/lsp/index.js +676 -71
- package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -169
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -16
- package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +9 -2
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
- package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +2 -4
- package/node_modules/pi-lens/dist/clients/module-report.js +20 -45
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -20
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +68 -22
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +4 -0
- package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
- package/node_modules/pi-lens/dist/clients/path-utils.js +428 -13
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +77 -16
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
- package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +393 -122
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
- package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
- package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
- package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
- package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +15 -4
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
- package/node_modules/pi-lens/dist/clients/read-guard.js +21 -0
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +152 -109
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +18 -2
- package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
- package/node_modules/pi-lens/dist/clients/runtime-session.js +139 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +168 -35
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
- package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
- package/node_modules/pi-lens/dist/clients/sanitize.js +7 -1
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
- package/node_modules/pi-lens/dist/clients/sgconfig.js +6 -1
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
- package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +239 -0
- package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
- package/node_modules/pi-lens/dist/clients/tool-policy.js +91 -21
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +86 -48
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
- package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
- package/node_modules/pi-lens/dist/clients/user-notify.js +6 -2
- package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/word-index-store.js +20 -5
- package/node_modules/pi-lens/dist/clients/word-index.js +243 -31
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +3 -0
- package/node_modules/pi-lens/dist/index.js +64335 -54300
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +127 -5
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +95 -24
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +67 -80
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
- package/node_modules/pi-lens/dist/tools/shared.js +0 -1
- package/node_modules/pi-lens/docs/agent-guide.md +64 -1
- package/node_modules/pi-lens/docs/configuration.md +222 -0
- package/node_modules/pi-lens/docs/dependencies.md +3 -3
- package/node_modules/pi-lens/docs/features.md +44 -5
- package/node_modules/pi-lens/docs/language-coverage.md +2 -2
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +27 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +38 -0
- package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +182 -0
- package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
- package/node_modules/pi-lens/package.json +15 -23
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
- package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +27 -0
- package/node_modules/pi-mcp-adapter/README.md +13 -13
- package/node_modules/pi-mcp-adapter/commands.ts +58 -21
- package/node_modules/pi-mcp-adapter/config.ts +18 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +26 -6
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +4 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +13 -4
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +11 -3
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/error-signal.ts +15 -4
- package/node_modules/pi-mcp-adapter/errors.ts +141 -0
- package/node_modules/pi-mcp-adapter/host-html-template.ts +58 -5
- package/node_modules/pi-mcp-adapter/index.ts +2 -3
- package/node_modules/pi-mcp-adapter/init.ts +5 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +30 -9
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +66 -28
- package/node_modules/pi-mcp-adapter/mcp-status.ts +2 -0
- package/node_modules/pi-mcp-adapter/package.json +2 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +66 -13
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +217 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +337 -6
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +1 -0
- package/node_modules/pi-mcp-adapter/types.ts +18 -3
- package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +18 -2
- package/node_modules/pi-mcp-adapter/ui-server.ts +179 -54
- package/node_modules/pi-mcp-adapter/ui-session.ts +28 -8
- package/node_modules/pi-web-access/CHANGELOG.md +28 -0
- package/node_modules/pi-web-access/README.md +62 -15
- package/node_modules/pi-web-access/curator-page.ts +3 -1
- package/node_modules/pi-web-access/curator-server.ts +5 -1
- package/node_modules/pi-web-access/gemini-search.ts +8 -4
- package/node_modules/pi-web-access/github-extract.ts +242 -1
- package/node_modules/pi-web-access/index.ts +118 -64
- package/node_modules/pi-web-access/mistral-search.ts +281 -0
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/perplexity.ts +14 -1
- package/node_modules/pi-web-access/utils.ts +23 -6
- package/node_modules/pi-web-access/xai-search.ts +96 -33
- package/package.json +15 -11
|
@@ -12,6 +12,7 @@ import * as nodeFs from "node:fs";
|
|
|
12
12
|
import fs from "node:fs/promises";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { pathToFileURL, URL } from "node:url";
|
|
15
|
+
import { BoundedFifoMap } from "../bounded-cache.js";
|
|
15
16
|
import { getProjectIgnoreMatcher, isExcludedDirName } from "../file-utils.js";
|
|
16
17
|
import { recordLsp } from "../widget-state.js";
|
|
17
18
|
import { applyAuxiliarySuppressions } from "../dispatch/auxiliary-lsp.js";
|
|
@@ -22,7 +23,10 @@ import { logSessionStart } from "../sessionstart-logger.js";
|
|
|
22
23
|
import { incrementDegradationCount, recordDegradation, recordDegradationOnce, } from "../degradation-ledger.js";
|
|
23
24
|
import { isLspSpawnAllowedByTrust, assertInstallAllowed, projectTrustDenialReason, } from "../project-trust.js";
|
|
24
25
|
import { shouldPreferPullOnlyDiagnostics } from "../lsp-budget.js";
|
|
25
|
-
import {
|
|
26
|
+
import { sampleProcessTreeCpuPercent } from "../resource-sampler.js";
|
|
27
|
+
import { bounded, withDeadline, withTimeout } from "../deadline-utils.js";
|
|
28
|
+
import { getAmbientAbortSignal } from "../safe-spawn.js";
|
|
29
|
+
import { abortDeferredLspWork } from "../deferred-lsp-work.js";
|
|
26
30
|
import { acquireWorkspaceSweepHold, clearWorkspaceSweepHoldForSessionStart, } from "./workspace-sweep-hold.js";
|
|
27
31
|
import { DocumentDriftTracker, fingerprintDocumentContent, } from "./document-drift.js";
|
|
28
32
|
import { markPendingAuxiliaryCoverage, napiFallbackCoveredSince, } from "./pending-aux-coverage.js";
|
|
@@ -37,7 +41,7 @@ import { getServersForFileWithConfig, getServerInitOverride, } from "./config.js
|
|
|
37
41
|
import { getSessionRootsForTelemetry, isOutsideAllSessionRoots, } from "./session-roots.js";
|
|
38
42
|
import { getProcessSingleton } from "../process-singletons.js";
|
|
39
43
|
import { getLanguageId } from "./language.js";
|
|
40
|
-
import { LSP_SERVERS, enforceLspRootCeiling, hasProjectBoundaryMarker, isDirectLspCommandTemporarilyUnavailable, resetClassicTsRepairGuard, } from "./server.js";
|
|
44
|
+
import { LSP_SERVERS, enforceLspRootCeiling, hasProjectBoundaryMarker, isDirectLspCommandTemporarilyUnavailable, resetClassicTsRepairGuard, resetLspLaunchAvailabilityGeneration, } from "./server.js";
|
|
41
45
|
import { classifyCascadeWaitTier, classifyServerWaitTier, getStrategy, } from "./wait-policy/index.js";
|
|
42
46
|
const WORKSPACE_ATTRIBUTION_CLIENT_CAP = 16;
|
|
43
47
|
import { raceToCompletion } from "./aggregation.js";
|
|
@@ -468,6 +472,19 @@ function notifyWriteBudgetMs() {
|
|
|
468
472
|
const raw = Number(process.env.PI_LENS_LSP_NOTIFY_BUDGET_MS);
|
|
469
473
|
return Number.isFinite(raw) && raw > 0 ? raw : 2000;
|
|
470
474
|
}
|
|
475
|
+
// #2239: the SAME effective per-server wait floor `getClientForFile` uses to
|
|
476
|
+
// size its own acquisition race — the caller's declared budget, raised to
|
|
477
|
+
// whatever the file's primary server(s) configure via `clientWaitTimeoutMs`
|
|
478
|
+
// (Ruby 30s, and the Bash/JSON/Vue/Svelte/Prisma overrides #2233 added). A
|
|
479
|
+
// cold primary spawn is allowed to run this long, so anything downstream that
|
|
480
|
+
// charges itself against `maxWaitMs` alone — rather than this floor — sees an
|
|
481
|
+
// already-elapsed time it never budgeted for and clamps to zero.
|
|
482
|
+
function primaryServerWaitFloorMs(filePath, maxWaitMs) {
|
|
483
|
+
const serverWaitOverrideMs = getServersForFileWithConfig(filePath)
|
|
484
|
+
.filter((s) => s.role !== "auxiliary")
|
|
485
|
+
.reduce((max, server) => Math.max(max, server.clientWaitTimeoutMs ?? 0), 0);
|
|
486
|
+
return Math.max(maxWaitMs ?? 0, serverWaitOverrideMs);
|
|
487
|
+
}
|
|
471
488
|
// #1459: how long ONE auxiliary notify write may stay outstanding before the
|
|
472
489
|
// server counts as wedged rather than merely slow. A scanner whose per-file work
|
|
473
490
|
// exceeds the write budget is normal (opengrep routinely needs >2s on a large
|
|
@@ -479,6 +496,30 @@ const NOTIFY_WEDGED_BUDGET_MULTIPLIER = 5;
|
|
|
479
496
|
function notifyWedgedMs() {
|
|
480
497
|
return notifyWriteBudgetMs() * NOTIFY_WEDGED_BUDGET_MULTIPLIER;
|
|
481
498
|
}
|
|
499
|
+
// #2358: the wedge window's teardown decision gained a liveness discriminator.
|
|
500
|
+
// The fixed window above stays the FLOOR; a server whose drain history says it
|
|
501
|
+
// answers per-write in `ewmaMs` with `unacked` documents queued earns
|
|
502
|
+
// `k x ewmaMs x unacked` of patience instead — the issue's "a server that
|
|
503
|
+
// historically answers in 855 ms with 8 writes queued earns 15-20 s". The cap
|
|
504
|
+
// bounds the whole thing so a busy-but-never-draining server still respawns.
|
|
505
|
+
const NOTIFY_WEDGED_EWMA_MULTIPLIER = 2;
|
|
506
|
+
const NOTIFY_WEDGED_CAP_MS = 60_000;
|
|
507
|
+
function notifyWedgedCapMs() {
|
|
508
|
+
const raw = Number(process.env.PI_LENS_LSP_NOTIFY_WEDGED_CAP_MS);
|
|
509
|
+
return Number.isFinite(raw) && raw > 0 ? raw : NOTIFY_WEDGED_CAP_MS;
|
|
510
|
+
}
|
|
511
|
+
// #2358: how long the CPU-liveness discriminator watches the server between
|
|
512
|
+
// two process-CPU samples before deciding "busy" vs "flat". Only ever spent
|
|
513
|
+
// on a server whose write is already past its patience window — never on the
|
|
514
|
+
// per-edit hot path.
|
|
515
|
+
function notifyStallCpuSampleMs() {
|
|
516
|
+
const raw = Number(process.env.PI_LENS_LSP_NOTIFY_STALL_CPU_SAMPLE_MS);
|
|
517
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 1200;
|
|
518
|
+
}
|
|
519
|
+
// A server burning more than this percent of one core across the sample
|
|
520
|
+
// window counts as BUSY (progressing), not flat (wedged). A single-threaded
|
|
521
|
+
// scanner under burst sits near 100; an idle or dead one near 0.
|
|
522
|
+
const NOTIFY_STALL_CPU_BUSY_FLOOR_PERCENT = 10;
|
|
482
523
|
// #1714: how many document notifies one auxiliary may hold UNACKNOWLEDGED
|
|
483
524
|
// before the next notify has to prove the server drained its input.
|
|
484
525
|
//
|
|
@@ -712,6 +753,22 @@ export class LSPService {
|
|
|
712
753
|
* successful write clears its entry.
|
|
713
754
|
*/
|
|
714
755
|
notifyWriteBackpressureStreak = new Map();
|
|
756
|
+
/**
|
|
757
|
+
* #2358: EWMA of one auxiliary's per-document drain latency
|
|
758
|
+
* (ms per unacknowledged write), keyed by "serverId:normalizedRoot".
|
|
759
|
+
*
|
|
760
|
+
* Updated from DRAINED notify barriers only: the round-trip proves the
|
|
761
|
+
* server processed its `outstanding` backlog, and
|
|
762
|
+
* duration / outstanding is the per-write service time a single-threaded
|
|
763
|
+
* scanner actually achieves under load. The breaker grants a wedged write
|
|
764
|
+
* `k x ewma x unacked` of patience from it, so a slow-but-alive scanner is
|
|
765
|
+
* not killed at the fixed window by construction (#2358).
|
|
766
|
+
*
|
|
767
|
+
* Cleared by the service teardown (via `session_start`) like the rest of
|
|
768
|
+
* the breaker state; a stale throughput estimate from a previous session's
|
|
769
|
+
* client must not price the next one.
|
|
770
|
+
*/
|
|
771
|
+
auxNotifyDrainLatencyEwma = new Map();
|
|
715
772
|
/**
|
|
716
773
|
* #1714: unacknowledged auxiliary document notifies per server key
|
|
717
774
|
* ("serverId:normalizedRoot" — the same identity as every other gate here).
|
|
@@ -748,6 +805,15 @@ export class LSPService {
|
|
|
748
805
|
* never lands is still demoted (see {@link demoteForNotifyStall}).
|
|
749
806
|
*/
|
|
750
807
|
outstandingAuxNotifyWrites = new Map();
|
|
808
|
+
/**
|
|
809
|
+
* #2356: auxiliary clients removed by the notify-stall breaker are a
|
|
810
|
+
* temporary absence, not a missing scanner. Keep the demotion identity after
|
|
811
|
+
* deleting the client so the turn-end late-coverage probe can re-arm the
|
|
812
|
+
* pending pair until a replacement is published (or its existing bounded
|
|
813
|
+
* re-arm ceiling is reached). A successful replacement removes the marker.
|
|
814
|
+
*/
|
|
815
|
+
static MAX_NOTIFY_STALL_DEMOTIONS = 50;
|
|
816
|
+
notifyStallDemotions = new BoundedFifoMap(LSPService.MAX_NOTIFY_STALL_DEMOTIONS);
|
|
751
817
|
/** LRU clock for capacity eviction, keyed by the canonical server/root key. */
|
|
752
818
|
clientLastUsedAt = new Map();
|
|
753
819
|
/**
|
|
@@ -948,6 +1014,7 @@ export class LSPService {
|
|
|
948
1014
|
return false;
|
|
949
1015
|
}
|
|
950
1016
|
const [victimKey, victimClient] = victim;
|
|
1017
|
+
this.releaseOutstandingAuxNotifyWrite(victimKey);
|
|
951
1018
|
await victimClient.shutdown({ reason: "client_ceiling_lru" });
|
|
952
1019
|
this.state.clients.delete(victimKey);
|
|
953
1020
|
this.state.clientSpawnedAt.delete(victimKey);
|
|
@@ -964,6 +1031,15 @@ export class LSPService {
|
|
|
964
1031
|
clearTimeout(timer);
|
|
965
1032
|
this.typeScriptIdleTimers.delete(key);
|
|
966
1033
|
}
|
|
1034
|
+
/** Release a notify token and its timer when its client generation retires. */
|
|
1035
|
+
releaseOutstandingAuxNotifyWrite(key, token) {
|
|
1036
|
+
const current = this.outstandingAuxNotifyWrites.get(key);
|
|
1037
|
+
if (!current || (token !== undefined && current !== token))
|
|
1038
|
+
return;
|
|
1039
|
+
clearTimeout(current.wedgeTimer);
|
|
1040
|
+
this.outstandingAuxNotifyWrites.delete(key);
|
|
1041
|
+
current.resolveSettled();
|
|
1042
|
+
}
|
|
967
1043
|
scheduleTypeScriptIdleEviction(key) {
|
|
968
1044
|
if (!key.startsWith("typescript:"))
|
|
969
1045
|
return;
|
|
@@ -989,6 +1065,7 @@ export class LSPService {
|
|
|
989
1065
|
// Publish the cold state synchronously before awaiting teardown. A request
|
|
990
1066
|
// arriving while shutdown is in progress therefore waits on the spawn gate
|
|
991
1067
|
// and creates a fresh client; it can never receive this retiring one.
|
|
1068
|
+
this.releaseOutstandingAuxNotifyWrite(key);
|
|
992
1069
|
this.state.clients.delete(key);
|
|
993
1070
|
this.state.clientSpawnedAt.delete(key);
|
|
994
1071
|
this.state.demonstratedReady.delete(key);
|
|
@@ -1130,23 +1207,130 @@ export class LSPService {
|
|
|
1130
1207
|
return;
|
|
1131
1208
|
}
|
|
1132
1209
|
this.notifyWriteBackpressureStreak.delete(key);
|
|
1133
|
-
|
|
1210
|
+
// #2358: the consecutive-timeout ladder lands here too, and its verdict
|
|
1211
|
+
// must not kill a server that is demonstrably BUSY. The guarded demotion
|
|
1212
|
+
// samples the live process CPU and merely resets the ladder (the next
|
|
1213
|
+
// three timeouts re-try) when the server is progressing — the wedged-write
|
|
1214
|
+
// path's cap owns a busy-but-never-draining server, not this streak.
|
|
1215
|
+
void this.demoteNotifyStallCpuGuarded(key, entry, filePath, {
|
|
1134
1216
|
consecutiveTimeouts: NOTIFY_BACKPRESSURE_BROKEN_AFTER,
|
|
1135
1217
|
});
|
|
1136
1218
|
}
|
|
1219
|
+
/**
|
|
1220
|
+
* #2358: liveness-guarded teardown for the STREAK ladder.
|
|
1221
|
+
*
|
|
1222
|
+
* The wedged-write path ({@link decideNotifyStallTeardown}) keeps its own
|
|
1223
|
+
* re-arm loop so a busy server's outstanding write stays un-torn-down; the
|
|
1224
|
+
* streak ladder has no write to hold — it simply does not demote, and the
|
|
1225
|
+
* next three timeouts re-attempt. Generation-guarded after the await so a
|
|
1226
|
+
* client replaced mid-sample is never demoted for a verdict about its
|
|
1227
|
+
* predecessor.
|
|
1228
|
+
*/
|
|
1229
|
+
async demoteNotifyStallCpuGuarded(key, entry, filePath, reason) {
|
|
1230
|
+
if (this.state.clients.get(key) !== entry.client)
|
|
1231
|
+
return;
|
|
1232
|
+
// The CPU verdict is asynchronous, but the streak has already committed to
|
|
1233
|
+
// this client's teardown path. Release the TypeScript idle-timer ownership
|
|
1234
|
+
// before sampling so another idle callback cannot target the same client
|
|
1235
|
+
// while the verdict is in flight. A BUSY verdict below re-arms a fresh timer.
|
|
1236
|
+
this.clearTypeScriptIdleTimer(key);
|
|
1237
|
+
const verdict = await this.notifyStallCpuVerdict(entry);
|
|
1238
|
+
if (verdict.cpuVerdict === "busy") {
|
|
1239
|
+
if (this.state.clients.get(key) === entry.client) {
|
|
1240
|
+
this.scheduleTypeScriptIdleEviction(key);
|
|
1241
|
+
}
|
|
1242
|
+
this.logNotifyStallCpuBusy(key, entry, filePath, 0, verdict);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (this.state.clients.get(key) !== entry.client)
|
|
1246
|
+
return;
|
|
1247
|
+
this.demoteForNotifyStall(key, entry, filePath, {
|
|
1248
|
+
...reason,
|
|
1249
|
+
discriminator: "consecutive-timeouts",
|
|
1250
|
+
cpuVerdict: verdict.cpuVerdict,
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* #2358: classify a stalled server's live process CPU.
|
|
1255
|
+
*
|
|
1256
|
+
* A client with no process handle (a test/mock double, a legacy client)
|
|
1257
|
+
* reports "unmeasured" — the caller keeps the pre-#2358 demote-at-budget
|
|
1258
|
+
* behavior, because there is no liveness evidence to override it. A sampled
|
|
1259
|
+
* process that burns a core is BUSY; anything else is FLAT, including a
|
|
1260
|
+
* query that failed to resolve the pid — a server we cannot measure is not
|
|
1261
|
+
* proven busy, and the replacement self-heal is cheap.
|
|
1262
|
+
*/
|
|
1263
|
+
async notifyStallCpuVerdict(entry) {
|
|
1264
|
+
const pid = entry.client.getProcessPid?.();
|
|
1265
|
+
if (pid === undefined || !(Number.isFinite(pid) && pid > 0)) {
|
|
1266
|
+
return { cpuVerdict: "unmeasured", cpuPercent: null };
|
|
1267
|
+
}
|
|
1268
|
+
const sample = await sampleProcessTreeCpuPercent(pid, notifyStallCpuSampleMs(), NOTIFY_STALL_CPU_BUSY_FLOOR_PERCENT);
|
|
1269
|
+
if (!sample.measured) {
|
|
1270
|
+
return { cpuVerdict: "unmeasured", cpuPercent: null };
|
|
1271
|
+
}
|
|
1272
|
+
if (sample.busy) {
|
|
1273
|
+
return { cpuVerdict: "busy", cpuPercent: sample.cpuPercent };
|
|
1274
|
+
}
|
|
1275
|
+
return { cpuVerdict: "flat", cpuPercent: sample.cpuPercent };
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* #2358: one bounded row per busy defer. Not a failure record — the
|
|
1279
|
+
* discrimination succeeded — so it is a plain latency phase; its volume is
|
|
1280
|
+
* bounded by the re-arm cadence of one outstanding write (at most the wedge
|
|
1281
|
+
* cap divided by the budget, ~6 fires at defaults).
|
|
1282
|
+
*/
|
|
1283
|
+
logNotifyStallCpuBusy(key, entry, filePath, outstandingMs, verdict, extra = {}) {
|
|
1284
|
+
emitBounded("lsp_notify_stall_cpu_busy", `${key}:${normalizeMapKey(filePath)}`, {
|
|
1285
|
+
type: "phase",
|
|
1286
|
+
durationMs: outstandingMs,
|
|
1287
|
+
metadata: {
|
|
1288
|
+
serverId: entry.info.id,
|
|
1289
|
+
clientKey: key,
|
|
1290
|
+
outstandingMs,
|
|
1291
|
+
cpuPercent: verdict.cpuPercent ?? null,
|
|
1292
|
+
...extra,
|
|
1293
|
+
},
|
|
1294
|
+
}, {
|
|
1295
|
+
ledgerKind: "lsp-notify-stall-cpu-busy",
|
|
1296
|
+
risingEdgePer: "identity",
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* #2358: the patience window a wedged auxiliary write earns.
|
|
1301
|
+
*
|
|
1302
|
+
* max(fixed floor, k x EWMA per-write latency x unacked depth), capped so a
|
|
1303
|
+
* busy-but-never-draining server still respawns within the cap. The EWMA
|
|
1304
|
+
* comes from DRAINED notify barriers (see {@link noteAuxNotifyDrainLatency});
|
|
1305
|
+
* with no history the fixed floor (the pre-#2358 window) stands.
|
|
1306
|
+
*/
|
|
1307
|
+
auxNotifyWedgeBudgetMs(key) {
|
|
1308
|
+
let budgetMs = notifyWedgedMs();
|
|
1309
|
+
const ewma = this.auxNotifyDrainLatencyEwma.get(key);
|
|
1310
|
+
if (ewma !== undefined && ewma > 0) {
|
|
1311
|
+
const unacked = this.auxNotifyInflight.get(key)?.unacked ?? 0;
|
|
1312
|
+
budgetMs = Math.max(budgetMs, NOTIFY_WEDGED_EWMA_MULTIPLIER * ewma * unacked);
|
|
1313
|
+
}
|
|
1314
|
+
return Math.min(budgetMs, notifyWedgedCapMs());
|
|
1315
|
+
}
|
|
1137
1316
|
/**
|
|
1138
1317
|
* The demotion itself, shared by the #743 consecutive-timeout streak and the
|
|
1139
1318
|
* #1459 wedged-write rule. Both mean the same thing — this client's input path
|
|
1140
1319
|
* is not moving — and both need the same teardown.
|
|
1141
1320
|
*/
|
|
1142
1321
|
demoteForNotifyStall(key, entry, filePath, reason) {
|
|
1322
|
+
// An async verdict belongs to one client generation. Never let a
|
|
1323
|
+
// predecessor's decision delete or cool down its replacement.
|
|
1324
|
+
if (this.state.clients.get(key) !== entry.client)
|
|
1325
|
+
return;
|
|
1143
1326
|
this.notifyWriteBackpressureStreak.delete(key);
|
|
1144
|
-
this.
|
|
1327
|
+
this.releaseOutstandingAuxNotifyWrite(key);
|
|
1145
1328
|
// #1714: the demoted client is torn down, so its backlog count describes a
|
|
1146
1329
|
// process that no longer exists. Leaving it would make the replacement start
|
|
1147
1330
|
// at the ceiling and pay a barrier on its first file.
|
|
1148
1331
|
this.auxNotifyInflight.delete(key);
|
|
1149
1332
|
this.state.broken.set(key, Date.now() + BROKEN_BASE_COOLDOWN_MS);
|
|
1333
|
+
this.notifyStallDemotions.set(key, Date.now());
|
|
1150
1334
|
void entry.client.shutdown().catch(() => { });
|
|
1151
1335
|
this.state.clients.delete(key);
|
|
1152
1336
|
this.state.clientSpawnedAt.delete(key);
|
|
@@ -1209,6 +1393,87 @@ export class LSPService {
|
|
|
1209
1393
|
}
|
|
1210
1394
|
this.auxNotifyInflight.set(key, { client, unacked: 1 });
|
|
1211
1395
|
}
|
|
1396
|
+
/**
|
|
1397
|
+
* #2358: fold one DRAINED notify barrier's per-write latency into the
|
|
1398
|
+
* per-key EWMA. A drained round-trip proves the server processed its
|
|
1399
|
+
* `outstanding` backlog after the ping; duration / outstanding is the
|
|
1400
|
+
* per-document service time the single-threaded scanner actually achieves
|
|
1401
|
+
* under load, which is exactly what a busy-server patience window must be
|
|
1402
|
+
* priced from (#2358's evidence: an 855 ms per-answer server with an
|
|
1403
|
+
* 8-write backlog, able to drain in 10-20 s while healthy).
|
|
1404
|
+
*/
|
|
1405
|
+
noteAuxNotifyDrainLatency(key, durationMs, outstanding, record) {
|
|
1406
|
+
if (!(outstanding > 0 && durationMs > 0))
|
|
1407
|
+
return;
|
|
1408
|
+
if (record && this.auxNotifyInflight.get(key) !== record)
|
|
1409
|
+
return;
|
|
1410
|
+
const perWriteMs = durationMs / outstanding;
|
|
1411
|
+
const prev = this.auxNotifyDrainLatencyEwma.get(key);
|
|
1412
|
+
this.auxNotifyDrainLatencyEwma.set(key, prev === undefined ? perWriteMs : 0.5 * perWriteMs + 0.5 * prev);
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* #2358: decide whether a wedged notify write's teardown really fires.
|
|
1416
|
+
*
|
|
1417
|
+
* Before #2358 the breaker killed a server whose write stayed outstanding
|
|
1418
|
+
* past a FIXED window, conflating a dead input path with a busy scanner
|
|
1419
|
+
* draining a burst — the live opengrep kill at #2358's head. The decision
|
|
1420
|
+
* now has two guards, and the record names which one fired:
|
|
1421
|
+
*
|
|
1422
|
+
* - the window is ADAPTIVE — max(fixed floor, k x EWMA per-write latency x
|
|
1423
|
+
* unacked depth), capped at {@link notifyWedgedCapMs} — so a server that
|
|
1424
|
+
* historically answers in 855 ms with 8 writes queued earns 13.7 s (k=2),
|
|
1425
|
+
* not 10 s;
|
|
1426
|
+
* - past the window, the server's process CPU is sampled twice; a BUSY
|
|
1427
|
+
* server is left alone (the write stays outstanding and the caller re-arms
|
|
1428
|
+
* this timer), a FLAT or unmeasured one is torn down.
|
|
1429
|
+
*
|
|
1430
|
+
* The hard cap still kills: a busy server that cannot drain inside it is
|
|
1431
|
+
* damaged, and the replacement is the self-heal the breaker exists to
|
|
1432
|
+
* provide.
|
|
1433
|
+
*/
|
|
1434
|
+
async decideNotifyStallTeardown(key, entry, filePath, token) {
|
|
1435
|
+
if (this.state.clients.get(key) !== entry.client ||
|
|
1436
|
+
this.outstandingAuxNotifyWrites.get(key) !== token) {
|
|
1437
|
+
throw new Error("stale notify generation");
|
|
1438
|
+
}
|
|
1439
|
+
const outstandingMs = Date.now() - token.startedAt;
|
|
1440
|
+
const armedBudgetMs = token.armedBudgetMs;
|
|
1441
|
+
if (outstandingMs >= notifyWedgedCapMs()) {
|
|
1442
|
+
return {
|
|
1443
|
+
action: "demote",
|
|
1444
|
+
reason: {
|
|
1445
|
+
outstandingMs,
|
|
1446
|
+
discriminator: "cap-exceeded",
|
|
1447
|
+
budgetMs: armedBudgetMs,
|
|
1448
|
+
},
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
const verdict = await this.notifyStallCpuVerdict(entry);
|
|
1452
|
+
if (this.state.clients.get(key) !== entry.client ||
|
|
1453
|
+
this.outstandingAuxNotifyWrites.get(key) !== token) {
|
|
1454
|
+
throw new Error("stale notify generation");
|
|
1455
|
+
}
|
|
1456
|
+
if (verdict.cpuVerdict === "busy") {
|
|
1457
|
+
this.logNotifyStallCpuBusy(key, entry, filePath, outstandingMs, verdict, {
|
|
1458
|
+
budgetMs: armedBudgetMs,
|
|
1459
|
+
});
|
|
1460
|
+
return { action: "rearm", budgetMs: this.auxNotifyWedgeBudgetMs(key) };
|
|
1461
|
+
}
|
|
1462
|
+
return {
|
|
1463
|
+
action: "demote",
|
|
1464
|
+
reason: {
|
|
1465
|
+
outstandingMs,
|
|
1466
|
+
discriminator: verdict.cpuVerdict === "unmeasured"
|
|
1467
|
+
? "budget-exceeded"
|
|
1468
|
+
: "budget-exceeded-cpu-flat",
|
|
1469
|
+
budgetMs: armedBudgetMs,
|
|
1470
|
+
ewmaInputMs: this.auxNotifyDrainLatencyEwma.get(key),
|
|
1471
|
+
unackedDepth: this.auxNotifyInflight.get(key)?.unacked ?? 0,
|
|
1472
|
+
cpuVerdict: verdict.cpuVerdict,
|
|
1473
|
+
cpuPercent: verdict.cpuPercent ?? null,
|
|
1474
|
+
},
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1212
1477
|
/**
|
|
1213
1478
|
* #1714: is this auxiliary already holding as many documents as it may?
|
|
1214
1479
|
*
|
|
@@ -1339,11 +1604,17 @@ export class LSPService {
|
|
|
1339
1604
|
// and the waiter is what latches. A second latch here would be
|
|
1340
1605
|
// unreachable, and no test could hold it honest.
|
|
1341
1606
|
}
|
|
1607
|
+
// #2358: a DRAINED round-trip is a per-write latency measurement —
|
|
1608
|
+
// the server really processed its backlog — so it feeds the EWMA
|
|
1609
|
+
// the adaptive wedge window is priced from.
|
|
1610
|
+
const durationMs = Date.now() - startedAt;
|
|
1611
|
+
if (drained)
|
|
1612
|
+
this.noteAuxNotifyDrainLatency(key, durationMs, outstanding, record);
|
|
1342
1613
|
this.noteDrainBarrierOutcome(key, entry, filePath, context, {
|
|
1343
1614
|
unacked: outstanding,
|
|
1344
1615
|
limit,
|
|
1345
1616
|
waitMs,
|
|
1346
|
-
durationMs
|
|
1617
|
+
durationMs,
|
|
1347
1618
|
outcome: drained ? "drained" : "stalled",
|
|
1348
1619
|
});
|
|
1349
1620
|
return drained;
|
|
@@ -1466,7 +1737,7 @@ export class LSPService {
|
|
|
1466
1737
|
// respawned) says nothing about this client's stdin — drop it, so a stale
|
|
1467
1738
|
// entry can never starve a healthy server.
|
|
1468
1739
|
if (outstanding && outstanding.client !== entry.client) {
|
|
1469
|
-
this.
|
|
1740
|
+
this.releaseOutstandingAuxNotifyWrite(clientKey, outstanding);
|
|
1470
1741
|
}
|
|
1471
1742
|
else if (outstanding) {
|
|
1472
1743
|
const outstandingMs = Date.now() - outstanding.startedAt;
|
|
@@ -1503,30 +1774,70 @@ export class LSPService {
|
|
|
1503
1774
|
startedAt: Date.now(),
|
|
1504
1775
|
client: entry.client,
|
|
1505
1776
|
settled,
|
|
1506
|
-
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1509
|
-
//
|
|
1510
|
-
//
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1777
|
+
armedBudgetMs: 0,
|
|
1778
|
+
// SAFETY: the handle is a torn-initialized placeholder. It is armed
|
|
1779
|
+
// a beat later in this same atomic claim (no await in between) and
|
|
1780
|
+
// thereafter only ever written by this token's own fire/release
|
|
1781
|
+
// closures, so the placeholder value is never read by anyone.
|
|
1782
|
+
wedgeTimer: undefined,
|
|
1783
|
+
resolveSettled: () => resolveSettled?.(),
|
|
1784
|
+
};
|
|
1785
|
+
// A write nothing accepts for the whole wedge window is a dead input
|
|
1786
|
+
// path, not a slow scan. Armed HERE rather than checked by the next
|
|
1787
|
+
// waiter: inside a burst every waiter arrives within one budget, so a
|
|
1788
|
+
// waiter-side check could never see the wedge window elapse and a
|
|
1789
|
+
// wedged scanner was never demoted. Unref'd so it cannot hold a
|
|
1790
|
+
// one-shot host alive, and cleared on release.
|
|
1791
|
+
//
|
|
1792
|
+
// #2358: the window itself is now ADAPTIVE (issue's `max(fixed,
|
|
1793
|
+
// k x EWMA per-answer latency x unacked depth)`, capped), and when it
|
|
1794
|
+
// fires the server's process CPU is sampled before the kill — a busy
|
|
1795
|
+
// server is left alone and the timer re-arms, so a scanner draining a
|
|
1796
|
+
// burst is never killed by construction. Every fire re-checks the
|
|
1797
|
+
// token identity after its awaits, so a release or a concurrent
|
|
1798
|
+
// demotion that lands mid-sample aborts the decision.
|
|
1799
|
+
const fireWedge = async () => {
|
|
1800
|
+
if (this.outstandingAuxNotifyWrites.get(clientKey) !== token ||
|
|
1801
|
+
this.state.clients.get(clientKey) !== entry.client) {
|
|
1802
|
+
this.releaseOutstandingAuxNotifyWrite(clientKey, token);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
let decision;
|
|
1806
|
+
try {
|
|
1807
|
+
decision = await this.decideNotifyStallTeardown(clientKey, entry, filePath, token);
|
|
1808
|
+
}
|
|
1809
|
+
catch {
|
|
1810
|
+
// A failed decision must not strand the outstanding write or the
|
|
1811
|
+
// waiter: fall back to the pre-#2358 demote-at-budget teardown.
|
|
1812
|
+
decision = {
|
|
1813
|
+
action: "demote",
|
|
1814
|
+
reason: {
|
|
1815
|
+
outstandingMs: Date.now() - token.startedAt,
|
|
1816
|
+
discriminator: "budget-exceeded",
|
|
1817
|
+
budgetMs: token.armedBudgetMs,
|
|
1818
|
+
},
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
if (this.outstandingAuxNotifyWrites.get(clientKey) !== token ||
|
|
1822
|
+
this.state.clients.get(clientKey) !== entry.client) {
|
|
1823
|
+
this.releaseOutstandingAuxNotifyWrite(clientKey, token);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
if (decision.action === "demote") {
|
|
1827
|
+
this.demoteForNotifyStall(clientKey, entry, filePath, decision.reason);
|
|
1518
1828
|
resolveSettled?.();
|
|
1519
|
-
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
token.wedgeTimer = setTimeout(() => void fireWedge(), decision.budgetMs);
|
|
1832
|
+
token.wedgeTimer.unref?.();
|
|
1520
1833
|
};
|
|
1834
|
+
token.armedBudgetMs = this.auxNotifyWedgeBudgetMs(clientKey);
|
|
1835
|
+
token.wedgeTimer = setTimeout(() => void fireWedge(), token.armedBudgetMs);
|
|
1521
1836
|
token.wedgeTimer.unref?.();
|
|
1522
1837
|
this.outstandingAuxNotifyWrites.set(clientKey, token);
|
|
1523
1838
|
return {
|
|
1524
1839
|
release: () => {
|
|
1525
|
-
|
|
1526
|
-
if (this.outstandingAuxNotifyWrites.get(clientKey) === token) {
|
|
1527
|
-
this.outstandingAuxNotifyWrites.delete(clientKey);
|
|
1528
|
-
}
|
|
1529
|
-
resolveSettled?.();
|
|
1840
|
+
this.releaseOutstandingAuxNotifyWrite(clientKey, token);
|
|
1530
1841
|
},
|
|
1531
1842
|
};
|
|
1532
1843
|
}
|
|
@@ -1607,6 +1918,30 @@ export class LSPService {
|
|
|
1607
1918
|
return entries.sort((a, b) => (priority.get(a.serverId) ?? Number.MAX_SAFE_INTEGER) -
|
|
1608
1919
|
(priority.get(b.serverId) ?? Number.MAX_SAFE_INTEGER));
|
|
1609
1920
|
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Documents currently open on any live client rooted at or under `cwd`
|
|
1923
|
+
* (#2430), as normalized path keys.
|
|
1924
|
+
*
|
|
1925
|
+
* The third contributor to the observational mutation net's tracked-file
|
|
1926
|
+
* set, alongside the read-guard's read/write set and the widget's diagnostic
|
|
1927
|
+
* files. It is pure in-memory enumeration — no spawn, no open, no stat —
|
|
1928
|
+
* and it is deliberately capped by the caller rather than here, because the
|
|
1929
|
+
* cap belongs to the sweep's budget, not to the pool's bookkeeping.
|
|
1930
|
+
*/
|
|
1931
|
+
getOpenDocumentPaths(cwd) {
|
|
1932
|
+
const paths = new Set();
|
|
1933
|
+
for (const { client } of this.activeClientsForCwd(cwd)) {
|
|
1934
|
+
try {
|
|
1935
|
+
for (const filePath of client.openDocumentPaths?.() ?? [])
|
|
1936
|
+
paths.add(filePath);
|
|
1937
|
+
}
|
|
1938
|
+
catch {
|
|
1939
|
+
// A client torn down mid-enumeration contributes nothing; the sweep
|
|
1940
|
+
// is advisory and must never break on a dying server.
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return [...paths];
|
|
1944
|
+
}
|
|
1610
1945
|
/**
|
|
1611
1946
|
* Get or create LSP client for a file
|
|
1612
1947
|
* Prevents duplicate client creation via in-flight promise tracking
|
|
@@ -1617,14 +1952,76 @@ export class LSPService {
|
|
|
1617
1952
|
// Primary selection considers language servers only — auxiliary servers
|
|
1618
1953
|
// (opengrep, …) attach alongside the primary and are never chosen as it.
|
|
1619
1954
|
const servers = getServersForFileWithConfig(filePath).filter((s) => s.role !== "auxiliary");
|
|
1620
|
-
|
|
1955
|
+
// #2525: candidate roots are resolved LAZILY, and at most once per call.
|
|
1956
|
+
// They are deliberately NOT hoisted: `NearestRoot` (clients/lsp/server.ts)
|
|
1957
|
+
// caches successful hits but never negatives, so a fallback that cannot
|
|
1958
|
+
// root here — DenoServer (`fallbackFor: "typescript"`, gated on
|
|
1959
|
+
// deno.json(c)) in a project with no deno.json — re-walks to the
|
|
1960
|
+
// filesystem root on EVERY call, by design, so that a deno.json
|
|
1961
|
+
// scaffolded mid-session is picked up without a restart. Hoisting would
|
|
1962
|
+
// charge that walk to every touch of every .ts file in every non-Deno
|
|
1963
|
+
// project. The sequential trial loop below needs no roots of its own
|
|
1964
|
+
// (`ensureClientForServer` resolves and bails at `!root` before any spawn
|
|
1965
|
+
// work), so a served touch — the hot path — resolves nothing extra at
|
|
1966
|
+
// all. Only the branches that EMIT a candidate list ask, and all of them
|
|
1967
|
+
// are cold paths.
|
|
1968
|
+
const rootMemo = new Map();
|
|
1969
|
+
/**
|
|
1970
|
+
* Split the candidates into the ones that could actually have been waited
|
|
1971
|
+
* on and the ones that never had a spawn slot. A record's subject must be
|
|
1972
|
+
* its producer (#1550) — the #2524 shape.
|
|
1973
|
+
*
|
|
1974
|
+
* #2525 round 3 (F3): no SHIPPED `server.root` rejects today — every one
|
|
1975
|
+
* bottoms out in `markerExists`, which only ever resolves `false` on a
|
|
1976
|
+
* miss — so this was reasoned "removed by construction". A reviewer probe
|
|
1977
|
+
* disproved that: a `root()` that rejects makes this `Promise.all` reject,
|
|
1978
|
+
* which throws out of `getClientForFile` itself (the caller never gets
|
|
1979
|
+
* `undefined` back, and `lsp_client_wait_timeout` is never emitted). A
|
|
1980
|
+
* per-server catch degrades that candidate to unrooted instead of taking
|
|
1981
|
+
* the whole partition down — the same "producer" the memo already knows
|
|
1982
|
+
* (a failed root resolution never got a client either), now WITH the
|
|
1983
|
+
* failure reason so the record says why, not just that.
|
|
1984
|
+
*/
|
|
1985
|
+
const partitionCandidates = async () => {
|
|
1986
|
+
const resolved = await Promise.all(servers.map(async (server) => {
|
|
1987
|
+
try {
|
|
1988
|
+
return {
|
|
1989
|
+
server,
|
|
1990
|
+
root: await this.resolveServerRootOnce(server, filePath, rootMemo),
|
|
1991
|
+
reason: undefined,
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
catch (error) {
|
|
1995
|
+
return {
|
|
1996
|
+
server,
|
|
1997
|
+
root: undefined,
|
|
1998
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
}));
|
|
2002
|
+
const unrootedReasons = {};
|
|
2003
|
+
for (const entry of resolved) {
|
|
2004
|
+
if (entry.root === undefined && entry.reason !== undefined) {
|
|
2005
|
+
unrootedReasons[entry.server.id] = entry.reason;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return {
|
|
2009
|
+
rooted: resolved
|
|
2010
|
+
.filter((entry) => entry.root !== undefined)
|
|
2011
|
+
.map(({ server, root }) => ({ server, root })),
|
|
2012
|
+
unrootedIds: resolved
|
|
2013
|
+
.filter((entry) => entry.root === undefined)
|
|
2014
|
+
.map((entry) => entry.server.id),
|
|
2015
|
+
unrootedReasons,
|
|
2016
|
+
};
|
|
2017
|
+
};
|
|
1621
2018
|
// hardCapMs is a caller-imposed ceiling (e.g. pipeline budget) that
|
|
1622
2019
|
// prevents tool_result from blocking the TUI for the full LSP cold-start
|
|
1623
2020
|
// window. When no server config sets a wait (serverWaitOverrideMs = 0),
|
|
1624
2021
|
// hardCapMs is used directly — Math.min(0, cap) = 0 would otherwise
|
|
1625
2022
|
// take the no-timeout branch and block indefinitely (e.g. pyright, which
|
|
1626
2023
|
// has no clientWaitTimeoutMs but can take 30s to initialize on cold start).
|
|
1627
|
-
const serverBaseMs =
|
|
2024
|
+
const serverBaseMs = primaryServerWaitFloorMs(filePath, maxWaitMs);
|
|
1628
2025
|
const effectiveMaxWaitMs = hardCapMs !== undefined
|
|
1629
2026
|
? serverBaseMs > 0
|
|
1630
2027
|
? Math.min(serverBaseMs, hardCapMs)
|
|
@@ -1655,7 +2052,10 @@ export class LSPService {
|
|
|
1655
2052
|
// joined it, instead of pinning a starter label on every joiner.
|
|
1656
2053
|
let erroredServerId;
|
|
1657
2054
|
let erroredOutcome;
|
|
1658
|
-
// Try each matching server
|
|
2055
|
+
// Try each matching server in registration order.
|
|
2056
|
+
// `ensureClientForServer` resolves the root itself and returns before
|
|
2057
|
+
// any spawn work when there is none, so an unrooted fallback costs
|
|
2058
|
+
// this loop one already-memoized resolution and no spawn slot.
|
|
1659
2059
|
for (const server of servers) {
|
|
1660
2060
|
// A box, not a `let`: control-flow analysis cannot see the callback
|
|
1661
2061
|
// write and would narrow a plain local to its initializer.
|
|
@@ -1664,7 +2064,7 @@ export class LSPService {
|
|
|
1664
2064
|
};
|
|
1665
2065
|
const spawned = await this.ensureClientForServer(filePath, server, resolvedRoots, noteSpawnInFlight, (reported) => {
|
|
1666
2066
|
acquisition.outcome = reported;
|
|
1667
|
-
});
|
|
2067
|
+
}, rootMemo);
|
|
1668
2068
|
if (spawned) {
|
|
1669
2069
|
logLatency({
|
|
1670
2070
|
type: "phase",
|
|
@@ -1710,8 +2110,16 @@ export class LSPService {
|
|
|
1710
2110
|
},
|
|
1711
2111
|
});
|
|
1712
2112
|
}
|
|
2113
|
+
// #2525: reuse the roots the trial loop above already resolved rather
|
|
2114
|
+
// than re-walking with the RAW `server.root`. Two reasons: the raw
|
|
2115
|
+
// call skips the ceiling/session-root gates, so this dedupe key could
|
|
2116
|
+
// disagree with the identity the acquisition path actually used; and
|
|
2117
|
+
// on the unserved path — the only path that reaches here — every
|
|
2118
|
+
// unrooted candidate would pay a second full walk to the filesystem
|
|
2119
|
+
// root, on every touch, before the `unavailableLogged` dedupe even
|
|
2120
|
+
// looked at it.
|
|
1713
2121
|
const unavailable = (await Promise.all(servers.map(async (server) => {
|
|
1714
|
-
const root = await
|
|
2122
|
+
const root = await this.resolveServerRootOnce(server, filePath, rootMemo);
|
|
1715
2123
|
return {
|
|
1716
2124
|
server,
|
|
1717
2125
|
key: `${server.id}:${root ? normalizeMapKey(root) : "<unresolved>"}`,
|
|
@@ -1767,11 +2175,13 @@ export class LSPService {
|
|
|
1767
2175
|
// `inFlight` is cleared in ensureClientForServer's finally block, so a
|
|
1768
2176
|
// settled acquisition can still be present here. Re-read the published
|
|
1769
2177
|
// clients at the decision point; a usable client outranks the sentinel.
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2178
|
+
// #2525: this is a cold path, so it is where the roots get resolved —
|
|
2179
|
+
// once, memoized with the trial loop's own resolutions. An unrooted
|
|
2180
|
+
// fallback can never have a client keyed under its (non-existent)
|
|
2181
|
+
// root, so it is neither probed here nor named in the record below.
|
|
2182
|
+
const { rooted, unrootedIds, unrootedReasons } = await partitionCandidates();
|
|
2183
|
+
for (const { server, root } of rooted) {
|
|
2184
|
+
const client = this.state.clients.get(`${server.id}:${normalizeMapKey(root)}`);
|
|
1775
2185
|
if (client?.isAlive())
|
|
1776
2186
|
return { client, info: server };
|
|
1777
2187
|
}
|
|
@@ -1783,16 +2193,46 @@ export class LSPService {
|
|
|
1783
2193
|
durationMs: 0,
|
|
1784
2194
|
metadata: {
|
|
1785
2195
|
maxWaitMs: effectiveMaxWaitMs,
|
|
1786
|
-
serverIds:
|
|
2196
|
+
serverIds: rooted.map(({ server }) => server.id),
|
|
2197
|
+
unrootedCandidates: {
|
|
2198
|
+
count: unrootedIds.length,
|
|
2199
|
+
ids: unrootedIds,
|
|
2200
|
+
...(Object.keys(unrootedReasons).length > 0
|
|
2201
|
+
? { reasons: unrootedReasons }
|
|
2202
|
+
: {}),
|
|
2203
|
+
},
|
|
1787
2204
|
reason: "budget_skipped_known_slow",
|
|
1788
2205
|
},
|
|
1789
2206
|
});
|
|
1790
2207
|
return undefined;
|
|
1791
2208
|
}
|
|
1792
2209
|
if (waitResult === timeoutSentinel) {
|
|
2210
|
+
// #2525: likewise a cold path — resolve here, not up front.
|
|
2211
|
+
//
|
|
2212
|
+
// #2525 round 3 (L2): `partitionCandidates` does UNCACHED negative
|
|
2213
|
+
// root walks (`NearestRoot` never caches misses, by design — see the
|
|
2214
|
+
// comment above `rootMemo`) and this branch only reaches it AFTER
|
|
2215
|
+
// `effectiveMaxWaitMs` has already elapsed. Measured by the reviewer:
|
|
2216
|
+
// 2.06ms native, ~150ms on the #462 slow-FS profile — unbounded on top
|
|
2217
|
+
// of a budget whose entire contract is "return within the cap". Bound
|
|
2218
|
+
// it with a small ceiling of its own (a quarter of the caller's hard
|
|
2219
|
+
// cap when one was given, capped at 250ms) rather than leaving it the
|
|
2220
|
+
// one piece of async work in this function with no bound at all
|
|
2221
|
+
// (the "async work needs both bounds" shape — here there is no
|
|
2222
|
+
// per-call abort signal to race, only the timer bound applies).
|
|
2223
|
+
const partitionBudgetMs = hardCapMs !== undefined ? Math.min(250, hardCapMs / 4) : 250;
|
|
2224
|
+
const partitioned = await withDeadline(partitionCandidates(), {
|
|
2225
|
+
ms: partitionBudgetMs,
|
|
2226
|
+
onTimeout: "undefined",
|
|
2227
|
+
});
|
|
2228
|
+
const { rooted, unrootedIds, unrootedReasons } = partitioned ?? {
|
|
2229
|
+
rooted: [],
|
|
2230
|
+
unrootedIds: [],
|
|
2231
|
+
unrootedReasons: {},
|
|
2232
|
+
};
|
|
1793
2233
|
// Snapshot known client health — scan by serverId prefix (no root needed)
|
|
1794
2234
|
const knownHealth = [...this.state.clients.entries()]
|
|
1795
|
-
.filter(([k]) =>
|
|
2235
|
+
.filter(([k]) => rooted.some(({ server }) => k.startsWith(`${server.id}:`)))
|
|
1796
2236
|
.map(([k, c]) => ({
|
|
1797
2237
|
serverId: k.split(":")[0],
|
|
1798
2238
|
alive: c.isAlive(),
|
|
@@ -1805,9 +2245,23 @@ export class LSPService {
|
|
|
1805
2245
|
durationMs: effectiveMaxWaitMs,
|
|
1806
2246
|
metadata: {
|
|
1807
2247
|
maxWaitMs: effectiveMaxWaitMs,
|
|
1808
|
-
serverIds
|
|
2248
|
+
// `serverIds` names the ROOTED pool the timeout record reports
|
|
2249
|
+
// against, not "servers this call actually waited on" — the
|
|
2250
|
+
// sequential trial loop can time out having reached only the
|
|
2251
|
+
// first of several rooted candidates (case 2 in the regression
|
|
2252
|
+
// test pins both `["typescript","deno"]` even though the loop
|
|
2253
|
+
// never got past `typescript`).
|
|
2254
|
+
serverIds: rooted.map(({ server }) => server.id),
|
|
2255
|
+
unrootedCandidates: {
|
|
2256
|
+
count: unrootedIds.length,
|
|
2257
|
+
ids: unrootedIds,
|
|
2258
|
+
...(Object.keys(unrootedReasons).length > 0
|
|
2259
|
+
? { reasons: unrootedReasons }
|
|
2260
|
+
: {}),
|
|
2261
|
+
},
|
|
1809
2262
|
// servers absent from knownHealth were never spawned or are still spawning
|
|
1810
2263
|
knownClientHealth: knownHealth,
|
|
2264
|
+
...(partitioned === undefined ? { partition: "timed-out" } : {}),
|
|
1811
2265
|
},
|
|
1812
2266
|
});
|
|
1813
2267
|
return undefined;
|
|
@@ -1815,8 +2269,10 @@ export class LSPService {
|
|
|
1815
2269
|
return waitResult;
|
|
1816
2270
|
}
|
|
1817
2271
|
/**
|
|
1818
|
-
* Get or create
|
|
1819
|
-
*
|
|
2272
|
+
* Get or create all complementary LSP clients that can serve a file. Alternate
|
|
2273
|
+
* language servers marked with `fallbackFor` remain sequential fallbacks: an
|
|
2274
|
+
* aggregate diagnostics pass must not launch them beside a working preferred
|
|
2275
|
+
* server merely because it requested `clientScope: "all"`.
|
|
1820
2276
|
*/
|
|
1821
2277
|
async getClientsForFile(filePath, excludeServerIds, resolvedRoots) {
|
|
1822
2278
|
const allServers = getServersForFileWithConfig(filePath);
|
|
@@ -1825,28 +2281,59 @@ export class LSPService {
|
|
|
1825
2281
|
: allServers;
|
|
1826
2282
|
if (servers.length === 0)
|
|
1827
2283
|
return { clients: [], serverCountAttempted: 0 };
|
|
1828
|
-
//
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2284
|
+
// Resolve once to keep the attempted count tied to servers with a real root.
|
|
2285
|
+
const rootedServers = (await Promise.all(servers.map(async (server) => ({
|
|
2286
|
+
server,
|
|
2287
|
+
root: await this.resolveServerRoot(server, filePath),
|
|
2288
|
+
})))).filter((entry) => entry.root !== undefined);
|
|
2289
|
+
let serverCountAttempted = 0;
|
|
2290
|
+
const acquisitions = new Map();
|
|
2291
|
+
const acquire = (server) => {
|
|
2292
|
+
serverCountAttempted += 1;
|
|
2293
|
+
return this.ensureClientForServer(filePath, server, resolvedRoots);
|
|
1836
2294
|
};
|
|
2295
|
+
// Start complementary servers immediately. An alternate waits only for its
|
|
2296
|
+
// own preferred server, not for unrelated scanners, and starts if that server
|
|
2297
|
+
// declines. Registry order makes chained fallbacks deterministic.
|
|
2298
|
+
for (const { server } of rootedServers) {
|
|
2299
|
+
const preferred = server.fallbackFor
|
|
2300
|
+
? acquisitions.get(server.fallbackFor)
|
|
2301
|
+
: undefined;
|
|
2302
|
+
const acquisition = preferred
|
|
2303
|
+
? preferred.then((entry) => entry === undefined ? acquire(server) : undefined)
|
|
2304
|
+
: acquire(server);
|
|
2305
|
+
acquisitions.set(server.id, acquisition);
|
|
2306
|
+
}
|
|
2307
|
+
const results = await Promise.all(acquisitions.values());
|
|
2308
|
+
const clients = results.filter((entry) => entry !== undefined);
|
|
2309
|
+
return { clients, serverCountAttempted };
|
|
1837
2310
|
}
|
|
1838
2311
|
/**
|
|
1839
2312
|
* Spawn/get the AUXILIARY clients for a file (role:"auxiliary") restricted to
|
|
1840
2313
|
* the enabled set. These attach alongside the primary on the with-auxiliary
|
|
1841
2314
|
* diagnostics path (cross-cutting scanners like opengrep).
|
|
1842
2315
|
*/
|
|
1843
|
-
async getAuxiliaryClientsForFile(filePath, enabledIds, onOutcome) {
|
|
2316
|
+
async getAuxiliaryClientsForFile(filePath, enabledIds, onOutcome, maxWaitMs, signal, hook) {
|
|
1844
2317
|
if (this.checkDestroyed() || enabledIds.size === 0)
|
|
1845
2318
|
return [];
|
|
1846
2319
|
const servers = getServersForFileWithConfig(filePath).filter((s) => s.role === "auxiliary" && enabledIds.has(s.id));
|
|
1847
2320
|
if (servers.length === 0)
|
|
1848
2321
|
return [];
|
|
1849
|
-
const
|
|
2322
|
+
const rootMemo = new Map();
|
|
2323
|
+
const effectiveSignal = signal ?? getAmbientAbortSignal();
|
|
2324
|
+
const effectiveHook = hook ?? "tool_result_edit";
|
|
2325
|
+
const spawned = await Promise.all(servers.map(async (server) => {
|
|
2326
|
+
const acquisition = this.ensureClientForServer(filePath, server, undefined, undefined, (reported) => onOutcome?.(server.id, reported), rootMemo);
|
|
2327
|
+
if (!maxWaitMs || maxWaitMs <= 0) {
|
|
2328
|
+
return acquisition;
|
|
2329
|
+
}
|
|
2330
|
+
return bounded(acquisition, {
|
|
2331
|
+
ms: maxWaitMs,
|
|
2332
|
+
signal: effectiveSignal,
|
|
2333
|
+
hook: effectiveHook,
|
|
2334
|
+
label: `auxiliary-spawn:${server.id}`,
|
|
2335
|
+
});
|
|
2336
|
+
}));
|
|
1850
2337
|
return spawned.filter((entry) => Boolean(entry));
|
|
1851
2338
|
}
|
|
1852
2339
|
/**
|
|
@@ -1928,9 +2415,11 @@ export class LSPService {
|
|
|
1928
2415
|
* the Gate-B readiness check, this NEVER creates or warms a client — it only
|
|
1929
2416
|
* resolves each requested server's root and reads the already-connected
|
|
1930
2417
|
* client's cached diagnostics for `filePath`. Servers with no live client
|
|
1931
|
-
* are
|
|
1932
|
-
*
|
|
1933
|
-
*
|
|
2418
|
+
* are absent unless notify-stall teardown marked that generation as
|
|
2419
|
+
* replaceable; that status carries the demotion timestamp so turn-end late
|
|
2420
|
+
* coverage can correlate each pair to the removed generation. A live
|
|
2421
|
+
* client is present even when its per-file cache entry is empty; its
|
|
2422
|
+
* timestamp distinguishes a published clean result from no publication.
|
|
1934
2423
|
*/
|
|
1935
2424
|
async readCachedDiagnosticsForServers(filePath, serverIds) {
|
|
1936
2425
|
const out = new Map();
|
|
@@ -1944,8 +2433,20 @@ export class LSPService {
|
|
|
1944
2433
|
continue;
|
|
1945
2434
|
const key = `${server.id}:${normalizeMapKey(root)}`;
|
|
1946
2435
|
const client = this.state.clients.get(key);
|
|
1947
|
-
if (!client?.isAlive())
|
|
2436
|
+
if (!client?.isAlive()) {
|
|
2437
|
+
const demotedAt = this.notifyStallDemotions.get(key);
|
|
2438
|
+
if (demotedAt !== undefined) {
|
|
2439
|
+
out.set(server.id, {
|
|
2440
|
+
diags: [],
|
|
2441
|
+
notifyStallDemoted: true,
|
|
2442
|
+
demotedAt,
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
1948
2445
|
continue;
|
|
2446
|
+
}
|
|
2447
|
+
// A replacement is live again. The old generation's marker no longer
|
|
2448
|
+
// describes this client and must not make a later absence look transient.
|
|
2449
|
+
this.notifyStallDemotions.delete(key);
|
|
1949
2450
|
const entry = client.getAllDiagnostics().get(normalizeMapKey(filePath));
|
|
1950
2451
|
out.set(server.id, {
|
|
1951
2452
|
diags: entry?.diags ?? [],
|
|
@@ -2156,7 +2657,29 @@ export class LSPService {
|
|
|
2156
2657
|
noteSpawnVerdict(key, verdict) {
|
|
2157
2658
|
this.lastSpawnVerdict.set(key, verdict);
|
|
2158
2659
|
}
|
|
2159
|
-
|
|
2660
|
+
/**
|
|
2661
|
+
* {@link resolveServerRoot}, memoized against a caller-owned map so one
|
|
2662
|
+
* acquisition asks a given server for its root at most once (#2525).
|
|
2663
|
+
*
|
|
2664
|
+
* Resolution is cheap exactly when it SUCCEEDS: `NearestRoot` caches hits,
|
|
2665
|
+
* but deliberately never caches negatives (so a marker scaffolded
|
|
2666
|
+
* mid-session is picked up without a restart). An unrooted candidate
|
|
2667
|
+
* therefore re-walks to the filesystem root on every call, which is why the
|
|
2668
|
+
* acquisition path and the records it emits must share one resolution
|
|
2669
|
+
* instead of each taking their own. Without a `memo` this is a plain
|
|
2670
|
+
* passthrough, for callers that have no acquisition to share with.
|
|
2671
|
+
*/
|
|
2672
|
+
resolveServerRootOnce(server, filePath, memo) {
|
|
2673
|
+
if (!memo)
|
|
2674
|
+
return this.resolveServerRoot(server, filePath);
|
|
2675
|
+
const pending = memo.get(server.id);
|
|
2676
|
+
if (pending !== undefined)
|
|
2677
|
+
return pending;
|
|
2678
|
+
const started = this.resolveServerRoot(server, filePath);
|
|
2679
|
+
memo.set(server.id, started);
|
|
2680
|
+
return started;
|
|
2681
|
+
}
|
|
2682
|
+
async ensureClientForServer(filePath, server, resolvedRoots, onSpawnInFlight, onOutcome, rootMemo) {
|
|
2160
2683
|
const handoff = this.generationHandoff;
|
|
2161
2684
|
if (handoff) {
|
|
2162
2685
|
await handoff;
|
|
@@ -2166,7 +2689,7 @@ export class LSPService {
|
|
|
2166
2689
|
if (this.checkDestroyed())
|
|
2167
2690
|
return undefined;
|
|
2168
2691
|
}
|
|
2169
|
-
const root = await this.
|
|
2692
|
+
const root = await this.resolveServerRootOnce(server, filePath, rootMemo);
|
|
2170
2693
|
if (!root || this.checkDestroyed())
|
|
2171
2694
|
return undefined;
|
|
2172
2695
|
if (server.role !== "auxiliary") {
|
|
@@ -2269,6 +2792,7 @@ export class LSPService {
|
|
|
2269
2792
|
intentional: wasIntentional,
|
|
2270
2793
|
},
|
|
2271
2794
|
});
|
|
2795
|
+
this.releaseOutstandingAuxNotifyWrite(key);
|
|
2272
2796
|
try {
|
|
2273
2797
|
await existing.shutdown();
|
|
2274
2798
|
}
|
|
@@ -2618,6 +3142,9 @@ export class LSPService {
|
|
|
2618
3142
|
diagnosticProviderKind: "unavailable",
|
|
2619
3143
|
};
|
|
2620
3144
|
this.state.clients.set(key, client);
|
|
3145
|
+
// #2356: this generation is the replacement the late-coverage probe was
|
|
3146
|
+
// waiting for. Clear the retired-generation marker before any later probe.
|
|
3147
|
+
this.notifyStallDemotions.delete(key);
|
|
2621
3148
|
this.unavailableLogged.delete(key);
|
|
2622
3149
|
// #1934 review F1: a success retires the previous verdict, so the map
|
|
2623
3150
|
// never outlives the attempts it describes.
|
|
@@ -2800,10 +3327,27 @@ export class LSPService {
|
|
|
2800
3327
|
else if (clientScope === "with-auxiliary") {
|
|
2801
3328
|
// Primary language server + the enabled cross-cutting auxiliaries
|
|
2802
3329
|
// (opengrep, …). The aggregation layer merges/dedups their diagnostics.
|
|
3330
|
+
const auxStartedAt = Date.now();
|
|
2803
3331
|
const [entry, aux] = await Promise.all([
|
|
2804
3332
|
this.getClientForFile(filePath, options.maxClientWaitMs, undefined, resolvedPrimaryRoots, waitSkipReasons),
|
|
2805
|
-
this.getAuxiliaryClientsForFile(filePath, new Set(options.auxiliaryServerIds ?? []), noteColdAuxiliary),
|
|
3333
|
+
this.getAuxiliaryClientsForFile(filePath, new Set(options.auxiliaryServerIds ?? []), noteColdAuxiliary, options.maxClientWaitMs, undefined, options.hook),
|
|
2806
3334
|
]);
|
|
3335
|
+
const auxDurationMs = Date.now() - auxStartedAt;
|
|
3336
|
+
if (options.auxiliaryServerIds && options.auxiliaryServerIds.length > 0) {
|
|
3337
|
+
logLatency({
|
|
3338
|
+
type: "phase",
|
|
3339
|
+
phase: "auxiliary_readiness",
|
|
3340
|
+
filePath: normalizedPath,
|
|
3341
|
+
durationMs: auxDurationMs,
|
|
3342
|
+
metadata: {
|
|
3343
|
+
serverIds: aux.map((s) => s.info.id),
|
|
3344
|
+
attemptedCount: options.auxiliaryServerIds.length,
|
|
3345
|
+
readyCount: aux.length,
|
|
3346
|
+
coldServerIds: [...coldAuxiliaryServerIds],
|
|
3347
|
+
source,
|
|
3348
|
+
},
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
2807
3351
|
spawned = entry ? [entry, ...aux] : aux;
|
|
2808
3352
|
serverCountAttempted = spawned.length;
|
|
2809
3353
|
}
|
|
@@ -3008,14 +3552,26 @@ export class LSPService {
|
|
|
3008
3552
|
if (!notifySkipped) {
|
|
3009
3553
|
const budget = notifyWriteBudgetMs();
|
|
3010
3554
|
// #1459: how long a queued auxiliary may wait for its resync slot. Bounded
|
|
3011
|
-
// by the write budget
|
|
3012
|
-
//
|
|
3013
|
-
//
|
|
3555
|
+
// by the write budget and by the effective primary wait floor, minus what
|
|
3556
|
+
// which includes the caller's `maxClientWaitMs` and any primary
|
|
3557
|
+
// server `clientWaitTimeoutMs` override. The elapsed client wait is
|
|
3558
|
+
// subtracted below.
|
|
3014
3559
|
// A flat write-budget wait would tax a caller that asked for less than one
|
|
3015
3560
|
// budget in total. Non-positive means "no time left to queue": the server
|
|
3016
3561
|
// is reported as uncovered immediately.
|
|
3562
|
+
//
|
|
3563
|
+
// #2239: "what the caller already declared" is `primaryServerWaitFloorMs`,
|
|
3564
|
+
// not the raw `options.maxClientWaitMs` — the SAME floor `getClientForFile`
|
|
3565
|
+
// races the primary spawn against. A cold primary configured with its own
|
|
3566
|
+
// `clientWaitTimeoutMs` (Ruby 30s, and the #2233 Bash/JSON/Vue/Svelte/Prisma
|
|
3567
|
+
// overrides) is allowed to run past the caller's flat budget, and did just
|
|
3568
|
+
// that by the time this line runs — charging the subtraction against the
|
|
3569
|
+
// flat value alone always went negative and clamped to zero. The outer
|
|
3570
|
+
// `Math.min(budget, …)` is unchanged, so this raises what "already spent"
|
|
3571
|
+
// is measured against without widening the wait beyond one write budget.
|
|
3017
3572
|
const queueWaitMs = options.maxClientWaitMs !== undefined
|
|
3018
|
-
? Math.min(budget, Math.max(0, options.maxClientWaitMs
|
|
3573
|
+
? Math.min(budget, Math.max(0, primaryServerWaitFloorMs(filePath, options.maxClientWaitMs) -
|
|
3574
|
+
(Date.now() - startedAt)))
|
|
3019
3575
|
: budget;
|
|
3020
3576
|
await Promise.all(spawned.map(async (entry) => {
|
|
3021
3577
|
// #743: this server already has this content from a recent touch
|
|
@@ -3103,10 +3659,13 @@ export class LSPService {
|
|
|
3103
3659
|
// streak outright, so only the late case retracts. A landing
|
|
3104
3660
|
// past the WEDGE window keeps its strike: at that point the
|
|
3105
3661
|
// stall was long enough that #743's demotion is the honest
|
|
3106
|
-
// verdict, not a latency artifact.
|
|
3662
|
+
// verdict, not a latency artifact. #2358: the wedge window
|
|
3663
|
+
// is the adaptive one the timer itself arms with, so a
|
|
3664
|
+
// write a BUSY server would have been left alone for also
|
|
3665
|
+
// retracts its strike instead of accruing a ladder charge.
|
|
3107
3666
|
const outstandingMs = Date.now() - writeStartedAt;
|
|
3108
3667
|
if (outstandingMs > budget &&
|
|
3109
|
-
outstandingMs <=
|
|
3668
|
+
outstandingMs <= this.auxNotifyWedgeBudgetMs(clientKey)) {
|
|
3110
3669
|
this.retractNotifyWriteBackpressure(clientKey, entry.info.id, filePath, outstandingMs, client);
|
|
3111
3670
|
}
|
|
3112
3671
|
}, release);
|
|
@@ -4398,9 +4957,7 @@ export class LSPService {
|
|
|
4398
4957
|
reason: reasons.join(", ") || "scanner coverage gap",
|
|
4399
4958
|
});
|
|
4400
4959
|
}
|
|
4401
|
-
|
|
4402
|
-
type: "phase",
|
|
4403
|
-
phase: "lsp_scanner_coverage_gap",
|
|
4960
|
+
emitBounded("lsp_scanner_coverage_gap", `${source}:${normalizedPath}`, {
|
|
4404
4961
|
filePath: normalizedPath,
|
|
4405
4962
|
durationMs: Date.now() - startedAt,
|
|
4406
4963
|
metadata: {
|
|
@@ -4409,10 +4966,10 @@ export class LSPService {
|
|
|
4409
4966
|
...(brokenSkippedServerIds.length > 0 && {
|
|
4410
4967
|
brokenSkippedServerIds,
|
|
4411
4968
|
}),
|
|
4412
|
-
// #1586: the deferrals this touch is actually uncovered for. The
|
|
4969
|
+
// #1586: the deferrals this touch is actually uncovered for. The
|
|
4413
4970
|
// gate action keeps its own record in `lsp_notify_resync_deferred`;
|
|
4414
|
-
// this row exists to prove a blackout, and a scanner already bound
|
|
4415
|
-
// these bytes is not one.
|
|
4971
|
+
// this row exists to prove a blackout, and a scanner already bound
|
|
4972
|
+
// to these bytes is not one.
|
|
4416
4973
|
...(uncoveredDeferredServerIds.length > 0 && {
|
|
4417
4974
|
deferredResyncServerIds: uncoveredDeferredServerIds,
|
|
4418
4975
|
}),
|
|
@@ -5841,6 +6398,23 @@ export class LSPService {
|
|
|
5841
6398
|
// never proved it can answer diagnostics — that's a failed warm-up. A
|
|
5842
6399
|
// server with an unresolvable key never spawns here, so it can't fail this
|
|
5843
6400
|
// way and is excluded (never skipped by the caller for this file).
|
|
6401
|
+
// #2525: same split the readiness logic above already makes — `keys[i]`
|
|
6402
|
+
// is `undefined` exactly when the server resolves no key for this file,
|
|
6403
|
+
// i.e. it will never spawn here (DenoServer without a deno.json). The
|
|
6404
|
+
// `lsp_sweep_warmup_*` records must not claim to have warmed it. Free:
|
|
6405
|
+
// `keys` is already resolved, so this costs no filesystem work.
|
|
6406
|
+
const rootedServerIds = [];
|
|
6407
|
+
const unrootedServerIds = [];
|
|
6408
|
+
for (let i = 0; i < servers.length; i++) {
|
|
6409
|
+
if (keys[i] !== undefined)
|
|
6410
|
+
rootedServerIds.push(servers[i].id);
|
|
6411
|
+
else
|
|
6412
|
+
unrootedServerIds.push(servers[i].id);
|
|
6413
|
+
}
|
|
6414
|
+
const unrootedCandidates = {
|
|
6415
|
+
count: unrootedServerIds.length,
|
|
6416
|
+
ids: unrootedServerIds,
|
|
6417
|
+
};
|
|
5844
6418
|
const stillColdServerIds = () => {
|
|
5845
6419
|
const cold = [];
|
|
5846
6420
|
for (let i = 0; i < servers.length; i++) {
|
|
@@ -5860,7 +6434,12 @@ export class LSPService {
|
|
|
5860
6434
|
phase: "lsp_sweep_warmup_start",
|
|
5861
6435
|
filePath: representativeFile,
|
|
5862
6436
|
durationMs: 0,
|
|
5863
|
-
metadata: {
|
|
6437
|
+
metadata: {
|
|
6438
|
+
serverIds: rootedServerIds,
|
|
6439
|
+
unrootedCandidates,
|
|
6440
|
+
timeoutMs,
|
|
6441
|
+
attempt,
|
|
6442
|
+
},
|
|
5864
6443
|
});
|
|
5865
6444
|
const warmupAttempt = this.touchFile(representativeFile, content, {
|
|
5866
6445
|
diagnostics: "document",
|
|
@@ -5910,7 +6489,8 @@ export class LSPService {
|
|
|
5910
6489
|
filePath: representativeFile,
|
|
5911
6490
|
durationMs: Date.now() - startedAt,
|
|
5912
6491
|
metadata: {
|
|
5913
|
-
serverIds:
|
|
6492
|
+
serverIds: rootedServerIds,
|
|
6493
|
+
unrootedCandidates,
|
|
5914
6494
|
timeoutMs,
|
|
5915
6495
|
attempt,
|
|
5916
6496
|
coldServerIds,
|
|
@@ -6935,6 +7515,9 @@ export class LSPService {
|
|
|
6935
7515
|
if (this.checkDestroyed())
|
|
6936
7516
|
return;
|
|
6937
7517
|
this.isDestroyed = true;
|
|
7518
|
+
for (const [key, token] of this.outstandingAuxNotifyWrites) {
|
|
7519
|
+
this.releaseOutstandingAuxNotifyWrite(key, token);
|
|
7520
|
+
}
|
|
6938
7521
|
for (const key of this.typeScriptIdleTimers.keys()) {
|
|
6939
7522
|
this.clearTypeScriptIdleTimer(key);
|
|
6940
7523
|
}
|
|
@@ -6987,7 +7570,15 @@ export class LSPService {
|
|
|
6987
7570
|
// #1459: every gated client is gone, so no outstanding-write record can
|
|
6988
7571
|
// describe a live one. The gate's identity check already neutralises a stale
|
|
6989
7572
|
// entry; clearing keeps the map honest rather than relying on that.
|
|
6990
|
-
this.outstandingAuxNotifyWrites
|
|
7573
|
+
for (const [key, token] of this.outstandingAuxNotifyWrites) {
|
|
7574
|
+
this.releaseOutstandingAuxNotifyWrite(key, token);
|
|
7575
|
+
}
|
|
7576
|
+
this.notifyStallDemotions.clear();
|
|
7577
|
+
// #2358: same reasoning — a per-write latency estimate belongs to a client
|
|
7578
|
+
// generation, and every client is gone. `session_start` reaches this
|
|
7579
|
+
// through the service reset, so the adaptive wedge window re-arms with
|
|
7580
|
+
// the session rather than being priced by a previous one's throughput.
|
|
7581
|
+
this.auxNotifyDrainLatencyEwma.clear();
|
|
6991
7582
|
// #1714: same reasoning — a backlog count belongs to a client generation,
|
|
6992
7583
|
// and every client is gone. `session_start` reaches this through the service
|
|
6993
7584
|
// reset, so the pacing state re-arms with the session rather than living for
|
|
@@ -7189,6 +7780,20 @@ export async function notifyExternalFileChange(filePath, type) {
|
|
|
7189
7780
|
return getLSPService().notifyExternalFileChange(filePath, type);
|
|
7190
7781
|
}
|
|
7191
7782
|
export function resetLSPService(options = {}) {
|
|
7783
|
+
// #2504 review round 2 (F3): whatever the retiring service was still being
|
|
7784
|
+
// asked for off-hook must stop HERE, before any teardown and before the
|
|
7785
|
+
// `!retiringService` early return below (a `session_start` reset with no
|
|
7786
|
+
// live service must still retire a loop the previous session armed). This
|
|
7787
|
+
// is the one choke point every lifecycle path goes through —
|
|
7788
|
+
// `session_shutdown`, `session_start`, and the idle reset — so the
|
|
7789
|
+
// deferred actionable-warnings pull cannot open another file against a
|
|
7790
|
+
// dying service, cannot re-spawn one after the idle reset, and cannot be
|
|
7791
|
+
// mid-`openFile` when the loop closes (#234). Aborting spawns nothing.
|
|
7792
|
+
abortDeferredLspWork(`lsp service reset: ${options.reason ?? "unspecified"}`);
|
|
7793
|
+
// Invalidate availability publication started by the retiring service before
|
|
7794
|
+
// any asynchronous teardown. The launch seam checks this generation after
|
|
7795
|
+
// every managed lookup, install, and process launch (#2351).
|
|
7796
|
+
resetLspLaunchAvailabilityGeneration();
|
|
7192
7797
|
// A new session must get its own classic-tsserver-repair attempt: the
|
|
7193
7798
|
// guard is a process-lifetime flag (see resetClassicTsRepairGuard), so a
|
|
7194
7799
|
// repair that failed transiently in an earlier session must not stay
|