@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
|
@@ -32,6 +32,12 @@
|
|
|
32
32
|
* NOT tier-3 — the caller keeps today's full in-lane wait. Fail-safe is
|
|
33
33
|
* always "wait like before".
|
|
34
34
|
*
|
|
35
|
+
* Native TS7 is the cascade-only exception. It is not silent on clean, but
|
|
36
|
+
* its publication does not settle inside the cold-snapshot budget. Cascade
|
|
37
|
+
* classifies it as `collect-later`, sends the same no-wait touch, and
|
|
38
|
+
* reconciles its later per-file push or pull publication. The shared server
|
|
39
|
+
* policy remains `waits`, so main-lane behavior does not change.
|
|
40
|
+
*
|
|
35
41
|
* #524/#529/#541/#558: a server id can now be backed by more than one actual
|
|
36
42
|
* binary — "typescript" is classic typescript-language-server OR TS7's
|
|
37
43
|
* native `tsc --lsp --stdio` (PR #526). PR #526 originally routed the
|
|
@@ -46,9 +52,11 @@
|
|
|
46
52
|
* confirmed still silent (`cleanPubs=0(v:0)`) in the same run. This is
|
|
47
53
|
* therefore an EVIDENCE-BASED revert, not the original unverified caution:
|
|
48
54
|
* native-ts7's clean-signal behavior IS known, and it is "publishes, not
|
|
49
|
-
* silent". The
|
|
50
|
-
*
|
|
51
|
-
*
|
|
55
|
+
* silent". The shared classifier still routes a native-ts7 snapshot through
|
|
56
|
+
* `waits`. The cascade-only wrapper routes it through `collect-later` because
|
|
57
|
+
* the measured publication arrives after the in-lane budget. The shared
|
|
58
|
+
* `silentOnClean` flag stays `true` for classic.
|
|
59
|
+
* `scripts/probe-clean-signal.mjs`'s drift check no
|
|
52
60
|
* longer compares native-ts7 rows against the shared marker (it now expects
|
|
53
61
|
* `false` for them explicitly) — see that file's header for the regression
|
|
54
62
|
* watch this sets up for a future TS7 build that becomes silent again.
|
|
@@ -57,7 +65,32 @@ import { logCascade } from "../cascade-logger.js";
|
|
|
57
65
|
import { logLatency } from "../latency-logger.js";
|
|
58
66
|
import { normalizeMapKey } from "../path-utils.js";
|
|
59
67
|
import { registerQuietWindowTask } from "../quiet-window.js";
|
|
60
|
-
|
|
68
|
+
import { classifyCascadeWaitTier as classifySharedCascadeWaitTier, classifyServerWaitTier, resolvePrimaryServerForWaitPolicy, } from "./wait-policy/classification.js";
|
|
69
|
+
export { classifyServerWaitTier };
|
|
70
|
+
/**
|
|
71
|
+
* The cascade lane's wait tier for `filePath`. DELEGATES to the shared
|
|
72
|
+
* `wait-policy/classification.ts` rule — this wrapper adds exactly one
|
|
73
|
+
* cascade-only override on top of it (native TS7's push-only snapshot →
|
|
74
|
+
* `collect-later`) and never re-implements the classification itself.
|
|
75
|
+
*
|
|
76
|
+
* #1444 coverage tradeoff: the no-wait touch this tier selects is
|
|
77
|
+
* `clientScope: "primary"`, and the tier itself is decided from the PRIMARY
|
|
78
|
+
* (non-auxiliary) server alone. That tradeoff already existed for
|
|
79
|
+
* `tier3-silent`; the override enlarges the population it applies to — every
|
|
80
|
+
* native-TS7 TypeScript neighbour now takes the no-wait path too, so an
|
|
81
|
+
* auxiliary server configured for those files no longer gets touched in-lane
|
|
82
|
+
* for them (its findings arrive via the next per-edit dispatch, as they
|
|
83
|
+
* already did for classic tier-3 files).
|
|
84
|
+
*/
|
|
85
|
+
export function classifyCascadeWaitTier(lspService, filePath, snapshots) {
|
|
86
|
+
const primary = resolvePrimaryServerForWaitPolicy(filePath, snapshots);
|
|
87
|
+
if (primary?.serverId === "typescript" &&
|
|
88
|
+
primary.snapshot?.launchVariant === "native-ts7" &&
|
|
89
|
+
primary.snapshot.workspaceDiagnosticsSupport?.mode === "push-only") {
|
|
90
|
+
return "collect-later";
|
|
91
|
+
}
|
|
92
|
+
return classifySharedCascadeWaitTier(lspService, filePath, snapshots);
|
|
93
|
+
}
|
|
61
94
|
// --- Kill switch (lazy, memoized — house style per clients/runtime-config.ts /
|
|
62
95
|
// clients/quiet-window.ts's isQuietWindowEnabled) ---
|
|
63
96
|
let _enabledCache;
|
|
@@ -158,6 +191,7 @@ export async function reconcileOutstandingCascadeTouches(lspService) {
|
|
|
158
191
|
outcome: found ? "resolved-found" : "resolved-clean",
|
|
159
192
|
ageMs,
|
|
160
193
|
diagnosticCount: entry.diags.length,
|
|
194
|
+
publishedAt: entry.ts,
|
|
161
195
|
// #1023: carry the diagnostics so the task can re-surface them.
|
|
162
196
|
...(found && { diagnostics: entry.diags }),
|
|
163
197
|
});
|
|
@@ -199,17 +233,28 @@ export function registerCascadeTierReconcileTask(getLspService, options = {}) {
|
|
|
199
233
|
if (outcomes.length === 0)
|
|
200
234
|
return;
|
|
201
235
|
// #1023: re-inject each resolved-found neighbor error so it reaches the
|
|
202
|
-
// agent (previously logs-only).
|
|
203
|
-
//
|
|
236
|
+
// agent (previously logs-only). #1444: hand each resolved-CLEAN outcome to
|
|
237
|
+
// the footer reconcile for the mirror-image case. Isolated per-outcome — a
|
|
238
|
+
// throwing callback must not drop the log line or the sibling deliveries.
|
|
204
239
|
for (const o of outcomes) {
|
|
205
|
-
if (o.outcome !== "resolved-found" || !o.diagnostics?.length)
|
|
206
|
-
continue;
|
|
207
240
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
241
|
+
if (o.outcome === "resolved-found" && o.diagnostics?.length) {
|
|
242
|
+
options.onResolvedFound?.({
|
|
243
|
+
filePath: o.filePath,
|
|
244
|
+
serverId: o.serverId,
|
|
245
|
+
diagnostics: o.diagnostics,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
else if (o.outcome === "resolved-clean" && o.publishedAt != null) {
|
|
249
|
+
// #1444: the stale-footer half of the same honesty problem — the
|
|
250
|
+
// neighbour proved clean, but only after the in-lane wait was
|
|
251
|
+
// skipped, so nothing has cleared its earlier error entries.
|
|
252
|
+
options.onResolvedClean?.({
|
|
253
|
+
filePath: o.filePath,
|
|
254
|
+
serverId: o.serverId,
|
|
255
|
+
publishedAt: o.publishedAt,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
213
258
|
}
|
|
214
259
|
catch {
|
|
215
260
|
// best-effort surfacing; the log below is the durable record.
|
|
@@ -24,6 +24,7 @@ import { newLspMutationCorrelationId, } from "../lsp-mutation.js";
|
|
|
24
24
|
import { applyWorkspaceEdit, normalizeWorkspaceEditToUtf16, } from "./edits.js";
|
|
25
25
|
import { recordLspChild, removeLspChild } from "../instance-registry.js";
|
|
26
26
|
import { normalizeMapKey, uriToPath } from "./path-utils.js";
|
|
27
|
+
import { probeTsserverProjectIdentity } from "./tsserver-sync.js";
|
|
27
28
|
import { ADVERTISED_POSITION_ENCODINGS, convertCharacterOffset, lineTextAt, negotiatePositionEncoding, } from "./position-encoding.js";
|
|
28
29
|
import { getStrategy } from "./wait-policy/index.js";
|
|
29
30
|
import { WatchedFilesQueue } from "./watch-queue.js";
|
|
@@ -151,6 +152,10 @@ const WORKSPACE_PULL_RESULT_CACHE_MAX = 4096;
|
|
|
151
152
|
// stops a hung server from blocking the caller forever. On timeout the command
|
|
152
153
|
// may still be applying server-side; we surface that rather than pretend it ran.
|
|
153
154
|
const EXECUTE_COMMAND_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_EXECUTE_COMMAND_TIMEOUT_MS", 30_000);
|
|
155
|
+
// #1412 H1: short ceiling for the read-only tsserver project-identity probe.
|
|
156
|
+
// This is a telemetry sample, not a mutation — it must never hold the door
|
|
157
|
+
// open for anything close to EXECUTE_COMMAND_TIMEOUT_MS.
|
|
158
|
+
const PROBE_COMMAND_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_PROJECT_IDENTITY_PROBE_TIMEOUT_MS", 2_500);
|
|
154
159
|
const LSP_CRASH_CODES = new Set([
|
|
155
160
|
"ERR_STREAM_DESTROYED",
|
|
156
161
|
"ERR_STREAM_WRITE_AFTER_END",
|
|
@@ -434,9 +439,16 @@ function getMergedDiagnosticsForPath(state, normalizedPath) {
|
|
|
434
439
|
return mergeDiagnosticLists(state.pushDiagnostics?.get(normalizedPath) ??
|
|
435
440
|
legacy.diagnostics?.get(normalizedPath), state.documentPullDiagnostics?.get(normalizedPath));
|
|
436
441
|
}
|
|
437
|
-
|
|
442
|
+
/** Exported for tests: the quiet-window timer cancel on clear/resync is the
|
|
443
|
+
* headline #1412 safety property (a stale versionless publication must never
|
|
444
|
+
* land after the document content changed). */
|
|
445
|
+
export function clearDiagnosticsForPath(state, normalizedPath) {
|
|
438
446
|
const legacy = state;
|
|
439
447
|
state.pushDiagnostics?.delete(normalizedPath);
|
|
448
|
+
const pending = state.pendingDiagnostics?.get(normalizedPath);
|
|
449
|
+
if (pending)
|
|
450
|
+
clearTimeout(pending);
|
|
451
|
+
state.pendingDiagnostics?.delete(normalizedPath);
|
|
440
452
|
state.pushDiagnosticTimestamps?.delete(normalizedPath);
|
|
441
453
|
state.documentPullDiagnostics?.delete(normalizedPath);
|
|
442
454
|
state.documentPullDiagnosticTimestamps?.delete(normalizedPath);
|
|
@@ -454,6 +466,32 @@ function clearDiagnosticsForPath(state, normalizedPath) {
|
|
|
454
466
|
legacy.diagnostics?.delete(normalizedPath);
|
|
455
467
|
legacy.diagnosticTimestamps?.delete(normalizedPath);
|
|
456
468
|
}
|
|
469
|
+
function logTypeScriptPullSettle(state, normalizedPath) {
|
|
470
|
+
if (state.serverId !== "typescript")
|
|
471
|
+
return;
|
|
472
|
+
const diagnostics = state.documentPullDiagnostics.get(normalizedPath) ?? [];
|
|
473
|
+
const elapsedSinceDidOpenMs = Math.max(0, Date.now() - (state.documentOpenedAt.get(normalizedPath) ?? Date.now()));
|
|
474
|
+
const diagnosticCodes = [...new Set(diagnostics
|
|
475
|
+
.map((diagnostic) => diagnostic.code)
|
|
476
|
+
.filter((code) => code !== undefined)
|
|
477
|
+
.map(String))].slice(0, 8);
|
|
478
|
+
logLatency({
|
|
479
|
+
type: "phase",
|
|
480
|
+
phase: "lsp_typescript_diagnostic_sequence",
|
|
481
|
+
filePath: normalizedPath,
|
|
482
|
+
durationMs: elapsedSinceDidOpenMs,
|
|
483
|
+
metadata: {
|
|
484
|
+
launchVariant: state.launchVariant ?? "unknown",
|
|
485
|
+
publicationIndex: state.diagnosticPublicationCounts.get(normalizedPath) ?? 0,
|
|
486
|
+
version: null,
|
|
487
|
+
diagnosticCount: diagnostics.length,
|
|
488
|
+
diagnosticCodes,
|
|
489
|
+
elapsedSinceDidOpenMs,
|
|
490
|
+
settledReturn: true,
|
|
491
|
+
settleSource: "pull",
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
}
|
|
457
495
|
/**
|
|
458
496
|
* #1095: fingerprint the EXACT didOpen/didChange payload text at SEND time and
|
|
459
497
|
* tag it with the document version it was sent as, so a later
|
|
@@ -558,7 +596,45 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
558
596
|
message: `server=${state.serverId} pubVersion=${docVersion} docVersion=${state.documentVersions?.get(normalizedPath)} diags=${newDiags.length}`,
|
|
559
597
|
});
|
|
560
598
|
}
|
|
561
|
-
const strategy = getStrategy(state.serverId);
|
|
599
|
+
const strategy = getStrategy(state.serverId, state.launchVariant);
|
|
600
|
+
// Publication counting and code extraction exist only for the
|
|
601
|
+
// TypeScript diagnostic-sequence telemetry; skip the bookkeeping for
|
|
602
|
+
// every other push server on this hot receive path.
|
|
603
|
+
const isTypeScriptTelemetry = state.serverId === "typescript";
|
|
604
|
+
const publicationIndex = isTypeScriptTelemetry
|
|
605
|
+
? (state.diagnosticPublicationCounts.get(normalizedPath) ?? 0) + 1
|
|
606
|
+
: 0;
|
|
607
|
+
if (isTypeScriptTelemetry) {
|
|
608
|
+
state.diagnosticPublicationCounts.set(normalizedPath, publicationIndex);
|
|
609
|
+
}
|
|
610
|
+
const diagnosticCodes = isTypeScriptTelemetry
|
|
611
|
+
? [...new Set(newDiags
|
|
612
|
+
.map((diagnostic) => diagnostic.code)
|
|
613
|
+
.filter((code) => code !== undefined)
|
|
614
|
+
.map(String))].slice(0, 8)
|
|
615
|
+
: [];
|
|
616
|
+
const logSequence = (settledReturn, settleSource) => {
|
|
617
|
+
if (state.serverId !== "typescript")
|
|
618
|
+
return;
|
|
619
|
+
const elapsedSinceDidOpenMs = Math.max(0, Date.now() -
|
|
620
|
+
(state.documentOpenedAt.get(normalizedPath) ?? Date.now()));
|
|
621
|
+
logLatency({
|
|
622
|
+
type: "phase",
|
|
623
|
+
phase: "lsp_typescript_diagnostic_sequence",
|
|
624
|
+
filePath: normalizedPath,
|
|
625
|
+
durationMs: elapsedSinceDidOpenMs,
|
|
626
|
+
metadata: {
|
|
627
|
+
launchVariant: state.launchVariant ?? "unknown",
|
|
628
|
+
publicationIndex,
|
|
629
|
+
version: docVersion ?? null,
|
|
630
|
+
diagnosticCount: newDiags.length,
|
|
631
|
+
diagnosticCodes,
|
|
632
|
+
elapsedSinceDidOpenMs,
|
|
633
|
+
settledReturn,
|
|
634
|
+
...(settleSource && { settleSource }),
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
};
|
|
562
638
|
// Record the document version these diagnostics were computed against
|
|
563
639
|
// (when the server reports it) so waitForDiagnostics can reject results
|
|
564
640
|
// that lag behind the latest didChange instead of serving them as fresh.
|
|
@@ -624,8 +700,10 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
624
700
|
recordDocVersion();
|
|
625
701
|
state.diagnosticsVersion += 1;
|
|
626
702
|
state.diagnosticEmitter.emit("diagnostics", normalizedPath);
|
|
703
|
+
logSequence(true, "first-push");
|
|
627
704
|
return;
|
|
628
705
|
}
|
|
706
|
+
logSequence(false);
|
|
629
707
|
const existingTimer = state.pendingDiagnostics.get(normalizedPath);
|
|
630
708
|
if (existingTimer)
|
|
631
709
|
clearTimeout(existingTimer);
|
|
@@ -638,6 +716,7 @@ export function setupIncomingHandlers(state, initialization) {
|
|
|
638
716
|
recordDocVersion();
|
|
639
717
|
state.diagnosticsVersion += 1;
|
|
640
718
|
state.diagnosticEmitter.emit("diagnostics", normalizedPath);
|
|
719
|
+
logSequence(true, "quiet-window");
|
|
641
720
|
}, strategy.debounceMs);
|
|
642
721
|
state.pendingDiagnostics.set(normalizedPath, timer);
|
|
643
722
|
});
|
|
@@ -998,10 +1077,12 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
|
|
|
998
1077
|
// version baseline (`minVersion === undefined`), so a failed pull returned
|
|
999
1078
|
// 0 and was read as a fresh clean.
|
|
1000
1079
|
let outcome = await clientRequestPullDiagnostics(state, filePath, timeoutMs);
|
|
1001
|
-
if (outcome.status === "found")
|
|
1080
|
+
if (outcome.status === "found") {
|
|
1081
|
+
logTypeScriptPullSettle(state, normalizedPath);
|
|
1002
1082
|
return;
|
|
1083
|
+
}
|
|
1003
1084
|
let sawClean = outcome.status === "clean";
|
|
1004
|
-
const strategy = getStrategy(state.serverId);
|
|
1085
|
+
const strategy = getStrategy(state.serverId, state.launchVariant);
|
|
1005
1086
|
const retryBudgetMs = strategy.pullRetryBudgetMs > 0
|
|
1006
1087
|
? Math.min(timeoutMs, strategy.pullRetryBudgetMs)
|
|
1007
1088
|
: 0;
|
|
@@ -1016,10 +1097,16 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
|
|
|
1016
1097
|
if (outcome.status === "clean")
|
|
1017
1098
|
sawClean = true;
|
|
1018
1099
|
}
|
|
1019
|
-
if (options.pullOnly)
|
|
1100
|
+
if (options.pullOnly) {
|
|
1101
|
+
if (outcome.status === "found" || sawClean) {
|
|
1102
|
+
logTypeScriptPullSettle(state, normalizedPath);
|
|
1103
|
+
}
|
|
1020
1104
|
return;
|
|
1021
|
-
|
|
1105
|
+
}
|
|
1106
|
+
if (outcome.status === "found" || sawClean) {
|
|
1107
|
+
logTypeScriptPullSettle(state, normalizedPath);
|
|
1022
1108
|
return;
|
|
1109
|
+
}
|
|
1023
1110
|
}
|
|
1024
1111
|
if (hasFreshDiagnostics() &&
|
|
1025
1112
|
!isVersionStale() &&
|
|
@@ -1037,7 +1124,7 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
|
|
|
1037
1124
|
clearTimeout(debounceTimer);
|
|
1038
1125
|
// Adaptive debounce: use time since last push to compute remaining
|
|
1039
1126
|
// wait instead of always waiting the full debounce window.
|
|
1040
|
-
const strategy = getStrategy(state.serverId);
|
|
1127
|
+
const strategy = getStrategy(state.serverId, state.launchVariant);
|
|
1041
1128
|
const hit = state.pushDiagnosticTimestamps.get(normalizedPath);
|
|
1042
1129
|
const timeSincePush = hit ? Date.now() - hit : Infinity;
|
|
1043
1130
|
const remaining = Math.max(0, strategy.debounceMs - timeSincePush);
|
|
@@ -1080,7 +1167,7 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
|
|
|
1080
1167
|
// Scanners that only re-scan on a fresh open (opengrep ignores didChange):
|
|
1081
1168
|
// close + reopen so the re-edit actually triggers a re-scan instead of
|
|
1082
1169
|
// silently publishing nothing.
|
|
1083
|
-
if (getStrategy(state.serverId).reopenOnResync) {
|
|
1170
|
+
if (getStrategy(state.serverId, state.launchVariant).reopenOnResync) {
|
|
1084
1171
|
await safeSendNotification(state.connection, "textDocument/didClose", {
|
|
1085
1172
|
textDocument: { uri },
|
|
1086
1173
|
});
|
|
@@ -1096,6 +1183,8 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
|
|
|
1096
1183
|
// for a stale view (worse than "unknown"). Monotonic versions make that
|
|
1097
1184
|
// late echo strictly older → dropped by isSupersededPush → never bound.
|
|
1098
1185
|
state.documentVersions.set(normalizedPath, version);
|
|
1186
|
+
state.documentOpenedAt.set(normalizedPath, Date.now());
|
|
1187
|
+
state.diagnosticPublicationCounts.set(normalizedPath, 0);
|
|
1099
1188
|
if (!isClientAlive(state))
|
|
1100
1189
|
return;
|
|
1101
1190
|
await safeSendNotification(state.connection, "textDocument/didOpen", {
|
|
@@ -1115,6 +1204,8 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
|
|
|
1115
1204
|
}
|
|
1116
1205
|
state.pendingOpens.add(normalizedPath);
|
|
1117
1206
|
state.documentVersions.set(normalizedPath, 0);
|
|
1207
|
+
state.documentOpenedAt.set(normalizedPath, Date.now());
|
|
1208
|
+
state.diagnosticPublicationCounts.set(normalizedPath, 0);
|
|
1118
1209
|
clearDiagnosticsForPath(state, normalizedPath); // always clear for initial open
|
|
1119
1210
|
// Send workspace notification first (like opencode does).
|
|
1120
1211
|
// Skipped in silent mode — cascade reads a file for diagnostics,
|
|
@@ -1149,6 +1240,24 @@ export async function handleNotifyOpen(state, filePath, content, languageId, pre
|
|
|
1149
1240
|
state.openDocuments.add(normalizedPath);
|
|
1150
1241
|
state.closedDocuments?.delete(normalizedPath);
|
|
1151
1242
|
state.openDocumentUris?.set(normalizedPath, uri);
|
|
1243
|
+
// Telemetry is deliberately detached after didOpen succeeds.
|
|
1244
|
+
// #1412 H1: routed through runReadOnlyServerCommand, NOT runServerCommand —
|
|
1245
|
+
// the probe must never open the serverEditsAllowed/activeMutationContext
|
|
1246
|
+
// mutation-acceptance window; it is a diagnostic sample, not a mutation, and
|
|
1247
|
+
// carries its own short PROBE_COMMAND_TIMEOUT_MS backstop. The probe itself
|
|
1248
|
+
// is classic-only and swallows every failure.
|
|
1249
|
+
void probeTsserverProjectIdentity({
|
|
1250
|
+
serverId: state.serverId,
|
|
1251
|
+
launchVariant: state.launchVariant,
|
|
1252
|
+
clientRoot: state.root,
|
|
1253
|
+
file: filePath,
|
|
1254
|
+
normalizedFile: normalizedPath,
|
|
1255
|
+
probedFiles: state.projectIdentityProbedFiles ??
|
|
1256
|
+
(state.projectIdentityProbedFiles = new Set()),
|
|
1257
|
+
commandChannel: {
|
|
1258
|
+
executeCommand: (command, args) => runReadOnlyServerCommand(state, command, args),
|
|
1259
|
+
},
|
|
1260
|
+
});
|
|
1152
1261
|
}
|
|
1153
1262
|
export async function handleNotifyChange(state, filePath, content) {
|
|
1154
1263
|
if (!isClientAlive(state))
|
|
@@ -1162,6 +1271,8 @@ export async function handleNotifyChange(state, filePath, content) {
|
|
|
1162
1271
|
textDocument: { uri, languageId: "plaintext", version: 0, text: content },
|
|
1163
1272
|
});
|
|
1164
1273
|
state.documentVersions.set(normalizedPath, 0);
|
|
1274
|
+
state.documentOpenedAt.set(normalizedPath, Date.now());
|
|
1275
|
+
state.diagnosticPublicationCounts.set(normalizedPath, 0);
|
|
1165
1276
|
recordSentContent(state, normalizedPath, 0, content);
|
|
1166
1277
|
state.openDocuments.add(normalizedPath);
|
|
1167
1278
|
state.openDocumentUris?.set(normalizedPath, uri);
|
|
@@ -1194,6 +1305,13 @@ export async function closeDocument(state, filePath) {
|
|
|
1194
1305
|
state.closedDocuments?.add(normalizedPath);
|
|
1195
1306
|
state.openDocumentUris?.delete(normalizedPath);
|
|
1196
1307
|
state.documentVersions.delete(normalizedPath);
|
|
1308
|
+
state.documentOpenedAt.delete(normalizedPath);
|
|
1309
|
+
state.diagnosticPublicationCounts.delete(normalizedPath);
|
|
1310
|
+
// #1412 L1: projectIdentityProbedFiles is a claim-once memo scoped to the
|
|
1311
|
+
// document's open lifetime (re-probing a closed-then-reopened file is
|
|
1312
|
+
// harmless and cheap) — mirror openDocuments' own per-close cleanup so it
|
|
1313
|
+
// doesn't grow unbounded across a long session's worth of open/close churn.
|
|
1314
|
+
state.projectIdentityProbedFiles?.delete(normalizedPath);
|
|
1197
1315
|
clearDiagnosticsForPath(state, normalizedPath);
|
|
1198
1316
|
}
|
|
1199
1317
|
export async function clientShutdown(state, options = {}) {
|
|
@@ -1208,6 +1326,9 @@ export async function clientShutdown(state, options = {}) {
|
|
|
1208
1326
|
state.pendingOpens.clear();
|
|
1209
1327
|
state.openDocuments.clear();
|
|
1210
1328
|
state.openDocumentUris?.clear();
|
|
1329
|
+
// #1412 L1: mirror openDocuments' clear — a shut-down/evicted client's
|
|
1330
|
+
// probe memo is moot along with everything else document-scoped.
|
|
1331
|
+
state.projectIdentityProbedFiles?.clear();
|
|
1211
1332
|
// #271: drop any pending watched-files batch + its timer (a dying client's
|
|
1212
1333
|
// queued FS changes are moot, and the timer must not outlive the connection).
|
|
1213
1334
|
state.watchQueue?.cancel();
|
|
@@ -1409,6 +1530,43 @@ export async function runServerCommand(state, command, args, timeoutMs = EXECUTE
|
|
|
1409
1530
|
state.activeMutationContext = undefined;
|
|
1410
1531
|
}
|
|
1411
1532
|
}
|
|
1533
|
+
// #1412 H1/H2: read-only sibling of runServerCommand for telemetry/identity
|
|
1534
|
+
// probes that must NOT participate in the mutation-acceptance window. Unlike
|
|
1535
|
+
// runServerCommand this never touches serverEditsAllowed, activeMutationDepth,
|
|
1536
|
+
// or activeMutationContext — a probe firing mid-flight must leave a concurrent
|
|
1537
|
+
// real executeCommand's mutation context untouched, and must not itself open
|
|
1538
|
+
// the workspace/applyEdit acceptance window (client.ts's applyEdit handler
|
|
1539
|
+
// gates on serverEditsAllowed > 0). Preserves the allowlist-by-advertisement
|
|
1540
|
+
// invariant. Short PROBE_COMMAND_TIMEOUT_MS backstop — this is a diagnostic
|
|
1541
|
+
// sample, not a mutation, and must never hold anything up for anywhere near
|
|
1542
|
+
// EXECUTE_COMMAND_TIMEOUT_MS.
|
|
1543
|
+
export async function runReadOnlyServerCommand(state, command, args, timeoutMs = PROBE_COMMAND_TIMEOUT_MS) {
|
|
1544
|
+
if (!isClientAlive(state)) {
|
|
1545
|
+
return { executed: false, reason: "lsp client not alive" };
|
|
1546
|
+
}
|
|
1547
|
+
if (!state.advertisedCommands.has(command)) {
|
|
1548
|
+
return {
|
|
1549
|
+
executed: false,
|
|
1550
|
+
reason: `command "${command}" is not advertised by the ${state.serverId} server`,
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
const result = await withTimeout(safeSendRequest(state.connection, "workspace/executeCommand", {
|
|
1555
|
+
command,
|
|
1556
|
+
arguments: args ?? [],
|
|
1557
|
+
}), timeoutMs);
|
|
1558
|
+
return { executed: true, result };
|
|
1559
|
+
}
|
|
1560
|
+
catch (err) {
|
|
1561
|
+
if (err instanceof Error && err.message.startsWith("Timeout after")) {
|
|
1562
|
+
return {
|
|
1563
|
+
executed: false,
|
|
1564
|
+
reason: `workspace/executeCommand timed out after ${timeoutMs}ms`,
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
throw err;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1412
1570
|
function validateWorkspaceEditVersions(state, edit) {
|
|
1413
1571
|
for (const change of edit.documentChanges ?? []) {
|
|
1414
1572
|
if (typeof change !== "object" || change === null || !("textDocument" in change))
|
|
@@ -1592,6 +1750,8 @@ export async function createLSPClient(options) {
|
|
|
1592
1750
|
documentPullDiagnosticTimestamps: new Map(),
|
|
1593
1751
|
pullFailureHistory: [],
|
|
1594
1752
|
pendingDiagnostics: new Map(),
|
|
1753
|
+
diagnosticPublicationCounts: new Map(),
|
|
1754
|
+
documentOpenedAt: new Map(),
|
|
1595
1755
|
diagnosticEmitter,
|
|
1596
1756
|
diagnosticsVersion: 0,
|
|
1597
1757
|
documentVersions: new Map(),
|
|
@@ -1604,6 +1764,7 @@ export async function createLSPClient(options) {
|
|
|
1604
1764
|
closedDocuments: new Set(),
|
|
1605
1765
|
openDocumentUris: new Map(),
|
|
1606
1766
|
pendingOpens: new Set(),
|
|
1767
|
+
projectIdentityProbedFiles: new Set(),
|
|
1607
1768
|
// these are filled in after initialize — cast to avoid two-phase init
|
|
1608
1769
|
workspaceDiagnosticsSupport: undefined,
|
|
1609
1770
|
operationSupport: undefined,
|
|
@@ -2,6 +2,35 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { PathKeyedMap } from "../path-keyed-map.js";
|
|
4
4
|
import { normalizeEphemeralMapKey } from "../path-utils.js";
|
|
5
|
+
/**
|
|
6
|
+
* #1470: the single source of truth for "which servers did this touch NOT hear
|
|
7
|
+
* from". Read this instead of comparing `confirmation` to a string literal — a
|
|
8
|
+
* consumer that tests `confirmation === "confirmed"` is correct only by accident
|
|
9
|
+
* (it happens to fail closed for `"partial"`), and one that tests `!inconclusive`
|
|
10
|
+
* is outright wrong, because a partially-confirmed touch is deliberately NOT
|
|
11
|
+
* inconclusive: the primary's answer is still trustworthy and must survive.
|
|
12
|
+
*/
|
|
13
|
+
export function touchCoverageGap(result) {
|
|
14
|
+
return result?.unconfirmedServerIds ?? [];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* #1470: did this touch complete its configured confirmation policy at all —
|
|
18
|
+
* `"confirmed"` (for every spawned server) or `"partial"` (for every server
|
|
19
|
+
* except the named cut-off auxiliaries)?
|
|
20
|
+
*
|
|
21
|
+
* This is deliberately NOT `confirmation === "confirmed"`. A consumer asking
|
|
22
|
+
* "did the PRIMARY confirm?" must answer yes for a partial touch: `partial`
|
|
23
|
+
* implies neither the notify write nor the diagnostics wait lapsed, so the
|
|
24
|
+
* silent-clean gates ran to completion exactly as they do for a full
|
|
25
|
+
* confirmation. Reading `=== "confirmed"` there looks safely fail-closed but
|
|
26
|
+
* reports "the language server could not confirm clean" when the truth is "the
|
|
27
|
+
* language server confirmed clean and a scanner was cut off" — the same overclaim
|
|
28
|
+
* pointing the other way. Pair this with {@link touchCoverageGap}, which names
|
|
29
|
+
* what the touch does not speak for.
|
|
30
|
+
*/
|
|
31
|
+
export function touchCompletedConfirmationPolicy(result) {
|
|
32
|
+
return result?.confirmation !== undefined;
|
|
33
|
+
}
|
|
5
34
|
/**
|
|
6
35
|
* Fingerprint the EXACT text handed to didOpen/didChange. sha256 over the raw
|
|
7
36
|
* string — no normalization — so the disk comparison (which reads the file with
|