pi-lens 4.1.0 → 4.1.2

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.
Files changed (206) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/README.md +106 -0
  3. package/dist/clients/actionable-warnings-logger.js +2 -1
  4. package/dist/clients/actionable-warnings.js +14 -5
  5. package/dist/clients/advisory-provenance.js +48 -18
  6. package/dist/clients/ast-grep-client.js +1 -3
  7. package/dist/clients/atomic-write-staging.js +16 -4
  8. package/dist/clients/bash-file-access.js +99 -6
  9. package/dist/clients/biome-client.js +1 -3
  10. package/dist/clients/blocker-freshness.js +21 -8
  11. package/dist/clients/blocker-past-eof.js +16 -6
  12. package/dist/clients/bounded-cache.js +24 -8
  13. package/dist/clients/bounded-pid-file-lock.js +4 -3
  14. package/dist/clients/bounded-telemetry.js +14 -0
  15. package/dist/clients/cache-manager.js +6 -7
  16. package/dist/clients/cache-observability.js +782 -110
  17. package/dist/clients/call-graph.js +238 -63
  18. package/dist/clients/codebase-model.js +13 -5
  19. package/dist/clients/complexity-client.js +1 -3
  20. package/dist/clients/dead-code-client.js +19 -8
  21. package/dist/clients/dead-code-logger.js +1 -2
  22. package/dist/clients/deadline-utils.js +3 -1
  23. package/dist/clients/debug-heap.js +8 -1
  24. package/dist/clients/degradation-ledger.js +22 -1
  25. package/dist/clients/demoted-finding-render.js +155 -0
  26. package/dist/clients/dependency-checker.js +11 -7
  27. package/dist/clients/deps/pi-tui.js +8 -2
  28. package/dist/clients/deps/typebox.js +8 -3
  29. package/dist/clients/diagnostic-dispositions.js +39 -2
  30. package/dist/clients/diagnostic-line-freshness.js +5 -1
  31. package/dist/clients/dispatch/auxiliary-lsp.js +2 -3
  32. package/dist/clients/dispatch/dispatcher.js +13 -1
  33. package/dist/clients/dispatch/facts/function-facts.js +3 -1
  34. package/dist/clients/dispatch/facts/import-facts.js +2 -1
  35. package/dist/clients/dispatch/facts/tree-sitter-facts.js +1 -1
  36. package/dist/clients/dispatch/integration.js +9 -4
  37. package/dist/clients/dispatch/rules/error-swallowing.js +3 -1
  38. package/dist/clients/dispatch/rules/pass-through-wrappers.js +6 -2
  39. package/dist/clients/dispatch/rules/unsafe-boundary.js +31 -7
  40. package/dist/clients/dispatch/runners/actionlint.js +5 -2
  41. package/dist/clients/dispatch/runners/biome-check.js +10 -14
  42. package/dist/clients/dispatch/runners/cpp-check.js +5 -1
  43. package/dist/clients/dispatch/runners/detekt.js +9 -6
  44. package/dist/clients/dispatch/runners/dotnet-build.js +1 -1
  45. package/dist/clients/dispatch/runners/elixir-check.js +3 -1
  46. package/dist/clients/dispatch/runners/eslint.js +25 -6
  47. package/dist/clients/dispatch/runners/fish-indent.js +1 -1
  48. package/dist/clients/dispatch/runners/gleam-check.js +1 -1
  49. package/dist/clients/dispatch/runners/golangci-lint.js +3 -1
  50. package/dist/clients/dispatch/runners/hadolint.js +22 -15
  51. package/dist/clients/dispatch/runners/helm-lint.js +4 -1
  52. package/dist/clients/dispatch/runners/htmlhint.js +33 -16
  53. package/dist/clients/dispatch/runners/javac.js +1 -1
  54. package/dist/clients/dispatch/runners/ktlint.js +1 -1
  55. package/dist/clients/dispatch/runners/lsp.js +1 -1
  56. package/dist/clients/dispatch/runners/markdownlint.js +34 -12
  57. package/dist/clients/dispatch/runners/mypy.js +15 -17
  58. package/dist/clients/dispatch/runners/oxlint.js +231 -15
  59. package/dist/clients/dispatch/runners/php-lint.js +1 -1
  60. package/dist/clients/dispatch/runners/phpstan.js +52 -15
  61. package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -6
  62. package/dist/clients/dispatch/runners/pyright.js +23 -14
  63. package/dist/clients/dispatch/runners/rust-clippy.js +1 -1
  64. package/dist/clients/dispatch/runners/shellcheck.js +7 -9
  65. package/dist/clients/dispatch/runners/shfmt.js +5 -2
  66. package/dist/clients/dispatch/runners/spellcheck.js +10 -15
  67. package/dist/clients/dispatch/runners/sqlfluff.js +25 -17
  68. package/dist/clients/dispatch/runners/stylelint.js +40 -20
  69. package/dist/clients/dispatch/runners/swiftlint.js +8 -8
  70. package/dist/clients/dispatch/runners/taplo.js +110 -23
  71. package/dist/clients/dispatch/runners/terragrunt.js +13 -8
  72. package/dist/clients/dispatch/runners/tflint.js +25 -15
  73. package/dist/clients/dispatch/runners/tree-sitter.js +17 -3
  74. package/dist/clients/dispatch/runners/utils/candidate-probe.js +7 -1
  75. package/dist/clients/dispatch/runners/utils/runner-helpers.js +24 -1
  76. package/dist/clients/dispatch/runners/utils/tool-failure.js +152 -1
  77. package/dist/clients/dispatch/runners/vale.js +16 -14
  78. package/dist/clients/dispatch/runners/yaml-rule-parser.js +1 -3
  79. package/dist/clients/dispatch/runners/yamllint.js +13 -16
  80. package/dist/clients/dispatch/runners/zig-check.js +1 -1
  81. package/dist/clients/dispatch/suppress-writer.js +2 -1
  82. package/dist/clients/dispatch/types.js +6 -1
  83. package/dist/clients/dispatch/utils/lsp-diagnostics.js +6 -2
  84. package/dist/clients/extension-log.js +5 -3
  85. package/dist/clients/feature-hints.js +8 -1
  86. package/dist/clients/file-kinds.js +34 -129
  87. package/dist/clients/file-utils.js +17 -1
  88. package/dist/clients/finding-delivery-gate.js +79 -2
  89. package/dist/clients/formatters.js +144 -27
  90. package/dist/clients/freshness.js +38 -0
  91. package/dist/clients/git-guard.js +43 -13
  92. package/dist/clients/go-client.js +1 -3
  93. package/dist/clients/grammar-source.js +1 -1
  94. package/dist/clients/gzip-stage-write.js +30 -4
  95. package/dist/clients/host-edit-normalize.js +1 -3
  96. package/dist/clients/install-diagnostics.js +4 -2
  97. package/dist/clients/installer/index.js +86 -77
  98. package/dist/clients/instance-reaper.js +5 -2
  99. package/dist/clients/instance-registry.js +4 -2
  100. package/dist/clients/jscpd-client.js +11 -3
  101. package/dist/clients/knip-client.js +29 -15
  102. package/dist/clients/language-profile.js +11 -3
  103. package/dist/clients/latency-logger.js +13 -2
  104. package/dist/clients/lens-config.js +1 -1
  105. package/dist/clients/lens-map.js +8 -3
  106. package/dist/clients/live-bus-emitter.js +1 -1
  107. package/dist/clients/lsp/cascade-tier.js +27 -2
  108. package/dist/clients/lsp/client.js +144 -5
  109. package/dist/clients/lsp/config.js +4 -6
  110. package/dist/clients/lsp/edits.js +119 -32
  111. package/dist/clients/lsp/index.js +245 -41
  112. package/dist/clients/lsp/inferred-project.js +2 -1
  113. package/dist/clients/lsp/jvm-runtime.js +4 -1
  114. package/dist/clients/lsp/launch.js +11 -3
  115. package/dist/clients/lsp/pending-aux-coverage.js +152 -0
  116. package/dist/clients/lsp/server.js +19 -7
  117. package/dist/clients/lsp/tsserver-sync.js +10 -2
  118. package/dist/clients/lsp/workspace-diagnostics-cache.js +9 -4
  119. package/dist/clients/lsp-document-symbols.js +1 -3
  120. package/dist/clients/lsp-mutation.js +10 -20
  121. package/dist/clients/mcp/host-shim.js +1 -1
  122. package/dist/clients/memory-sampler.js +73 -7
  123. package/dist/clients/metrics-client.js +1 -3
  124. package/dist/clients/middle-man-analysis.js +8 -3
  125. package/dist/clients/module-report-lsp.js +7 -1
  126. package/dist/clients/module-report.js +28 -18
  127. package/dist/clients/ndjson-logger.js +117 -28
  128. package/dist/clients/opaque-mutation-scan.js +220 -0
  129. package/dist/clients/package-manager.js +6 -1
  130. package/dist/clients/pipeline.js +57 -9
  131. package/dist/clients/project-conventions.js +2 -8
  132. package/dist/clients/project-diagnostics/cache.js +10 -7
  133. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +2 -1
  134. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +3 -1
  135. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +13 -9
  136. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +3 -1
  137. package/dist/clients/project-diagnostics/scanner.js +22 -10
  138. package/dist/clients/project-lens-config.js +12 -5
  139. package/dist/clients/project-report.js +62 -27
  140. package/dist/clients/project-snapshot-fingerprint.js +48 -0
  141. package/dist/clients/project-snapshot-persist-worker.js +15 -1
  142. package/dist/clients/project-snapshot.js +472 -66
  143. package/dist/clients/read-expansion.js +12 -2
  144. package/dist/clients/read-guard-logger.js +12 -6
  145. package/dist/clients/read-guard-tool-lines.js +5 -2
  146. package/dist/clients/read-guard.js +88 -19
  147. package/dist/clients/resource-sampler.js +1 -1
  148. package/dist/clients/reverse-deps.js +6 -5
  149. package/dist/clients/review-graph/builder.js +236 -48
  150. package/dist/clients/review-graph/git-identity.js +8 -2
  151. package/dist/clients/review-graph/import-resolvers.js +10 -1
  152. package/dist/clients/review-graph/query.js +2 -1
  153. package/dist/clients/review-graph/revision-drift.js +21 -0
  154. package/dist/clients/review-graph/tsconfig-paths.js +4 -2
  155. package/dist/clients/review-graph/workspace-modules.js +8 -5
  156. package/dist/clients/ruff-client.js +6 -5
  157. package/dist/clients/runtime-agent-end.js +63 -31
  158. package/dist/clients/runtime-context.js +12 -4
  159. package/dist/clients/runtime-coordinator.js +127 -4
  160. package/dist/clients/runtime-session.js +55 -0
  161. package/dist/clients/runtime-tool-call.js +59 -3
  162. package/dist/clients/runtime-tool-result.js +148 -34
  163. package/dist/clients/runtime-turn.js +194 -8
  164. package/dist/clients/rust-client.js +1 -3
  165. package/dist/clients/safe-spawn.js +150 -11
  166. package/dist/clients/security-scan-client.js +1 -3
  167. package/dist/clients/session-event-guard.js +184 -0
  168. package/dist/clients/session-summary.js +3 -1
  169. package/dist/clients/sg-runner.js +1 -3
  170. package/dist/clients/source-filter.js +16 -8
  171. package/dist/clients/source-walker.js +1 -1
  172. package/dist/clients/spawn-timeout-cooldown.js +96 -0
  173. package/dist/clients/startup-scan.js +9 -3
  174. package/dist/clients/tool-definition.js +7 -0
  175. package/dist/clients/tool-policy.js +26 -11
  176. package/dist/clients/tool-render.js +2 -1
  177. package/dist/clients/tree-sitter-client.js +10 -4
  178. package/dist/clients/tree-sitter-logger.js +2 -1
  179. package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
  180. package/dist/clients/warm-attach.js +12 -5
  181. package/dist/clients/widget-state.js +27 -11
  182. package/dist/clients/word-index.js +3 -1
  183. package/dist/clients/workspace-topology.js +8 -2
  184. package/dist/clients/zizmor-config.js +9 -1
  185. package/dist/i18n.js +13 -5
  186. package/dist/index.js +15811 -12810
  187. package/dist/mcp/server.js +3 -1
  188. package/dist/tools/ast-grep-replace.js +68 -12
  189. package/dist/tools/lens-diagnostic-mark.js +21 -7
  190. package/dist/tools/lens-diagnostics.js +174 -38
  191. package/dist/tools/lsp-diagnostics.js +24 -7
  192. package/dist/tools/lsp-navigation.js +3 -1
  193. package/dist/tools/module-report.js +1 -1
  194. package/dist/tools/project-report.js +1 -1
  195. package/dist/tools/symbol-search.js +5 -2
  196. package/docs/durable-store-audit-1202.md +1 -1
  197. package/docs/servercapabilities.md +1 -1
  198. package/package.json +160 -143
  199. package/rules/typos/_typos.toml +2 -2
  200. package/scripts/analyze-pi-lens-logs.mjs +31 -13
  201. package/scripts/download-grammars.js +9 -3
  202. package/scripts/install-selftest.mjs +99 -11
  203. package/scripts/lib/host-provided-deps.mjs +75 -0
  204. package/scripts/lib/warm-loader-cache.mjs +285 -0
  205. package/scripts/rpc-load-check.mjs +30 -9
  206. package/scripts/warm-loader-cache.mjs +233 -0
