pi-lens 3.8.63 → 3.8.64
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/CHANGELOG.md +22 -0
- package/README.md +12 -9
- package/dist/clients/deadline-utils.js +23 -0
- package/dist/clients/installer/index.js +18 -27
- package/dist/clients/log-cleanup.js +36 -13
- package/dist/clients/lsp/client.js +44 -0
- package/dist/clients/lsp/index.js +208 -33
- package/dist/clients/lsp/interactive-install.js +14 -4
- package/dist/clients/lsp/launch.js +29 -100
- package/dist/clients/mcp/review.js +7 -4
- package/dist/clients/package-manager.js +223 -0
- package/dist/clients/pipeline.js +55 -2
- package/dist/clients/project-metadata.js +3 -23
- package/dist/clients/safe-spawn.js +9 -0
- package/dist/clients/tree-sitter-client.js +72 -14
- package/dist/index.js +16 -0
- package/dist/mcp/server.js +4 -2
- package/dist/tools/ast-grep-search.js +10 -6
- package/dist/tools/lens-diagnostics.js +43 -10
- package/dist/tools/lsp-diagnostics.js +23 -4
- package/dist/tools/scan-progress.js +38 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-bash.wasm.json +5 -0
- package/grammars/tree-sitter-css.wasm +0 -0
- package/grammars/tree-sitter-css.wasm.json +5 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-go.wasm.json +5 -0
- package/grammars/tree-sitter-html.wasm +0 -0
- package/grammars/tree-sitter-html.wasm.json +5 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-java.wasm.json +5 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm.json +5 -0
- package/grammars/tree-sitter-json.wasm +0 -0
- package/grammars/tree-sitter-json.wasm.json +5 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-python.wasm.json +5 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm.json +5 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm.json +5 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm.json +5 -0
- package/grammars/tree-sitter-yaml.wasm +0 -0
- package/grammars/tree-sitter-yaml.wasm.json +5 -0
- package/package.json +7 -5
- package/scripts/analyze-pi-lens-logs.mjs +101 -3
- package/scripts/download-grammars.js +171 -30
- package/scripts/grammars.lock.json +32 -0
|
@@ -11,15 +11,26 @@
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { Type } from "../clients/deps/typebox.js";
|
|
13
13
|
import { compactRenderResult } from "./render-compact.js";
|
|
14
|
+
import { combineAbortSignals } from "../clients/deadline-utils.js";
|
|
14
15
|
import { getProjectIgnoreMatcher } from "../clients/file-utils.js";
|
|
15
16
|
import { getLSPService } from "../clients/lsp/index.js";
|
|
16
17
|
import { loadProjectDiagnosticsDeltaReport, loadProjectDiagnosticsSnapshot, reconcileProjectDiagnosticsSnapshot, } from "../clients/project-diagnostics/cache.js";
|
|
17
18
|
import { scanProjectDiagnostics } from "../clients/project-diagnostics/scanner.js";
|
|
18
19
|
import { getFileDiagnosticSummaries, reconcileStaleWidgetFiles, } from "../clients/widget-state.js";
|
|
20
|
+
import { makeProgressReporter } from "./scan-progress.js";
|
|
19
21
|
// The widget state exposes the full per-file diagnostic set; this is the tool's
|
|
20
22
|
// own generous display budget per file (independent of the TUI's 12 cap), to
|
|
21
23
|
// keep output bounded on a pathologically broken file.
|
|
22
24
|
const MAX_DIAGNOSTICS_PER_FILE = 50;
|
|
25
|
+
// Wall-clock ceiling for the whole mode=full scan. Even with per-file budgets
|
|
26
|
+
// and abort-signal honoring, a pathological state (a language server hanging on
|
|
27
|
+
// spawn/initialize across many files) could otherwise stall the tool
|
|
28
|
+
// indefinitely — an unattended session was observed hung for ~8h. This hard cap
|
|
29
|
+
// aborts the scan so it always returns (partial) rather than never. Env-tunable.
|
|
30
|
+
const FULL_SCAN_WALL_CLOCK_MS = (() => {
|
|
31
|
+
const raw = Number(process.env.PI_LENS_LENS_DIAGNOSTICS_FULL_TIMEOUT_MS);
|
|
32
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 180_000; // 3 min default
|
|
33
|
+
})();
|
|
23
34
|
export function createLensDiagnosticsTool(cacheManager, getCwd, getLspService = getLSPService,
|
|
24
35
|
// Flush any debounced per-edit dispatches before reading, so files the agent
|
|
25
36
|
// fixed earlier in the turn are re-dispatched and the widget reflects the
|
|
@@ -95,7 +106,7 @@ flushPending = async () => { }) {
|
|
|
95
106
|
description: "Filter by severity (default: all).",
|
|
96
107
|
})),
|
|
97
108
|
}),
|
|
98
|
-
async execute(_toolCallId, params, signal,
|
|
109
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
99
110
|
const mode = params.mode ?? "delta";
|
|
100
111
|
const severity = params.severity ?? "all";
|
|
101
112
|
const refreshRunners = params.refreshRunners;
|
|
@@ -105,6 +116,11 @@ flushPending = async () => { }) {
|
|
|
105
116
|
const maxProjectFiles = parsePositiveInt(params.maxProjectFiles);
|
|
106
117
|
const maxLspFiles = parsePositiveInt(params.maxLspFiles);
|
|
107
118
|
const cwd = ctx.cwd ?? getCwd();
|
|
119
|
+
// Escape aborts the agent *turn*, which fires ctx.signal (the turn-wired
|
|
120
|
+
// abort); the positional signal is the tool-call signal. A registered
|
|
121
|
+
// extension tool only reliably sees the turn abort via ctx.signal, so honor
|
|
122
|
+
// BOTH — else a long mode=full scan ignores Escape (the reported bug).
|
|
123
|
+
const abortSignal = combineAbortSignals(signal, ctx.signal);
|
|
108
124
|
// Reflect the agent's just-made fixes before reporting: flush pending
|
|
109
125
|
// per-edit dispatches (re-records fixed files), then drop entries whose
|
|
110
126
|
// file changed on disk afterwards / was deleted (stale, e.g. external
|
|
@@ -115,11 +131,21 @@ flushPending = async () => { }) {
|
|
|
115
131
|
return formatAllMode(cwd, severity, undefined, undefined, staleDropped);
|
|
116
132
|
}
|
|
117
133
|
if (mode === "full") {
|
|
134
|
+
// Fold a hard wall-clock ceiling into the abort signal so the scan
|
|
135
|
+
// always terminates (partial) even if a hung server would otherwise
|
|
136
|
+
// stall it forever. AbortSignal.timeout aborts with a TimeoutError.
|
|
137
|
+
const ceiling = AbortSignal.timeout(FULL_SCAN_WALL_CLOCK_MS);
|
|
138
|
+
const fullSignal = combineAbortSignals(abortSignal, ceiling);
|
|
139
|
+
// Stream a throttled progress bar: the full scan is opaque for minutes
|
|
140
|
+
// otherwise.
|
|
141
|
+
const onProgress = makeProgressReporter(onUpdate);
|
|
118
142
|
return formatFullMode(cwd, severity, getLspService(), {
|
|
119
143
|
refreshRunners,
|
|
120
144
|
maxProjectFiles,
|
|
121
145
|
maxLspFiles,
|
|
122
|
-
signal,
|
|
146
|
+
signal: fullSignal,
|
|
147
|
+
wallClockMs: FULL_SCAN_WALL_CLOCK_MS,
|
|
148
|
+
onProgress,
|
|
123
149
|
});
|
|
124
150
|
}
|
|
125
151
|
return formatDeltaMode(cacheManager, cwd, severity);
|
|
@@ -421,6 +447,7 @@ async function formatFullMode(cwd, severity, lspService, options = {}) {
|
|
|
421
447
|
runWorkspaceDiagnostics.call(lspService, cwd, {
|
|
422
448
|
maxFiles: options.maxLspFiles,
|
|
423
449
|
signal,
|
|
450
|
+
onProgress: options.onProgress,
|
|
424
451
|
}),
|
|
425
452
|
getProjectDiagnosticsSnapshotForFullMode(cwd, options),
|
|
426
453
|
]);
|
|
@@ -449,16 +476,22 @@ async function formatFullMode(cwd, severity, lspService, options = {}) {
|
|
|
449
476
|
turnIndex: projectDelta.turnIndex,
|
|
450
477
|
},
|
|
451
478
|
});
|
|
452
|
-
//
|
|
453
|
-
//
|
|
479
|
+
// Stopped mid-scan: the results above are whatever completed before the abort.
|
|
480
|
+
// Tell the agent so it doesn't read a partial sweep as "clean" (#341). The
|
|
481
|
+
// abort is either a user/turn cancel (Escape) or the wall-clock ceiling firing
|
|
482
|
+
// (AbortSignal.timeout → TimeoutError), which guarantees the scan can't hang
|
|
483
|
+
// indefinitely — distinguish them so the agent knows whether to just re-run.
|
|
454
484
|
if (aborted) {
|
|
455
|
-
const
|
|
456
|
-
|
|
485
|
+
const timedOut = signal
|
|
486
|
+
?.reason?.name === "TimeoutError";
|
|
487
|
+
const note = timedOut
|
|
488
|
+
? `\n\n⚠ Scan exceeded its ${Math.round((options.wallClockMs ?? 0) / 1000)}s time budget and was stopped — results are partial. ` +
|
|
489
|
+
"Narrow it with maxLspFiles, or raise PI_LENS_LENS_DIAGNOSTICS_FULL_TIMEOUT_MS."
|
|
490
|
+
: "\n\n⚠ Scan cancelled before completion — results are partial. " +
|
|
491
|
+
"Re-run with a smaller maxLspFiles to finish within budget.";
|
|
457
492
|
return {
|
|
458
|
-
content: [
|
|
459
|
-
|
|
460
|
-
],
|
|
461
|
-
details: result.details,
|
|
493
|
+
content: [{ type: "text", text: result.content[0].text + note }],
|
|
494
|
+
details: { ...result.details, timedOut },
|
|
462
495
|
};
|
|
463
496
|
}
|
|
464
497
|
return result;
|
|
@@ -9,7 +9,9 @@ import * as path from "node:path";
|
|
|
9
9
|
import { Type } from "../clients/deps/typebox.js";
|
|
10
10
|
import { getProjectIgnoreMatcher, isExcludedDirName, } from "../clients/file-utils.js";
|
|
11
11
|
import { getLSPService } from "../clients/lsp/index.js";
|
|
12
|
+
import { combineAbortSignals } from "../clients/deadline-utils.js";
|
|
12
13
|
import { baseName, compactRenderResult } from "./render-compact.js";
|
|
14
|
+
import { makeProgressReporter } from "./scan-progress.js";
|
|
13
15
|
const LANG_EXTENSIONS = {
|
|
14
16
|
".ts": [".ts", ".tsx", ".mts", ".cts"],
|
|
15
17
|
".tsx": [".ts", ".tsx", ".mts", ".cts"],
|
|
@@ -76,17 +78,24 @@ function boundedPositiveInt(value, fallback, min, max) {
|
|
|
76
78
|
return fallback;
|
|
77
79
|
return Math.max(min, Math.min(max, parsed));
|
|
78
80
|
}
|
|
79
|
-
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
81
|
+
async function mapWithConcurrency(items, concurrency, mapper, signal, onProgress) {
|
|
80
82
|
const results = [];
|
|
81
83
|
let nextIndex = 0;
|
|
84
|
+
let completed = 0;
|
|
82
85
|
const workers = Math.min(Math.max(1, concurrency), items.length);
|
|
83
86
|
await Promise.all(Array.from({ length: workers }, async () => {
|
|
84
87
|
while (true) {
|
|
88
|
+
// Honor cancellation (Escape / turn abort): stop pulling new items
|
|
89
|
+
// rather than grind the whole batch. Completed entries are returned.
|
|
90
|
+
if (signal?.aborted)
|
|
91
|
+
return;
|
|
85
92
|
const index = nextIndex;
|
|
86
93
|
nextIndex += 1;
|
|
87
94
|
if (index >= items.length)
|
|
88
95
|
return;
|
|
89
96
|
results[index] = await mapper(items[index], index);
|
|
97
|
+
completed += 1;
|
|
98
|
+
onProgress?.(completed, items.length);
|
|
90
99
|
}
|
|
91
100
|
}));
|
|
92
101
|
return results;
|
|
@@ -182,7 +191,13 @@ export function createLspDiagnosticsTool() {
|
|
|
182
191
|
description: "Optional per-file LSP wait budget for batch diagnostics. Uses server defaults when omitted.",
|
|
183
192
|
})),
|
|
184
193
|
}),
|
|
185
|
-
async execute(_toolCallId, params, _signal,
|
|
194
|
+
async execute(_toolCallId, params, _signal, onUpdate, ctx) {
|
|
195
|
+
// Escape aborts the turn via ctx.signal; honor both it and the tool-call
|
|
196
|
+
// signal so a batch/directory scan cancels rather than grinding on.
|
|
197
|
+
const signal = combineAbortSignals(_signal, ctx.signal);
|
|
198
|
+
// Stream a throttled progress bar for batch/directory scans (opaque for
|
|
199
|
+
// seconds-to-minutes otherwise).
|
|
200
|
+
const onProgress = makeProgressReporter(onUpdate, "Scanning LSP diagnostics");
|
|
186
201
|
const typedParams = params;
|
|
187
202
|
const severity = (typedParams.severity ?? "all");
|
|
188
203
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -209,6 +224,8 @@ export function createLspDiagnosticsTool() {
|
|
|
209
224
|
return runBatchFileDiagnostics(absPaths, severity, lspService, {
|
|
210
225
|
concurrency,
|
|
211
226
|
waitMs,
|
|
227
|
+
signal,
|
|
228
|
+
onProgress,
|
|
212
229
|
});
|
|
213
230
|
}
|
|
214
231
|
const rawPath = typedParams.path;
|
|
@@ -244,6 +261,8 @@ export function createLspDiagnosticsTool() {
|
|
|
244
261
|
return runDirectoryDiagnostics(absPath, severity, lspService, {
|
|
245
262
|
concurrency,
|
|
246
263
|
waitMs,
|
|
264
|
+
signal,
|
|
265
|
+
onProgress,
|
|
247
266
|
});
|
|
248
267
|
}
|
|
249
268
|
return runFileDiagnostics(absPath, severity, lspService, waitMs);
|
|
@@ -354,7 +373,7 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
|
|
|
354
373
|
details: { mode: "batch", severity, filesChecked: 0 },
|
|
355
374
|
};
|
|
356
375
|
}
|
|
357
|
-
const results = await mapWithConcurrency(absPaths, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs));
|
|
376
|
+
const results = await mapWithConcurrency(absPaths, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs), options.signal, options.onProgress);
|
|
358
377
|
const fileErrors = results.flatMap((result) => result.error ? [result.error] : []);
|
|
359
378
|
const lspHealthWarnings = results.flatMap((result) => result.unavailable ? [result.unavailable] : []);
|
|
360
379
|
const allDiags = results.flatMap((result) => result.diagnostics);
|
|
@@ -436,7 +455,7 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
|
|
|
436
455
|
}
|
|
437
456
|
const wasCapped = collectedFiles.length > MAX_FILES;
|
|
438
457
|
const filesToProcess = collectedFiles.slice(0, MAX_FILES);
|
|
439
|
-
const results = await mapWithConcurrency(filesToProcess, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs));
|
|
458
|
+
const results = await mapWithConcurrency(filesToProcess, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs), options.signal, options.onProgress);
|
|
440
459
|
const fileErrors = results.flatMap((result) => result.error ? [result.error] : []);
|
|
441
460
|
const lspHealthWarnings = results.flatMap((result) => result.unavailable ? [result.unavailable] : []);
|
|
442
461
|
const allDiags = results.flatMap((result) => result.diagnostics);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared progress-streaming helper for the long-running full/batch/directory
|
|
3
|
+
* diagnostic scans (`lens_diagnostics mode=full`, `lsp_diagnostics`). Those runs
|
|
4
|
+
* are opaque for minutes; this streams a throttled progress bar to the tool's
|
|
5
|
+
* `onUpdate` callback so the agent/user sees movement.
|
|
6
|
+
*/
|
|
7
|
+
/** A ≤20-char ASCII bar + counts, e.g. `Scanning… [████░░░░░░] 45/123 (37%)`. */
|
|
8
|
+
export function renderScanProgress(completed, total, label = "Scanning project diagnostics") {
|
|
9
|
+
const pct = total > 0 ? Math.min(100, Math.round((completed / total) * 100)) : 0;
|
|
10
|
+
const width = 20;
|
|
11
|
+
const filled = Math.round((pct / 100) * width);
|
|
12
|
+
const bar = "█".repeat(filled) + "░".repeat(Math.max(0, width - filled));
|
|
13
|
+
return `${label}… [${bar}] ${completed}/${total} (${pct}%)`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build a throttled `(completed, total) => void` that streams the progress bar to
|
|
17
|
+
* `onUpdate` — at most once per `throttleMs` (default 250ms, ~4×/s) plus the
|
|
18
|
+
* final tick so the bar always lands on 100%. Returns `undefined` when the SDK
|
|
19
|
+
* gave no callback, so callers can pass it straight through as an optional.
|
|
20
|
+
*/
|
|
21
|
+
export function makeProgressReporter(onUpdate, label, throttleMs = 250) {
|
|
22
|
+
const emit = onUpdate;
|
|
23
|
+
if (typeof emit !== "function")
|
|
24
|
+
return undefined;
|
|
25
|
+
let lastEmit = 0;
|
|
26
|
+
return (completed, total) => {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
if (completed < total && now - lastEmit < throttleMs)
|
|
29
|
+
return;
|
|
30
|
+
lastEmit = now;
|
|
31
|
+
emit({
|
|
32
|
+
content: [
|
|
33
|
+
{ type: "text", text: renderScanProgress(completed, total, label) },
|
|
34
|
+
],
|
|
35
|
+
details: { phase: "scanning", completed, total },
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.64",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
|
|
6
6
|
"repository": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc --project tsconfig.build.json",
|
|
17
17
|
"build:dist": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc --project tsconfig.dist.json --noCheck",
|
|
18
|
-
"prepare": "npm run build:dist",
|
|
18
|
+
"prepare": "npm run build:dist && node scripts/download-grammars.js --core --dest grammars",
|
|
19
19
|
"lint": "tsc --project tsconfig.json",
|
|
20
20
|
"watch": "tsc --watch",
|
|
21
21
|
"test": "vitest run",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"check": "node scripts/check-extensions.mjs",
|
|
26
26
|
"selftest:install": "node scripts/install-selftest.mjs",
|
|
27
27
|
"check:lockfile": "node scripts/check-lockfile-sync.mjs",
|
|
28
|
+
"check:grammars": "node scripts/check-grammar-provenance.mjs",
|
|
28
29
|
"audit:tree-sitter": "node scripts/audit-tree-sitter-rules.mjs",
|
|
29
30
|
"audit:rule-catalog": "node scripts/validate-rule-catalog.mjs",
|
|
30
31
|
"audit:playground": "node scripts/playground-verify-rule.mjs",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"changelog:release": "node scripts/changelog-release.mjs",
|
|
34
35
|
"changelog:extract": "node scripts/changelog-extract.mjs",
|
|
35
36
|
"release:backfill-notes": "node scripts/backfill-github-releases.mjs",
|
|
37
|
+
"release:backfill-thanks": "node scripts/backfill-release-thanks.mjs",
|
|
36
38
|
"docs:rule-catalogs": "node scripts/gen-rule-catalogs.mjs",
|
|
37
39
|
"banner": "npx -y sharp-cli --input banner.svg --output banner.png resize 2200 620",
|
|
38
40
|
"download-grammars": "node scripts/download-grammars.js",
|
|
@@ -40,8 +42,7 @@
|
|
|
40
42
|
"harness:python-poc": "node scripts/run-harness.mjs cases/python/pipeline-dispatch-poc --pi-bin \"C:\\Users\\R3LiC\\AppData\\Roaming\\npm\\pi.cmd\"",
|
|
41
43
|
"harness:ts-format-poc": "node scripts/run-harness.mjs cases/ts/format-default-poc --pi-bin \"C:\\Users\\R3LiC\\AppData\\Roaming\\npm\\pi.cmd\"",
|
|
42
44
|
"harness:python-autofix-poc": "node scripts/run-harness.mjs cases/python/autofix-default-poc --pi-bin \"C:\\Users\\R3LiC\\AppData\\Roaming\\npm\\pi.cmd\"",
|
|
43
|
-
"harness:go-poc": "node scripts/run-harness.mjs cases/go/pipeline-dispatch-poc --pi-bin \"C:\\Users\\R3LiC\\AppData\\Roaming\\npm\\pi.cmd\""
|
|
44
|
-
"postinstall": "node scripts/download-grammars.js"
|
|
45
|
+
"harness:go-poc": "node scripts/run-harness.mjs cases/go/pipeline-dispatch-poc --pi-bin \"C:\\Users\\R3LiC\\AppData\\Roaming\\npm\\pi.cmd\""
|
|
45
46
|
},
|
|
46
47
|
"keywords": [
|
|
47
48
|
"pi",
|
|
@@ -62,10 +63,12 @@
|
|
|
62
63
|
"license": "MIT",
|
|
63
64
|
"files": [
|
|
64
65
|
"dist/",
|
|
66
|
+
"grammars/",
|
|
65
67
|
"config/",
|
|
66
68
|
"rules/",
|
|
67
69
|
"skills/",
|
|
68
70
|
"scripts/download-grammars.js",
|
|
71
|
+
"scripts/grammars.lock.json",
|
|
69
72
|
"scripts/analyze-pi-lens-logs.mjs",
|
|
70
73
|
"scripts/install-selftest.mjs",
|
|
71
74
|
"scripts/rpc-load-check.mjs",
|
|
@@ -93,7 +96,6 @@
|
|
|
93
96
|
}
|
|
94
97
|
},
|
|
95
98
|
"optionalDependencies": {
|
|
96
|
-
"tree-sitter-wasms": "npm:null@^0.11.0",
|
|
97
99
|
"web-tree-sitter": "0.25.10"
|
|
98
100
|
},
|
|
99
101
|
"devDependencies": {
|
|
@@ -170,6 +170,16 @@ function createState(files) {
|
|
|
170
170
|
slowTotals: [],
|
|
171
171
|
toolResults: counter(),
|
|
172
172
|
phaseCounts: counter(),
|
|
173
|
+
phaseTimeouts: counter(),
|
|
174
|
+
workspace: {
|
|
175
|
+
started: 0,
|
|
176
|
+
completed: 0,
|
|
177
|
+
aborted: 0,
|
|
178
|
+
timedOutSweeps: 0,
|
|
179
|
+
timedOutFilesTotal: 0,
|
|
180
|
+
sweeps: [],
|
|
181
|
+
progress: [],
|
|
182
|
+
},
|
|
173
183
|
},
|
|
174
184
|
diagnostics: {
|
|
175
185
|
bySeverity: counter(),
|
|
@@ -293,6 +303,7 @@ async function analyzeLatency(files, state) {
|
|
|
293
303
|
} else if (entry.type === "phase") {
|
|
294
304
|
const phase = entry.phase ?? "unknown";
|
|
295
305
|
state.latency.phaseCounts.inc(phase);
|
|
306
|
+
if (/_timeout$/.test(phase)) state.latency.phaseTimeouts.inc(phase);
|
|
296
307
|
if (
|
|
297
308
|
phase === "total" &&
|
|
298
309
|
(entry.durationMs ?? 0) >= thresholds.totalSlowMs
|
|
@@ -304,6 +315,33 @@ async function analyzeLatency(files, state) {
|
|
|
304
315
|
limit * 3,
|
|
305
316
|
byDuration,
|
|
306
317
|
);
|
|
318
|
+
} else if (phase === "lsp_workspace_diagnostics_start") {
|
|
319
|
+
state.latency.workspace.started += 1;
|
|
320
|
+
} else if (phase === "lsp_workspace_diagnostics_progress") {
|
|
321
|
+
// Keep the heartbeats so an incomplete sweep can show how far it
|
|
322
|
+
// got (completed X/Y) before it went silent — the hang forensics.
|
|
323
|
+
pushTop(
|
|
324
|
+
state.latency.workspace.progress,
|
|
325
|
+
summarizeWorkspaceSweep(entry),
|
|
326
|
+
limit * 3,
|
|
327
|
+
byDuration,
|
|
328
|
+
);
|
|
329
|
+
} else if (phase === "lsp_workspace_diagnostics") {
|
|
330
|
+
const ws = state.latency.workspace;
|
|
331
|
+
ws.completed += 1;
|
|
332
|
+
if (entry.metadata?.aborted) ws.aborted += 1;
|
|
333
|
+
const timedOut = Number(entry.metadata?.timedOutFiles ?? 0);
|
|
334
|
+
if (timedOut > 0) {
|
|
335
|
+
ws.timedOutSweeps += 1;
|
|
336
|
+
ws.timedOutFilesTotal += timedOut;
|
|
337
|
+
state.smellTotals.inc("lsp-workspace-file-timeouts");
|
|
338
|
+
pushTop(
|
|
339
|
+
ws.sweeps,
|
|
340
|
+
summarizeWorkspaceSweep(entry),
|
|
341
|
+
limit * 3,
|
|
342
|
+
byDuration,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
307
345
|
}
|
|
308
346
|
} else if (entry.type === "tool_result") {
|
|
309
347
|
state.latency.toolResults.inc(entry.result ?? "unknown");
|
|
@@ -539,9 +577,12 @@ async function analyzeSessionStart(files, state) {
|
|
|
539
577
|
);
|
|
540
578
|
}
|
|
541
579
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
)
|
|
580
|
+
// Runtime logs "success runMs=<n> queuedMs=<n>"; older logs used
|
|
581
|
+
// "success (<n>ms)". Match both so a format drift can't silently zero
|
|
582
|
+
// this smell again (it did: the `(<n>ms)` regex matched 0 of ~2k rows).
|
|
583
|
+
const task =
|
|
584
|
+
/session_start task ([^:]+): success runMs=(\d+)/.exec(message) ??
|
|
585
|
+
/session_start task ([^:]+): success \((\d+)ms\)/.exec(message);
|
|
545
586
|
if (task && Number(task[2]) >= thresholds.backgroundSlowMs) {
|
|
546
587
|
state.smellTotals.inc("slow-background-tasks");
|
|
547
588
|
pushTop(
|
|
@@ -798,6 +839,24 @@ function summarizeLatency(entry) {
|
|
|
798
839
|
};
|
|
799
840
|
}
|
|
800
841
|
|
|
842
|
+
function summarizeWorkspaceSweep(entry) {
|
|
843
|
+
const md = entry.metadata ?? {};
|
|
844
|
+
const bits = [];
|
|
845
|
+
if (md.completed != null)
|
|
846
|
+
bits.push(`completed ${md.completed}/${md.total ?? md.fileCount ?? "?"}`);
|
|
847
|
+
else if (md.filesChecked != null) bits.push(`checked ${md.filesChecked}`);
|
|
848
|
+
if (md.timedOutFiles) bits.push(`timedOutFiles=${md.timedOutFiles}`);
|
|
849
|
+
if (md.aborted) bits.push("aborted");
|
|
850
|
+
if (md.perFileMs) bits.push(`perFileMs=${md.perFileMs}`);
|
|
851
|
+
return {
|
|
852
|
+
ts: entry.ts,
|
|
853
|
+
durationMs: entry.durationMs,
|
|
854
|
+
project: projectOf(entry.filePath),
|
|
855
|
+
filePath: shortPath(entry.filePath),
|
|
856
|
+
message: bits.join(", "),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
801
860
|
function summarizeDiagnostic(entry) {
|
|
802
861
|
return {
|
|
803
862
|
ts: entry.timestamp ?? entry.ts,
|
|
@@ -1047,6 +1106,22 @@ function buildReport(state) {
|
|
|
1047
1106
|
"Actionable-warnings advisory pipeline logged an error",
|
|
1048
1107
|
state.actionable.errors.slice(0, limit),
|
|
1049
1108
|
);
|
|
1109
|
+
const ws = state.latency.workspace;
|
|
1110
|
+
const incompleteSweeps = Math.max(0, ws.started - ws.completed);
|
|
1111
|
+
addSmell(
|
|
1112
|
+
smells,
|
|
1113
|
+
"lsp-workspace-diagnostics-incomplete",
|
|
1114
|
+
incompleteSweeps,
|
|
1115
|
+
"Full LSP workspace sweeps that logged a start but never a completion (hang/kill signature — the 8h-hang class #383 hardened)",
|
|
1116
|
+
ws.progress.slice(0, limit),
|
|
1117
|
+
);
|
|
1118
|
+
addSmell(
|
|
1119
|
+
smells,
|
|
1120
|
+
"lsp-workspace-file-timeouts",
|
|
1121
|
+
smellCount("lsp-workspace-file-timeouts"),
|
|
1122
|
+
`Full LSP sweeps where files hit the per-file budget (${ws.timedOutFilesTotal} files across ${ws.timedOutSweeps} sweeps)`,
|
|
1123
|
+
ws.sweeps.slice(0, limit),
|
|
1124
|
+
);
|
|
1050
1125
|
|
|
1051
1126
|
return {
|
|
1052
1127
|
window: state.window,
|
|
@@ -1071,6 +1146,15 @@ function buildReport(state) {
|
|
|
1071
1146
|
runnerBlockingFindings: state.latency.runnerBlockingFindings.toJSON(),
|
|
1072
1147
|
toolResults: state.latency.toolResults.toJSON(),
|
|
1073
1148
|
phaseCounts: state.latency.phaseCounts.top(limit),
|
|
1149
|
+
phaseTimeouts: state.latency.phaseTimeouts.toJSON(),
|
|
1150
|
+
workspaceDiagnostics: {
|
|
1151
|
+
started: ws.started,
|
|
1152
|
+
completed: ws.completed,
|
|
1153
|
+
incomplete: incompleteSweeps,
|
|
1154
|
+
aborted: ws.aborted,
|
|
1155
|
+
timedOutSweeps: ws.timedOutSweeps,
|
|
1156
|
+
timedOutFilesTotal: ws.timedOutFilesTotal,
|
|
1157
|
+
},
|
|
1074
1158
|
},
|
|
1075
1159
|
cascade: {
|
|
1076
1160
|
phases: state.cascade.phases.toJSON(),
|
|
@@ -1252,6 +1336,20 @@ function printReport(report) {
|
|
|
1252
1336
|
if (Object.keys(ag.errorKinds).length)
|
|
1253
1337
|
console.log(` error kinds: ${JSON.stringify(ag.errorKinds)}`);
|
|
1254
1338
|
}
|
|
1339
|
+
|
|
1340
|
+
const ws = report.latency.workspaceDiagnostics;
|
|
1341
|
+
if (ws && (ws.started || ws.completed)) {
|
|
1342
|
+
console.log("\nLSP workspace sweeps (lens_diagnostics full)");
|
|
1343
|
+
console.log(
|
|
1344
|
+
` started=${ws.started} completed=${ws.completed} incomplete=${ws.incomplete} aborted=${ws.aborted} fileTimeouts=${ws.timedOutFilesTotal} (in ${ws.timedOutSweeps} sweeps)`,
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
const timeouts = Object.entries(report.latency.phaseTimeouts ?? {});
|
|
1348
|
+
if (timeouts.length) {
|
|
1349
|
+
console.log("\nPhase timeouts");
|
|
1350
|
+
for (const [k, v] of timeouts)
|
|
1351
|
+
console.log(` ${String(v).padStart(5)} ${k}`);
|
|
1352
|
+
}
|
|
1255
1353
|
}
|
|
1256
1354
|
|
|
1257
1355
|
function section(title, rows, formatter) {
|