pi-lens 3.8.70 → 3.8.71
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 +24 -0
- package/README.md +16 -0
- package/dist/clients/bus-events-logger.js +10 -8
- package/dist/clients/cache/rule-cache.js +9 -5
- package/dist/clients/cache-manager.js +19 -13
- package/dist/clients/diagnostic-dispositions.js +298 -0
- package/dist/clients/dispatch/auxiliary-lsp.js +85 -3
- package/dist/clients/dispatch/dispatcher.js +6 -1
- package/dist/clients/dispatch/integration.js +19 -4
- package/dist/clients/dispatch/runners/lsp.js +5 -32
- package/dist/clients/dispatch/runners/oxlint.js +1 -1
- package/dist/clients/dispatch/suppress-writer.js +68 -0
- package/dist/clients/dispatch/utils/lsp-diagnostics.js +4 -1
- package/dist/clients/disposition-logger.js +39 -0
- package/dist/clients/disposition-publish.js +109 -0
- package/dist/clients/file-utils.js +94 -33
- package/dist/clients/format-events-publish.js +299 -0
- package/dist/clients/git-tracked-ignore.js +208 -0
- package/dist/clients/instance-registry.js +74 -4
- package/dist/clients/json-cache-read.js +65 -0
- package/dist/clients/language-profile.js +4 -0
- package/dist/clients/lens-map.js +1079 -0
- package/dist/clients/lsp/aggregation.js +69 -2
- package/dist/clients/lsp/client.js +72 -7
- package/dist/clients/lsp/index.js +486 -14
- package/dist/clients/lsp/server.js +17 -1
- package/dist/clients/lsp/tsserver-sync.js +145 -0
- package/dist/clients/lsp/workspace-diagnostics-cache.js +172 -0
- package/dist/clients/mcp/session.js +8 -0
- package/dist/clients/opengrep-config.js +2 -10
- package/dist/clients/path-utils.js +26 -0
- package/dist/clients/pipeline.js +4 -0
- package/dist/clients/project-diagnostics/cache.js +5 -12
- package/dist/clients/project-snapshot.js +3 -6
- package/dist/clients/read-guard.js +1 -1
- package/dist/clients/review-graph/builder.js +78 -52
- package/dist/clients/review-graph/import-resolvers.js +53 -19
- package/dist/clients/runtime-agent-end.js +26 -0
- package/dist/clients/runtime-coordinator.js +9 -1
- package/dist/clients/runtime-session.js +53 -11
- package/dist/clients/runtime-tool-call.js +789 -0
- package/dist/clients/runtime-tool-result.js +15 -2
- package/dist/clients/runtime-turn.js +9 -2
- package/dist/clients/session-state-store.js +6 -9
- package/dist/clients/sgconfig.js +2 -9
- package/dist/clients/source-filter.js +5 -0
- package/dist/clients/startup-scan.js +68 -1
- package/dist/clients/typos-config.js +2 -11
- package/dist/clients/widget-state.js +22 -0
- package/dist/clients/zizmor-config.js +2 -10
- package/dist/index.js +21089 -18685
- package/dist/tools/lens-diagnostic-mark.js +158 -0
- package/dist/tools/lens-diagnostics.js +41 -7
- package/dist/tools/lsp-diagnostics.js +99 -119
- package/docs/agent-tools.md +11 -4
- package/docs/dispositions.md +85 -0
- package/docs/environment-variables.md +16 -0
- package/docs/fable.md +10 -9
- package/docs/servercapabilities.md +1 -1
- package/docs/tools.md +3 -0
- package/package.json +6 -1
- package/rules/ast-grep-rules/rule-tests/find-import-file-without-extension-test.yml +4 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +5 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +9 -0
- package/rules/ast-grep-rules/rules/find-import-file-without-extension.yml +3 -1
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +12 -0
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +12 -0
- package/docs/agent-tooling-improvement-plan.md +0 -660
- package/docs/inspiration_gograph.md +0 -601
- package/docs/inspiration_neovim.md +0 -1795
- package/docs/inspiration_neovim2.md +0 -402
- package/docs/inspiration_tree_sitter_rules.md +0 -369
- package/docs/inspiration_tree_sitter_rules_investigation.md +0 -328
- package/docs/inspiration_tree_sitter_rules_round2.md +0 -470
- package/docs/lsp-bridge-analysis.md +0 -444
- package/docs/newinspiration.md +0 -338
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-elixir.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-lua.wasm +0 -0
- package/grammars/tree-sitter-ocaml.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-toml.wasm +0 -0
- package/grammars/tree-sitter-vue.wasm +0 -0
- package/grammars/tree-sitter-zig.wasm +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared "untracked AND ignored" file-id computation (#679's `/lens-map`,
|
|
3
|
+
* reused by #694's review-graph ignore-gated node creation) — and, since
|
|
4
|
+
* #703, the sibling "tracked files" set that lets `getProjectIgnoreMatcher`
|
|
5
|
+
* (`file-utils.ts`) honor the OTHER half of the same git semantic.
|
|
6
|
+
*
|
|
7
|
+
* THE critical git semantic this exists to respect: a TRACKED file is never
|
|
8
|
+
* ignored, even when a `.gitignore` pattern matches it (pi-lens's own
|
|
9
|
+
* committed `clients/deps/*.js` vendored sources match the repo's `*.js`
|
|
10
|
+
* ignore pattern and MUST stay graph/map nodes) — which is why this asks git
|
|
11
|
+
* itself (`ls-files --others --ignored --exclude-standard` for the untracked
|
|
12
|
+
* side, `ls-files` alone for the tracked side) instead of running a pattern
|
|
13
|
+
* matcher over `.gitignore` (a matcher-only approach would wrongly drop
|
|
14
|
+
* tracked vendored files, e.g. #703's `clients/test-runner-client.ts`
|
|
15
|
+
* matching `.gitignore`'s `test-*.ts`).
|
|
16
|
+
*
|
|
17
|
+
* Degradation: when git is absent/fails/times out (not a git repo, bare
|
|
18
|
+
* checkout, etc.) both `collectUntrackedIgnoredIds` and `collectTrackedFiles`
|
|
19
|
+
* return `undefined` and every caller SKIPS/degrades — the graph/map shows
|
|
20
|
+
* what's known rather than guessing via a matcher that can't see tracked
|
|
21
|
+
* status, and `getProjectIgnoreMatcher` falls back to pattern-only behavior.
|
|
22
|
+
*
|
|
23
|
+
* Keying asymmetry (deliberate, not an oversight): `collectUntrackedIgnoredIds`
|
|
24
|
+
* keys its ids with `normalizeMapKey` (realpath-backed) because those ids are
|
|
25
|
+
* compared against review-graph/map NODE ids — externally-produced state that
|
|
26
|
+
* genuinely needs canonical on-disk casing/symlink resolution to match up.
|
|
27
|
+
* `collectTrackedFiles`'s ids, by contrast, are only ever compared against a
|
|
28
|
+
* `ProjectIgnoreMatcher`'s own `path.resolve`'d paths, produced by THIS
|
|
29
|
+
* process in the SAME walk — an ephemeral, self-consistent comparison per
|
|
30
|
+
* `normalizeEphemeralMapKey`'s contract (`path-utils.ts`), so it uses the
|
|
31
|
+
* cheap syntactic-only fold instead. Do not "fix" this asymmetry by unifying
|
|
32
|
+
* the two — they solve different problems (#703 review).
|
|
33
|
+
*/
|
|
34
|
+
import * as path from "node:path";
|
|
35
|
+
import { isExcludedDirName } from "./file-utils.js";
|
|
36
|
+
import { normalizeEphemeralMapKey, normalizeMapKey } from "./path-utils.js";
|
|
37
|
+
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
38
|
+
/**
|
|
39
|
+
* Parses `git ls-files --others --ignored --exclude-standard` output
|
|
40
|
+
* (repo-relative paths, one per line) into normalized map-key file ids.
|
|
41
|
+
* Exported as a pure function so the parse/normalize step is unit-testable
|
|
42
|
+
* without mocking the spawn.
|
|
43
|
+
*/
|
|
44
|
+
export function parseUntrackedIgnoredOutput(stdout, cwd) {
|
|
45
|
+
const ids = new Set();
|
|
46
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
47
|
+
const rel = line.trim();
|
|
48
|
+
if (!rel)
|
|
49
|
+
continue;
|
|
50
|
+
// Paths inside shared-excluded dirs (node_modules, dist, .git, …) can
|
|
51
|
+
// never be review-graph/map nodes — the graph walk itself routes
|
|
52
|
+
// exclusion through `isExcludedDirName` — so skip them BEFORE paying for
|
|
53
|
+
// `normalizeMapKey` (realpath-backed, per-call filesystem cost): on
|
|
54
|
+
// pi-lens itself this prunes a 66k-line ignored list (node_modules) down
|
|
55
|
+
// to ~1.6k paths that actually need normalizing.
|
|
56
|
+
const dirSegments = rel.split("/").slice(0, -1);
|
|
57
|
+
if (dirSegments.some((segment) => isExcludedDirName(segment)))
|
|
58
|
+
continue;
|
|
59
|
+
ids.add(normalizeMapKey(path.join(cwd, rel)));
|
|
60
|
+
}
|
|
61
|
+
return ids;
|
|
62
|
+
}
|
|
63
|
+
async function fetchUntrackedIgnoredIds(cwd) {
|
|
64
|
+
try {
|
|
65
|
+
const result = await safeSpawnAsync("git", ["ls-files", "--others", "--ignored", "--exclude-standard"], { cwd, timeout: 10_000, resourceLabel: "git-tracked-ignore" });
|
|
66
|
+
if (result.error || result.status !== 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
return parseUntrackedIgnoredOutput(result.stdout, cwd);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// #694: the review-graph build calls this on EVERY incremental/cascade
|
|
75
|
+
// rebuild (a hot, per-edit path), not just `/lens-map`'s one-shot generation —
|
|
76
|
+
// spawning `git ls-files` per edit would be a real per-keystroke cost. Memoize
|
|
77
|
+
// per cwd with a short time bound: cheap enough that a `.gitignore` edit or a
|
|
78
|
+
// newly-untracked file takes effect within a few seconds, but a burst of
|
|
79
|
+
// per-edit rebuilds within that window shares one spawn.
|
|
80
|
+
const CACHE_TTL_MS = 30_000;
|
|
81
|
+
const _cache = new Map();
|
|
82
|
+
/**
|
|
83
|
+
* The untracked-AND-ignored id set for `cwd`, memoized per-process with a
|
|
84
|
+
* {@link CACHE_TTL_MS} time bound so a hot rebuild loop (review-graph
|
|
85
|
+
* incremental/cascade builds) never spawns `git` per file/per edit — see the
|
|
86
|
+
* module doc for the tracked-file semantic and the degrade-to-undefined
|
|
87
|
+
* contract.
|
|
88
|
+
*/
|
|
89
|
+
export function collectUntrackedIgnoredIds(cwd) {
|
|
90
|
+
const key = normalizeMapKey(cwd);
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const cached = _cache.get(key);
|
|
93
|
+
if (cached && now - cached.fetchedAtMs < CACHE_TTL_MS)
|
|
94
|
+
return cached.promise;
|
|
95
|
+
const promise = fetchUntrackedIgnoredIds(cwd);
|
|
96
|
+
_cache.set(key, { promise, fetchedAtMs: now });
|
|
97
|
+
return promise;
|
|
98
|
+
}
|
|
99
|
+
/** Test hook: drop the memoized cache so a test's fake git state is re-read. */
|
|
100
|
+
export function _resetUntrackedIgnoredCacheForTests() {
|
|
101
|
+
_cache.clear();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parses `git ls-files` output (repo-relative paths, one per line, tracked
|
|
105
|
+
* files only — no `--others`) into normalized ids, keyed for comparison
|
|
106
|
+
* against a `ProjectIgnoreMatcher`'s own (non-realpath'd) `path.resolve`'d
|
|
107
|
+
* paths — NOT against {@link parseUntrackedIgnoredOutput}'s ids, which are
|
|
108
|
+
* realpath-keyed for a different consumer (review-graph/map node ids). The
|
|
109
|
+
* two sets are NOT directly comparable/mergeable despite the shared
|
|
110
|
+
* dir-exclusion-prune shape — see this file's module doc for the asymmetry.
|
|
111
|
+
*
|
|
112
|
+
* `cwd` is realpath'd exactly ONCE here (not per file) to reconcile the one
|
|
113
|
+
* divergence that matters in practice: on Windows, the matcher's
|
|
114
|
+
* `resolvedRoot` comes from `path.resolve` while this fetch's `cwd` may carry
|
|
115
|
+
* different on-disk casing (e.g. `c:\users` vs `C:\Users`) — a single
|
|
116
|
+
* realpath on the shared root, followed by a cheap syntactic fold
|
|
117
|
+
* (`normalizeEphemeralMapKey`) on every per-file join, reconciles that without
|
|
118
|
+
* paying `realpathSync` per tracked file (#703 perf follow-up — a ~2k-file
|
|
119
|
+
* repo was doing ~2k realpath calls in one synchronous burst per fetch).
|
|
120
|
+
* Accepted edge case: a symlinked or 8.3-short-name project root can still
|
|
121
|
+
* make the cheap per-file fold miss even after this one realpath — degrades
|
|
122
|
+
* to today's pattern-only behavior for that root, consistent with the
|
|
123
|
+
* fail-open contract elsewhere in this module.
|
|
124
|
+
*/
|
|
125
|
+
export function parseTrackedFilesOutput(stdout, cwd) {
|
|
126
|
+
const ids = new Set();
|
|
127
|
+
const base = normalizeMapKey(cwd);
|
|
128
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
129
|
+
const rel = line.trim();
|
|
130
|
+
if (!rel)
|
|
131
|
+
continue;
|
|
132
|
+
const dirSegments = rel.split("/").slice(0, -1);
|
|
133
|
+
if (dirSegments.some((segment) => isExcludedDirName(segment)))
|
|
134
|
+
continue;
|
|
135
|
+
ids.add(normalizeEphemeralMapKey(path.join(base, rel)));
|
|
136
|
+
}
|
|
137
|
+
return ids;
|
|
138
|
+
}
|
|
139
|
+
async function fetchTrackedFiles(cwd) {
|
|
140
|
+
try {
|
|
141
|
+
const result = await safeSpawnAsync("git", ["ls-files"], {
|
|
142
|
+
cwd,
|
|
143
|
+
timeout: 10_000,
|
|
144
|
+
resourceLabel: "git-tracked-ignore",
|
|
145
|
+
});
|
|
146
|
+
if (result.error || result.status !== 0)
|
|
147
|
+
return undefined;
|
|
148
|
+
return parseTrackedFilesOutput(result.stdout, cwd);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const _trackedCache = new Map();
|
|
155
|
+
// Sync snapshot of the most recently RESOLVED tracked-files fetch per root,
|
|
156
|
+
// keyed the same way as `_trackedCache`. `getProjectIgnoreMatcher`'s
|
|
157
|
+
// `isIgnored` is sync-and-hot (#703 constraint) and can't await a promise, so
|
|
158
|
+
// this is the seam it reads from: `ensureTrackedIndex` (file-utils.ts) awaits
|
|
159
|
+
// `collectTrackedFiles` ONCE per walk, which populates this snapshot as a
|
|
160
|
+
// side effect; `isIgnored` then reads the snapshot synchronously for every
|
|
161
|
+
// file in that same walk. A caller that never primes sees an absent snapshot
|
|
162
|
+
// entry and degrades to pattern-only behavior (fail-open, by design).
|
|
163
|
+
const _trackedSnapshot = new Map();
|
|
164
|
+
/**
|
|
165
|
+
* The tracked-files id set for `cwd` (i.e. `git ls-files`, no `--others`),
|
|
166
|
+
* memoized per-process with the same {@link CACHE_TTL_MS} time bound as
|
|
167
|
+
* {@link collectUntrackedIgnoredIds} — see that function's doc for the hot
|
|
168
|
+
* per-edit-rebuild-loop rationale. Also updates the synchronous snapshot
|
|
169
|
+
* `getTrackedFilesSnapshot` reads, so sync hot-path callers can consult a
|
|
170
|
+
* cheap in-memory Set instead of awaiting this promise per file.
|
|
171
|
+
*/
|
|
172
|
+
export function collectTrackedFiles(cwd) {
|
|
173
|
+
// #703 perf follow-up: `normalizeEphemeralMapKey`, not `normalizeMapKey` —
|
|
174
|
+
// this map's only readers/writers are `ensureTrackedIndex`/`isIgnored`
|
|
175
|
+
// (file-utils.ts), both of which derive `cwd` from the SAME matcher's
|
|
176
|
+
// `resolvedRoot` (`path.resolve`, not realpath) within one process, so the
|
|
177
|
+
// cheap syntactic fold is sufficient and avoids a realpath per call — see
|
|
178
|
+
// this file's module doc for why this diverges from `collectUntrackedIgnoredIds`.
|
|
179
|
+
const key = normalizeEphemeralMapKey(cwd);
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
const cached = _trackedCache.get(key);
|
|
182
|
+
if (cached && now - cached.fetchedAtMs < CACHE_TTL_MS)
|
|
183
|
+
return cached.promise;
|
|
184
|
+
const promise = fetchTrackedFiles(cwd).then((result) => {
|
|
185
|
+
_trackedSnapshot.set(key, result);
|
|
186
|
+
return result;
|
|
187
|
+
});
|
|
188
|
+
_trackedCache.set(key, { promise, fetchedAtMs: now });
|
|
189
|
+
return promise;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Synchronous read of the most recent {@link collectTrackedFiles} result for
|
|
193
|
+
* `cwd`. Returns `undefined` when nothing has primed this root yet (never
|
|
194
|
+
* called `collectTrackedFiles`), when git degraded (no repo/spawn failure),
|
|
195
|
+
* OR when a fetch is still in flight — all three collapse to the same
|
|
196
|
+
* fail-open "tracked status unknown, use pattern-only behavior" contract.
|
|
197
|
+
* Called on every `isIgnored` check that reaches the tracked-rescue branch
|
|
198
|
+
* (file-utils.ts's `isTrackedAndRescued`), so this key MUST stay the cheap
|
|
199
|
+
* `normalizeEphemeralMapKey` fold, not a realpath.
|
|
200
|
+
*/
|
|
201
|
+
export function getTrackedFilesSnapshot(cwd) {
|
|
202
|
+
return _trackedSnapshot.get(normalizeEphemeralMapKey(cwd));
|
|
203
|
+
}
|
|
204
|
+
/** Test hook: drop the memoized tracked-files cache/snapshot. */
|
|
205
|
+
export function _resetTrackedFilesCacheForTests() {
|
|
206
|
+
_trackedCache.clear();
|
|
207
|
+
_trackedSnapshot.clear();
|
|
208
|
+
}
|
|
@@ -23,6 +23,16 @@
|
|
|
23
23
|
import * as fs from "node:fs";
|
|
24
24
|
import * as path from "node:path";
|
|
25
25
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
26
|
+
// #735: reuse the #449/#525 reaper's exact conservative liveness check
|
|
27
|
+
// (`process.kill(pid, 0)`, ESRCH-only-means-dead) rather than inventing a
|
|
28
|
+
// second one — see realIsPidAlive's own docstring, which already calls out
|
|
29
|
+
// clients/lsp-budget.ts as a precedent consumer. This creates a live-binding
|
|
30
|
+
// import cycle with instance-reaper.ts (which imports readInstanceRegistry/
|
|
31
|
+
// isInstanceRegistryEnabled from here); safe under Node ESM because every
|
|
32
|
+
// use on both sides happens inside function bodies, never at module-
|
|
33
|
+
// evaluation time, so both modules are fully initialized before either
|
|
34
|
+
// import is actually invoked.
|
|
35
|
+
import { realIsPidAlive } from "./instance-reaper.js";
|
|
26
36
|
import { normalizeFilePath } from "./path-utils.js";
|
|
27
37
|
function registryPath() {
|
|
28
38
|
return path.join(getGlobalPiLensDir(), "instances.json");
|
|
@@ -264,9 +274,32 @@ export function deregisterInstance() {
|
|
|
264
274
|
* short-lived and sampled separately per-invocation via
|
|
265
275
|
* clients/resource-sampler.ts into clients/latency-logger.ts, not carried in
|
|
266
276
|
* the registry (see the module docstring's scope note).
|
|
277
|
+
*
|
|
278
|
+
* #735: `isPidAlive`, when supplied, drops any instance whose owning pid is
|
|
279
|
+
* confirmed dead BEFORE aggregation — a hard-killed pi process otherwise
|
|
280
|
+
* leaves a registry entry with heartbeat-cached RSS that reads as a live,
|
|
281
|
+
* resource-consuming instance until it eventually ages out (up to
|
|
282
|
+
* `STALE_HEARTBEAT_MS`, see clients/instance-reaper.ts). Dropped rather than
|
|
283
|
+
* flagged `stale: true`: a dead pid is unambiguous (unlike heartbeat
|
|
284
|
+
* staleness, which the reaper deliberately treats as "maybe idle-but-alive"
|
|
285
|
+
* and never uses to justify removing/hiding anything) — the wire shape and
|
|
286
|
+
* every existing caller (chiefly `pilens_health`'s headline instance
|
|
287
|
+
* count/RSS/CPU numbers) simply expects a footprint of currently-live
|
|
288
|
+
* instances. Left `undefined` for pure/synchronous callers (incl. every
|
|
289
|
+
* pre-#735 unit test) that pass a plain snapshot with no intent to check OS
|
|
290
|
+
* process state — no filtering happens, preserving prior behavior exactly.
|
|
291
|
+
* No pid-reuse identity check is applied here (unlike the reaper's child-pid
|
|
292
|
+
* `matchProcess`): `InstanceEntry` never recorded the host's own command
|
|
293
|
+
* line to verify against (same gap #525 called out for the reaper's PARENT
|
|
294
|
+
* pid, deliberately left unfixed there too), and a health-report false
|
|
295
|
+
* positive is a much smaller blast radius than the reaper's mistaken kill —
|
|
296
|
+
* so plain liveness is judged sufficient here.
|
|
267
297
|
*/
|
|
268
|
-
export function computeResourceFootprint(instances) {
|
|
269
|
-
const
|
|
298
|
+
export function computeResourceFootprint(instances, isPidAlive) {
|
|
299
|
+
const liveInstances = isPidAlive
|
|
300
|
+
? instances.filter((instance) => isPidAlive(instance.pid))
|
|
301
|
+
: instances;
|
|
302
|
+
const perInstance = liveInstances.map((instance) => {
|
|
270
303
|
const lspChildRssBytes = instance.lspChildren.reduce((sum, child) => sum + (child.rssBytes ?? 0), 0);
|
|
271
304
|
const lspChildCpuPercent = instance.lspChildren.reduce((sum, child) => sum + (child.cpuPercent ?? 0), 0);
|
|
272
305
|
return {
|
|
@@ -300,8 +333,45 @@ export function computeResourceFootprint(instances) {
|
|
|
300
333
|
* side of "how much CPU/RAM is pi-lens using right now" (#620). Best-effort
|
|
301
334
|
* (readInstanceRegistry never throws); the answer only reflects whatever
|
|
302
335
|
* heartbeats have landed so far.
|
|
336
|
+
*
|
|
337
|
+
* #735: defaults to the reaper's `realIsPidAlive` so dead-pid registry
|
|
338
|
+
* entries (a hard-killed pi process) are excluded from both the returned
|
|
339
|
+
* footprint AND — opportunistically, fire-and-forget, best-effort — pruned
|
|
340
|
+
* from the on-disk registry, the same "entry removal only, never blocks the
|
|
341
|
+
* caller" convention `sweepOrphans`/`pruneDeadInstances` already use.
|
|
342
|
+
* Pruning here is a bonus cleanup, not a substitute for the reaper sweep:
|
|
343
|
+
* this path only prunes pids this particular read happened to find dead,
|
|
344
|
+
* while the reaper sweep is the authoritative, scheduled cleanup. Injectable
|
|
345
|
+
* so tests can pass a fake predicate (or omit filtering entirely by passing
|
|
346
|
+
* a function that always returns true) without touching real OS process
|
|
347
|
+
* state.
|
|
303
348
|
*/
|
|
304
|
-
export async function getResourceFootprint() {
|
|
349
|
+
export async function getResourceFootprint(isPidAlive = realIsPidAlive) {
|
|
305
350
|
const instances = await readInstanceRegistry();
|
|
306
|
-
|
|
351
|
+
const deadPids = new Set(instances.filter((instance) => !isPidAlive(instance.pid)).map((instance) => instance.pid));
|
|
352
|
+
if (deadPids.size > 0) {
|
|
353
|
+
// Fire-and-forget: a health-report read must never block on, or fail
|
|
354
|
+
// because of, a registry write.
|
|
355
|
+
prunePids(deadPids).catch(() => {
|
|
356
|
+
// best-effort — a dead-pid entry that fails to prune here is simply
|
|
357
|
+
// re-evaluated (and re-dropped from the report) on the next read, and
|
|
358
|
+
// remains catchable by the scheduled reaper sweep regardless.
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return computeResourceFootprint(instances, isPidAlive);
|
|
362
|
+
}
|
|
363
|
+
/** Best-effort removal of specific dead pids' entries from the on-disk
|
|
364
|
+
* registry (#735). Re-reads immediately before writing (rather than reusing
|
|
365
|
+
* an earlier snapshot) to narrow — not eliminate — the last-writer-wins race
|
|
366
|
+
* already accepted for this module's read-modify-write model (see the
|
|
367
|
+
* module docstring). Mirrors clients/instance-reaper.ts's
|
|
368
|
+
* `pruneDeadInstances`, kept local here rather than imported to avoid
|
|
369
|
+
* reaching back across the same import edge `realIsPidAlive` already
|
|
370
|
+
* crosses in the other direction. */
|
|
371
|
+
async function prunePids(deadPids) {
|
|
372
|
+
const file = await readRegistryAsync();
|
|
373
|
+
const remaining = file.instances.filter((entry) => !deadPids.has(entry.pid));
|
|
374
|
+
if (remaining.length === file.instances.length)
|
|
375
|
+
return;
|
|
376
|
+
await writeRegistryAsync({ instances: remaining });
|
|
307
377
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared "read a JSON cache file off disk" boilerplate (#676).
|
|
3
|
+
*
|
|
4
|
+
* At least 7 sites across the codebase hand-duplicated the same shape:
|
|
5
|
+
* `JSON.parse(fs.readFileSync(path, "utf-8"))` → structural/version guard →
|
|
6
|
+
* return the parsed value, or `undefined`/`null` on ANY throw (missing file,
|
|
7
|
+
* unreadable, corrupt JSON, or a `validate` callback itself throwing). This
|
|
8
|
+
* module extracts only that read/parse/try-catch mechanics; every call
|
|
9
|
+
* site's own "is this actually a valid, current-version cache?" logic stays
|
|
10
|
+
* put, as the `validate` callback — consolidation is only meant to remove
|
|
11
|
+
* the duplicated plumbing, not to unify subtly different validation rules
|
|
12
|
+
* that call sites had for good reason (e.g. a second `ruleHash` field check
|
|
13
|
+
* alongside the version check).
|
|
14
|
+
*
|
|
15
|
+
* `validate` returning `undefined` means "treat this like a parse failure" —
|
|
16
|
+
* the same fail-open posture every existing site already had for a
|
|
17
|
+
* wrong-version/wrong-shape cache.
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from "node:fs";
|
|
20
|
+
import { promises as fsPromises } from "node:fs";
|
|
21
|
+
/**
|
|
22
|
+
* Synchronous read: `JSON.parse(fs.readFileSync(path, "utf-8"))`, run through
|
|
23
|
+
* `validate`, with any failure (missing file, unreadable, corrupt JSON,
|
|
24
|
+
* `validate` throwing) swallowed to `undefined`.
|
|
25
|
+
*
|
|
26
|
+
* `onError`, when provided, is invoked with the caught error BEFORE
|
|
27
|
+
* `undefined` is returned — for sites that log a read failure today (e.g.
|
|
28
|
+
* `CacheManager`'s verbose `this.log(...)`) rather than silently swallowing
|
|
29
|
+
* it. `onError` itself is never allowed to throw the failure back out.
|
|
30
|
+
*/
|
|
31
|
+
export function readJsonCache(path, validate, onError) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(fs.readFileSync(path, "utf-8"));
|
|
34
|
+
return validate(parsed);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
try {
|
|
38
|
+
onError?.(err);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* onError must never mask the original fail-open behavior */
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Async sibling of {@link readJsonCache}, for call sites already on the
|
|
48
|
+
* `fs.promises` API (e.g. `session-state-store.ts`). Same fail-open contract.
|
|
49
|
+
*/
|
|
50
|
+
export async function readJsonCacheAsync(path, validate, onError) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = await fsPromises.readFile(path, "utf-8");
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
return validate(parsed);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
try {
|
|
58
|
+
onError?.(err);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* onError must never mask the original fail-open behavior */
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -282,6 +282,10 @@ const MAX_WARMUP_SOURCE_FILES = 2000;
|
|
|
282
282
|
export async function collectSourceFilesForWarmup(rootDir, maxFiles = MAX_WARMUP_SOURCE_FILES, yieldEvery = 100) {
|
|
283
283
|
const root = path.resolve(rootDir);
|
|
284
284
|
const ignoreMatcher = getProjectIgnoreMatcher(root);
|
|
285
|
+
// #703: prime the tracked-files set once before the walk so a tracked file
|
|
286
|
+
// matching a `.gitignore`/global pattern still counts toward language
|
|
287
|
+
// detection. Fail-open on no-git/spawn failure.
|
|
288
|
+
await ignoreMatcher.ensureTrackedIndex();
|
|
285
289
|
const stack = [root];
|
|
286
290
|
const out = [];
|
|
287
291
|
let processedSinceYield = 0;
|