@@ -45,10 +45,10 @@ export function tokenizeShellCommand(command) {
45
45
  word = "";
46
46
  atTokenStart = true;
47
47
  };
48
- const flushSegment = () => {
48
+ const flushSegment = (terminator) => {
49
49
  flushWord();
50
50
  if (tokens.length > 0 || unsupported)
51
- segments.push({ tokens, unsupported });
51
+ segments.push({ tokens, unsupported, terminator });
52
52
  tokens = [];
53
53
  unsupported = false;
54
54
  atTokenStart = true;
@@ -122,13 +122,18 @@ export function tokenizeShellCommand(command) {
122
122
  }
123
123
  if (ch === ";" || ch === "\n" || ch === "|" || ch === "&") {
124
124
  flushWord();
125
+ let terminator;
125
126
  if (ch === "|" && next === "|")
126
127
  i++;
127
128
  else if (ch === "&" && next === "&")
128
129
  i++;
129
- else if (ch === "|" || ch === "&")
130
+ else if (ch === "|") {
130
131
  unsupported = true;
131
- flushSegment();
132
+ terminator = "pipe";
133
+ }
134
+ else if (ch === "&")
135
+ unsupported = true;
136
+ flushSegment(terminator);
132
137
  continue;
133
138
  }
134
139
  if (ch === "<" || ch === ">") {
@@ -514,6 +519,83 @@ function parseGrepLineWithoutFile(line, file) {
514
519
  return undefined;
515
520
  return { file, startLine: lineNumber, endLine: lineNumber };
516
521
  }
522
+ /**
523
+ * Verbs that DROP printed lines from a piped stream (#1908, review F4): a
524
+ * hit that survives such a tail cannot be trusted to still carry the
525
+ * -A/-B/-C context grep's flags declared, because that context may have
526
+ * been among the dropped lines. `head`/`tail` drop by position (even bare,
527
+ * since both default to a 10-line window); `sed` with a `q` command drops
528
+ * everything after the addressed line; `uniq` drops adjacent duplicate
529
+ * lines, which can include a context line that happens to repeat.
530
+ *
531
+ * The criterion is DROPPING, not "changes grep's output shape" — a review
532
+ * finding (#1913 F4) caught this file previously conflating the two:
533
+ * - `head`/`tail`/`sed q`/`uniq` downstream of a matching grep → some
534
+ * printed lines are genuinely gone, so credit falls back to
535
+ * match-line-only (the same conservative default already used for
536
+ * unparseable commands) rather than trusting the declared flags.
537
+ * - `sort` and other REORDER-ONLY filters keep every line grep printed,
538
+ * just in a different order — a surviving `file:line:` match still
539
+ * carries the full context grep actually printed, so no special case is
540
+ * needed (proven by the "still credits full context through a
541
+ * non-truncating pipe tail" test below).
542
+ * - `wc` and similar filters that REPLACE grep's output entirely (with a
543
+ * count, say) leave no `file:line:`-shaped line at all, so
544
+ * `parseGrepOutputSearchReads` already finds zero matches in the real
545
+ * captured stdout. Also no special case needed, but for the OPPOSITE
546
+ * reason from `sort`: nothing survives to credit, rather than
547
+ * everything surviving intact.
548
+ * - A pass-through filter (`cat`, `grep -v`, ...) between grep and a
549
+ * dropping tail still counts: the walk below follows the whole pipe
550
+ * chain, not just the immediate next segment.
551
+ * - Out of scope, deliberately: `sed -n 'Np'` range-address filtering
552
+ * without `q`, and `sort -u`, also drop lines but aren't in #1908's
553
+ * stated scope — revisit if they recur in the field. Also out of
554
+ * scope: a truncating tail hidden inside a subshell/brace-group
555
+ * (`( ... )`, `{ ...; }`) or reimplemented in `awk`/`perl` — the shared
556
+ * tokenizer this file uses doesn't parse inside those, matching its
557
+ * documented "small conservative shell lexer" scope.
558
+ */
559
+ const LINE_DROPPING_TAIL_VERBS = new Set(["head", "tail", "uniq"]);
560
+ function isSedQuitCommand(args) {
561
+ for (const arg of args) {
562
+ const token = stripQuotes(arg);
563
+ if (token.startsWith("-"))
564
+ continue;
565
+ // A `q` command anchored at the start of the script or after a `;`/
566
+ // newline separator, optionally preceded by a numeric/`$` address
567
+ // (`q`, `1q`, `$q`, `1,3p;q`). Deliberately not a full sed parser.
568
+ if (/(^|[;\n])\s*(\d+|\$)?\s*q\b/.test(token))
569
+ return true;
570
+ }
571
+ return false;
572
+ }
573
+ function isLineDroppingTailCommand(tokens) {
574
+ const verb = path.basename(stripQuotes(tokens[0] ?? ""));
575
+ if (LINE_DROPPING_TAIL_VERBS.has(verb))
576
+ return true;
577
+ if (verb === "sed")
578
+ return isSedQuitCommand(tokens.slice(1));
579
+ return false;
580
+ }
581
+ /**
582
+ * True when the grep segment starting at `index` feeds — directly or through
583
+ * a chain of pipes — into a line-dropping tail (#1908). Only an unquoted `|`
584
+ * counts: a `;`/`&&`/`&`-separated command downstream never receives grep's
585
+ * stdout, so it must not downgrade credit (#1913 F2 — this was previously
586
+ * unverified by any test that could tell a real pipe from mere adjacency).
587
+ */
588
+ function grepPipesIntoTruncatingTail(segments, index) {
589
+ if (segments[index]?.terminator !== "pipe")
590
+ return false;
591
+ for (let j = index + 1; j < segments.length; j++) {
592
+ if (isLineDroppingTailCommand(segments[j].tokens))
593
+ return true;
594
+ if (segments[j].terminator !== "pipe")
595
+ return false;
596
+ }
597
+ return false;
598
+ }
517
599
  function collectGrepCommandFiles(command, cwd) {
518
600
  const files = new Set();
519
601
  let hasLineNumberGrep = false;
@@ -522,7 +604,14 @@ function collectGrepCommandFiles(command, cwd) {
522
604
  // contributing grep printed, so no line is credited that a hit from the
523
605
  // narrowest grep never delivered.
524
606
  let context;
525
- for (const tokens of commandSegments(command)) {
607
+ // #1908: if ANY contributing grep pipes into a truncating tail, fall back
608
+ // to match-line-only credit for the whole command — the parsed hits carry
609
+ // no segment identity, so a per-segment override isn't representable in
610
+ // the aggregate `context` this function returns.
611
+ let truncated = false;
612
+ const segments = tokenizeShellCommand(command);
613
+ for (let i = 0; i < segments.length; i++) {
614
+ const tokens = segments[i].tokens;
526
615
  const verb = path.basename(stripQuotes(tokens[0] ?? ""));
527
616
  if (verb !== "grep" && verb !== "egrep" && verb !== "fgrep")
528
617
  continue;
@@ -539,11 +628,15 @@ function collectGrepCommandFiles(command, cwd) {
539
628
  : segmentContext;
540
629
  for (const file of extractGrepSearchFiles(args, cwd))
541
630
  files.add(file);
631
+ if (grepPipesIntoTruncatingTail(segments, i))
632
+ truncated = true;
542
633
  }
543
634
  return {
544
635
  hasLineNumberGrep,
545
636
  files,
546
- context: context ?? { before: 0, after: 0 },
637
+ context: truncated
638
+ ? { before: 0, after: 0 }
639
+ : (context ?? { before: 0, after: 0 }),
547
640
  };
548
641
  }
549
642
  function dedupePushSearchRead(out, seen, loc) {
@@ -58,9 +58,7 @@ export class BiomeClient {
58
58
  ensureFlight = createSingleFlight();
59
59
  log;
60
60
  constructor(verbose = false) {
61
- this.log = verbose
62
- ? createSubsystemLogger("biome")
63
- : () => { };
61
+ this.log = verbose ? createSubsystemLogger("biome") : () => { };
64
62
  }
65
63
  /**
66
64
  * Resolve the fastest available biome binary for `cwd`.
@@ -126,7 +126,11 @@ async function resolveForwardImportsMemoized(cwd, filePath, resolveForwardImport
126
126
  catch {
127
127
  imports = [];
128
128
  }
129
- importResolutionMemo.set(filePath, { mtimeMs: sig.mtimeMs, size: sig.size, imports });
129
+ importResolutionMemo.set(filePath, {
130
+ mtimeMs: sig.mtimeMs,
131
+ size: sig.size,
132
+ imports,
133
+ });
130
134
  return imports;
131
135
  }
132
136
  /** Test-only: clear the extractor and import-resolution memos. */
@@ -232,17 +236,26 @@ export async function collectForwardImportMtimes(cwd, filePath, resolveForwardIm
232
236
  * +50ms). +50ms comfortably clears the measured skew while staying far below the
233
237
  * gap between genuinely distinct edits.
234
238
  */
235
- export const MTIME_DRIFT_TOLERANCE_MS = 50;
239
+ // Single implementation lives in the freshness kernel (#1739); this
240
+ // re-export preserves every existing importer's path.
241
+ export { MTIME_DRIFT_TOLERANCE_MS } from "./freshness.js";
242
+ import { freshnessFromMtime } from "./freshness.js";
236
243
  async function detectDrift(cwd, filePath, recordedAtMs, resolveForwardImports, turnIndex) {
237
244
  const drifted = [];
238
- const ownMtimeMs = await statMtimeMs(filePath);
239
- if (ownMtimeMs !== undefined && ownMtimeMs > recordedAtMs + MTIME_DRIFT_TOLERANCE_MS) {
245
+ const ownFreshness = freshnessFromMtime({
246
+ mtimeMs: await statMtimeMs(filePath),
247
+ referenceMs: recordedAtMs,
248
+ });
249
+ if (ownFreshness.verdict === "stale")
240
250
  drifted.push(filePath);
241
- }
242
251
  const { mtimes, truncated } = await collectForwardImportMtimes(cwd, filePath, resolveForwardImports, turnIndex);
243
- for (const { path, mtimeMs } of mtimes) {
244
- if (mtimeMs > recordedAtMs + MTIME_DRIFT_TOLERANCE_MS)
245
- drifted.push(path);
252
+ for (const { path: depPath, mtimeMs } of mtimes) {
253
+ const verdict = freshnessFromMtime({
254
+ mtimeMs,
255
+ referenceMs: recordedAtMs,
256
+ });
257
+ if (verdict.verdict === "stale")
258
+ drifted.push(depPath);
246
259
  }
247
260
  return { drifted, truncated };
248
261
  }
@@ -56,7 +56,7 @@ export function sweepInlineBlockerPastEof(runtime, cwd, options) {
56
56
  total: 0,
57
57
  checked: 0,
58
58
  demoted: 0,
59
- healed: 0,
59
+ deadLinesByPath: new Map(),
60
60
  };
61
61
  const resync = options?.resync ?? resyncDocumentOnPastEof;
62
62
  let entries;
@@ -101,15 +101,25 @@ export function sweepInlineBlockerPastEof(runtime, cwd, options) {
101
101
  // The record cites any of its lines being past EOF as a whole-record
102
102
  // demotion — one summary string, not one flag per line.
103
103
  const isPastEof = diagnostics.some((d) => d.stale);
104
+ if (isPastEof) {
105
+ // #1944: record WHICH cited lines died, before the transition check
106
+ // below short-circuits. A record demoted on an earlier turn does not
107
+ // transition, yet its dead lines are exactly what the delivery
108
+ // surface must annotate and retire on.
109
+ counts.deadLinesByPath.set(entry.filePath, diagnostics
110
+ .filter((d) => d.stale)
111
+ .map((d) => d.line)
112
+ .filter((line) => typeof line === "number"));
113
+ }
104
114
  const transitioned = runtime.setInlineBlockerPastEofStale(entry.filePath, isPastEof);
105
115
  if (!transitioned)
106
116
  continue;
107
- if (isPastEof) {
117
+ // Only the rising edge is countable here — see `BlockerPastEofCounts`
118
+ // for why the falling edge cannot occur on this store anymore. The
119
+ // call below still un-demotes, so the STORE stays honest even though
120
+ // nothing can observe the transition.
121
+ if (isPastEof)
108
122
  counts.demoted += 1;
109
- }
110
- else {
111
- counts.healed += 1;
112
- }
113
123
  }
114
124
  catch {
115
125
  // Per-entry failure: leave the entry as-is.
@@ -13,7 +13,9 @@ export class BoundedLruCache {
13
13
  }
14
14
  return value;
15
15
  }
16
- has(key) { return this.entries.has(key); }
16
+ has(key) {
17
+ return this.entries.has(key);
18
+ }
17
19
  set(key, value) {
18
20
  this.entries.delete(key);
19
21
  this.entries.set(key, value);
@@ -24,11 +26,25 @@ export class BoundedLruCache {
24
26
  this.entries.delete(oldest);
25
27
  }
26
28
  }
27
- delete(key) { return this.entries.delete(key); }
28
- clear() { this.entries.clear(); }
29
- get size() { return this.entries.size; }
30
- keys() { return this.entries.keys(); }
31
- values() { return this.entries.values(); }
32
- entriesArray() { return [...this.entries.entries()]; }
33
- [Symbol.iterator]() { return this.entries[Symbol.iterator](); }
29
+ delete(key) {
30
+ return this.entries.delete(key);
31
+ }
32
+ clear() {
33
+ this.entries.clear();
34
+ }
35
+ get size() {
36
+ return this.entries.size;
37
+ }
38
+ keys() {
39
+ return this.entries.keys();
40
+ }
41
+ values() {
42
+ return this.entries.values();
43
+ }
44
+ entriesArray() {
45
+ return [...this.entries.entries()];
46
+ }
47
+ [Symbol.iterator]() {
48
+ return this.entries[Symbol.iterator]();
49
+ }
34
50
  }
@@ -35,8 +35,7 @@ async function releaseQuarantineLock(lockPath, token) {
35
35
  break;
36
36
  }
37
37
  catch (error) {
38
- if (error.code !== "ENOENT" ||
39
- attempt === 2)
38
+ if (error.code !== "ENOENT" || attempt === 2)
40
39
  return;
41
40
  await new Promise((resolve) => setImmediate(resolve));
42
41
  }
@@ -111,7 +110,9 @@ async function tryAcquireQuarantineLock(lockPath, staleMs) {
111
110
  await fsp.writeFile(path.join(lockPath, "owner.json"), JSON.stringify(owner), "utf8");
112
111
  }
113
112
  catch (error) {
114
- await fsp.rm(lockPath, { recursive: true, force: true }).catch(() => { });
113
+ await fsp
114
+ .rm(lockPath, { recursive: true, force: true })
115
+ .catch(() => { });
115
116
  throw error;
116
117
  }
117
118
  return () => releaseQuarantineLock(lockPath, owner.token);
@@ -77,12 +77,26 @@ export const BOUNDED_TELEMETRY_PHASES = [
77
77
  "lsp_client_skipped_broken",
78
78
  /** #1743 review: the same skip, for a temporarily unavailable command. */
79
79
  "lsp_client_skipped_unavailable_command",
80
+ /**
81
+ * #1934: `getWarmClientForFile` found no live client for a file that has at
82
+ * least one language server with a resolvable root. Its callers run per
83
+ * file (the cascade quiet window, read expansion, `hasWarmLSP`), so it is
84
+ * rising-edge per candidate (server, root) set, with the ledger holding the
85
+ * exact count.
86
+ */
87
+ "lsp_warm_client_missing",
80
88
  /**
81
89
  * #1723: an event-loop block at or above the floor. Not a degradation, so
82
90
  * no ledger kind; bounded by call cadence (one `turn_end` runs it once per
83
91
  * turn) rather than by an option here.
84
92
  */
85
93
  "loop_block",
94
+ /**
95
+ * #1925: a session event skipped because its ctx was invalidated by a
96
+ * session replacement or reload. A replaced session can drain a whole
97
+ * queue of them, so it is rising-edge per event name.
98
+ */
99
+ "session_event_stale_ctx_skip",
86
100
  ];
87
101
  const REGISTERED_PHASES = new Set(BOUNDED_TELEMETRY_PHASES);
88
102
  /** Report whether a phase name is in the registry (used by the sweep). */
@@ -39,9 +39,7 @@ function getTurnStatePath(cwd) {
39
39
  export class CacheManager {
40
40
  log;
41
41
  constructor(verbose = false) {
42
- this.log = verbose
43
- ? createSubsystemLogger("cache")
44
- : () => { };
42
+ this.log = verbose ? createSubsystemLogger("cache") : () => { };
45
43
  }
46
44
  /**
47
45
  * Convert a file path to a stable turn-state key.
@@ -226,11 +224,12 @@ export class CacheManager {
226
224
  // stale-session eviction behavior for this legacy shape.
227
225
  return "available";
228
226
  }
229
- if (state.owner.kind === owner.kind &&
230
- state.owner.id === owner.id) {
227
+ if (state.owner.kind === owner.kind && state.owner.id === owner.id) {
231
228
  return "owned";
232
229
  }
233
- return this.isTurnStateOwnerStale(state.owner) ? "available" : "foreign-live";
230
+ return this.isTurnStateOwnerStale(state.owner)
231
+ ? "available"
232
+ : "foreign-live";
234
233
  }
235
234
  isTurnStateOwnerStale(owner) {
236
235
  if (owner.pid > 0 && owner.pid !== process.pid) {
@@ -243,7 +242,7 @@ export class CacheManager {
243
242
  }
244
243
  }
245
244
  const lastSeen = Date.parse(owner.lastSeen);
246
- return !Number.isFinite(lastSeen) || Date.now() - lastSeen > TURN_OWNER_STALE_MS;
245
+ return (!Number.isFinite(lastSeen) || Date.now() - lastSeen > TURN_OWNER_STALE_MS);
247
246
  }
248
247
  /**
249
248
  * Add or update a file's modified ranges in turn state.