@herbertgao/pi-extensions 2026.8.5 → 2026.8.6
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 +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +48 -6
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +10 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +5 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +24 -17
- package/node_modules/@narumitw/pi-btw/package.json +5 -5
- package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +33 -13
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/pi-lens/CHANGELOG.md +95 -0
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
- package/node_modules/pi-lens/dist/clients/biome-client.js +121 -13
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +11 -3
- package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +16 -4
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +135 -30
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +19 -7
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +6 -4
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +114 -10
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +101 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +230 -50
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +296 -3
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +39 -8
- package/node_modules/pi-lens/dist/clients/git-guard.js +18 -21
- package/node_modules/pi-lens/dist/clients/go-client.js +21 -39
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +116 -7
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +4 -9
- package/node_modules/pi-lens/dist/clients/knip-client.js +51 -14
- package/node_modules/pi-lens/dist/clients/latency-logger.js +50 -1
- package/node_modules/pi-lens/dist/clients/lens-events.js +56 -25
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +45 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +58 -13
- package/node_modules/pi-lens/dist/clients/lsp/client.js +169 -8
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +29 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +264 -66
- package/node_modules/pi-lens/dist/clients/lsp/server.js +247 -60
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +4 -0
- package/node_modules/pi-lens/dist/clients/mcp/session.js +30 -17
- package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +29 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
- package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +153 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +104 -11
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +170 -22
- package/node_modules/pi-lens/dist/clients/runtime-session.js +28 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +124 -10
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +388 -35
- package/node_modules/pi-lens/dist/clients/rust-client.js +21 -37
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +88 -5
- package/node_modules/pi-lens/dist/clients/sg-runner.js +141 -23
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +18 -11
- package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +431 -24
- package/node_modules/pi-lens/dist/clients/tool-policy.js +2 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +17 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +305 -33
- package/node_modules/pi-lens/dist/index.js +4827 -1633
- package/node_modules/pi-lens/dist/mcp/server.js +8 -2
- package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +3 -2
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +62 -7
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +38 -13
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +11 -3
- package/node_modules/pi-lens/docs/features.md +14 -1
- package/node_modules/pi-lens/docs/globalconfig.md +11 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +1 -1
- package/node_modules/pi-lens/docs/settings.md +6 -0
- package/node_modules/pi-lens/docs/usage.md +23 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -0
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +79 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
- package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +13 -0
- package/node_modules/pi-mcp-adapter/README.md +4 -1
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/cli.js +25 -4
- package/node_modules/pi-mcp-adapter/config.ts +4 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +18 -15
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +18 -8
- package/node_modules/pi-mcp-adapter/package.json +2 -1
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +5 -0
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +38 -18
- package/node_modules/pi-mcp-adapter/types.ts +93 -10
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +18 -12
- package/node_modules/pi-web-access/auth-fetch.ts +148 -0
- package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
- package/node_modules/pi-web-access/curator-page.ts +5 -3
- package/node_modules/pi-web-access/curator-server.ts +2 -1
- package/node_modules/pi-web-access/extract.ts +106 -34
- package/node_modules/pi-web-access/fetch-params.ts +17 -3
- package/node_modules/pi-web-access/firecrawl.ts +172 -12
- package/node_modules/pi-web-access/gemini-search.ts +18 -4
- package/node_modules/pi-web-access/index.ts +120 -48
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/summary-review.ts +11 -5
- package/node_modules/pi-web-access/youtube-extract.ts +2 -2
- package/package.json +9 -9
|
@@ -22,10 +22,10 @@ import { incrementDegradationCount, recordDegradation, recordDegradationOnce, }
|
|
|
22
22
|
import { isLspSpawnAllowedByTrust, assertInstallAllowed, projectTrustDenialReason, } from "../project-trust.js";
|
|
23
23
|
import { shouldPreferPullOnlyDiagnostics } from "../lsp-budget.js";
|
|
24
24
|
import { withDeadline } from "../deadline-utils.js";
|
|
25
|
-
import { isAtOrAboveHomeDir, normalizeMapKey, uriToPath, } from "../path-utils.js";
|
|
25
|
+
import { isAtOrAboveHomeDir, isWindowsPath, normalizeMapKey, uriToPath, } from "../path-utils.js";
|
|
26
26
|
import { recordLspMutation, } from "../lsp-mutation.js";
|
|
27
27
|
import { createLSPClient } from "./client.js";
|
|
28
|
-
import { bindingStateLabel, composeBoundToCurrentDisk, createDiskBindingCache, } from "./diagnostic-binding.js";
|
|
28
|
+
import { bindingStateLabel, composeBoundToCurrentDisk, createDiskBindingCache, touchCoverageGap, } from "./diagnostic-binding.js";
|
|
29
29
|
import { getServersForFileWithConfig, getServerInitOverride } from "./config.js";
|
|
30
30
|
import { getLanguageId } from "./language.js";
|
|
31
31
|
import { LSP_SERVERS, enforceLspRootCeiling, hasProjectBoundaryMarker, isDirectLspCommandTemporarilyUnavailable, } from "./server.js";
|
|
@@ -230,13 +230,15 @@ function readTsserverSyncGraceMs() {
|
|
|
230
230
|
}
|
|
231
231
|
/**
|
|
232
232
|
* Read the `PI_LENS_AUX_GRACE_MS` env override at call time (not module
|
|
233
|
-
* load time) so tests can set it per-case. Controls how long
|
|
234
|
-
* promises (opengrep, ast-grep, zizmor, …) are waited after
|
|
235
|
-
* promises have settled in both getDiagnostics
|
|
236
|
-
* touchFile push wait
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
233
|
+
* load time) so tests can set it per-case. Controls the CEILING on how long
|
|
234
|
+
* auxiliary-role promises (opengrep, ast-grep, zizmor, …) are waited after
|
|
235
|
+
* all primary-role promises have settled, in both getDiagnostics
|
|
236
|
+
* (raceToCompletion) and the touchFile push wait (#1458 S2 — the two lanes
|
|
237
|
+
* share the same declared-budget-capped-by-ceiling shape). Each auxiliary
|
|
238
|
+
* still gets only its OWN declared `aggregateWaitMs` up to this ceiling —
|
|
239
|
+
* this is not a flat per-touch wait. Returns undefined when the var is
|
|
240
|
+
* absent; each call site then supplies its own default ceiling (touchFile:
|
|
241
|
+
* 2000ms; getDiagnostics: 2000ms — see the `?? 2000` at each call site).
|
|
240
242
|
*/
|
|
241
243
|
function readEnvAuxGraceMs() {
|
|
242
244
|
const raw = process.env.PI_LENS_AUX_GRACE_MS;
|
|
@@ -636,10 +638,13 @@ export class LSPService {
|
|
|
636
638
|
if (!key.startsWith(prefix))
|
|
637
639
|
continue;
|
|
638
640
|
const ancestorKey = key.slice(prefix.length);
|
|
639
|
-
const
|
|
641
|
+
const pathApi = isWindowsPath(ancestorKey) || isWindowsPath(rootKey)
|
|
642
|
+
? path.win32
|
|
643
|
+
: path;
|
|
644
|
+
const relative = pathApi.relative(ancestorKey, rootKey);
|
|
640
645
|
if (relative === "" ||
|
|
641
646
|
relative.startsWith("..") ||
|
|
642
|
-
|
|
647
|
+
pathApi.isAbsolute(relative)) {
|
|
643
648
|
continue;
|
|
644
649
|
}
|
|
645
650
|
if (!nearestAncestor || ancestorKey.length > nearestAncestor.length) {
|
|
@@ -1699,6 +1704,24 @@ export class LSPService {
|
|
|
1699
1704
|
const notifySkippedServerIds = new Set(spawnedServerIds.filter((serverId) => this.shouldSkipNotify(filePath, content, clientScope, serverId)));
|
|
1700
1705
|
const notifySkipped = spawned.length > 0 && notifySkippedServerIds.size === spawned.length;
|
|
1701
1706
|
const diagnosticBaselines = new Map(spawned.map((entry) => [entry.client, entry.client.diagnosticsVersion]));
|
|
1707
|
+
// #1458: read a late auxiliary publication BEFORE the ordinary resync
|
|
1708
|
+
// clears its client cache. Carry it only when the publication's exact
|
|
1709
|
+
// sent-content fingerprint matches this touch's content. A changed edit,
|
|
1710
|
+
// version-less publication, or malformed binding fails closed and is not
|
|
1711
|
+
// replayed. The fresh notify still runs below, so scanners continue toward
|
|
1712
|
+
// a publication for this touch while the prior late result reaches the read.
|
|
1713
|
+
const touchContentHash = this.hashContent(content);
|
|
1714
|
+
const carriedAuxiliary = options.collectDiagnostics
|
|
1715
|
+
? spawned.flatMap((entry) => {
|
|
1716
|
+
if (entry.info.role !== "auxiliary")
|
|
1717
|
+
return [];
|
|
1718
|
+
const binding = entry.client.getDiagnosticBinding?.(filePath);
|
|
1719
|
+
if (binding?.contentHash !== touchContentHash)
|
|
1720
|
+
return [];
|
|
1721
|
+
const diags = entry.client.getDiagnostics(filePath);
|
|
1722
|
+
return diags.length > 0 ? [{ diags, binding }] : [];
|
|
1723
|
+
})
|
|
1724
|
+
: [];
|
|
1702
1725
|
// #743: PER-SERVER notify-write deadlines. Each server's didOpen/didChange
|
|
1703
1726
|
// write gets its OWN notifyWriteBudgetMs budget rather than one shared
|
|
1704
1727
|
// deadline over a single Promise.all — otherwise one backpressured server
|
|
@@ -1827,7 +1850,7 @@ export class LSPService {
|
|
|
1827
1850
|
const sweepFirstTouch = new Map();
|
|
1828
1851
|
if (options.sweepIndexGate) {
|
|
1829
1852
|
for (const entry of spawned) {
|
|
1830
|
-
const strategy = getStrategy(entry.client.serverId);
|
|
1853
|
+
const strategy = getStrategy(entry.client.serverId, entry.client.getLaunchVariant?.());
|
|
1831
1854
|
if (strategy.workspaceIndexing) {
|
|
1832
1855
|
sweepFirstTouch.set(entry.client.serverId, options.sweepIndexGate.consumeFirstTouch(entry.client.serverId));
|
|
1833
1856
|
}
|
|
@@ -1848,7 +1871,7 @@ export class LSPService {
|
|
|
1848
1871
|
const silentCleanWarmupServers = new Set();
|
|
1849
1872
|
if (options.warmupOverride && (options.warmupAttempt ?? 1) <= 1) {
|
|
1850
1873
|
for (const entry of spawned) {
|
|
1851
|
-
const strategy = getStrategy(entry.client.serverId);
|
|
1874
|
+
const strategy = getStrategy(entry.client.serverId, entry.client.getLaunchVariant?.());
|
|
1852
1875
|
if (strategy.workspaceIndexing !== true)
|
|
1853
1876
|
continue;
|
|
1854
1877
|
try {
|
|
@@ -1877,7 +1900,8 @@ export class LSPService {
|
|
|
1877
1900
|
// slow aux (opengrep) can't override the per-edit cap. Resolves as soon
|
|
1878
1901
|
// as a server publishes; this is just its individual deadline. (#242)
|
|
1879
1902
|
const perServerTimeout = (serverId) => {
|
|
1880
|
-
const
|
|
1903
|
+
const launchVariant = spawned.find((entry) => entry.client.serverId === serverId)?.client.getLaunchVariant?.();
|
|
1904
|
+
const strategy = getStrategy(serverId, launchVariant);
|
|
1881
1905
|
let strategyWait = strategy.aggregateWaitMs;
|
|
1882
1906
|
// #645: a `workspaceIndexing` server (marksman) only needs the
|
|
1883
1907
|
// full budget for the FIRST same-sweep touch to it — every
|
|
@@ -1951,7 +1975,7 @@ export class LSPService {
|
|
|
1951
1975
|
clientScope === "primary" &&
|
|
1952
1976
|
spawned.length === 1 &&
|
|
1953
1977
|
spawned[0].client.serverId === "typescript" &&
|
|
1954
|
-
getStrategy(spawned[0].client.serverId).silentOnClean === true) {
|
|
1978
|
+
getStrategy(spawned[0].client.serverId, spawned[0].client.getLaunchVariant?.()).silentOnClean === true) {
|
|
1955
1979
|
try {
|
|
1956
1980
|
const snapshots = await this.getCapabilitySnapshots(filePath);
|
|
1957
1981
|
tsserverSyncEligible =
|
|
@@ -1979,7 +2003,17 @@ export class LSPService {
|
|
|
1979
2003
|
// The #707 tsserver sync race operates exclusively on single-server
|
|
1980
2004
|
// primary-scope touches (guarded by `clientScope === "primary" &&
|
|
1981
2005
|
// spawned.length === 1`), so there is NO interaction with this path.
|
|
2006
|
+
// #1458 S4: also gated on `collectDiagnostics` — a non-collecting
|
|
2007
|
+
// with-auxiliary touch has nothing to carry the aux wait's result
|
|
2008
|
+
// INTO (its diagnostics are discarded either way), so paying up to
|
|
2009
|
+
// `auxCeilingMs` of extra latency for it buys nothing. Both current
|
|
2010
|
+
// callers (`getDiagnostics`'s with-auxiliary path and the cascade's
|
|
2011
|
+
// collecting touch) already pass `collectDiagnostics: true`, so this
|
|
2012
|
+
// is latent-today defense, not a behavior change — but a future
|
|
2013
|
+
// non-collecting with-auxiliary caller must not silently inherit the
|
|
2014
|
+
// full aux-grace cost for diagnostics it's about to throw away.
|
|
1982
2015
|
const hasTouchAuxiliaries = clientScope === "with-auxiliary" &&
|
|
2016
|
+
options.collectDiagnostics === true &&
|
|
1983
2017
|
spawned.some((e) => e.info.role === "auxiliary");
|
|
1984
2018
|
// Per-server wait promises (each already bounded by its own
|
|
1985
2019
|
// perServerTimeout — unchanged from before R8).
|
|
@@ -2017,49 +2051,100 @@ export class LSPService {
|
|
|
2017
2051
|
? (() => {
|
|
2018
2052
|
// Primary waits: all non-auxiliary servers.
|
|
2019
2053
|
const primaryWaits = perServerWaits.filter((_, i) => spawned[i].info.role !== "auxiliary");
|
|
2020
|
-
// Aux waits: auxiliary servers (advisory).
|
|
2054
|
+
// Aux waits: auxiliary servers (advisory). `client` and the
|
|
2055
|
+
// pre-notify `diagnosticsVersion` baseline travel alongside the
|
|
2056
|
+
// promise so the outcome can be decided from EVIDENCE after the
|
|
2057
|
+
// race, not from how the raced promise settled (#1458 S1 — see
|
|
2058
|
+
// below).
|
|
2021
2059
|
const auxWaits = perServerWaits
|
|
2022
2060
|
.map((p, i) => spawned[i].info.role === "auxiliary"
|
|
2023
|
-
? {
|
|
2061
|
+
? {
|
|
2062
|
+
promise: p,
|
|
2063
|
+
serverId: spawned[i].info.id,
|
|
2064
|
+
client: spawned[i].client,
|
|
2065
|
+
baseline: diagnosticBaselines.get(spawned[i].client),
|
|
2066
|
+
}
|
|
2024
2067
|
: null)
|
|
2025
2068
|
.filter((x) => x !== null);
|
|
2026
|
-
const
|
|
2027
|
-
// After all primaries settle, give
|
|
2028
|
-
//
|
|
2029
|
-
//
|
|
2030
|
-
//
|
|
2069
|
+
const auxCeilingMs = readEnvAuxGraceMs() ?? 2000;
|
|
2070
|
+
// After all primaries settle, give each auxiliary the smaller of
|
|
2071
|
+
// its declared wait budget and the global auxiliary ceiling. The
|
|
2072
|
+
// 2000ms default admits measured ~1.3s warm scanner runs without
|
|
2073
|
+
// making every edit pay opengrep's 3500ms cold-start allowance.
|
|
2074
|
+
// Late aux results are dropped from this wait. A later unchanged-
|
|
2075
|
+
// content read may carry a SHA-256-bound cache publication before its
|
|
2076
|
+
// resync clears the cache; changed or unknown content never replays.
|
|
2077
|
+
// Aux servers that answer within the grace are included automatically since
|
|
2031
2078
|
// their waitForDiagnostics already resolved. The cut-off server ids
|
|
2032
2079
|
// are logged in the latency metadata (lsp_touch_file phase, field
|
|
2033
2080
|
// `auxCutOffServerIds`).
|
|
2034
2081
|
return Promise.all(primaryWaits).then(async () => {
|
|
2035
2082
|
if (auxWaits.length === 0)
|
|
2036
2083
|
return;
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2084
|
+
const auxWaitStartedAt = Date.now();
|
|
2085
|
+
const outcomes = await Promise.all(auxWaits.map(async (aux) => {
|
|
2086
|
+
const budgetMs = Math.min(timeoutFor(aux.serverId), auxCeilingMs);
|
|
2087
|
+
let timer;
|
|
2088
|
+
const timeout = new Promise((resolve) => {
|
|
2089
|
+
timer = setTimeout(() => resolve(false), budgetMs);
|
|
2090
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
2091
|
+
timer.unref?.();
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
const raced = await Promise.race([
|
|
2095
|
+
aux.promise.then(() => true),
|
|
2096
|
+
timeout,
|
|
2097
|
+
]);
|
|
2098
|
+
if (timer)
|
|
2099
|
+
clearTimeout(timer);
|
|
2100
|
+
// #1458 S1: `waitForDiagnostics` RESOLVES on its own timeout
|
|
2101
|
+
// (client.ts) — it never rejects, and a silent scanner that
|
|
2102
|
+
// published nothing looks identical, promise-wise, to one
|
|
2103
|
+
// that answered. `raced === true` only means "the promise
|
|
2104
|
+
// settled before our timer fired"; it is not proof anything
|
|
2105
|
+
// was published. Decide the outcome from evidence instead:
|
|
2106
|
+
// did this aux's `diagnosticsVersion` advance past the
|
|
2107
|
+
// pre-notify baseline captured before the wait started?
|
|
2108
|
+
// - raced === false → "cut_off" (our timer won;
|
|
2109
|
+
// the aux's own wait never got to answer for itself).
|
|
2110
|
+
// - raced === true, no evidence → "silent" (the aux's own
|
|
2111
|
+
// wait gave up within its budget with nothing to report —
|
|
2112
|
+
// NOT the same as having answered).
|
|
2113
|
+
// - raced === true, evidence → "answered" (a fresh
|
|
2114
|
+
// publication actually landed for this touch).
|
|
2115
|
+
const publishedEvidence = raced &&
|
|
2116
|
+
Number.isFinite(aux.baseline) &&
|
|
2117
|
+
aux.client.diagnosticsVersion > aux.baseline;
|
|
2118
|
+
const outcome = !raced
|
|
2119
|
+
? "cut_off"
|
|
2120
|
+
: publishedEvidence
|
|
2121
|
+
? "answered"
|
|
2122
|
+
: "silent";
|
|
2123
|
+
return {
|
|
2124
|
+
serverId: aux.serverId,
|
|
2125
|
+
outcome,
|
|
2126
|
+
budgetMs,
|
|
2127
|
+
elapsedMs: Date.now() - auxWaitStartedAt,
|
|
2128
|
+
// #1458 S3: elapsed measured from BEFORE the primary wait
|
|
2129
|
+
// (waitStartedAt), not just from auxWaitStartedAt — this is
|
|
2130
|
+
// what lets a latency row validate the ~1.3s warm-scanner
|
|
2131
|
+
// figure the 2000ms ceiling was set from; `elapsedMs` alone
|
|
2132
|
+
// only covers the POST-primary aux phase.
|
|
2133
|
+
elapsedSinceNotifyMs: Date.now() - waitStartedAt,
|
|
2134
|
+
};
|
|
2135
|
+
}));
|
|
2136
|
+
const unfinished = outcomes
|
|
2137
|
+
.filter((outcome) => outcome.outcome === "cut_off")
|
|
2138
|
+
.map((outcome) => outcome.serverId);
|
|
2139
|
+
if (unfinished.length > 0)
|
|
2140
|
+
auxCutOffServerIds = unfinished;
|
|
2141
|
+
logLatency({
|
|
2142
|
+
type: "phase",
|
|
2143
|
+
phase: "lsp_aux_wait_outcome",
|
|
2144
|
+
filePath: normalizedPath,
|
|
2145
|
+
durationMs: Date.now() - auxWaitStartedAt,
|
|
2146
|
+
metadata: { clientScope, outcomes },
|
|
2041
2147
|
});
|
|
2042
|
-
const auxAll = Promise.all(auxWaits.map((a) => a.promise)).then(() => "done");
|
|
2043
|
-
const outcome = await Promise.race([auxAll, auxTimeout]);
|
|
2044
|
-
if (outcome === "timeout") {
|
|
2045
|
-
// Record which auxiliaries did NOT finish in time.
|
|
2046
|
-
const unfinished = [];
|
|
2047
|
-
for (const a of auxWaits) {
|
|
2048
|
-
let done = false;
|
|
2049
|
-
// Check synchronously if already resolved by racing against
|
|
2050
|
-
// a resolved promise.
|
|
2051
|
-
await Promise.race([
|
|
2052
|
-
a.promise.then(() => {
|
|
2053
|
-
done = true;
|
|
2054
|
-
}),
|
|
2055
|
-
Promise.resolve(),
|
|
2056
|
-
]);
|
|
2057
|
-
if (!done)
|
|
2058
|
-
unfinished.push(a.serverId);
|
|
2059
|
-
}
|
|
2060
|
-
if (unfinished.length > 0)
|
|
2061
|
-
auxCutOffServerIds = unfinished;
|
|
2062
|
-
}
|
|
2063
2148
|
});
|
|
2064
2149
|
})()
|
|
2065
2150
|
: Promise.all(perServerWaits).then(() => { });
|
|
@@ -2178,6 +2263,12 @@ export class LSPService {
|
|
|
2178
2263
|
durationMs: waitedMs,
|
|
2179
2264
|
metadata: {
|
|
2180
2265
|
source,
|
|
2266
|
+
// #1444: WHICH server(s) burned the budget — without this the
|
|
2267
|
+
// ~221/day timeout rows can't be attributed to a server at all.
|
|
2268
|
+
// `info.id` (not `client.serverId`) for the same reason the
|
|
2269
|
+
// degradation ledger above uses it: test doubles and lightweight
|
|
2270
|
+
// clients need not expose `serverId`.
|
|
2271
|
+
serverIds: spawned.map((e) => e.info.id),
|
|
2181
2272
|
clientScope,
|
|
2182
2273
|
diagnosticsMode,
|
|
2183
2274
|
timeoutMs,
|
|
@@ -2269,7 +2360,10 @@ export class LSPService {
|
|
|
2269
2360
|
let collected = options.collectDiagnostics
|
|
2270
2361
|
? tsserverSyncConfirmed !== undefined
|
|
2271
2362
|
? mergeLspDiagnostics(tsserverSyncConfirmed)
|
|
2272
|
-
: mergeLspDiagnostics(
|
|
2363
|
+
: mergeLspDiagnostics([
|
|
2364
|
+
...spawned.flatMap((entry) => entry.client.getDiagnostics(filePath)),
|
|
2365
|
+
...carriedAuxiliary.flatMap((entry) => entry.diags),
|
|
2366
|
+
])
|
|
2273
2367
|
: undefined;
|
|
2274
2368
|
// #1095 (P3-b): whether `collected` came from a tsserver sync confirm
|
|
2275
2369
|
// (`tsserverSyncRequest`) rather than the publish cache. A sync-confirmed
|
|
@@ -2367,7 +2461,7 @@ export class LSPService {
|
|
|
2367
2461
|
!tsserverSyncEligible &&
|
|
2368
2462
|
clientScope === "primary" &&
|
|
2369
2463
|
spawned.length === 1 &&
|
|
2370
|
-
getStrategy(spawned[0].client.serverId).silentOnClean === true) {
|
|
2464
|
+
getStrategy(spawned[0].client.serverId, spawned[0].client.getLaunchVariant?.()).silentOnClean === true) {
|
|
2371
2465
|
try {
|
|
2372
2466
|
const snapshots = await this.getCapabilitySnapshots(filePath);
|
|
2373
2467
|
if (classifyCascadeWaitTier(this, filePath, snapshots) === "tier3-silent") {
|
|
@@ -2412,6 +2506,22 @@ export class LSPService {
|
|
|
2412
2506
|
// findings that just hadn't arrived yet — it must not be trusted as a
|
|
2413
2507
|
// confirmed answer.
|
|
2414
2508
|
const inconclusive = notifyWriteTimedOut || diagnosticsTimedOut;
|
|
2509
|
+
// #1470: an auxiliary whose push wait was CUT OFF by the aux grace timer
|
|
2510
|
+
// (R8/#714) contributed exactly as much evidence about this file as one that
|
|
2511
|
+
// went silent inside its own budget — none. A hung opengrep resolved
|
|
2512
|
+
// `confirmation: "confirmed"` and read as confirmed-clean on the security
|
|
2513
|
+
// lane. (The SILENT case still does, and is NOT addressed here: a scanner
|
|
2514
|
+
// that settles inside its own budget without publishing also yields an
|
|
2515
|
+
// unqualified confirmation. Same #533 class, same lane, different door —
|
|
2516
|
+
// filed as #1493, deliberately untouched by this change.)
|
|
2517
|
+
// This does NOT flip the touch to inconclusive: that would discard a
|
|
2518
|
+
// primary answer that IS trustworthy (#533 honesty doctrine cuts both ways —
|
|
2519
|
+
// overclaiming and underclaiming are both dishonest). Instead the confirmation
|
|
2520
|
+
// is NARROWED: `"partial"`, naming the servers it does not speak for, so every
|
|
2521
|
+
// consumer that treats confirmation as proof of coverage fails closed while
|
|
2522
|
+
// the primary's findings still flow.
|
|
2523
|
+
const unconfirmedServerIds = auxCutOffServerIds ?? [];
|
|
2524
|
+
const coverageGap = unconfirmedServerIds.length > 0;
|
|
2415
2525
|
// #667: a confirmed (non-inconclusive) diagnostics-mode touch is the
|
|
2416
2526
|
// "actually warm" signal `ensureWarmForSweep` waits for — mark every
|
|
2417
2527
|
// spawned server so a later sweep in this session sees the check as a
|
|
@@ -2422,11 +2532,27 @@ export class LSPService {
|
|
|
2422
2532
|
// write stalled must still be eligible, so only servers whose OWN write
|
|
2423
2533
|
// timed out are skipped here (rather than gating the whole loop on the
|
|
2424
2534
|
// file-level `inconclusive`).
|
|
2535
|
+
//
|
|
2536
|
+
// #1470: same per-server reasoning for a CUT-OFF auxiliary. "Demonstrated
|
|
2537
|
+
// ready" means this server answered for this file; an auxiliary our grace
|
|
2538
|
+
// timer cut off demonstrably did not.
|
|
2539
|
+
//
|
|
2540
|
+
// NO TEST PINS THIS LINE, and that is a property of today's readers rather
|
|
2541
|
+
// than a coverage gap: `ensureWarmForSweep` filters `role === "auxiliary"`
|
|
2542
|
+
// out of its server list entirely, so no reader consumes an auxiliary's
|
|
2543
|
+
// `demonstratedReady` mark and deleting this `continue` changes no observable
|
|
2544
|
+
// behavior (verified by mutation — the LSP suite stays green). It stays
|
|
2545
|
+
// because the mark's meaning is "this server answered", and the moment any
|
|
2546
|
+
// reader stops filtering auxiliaries out, marking a cut-off scanner warm
|
|
2547
|
+
// would let it skip a warm-up it never earned.
|
|
2425
2548
|
const notifyTimedOutServerIds = new Set(notifyWriteTimedOutServerIds);
|
|
2549
|
+
const cutOffServerIds = new Set(unconfirmedServerIds);
|
|
2426
2550
|
if (diagnosticsMode !== "none" && !diagnosticsTimedOut) {
|
|
2427
2551
|
for (const entry of spawned) {
|
|
2428
2552
|
if (notifyTimedOutServerIds.has(entry.info.id))
|
|
2429
2553
|
continue;
|
|
2554
|
+
if (cutOffServerIds.has(entry.info.id))
|
|
2555
|
+
continue;
|
|
2430
2556
|
const key = await this.demonstratedReadyKeyFor(entry.info, filePath);
|
|
2431
2557
|
if (key)
|
|
2432
2558
|
this.markDemonstratedReadyKey(key);
|
|
@@ -2441,7 +2567,14 @@ export class LSPService {
|
|
|
2441
2567
|
// empty `collected` must never erase a previously-confirmed non-empty
|
|
2442
2568
|
// record (that's the #570 bug — a timeout silently reporting as clean
|
|
2443
2569
|
// and wiping out known-good diagnostic state).
|
|
2444
|
-
|
|
2570
|
+
// #1470: a PARTIAL touch is the same hazard wearing a different flag. Its
|
|
2571
|
+
// merged array is missing whatever the cut-off auxiliary would have said, so
|
|
2572
|
+
// priming the cache with it would let `actionable-warnings`' hash-guarded
|
|
2573
|
+
// read replay a partially-covered result as an authoritative observation —
|
|
2574
|
+
// and an empty one would DELETE a previously-confirmed record on the strength
|
|
2575
|
+
// of a scanner that never answered. Skip the prime; the next read pays a real
|
|
2576
|
+
// round trip instead of trusting an incomplete one.
|
|
2577
|
+
if (collected !== undefined && !inconclusive && !coverageGap) {
|
|
2445
2578
|
const normalizedKey = normalizeMapKey(filePath);
|
|
2446
2579
|
if (collected.length > 0) {
|
|
2447
2580
|
this.lastKnownDiagnostics.set(normalizedKey, collected);
|
|
@@ -2465,6 +2598,14 @@ export class LSPService {
|
|
|
2465
2598
|
if (collected !== undefined && inconclusive) {
|
|
2466
2599
|
result.inconclusive = true;
|
|
2467
2600
|
}
|
|
2601
|
+
else if (collected !== undefined && coverageGap) {
|
|
2602
|
+
// #1470: narrowed, not collapsed. The primary's findings ride along in
|
|
2603
|
+
// `.diags` exactly as before; what changes is that the touch now states
|
|
2604
|
+
// which servers it does not speak for, so no consumer can read this as a
|
|
2605
|
+
// full clean bill of health.
|
|
2606
|
+
result.confirmation = "partial";
|
|
2607
|
+
result.unconfirmedServerIds = [...unconfirmedServerIds];
|
|
2608
|
+
}
|
|
2468
2609
|
else if (collected !== undefined) {
|
|
2469
2610
|
// Preserve the lower-level affirmative result across consumers. In
|
|
2470
2611
|
// particular, the silent-clean gates above clear diagnosticsTimedOut only
|
|
@@ -2488,7 +2629,10 @@ export class LSPService {
|
|
|
2488
2629
|
// Optional-chain so a client without the getter (test doubles, a
|
|
2489
2630
|
// partially-mocked client) yields "unknown" rather than throwing —
|
|
2490
2631
|
// unknown preserves pre-#1095 behavior for that contributor.
|
|
2491
|
-
|
|
2632
|
+
[
|
|
2633
|
+
...spawned.map((entry) => entry.client.getDiagnosticBinding?.(filePath)),
|
|
2634
|
+
...carriedAuxiliary.map((entry) => entry.binding),
|
|
2635
|
+
]);
|
|
2492
2636
|
result.binding = binding;
|
|
2493
2637
|
}
|
|
2494
2638
|
// The recent-touches entries are recorded per server inside the notify-write
|
|
@@ -2527,6 +2671,13 @@ export class LSPService {
|
|
|
2527
2671
|
}),
|
|
2528
2672
|
diagnosticsTimedOut,
|
|
2529
2673
|
inconclusive,
|
|
2674
|
+
// #1470: the touch's own honesty verdict, so a `cut_off` row in
|
|
2675
|
+
// `lsp_aux_wait_outcome` can be joined to the touch that produced it and
|
|
2676
|
+
// shown NOT to have claimed confirmation for that server's coverage.
|
|
2677
|
+
// Absent for a non-collecting touch, which claims nothing either way.
|
|
2678
|
+
...(result.confirmation !== undefined && {
|
|
2679
|
+
confirmation: result.confirmation,
|
|
2680
|
+
}),
|
|
2530
2681
|
// R8 (#714): server ids of auxiliaries whose push wait was cut off by
|
|
2531
2682
|
// the aux grace window (primary settled clean + aux timed out in grace).
|
|
2532
2683
|
// Absent when no aux was cut off. These servers' diagnostics are
|
|
@@ -2624,7 +2775,8 @@ export class LSPService {
|
|
|
2624
2775
|
}
|
|
2625
2776
|
const clientWaits = spawned.map(async (entry) => {
|
|
2626
2777
|
const waitStart = Date.now();
|
|
2627
|
-
const
|
|
2778
|
+
const launchVariant = entry.client.getLaunchVariant?.();
|
|
2779
|
+
const strategy = getStrategy(entry.info.id, launchVariant);
|
|
2628
2780
|
await entry.client.waitForDiagnostics(filePath, strategy.aggregateWaitMs);
|
|
2629
2781
|
let diagnostics = entry.client.getDiagnostics(filePath);
|
|
2630
2782
|
const firstWaitMs = Date.now() - waitStart;
|
|
@@ -2636,6 +2788,7 @@ export class LSPService {
|
|
|
2636
2788
|
}
|
|
2637
2789
|
return {
|
|
2638
2790
|
serverId: entry.info.id,
|
|
2791
|
+
launchVariant,
|
|
2639
2792
|
waitMs: Date.now() - waitStart,
|
|
2640
2793
|
diagnosticCount: diagnostics.length,
|
|
2641
2794
|
diagnostics,
|
|
@@ -2644,25 +2797,48 @@ export class LSPService {
|
|
|
2644
2797
|
// Document mode: 0ms grace — return as soon as any client has results.
|
|
2645
2798
|
// Full mode: 400ms grace — wait a bit for other clients to catch up.
|
|
2646
2799
|
const graceMs = diagnosticsMode === "document" ? 0 : EARLY_UNBLOCK_GRACE_MS;
|
|
2647
|
-
// R8 (#714): per-promise role descriptors so raceToCompletion
|
|
2648
|
-
// a bounded aux grace once all primary-role promises have
|
|
2649
|
-
// Servers with role:"auxiliary" (opengrep, ast-grep, zizmor, …)
|
|
2650
|
-
//
|
|
2651
|
-
//
|
|
2800
|
+
// R8 (#714) / #1458 S2: per-promise role descriptors so raceToCompletion
|
|
2801
|
+
// can apply a bounded aux grace once all primary-role promises have
|
|
2802
|
+
// settled. Servers with role:"auxiliary" (opengrep, ast-grep, zizmor, …)
|
|
2803
|
+
// get their OWN declared `aggregateWaitMs` budget after the primary
|
|
2804
|
+
// settles, capped by the PI_LENS_AUX_GRACE_MS global ceiling (default
|
|
2805
|
+
// 2000ms) — the same "declared budget, capped by a ceiling" shape
|
|
2806
|
+
// `touchFile`'s with-auxiliary push wait uses, so this lane can no longer
|
|
2807
|
+
// starve a scanner whose measured warm run (e.g. opengrep ~1.3s) is
|
|
2808
|
+
// shorter than the ceiling but longer than a flat short grace. Late
|
|
2809
|
+
// arrivals are still dropped (advisory only — they land in the client
|
|
2652
2810
|
// cache and surface on the next edit). Primary-only callers have no
|
|
2653
2811
|
// auxiliary descriptors, so this path is never entered and there is
|
|
2654
2812
|
// zero behavior change for the single-server hot path.
|
|
2655
|
-
const diagDescriptors = spawned.map((entry) =>
|
|
2656
|
-
|
|
2657
|
-
|
|
2813
|
+
const diagDescriptors = spawned.map((entry) => {
|
|
2814
|
+
if (entry.info.role !== "auxiliary")
|
|
2815
|
+
return { role: "primary" };
|
|
2816
|
+
const strategy = getStrategy(entry.info.id, entry.client.getLaunchVariant?.());
|
|
2817
|
+
// Deleting this `budgetMs` fails no end-to-end test, and that is
|
|
2818
|
+
// expected rather than a coverage gap: every promise in `clientWaits`
|
|
2819
|
+
// already self-bounds at this same `strategy.aggregateWaitMs`, so an
|
|
2820
|
+
// auxiliary cuts itself off at its declared budget whether or not the
|
|
2821
|
+
// shared grace timer also knows about it. The descriptor is what keeps
|
|
2822
|
+
// the two agreeing — it is the mitigation for the over-granting
|
|
2823
|
+
// `raceToCompletion` documents, and it starts mattering the moment a
|
|
2824
|
+
// promise here stops self-bounding. The narrowing itself is pinned in
|
|
2825
|
+
// tests/clients/lsp/aggregation.test.ts, which can build the
|
|
2826
|
+
// non-self-bounded promises this path cannot.
|
|
2827
|
+
return { role: "auxiliary", budgetMs: strategy.aggregateWaitMs };
|
|
2828
|
+
});
|
|
2658
2829
|
// Result-aware racing: trigger early-unblock when any client has results,
|
|
2659
2830
|
// OR when a seedFirstPush server returns (its first push is authoritative
|
|
2660
2831
|
// even when empty — waiting longer yields nothing more).
|
|
2661
|
-
const perServer = await raceToCompletion(clientWaits, (results) => results.some((r) => r.diagnosticCount > 0 ||
|
|
2662
|
-
|
|
2832
|
+
const perServer = await raceToCompletion(clientWaits, (results) => results.some((r) => r.diagnosticCount > 0 ||
|
|
2833
|
+
getStrategy(r.serverId, r.launchVariant).seedFirstPush), {
|
|
2834
|
+
timeoutMs: Math.max(...spawned.map((entry) => getStrategy(entry.info.id, entry.client.getLaunchVariant?.())
|
|
2835
|
+
.aggregateWaitMs)),
|
|
2663
2836
|
graceMs,
|
|
2664
2837
|
descriptors: diagDescriptors,
|
|
2665
|
-
|
|
2838
|
+
// #1458 S2: ceiling, not a flat wait — each auxiliary's own
|
|
2839
|
+
// budgetMs (above) determines the actual per-touch grace up to
|
|
2840
|
+
// this cap. Matches touchFile's `readEnvAuxGraceMs() ?? 2000`.
|
|
2841
|
+
auxGraceMs: readEnvAuxGraceMs() ?? 2000,
|
|
2666
2842
|
});
|
|
2667
2843
|
// Fill in any slots that timed out before producing results.
|
|
2668
2844
|
const earlyUnblockedCount = spawned.length - perServer.length;
|
|
@@ -2670,7 +2846,8 @@ export class LSPService {
|
|
|
2670
2846
|
const found = perServer.find((r) => r.serverId === entry.info.id);
|
|
2671
2847
|
return (found ?? {
|
|
2672
2848
|
serverId: entry.info.id,
|
|
2673
|
-
|
|
2849
|
+
launchVariant: entry.client.getLaunchVariant?.(),
|
|
2850
|
+
waitMs: getStrategy(entry.info.id, entry.client.getLaunchVariant?.()).aggregateWaitMs,
|
|
2674
2851
|
diagnosticCount: 0,
|
|
2675
2852
|
diagnostics: [],
|
|
2676
2853
|
});
|
|
@@ -3745,7 +3922,17 @@ export class LSPService {
|
|
|
3745
3922
|
// answer, so `available` implies confirmed) — wrap it as `{ diags }`;
|
|
3746
3923
|
// the incumbent branch already returns the wrapper.
|
|
3747
3924
|
const touchResult = attached?.available
|
|
3748
|
-
? {
|
|
3925
|
+
? {
|
|
3926
|
+
diags: attached.response.diagnostics,
|
|
3927
|
+
// #1470: the incumbent's coverage gap crosses the socket as an
|
|
3928
|
+
// explicit DTO field, so carry it onto the wrapper the sweep
|
|
3929
|
+
// reads. Dropping it here would let a partially covered
|
|
3930
|
+
// incumbent answer be persisted as a confirmed sweep result —
|
|
3931
|
+
// the same false clean this change closes on the local route.
|
|
3932
|
+
...(attached.response.unconfirmedServerIds !== undefined && {
|
|
3933
|
+
unconfirmedServerIds: attached.response.unconfirmedServerIds,
|
|
3934
|
+
}),
|
|
3935
|
+
}
|
|
3749
3936
|
: await withDeadline(this.touchFile(filePath, content, {
|
|
3750
3937
|
diagnostics: "document",
|
|
3751
3938
|
collectDiagnostics: true,
|
|
@@ -3767,7 +3954,18 @@ export class LSPService {
|
|
|
3767
3954
|
// `perFileMs` deadline, which only catches a touch that never returned at
|
|
3768
3955
|
// all within budget. Either one means the result wasn't confirmed.
|
|
3769
3956
|
const inconclusive = touchResult?.inconclusive === true;
|
|
3770
|
-
|
|
3957
|
+
// #1470: a cut-off auxiliary is the THIRD reason this result is not a
|
|
3958
|
+
// confirmed observation, and it is deliberately not `inconclusive`. The
|
|
3959
|
+
// record loop below persists every `!timedOut` result into the workspace
|
|
3960
|
+
// cache, so reading `inconclusive` alone caches a partially covered
|
|
3961
|
+
// answer as clean and replays it on every later sweep. Today the only
|
|
3962
|
+
// route that can reach this branch with a gap is the warm-attach
|
|
3963
|
+
// incumbent, whose touch runs `clientScope: "with-auxiliary"`; the
|
|
3964
|
+
// sweep's own local touch uses `clientScope: "all"`, which never arms
|
|
3965
|
+
// the aux grace timer at all. Both are gated here so a future scope
|
|
3966
|
+
// change cannot reopen the hole silently.
|
|
3967
|
+
const coverageGap = touchCoverageGap(touchResult).length > 0;
|
|
3968
|
+
const timedOut = touchResult === undefined || inconclusive || coverageGap;
|
|
3771
3969
|
if (timedOut)
|
|
3772
3970
|
timedOutFiles += 1;
|
|
3773
3971
|
// #1104 (shape 5 — AGENTS.md): the touch's content binding is an
|