@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,11 +22,14 @@ import * as path from "node:path";
|
|
|
22
22
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
23
23
|
import { assertInstallAllowed } from "./project-trust.js";
|
|
24
24
|
import { SecurityScanClient } from "./security-scan-client.js";
|
|
25
|
+
import { classifyProbeFailure, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
25
26
|
const EMPTY_RESULT = {
|
|
26
27
|
success: false,
|
|
27
28
|
findings: [],
|
|
28
29
|
};
|
|
29
30
|
const SCAN_TIMEOUT_MS = 120_000;
|
|
31
|
+
/** Budget for the `go install` auto-install, ms. */
|
|
32
|
+
const INSTALL_TIMEOUT_MS = 60_000;
|
|
30
33
|
// --- Client ---
|
|
31
34
|
export class GovulncheckClient extends SecurityScanClient {
|
|
32
35
|
constructor(verbose = false) {
|
|
@@ -54,6 +57,15 @@ export class GovulncheckClient extends SecurityScanClient {
|
|
|
54
57
|
this.available = true;
|
|
55
58
|
return true;
|
|
56
59
|
}
|
|
60
|
+
if (this.probeWasTransient()) {
|
|
61
|
+
// #1467: a timed-out/killed probe says nothing about whether
|
|
62
|
+
// govulncheck is on PATH. Latching `false` here disabled it for the
|
|
63
|
+
// life of the process; a `go install` here would be a heavyweight
|
|
64
|
+
// reaction to a host hiccup. Record an expiring verdict, retry later.
|
|
65
|
+
this.log("govulncheck probe timed out; retrying later (not installing)");
|
|
66
|
+
this.markTransientlyUnavailable(this.lastProbeCause ?? "probe-timeout");
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
57
69
|
if (!assertInstallAllowed("govulncheck go install")) {
|
|
58
70
|
// Deliberately NOT latching `available = false` (#1350 delta review):
|
|
59
71
|
// trust denial is policy, not tool absence -- a later trust grant
|
|
@@ -67,27 +79,100 @@ export class GovulncheckClient extends SecurityScanClient {
|
|
|
67
79
|
// project has a go.mod the user has the Go toolchain by definition.
|
|
68
80
|
// Same shape as rust-clippy / cargo: lean on the language's own
|
|
69
81
|
// install mechanism rather than adding a new installer strategy.
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
const goProbeStartedAt = Date.now();
|
|
83
|
+
const goSampler = startHostStallSampler();
|
|
84
|
+
let goOnPath;
|
|
85
|
+
let goHostStallMs;
|
|
86
|
+
try {
|
|
87
|
+
goOnPath = await safeSpawnAsync("go", ["version"], {
|
|
88
|
+
timeout: 5000,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
goHostStallMs = goSampler.stop();
|
|
93
|
+
}
|
|
73
94
|
if (goOnPath.error || goOnPath.status !== 0) {
|
|
95
|
+
const { outcome, cause } = classifyProbeFailure(goOnPath, {
|
|
96
|
+
hostStallMs: goHostStallMs,
|
|
97
|
+
});
|
|
98
|
+
if (outcome === "transient") {
|
|
99
|
+
this.log("`go version` probe timed out; retrying govulncheck later");
|
|
100
|
+
const retryAfterMs = this.markTransientlyUnavailable(cause);
|
|
101
|
+
logAvailabilityDecision({
|
|
102
|
+
tool: "govulncheck",
|
|
103
|
+
verdict: "unavailable",
|
|
104
|
+
outcome,
|
|
105
|
+
cause,
|
|
106
|
+
elapsedMs: Date.now() - goProbeStartedAt,
|
|
107
|
+
latched: false,
|
|
108
|
+
hostStallMs: goHostStallMs,
|
|
109
|
+
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
110
|
+
budgetMs: 5000,
|
|
111
|
+
});
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
74
114
|
this.log("go binary not on PATH — cannot auto-install govulncheck");
|
|
75
115
|
this.available = false;
|
|
76
116
|
return false;
|
|
77
117
|
}
|
|
78
118
|
this.log("govulncheck not found, attempting auto-install via go install");
|
|
79
|
-
const
|
|
119
|
+
const installStartedAt = Date.now();
|
|
120
|
+
const installSampler = startHostStallSampler();
|
|
121
|
+
let install;
|
|
122
|
+
let installHostStallMs;
|
|
123
|
+
try {
|
|
124
|
+
install = await safeSpawnAsync("go", ["install", "golang.org/x/vuln/cmd/govulncheck@latest"], { timeout: INSTALL_TIMEOUT_MS });
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
installHostStallMs = installSampler.stop();
|
|
128
|
+
}
|
|
80
129
|
if (install.error || install.status !== 0) {
|
|
81
130
|
this.log(`govulncheck auto-install failed: ${(install.stderr ?? "").slice(0, 200)}`);
|
|
131
|
+
// #1476: `go install` runs against a 60 s budget over the network. A
|
|
132
|
+
// timed-out or killed install says nothing about whether govulncheck
|
|
133
|
+
// can ever be installed here, so it must not latch the durable
|
|
134
|
+
// "tool is not installed" verdict the way a real install refusal does.
|
|
135
|
+
// A non-zero exit (module not found, compile error) still latches:
|
|
136
|
+
// that IS evidence about this machine, and retrying it every turn
|
|
137
|
+
// would be a `go install` storm.
|
|
138
|
+
const { outcome, cause } = classifyProbeFailure(install, {
|
|
139
|
+
hostStallMs: installHostStallMs,
|
|
140
|
+
});
|
|
141
|
+
if (outcome === "transient") {
|
|
142
|
+
const retryAfterMs = this.markTransientlyUnavailable(cause);
|
|
143
|
+
// One record per decision, so an install that keeps timing out is
|
|
144
|
+
// readable in latency.log the same day (#1467's forensic trail).
|
|
145
|
+
logAvailabilityDecision({
|
|
146
|
+
tool: "govulncheck",
|
|
147
|
+
verdict: "unavailable",
|
|
148
|
+
outcome,
|
|
149
|
+
cause,
|
|
150
|
+
elapsedMs: Date.now() - installStartedAt,
|
|
151
|
+
latched: false,
|
|
152
|
+
hostStallMs: installHostStallMs,
|
|
153
|
+
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
154
|
+
budgetMs: INSTALL_TIMEOUT_MS,
|
|
155
|
+
});
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
82
158
|
this.available = false;
|
|
83
159
|
return false;
|
|
84
160
|
}
|
|
85
161
|
// `go install` writes to `$GOBIN` or `$GOPATH/bin`. The user may not
|
|
86
162
|
// have that on `$PATH`. Re-probe by name (works when it is on PATH)
|
|
87
163
|
// then fall back to the canonical bin dirs.
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
164
|
+
const reprobeStartedAt = Date.now();
|
|
165
|
+
const reprobeSampler = startHostStallSampler();
|
|
166
|
+
let reprobe;
|
|
167
|
+
let reprobeHostStallMs;
|
|
168
|
+
try {
|
|
169
|
+
reprobe = await safeSpawnAsync("govulncheck", ["-version"], {
|
|
170
|
+
timeout: 5000,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
reprobeHostStallMs = reprobeSampler.stop();
|
|
175
|
+
}
|
|
91
176
|
if (!reprobe.error && reprobe.status === 0) {
|
|
92
177
|
this.log("govulncheck auto-installed and found on PATH");
|
|
93
178
|
this.available = true;
|
|
@@ -118,6 +203,30 @@ export class GovulncheckClient extends SecurityScanClient {
|
|
|
118
203
|
// fall through to next candidate
|
|
119
204
|
}
|
|
120
205
|
}
|
|
206
|
+
// The install SUCCEEDED, so the tool is on this machine somewhere. If the
|
|
207
|
+
// re-probe merely timed out, latching "not installed" would be the #1467
|
|
208
|
+
// mistake one step later in the same function (#1476).
|
|
209
|
+
const { outcome, cause } = classifyProbeFailure(reprobe, {
|
|
210
|
+
hostStallMs: reprobeHostStallMs,
|
|
211
|
+
});
|
|
212
|
+
if (outcome === "transient") {
|
|
213
|
+
this.log("govulncheck installed but the re-probe timed out; retrying later");
|
|
214
|
+
const retryAfterMs = this.markTransientlyUnavailable(cause);
|
|
215
|
+
logAvailabilityDecision({
|
|
216
|
+
tool: "govulncheck",
|
|
217
|
+
verdict: "unavailable",
|
|
218
|
+
outcome,
|
|
219
|
+
cause,
|
|
220
|
+
// The re-probe's own wall time. A hard-coded 0 here was the #1474
|
|
221
|
+
// defect verbatim: a duration field that measures nothing.
|
|
222
|
+
elapsedMs: Date.now() - reprobeStartedAt,
|
|
223
|
+
latched: false,
|
|
224
|
+
hostStallMs: reprobeHostStallMs,
|
|
225
|
+
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
226
|
+
budgetMs: 5000,
|
|
227
|
+
});
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
121
230
|
this.log("govulncheck auto-install succeeded but binary not locatable — check $GOBIN / $GOPATH");
|
|
122
231
|
this.available = false;
|
|
123
232
|
return false;
|
|
@@ -15,7 +15,7 @@ export function createDefaultHostPorts(overrides = {}) {
|
|
|
15
15
|
suppressesUserNotify: () => false,
|
|
16
16
|
},
|
|
17
17
|
log: { extension: () => { }, debug: () => { }, sink: () => () => { } },
|
|
18
|
-
emit: { bus: () => { }
|
|
18
|
+
emit: { bus: () => { } },
|
|
19
19
|
status: { set: () => { } },
|
|
20
20
|
spawn: { abortSignal: () => undefined, isAllowed: () => true },
|
|
21
21
|
render: { invalidate: () => { } },
|
|
@@ -241,7 +241,11 @@ export const TOOLS = [
|
|
|
241
241
|
checkCommand: "tsc",
|
|
242
242
|
checkArgs: ["--version"],
|
|
243
243
|
installStrategy: "npm",
|
|
244
|
-
|
|
244
|
+
// The managed compiler serves the classic typescript-language-server
|
|
245
|
+
// fallback. TypeScript 7 removed lib/tsserver.js and is selected only from
|
|
246
|
+
// project-local installs through the native `tsc --lsp --stdio` path.
|
|
247
|
+
// Revisit when typescript-language-server supports TS 7 — refs #1436.
|
|
248
|
+
packageName: "typescript@5.9.3",
|
|
245
249
|
binaryName: "tsc",
|
|
246
250
|
},
|
|
247
251
|
{
|
|
@@ -12,11 +12,11 @@ import * as fs from "node:fs";
|
|
|
12
12
|
import { mkdtempSync } from "node:fs";
|
|
13
13
|
import * as os from "node:os";
|
|
14
14
|
import * as path from "node:path";
|
|
15
|
-
import { getExcludedDirGlobs,
|
|
15
|
+
import { getExcludedDirGlobs, getProjectIgnoreGlobs, getProjectIgnoreMatcher, } from "./file-utils.js";
|
|
16
16
|
import { findNodeToolBinary } from "./package-manager.js";
|
|
17
17
|
import { isAtOrAboveHomeDir, isFullyQualified } from "./path-utils.js";
|
|
18
18
|
import { getJscpdMaxEntriesDerived } from "./project-scale.js";
|
|
19
|
-
import { createAvailabilityChecker, resolveAvailableOrInstall } from "./dispatch/runners/utils/runner-helpers.js";
|
|
19
|
+
import { createAvailabilityChecker, findManagedNodeToolBinary, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
20
20
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
21
21
|
import { shouldRecurseIntoDir, walkTreeStackSync } from "./source-walker.js";
|
|
22
22
|
const EMPTY_RESULT = {
|
|
@@ -29,13 +29,8 @@ const EMPTY_RESULT = {
|
|
|
29
29
|
const SCAN_TIMEOUT_MS = 30_000;
|
|
30
30
|
const jscpdAvailability = createAvailabilityChecker("jscpd", "", ["--version"], {
|
|
31
31
|
probeTimeout: 1500,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const candidates = process.platform === "win32"
|
|
35
|
-
? [`${localBase}.cmd`, `${localBase}.exe`, localBase]
|
|
36
|
-
: [localBase];
|
|
37
|
-
return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
|
|
38
|
-
},
|
|
32
|
+
// One definition of the managed-shim fast path, shared with knip (#1476).
|
|
33
|
+
fastPath: () => findManagedNodeToolBinary("jscpd"),
|
|
39
34
|
});
|
|
40
35
|
// --- Client ---
|
|
41
36
|
export class JscpdClient {
|
|
@@ -14,7 +14,8 @@ import * as path from "node:path";
|
|
|
14
14
|
import { getProjectDataDir } from "./file-utils.js";
|
|
15
15
|
import { findNearestMarkerRoot } from "./path-utils.js";
|
|
16
16
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
17
|
-
import { createAvailabilityChecker, getManagedToolEnvironment, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
17
|
+
import { createAvailabilityChecker, findManagedNodeToolBinary, getManagedToolEnvironment, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
18
|
+
import { createAvailabilityLatch, describeUnavailability, } from "./dispatch/runners/utils/availability-policy.js";
|
|
18
19
|
const EMPTY_RESULT = {
|
|
19
20
|
success: false,
|
|
20
21
|
issues: [],
|
|
@@ -61,8 +62,14 @@ export class KnipClient {
|
|
|
61
62
|
knipAvailability = createAvailabilityChecker("knip", ".cmd", ["--version"], {
|
|
62
63
|
environment: (cwd) => getManagedToolEnvironment("knip", cwd),
|
|
63
64
|
unclassifiedFailureOutcome: "missing",
|
|
65
|
+
fastPath: () => findManagedNodeToolBinary("knip"),
|
|
64
66
|
});
|
|
65
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Client-side memo. Only a DURABLE verdict is latched — a transient probe
|
|
69
|
+
* failure expires, so an installed knip becomes available again without a
|
|
70
|
+
* host restart (#1467).
|
|
71
|
+
*/
|
|
72
|
+
availabilityLatch = createAvailabilityLatch();
|
|
66
73
|
knipCommand = "knip";
|
|
67
74
|
ensureInFlight = null;
|
|
68
75
|
log;
|
|
@@ -104,12 +111,17 @@ export class KnipClient {
|
|
|
104
111
|
return findNearestMarkerRoot(startDir, ["package.json", "knip.json", "knip.ts", "knip.config.js", "knip.config.ts"], { boundaries: [".git", ".hg", ".svn"], homeDir: homeDirOverride });
|
|
105
112
|
}
|
|
106
113
|
/**
|
|
107
|
-
* Check if knip CLI is available, auto-install if not
|
|
114
|
+
* Check if knip CLI is available, auto-install if not.
|
|
115
|
+
*
|
|
116
|
+
* The memo returns `null` when the last verdict was transient and its
|
|
117
|
+
* cooldown has expired, which re-enters the probe. That is the difference
|
|
118
|
+
* between "knip is missing" (a fact worth caching) and "the probe timed out"
|
|
119
|
+
* (a moment worth retrying).
|
|
108
120
|
*/
|
|
109
121
|
async ensureAvailable() {
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
return
|
|
122
|
+
const memo = this.availabilityLatch.read();
|
|
123
|
+
if (memo !== null)
|
|
124
|
+
return memo;
|
|
113
125
|
if (this.ensureInFlight)
|
|
114
126
|
return this.ensureInFlight;
|
|
115
127
|
this.ensureInFlight = this.doEnsureAvailable();
|
|
@@ -121,11 +133,39 @@ export class KnipClient {
|
|
|
121
133
|
}
|
|
122
134
|
}
|
|
123
135
|
async doEnsureAvailable() {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
if (resolved)
|
|
136
|
+
const cwd = process.cwd();
|
|
137
|
+
const resolved = await resolveAvailableOrInstall(this.knipAvailability, "knip", cwd);
|
|
138
|
+
if (resolved !== null) {
|
|
127
139
|
this.knipCommand = resolved;
|
|
128
|
-
|
|
140
|
+
this.availabilityLatch.noteAvailable();
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
const verdict = this.knipAvailability.getVerdict(cwd);
|
|
144
|
+
this.availabilityLatch.noteUnavailable(verdict.outcome ?? "missing", verdict.cause ?? "not-found");
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The single place a knip-unavailable result is worded. A transient probe
|
|
149
|
+
* failure must never be reported as "install knip" — knip is on disk.
|
|
150
|
+
*/
|
|
151
|
+
unavailableResult() {
|
|
152
|
+
const verdict = this.knipAvailability.getVerdict(process.cwd());
|
|
153
|
+
const transient = verdict.outcome === "transient";
|
|
154
|
+
const retryAfterMs = verdict.retryAtMs
|
|
155
|
+
? Math.max(0, verdict.retryAtMs - Date.now())
|
|
156
|
+
: undefined;
|
|
157
|
+
return {
|
|
158
|
+
...EMPTY_RESULT,
|
|
159
|
+
failureKind: transient ? "unavailable-transient" : "unavailable-missing",
|
|
160
|
+
summary: describeUnavailability({
|
|
161
|
+
tool: "Knip",
|
|
162
|
+
installHint: "npm install -D knip",
|
|
163
|
+
outcome: verdict.outcome,
|
|
164
|
+
cause: verdict.cause,
|
|
165
|
+
elapsedMs: verdict.elapsedMs,
|
|
166
|
+
retryAfterMs,
|
|
167
|
+
}),
|
|
168
|
+
};
|
|
129
169
|
}
|
|
130
170
|
/**
|
|
131
171
|
* Run knip analysis on the project.
|
|
@@ -152,10 +192,7 @@ export class KnipClient {
|
|
|
152
192
|
};
|
|
153
193
|
}
|
|
154
194
|
if (!(await this.ensureAvailable())) {
|
|
155
|
-
return
|
|
156
|
-
...EMPTY_RESULT,
|
|
157
|
-
summary: "Knip not available. Install with: npm install -D knip",
|
|
158
|
-
};
|
|
195
|
+
return this.unavailableResult();
|
|
159
196
|
}
|
|
160
197
|
const key = path.resolve(targetDir);
|
|
161
198
|
const existing = this.inFlight.get(key);
|
|
@@ -17,6 +17,55 @@ const writer = createNdjsonLogger({
|
|
|
17
17
|
* the test-mode guard so it is deterministic and unit-testable.
|
|
18
18
|
*/
|
|
19
19
|
let lastPhase;
|
|
20
|
+
/**
|
|
21
|
+
* Phases excluded from `lastPhase` attribution alongside `loop_block`.
|
|
22
|
+
* #1412 L3: `lsp_typescript_project_identity` is the classic-TS first-open
|
|
23
|
+
* project-identity probe (tsserver-sync.ts) — a detached, best-effort
|
|
24
|
+
* telemetry sample fired on every first didOpen, not genuine work. Letting it
|
|
25
|
+
* win `lastPhase` would overwrite the real stall attribution for a
|
|
26
|
+
* loop_block that happens to land right after a first open.
|
|
27
|
+
* #1432 review: `advisory_provenance_decision`,
|
|
28
|
+
* `authoritative_content_attachment_decision`, and
|
|
29
|
+
* `agent_end_deferred_mutation_drain` are the same shape — zero-duration
|
|
30
|
+
* decision telemetry, not genuine work — so they get the same exclusion.
|
|
31
|
+
* `agent_end_deferred_mutation_requeue` (S2d gap 4's per-requeue record) and
|
|
32
|
+
* `session_end_bus_rollup` (S2d gap 5's session-end rollup) are new
|
|
33
|
+
* zero-duration siblings of the same shape, added alongside them.
|
|
34
|
+
*
|
|
35
|
+
* `lsp_aux_wait_outcome` (#1458) is DIFFERENT from every entry above: its
|
|
36
|
+
* `durationMs` is a REAL bounded wait (the post-primary auxiliary grace, up
|
|
37
|
+
* to a few seconds), not zero-duration decision telemetry. It is still
|
|
38
|
+
* excluded because it is a WAIT-OUTCOME RECORD written after the aux wait
|
|
39
|
+
* already completed, not the stall itself — that wait ran inside the
|
|
40
|
+
* `lsp_touch_file`/diagnostics phase surrounding it, so letting this summary
|
|
41
|
+
* row win `lastPhase` would misattribute a `loop_block` to the record instead
|
|
42
|
+
* of to whatever phase is actually stalled when the block fires.
|
|
43
|
+
* #1453: `tool_set_mutation` records an active-tool-set rewrite (zero-duration
|
|
44
|
+
* bookkeeping, and it fires during session_start where a real stall must stay
|
|
45
|
+
* attributed to the work around it), so it is excluded for the same reason.
|
|
46
|
+
* #1467: `availability_decision` records a tool-probe verdict. Its `durationMs`
|
|
47
|
+
* is the child's own probe time (often zero for a fast path or a cached
|
|
48
|
+
* decision) and the record is bookkeeping ABOUT a probe, not host work — a
|
|
49
|
+
* loop_block landing next to one must stay attributed to whatever really
|
|
50
|
+
* stalled the loop, which is frequently the very thing that expired the probe.
|
|
51
|
+
*
|
|
52
|
+
* #1461 slice 1: `finding_dead_path_drop` is the same shape — the record a
|
|
53
|
+
* delivery seam writes when it drops findings whose cited path no longer
|
|
54
|
+
* exists.
|
|
55
|
+
*/
|
|
56
|
+
const LAST_PHASE_EXCLUDED = new Set([
|
|
57
|
+
"loop_block",
|
|
58
|
+
"lsp_typescript_project_identity",
|
|
59
|
+
"advisory_provenance_decision",
|
|
60
|
+
"authoritative_content_attachment_decision",
|
|
61
|
+
"agent_end_deferred_mutation_drain",
|
|
62
|
+
"agent_end_deferred_mutation_requeue",
|
|
63
|
+
"session_end_bus_rollup",
|
|
64
|
+
"lsp_aux_wait_outcome",
|
|
65
|
+
"tool_set_mutation",
|
|
66
|
+
"availability_decision",
|
|
67
|
+
"finding_dead_path_drop",
|
|
68
|
+
]);
|
|
20
69
|
/**
|
|
21
70
|
* The last non-`loop_block` phase logged, or undefined if none yet. Carries its
|
|
22
71
|
* own `ts` so a consumer can gauge staleness: it is intentionally NOT cleared at
|
|
@@ -29,7 +78,7 @@ export function getLastLoggedPhase() {
|
|
|
29
78
|
}
|
|
30
79
|
export function logLatency(entry) {
|
|
31
80
|
const ts = new Date().toISOString();
|
|
32
|
-
if (entry.type === "phase" && entry.phase && entry.phase
|
|
81
|
+
if (entry.type === "phase" && entry.phase && !LAST_PHASE_EXCLUDED.has(entry.phase)) {
|
|
33
82
|
lastPhase = { phase: entry.phase, ts };
|
|
34
83
|
}
|
|
35
84
|
if (isTestMode()) {
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { logExtension } from "./extension-log.js";
|
|
2
|
+
import { logBusEvent } from "./bus-events-logger.js";
|
|
3
|
+
import { normalizeFilePath } from "./path-utils.js";
|
|
4
|
+
import { createLiveBusEmitter, recordStaleBusFailure, resolveLiveBusEmitter, } from "./live-bus-emitter.js";
|
|
2
5
|
export const LENS_EVENT_VERSION = 1;
|
|
3
6
|
export const LENS_EVENT_NAMES = {
|
|
4
7
|
analysisComplete: "pi-lens/analysis-complete",
|
|
5
8
|
findings: "pi-lens/findings",
|
|
6
9
|
turnFindings: "pi-lens/turn-findings",
|
|
7
10
|
};
|
|
8
|
-
|
|
9
|
-
let lensEventBusGetter;
|
|
10
|
-
export function initLensEvents(pi) {
|
|
11
|
-
lensEventBus = pi.events;
|
|
12
|
-
lensEventBusGetter = undefined;
|
|
13
|
-
}
|
|
11
|
+
const liveEmitter = createLiveBusEmitter();
|
|
14
12
|
/** Keep deferred deliveries on the live extension activation. */
|
|
15
13
|
export function initLensEventsGetter(getter) {
|
|
16
|
-
|
|
17
|
-
lensEventBus = undefined;
|
|
14
|
+
liveEmitter.wireGetter(getter);
|
|
18
15
|
}
|
|
19
16
|
function truncateText(value, maxChars) {
|
|
20
17
|
if (value === undefined)
|
|
@@ -38,28 +35,62 @@ function normalizeDiagnostics(diagnostics) {
|
|
|
38
35
|
// during startup/replacement windows) but must not be invisible. One line per
|
|
39
36
|
// process — enough to notice a permanently-missing bus without spamming.
|
|
40
37
|
let _droppedEmitLogged = false;
|
|
38
|
+
// Emit-failure gating (H1, #1415 review): occurrence-scoped, matching every
|
|
39
|
+
// other producer's `hasLoggedFailure` guard (see clients/bus-publish.ts) and
|
|
40
|
+
// AGENTS.md's "one bus-stale degradation per occurrence" invariant. A success
|
|
41
|
+
// re-arms it, so a later failure records a fresh degradation rather than
|
|
42
|
+
// being silently absorbed by an earlier one.
|
|
43
|
+
let hasLoggedFailure = false;
|
|
44
|
+
/** Test-only: reset module state between test files. */
|
|
45
|
+
export function _resetForTests() {
|
|
46
|
+
liveEmitter.reset();
|
|
47
|
+
_droppedEmitLogged = false;
|
|
48
|
+
hasLoggedFailure = false;
|
|
49
|
+
}
|
|
41
50
|
function emitLensEvent(eventName, payload) {
|
|
42
51
|
setImmediate(() => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
const rawCwd = payload.cwd ?? process.cwd();
|
|
53
|
+
// M1: normalizeFilePath is a sync realpathSync.native on Windows — build
|
|
54
|
+
// the resolveLiveBusEmitter log entry lazily so the ready-path (the
|
|
55
|
+
// common case) never pays it. Only the stale-session branch needs it.
|
|
56
|
+
const resolution = resolveLiveBusEmitter(liveEmitter, () => ({
|
|
57
|
+
event: eventName,
|
|
58
|
+
cwd: normalizeFilePath(rawCwd),
|
|
59
|
+
}));
|
|
60
|
+
if (resolution.outcome === "stale-session")
|
|
61
|
+
return;
|
|
62
|
+
if (resolution.outcome === "unwired") {
|
|
63
|
+
if (!_droppedEmitLogged) {
|
|
64
|
+
_droppedEmitLogged = true;
|
|
65
|
+
logExtension({
|
|
66
|
+
subsystem: "lens-events",
|
|
67
|
+
level: "warn",
|
|
68
|
+
message: `lens event dropped (no live bus): ${eventName} — further drops logged once per process`,
|
|
69
|
+
metadata: { eventName },
|
|
70
|
+
});
|
|
57
71
|
}
|
|
58
|
-
|
|
72
|
+
return;
|
|
59
73
|
}
|
|
60
|
-
|
|
74
|
+
const cwd = normalizeFilePath(rawCwd);
|
|
75
|
+
try {
|
|
76
|
+
resolution.emit(eventName, payload);
|
|
77
|
+
hasLoggedFailure = false;
|
|
78
|
+
logBusEvent({ event: eventName, outcome: "emitted", cwd });
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
logBusEvent({
|
|
82
|
+
event: eventName,
|
|
83
|
+
outcome: "emit_failed",
|
|
84
|
+
cwd,
|
|
85
|
+
error: String(err),
|
|
86
|
+
ctxSource: resolution.ctxSource,
|
|
87
|
+
});
|
|
88
|
+
if (!hasLoggedFailure) {
|
|
89
|
+
hasLoggedFailure = true;
|
|
90
|
+
recordStaleBusFailure(eventName, err);
|
|
91
|
+
}
|
|
61
92
|
// Inter-extension events are observational. A listener must never break
|
|
62
|
-
// the pi-lens hook path or delay agent progress
|
|
93
|
+
// the pi-lens hook path or delay agent progress.
|
|
63
94
|
}
|
|
64
95
|
});
|
|
65
96
|
}
|
|
@@ -164,6 +164,14 @@ export const LENS_FLAGS = [
|
|
|
164
164
|
default: false,
|
|
165
165
|
scope: "global",
|
|
166
166
|
},
|
|
167
|
+
{
|
|
168
|
+
name: "no-lazy-tools",
|
|
169
|
+
description: "Keep all pi-lens tools active to avoid tool-list cache changes. Also via tools.lazy=false in ~/.pi-lens/config.json.",
|
|
170
|
+
configKey: "tools.lazy",
|
|
171
|
+
negated: true,
|
|
172
|
+
default: false,
|
|
173
|
+
scope: "global",
|
|
174
|
+
},
|
|
167
175
|
];
|
|
168
176
|
const byName = new Map(LENS_FLAGS.map((spec) => [spec.name, spec]));
|
|
169
177
|
export function getLensFlagSpec(name) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { recordDegradation } from "./degradation-ledger.js";
|
|
2
|
+
import { probeCtxActive } from "./session-lifecycle.js";
|
|
3
|
+
import { logBusEvent, } from "./bus-events-logger.js";
|
|
2
4
|
const STALE_CTX_MESSAGE = "This extension ctx is stale after session replacement";
|
|
3
5
|
/** Ledger a dead activation once, at the publisher's occurrence-window guard. */
|
|
4
6
|
export function recordStaleBusFailure(subject, error) {
|
|
@@ -19,8 +21,26 @@ export function createLiveBusEmitter() {
|
|
|
19
21
|
getter = next;
|
|
20
22
|
emit = undefined;
|
|
21
23
|
},
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
resolve() {
|
|
25
|
+
// Invoke the getter for every delivery so session_start rewiring can
|
|
26
|
+
// replace a captured pre-await activation with the current primary. When
|
|
27
|
+
// the current target is nevertheless confirmed stale, never invoke it.
|
|
28
|
+
const target = getter?.() ?? emit;
|
|
29
|
+
if (!target)
|
|
30
|
+
return { outcome: "unwired", ctxSource: "unwired" };
|
|
31
|
+
if (typeof target === "function") {
|
|
32
|
+
return { outcome: "ready", emit: target, ctxSource: "global-fallback" };
|
|
33
|
+
}
|
|
34
|
+
// Object targets are always "own"-sourced (only `wire(fn)`'s bare
|
|
35
|
+
// function arm is "global-fallback", and it never reaches this
|
|
36
|
+
// branch) — compute it once so the ready/stale-session outcomes
|
|
37
|
+
// below both carry the same value instead of restating the
|
|
38
|
+
// literal independently.
|
|
39
|
+
const ctxSource = "own";
|
|
40
|
+
if (probeCtxActive(target.ctx) === false) {
|
|
41
|
+
return { outcome: "stale-session", ctxSource };
|
|
42
|
+
}
|
|
43
|
+
return { outcome: "ready", emit: target.emit, ctxSource };
|
|
24
44
|
},
|
|
25
45
|
reset() {
|
|
26
46
|
emit = undefined;
|
|
@@ -28,3 +48,26 @@ export function createLiveBusEmitter() {
|
|
|
28
48
|
},
|
|
29
49
|
};
|
|
30
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve through the shared stale-session guard and record a declined
|
|
53
|
+
* target.
|
|
54
|
+
*
|
|
55
|
+
* `entry` is a THUNK, not a value (M1, #1415 review): building the log entry
|
|
56
|
+
* (every producer's version normalizes `cwd` via `normalizeFilePath`, a sync
|
|
57
|
+
* `realpathSync.native` call on Windows) is real per-publish cost that used
|
|
58
|
+
* to be paid on EVERY call regardless of outcome, even though it is only
|
|
59
|
+
* consumed on the `stale-session` branch. Invoking the thunk only there means
|
|
60
|
+
* the common `ready` path pays nothing for it.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveLiveBusEmitter(liveEmitter, entry) {
|
|
63
|
+
const resolution = liveEmitter.resolve();
|
|
64
|
+
if (resolution.outcome === "stale-session") {
|
|
65
|
+
logBusEvent({
|
|
66
|
+
...entry(),
|
|
67
|
+
outcome: "skipped_stale_session",
|
|
68
|
+
level: "info",
|
|
69
|
+
ctxSource: resolution.ctxSource,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return resolution;
|
|
73
|
+
}
|
|
@@ -26,9 +26,12 @@
|
|
|
26
26
|
* receive `options.auxGraceMs` before the race finalises. Aux results within
|
|
27
27
|
* that window are included; later arrivals are dropped. When no descriptors
|
|
28
28
|
* carry `role:"auxiliary"` the aux-grace path is never entered.
|
|
29
|
-
* @param options.auxGraceMs -
|
|
30
|
-
* primary promises have settled. Only relevant when at
|
|
31
|
-
* carries `role:"auxiliary"`. Defaults to 500ms.
|
|
29
|
+
* @param options.auxGraceMs - Ceiling on the grace period given to auxiliary
|
|
30
|
+
* promises after all primary promises have settled. Only relevant when at
|
|
31
|
+
* least one descriptor carries `role:"auxiliary"`. Defaults to 500ms. When a
|
|
32
|
+
* still-pending auxiliary's descriptor carries `budgetMs`, the actual window
|
|
33
|
+
* is `min(auxGraceMs, max(budgetMs of still-pending auxiliaries))` — see
|
|
34
|
+
* `PromiseDescriptor.budgetMs`.
|
|
32
35
|
*/
|
|
33
36
|
export async function raceToCompletion(promises, shouldComplete, options = { timeoutMs: 1500 }) {
|
|
34
37
|
const results = new Array(promises.length).fill(undefined);
|
|
@@ -76,7 +79,30 @@ export async function raceToCompletion(promises, shouldComplete, options = { tim
|
|
|
76
79
|
if (auxGraceStarted || completed)
|
|
77
80
|
return;
|
|
78
81
|
auxGraceStarted = true;
|
|
79
|
-
const
|
|
82
|
+
const ceilingMs = options.auxGraceMs ?? 500;
|
|
83
|
+
// #1458 S2: cap the grace window by the still-pending auxiliaries' own
|
|
84
|
+
// declared budgets, not just the flat ceiling — mirrors touchFile's
|
|
85
|
+
// per-server `min(declared, ceiling)`.
|
|
86
|
+
//
|
|
87
|
+
// This is ONE shared timer, not a deadline per promise, so it grants
|
|
88
|
+
// every pending auxiliary the window of its slowest pending sibling.
|
|
89
|
+
// That is safe here only because of an invariant the caller upholds,
|
|
90
|
+
// not because of anything this function enforces: every promise
|
|
91
|
+
// `getDiagnostics` passes in is already self-bounded by the same
|
|
92
|
+
// `strategy.aggregateWaitMs` it declares as `budgetMs`, so a fast
|
|
93
|
+
// auxiliary resolves on its own deadline long before a slow sibling's
|
|
94
|
+
// window expires. A caller that passes a promise NOT self-bounded by
|
|
95
|
+
// its descriptor's `budgetMs` — or one that outlives it, the way
|
|
96
|
+
// `expectSemanticSecondPush` extends a primary — would get a window
|
|
97
|
+
// wider than it declared. `PromiseDescriptor` is public, so treat that
|
|
98
|
+
// as a real constraint on new callers rather than a theoretical one.
|
|
99
|
+
const pendingAuxBudgets = [...auxIndices]
|
|
100
|
+
.filter((i) => results[i] === undefined)
|
|
101
|
+
.map((i) => descriptors[i]?.budgetMs)
|
|
102
|
+
.filter((b) => typeof b === "number" && b >= 0);
|
|
103
|
+
const graceMs = pendingAuxBudgets.length > 0
|
|
104
|
+
? Math.min(ceilingMs, Math.max(...pendingAuxBudgets))
|
|
105
|
+
: ceilingMs;
|
|
80
106
|
if (graceMs <= 0) {
|
|
81
107
|
finalize();
|
|
82
108
|
return;
|