@herbertgao/pi-extensions 2026.9.1 → 2026.9.3
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 +3 -2
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/node_modules/@herbertgao/sol-pi/LICENSE +19 -0
- package/node_modules/@herbertgao/sol-pi/README.md +159 -0
- package/node_modules/@herbertgao/sol-pi/SECURITY.md +26 -0
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +19 -0
- package/node_modules/@herbertgao/sol-pi/agents-install.md +150 -0
- package/node_modules/@herbertgao/sol-pi/assets/sol-pi-hero.png +0 -0
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +69 -0
- package/node_modules/@herbertgao/sol-pi/docs/configuration.md +75 -0
- package/node_modules/@herbertgao/sol-pi/package.json +76 -0
- package/node_modules/@herbertgao/sol-pi/scripts/check-pi-compat.mjs +32 -0
- package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +120 -0
- package/node_modules/@herbertgao/sol-pi/sol-pi.example.json +10 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +135 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +74 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +185 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +128 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/archive.ts +53 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/candidate.ts +101 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/config.ts +71 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +220 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +25 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +164 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +177 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/index.ts +227 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/ledger.ts +20 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +252 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +237 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +455 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +49 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +79 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +208 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +100 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/index.ts +42 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +17 -0
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/tui.ts +71 -0
- package/node_modules/@narumitw/pi-btw/README.md +57 -141
- package/node_modules/@narumitw/pi-btw/dist/index.ts +478 -43
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/docs/workflows.md +74 -0
- package/node_modules/@narumitw/pi-btw/package.json +2 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -3
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +32 -2
- package/node_modules/@narumitw/pi-btw/src/keybindings.ts +330 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +136 -19
- package/node_modules/@narumitw/pi-btw/src/settings.ts +32 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +57 -5
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +52 -30
- package/node_modules/pi-lens/CHANGELOG.md +260 -0
- package/node_modules/pi-lens/README.md +13 -8
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -1
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -7
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +1 -1
- package/node_modules/pi-lens/dist/clients/analysed-root.js +1 -0
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +1 -8
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +2 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +133 -1
- package/node_modules/pi-lens/dist/clients/biome-client.js +13 -2
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -6
- package/node_modules/pi-lens/dist/clients/bootstrap.js +4 -4
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +1 -1
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +2 -5
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +1 -9
- package/node_modules/pi-lens/dist/clients/cache-observability.js +122 -6
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +2 -2
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -7
- package/node_modules/pi-lens/dist/clients/child-unref.js +1 -19
- package/node_modules/pi-lens/dist/clients/complexity-client.js +5 -0
- package/node_modules/pi-lens/dist/clients/config-core/index.js +11 -8
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +1 -1
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +2 -9
- package/node_modules/pi-lens/dist/clients/config-core/records.js +1 -1
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +3 -5
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +3 -1
- package/node_modules/pi-lens/dist/clients/config-resolve.js +10 -7
- package/node_modules/pi-lens/dist/clients/config-schema.js +56 -2
- package/node_modules/pi-lens/dist/clients/config-warn.js +2 -2
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +8 -1
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -4
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +5 -1
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +1 -1
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +38 -41
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +18 -1
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -6
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -3
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +2 -2
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +7 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +19 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +15 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +3 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +12 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +5 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +1 -64
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +2 -20
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +5 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +2 -2
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -7
- package/node_modules/pi-lens/dist/clients/effective-config.js +12 -2
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +2 -2
- package/node_modules/pi-lens/dist/clients/extension-log.js +2 -0
- package/node_modules/pi-lens/dist/clients/file-kinds.js +1 -38
- package/node_modules/pi-lens/dist/clients/file-role.js +18 -1
- package/node_modules/pi-lens/dist/clients/file-time.js +1 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +5 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +4 -0
- package/node_modules/pi-lens/dist/clients/format-service.js +0 -1
- package/node_modules/pi-lens/dist/clients/formatters.js +66 -93
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +3 -4
- package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -0
- package/node_modules/pi-lens/dist/clients/git-guard.js +5 -0
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +42 -6
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +48 -7
- package/node_modules/pi-lens/dist/clients/grammar-source.js +3 -3
- package/node_modules/pi-lens/dist/clients/installer/index.js +416 -61
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +84 -18
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +6 -6
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +9 -1
- package/node_modules/pi-lens/dist/clients/knip-client.js +9 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +12 -11
- package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
- package/node_modules/pi-lens/dist/clients/latency-logger.js +7 -7
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +23 -0
- package/node_modules/pi-lens/dist/clients/lens-engine.js +3 -4
- package/node_modules/pi-lens/dist/clients/lens-events.js +1 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +18 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +1 -1
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -36
- package/node_modules/pi-lens/dist/clients/lsp/client.js +4 -3
- package/node_modules/pi-lens/dist/clients/lsp/config.js +37 -7
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +24 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +79 -1
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +3 -3
- package/node_modules/pi-lens/dist/clients/lsp/index.js +491 -64
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -8
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -2
- package/node_modules/pi-lens/dist/clients/lsp/server.js +62 -15
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +5 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +6 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +37 -4
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +1 -1
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +3 -0
- package/node_modules/pi-lens/dist/clients/mcp/session.js +2 -0
- package/node_modules/pi-lens/dist/clients/metrics-history.js +28 -107
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +1 -1
- package/node_modules/pi-lens/dist/clients/module-report.js +1 -1
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +3 -3
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +3 -3
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -4
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +9 -9
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +2 -6
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +2 -0
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +1 -1
- package/node_modules/pi-lens/dist/clients/package-manager.js +2 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +23 -2
- package/node_modules/pi-lens/dist/clients/performance-report.js +2 -2
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +9 -4
- package/node_modules/pi-lens/dist/clients/pipeline.js +5 -4
- package/node_modules/pi-lens/dist/clients/project-changes.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +38 -10
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +49 -1
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +3 -51
- package/node_modules/pi-lens/dist/clients/python-provenance.js +1 -1
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +3 -1
- package/node_modules/pi-lens/dist/clients/read-guard.js +5 -2
- package/node_modules/pi-lens/dist/clients/recent-touches.js +2 -2
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +3 -5
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +1 -1
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +2 -0
- package/node_modules/pi-lens/dist/clients/ruff-client.js +18 -9
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +10 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +5 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +58 -14
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +11 -1
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +4 -1
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +48 -1
- package/node_modules/pi-lens/dist/clients/sanitize.js +0 -11
- package/node_modules/pi-lens/dist/clients/scan-utils.js +1 -56
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -2
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +69 -1
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -24
- package/node_modules/pi-lens/dist/clients/sg-runner.js +2 -1
- package/node_modules/pi-lens/dist/clients/sgconfig.js +1 -5
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +1 -1
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +108 -0
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
- package/node_modules/pi-lens/dist/clients/startup-scan.js +2 -10
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +291 -76
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +77 -72
- package/node_modules/pi-lens/dist/clients/tool-config.js +199 -0
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +299 -0
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tool-probe.js +53 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +42 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +1 -52
- package/node_modules/pi-lens/dist/clients/trivy-client.js +9 -1
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -4
- package/node_modules/pi-lens/dist/clients/turn-context.js +52 -0
- package/node_modules/pi-lens/dist/clients/user-notify.js +0 -4
- package/node_modules/pi-lens/dist/clients/widget-state.js +4 -3
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -7
- package/node_modules/pi-lens/dist/clients/word-index-store.js +3 -7
- package/node_modules/pi-lens/dist/clients/word-index.js +9 -16
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +1 -1
- package/node_modules/pi-lens/dist/index.js +19969 -17481
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -2
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +1 -1
- package/node_modules/pi-lens/dist/mcp/server.js +135 -159
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +1 -1
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +1 -1
- package/node_modules/pi-lens/dist/tools/activate-tools.js +10 -7
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +2 -13
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +8 -13
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +83 -39
- package/node_modules/pi-lens/dist/tools/effective-config.js +4 -3
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +1 -10
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +354 -86
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +54 -112
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +20 -44
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +3 -3
- package/node_modules/pi-lens/dist/tools/module-report.js +5 -9
- package/node_modules/pi-lens/dist/tools/project-report.js +9 -17
- package/node_modules/pi-lens/dist/tools/render-compact.js +281 -2
- package/node_modules/pi-lens/dist/tools/symbol-search.js +3 -4
- package/node_modules/pi-lens/docs/agent-guide.md +3 -4
- package/node_modules/pi-lens/docs/agent-tools.md +46 -22
- package/node_modules/pi-lens/docs/configuration.md +9 -0
- package/node_modules/pi-lens/docs/environment-variables.md +0 -5
- package/node_modules/pi-lens/docs/features.md +5 -5
- package/node_modules/pi-lens/docs/globalconfig.md +31 -1
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +9 -1
- package/node_modules/pi-lens/docs/mcp.md +13 -1
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +71 -0
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +15 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +88 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +41 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +21 -2
- package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
- package/node_modules/pi-lens/docs/real-harness.md +46 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +17 -5
- package/node_modules/pi-lens/docs/servercapabilities.md +2 -3
- package/node_modules/pi-lens/docs/settings.md +37 -2
- package/node_modules/pi-lens/docs/tools_improvement2.md +4 -4
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
- package/node_modules/pi-lens/docs/usage.md +13 -3
- package/node_modules/pi-lens/docs/word-index.md +1 -1
- package/node_modules/pi-lens/package.json +15 -15
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +7 -3
- package/node_modules/pi-lens/rules/typos/_typos.toml +4 -2
- package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +1 -1
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +1 -1
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +4 -2
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +8 -6
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +19 -9
- package/node_modules/pi-web-access/CHANGELOG.md +29 -0
- package/node_modules/pi-web-access/README.md +33 -22
- package/node_modules/pi-web-access/abortable.ts +17 -0
- package/node_modules/pi-web-access/crawl4ai.ts +204 -0
- package/node_modules/pi-web-access/credential-source.ts +1 -0
- package/node_modules/pi-web-access/curator-page.ts +20 -2
- package/node_modules/pi-web-access/curator-run.ts +44 -0
- package/node_modules/pi-web-access/curator-server.ts +3 -1
- package/node_modules/pi-web-access/duckduckgo.ts +1 -1
- package/node_modules/pi-web-access/extract.ts +57 -12
- package/node_modules/pi-web-access/fetch-params.ts +1 -1
- package/node_modules/pi-web-access/gemini-search.ts +9 -5
- package/node_modules/pi-web-access/index.ts +25 -18
- package/node_modules/pi-web-access/openai-search.ts +35 -13
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/page-query.ts +5 -2
- package/node_modules/pi-web-access/query-rewrite.ts +5 -2
- package/node_modules/pi-web-access/serpapi.ts +220 -0
- package/node_modules/pi-web-access/ssrf-protection.ts +5 -1
- package/node_modules/pi-web-access/storage.ts +10 -0
- package/node_modules/pi-web-access/summary-review.ts +34 -21
- package/node_modules/pi-web-access/utils.ts +6 -4
- package/package.json +7 -4
- package/node_modules/pi-lens/dist/tools/ast-dump.js +0 -103
|
@@ -25,6 +25,7 @@ import { isLspSpawnAllowedByTrust, assertInstallAllowed, projectTrustDenialReaso
|
|
|
25
25
|
import { shouldPreferPullOnlyDiagnostics } from "../lsp-budget.js";
|
|
26
26
|
import { sampleProcessTreeCpuPercent } from "../resource-sampler.js";
|
|
27
27
|
import { bounded, withDeadline, withTimeout } from "../deadline-utils.js";
|
|
28
|
+
import { HOOK_WALL_BUDGET_MS } from "../hook-budgets.js";
|
|
28
29
|
import { getAmbientAbortSignal } from "../safe-spawn.js";
|
|
29
30
|
import { abortDeferredLspWork } from "../deferred-lsp-work.js";
|
|
30
31
|
import { acquireWorkspaceSweepHold, clearWorkspaceSweepHoldForSessionStart, } from "./workspace-sweep-hold.js";
|
|
@@ -33,7 +34,7 @@ import { markPendingAuxiliaryCoverage, napiFallbackCoveredSince, } from "./pendi
|
|
|
33
34
|
import { isAtOrAboveHomeDir, isWindowsPath, normalizeMapKey, uriToPath, } from "../path-utils.js";
|
|
34
35
|
import { recordLspMutation } from "../lsp-mutation.js";
|
|
35
36
|
import { createLSPClient } from "./client.js";
|
|
36
|
-
import { auxiliaryCoverageGap, bindingStateLabel, composeBoundToCurrentDisk, createDiskBindingCache, hashDiagnosticContent, resolveTouchVerdict, touchCoverageGap, } from "./diagnostic-binding.js";
|
|
37
|
+
import { auxiliaryCoverageGap, auxiliaryPublicationEvidence, bindingStateLabel, composeBoundToCurrentDisk, createDiskBindingCache, hashDiagnosticContent, resolveTouchVerdict, touchCoverageGap, } from "./diagnostic-binding.js";
|
|
37
38
|
import { getServersForFileWithConfig, getServerInitOverride, } from "./config.js";
|
|
38
39
|
// #2052: deliberately NOT taken from `config.js`. This module's import surface
|
|
39
40
|
// from `config.js` is mirrored by explicit `vi.mock` factories in ~58 test
|
|
@@ -41,9 +42,14 @@ import { getServersForFileWithConfig, getServerInitOverride, } from "./config.js
|
|
|
41
42
|
import { getSessionRootsForTelemetry, isOutsideAllSessionRoots, } from "./session-roots.js";
|
|
42
43
|
import { getProcessSingleton } from "../process-singletons.js";
|
|
43
44
|
import { getLanguageId } from "./language.js";
|
|
44
|
-
import {
|
|
45
|
+
import { getReverseDepsFromIndex, loadReverseDependencyIndexFromSnapshot, } from "../reverse-deps.js";
|
|
46
|
+
import { LSP_SERVERS, enforceLspRootCeiling, hasProjectBoundaryMarker, isDirectLspCommandTemporarilyUnavailable, resetClassicTsRepairGuard, resetLspLaunchAvailabilityGeneration, resolveLspServerCwd, } from "./server.js";
|
|
45
47
|
import { classifyCascadeWaitTier, classifyServerWaitTier, getStrategy, } from "./wait-policy/index.js";
|
|
46
48
|
const WORKSPACE_ATTRIBUTION_CLIENT_CAP = 16;
|
|
49
|
+
const AUX_WAIT_DEMOTION_THRESHOLD = 5;
|
|
50
|
+
const AUX_WAIT_DEMOTION_RATIO = 0.9;
|
|
51
|
+
const AUX_WAIT_REPROMOTION_THRESHOLD = 5;
|
|
52
|
+
const AUX_WAIT_REPROMOTION_RATIO = 0.5;
|
|
47
53
|
import { raceToCompletion } from "./aggregation.js";
|
|
48
54
|
import { applyWorkspaceEdit, mergeWorkspaceTextEditsByPriority, summarizeWorkspaceEdit, validateWorkspaceEdit, } from "./edits.js";
|
|
49
55
|
import { buildScopeKey, createWorkspaceDiagnosticsCacheContext, } from "./workspace-diagnostics-cache.js";
|
|
@@ -175,6 +181,7 @@ const NAV_CLIENT_WAIT_TIMEOUT_MS = Math.max(0, Number.parseInt(process.env.PI_LE
|
|
|
175
181
|
1500);
|
|
176
182
|
const TOUCH_DEBOUNCE_MS = Math.max(0, Number.parseInt(process.env.PI_LENS_LSP_TOUCH_DEBOUNCE_MS ?? "1500", 10) ||
|
|
177
183
|
1500);
|
|
184
|
+
const SCOPED_DEPENDENCY_TOUCH_MAX = 32;
|
|
178
185
|
// #1621: the rename-propagation notifies (`didClose` ahead of the rename,
|
|
179
186
|
// `workspace/didRenameFiles` after) share the exit-notify defect #1620 fixed —
|
|
180
187
|
// a notify write on a pipe that is not draining neither resolves nor rejects,
|
|
@@ -344,6 +351,9 @@ function mergeLspDiagnostics(diagnostics) {
|
|
|
344
351
|
}
|
|
345
352
|
return merged;
|
|
346
353
|
}
|
|
354
|
+
function attributeDiagnostics(serverId, diagnostics) {
|
|
355
|
+
return diagnostics.map((diagnostic) => ({ ...diagnostic, serverId }));
|
|
356
|
+
}
|
|
347
357
|
/**
|
|
348
358
|
* Group files by their primary language server id (#387/#631 — extracted
|
|
349
359
|
* from `runWorkspaceDiagnostics`'s inline grouping so other callers, e.g.
|
|
@@ -646,6 +656,17 @@ export class LSPService {
|
|
|
646
656
|
/** Server/root pairs that already emitted unavailable for the current occurrence. */
|
|
647
657
|
unavailableLogged = new Set();
|
|
648
658
|
optionalDisabled = new Set();
|
|
659
|
+
/**
|
|
660
|
+
* One first-contact diagnostic wait per custom server in this service
|
|
661
|
+
* generation. Concurrent touches share the probe; a hook cutoff never arms
|
|
662
|
+
* the navigation-only latch, so the next touch can try again.
|
|
663
|
+
*/
|
|
664
|
+
firstContactWaits = new Map();
|
|
665
|
+
/** Session-scoped adaptive demotion for budget-hitting auxiliaries. */
|
|
666
|
+
auxWaitPressureStreak = new Map();
|
|
667
|
+
auxWaitFastAnswerStreak = new Map();
|
|
668
|
+
demotedAuxiliaryServerIds = new Set();
|
|
669
|
+
demotedAuxiliaryBudgets = new Map();
|
|
649
670
|
/**
|
|
650
671
|
* #1934 review F1: what the last COMPLETED `spawnClient` call for a
|
|
651
672
|
* (server, root) key decided, written by that call at every point it
|
|
@@ -833,6 +854,8 @@ export class LSPService {
|
|
|
833
854
|
clientSpawnGate = Promise.resolve();
|
|
834
855
|
/** True after shutdown() has been called; blocks new operations */
|
|
835
856
|
isDestroyed = false;
|
|
857
|
+
/** Aborts bounded in-flight service work when this generation is retired. */
|
|
858
|
+
shutdownController = new AbortController();
|
|
836
859
|
/**
|
|
837
860
|
* #850: teardown completion for every singleton generation retired before
|
|
838
861
|
* this service was published. Only replacement services receive one; direct
|
|
@@ -852,15 +875,65 @@ export class LSPService {
|
|
|
852
875
|
clientSpawnedAt: new Map(),
|
|
853
876
|
demonstratedReady: new Set(),
|
|
854
877
|
demonstratedCold: new Set(),
|
|
878
|
+
diagnosticsPublished: new Set(),
|
|
879
|
+
diagnosticsUnsupported: new Set(),
|
|
855
880
|
};
|
|
856
881
|
}
|
|
882
|
+
isAuxiliaryWaitDemoted(serverId, root) {
|
|
883
|
+
return this.demotedAuxiliaryServerIds.has(`${serverId}:${normalizeMapKey(root ?? "")}`);
|
|
884
|
+
}
|
|
885
|
+
noteAuxiliaryWait(key, budgetMs, elapsedMs) {
|
|
886
|
+
if (this.demotedAuxiliaryServerIds.has(key))
|
|
887
|
+
return;
|
|
888
|
+
if (elapsedMs < AUX_WAIT_DEMOTION_RATIO * budgetMs) {
|
|
889
|
+
this.auxWaitPressureStreak.delete(key);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const streak = (this.auxWaitPressureStreak.get(key) ?? 0) + 1;
|
|
893
|
+
if (streak >= AUX_WAIT_DEMOTION_THRESHOLD) {
|
|
894
|
+
this.demotedAuxiliaryServerIds.add(key);
|
|
895
|
+
this.demotedAuxiliaryBudgets.set(key, budgetMs);
|
|
896
|
+
this.auxWaitPressureStreak.delete(key);
|
|
897
|
+
this.auxWaitFastAnswerStreak.delete(key);
|
|
898
|
+
recordDegradationOnce({
|
|
899
|
+
kind: "aux_wait_demoted",
|
|
900
|
+
subject: key,
|
|
901
|
+
reason: "auxiliary reached at least 90% of its declared wait budget for five consecutive dispatches",
|
|
902
|
+
});
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
this.auxWaitPressureStreak.set(key, streak);
|
|
906
|
+
}
|
|
907
|
+
noteAuxiliaryLateAnswer(key, elapsedMs) {
|
|
908
|
+
if (!this.demotedAuxiliaryServerIds.has(key))
|
|
909
|
+
return;
|
|
910
|
+
const budgetMs = this.demotedAuxiliaryBudgets.get(key);
|
|
911
|
+
if (budgetMs === undefined ||
|
|
912
|
+
elapsedMs >= AUX_WAIT_REPROMOTION_RATIO * budgetMs) {
|
|
913
|
+
this.auxWaitFastAnswerStreak.delete(key);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const streak = (this.auxWaitFastAnswerStreak.get(key) ?? 0) + 1;
|
|
917
|
+
if (streak < AUX_WAIT_REPROMOTION_THRESHOLD) {
|
|
918
|
+
this.auxWaitFastAnswerStreak.set(key, streak);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
this.demotedAuxiliaryServerIds.delete(key);
|
|
922
|
+
this.demotedAuxiliaryBudgets.delete(key);
|
|
923
|
+
this.auxWaitFastAnswerStreak.delete(key);
|
|
924
|
+
recordDegradationOnce({
|
|
925
|
+
kind: "aux_wait_repromoted",
|
|
926
|
+
subject: key,
|
|
927
|
+
reason: "auxiliary produced five consecutive late answers below half its declared wait budget",
|
|
928
|
+
});
|
|
929
|
+
}
|
|
857
930
|
/**
|
|
858
931
|
* Resolve one server's client identity root. Root selection is hard-bounded
|
|
859
932
|
* by the session cwd, then config-only nested roots reuse an already-hosted
|
|
860
933
|
* same-server ancestor. Real nested projects retain independent clients.
|
|
861
934
|
*/
|
|
862
|
-
async resolveServerRoot(server, filePath) {
|
|
863
|
-
const candidate = await server.
|
|
935
|
+
async resolveServerRoot(server, filePath, onRootFailure) {
|
|
936
|
+
const candidate = await resolveLspServerCwd(server, filePath, this.sessionCwd ?? process.cwd(), onRootFailure);
|
|
864
937
|
if (!candidate)
|
|
865
938
|
return undefined;
|
|
866
939
|
// #2052: a file outside EVERY initialized session cwd gets no client at
|
|
@@ -1984,11 +2057,14 @@ export class LSPService {
|
|
|
1984
2057
|
*/
|
|
1985
2058
|
const partitionCandidates = async () => {
|
|
1986
2059
|
const resolved = await Promise.all(servers.map(async (server) => {
|
|
2060
|
+
let rootFailureReason;
|
|
1987
2061
|
try {
|
|
1988
2062
|
return {
|
|
1989
2063
|
server,
|
|
1990
|
-
root: await this.resolveServerRootOnce(server, filePath, rootMemo)
|
|
1991
|
-
|
|
2064
|
+
root: await this.resolveServerRootOnce(server, filePath, rootMemo, (reason) => {
|
|
2065
|
+
rootFailureReason = reason;
|
|
2066
|
+
}),
|
|
2067
|
+
reason: rootFailureReason,
|
|
1992
2068
|
};
|
|
1993
2069
|
}
|
|
1994
2070
|
catch (error) {
|
|
@@ -2455,6 +2531,18 @@ export class LSPService {
|
|
|
2455
2531
|
}
|
|
2456
2532
|
return out;
|
|
2457
2533
|
}
|
|
2534
|
+
/** Record a fast answer observed by the turn-end late-results path. */
|
|
2535
|
+
async observeLateAuxiliaryAnswer(filePath, serverId, elapsedMs) {
|
|
2536
|
+
for (const server of getServersForFileWithConfig(filePath)) {
|
|
2537
|
+
if (server.id !== serverId)
|
|
2538
|
+
continue;
|
|
2539
|
+
const root = await this.resolveServerRoot(server, filePath);
|
|
2540
|
+
if (root === undefined)
|
|
2541
|
+
return;
|
|
2542
|
+
this.noteAuxiliaryLateAnswer(`${serverId}:${normalizeMapKey(root)}`, elapsedMs);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2458
2546
|
/**
|
|
2459
2547
|
* #1668: deliver a `workspace/didChangeWatchedFiles` event for a disk
|
|
2460
2548
|
* change the client did not author through open-document sync — a bash
|
|
@@ -2481,6 +2569,45 @@ export class LSPService {
|
|
|
2481
2569
|
}
|
|
2482
2570
|
}
|
|
2483
2571
|
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Re-sync the open views affected by one recovered Git tree change. The
|
|
2574
|
+
* recovery seam already owns the changed-path set, so this deliberately
|
|
2575
|
+
* reads only the cached reverse-import index and never runs another diff.
|
|
2576
|
+
*/
|
|
2577
|
+
async resyncGitChangedFiles(changedPaths) {
|
|
2578
|
+
if (this.checkDestroyed() || changedPaths.length === 0)
|
|
2579
|
+
return;
|
|
2580
|
+
const targets = new Set();
|
|
2581
|
+
for (const changedPath of changedPaths) {
|
|
2582
|
+
const resolved = path.resolve(changedPath);
|
|
2583
|
+
for (const server of getServersForFileWithConfig(resolved)) {
|
|
2584
|
+
const root = await this.resolveServerRoot(server, resolved);
|
|
2585
|
+
if (!root)
|
|
2586
|
+
continue;
|
|
2587
|
+
const index = loadReverseDependencyIndexFromSnapshot({ cwd: root });
|
|
2588
|
+
for (const importer of index
|
|
2589
|
+
? getReverseDepsFromIndex(index, resolved)
|
|
2590
|
+
: []) {
|
|
2591
|
+
targets.add(importer);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
targets.add(resolved);
|
|
2595
|
+
}
|
|
2596
|
+
const openTargets = [...targets].filter((filePath) => this.hasLiveClientHoldingDocument(filePath));
|
|
2597
|
+
this.documentDrift.enqueueResync(openTargets);
|
|
2598
|
+
const pass = await this.sweepDocumentDrift({ force: true });
|
|
2599
|
+
logLatency({
|
|
2600
|
+
type: "phase",
|
|
2601
|
+
phase: "lsp_external_change_resync",
|
|
2602
|
+
filePath: changedPaths[0] ?? "",
|
|
2603
|
+
durationMs: 0,
|
|
2604
|
+
metadata: {
|
|
2605
|
+
changedCount: changedPaths.length,
|
|
2606
|
+
enqueued: openTargets.length,
|
|
2607
|
+
deferred: pass?.deferred ?? this.documentDrift.pendingResyncCount,
|
|
2608
|
+
},
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2484
2611
|
/**
|
|
2485
2612
|
* #1783: disk-drift backstop. Stat one batch of the documents a language
|
|
2486
2613
|
* server currently holds, and re-push any whose bytes on disk no longer
|
|
@@ -2669,13 +2796,13 @@ export class LSPService {
|
|
|
2669
2796
|
* instead of each taking their own. Without a `memo` this is a plain
|
|
2670
2797
|
* passthrough, for callers that have no acquisition to share with.
|
|
2671
2798
|
*/
|
|
2672
|
-
resolveServerRootOnce(server, filePath, memo) {
|
|
2799
|
+
resolveServerRootOnce(server, filePath, memo, onRootFailure) {
|
|
2673
2800
|
if (!memo)
|
|
2674
|
-
return this.resolveServerRoot(server, filePath);
|
|
2801
|
+
return this.resolveServerRoot(server, filePath, onRootFailure);
|
|
2675
2802
|
const pending = memo.get(server.id);
|
|
2676
2803
|
if (pending !== undefined)
|
|
2677
2804
|
return pending;
|
|
2678
|
-
const started = this.resolveServerRoot(server, filePath);
|
|
2805
|
+
const started = this.resolveServerRoot(server, filePath, onRootFailure);
|
|
2679
2806
|
memo.set(server.id, started);
|
|
2680
2807
|
return started;
|
|
2681
2808
|
}
|
|
@@ -3117,6 +3244,12 @@ export class LSPService {
|
|
|
3117
3244
|
}
|
|
3118
3245
|
const override = getServerInitOverride(server.id, filePath);
|
|
3119
3246
|
const mergedInit = mergeInitializationOptions(spawned.initialization, override?.initializationOptions);
|
|
3247
|
+
// A replacement process is a new first-contact identity even when the
|
|
3248
|
+
// configured server id is unchanged. Re-arm both capability latches before
|
|
3249
|
+
// the replacement can publish a snapshot.
|
|
3250
|
+
this.state.diagnosticsPublished.delete(server.id);
|
|
3251
|
+
this.state.diagnosticsUnsupported.delete(server.id);
|
|
3252
|
+
this.firstContactWaits.delete(server.id);
|
|
3120
3253
|
const client = await createLSPClient({
|
|
3121
3254
|
serverId: server.id,
|
|
3122
3255
|
process: spawned.process,
|
|
@@ -3125,6 +3258,9 @@ export class LSPService {
|
|
|
3125
3258
|
initialization: mergedInit,
|
|
3126
3259
|
initializeTimeoutMs: server.initializeTimeoutMs,
|
|
3127
3260
|
launchVariant: spawned.launchVariant,
|
|
3261
|
+
onDiagnosticsPublished: (serverId) => {
|
|
3262
|
+
this.state.diagnosticsPublished.add(serverId);
|
|
3263
|
+
},
|
|
3128
3264
|
});
|
|
3129
3265
|
// Guard 2: service was shut down while we were completing the initialize
|
|
3130
3266
|
// handshake. Shut down the live client best-effort and do not register it.
|
|
@@ -3286,6 +3422,11 @@ export class LSPService {
|
|
|
3286
3422
|
return;
|
|
3287
3423
|
}
|
|
3288
3424
|
const startedAt = Date.now();
|
|
3425
|
+
const hookDeadlineAt = options.hook !== undefined &&
|
|
3426
|
+
Object.hasOwn(HOOK_WALL_BUDGET_MS, options.hook)
|
|
3427
|
+
? startedAt +
|
|
3428
|
+
HOOK_WALL_BUDGET_MS[options.hook]
|
|
3429
|
+
: undefined;
|
|
3289
3430
|
const normalizedPath = normalizeMapKey(filePath);
|
|
3290
3431
|
const outsideRoot = await this.findOutsideProjectRoot(filePath);
|
|
3291
3432
|
if (outsideRoot) {
|
|
@@ -3394,6 +3535,19 @@ export class LSPService {
|
|
|
3394
3535
|
if (!leaseKeys) {
|
|
3395
3536
|
// An idle eviction won between selection and lease admission. Resolve the
|
|
3396
3537
|
// now-current client set and replay; no notification targets the retiree.
|
|
3538
|
+
if (this.checkDestroyed()) {
|
|
3539
|
+
const primaryServerIds = spawned
|
|
3540
|
+
.filter((entry) => entry.info.role !== "auxiliary")
|
|
3541
|
+
.map((entry) => entry.info.id);
|
|
3542
|
+
return {
|
|
3543
|
+
diags: [],
|
|
3544
|
+
inconclusive: true,
|
|
3545
|
+
...(primaryServerIds.length > 0 && {
|
|
3546
|
+
inconclusiveServerIds: primaryServerIds,
|
|
3547
|
+
}),
|
|
3548
|
+
inconclusiveReason: "diagnostics-wait",
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3397
3551
|
return this.touchFile(filePath, content, options);
|
|
3398
3552
|
}
|
|
3399
3553
|
try {
|
|
@@ -3475,7 +3629,9 @@ export class LSPService {
|
|
|
3475
3629
|
if (!bindingMatchesTouchContent(binding))
|
|
3476
3630
|
return [];
|
|
3477
3631
|
const diags = entry.client.getDiagnostics(filePath);
|
|
3478
|
-
return diags.length > 0
|
|
3632
|
+
return diags.length > 0
|
|
3633
|
+
? [{ serverId: entry.info.id, diags, binding }]
|
|
3634
|
+
: [];
|
|
3479
3635
|
})
|
|
3480
3636
|
: [];
|
|
3481
3637
|
// #1493: auxiliaries whose STORED publication already covers exactly the
|
|
@@ -3784,6 +3940,15 @@ export class LSPService {
|
|
|
3784
3940
|
// confirmation; `auxCutOffServerIds` stays cut_off-only so the R8 latency
|
|
3785
3941
|
// field keeps its original meaning.
|
|
3786
3942
|
let auxUnconfirmedServerIds;
|
|
3943
|
+
// #2810: the auxiliaries this touch handed to the collect-later store —
|
|
3944
|
+
// exactly the servers whose findings a turn-end drain can still deliver.
|
|
3945
|
+
// Written by the ONE producer that marks them, from the same array it
|
|
3946
|
+
// marks, so the promise the agent is shown cannot drift from the store.
|
|
3947
|
+
// Round 3 derived the promise from the #1459 resync deferrals instead —
|
|
3948
|
+
// the one set `pending-aux-coverage.ts` never marks — so the notice
|
|
3949
|
+
// promised delivery that could not happen, while the demoted server,
|
|
3950
|
+
// which IS marked, was reported as an unexplained silence on every edit.
|
|
3951
|
+
let lateDeliveryServerIds;
|
|
3787
3952
|
// #707: tsserver sync clean-confirm state. `tsserverSyncEligible` is the
|
|
3788
3953
|
// full gate (evaluated once, before the wait); `tsserverSyncConfirmed`
|
|
3789
3954
|
// holds the sync commands' answer when the racing confirm won the wait
|
|
@@ -3791,6 +3956,7 @@ export class LSPService {
|
|
|
3791
3956
|
// fallback below may still fill it in on a timed-out empty result).
|
|
3792
3957
|
let tsserverSyncEligible = false;
|
|
3793
3958
|
let tsserverSyncConfirmed;
|
|
3959
|
+
let diagnosticsUnsupportedServerIds = [];
|
|
3794
3960
|
if (diagnosticsMode !== "none") {
|
|
3795
3961
|
// Resolution: env wins so users can tune the cap without rebuilding.
|
|
3796
3962
|
// Otherwise, on the single-server hot path (primary scope), use that
|
|
@@ -3861,6 +4027,9 @@ export class LSPService {
|
|
|
3861
4027
|
root: entry.client.root,
|
|
3862
4028
|
operationSupport: entry.client.getOperationSupport(),
|
|
3863
4029
|
workspaceDiagnosticsSupport: entry.client.getWorkspaceDiagnosticsSupport(),
|
|
4030
|
+
customServer: entry.info.custom,
|
|
4031
|
+
diagnosticsPublished: this.state.diagnosticsPublished.has(entry.info.id),
|
|
4032
|
+
diagnosticsUnsupported: this.state.diagnosticsUnsupported.has(entry.info.id),
|
|
3864
4033
|
advertisedCommands: entry.client.getAdvertisedCommands(),
|
|
3865
4034
|
rawCapabilityKeys: entry.client.getRawCapabilityKeys?.() ?? [],
|
|
3866
4035
|
launchVariant: entry.client.getLaunchVariant?.(),
|
|
@@ -4004,6 +4173,29 @@ export class LSPService {
|
|
|
4004
4173
|
const hasTouchAuxiliaries = clientScope === "with-auxiliary" &&
|
|
4005
4174
|
options.collectDiagnostics === true &&
|
|
4006
4175
|
spawned.some((e) => e.info.role === "auxiliary");
|
|
4176
|
+
if (spawned.some((entry) => entry.info.custom === true)) {
|
|
4177
|
+
try {
|
|
4178
|
+
const snapshotHook = options.hook ?? "tool_result_edit";
|
|
4179
|
+
const snapshotBudget = hookDeadlineAt === undefined
|
|
4180
|
+
? HOOK_WALL_BUDGET_MS.tool_result_edit
|
|
4181
|
+
: Math.max(0, hookDeadlineAt - Date.now());
|
|
4182
|
+
const snapshots = await bounded(this.getCapabilitySnapshots(filePath), {
|
|
4183
|
+
ms: snapshotBudget,
|
|
4184
|
+
signal: getAmbientAbortSignal(),
|
|
4185
|
+
shutdownSignal: this.shutdownController.signal,
|
|
4186
|
+
hook: snapshotHook,
|
|
4187
|
+
label: `getCapabilitySnapshots:${filePath}`,
|
|
4188
|
+
});
|
|
4189
|
+
if (snapshots !== undefined) {
|
|
4190
|
+
diagnosticsUnsupportedServerIds = spawned
|
|
4191
|
+
.filter((entry) => classifyServerWaitTier(entry.info.id, snapshots.find((s) => s.serverId === entry.info.id)) === "diagnostics-unsupported")
|
|
4192
|
+
.map((entry) => entry.info.id);
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
catch {
|
|
4196
|
+
// Capability uncertainty fails closed and retains the wait.
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4007
4199
|
// Per-server wait promises (each already bounded by its own
|
|
4008
4200
|
// perServerTimeout — unchanged from before R8).
|
|
4009
4201
|
let pressureSnapshots = [];
|
|
@@ -4028,12 +4220,40 @@ export class LSPService {
|
|
|
4028
4220
|
? auxWaitBudgetMs(entry.client.serverId, true, configuredAuxCeilingMs, declaredServerTimeout)
|
|
4029
4221
|
: perServerDeclaredTimeouts[entryIndex];
|
|
4030
4222
|
});
|
|
4223
|
+
const firstContactCutOffServerIds = new Set();
|
|
4224
|
+
const firstContactErroredServerIds = new Set();
|
|
4225
|
+
const answeredForThisTouch = (entry) => {
|
|
4226
|
+
const baseline = diagnosticBaselines.get(entry.client);
|
|
4227
|
+
const currentPathVersion = readPathVersion(entry.client);
|
|
4228
|
+
if (auxiliaryPublicationEvidence({
|
|
4229
|
+
bindingMatchesContent: false,
|
|
4230
|
+
baseline,
|
|
4231
|
+
currentPathVersion,
|
|
4232
|
+
})) {
|
|
4233
|
+
return true;
|
|
4234
|
+
}
|
|
4235
|
+
try {
|
|
4236
|
+
return (entry.client.getAllDiagnostics?.().has(normalizedPath) === true);
|
|
4237
|
+
}
|
|
4238
|
+
catch {
|
|
4239
|
+
return false;
|
|
4240
|
+
}
|
|
4241
|
+
};
|
|
4031
4242
|
const perServerRaceBudgets = spawned.map((entry, entryIndex) => {
|
|
4032
4243
|
return hasTouchAuxiliaries && entry.info.role === "auxiliary"
|
|
4033
4244
|
? auxWaitBudgetMs(entry.client.serverId, coldAuxiliaryServerIds.has(entry.client.serverId), configuredAuxCeilingMs, perServerDeclaredTimeouts[entryIndex])
|
|
4034
4245
|
: perServerDeclaredTimeouts[entryIndex];
|
|
4035
4246
|
});
|
|
4036
4247
|
const perServerWaits = spawned.map((entry, entryIndex) => {
|
|
4248
|
+
if (hasTouchAuxiliaries &&
|
|
4249
|
+
entry.info.role === "auxiliary" &&
|
|
4250
|
+
this.isAuxiliaryWaitDemoted(entry.info.id, entry.client.root ?? filePath)) {
|
|
4251
|
+
return Promise.resolve(undefined);
|
|
4252
|
+
}
|
|
4253
|
+
if (this.state.diagnosticsUnsupported.has(entry.info.id) ||
|
|
4254
|
+
diagnosticsUnsupportedServerIds.includes(entry.info.id)) {
|
|
4255
|
+
return Promise.resolve(undefined);
|
|
4256
|
+
}
|
|
4037
4257
|
// #1459: a DEFERRED server never received this content, so its version
|
|
4038
4258
|
// can never advance past the baseline — waiting on it burns its whole
|
|
4039
4259
|
// budget and would flip the touch to `inconclusive`, discarding a
|
|
@@ -4063,7 +4283,7 @@ export class LSPService {
|
|
|
4063
4283
|
const isWarmupTouch = source === "lsp_sweep_warmup";
|
|
4064
4284
|
// #743: per-server — a server we DID push to still gets the
|
|
4065
4285
|
// version-baseline wait even when a sibling was debounced away.
|
|
4066
|
-
const
|
|
4286
|
+
const waitForDiagnostics = () => !notifySkippedServerIds.has(entry.info.id) &&
|
|
4067
4287
|
Number.isFinite(baseline)
|
|
4068
4288
|
? entry.client.waitForDiagnostics(filePath, serverTimeout, {
|
|
4069
4289
|
minVersion: baseline,
|
|
@@ -4080,7 +4300,73 @@ export class LSPService {
|
|
|
4080
4300
|
pullSettleSource: "pull-warmup",
|
|
4081
4301
|
})
|
|
4082
4302
|
: entry.client.waitForDiagnostics(filePath, serverTimeout);
|
|
4083
|
-
|
|
4303
|
+
const firstContact = entry.info.custom === true &&
|
|
4304
|
+
!this.state.diagnosticsPublished.has(entry.info.id) &&
|
|
4305
|
+
!this.state.diagnosticsUnsupported.has(entry.info.id) &&
|
|
4306
|
+
entry.client.getWorkspaceDiagnosticsSupport().mode !== "pull";
|
|
4307
|
+
if (!firstContact)
|
|
4308
|
+
return waitForDiagnostics().catch(() => undefined);
|
|
4309
|
+
let shared = this.firstContactWaits.get(entry.info.id);
|
|
4310
|
+
if (!shared) {
|
|
4311
|
+
let removeShutdownListener;
|
|
4312
|
+
const shutdown = new Promise((resolve) => {
|
|
4313
|
+
const onShutdown = () => resolve("shutdown");
|
|
4314
|
+
if (this.shutdownController.signal.aborted) {
|
|
4315
|
+
onShutdown();
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
this.shutdownController.signal.addEventListener("abort", onShutdown, {
|
|
4319
|
+
once: true,
|
|
4320
|
+
});
|
|
4321
|
+
removeShutdownListener = () => this.shutdownController.signal.removeEventListener("abort", onShutdown);
|
|
4322
|
+
});
|
|
4323
|
+
const probe = Promise.race([
|
|
4324
|
+
waitForDiagnostics().then(() => "completed", () => "errored"),
|
|
4325
|
+
shutdown,
|
|
4326
|
+
]);
|
|
4327
|
+
const completed = withDeadline(probe, {
|
|
4328
|
+
ms: serverTimeout,
|
|
4329
|
+
onTimeout: "undefined",
|
|
4330
|
+
});
|
|
4331
|
+
shared = completed
|
|
4332
|
+
.then((outcome) => {
|
|
4333
|
+
if (outcome === "errored" || outcome === "shutdown") {
|
|
4334
|
+
return "errored";
|
|
4335
|
+
}
|
|
4336
|
+
if (answeredForThisTouch(entry))
|
|
4337
|
+
return "answered";
|
|
4338
|
+
this.state.diagnosticsUnsupported.add(entry.info.id);
|
|
4339
|
+
recordDegradationOnce({
|
|
4340
|
+
kind: "lsp-diagnostics-unsupported",
|
|
4341
|
+
subject: entry.info.id,
|
|
4342
|
+
reason: "custom server has no pull provider and no publish evidence after first contact wait",
|
|
4343
|
+
});
|
|
4344
|
+
return "silent";
|
|
4345
|
+
})
|
|
4346
|
+
.catch(() => "errored")
|
|
4347
|
+
.finally(() => {
|
|
4348
|
+
removeShutdownListener?.();
|
|
4349
|
+
if (this.firstContactWaits.get(entry.info.id) === shared) {
|
|
4350
|
+
this.firstContactWaits.delete(entry.info.id);
|
|
4351
|
+
}
|
|
4352
|
+
});
|
|
4353
|
+
this.firstContactWaits.set(entry.info.id, shared);
|
|
4354
|
+
}
|
|
4355
|
+
const callerWait = hookDeadlineAt === undefined
|
|
4356
|
+
? shared
|
|
4357
|
+
: withDeadline(shared, {
|
|
4358
|
+
ms: Math.max(0, hookDeadlineAt - Date.now()),
|
|
4359
|
+
onTimeout: "undefined",
|
|
4360
|
+
});
|
|
4361
|
+
return callerWait.then((outcome) => {
|
|
4362
|
+
if (outcome === undefined) {
|
|
4363
|
+
firstContactCutOffServerIds.add(entry.info.id);
|
|
4364
|
+
}
|
|
4365
|
+
else if (outcome === "errored") {
|
|
4366
|
+
firstContactErroredServerIds.add(entry.info.id);
|
|
4367
|
+
}
|
|
4368
|
+
return undefined;
|
|
4369
|
+
});
|
|
4084
4370
|
});
|
|
4085
4371
|
// The push wait — same per-server budget composition as before #707;
|
|
4086
4372
|
// only the awaiting changed (assigned so it can be raced below).
|
|
@@ -4102,6 +4388,8 @@ export class LSPService {
|
|
|
4102
4388
|
client: spawned[i].client,
|
|
4103
4389
|
baseline: diagnosticBaselines.get(spawned[i].client),
|
|
4104
4390
|
budgetMs: perServerRaceBudgets[i],
|
|
4391
|
+
demoted: this.isAuxiliaryWaitDemoted(spawned[i].info.id, spawned[i].client.root ?? filePath),
|
|
4392
|
+
root: spawned[i].client.root ?? filePath,
|
|
4105
4393
|
}
|
|
4106
4394
|
: null)
|
|
4107
4395
|
.filter((x) => x !== null);
|
|
@@ -4121,6 +4409,22 @@ export class LSPService {
|
|
|
4121
4409
|
const auxWaitStartedAt = Date.now();
|
|
4122
4410
|
const outcomes = await Promise.all(auxWaits.map(async (aux) => {
|
|
4123
4411
|
const { budgetMs } = aux;
|
|
4412
|
+
if (aux.demoted) {
|
|
4413
|
+
return {
|
|
4414
|
+
serverId: aux.serverId,
|
|
4415
|
+
outcome: deferredResyncServerIds.has(aux.serverId)
|
|
4416
|
+
? "deferred"
|
|
4417
|
+
: "demoted",
|
|
4418
|
+
publishedThisContent: auxiliaryPublicationEvidence({
|
|
4419
|
+
bindingMatchesContent: auxCoversThisContent(aux.serverId),
|
|
4420
|
+
baseline: aux.baseline,
|
|
4421
|
+
currentPathVersion: readPathVersion(aux.client),
|
|
4422
|
+
}),
|
|
4423
|
+
budgetMs,
|
|
4424
|
+
elapsedMs: 0,
|
|
4425
|
+
elapsedSinceNotifyMs: 0,
|
|
4426
|
+
};
|
|
4427
|
+
}
|
|
4124
4428
|
let timer;
|
|
4125
4429
|
const timeout = new Promise((resolve) => {
|
|
4126
4430
|
timer = setTimeout(() => resolve(false), budgetMs);
|
|
@@ -4159,10 +4463,12 @@ export class LSPService {
|
|
|
4159
4463
|
// ignoring sibling paths — and it is the SAME axis `baseline`
|
|
4160
4464
|
// was captured on above.
|
|
4161
4465
|
const currentPathVersion = readPathVersion(aux.client);
|
|
4162
|
-
const publishedEvidence =
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
currentPathVersion
|
|
4466
|
+
const publishedEvidence = auxiliaryPublicationEvidence({
|
|
4467
|
+
bindingMatchesContent: false,
|
|
4468
|
+
baseline: aux.baseline,
|
|
4469
|
+
currentPathVersion,
|
|
4470
|
+
raced,
|
|
4471
|
+
});
|
|
4166
4472
|
// #1459: a DEFERRED aux was never sent this content and is not
|
|
4167
4473
|
// waited on at all, so its instantly-resolved placeholder
|
|
4168
4474
|
// promise must not read as "silent". "Silent" is the reserved
|
|
@@ -4176,6 +4482,10 @@ export class LSPService {
|
|
|
4176
4482
|
: publishedEvidence
|
|
4177
4483
|
? "answered"
|
|
4178
4484
|
: "silent";
|
|
4485
|
+
const elapsedMs = Date.now() - auxWaitStartedAt;
|
|
4486
|
+
if (outcome !== "deferred") {
|
|
4487
|
+
this.noteAuxiliaryWait(`${aux.serverId}:${normalizeMapKey(aux.root)}`, budgetMs, elapsedMs);
|
|
4488
|
+
}
|
|
4179
4489
|
return {
|
|
4180
4490
|
serverId: aux.serverId,
|
|
4181
4491
|
outcome,
|
|
@@ -4185,9 +4495,12 @@ export class LSPService {
|
|
|
4185
4495
|
// did not narrow the touch.
|
|
4186
4496
|
// #1586: through the one predicate, so this row and the merge
|
|
4187
4497
|
// below cannot disagree about the same scanner.
|
|
4498
|
+
// #2914: binding-only by construction — the direct call
|
|
4499
|
+
// below is master's line; the stamp axis already decided
|
|
4500
|
+
// the outcome above and must not re-admit the row here.
|
|
4188
4501
|
publishedThisContent: auxCoversThisContent(aux.serverId),
|
|
4189
4502
|
budgetMs,
|
|
4190
|
-
elapsedMs
|
|
4503
|
+
elapsedMs,
|
|
4191
4504
|
// #1458 S3: elapsed measured from BEFORE the primary wait
|
|
4192
4505
|
// (waitStartedAt), not just from auxWaitStartedAt — this is
|
|
4193
4506
|
// what lets a latency row validate the ~1.3s warm-scanner
|
|
@@ -4220,7 +4533,9 @@ export class LSPService {
|
|
|
4220
4533
|
// revision and probing it would replay stale findings.
|
|
4221
4534
|
const collectLaterServerIds = outcomes
|
|
4222
4535
|
.filter((o) => !o.publishedThisContent &&
|
|
4223
|
-
(o.outcome === "cut_off" ||
|
|
4536
|
+
(o.outcome === "cut_off" ||
|
|
4537
|
+
o.outcome === "silent" ||
|
|
4538
|
+
o.outcome === "demoted") &&
|
|
4224
4539
|
// #2324 R2-A/R3-A: ast-grep's napi fallback is a
|
|
4225
4540
|
// SECOND producer of coverage for this exact pair,
|
|
4226
4541
|
// dispatched CONCURRENTLY with this whole touch
|
|
@@ -4240,7 +4555,8 @@ export class LSPService {
|
|
|
4240
4555
|
napiFallbackCoveredSince(filePath, startedAt)))
|
|
4241
4556
|
.map((o) => o.serverId);
|
|
4242
4557
|
if (collectLaterServerIds.length > 0) {
|
|
4243
|
-
|
|
4558
|
+
lateDeliveryServerIds = collectLaterServerIds;
|
|
4559
|
+
markPendingAuxiliaryCoverage(filePath, collectLaterServerIds, Date.now());
|
|
4244
4560
|
}
|
|
4245
4561
|
logLatency({
|
|
4246
4562
|
type: "phase",
|
|
@@ -4257,9 +4573,14 @@ export class LSPService {
|
|
|
4257
4573
|
});
|
|
4258
4574
|
})()
|
|
4259
4575
|
: Promise.all(perServerWaits).then(() => { });
|
|
4260
|
-
pushWait
|
|
4576
|
+
// Mark on BOTH paths: a rejected `pushWait` is settled too, and a
|
|
4577
|
+
// resolve-only `.then` left a derived promise that rejected unhandled
|
|
4578
|
+
// whenever a per-server wait failed (oxlint no-floating-promises,
|
|
4579
|
+
// 2026-09-07). The rejection itself still reaches the awaiters below.
|
|
4580
|
+
const markPushWaitSettled = () => {
|
|
4261
4581
|
pushWaitSettled = true;
|
|
4262
|
-
}
|
|
4582
|
+
};
|
|
4583
|
+
void pushWait.then(markPushWaitSettled, markPushWaitSettled);
|
|
4263
4584
|
if (tsserverSyncEligible) {
|
|
4264
4585
|
// #707 racing variant: rather than burning the full push-wait budget
|
|
4265
4586
|
// on a silent-on-clean server (which by definition never answers on a
|
|
@@ -4331,6 +4652,35 @@ export class LSPService {
|
|
|
4331
4652
|
await pushWait;
|
|
4332
4653
|
}
|
|
4333
4654
|
const waitedMs = Date.now() - waitStartedAt;
|
|
4655
|
+
const firstContactCustomServerIds = spawned
|
|
4656
|
+
.filter((entry) => entry.info.custom === true &&
|
|
4657
|
+
!this.state.diagnosticsPublished.has(entry.info.id) &&
|
|
4658
|
+
!this.state.diagnosticsUnsupported.has(entry.info.id) &&
|
|
4659
|
+
!firstContactCutOffServerIds.has(entry.info.id) &&
|
|
4660
|
+
!firstContactErroredServerIds.has(entry.info.id) &&
|
|
4661
|
+
entry.client.getWorkspaceDiagnosticsSupport().mode !== "pull")
|
|
4662
|
+
.map((entry) => entry.info.id);
|
|
4663
|
+
for (const serverId of firstContactCustomServerIds) {
|
|
4664
|
+
const entry = spawned.find((candidate) => candidate.info.id === serverId);
|
|
4665
|
+
if (!entry || answeredForThisTouch(entry))
|
|
4666
|
+
continue;
|
|
4667
|
+
this.state.diagnosticsUnsupported.add(serverId);
|
|
4668
|
+
diagnosticsUnsupportedServerIds.push(serverId);
|
|
4669
|
+
recordDegradationOnce({
|
|
4670
|
+
kind: "lsp-diagnostics-unsupported",
|
|
4671
|
+
subject: serverId,
|
|
4672
|
+
reason: "custom server has no pull provider and no publish evidence after first contact wait",
|
|
4673
|
+
});
|
|
4674
|
+
}
|
|
4675
|
+
for (const entry of spawned) {
|
|
4676
|
+
if (entry.info.custom === true &&
|
|
4677
|
+
this.state.diagnosticsUnsupported.has(entry.info.id)) {
|
|
4678
|
+
diagnosticsUnsupportedServerIds.push(entry.info.id);
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
diagnosticsUnsupportedServerIds = [
|
|
4682
|
+
...new Set(diagnosticsUnsupportedServerIds),
|
|
4683
|
+
];
|
|
4334
4684
|
// #1533: the same auxiliary coverage evidence for a collecting touch that
|
|
4335
4685
|
// did NOT enter the aux-grace wait — in practice `clientScope: "all"`, the
|
|
4336
4686
|
// batch/directory scan surface. Auxiliaries ARE spawned on that scope
|
|
@@ -4406,9 +4756,11 @@ export class LSPService {
|
|
|
4406
4756
|
const outcomes = auxEntries.map((entry) => {
|
|
4407
4757
|
const baseline = diagnosticBaselines.get(entry.client);
|
|
4408
4758
|
const currentPathVersion = readPathVersion(entry.client);
|
|
4409
|
-
const publishedEvidence =
|
|
4410
|
-
|
|
4411
|
-
|
|
4759
|
+
const publishedEvidence = auxiliaryPublicationEvidence({
|
|
4760
|
+
bindingMatchesContent: false,
|
|
4761
|
+
baseline,
|
|
4762
|
+
currentPathVersion,
|
|
4763
|
+
});
|
|
4412
4764
|
return {
|
|
4413
4765
|
serverId: entry.info.id,
|
|
4414
4766
|
outcome: deferredResyncServerIds.has(entry.info.id)
|
|
@@ -4416,6 +4768,9 @@ export class LSPService {
|
|
|
4416
4768
|
: publishedEvidence
|
|
4417
4769
|
? "answered"
|
|
4418
4770
|
: "silent",
|
|
4771
|
+
// #2914: binding-only by construction — the direct call
|
|
4772
|
+
// below is master's line; the stamp axis already decided
|
|
4773
|
+
// the outcome above and must not re-admit the row here.
|
|
4419
4774
|
publishedThisContent: auxCoversThisContent(entry.info.id),
|
|
4420
4775
|
budgetMs: timeoutFor(entry.client.serverId),
|
|
4421
4776
|
elapsedMs: waitedMs,
|
|
@@ -4454,7 +4809,8 @@ export class LSPService {
|
|
|
4454
4809
|
},
|
|
4455
4810
|
});
|
|
4456
4811
|
}
|
|
4457
|
-
else if (waitedMs + 20 >= timeoutMs
|
|
4812
|
+
else if (waitedMs + 20 >= timeoutMs ||
|
|
4813
|
+
firstContactErroredServerIds.size > 0) {
|
|
4458
4814
|
// Within ~20 ms of the configured budget we treat it as a timeout;
|
|
4459
4815
|
// the LSP didn't beat the cap. Diagnostics that arrive late still
|
|
4460
4816
|
// land in the client's cache and surface on the next edit.
|
|
@@ -4479,25 +4835,10 @@ export class LSPService {
|
|
|
4479
4835
|
// reads as unanswered, which for a primary is exactly the pre-#1549
|
|
4480
4836
|
// verdict. This block can therefore only ever NARROW an inconclusive
|
|
4481
4837
|
// touch, never create one.
|
|
4482
|
-
const answeredForThisTouch = (entry) => {
|
|
4483
|
-
const baseline = diagnosticBaselines.get(entry.client);
|
|
4484
|
-
const currentPathVersion = readPathVersion(entry.client);
|
|
4485
|
-
if (Number.isFinite(baseline) &&
|
|
4486
|
-
currentPathVersion !== undefined &&
|
|
4487
|
-
currentPathVersion > baseline) {
|
|
4488
|
-
return true;
|
|
4489
|
-
}
|
|
4490
|
-
try {
|
|
4491
|
-
return (entry.client.getAllDiagnostics?.().has(normalizedPath) === true);
|
|
4492
|
-
}
|
|
4493
|
-
catch {
|
|
4494
|
-
// Fail closed: an unreadable cache is not evidence of an answer.
|
|
4495
|
-
return false;
|
|
4496
|
-
}
|
|
4497
|
-
};
|
|
4498
4838
|
// A deferred server was never sent this content and is not waited on, so
|
|
4499
4839
|
// it cannot have "timed out" — it is already reported as a coverage gap.
|
|
4500
|
-
const waited = spawned.filter((entry) => !deferredResyncServerIds.has(entry.info.id)
|
|
4840
|
+
const waited = spawned.filter((entry) => !deferredResyncServerIds.has(entry.info.id) &&
|
|
4841
|
+
!diagnosticsUnsupportedServerIds.includes(entry.info.id));
|
|
4501
4842
|
const unanswered = waited.filter((entry) => !answeredForThisTouch(entry));
|
|
4502
4843
|
diagnosticsUnansweredServerIds = unanswered.map((e) => e.info.id);
|
|
4503
4844
|
diagnosticsUnansweredPrimaryServerIds = unanswered
|
|
@@ -4507,8 +4848,9 @@ export class LSPService {
|
|
|
4507
4848
|
// preserve, so it keeps the pre-#1549 touch-wide verdict rather than
|
|
4508
4849
|
// absolving itself on an auxiliary's evidence.
|
|
4509
4850
|
const hasWaitedPrimary = waited.some((entry) => entry.info.role !== "auxiliary");
|
|
4851
|
+
const hasUnsupportedPrimary = diagnosticsUnsupportedServerIds.some((serverId) => spawned.some((entry) => entry.info.id === serverId && entry.info.role !== "auxiliary"));
|
|
4510
4852
|
diagnosticsTimedOut =
|
|
4511
|
-
!hasWaitedPrimary ||
|
|
4853
|
+
(!hasWaitedPrimary && !hasUnsupportedPrimary) ||
|
|
4512
4854
|
diagnosticsUnansweredPrimaryServerIds.length > 0;
|
|
4513
4855
|
for (const entry of unanswered) {
|
|
4514
4856
|
incrementDegradationCount({
|
|
@@ -4693,7 +5035,7 @@ export class LSPService {
|
|
|
4693
5035
|
// diagnostics from the client cache as always.
|
|
4694
5036
|
let collected = options.collectDiagnostics
|
|
4695
5037
|
? tsserverSyncConfirmed !== undefined
|
|
4696
|
-
? mergeLspDiagnostics(tsserverSyncConfirmed)
|
|
5038
|
+
? mergeLspDiagnostics(attributeDiagnostics(spawned[0]?.info.id ?? "unknown", tsserverSyncConfirmed))
|
|
4697
5039
|
: mergeLspDiagnostics([
|
|
4698
5040
|
// #1459: a DEFERRED server's cache still holds the PREVIOUS
|
|
4699
5041
|
// content's findings — the resync that would have cleared it never
|
|
@@ -4705,8 +5047,8 @@ export class LSPService {
|
|
|
4705
5047
|
// coverage naming and the merged binding read too.
|
|
4706
5048
|
...spawned.flatMap((entry) => droppedAuxiliaryServerIds.has(entry.info.id)
|
|
4707
5049
|
? []
|
|
4708
|
-
: entry.client.getDiagnostics(filePath)),
|
|
4709
|
-
...carriedAuxiliary.flatMap((entry) => entry.diags),
|
|
5050
|
+
: attributeDiagnostics(entry.info.id, entry.client.getDiagnostics(filePath))),
|
|
5051
|
+
...carriedAuxiliary.flatMap((entry) => attributeDiagnostics(entry.serverId, entry.diags)),
|
|
4710
5052
|
])
|
|
4711
5053
|
: undefined;
|
|
4712
5054
|
// #1095 (P3-b): whether `collected` came from a tsserver sync confirm
|
|
@@ -4746,7 +5088,9 @@ export class LSPService {
|
|
|
4746
5088
|
retractPrimaryTimeoutAttribution(); // #1549
|
|
4747
5089
|
syncConfirmed = true;
|
|
4748
5090
|
collected =
|
|
4749
|
-
syncResult.length > 0
|
|
5091
|
+
syncResult.length > 0
|
|
5092
|
+
? mergeLspDiagnostics(attributeDiagnostics(spawned[0]?.info.id ?? "unknown", syncResult))
|
|
5093
|
+
: [];
|
|
4750
5094
|
logLatency({
|
|
4751
5095
|
type: "phase",
|
|
4752
5096
|
phase: "lsp_tsserver_sync_confirm",
|
|
@@ -5061,6 +5405,11 @@ export class LSPService {
|
|
|
5061
5405
|
// collect, `binding` only for a collecting touch — a non-collecting touch
|
|
5062
5406
|
// keeps resolving `{ diags: [] }`, no flags.
|
|
5063
5407
|
const result = { diags: collected ?? [] };
|
|
5408
|
+
const primaryDiagnosticsUnsupported = diagnosticsUnsupportedServerIds.some((serverId) => spawned.some((entry) => entry.info.id === serverId && entry.info.role !== "auxiliary"));
|
|
5409
|
+
if (diagnosticsUnsupportedServerIds.length > 0) {
|
|
5410
|
+
result.diagnosticsUnsupportedServerIds =
|
|
5411
|
+
diagnosticsUnsupportedServerIds;
|
|
5412
|
+
}
|
|
5064
5413
|
if (collected !== undefined && inconclusive) {
|
|
5065
5414
|
result.inconclusive = true;
|
|
5066
5415
|
// #1549: name the primary that produced the verdict and which deadline it
|
|
@@ -5073,6 +5422,13 @@ export class LSPService {
|
|
|
5073
5422
|
}
|
|
5074
5423
|
result.inconclusiveReason = verdict.inconclusiveReason;
|
|
5075
5424
|
}
|
|
5425
|
+
else if (collected !== undefined && primaryDiagnosticsUnsupported) {
|
|
5426
|
+
// A navigation-only primary has no diagnostic confirmation to report,
|
|
5427
|
+
// but an uncovered auxiliary still needs to be named for delivery.
|
|
5428
|
+
if (unconfirmedServerIds.length > 0) {
|
|
5429
|
+
result.unconfirmedServerIds = [...unconfirmedServerIds];
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5076
5432
|
else if (collected !== undefined && coverageGap) {
|
|
5077
5433
|
// #1470/#1493: narrowed, not collapsed. Reached for EITHER no-answer
|
|
5078
5434
|
// shape — a cut-off auxiliary or a silent one with nothing published for
|
|
@@ -5083,7 +5439,7 @@ export class LSPService {
|
|
|
5083
5439
|
result.confirmation = "partial";
|
|
5084
5440
|
result.unconfirmedServerIds = [...unconfirmedServerIds];
|
|
5085
5441
|
}
|
|
5086
|
-
else if (collected !== undefined) {
|
|
5442
|
+
else if (collected !== undefined && !primaryDiagnosticsUnsupported) {
|
|
5087
5443
|
// Preserve the lower-level affirmative result across consumers. In
|
|
5088
5444
|
// particular, the silent-clean gates above clear diagnosticsTimedOut only
|
|
5089
5445
|
// after a successful notify and capability-confirmed wait; reclassifying
|
|
@@ -5129,6 +5485,17 @@ export class LSPService {
|
|
|
5129
5485
|
// fully delivered. Nothing to record here: a skipped server keeps its original
|
|
5130
5486
|
// entry (and timestamp) so its window still expires naturally instead of being
|
|
5131
5487
|
// extended by every reuse.
|
|
5488
|
+
// #2810: the half of the coverage gap that has a delivery path. Derived
|
|
5489
|
+
// from `unconfirmedServerIds` so the two can never disagree about the
|
|
5490
|
+
// same scanner, and intersected with the marked set so "findings will
|
|
5491
|
+
// arrive through the late path" is said only where a pending pair
|
|
5492
|
+
// exists. Everything else in the gap — a #1459 deferral, a breaker
|
|
5493
|
+
// skip, an ast-grep already covered by its napi fallback — keeps the
|
|
5494
|
+
// honest "silent, diagnostics are incomplete" half.
|
|
5495
|
+
const lateDeliveryPending = unconfirmedServerIds.filter((serverId) => (lateDeliveryServerIds ?? []).includes(serverId));
|
|
5496
|
+
if (lateDeliveryPending.length > 0) {
|
|
5497
|
+
result.deferredServerIds = lateDeliveryPending;
|
|
5498
|
+
}
|
|
5132
5499
|
logLatency({
|
|
5133
5500
|
type: "phase",
|
|
5134
5501
|
phase: "lsp_touch_file",
|
|
@@ -5201,6 +5568,14 @@ export class LSPService {
|
|
|
5201
5568
|
...(uncoveredDeferredServerIds.length > 0 && {
|
|
5202
5569
|
deferredResyncServerIds: uncoveredDeferredServerIds,
|
|
5203
5570
|
}),
|
|
5571
|
+
// #2810: the scanners this touch promised late delivery for, so a
|
|
5572
|
+
// field query can join the promise to the pending pair that has to
|
|
5573
|
+
// honour it — and tell it apart from `deferredResyncServerIds`
|
|
5574
|
+
// above, which is the door that opened BEFORE any wait and has no
|
|
5575
|
+
// delivery path at all.
|
|
5576
|
+
...(lateDeliveryPending.length > 0 && {
|
|
5577
|
+
lateDeliveryServerIds: lateDeliveryPending,
|
|
5578
|
+
}),
|
|
5204
5579
|
// #1549: auxiliaries whose own deadline lapsed — the wait produced no
|
|
5205
5580
|
// publication, or the notify write never landed. Distinct from the fields
|
|
5206
5581
|
// above (a different door) and from `auxUnconfirmedServerIds` (derived
|
|
@@ -5437,7 +5812,7 @@ export class LSPService {
|
|
|
5437
5812
|
if (seen.has(key))
|
|
5438
5813
|
continue;
|
|
5439
5814
|
seen.add(key);
|
|
5440
|
-
merged.push(diagnostic);
|
|
5815
|
+
merged.push({ ...diagnostic, serverId: entry.serverId });
|
|
5441
5816
|
}
|
|
5442
5817
|
}
|
|
5443
5818
|
const rawCount = perServerFull.reduce((sum, entry) => sum + entry.diagnosticCount, 0);
|
|
@@ -5854,6 +6229,9 @@ export class LSPService {
|
|
|
5854
6229
|
root,
|
|
5855
6230
|
operationSupport: client.getOperationSupport(),
|
|
5856
6231
|
workspaceDiagnosticsSupport: client.getWorkspaceDiagnosticsSupport(),
|
|
6232
|
+
customServer: server.custom,
|
|
6233
|
+
diagnosticsPublished: this.state.diagnosticsPublished.has(server.id),
|
|
6234
|
+
diagnosticsUnsupported: this.state.diagnosticsUnsupported.has(server.id),
|
|
5857
6235
|
advertisedCommands: client.getAdvertisedCommands(),
|
|
5858
6236
|
rawCapabilityKeys: client.getRawCapabilityKeys?.() ?? [],
|
|
5859
6237
|
launchVariant: client.getLaunchVariant?.(),
|
|
@@ -5871,6 +6249,9 @@ export class LSPService {
|
|
|
5871
6249
|
root: client.root,
|
|
5872
6250
|
operationSupport: client.getOperationSupport(),
|
|
5873
6251
|
workspaceDiagnosticsSupport: client.getWorkspaceDiagnosticsSupport(),
|
|
6252
|
+
customServer: this.state.servers.get(serverId)?.custom,
|
|
6253
|
+
diagnosticsPublished: this.state.diagnosticsPublished.has(serverId),
|
|
6254
|
+
diagnosticsUnsupported: this.state.diagnosticsUnsupported.has(serverId),
|
|
5874
6255
|
advertisedCommands: client.getAdvertisedCommands(),
|
|
5875
6256
|
rawCapabilityKeys: client.getRawCapabilityKeys?.() ?? [],
|
|
5876
6257
|
launchVariant: client.getLaunchVariant?.(),
|
|
@@ -6599,6 +6980,42 @@ export class LSPService {
|
|
|
6599
6980
|
const workspaceSweepScopeKey = buildScopeKey("all", [
|
|
6600
6981
|
...WORKSPACE_SWEEP_EXCLUDED_SERVER_IDS,
|
|
6601
6982
|
]);
|
|
6983
|
+
const touchScopedOpenDependencies = async (filePath) => {
|
|
6984
|
+
if (options.files === undefined)
|
|
6985
|
+
return true;
|
|
6986
|
+
// Import coverage is language-neutral. Undefined means this file was
|
|
6987
|
+
// not covered by the facts seam, so its cache entry is not eligible.
|
|
6988
|
+
const imports = workspaceDiagnosticsCacheCtx.importsFor(filePath);
|
|
6989
|
+
if (!imports)
|
|
6990
|
+
return false;
|
|
6991
|
+
const openImports = imports.filter((dependency) => this.hasLiveClientHoldingDocument(dependency));
|
|
6992
|
+
const capped = openImports.length > SCOPED_DEPENDENCY_TOUCH_MAX;
|
|
6993
|
+
if (capped) {
|
|
6994
|
+
recordDegradationOnce({
|
|
6995
|
+
kind: "lsp_dependency_touch_capped",
|
|
6996
|
+
subject: filePath,
|
|
6997
|
+
reason: `scoped full scan limited open import touches to ${SCOPED_DEPENDENCY_TOUCH_MAX}`,
|
|
6998
|
+
});
|
|
6999
|
+
}
|
|
7000
|
+
await Promise.all(openImports
|
|
7001
|
+
.slice(0, SCOPED_DEPENDENCY_TOUCH_MAX)
|
|
7002
|
+
.map(async (dependency) => {
|
|
7003
|
+
let content;
|
|
7004
|
+
try {
|
|
7005
|
+
content = await fs.readFile(dependency, "utf8");
|
|
7006
|
+
}
|
|
7007
|
+
catch {
|
|
7008
|
+
return;
|
|
7009
|
+
}
|
|
7010
|
+
await this.touchFile(dependency, content, {
|
|
7011
|
+
diagnostics: "none",
|
|
7012
|
+
source: "lens_diagnostics_full_dependency",
|
|
7013
|
+
clientScope: "all",
|
|
7014
|
+
excludeServerIds: await this.serverIdsNotHoldingDocument(dependency),
|
|
7015
|
+
});
|
|
7016
|
+
}));
|
|
7017
|
+
return !capped;
|
|
7018
|
+
};
|
|
6602
7019
|
const cachedResults = [];
|
|
6603
7020
|
const filesToTouch = [];
|
|
6604
7021
|
const writeIndexByPath = new Map();
|
|
@@ -6615,18 +7032,22 @@ export class LSPService {
|
|
|
6615
7032
|
// exists to detect) must NOT be replayed and reconciled as confirmed via
|
|
6616
7033
|
// mode=full. On a mismatch, fall through to a fresh touch.
|
|
6617
7034
|
if (cached && cached.binding.boundToCurrentDisk !== false) {
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
7035
|
+
const dependenciesConfirmed = await touchScopedOpenDependencies(filePath);
|
|
7036
|
+
if (dependenciesConfirmed)
|
|
7037
|
+
cachedResults.push({
|
|
7038
|
+
filePath,
|
|
7039
|
+
diagnostics: cached.diagnostics,
|
|
7040
|
+
count: cached.count,
|
|
7041
|
+
// #1093: a cache hit replays an older observation — carry its
|
|
7042
|
+
// scan time so mode=full's footer reconcile stamps `touchedAt`
|
|
7043
|
+
// with when the truth was seen, not now().
|
|
7044
|
+
observedAt: cached.scannedAt,
|
|
7045
|
+
contentHash: cached.binding.contentHash,
|
|
7046
|
+
boundToCurrentDisk: cached.binding.boundToCurrentDisk,
|
|
7047
|
+
writeIndex: writeIndexByPath.get(normalizeMapKey(filePath)),
|
|
7048
|
+
});
|
|
7049
|
+
else
|
|
7050
|
+
filesToTouch.push(filePath);
|
|
6630
7051
|
}
|
|
6631
7052
|
else {
|
|
6632
7053
|
filesToTouch.push(filePath);
|
|
@@ -7042,7 +7463,7 @@ export class LSPService {
|
|
|
7042
7463
|
// ONE bracket for the whole fan-out, not one per file (#2272 review F1).
|
|
7043
7464
|
// Per-file bracketing was the first shape and it was inert for exactly
|
|
7044
7465
|
// the case above: `phaseFinished` pushes onto a ring capped at
|
|
7045
|
-
// `
|
|
7466
|
+
// `RECENT_PHASE_CAP` (5, clients/latency-logger.ts), while `turn_end`
|
|
7046
7467
|
// reads `getPhaseForWindow` AFTER the sweep returns. A 225-file sweep
|
|
7047
7468
|
// therefore evicted the blocking file's own bracket unless the block
|
|
7048
7469
|
// landed in the last five files, and the survivors were then rejected by
|
|
@@ -7412,16 +7833,17 @@ export class LSPService {
|
|
|
7412
7833
|
client.pruneDiagnostics((filePath, ts) => !existingPaths.has(filePath) || now - ts > CASCADE_DIAGNOSTICS_TTL_MS);
|
|
7413
7834
|
const clientDiags = client.getAllDiagnostics();
|
|
7414
7835
|
for (const [filePath, entry] of clientDiags) {
|
|
7836
|
+
const attributed = attributeDiagnostics(client.serverId, entry.diags);
|
|
7415
7837
|
const existing = all.get(filePath);
|
|
7416
7838
|
if (existing) {
|
|
7417
7839
|
existing.diags = mergeLspDiagnostics([
|
|
7418
7840
|
...existing.diags,
|
|
7419
|
-
...
|
|
7841
|
+
...attributed,
|
|
7420
7842
|
]);
|
|
7421
7843
|
existing.ts = Math.max(existing.ts, entry.ts);
|
|
7422
7844
|
}
|
|
7423
7845
|
else {
|
|
7424
|
-
all.set(filePath, { diags:
|
|
7846
|
+
all.set(filePath, { diags: attributed, ts: entry.ts });
|
|
7425
7847
|
}
|
|
7426
7848
|
const list = bindingsByPath.get(filePath) ?? [];
|
|
7427
7849
|
list.push(entry.binding);
|
|
@@ -7515,6 +7937,7 @@ export class LSPService {
|
|
|
7515
7937
|
if (this.checkDestroyed())
|
|
7516
7938
|
return;
|
|
7517
7939
|
this.isDestroyed = true;
|
|
7940
|
+
this.shutdownController.abort();
|
|
7518
7941
|
for (const [key, token] of this.outstandingAuxNotifyWrites) {
|
|
7519
7942
|
this.releaseOutstandingAuxNotifyWrite(key, token);
|
|
7520
7943
|
}
|
|
@@ -7563,6 +7986,7 @@ export class LSPService {
|
|
|
7563
7986
|
});
|
|
7564
7987
|
this.state.clients.clear();
|
|
7565
7988
|
this.state.broken.clear();
|
|
7989
|
+
this.firstContactWaits.clear();
|
|
7566
7990
|
// #1934 review F1: map hygiene alongside the breaker it sits next to.
|
|
7567
7991
|
// Not load-bearing — every read follows its own attempt's write — but a
|
|
7568
7992
|
// verdict for a client generation that no longer exists is dead weight.
|
|
@@ -7779,6 +8203,9 @@ export async function hasAuxiliaryLspPublishedForRoot(serverId, filePath) {
|
|
|
7779
8203
|
export async function notifyExternalFileChange(filePath, type) {
|
|
7780
8204
|
return getLSPService().notifyExternalFileChange(filePath, type);
|
|
7781
8205
|
}
|
|
8206
|
+
export async function resyncGitChangedFiles(changedPaths) {
|
|
8207
|
+
return getLSPService().resyncGitChangedFiles(changedPaths);
|
|
8208
|
+
}
|
|
7782
8209
|
export function resetLSPService(options = {}) {
|
|
7783
8210
|
// #2504 review round 2 (F3): whatever the retiring service was still being
|
|
7784
8211
|
// asked for off-hook must stop HERE, before any teardown and before the
|