@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
|
@@ -15,6 +15,7 @@ import { findNodeToolBinary } from "./package-manager.js";
|
|
|
15
15
|
import { isFullyQualified } from "./path-utils.js";
|
|
16
16
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
17
17
|
import { createAvailabilityChecker, discoverManagedTool, getManagedToolEnvironment, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
18
|
+
import { createAvailabilityLatch } from "./dispatch/runners/utils/availability-policy.js";
|
|
18
19
|
/**
|
|
19
20
|
* Build madge's argv for a circular-dependency scan.
|
|
20
21
|
*
|
|
@@ -158,7 +159,8 @@ export class DependencyChecker {
|
|
|
158
159
|
environment: (cwd) => getManagedToolEnvironment("madge", cwd),
|
|
159
160
|
unclassifiedFailureOutcome: "missing",
|
|
160
161
|
});
|
|
161
|
-
|
|
162
|
+
/** Transient-aware availability memo — see `ensureAvailable` (#1467). */
|
|
163
|
+
availabilityLatch = createAvailabilityLatch();
|
|
162
164
|
ensureInFlight = null;
|
|
163
165
|
checkInFlight = new Map();
|
|
164
166
|
scanInFlight = new Map();
|
|
@@ -320,12 +322,17 @@ export class DependencyChecker {
|
|
|
320
322
|
this.circularFiles = circularFiles;
|
|
321
323
|
}
|
|
322
324
|
/**
|
|
323
|
-
* Check if madge is available, auto-install if not
|
|
325
|
+
* Check if madge is available, auto-install if not.
|
|
326
|
+
*
|
|
327
|
+
* Shares knip's latch policy (#1467): only a durable verdict is memoized, so
|
|
328
|
+
* a probe that timed out — madge's `--version` is ~0.7 s today, but a host
|
|
329
|
+
* event-loop stall can expire any budget — does not disable madge for the
|
|
330
|
+
* life of the process.
|
|
324
331
|
*/
|
|
325
332
|
async ensureAvailable(cwd = process.cwd()) {
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
return
|
|
333
|
+
const memo = this.availabilityLatch.read();
|
|
334
|
+
if (memo !== null)
|
|
335
|
+
return memo;
|
|
329
336
|
if (this.ensureInFlight)
|
|
330
337
|
return this.ensureInFlight;
|
|
331
338
|
this.ensureInFlight = this.doEnsureAvailable(cwd);
|
|
@@ -338,8 +345,13 @@ export class DependencyChecker {
|
|
|
338
345
|
}
|
|
339
346
|
async doEnsureAvailable(cwd) {
|
|
340
347
|
const resolved = await resolveAvailableOrInstall(this.madgeAvailability, "madge", cwd);
|
|
341
|
-
|
|
342
|
-
|
|
348
|
+
if (resolved !== null) {
|
|
349
|
+
this.availabilityLatch.noteAvailable();
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
const verdict = this.madgeAvailability.getVerdict(cwd);
|
|
353
|
+
this.availabilityLatch.noteUnavailable(verdict.outcome ?? "missing", verdict.cause ?? "not-found");
|
|
354
|
+
return false;
|
|
343
355
|
}
|
|
344
356
|
/**
|
|
345
357
|
* Check if a file is part of a circular dependency (from cache)
|
|
@@ -278,7 +278,7 @@ export function _resetStateCacheForTests() {
|
|
|
278
278
|
* Neither can throw into the mark path; both are already internally
|
|
279
279
|
* fail-safe, but the try/catch keeps a future regression in either from
|
|
280
280
|
* breaking a mark. */
|
|
281
|
-
function emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing) {
|
|
281
|
+
function emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing, identity) {
|
|
282
282
|
try {
|
|
283
283
|
logDispositionEvent({
|
|
284
284
|
event: "mark",
|
|
@@ -290,6 +290,8 @@ function emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing) {
|
|
|
290
290
|
reason,
|
|
291
291
|
anchor,
|
|
292
292
|
previousDisposition: existing?.disposition,
|
|
293
|
+
model: identity?.model || undefined,
|
|
294
|
+
provider: identity?.provider || undefined,
|
|
293
295
|
});
|
|
294
296
|
publishDisposition({
|
|
295
297
|
cwd,
|
|
@@ -317,7 +319,7 @@ function emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing) {
|
|
|
317
319
|
* hang off it, so the agent tool and any future UI caller are covered without
|
|
318
320
|
* per-caller wiring.
|
|
319
321
|
*/
|
|
320
|
-
export function markDisposition(cwd, target, disposition, reason) {
|
|
322
|
+
export function markDisposition(cwd, target, disposition, reason, identity) {
|
|
321
323
|
const anchor = disposition === "false-positive"
|
|
322
324
|
? computeStrictAnchor(target)
|
|
323
325
|
: computeWeakAnchor(target);
|
|
@@ -327,7 +329,7 @@ export function markDisposition(cwd, target, disposition, reason) {
|
|
|
327
329
|
const existing = readState(cwd).dispositions?.[anchor];
|
|
328
330
|
if (disposition === "defer") {
|
|
329
331
|
deferredThisSession.add(anchor);
|
|
330
|
-
emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing);
|
|
332
|
+
emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing, identity);
|
|
331
333
|
return anchor;
|
|
332
334
|
}
|
|
333
335
|
const now = new Date().toISOString();
|
|
@@ -344,7 +346,7 @@ export function markDisposition(cwd, target, disposition, reason) {
|
|
|
344
346
|
lineText,
|
|
345
347
|
};
|
|
346
348
|
commitDisposition(cwd, anchor, entry);
|
|
347
|
-
emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing);
|
|
349
|
+
emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing, identity);
|
|
348
350
|
return anchor;
|
|
349
351
|
}
|
|
350
352
|
export function getDisposition(cwd, anchor) {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
*/
|
|
62
62
|
import { logBusEvent } from "./bus-events-logger.js";
|
|
63
63
|
import { normalizeFilePath } from "./path-utils.js";
|
|
64
|
-
import { createLiveBusEmitter, recordStaleBusFailure, } from "./live-bus-emitter.js";
|
|
64
|
+
import { createLiveBusEmitter, recordStaleBusFailure, resolveLiveBusEmitter, } from "./live-bus-emitter.js";
|
|
65
65
|
import { isBusPublishEnabled } from "./bus-publish.js";
|
|
66
66
|
export const BUS_DIAGNOSTICS_EVENT = "pilens:diagnostics";
|
|
67
67
|
export const BUS_DIAGNOSTICS_VERSION = 1;
|
|
@@ -149,8 +149,13 @@ export function publishDiagnostics(args) {
|
|
|
149
149
|
}
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
|
-
const
|
|
153
|
-
|
|
152
|
+
const resolution = resolveLiveBusEmitter(liveEmitter, () => ({
|
|
153
|
+
event: BUS_DIAGNOSTICS_EVENT,
|
|
154
|
+
cwd: normalizeFilePath(args.cwd),
|
|
155
|
+
}));
|
|
156
|
+
if (resolution.outcome === "stale-session")
|
|
157
|
+
return;
|
|
158
|
+
if (resolution.outcome === "unwired") {
|
|
154
159
|
if (!hasLoggedUnwired) {
|
|
155
160
|
hasLoggedUnwired = true;
|
|
156
161
|
logBusEvent({
|
|
@@ -161,6 +166,7 @@ export function publishDiagnostics(args) {
|
|
|
161
166
|
}
|
|
162
167
|
return;
|
|
163
168
|
}
|
|
169
|
+
const busEmit = resolution.emit;
|
|
164
170
|
try {
|
|
165
171
|
const fileEntries = args.files.map((f) => {
|
|
166
172
|
const normPath = normalizeFilePath(f.path);
|
|
@@ -204,6 +210,7 @@ export function publishDiagnostics(args) {
|
|
|
204
210
|
outcome: "emit_failed",
|
|
205
211
|
cwd: normalizeFilePath(args.cwd),
|
|
206
212
|
error: String(err),
|
|
213
|
+
ctxSource: resolution.ctxSource,
|
|
207
214
|
});
|
|
208
215
|
if (!hasLoggedFailure) {
|
|
209
216
|
hasLoggedFailure = true;
|
|
@@ -29,6 +29,7 @@ import { loadPiLensProjectConfig } from "../project-lens-config.js";
|
|
|
29
29
|
import { RUNTIME_CONFIG, getRunnerTimeoutFloorMs } from "../runtime-config.js";
|
|
30
30
|
import { safeSpawnAsync } from "../safe-spawn.js";
|
|
31
31
|
import { classifyDiagnostic } from "./diagnostic-taxonomy.js";
|
|
32
|
+
import { classifyProbeFailure, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./runners/utils/availability-policy.js";
|
|
32
33
|
import { applyDispositions } from "../diagnostic-dispositions.js";
|
|
33
34
|
import { applyInlineSuppressions } from "./inline-suppressions.js";
|
|
34
35
|
import { getToolPlan } from "./plan.js";
|
|
@@ -76,12 +77,49 @@ export function normalizeCacheKey(cmd) {
|
|
|
76
77
|
const normalized = cmd.replace(/\.(cmd|exe)$/i, "").toLowerCase();
|
|
77
78
|
return `session.toolCache.${normalized}`;
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
/** Probe budget for the generic `hasTool` availability check, ms. */
|
|
81
|
+
const TOOL_PROBE_TIMEOUT_MS = 5000;
|
|
82
|
+
/**
|
|
83
|
+
* Session fact holding the epoch ms after which a TRANSIENT verdict for a
|
|
84
|
+
* command may be re-probed. Kept beside the boolean fact so both share the
|
|
85
|
+
* session's lifetime — no second global to reset (#1476).
|
|
86
|
+
*/
|
|
87
|
+
function transientRetryKey(command) {
|
|
88
|
+
return `${normalizeCacheKey(command)}.retryAt`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Consecutive transient verdicts for a command, so the cooldown ESCALATES the
|
|
92
|
+
* way the policy documents (30 s, 60 s, 120 s … capped at 5 min) instead of
|
|
93
|
+
* sitting flat at 30 s.
|
|
94
|
+
*
|
|
95
|
+
* This is the highest-traffic availability consumer in the product. A flat
|
|
96
|
+
* cooldown here means a permanently sick host is re-probed every 30 s per
|
|
97
|
+
* command for the whole session — ten times the storm the policy claims to
|
|
98
|
+
* bound, and the one place the bound matters most. `createAvailabilityChecker`
|
|
99
|
+
* tracks the same counter; this keeps the two seams telling one story.
|
|
100
|
+
*/
|
|
101
|
+
function transientAttemptsKey(command) {
|
|
102
|
+
return `${normalizeCacheKey(command)}.transientAttempts`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Is `command` usable right now? Cached per session.
|
|
106
|
+
*
|
|
107
|
+
* Latch policy (#1467/#1476): a `false` from a genuine absence is durable and
|
|
108
|
+
* is remembered for the session. A `false` from a TIMED-OUT probe is not — this
|
|
109
|
+
* is the highest-traffic availability consumer in the codebase, and caching a
|
|
110
|
+
* host stall here silently disabled a healthy tool for every later dispatch in
|
|
111
|
+
* the session. A transient verdict instead holds only for a bounded cooldown,
|
|
112
|
+
* which also stops a sick host from being re-probed on every dispatch.
|
|
113
|
+
*/
|
|
114
|
+
export async function checkToolAvailability(command, facts) {
|
|
80
115
|
const key = normalizeCacheKey(command);
|
|
81
116
|
const cached = facts.getSessionFact(key);
|
|
82
117
|
if (cached !== undefined) {
|
|
83
118
|
return cached;
|
|
84
119
|
}
|
|
120
|
+
const retryAt = facts.getSessionFact(transientRetryKey(command));
|
|
121
|
+
if (retryAt !== undefined && Date.now() < retryAt)
|
|
122
|
+
return false;
|
|
85
123
|
// A command that isn't even on disk can't pass a --version probe; the ~μs
|
|
86
124
|
// stat/PATH walk saves a guaranteed-to-fail spawn round-trip per cold tool.
|
|
87
125
|
if (!(await isSpawnableCommand(command))) {
|
|
@@ -89,12 +127,68 @@ async function checkToolAvailability(command, facts) {
|
|
|
89
127
|
return false;
|
|
90
128
|
}
|
|
91
129
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
130
|
+
// The budget is enforced by a HOST-side timer, so measure the loop stall
|
|
131
|
+
// that overlapped the window and let the shared classifier read it.
|
|
132
|
+
const sampler = startHostStallSampler();
|
|
133
|
+
const startedAt = Date.now();
|
|
134
|
+
let result;
|
|
135
|
+
let hostStallMs;
|
|
136
|
+
try {
|
|
137
|
+
result = await safeSpawnAsync(command, ["--version"], {
|
|
138
|
+
timeout: TOOL_PROBE_TIMEOUT_MS,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
hostStallMs = sampler.stop();
|
|
143
|
+
}
|
|
144
|
+
const elapsedMs = Date.now() - startedAt;
|
|
145
|
+
if (result.status === 0) {
|
|
146
|
+
facts.setSessionFact(key, true);
|
|
147
|
+
// The tool answered: retire the cooldown facts rather than leaving a
|
|
148
|
+
// stale retry deadline behind a `true`.
|
|
149
|
+
facts.setSessionFact(transientRetryKey(command), 0);
|
|
150
|
+
facts.setSessionFact(transientAttemptsKey(command), 0);
|
|
151
|
+
logAvailabilityDecision({
|
|
152
|
+
tool: command,
|
|
153
|
+
verdict: "available",
|
|
154
|
+
outcome: "success",
|
|
155
|
+
cause: "ok",
|
|
156
|
+
elapsedMs,
|
|
157
|
+
latched: true,
|
|
158
|
+
hostStallMs,
|
|
159
|
+
budgetMs: TOOL_PROBE_TIMEOUT_MS,
|
|
160
|
+
});
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
// `missing` for anything unclassified preserves the pre-#1476 meaning of
|
|
164
|
+
// a non-zero exit: durable, cached for the session.
|
|
165
|
+
const { outcome, cause } = classifyProbeFailure(result, {
|
|
166
|
+
hostStallMs,
|
|
167
|
+
unclassifiedFailureOutcome: "missing",
|
|
94
168
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
169
|
+
let retryAfterMs;
|
|
170
|
+
if (outcome === "transient") {
|
|
171
|
+
const attempts = (facts.getSessionFact(transientAttemptsKey(command)) ?? 0) + 1;
|
|
172
|
+
facts.setSessionFact(transientAttemptsKey(command), attempts);
|
|
173
|
+
retryAfterMs = transientRetryDelayMs(attempts, cause);
|
|
174
|
+
facts.setSessionFact(transientRetryKey(command), Date.now() + retryAfterMs);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
facts.setSessionFact(key, false);
|
|
178
|
+
facts.setSessionFact(transientAttemptsKey(command), 0);
|
|
179
|
+
}
|
|
180
|
+
logAvailabilityDecision({
|
|
181
|
+
tool: command,
|
|
182
|
+
verdict: "unavailable",
|
|
183
|
+
outcome,
|
|
184
|
+
cause,
|
|
185
|
+
elapsedMs,
|
|
186
|
+
latched: outcome !== "transient",
|
|
187
|
+
hostStallMs,
|
|
188
|
+
...(retryAfterMs !== undefined && { retryAfterMs }),
|
|
189
|
+
budgetMs: TOOL_PROBE_TIMEOUT_MS,
|
|
190
|
+
});
|
|
191
|
+
return false;
|
|
98
192
|
}
|
|
99
193
|
catch {
|
|
100
194
|
facts.setSessionFact(key, false);
|
|
@@ -128,7 +222,10 @@ export function createDispatchContext(filePath, cwd, pi, facts, blockingOnly, mo
|
|
|
128
222
|
/** Authoritative workspace root; `cwd` may be a nested language root. */
|
|
129
223
|
projectRoot,
|
|
130
224
|
/** Ordered per-file pipeline token, when this is a post-write dispatch. */
|
|
131
|
-
writeIndex
|
|
225
|
+
writeIndex,
|
|
226
|
+
/** Runtime telemetry identity, when known (#1448) — threaded to the
|
|
227
|
+
* worklog append; see DispatchContext.telemetryModel's doc. */
|
|
228
|
+
telemetryModel, telemetryProvider) {
|
|
132
229
|
const absoluteFilePath = resolveRunnerPath(cwd, filePath);
|
|
133
230
|
const normalizedProjectRoot = normalizeMapKey(path.resolve(projectRoot ?? cwd));
|
|
134
231
|
const normalizedCwd = normalizeMapKey(resolveLanguageRootForFile(absoluteFilePath, cwd));
|
|
@@ -150,6 +247,8 @@ writeIndex) {
|
|
|
150
247
|
blockingOnly,
|
|
151
248
|
modifiedRanges,
|
|
152
249
|
writeIndex,
|
|
250
|
+
telemetryModel,
|
|
251
|
+
telemetryProvider,
|
|
153
252
|
async hasTool(command) {
|
|
154
253
|
return checkToolAvailability(command, facts);
|
|
155
254
|
},
|
|
@@ -638,11 +737,16 @@ export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
|
|
|
638
737
|
const blockers = visibleDiagnostics.filter((d) => d.semantic === "blocking");
|
|
639
738
|
const warnings = visibleDiagnostics.filter((d) => d.semantic === "warning" || d.semantic === "none");
|
|
640
739
|
const fixedItems = visibleDiagnostics.filter((d) => d.semantic === "fixed");
|
|
641
|
-
// Append fixed and fixable diagnostics to the persistent worklog
|
|
740
|
+
// Append fixed and fixable diagnostics to the persistent worklog, attributed
|
|
741
|
+
// to the runtime's active model/provider when known (#1448).
|
|
742
|
+
const worklogIdentity = {
|
|
743
|
+
model: ctx.telemetryModel,
|
|
744
|
+
provider: ctx.telemetryProvider,
|
|
745
|
+
};
|
|
642
746
|
if (fixedItems.length > 0) {
|
|
643
747
|
import("../fix-worklog.js")
|
|
644
748
|
.then(({ appendToWorklog }) => {
|
|
645
|
-
appendToWorklog(ctx.cwd, fixedItems, true);
|
|
749
|
+
appendToWorklog(ctx.cwd, fixedItems, true, worklogIdentity);
|
|
646
750
|
})
|
|
647
751
|
.catch(() => { });
|
|
648
752
|
}
|
|
@@ -650,7 +754,7 @@ export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
|
|
|
650
754
|
if (fixableWarnings.length > 0) {
|
|
651
755
|
import("../fix-worklog.js")
|
|
652
756
|
.then(({ appendToWorklog }) => {
|
|
653
|
-
appendToWorklog(ctx.cwd, fixableWarnings, false);
|
|
757
|
+
appendToWorklog(ctx.cwd, fixableWarnings, false, worklogIdentity);
|
|
654
758
|
})
|
|
655
759
|
.catch(() => { });
|
|
656
760
|
}
|
|
@@ -19,9 +19,8 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
import { formatCascadeNeighborDiagnostics } from "../cascade-format.js";
|
|
20
20
|
import { logCascade } from "../cascade-logger.js";
|
|
21
21
|
import { getDiagnosticTracker } from "../diagnostic-tracker.js";
|
|
22
|
-
import { isTierAwareCascadeEnabled, recordOutstandingCascadeTouch, } from "../lsp/cascade-tier.js";
|
|
23
|
-
import {
|
|
24
|
-
import { bindingStateLabel, } from "../lsp/diagnostic-binding.js";
|
|
22
|
+
import { classifyCascadeWaitTier, isTierAwareCascadeEnabled, recordOutstandingCascadeTouch, } from "../lsp/cascade-tier.js";
|
|
23
|
+
import { bindingStateLabel, touchCoverageGap, } from "../lsp/diagnostic-binding.js";
|
|
25
24
|
import { getServersForFileWithConfig } from "../lsp/config.js";
|
|
26
25
|
import { getLSPService } from "../lsp/index.js";
|
|
27
26
|
import { isExternalOrVendorFile, normalizeMapKey } from "../path-utils.js";
|
|
@@ -412,6 +411,11 @@ function readBoundToCurrentDisk(rawDiags) {
|
|
|
412
411
|
* a future flag cannot be silently missed at just one of the several gate sites:
|
|
413
412
|
* - `inconclusive` (#1093/#571): the notify/diagnostics wait lapsed its deadline,
|
|
414
413
|
* so a resolved `[]` is NOT a confirmed clean (the #533 false-clean trap).
|
|
414
|
+
* - a COVERAGE GAP (#1470): an auxiliary's push wait was cut off by the aux grace
|
|
415
|
+
* timer, so the merged result is missing whatever that scanner would have said.
|
|
416
|
+
* Such a touch is deliberately NOT `inconclusive` (the primary answered), which
|
|
417
|
+
* is exactly why it needs naming here: reading `!inconclusive` alone would let a
|
|
418
|
+
* hung opengrep wipe a live footer finding and seed the recently-clean cache.
|
|
415
419
|
* - `binding.boundToCurrentDisk === false` (#1095): the diagnostics were computed
|
|
416
420
|
* against a DIFFERENT disk state than what is on disk now (the server's view
|
|
417
421
|
* diverged / a pre-fix buffer) — not an observation of current disk. `true` and
|
|
@@ -426,7 +430,9 @@ function readInconclusive(rawDiags) {
|
|
|
426
430
|
return rawDiags?.inconclusive === true;
|
|
427
431
|
}
|
|
428
432
|
function isConfirmedTouch(rawDiags) {
|
|
429
|
-
return !readInconclusive(rawDiags) &&
|
|
433
|
+
return (!readInconclusive(rawDiags) &&
|
|
434
|
+
touchCoverageGap(rawDiags).length === 0 &&
|
|
435
|
+
readBoundToCurrentDisk(rawDiags) !== false);
|
|
430
436
|
}
|
|
431
437
|
const reverseDepsIndexCache = new Map();
|
|
432
438
|
const REVERSE_DEPS_MAX_WARM_ROOTS = 8;
|
|
@@ -666,6 +672,12 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
666
672
|
let importerSet = new Set();
|
|
667
673
|
let callerSet = new Set();
|
|
668
674
|
let referenceCount = 0;
|
|
675
|
+
// #1446 item 4: how many eligible neighbors the flat CASCADE_NEIGHBOUR_BUDGET
|
|
676
|
+
// cut off, distinct from candidates dropped by the filters above it
|
|
677
|
+
// (missing on disk, vendor, ignored, already-primary-this-turn) — those are
|
|
678
|
+
// never actionable regardless of budget, so counting them as "truncated"
|
|
679
|
+
// would overstate what a larger budget could actually recover.
|
|
680
|
+
let cascadeBudgetTruncated = 0;
|
|
669
681
|
if (CASCADE_GRAPH_KINDS.has(fileKind)) {
|
|
670
682
|
const graphStart = Date.now();
|
|
671
683
|
const graph = await buildOrUpdateGraph(cwd, [normalizedFile], sessionFacts, seqState);
|
|
@@ -1004,7 +1016,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1004
1016
|
...impact.directCallers,
|
|
1005
1017
|
]);
|
|
1006
1018
|
referenceCount = impact.neighborFiles.filter((n) => !importerOrCallerSet.has(n)).length;
|
|
1007
|
-
|
|
1019
|
+
const eligibleNeighbors = [...impact.neighborFiles]
|
|
1008
1020
|
.filter((n) => nodeFs.existsSync(n))
|
|
1009
1021
|
.filter((n) => !isExternalOrVendorFile(n, cwd))
|
|
1010
1022
|
// Honour the project's ignore config: a user-ignored neighbour (e.g.
|
|
@@ -1016,8 +1028,9 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1016
1028
|
.sort((a, b) => {
|
|
1017
1029
|
const rank = (p) => importerSet.has(p) ? 0 : callerSet.has(p) ? 1 : 2;
|
|
1018
1030
|
return rank(a) - rank(b);
|
|
1019
|
-
})
|
|
1020
|
-
|
|
1031
|
+
});
|
|
1032
|
+
cascadeBudgetTruncated = Math.max(0, eligibleNeighbors.length - CASCADE_NEIGHBOUR_BUDGET);
|
|
1033
|
+
sortedNeighbors = eligibleNeighbors.slice(0, CASCADE_NEIGHBOUR_BUDGET);
|
|
1021
1034
|
}
|
|
1022
1035
|
else {
|
|
1023
1036
|
logCascade({
|
|
@@ -1066,6 +1079,28 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1066
1079
|
// the latter must not collapse into a clean-looking result (#1104 honesty
|
|
1067
1080
|
// rule, same doctrine as #1023's graph-degraded indeterminate marker).
|
|
1068
1081
|
let fallbackBindingRejected = false;
|
|
1082
|
+
// #1444: neighbours whose in-lane wait was skipped for the quiet-window
|
|
1083
|
+
// reconcile to answer later. Logged on `cascade_result` so a cascade that
|
|
1084
|
+
// deferred EVERY neighbour is distinguishable from a genuine leaf (both are
|
|
1085
|
+
// `neighborCount: 0` with no output otherwise).
|
|
1086
|
+
let collectLaterSkipped = 0;
|
|
1087
|
+
// #1446 item 5: `recentlyCleanNeighborCache` and `neighborTouchCache` hits
|
|
1088
|
+
// are the whole point of both caches, but neither was ever counted — the
|
|
1089
|
+
// only visible signal was 267s/day of touch wall time with no way to tell
|
|
1090
|
+
// whether the caches were absorbing repeat work or every touch was cold.
|
|
1091
|
+
let recentlyCleanHits = 0;
|
|
1092
|
+
let cacheHits = 0;
|
|
1093
|
+
// F1 (#1446 follow-up): `coldTouches` must be counted at the point each
|
|
1094
|
+
// neighbour's OUTCOME is actually known, not derived from `coldSnapshotPaths`
|
|
1095
|
+
// (finalized earlier, before the cache-hit checks below run against it). Using
|
|
1096
|
+
// the pre-outcome list let a neighbour double-count (cold-snapshot AND cache/
|
|
1097
|
+
// recently-clean hit) or vanish from every bucket (an `activePaths` neighbour —
|
|
1098
|
+
// e.g. Python/Go — that misses both caches). These four counters partition the
|
|
1099
|
+
// touched-neighbour set `[...activePaths, ...coldSnapshotPaths]` exactly once
|
|
1100
|
+
// each: a neighbour with no LSP server configured is the only outcome
|
|
1101
|
+
// deliberately excluded (never attempted, no bucket).
|
|
1102
|
+
let deferredTouches = 0;
|
|
1103
|
+
let coldTouches = 0;
|
|
1069
1104
|
if (sortedNeighbors.length > 0) {
|
|
1070
1105
|
const snapshotPaths = sortedNeighbors.filter(shouldReadCascadeFromSnapshot);
|
|
1071
1106
|
const activePaths = sortedNeighbors.filter((n) => !shouldReadCascadeFromSnapshot(n));
|
|
@@ -1186,6 +1221,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1186
1221
|
turnSeq - recentlyClean.turnSeq <= RECENTLY_CLEAN_TTL_TURNS &&
|
|
1187
1222
|
!hasFreshPassiveErrors) {
|
|
1188
1223
|
producedLspData = true;
|
|
1224
|
+
recentlyCleanHits++;
|
|
1189
1225
|
const durationMs = Date.now() - neighborStart;
|
|
1190
1226
|
logCascade({
|
|
1191
1227
|
phase: "neighbor_snapshot",
|
|
@@ -1213,6 +1249,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1213
1249
|
const cached = writeSeq != null ? neighborTouchCache.get(cacheKey) : undefined;
|
|
1214
1250
|
if (cached?.turnSeq === turnSeq && cached?.writeSeq === writeSeq) {
|
|
1215
1251
|
producedLspData = true;
|
|
1252
|
+
cacheHits++;
|
|
1216
1253
|
const durationMs = Date.now() - neighborStart;
|
|
1217
1254
|
logCascade({
|
|
1218
1255
|
phase: "neighbor_snapshot",
|
|
@@ -1245,10 +1282,10 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1245
1282
|
}
|
|
1246
1283
|
// A6: async read to avoid blocking event loop on network-mounted drives
|
|
1247
1284
|
const content = await nodeFs.promises.readFile(neighborPath, "utf8");
|
|
1248
|
-
// #458: tier-aware cascade-lane wait. A Tier-3
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1285
|
+
// #458/#1444: tier-aware cascade-lane wait. A Tier-3 silent server
|
|
1286
|
+
// cannot give this wait an affirmative clean signal. Native TS7 does
|
|
1287
|
+
// publish, but not inside the cold-snapshot budget. In both cases the
|
|
1288
|
+
// in-lane budget is pure cost. Fire the touch (didOpen/
|
|
1252
1289
|
// didChange still happens — the server starts real work) and record
|
|
1253
1290
|
// it as outstanding for the agent_settled quiet window to reconcile
|
|
1254
1291
|
// instead of waiting here. Ambiguous/missing capability data always
|
|
@@ -1261,7 +1298,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1261
1298
|
try {
|
|
1262
1299
|
const snapshots = (await lspService.getCapabilitySnapshots?.(neighborPath)) ?? [];
|
|
1263
1300
|
const tier = classifyCascadeWaitTier(lspService, neighborPath, snapshots);
|
|
1264
|
-
if (tier === "tier3-silent") {
|
|
1301
|
+
if (tier === "tier3-silent" || tier === "collect-later") {
|
|
1265
1302
|
const spawnedForTouch = await lspService.getClientForFile(neighborPath);
|
|
1266
1303
|
if (spawnedForTouch) {
|
|
1267
1304
|
// Sampled BEFORE the touchFile notify: a publish landing
|
|
@@ -1283,6 +1320,14 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1283
1320
|
touchedAt,
|
|
1284
1321
|
});
|
|
1285
1322
|
const durationMs = Date.now() - neighborStart;
|
|
1323
|
+
if (tier === "collect-later")
|
|
1324
|
+
collectLaterSkipped++;
|
|
1325
|
+
// F1: both tier3-silent and collect-later skip the in-lane
|
|
1326
|
+
// wait and record an outstanding touch for the quiet-window
|
|
1327
|
+
// reconcile — neither a cache hit nor a genuine completed
|
|
1328
|
+
// cold touch, so both share this explicit "deferred" bucket
|
|
1329
|
+
// instead of falling out of the partition uncounted.
|
|
1330
|
+
deferredTouches++;
|
|
1286
1331
|
logCascade({
|
|
1287
1332
|
phase: "cascade_tier3_skip",
|
|
1288
1333
|
filePath,
|
|
@@ -1290,7 +1335,10 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1290
1335
|
durationMs,
|
|
1291
1336
|
lspServerCount: configuredServerCount,
|
|
1292
1337
|
coldSnapshot: isColdSnapshot,
|
|
1293
|
-
metadata: {
|
|
1338
|
+
metadata: {
|
|
1339
|
+
serverId: spawnedForTouch.client.serverId,
|
|
1340
|
+
waitTier: tier,
|
|
1341
|
+
},
|
|
1294
1342
|
});
|
|
1295
1343
|
// Deliberately NOT cached as clean/diagnosed — the wait was
|
|
1296
1344
|
// skipped, not resolved, so neither neighborTouchCache nor
|
|
@@ -1311,6 +1359,14 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1311
1359
|
// Cold-snapshot neighbors (autoPropagate LSP, server warm) use a tighter
|
|
1312
1360
|
// 1000ms budget — they should respond quickly; we'd rather return zero
|
|
1313
1361
|
// than block cascade for 2s on a slow open.
|
|
1362
|
+
// F1: this is the ONE remaining outcome after cache hit, recently-clean
|
|
1363
|
+
// hit, and tier-aware deferral have all been ruled out — a genuine
|
|
1364
|
+
// active LSP touch is being issued right now. Count it here (an
|
|
1365
|
+
// attempt, whether it resolves, times out, or the promise rejects
|
|
1366
|
+
// below in the allSettled catch) rather than from `coldSnapshotPaths`,
|
|
1367
|
+
// which is finalized before any of the above checks run and includes
|
|
1368
|
+
// neighbours that resolve via cache/recently-clean instead.
|
|
1369
|
+
coldTouches++;
|
|
1314
1370
|
const rawDiags = await lspService.touchFile(neighborPath, content, {
|
|
1315
1371
|
diagnostics: "document",
|
|
1316
1372
|
collectDiagnostics: true,
|
|
@@ -1336,6 +1392,10 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1336
1392
|
const confirmed = isConfirmedTouch(rawDiags);
|
|
1337
1393
|
const bindingRejected = readBoundToCurrentDisk(rawDiags) === false;
|
|
1338
1394
|
const inconclusive = readInconclusive(rawDiags);
|
|
1395
|
+
// #1470: the third, independent reason a touch is unconfirmed — an
|
|
1396
|
+
// auxiliary our grace timer cut off. Logged alongside the other two so
|
|
1397
|
+
// cascade.log alone still tells the three apart.
|
|
1398
|
+
const unconfirmedServerIds = touchCoverageGap(rawDiags);
|
|
1339
1399
|
// #692: `source: "cascade"` no longer overrides `rule` (see the
|
|
1340
1400
|
// doc comment on the sibling call above) — dropped rather than
|
|
1341
1401
|
// migrated to `scanOrigin` since cascade output never touches
|
|
@@ -1345,8 +1405,9 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1345
1405
|
// wrapper and are unaffected by this copy (the shape-5 fix).
|
|
1346
1406
|
const diags = convertLspDiagnostics(rawDiags.diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), neighborPath);
|
|
1347
1407
|
const durationMs = Date.now() - neighborStart;
|
|
1348
|
-
//
|
|
1349
|
-
|
|
1408
|
+
// Cache only a confirmed answer. An inconclusive or binding-rejected
|
|
1409
|
+
// result must not become a confirmed cache hit on the next cascade.
|
|
1410
|
+
if (writeSeq != null && confirmed) {
|
|
1350
1411
|
neighborTouchCache.set(cacheKey, {
|
|
1351
1412
|
turnSeq,
|
|
1352
1413
|
writeSeq,
|
|
@@ -1386,6 +1447,9 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1386
1447
|
metadata: {
|
|
1387
1448
|
inconclusive,
|
|
1388
1449
|
...(bindingRejected && { bindingState: bindingStateLabel(false) }),
|
|
1450
|
+
...(unconfirmedServerIds.length > 0 && {
|
|
1451
|
+
unconfirmedServerIds: [...unconfirmedServerIds],
|
|
1452
|
+
}),
|
|
1389
1453
|
},
|
|
1390
1454
|
});
|
|
1391
1455
|
// #1093/#1095: a completed, CONFIRMED active touch is a confirmed
|
|
@@ -1411,6 +1475,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1411
1475
|
reason: neighborReason(importerSet, callerSet, neighborPath),
|
|
1412
1476
|
diagnostics: diags,
|
|
1413
1477
|
lspTouched: true,
|
|
1478
|
+
...(inconclusive && { inconclusive: true }),
|
|
1414
1479
|
durationMs,
|
|
1415
1480
|
};
|
|
1416
1481
|
}));
|
|
@@ -1513,9 +1578,29 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1513
1578
|
metadata: {
|
|
1514
1579
|
filesWithErrors,
|
|
1515
1580
|
hasOutput: formatted.length > 0,
|
|
1581
|
+
// #1444: >0 means "answers are still outstanding", not "nothing found".
|
|
1582
|
+
collectLaterSkipped,
|
|
1516
1583
|
// Log when cascade ran but found nothing — distinguishes "clean" from "no signal"
|
|
1517
1584
|
noNeighbors: visibleNeighbors.length === 0,
|
|
1518
1585
|
noErrors: visibleNeighbors.length > 0 && filesWithErrors === 0,
|
|
1586
|
+
// #1446 item 5: cache effectiveness as a number instead of an inference
|
|
1587
|
+
// from `coldSnapshot`/`snapshotMissing` flags scattered across
|
|
1588
|
+
// per-neighbor `neighbor_touch`/`neighbor_snapshot` rows.
|
|
1589
|
+
// F1: cacheHits + recentlyCleanHits + deferredTouches + coldTouches
|
|
1590
|
+
// partition `[...activePaths, ...coldSnapshotPaths]` exactly — each
|
|
1591
|
+
// counter increments at the point its neighbour's outcome is actually
|
|
1592
|
+
// decided, not from `coldSnapshotPaths` (a pre-outcome list finalized
|
|
1593
|
+
// before the cache-hit checks run). A neighbour with no LSP server
|
|
1594
|
+
// configured is the one deliberately uncounted outcome (never touched).
|
|
1595
|
+
cacheHits,
|
|
1596
|
+
recentlyCleanHits,
|
|
1597
|
+
deferredTouches,
|
|
1598
|
+
coldTouches,
|
|
1599
|
+
// #1446 item 4: the budget in force and how many eligible candidates
|
|
1600
|
+
// it cut off this run — the correctness half (a truncated run being
|
|
1601
|
+
// silently discarded) is #1443; this is observability only.
|
|
1602
|
+
neighborBudget: CASCADE_NEIGHBOUR_BUDGET,
|
|
1603
|
+
budgetTruncated: cascadeBudgetTruncated,
|
|
1519
1604
|
neighbors: visibleNeighbors.slice(0, 10).map((n) => ({
|
|
1520
1605
|
file: n.filePath.replace(/\\/g, "/").split("/").slice(-2).join("/"),
|
|
1521
1606
|
diagnostics: n.diagnostics.length,
|
|
@@ -1742,7 +1827,7 @@ export async function dispatchLintWithResult(filePath, cwd, pi, modifiedRanges,
|
|
|
1742
1827
|
// Default true preserves the per-edit fast path (errors only). Callers that
|
|
1743
1828
|
// want the full picture (warnings + structural smells), e.g. the MCP review
|
|
1744
1829
|
// facade, pass blockingOnly=false to run every runner.
|
|
1745
|
-
const ctx = createDispatchContext(filePath, cwd, pi, sessionFacts, options?.blockingOnly ?? true, modifiedRanges, options?.projectRoot, options?.writeIndex);
|
|
1830
|
+
const ctx = createDispatchContext(filePath, cwd, pi, sessionFacts, options?.blockingOnly ?? true, modifiedRanges, options?.projectRoot, options?.writeIndex, options?.telemetryModel, options?.telemetryProvider);
|
|
1746
1831
|
sessionFacts.clearFileFactsFor(ctx.filePath);
|
|
1747
1832
|
const kind = ctx.kind;
|
|
1748
1833
|
if (!kind) {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* unified runner that delegates to the LSP service.
|
|
13
13
|
*/
|
|
14
14
|
import { logExtension } from "../../extension-log.js";
|
|
15
|
+
import { touchCoverageGap } from "../../lsp/diagnostic-binding.js";
|
|
15
16
|
import { getLSPService } from "../../lsp/index.js";
|
|
16
17
|
import { RUNTIME_CONFIG } from "../../runtime-config.js";
|
|
17
18
|
import { PRIORITY } from "../priorities.js";
|
|
@@ -111,6 +112,11 @@ const lspRunner = {
|
|
|
111
112
|
// one spawned server) — an empty `lspDiags` in that case is NOT a
|
|
112
113
|
// confirmed clean result and must not be reported as one (#570).
|
|
113
114
|
let diagnosticsInconclusive = false;
|
|
115
|
+
// #1470: server ids the touch carries no evidence for — an auxiliary whose
|
|
116
|
+
// push wait our aux grace timer cut off. The touch is NOT inconclusive (the
|
|
117
|
+
// primary answered, and its findings below are real), so this is tracked
|
|
118
|
+
// separately: the only claim it invalidates is "0 diagnostics means clean".
|
|
119
|
+
let unconfirmedServerIds = [];
|
|
114
120
|
let usedWarmAttach = false;
|
|
115
121
|
let failureReason = "";
|
|
116
122
|
const content = readFileContent(ctx.filePath);
|
|
@@ -131,11 +137,23 @@ const lspRunner = {
|
|
|
131
137
|
usedWarmAttach = attached?.available === true;
|
|
132
138
|
// #1179 (shape-5 structural fix): both branches normalize to the
|
|
133
139
|
// `touchFile` wrapper shape. The warm-attach IPC branch resolves a plain
|
|
134
|
-
// diagnostics array
|
|
135
|
-
//
|
|
136
|
-
//
|
|
140
|
+
// diagnostics array — `available` no longer implies a fully confirmed
|
|
141
|
+
// answer: a `partial` confirmation (an auxiliary cut off by the grace
|
|
142
|
+
// timer) is served as `available: true` too (the IPC gate at
|
|
143
|
+
// `clients/mcp/ipc.ts:248` rejects only `inconclusive`). Carry the
|
|
144
|
+
// incumbent's `unconfirmedServerIds` onto the wrapper so
|
|
145
|
+
// `touchCoverageGap` below sees it — dropping it here is the same
|
|
146
|
+
// false-clean defect already fixed at `clients/lsp/index.ts` (the
|
|
147
|
+
// workspace sweep wrapper) and `tools/lsp-diagnostics.ts` (the tool
|
|
148
|
+
// consumer); wrap it as `{ diags }`; the incumbent branch already
|
|
149
|
+
// returns the wrapper.
|
|
137
150
|
const touched = attached?.available
|
|
138
|
-
? {
|
|
151
|
+
? {
|
|
152
|
+
diags: attached.response.diagnostics,
|
|
153
|
+
...(attached.response.unconfirmedServerIds !== undefined && {
|
|
154
|
+
unconfirmedServerIds: attached.response.unconfirmedServerIds,
|
|
155
|
+
}),
|
|
156
|
+
}
|
|
139
157
|
: await lspService.touchFile(ctx.filePath, content, {
|
|
140
158
|
diagnostics: "document",
|
|
141
159
|
collectDiagnostics: true,
|
|
@@ -151,6 +169,7 @@ const lspRunner = {
|
|
|
151
169
|
else {
|
|
152
170
|
lspDiags = touched.diags;
|
|
153
171
|
diagnosticsInconclusive = touched.inconclusive === true;
|
|
172
|
+
unconfirmedServerIds = touchCoverageGap(touched);
|
|
154
173
|
}
|
|
155
174
|
}
|
|
156
175
|
catch (err) {
|
|
@@ -208,6 +227,20 @@ const lspRunner = {
|
|
|
208
227
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
209
228
|
}
|
|
210
229
|
if (lspDiags.length === 0) {
|
|
230
|
+
if (unconfirmedServerIds.length > 0) {
|
|
231
|
+
// #1470: an auxiliary was cut off by the aux grace timer, so this empty
|
|
232
|
+
// merged result is missing whatever that scanner would have said — a
|
|
233
|
+
// hung opengrep must not read as a clean bill of health on the security
|
|
234
|
+
// lane. `RunnerResult` has no channel for "clean for these servers,
|
|
235
|
+
// unknown for those", so the only honest verdict this seam can express
|
|
236
|
+
// for an EMPTY result is "not checked" — which is what "skipped" means
|
|
237
|
+
// here, and it lets the coverage notice say so once. Nothing is thrown
|
|
238
|
+
// away: the primary answered with zero findings, so there is nothing to
|
|
239
|
+
// report; when it DOES have findings the branches below still report
|
|
240
|
+
// them (see the non-empty path), which is how a trustworthy primary
|
|
241
|
+
// stays trustworthy under a cut-off auxiliary.
|
|
242
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
243
|
+
}
|
|
211
244
|
return {
|
|
212
245
|
status: "succeeded",
|
|
213
246
|
diagnostics: [],
|