@herbertgao/pi-extensions 2026.8.7 → 2026.8.9

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 (161) hide show
  1. package/README.md +8 -6
  2. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +24 -0
  3. package/node_modules/@czottmann/pi-automode/README.md +78 -112
  4. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +18 -14
  5. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +44 -0
  6. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +5 -0
  7. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +198 -104
  8. package/node_modules/@czottmann/pi-automode/docs/configuration.md +155 -0
  9. package/node_modules/@czottmann/pi-automode/docs/defaults.md +55 -14
  10. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +90 -0
  11. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +61 -26
  12. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +5 -0
  13. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +172 -18
  14. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +127 -30
  15. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +3 -0
  16. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +273 -31
  17. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +60 -5
  18. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +92 -8
  19. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +203 -13
  20. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +1 -0
  21. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +11 -0
  22. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +9 -1
  23. package/node_modules/@czottmann/pi-automode/package.json +9 -2
  24. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +63 -0
  25. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +3 -3
  26. package/node_modules/@herbertgao/pi-cc-extensions/README.md +3 -3
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +8 -3
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +56 -29
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +30 -17
  33. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  34. package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +29 -0
  35. package/node_modules/@herbertgao/pi-handoff/README.md +15 -18
  36. package/node_modules/@herbertgao/pi-handoff/package.json +11 -4
  37. package/node_modules/@herbertgao/pi-handoff/src/complete-text.ts +33 -0
  38. package/node_modules/@herbertgao/pi-handoff/src/index.ts +127 -232
  39. package/node_modules/@herbertgao/pi-handoff/src/session-query.ts +15 -30
  40. package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +14 -0
  41. package/node_modules/@herbertgao/pi-mermaid-open/README.md +21 -4
  42. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +11 -0
  43. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/viewer.mjs +305 -0
  44. package/node_modules/@herbertgao/pi-mermaid-open/package.json +15 -4
  45. package/node_modules/@herbertgao/pi-mermaid-open/src/index.ts +351 -101
  46. package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +12 -0
  47. package/node_modules/@herbertgao/pi-preferred-thinking/README.md +30 -14
  48. package/node_modules/@herbertgao/pi-preferred-thinking/package.json +7 -4
  49. package/node_modules/@herbertgao/pi-preferred-thinking/src/index.ts +140 -85
  50. package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +12 -0
  51. package/node_modules/@herbertgao/pi-recap/README.md +5 -3
  52. package/node_modules/@herbertgao/pi-recap/package.json +9 -3
  53. package/node_modules/@herbertgao/pi-recap/src/index.ts +3 -7
  54. package/node_modules/@herbertgao/pi-recap/src/model-picker.ts +6 -19
  55. package/node_modules/@herbertgao/pi-recap/src/models.ts +9 -21
  56. package/node_modules/@herbertgao/pi-recap/src/tui.ts +1 -1
  57. package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +49 -0
  58. package/node_modules/@herbertgao/pi-rename/README.md +13 -11
  59. package/node_modules/@herbertgao/pi-rename/package.json +13 -3
  60. package/node_modules/@herbertgao/pi-rename/src/index.ts +135 -59
  61. package/node_modules/@herbertgao/pi-rename/src/models.ts +5 -13
  62. package/node_modules/@herbertgao/pi-rename/src/naming.ts +2 -6
  63. package/node_modules/@herbertgao/pi-stash/package.json +3 -3
  64. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  65. package/node_modules/@herbertgao/pi-subagents/README.md +16 -3
  66. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  67. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +15 -4
  68. package/node_modules/@herbertgao/pi-subagents/src/index.ts +16 -0
  69. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +5 -1
  70. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
  71. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
  72. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
  73. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
  74. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
  75. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
  76. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
  77. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  78. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
  79. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
  80. package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
  81. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
  82. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
  83. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
  84. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
  85. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
  86. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
  87. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
  88. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
  89. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  90. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
  91. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  92. package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
  93. package/node_modules/pi-lens/CHANGELOG.md +90 -0
  94. package/node_modules/pi-lens/README.md +106 -0
  95. package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
  96. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
  97. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
  98. package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
  99. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
  100. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
  101. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
  102. package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
  119. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
  120. package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
  121. package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
  122. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
  123. package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
  124. package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
  125. package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
  126. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
  127. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
  128. package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
  129. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
  130. package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
  131. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
  132. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
  133. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
  134. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
  135. package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
  136. package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
  137. package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
  138. package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
  139. package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
  140. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
  141. package/node_modules/pi-lens/dist/index.js +1224 -393
  142. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
  143. package/node_modules/pi-lens/package.json +18 -5
  144. package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
  145. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
  146. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
  147. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
  148. package/node_modules/pi-web-access/CHANGELOG.md +27 -0
  149. package/node_modules/pi-web-access/README.md +8 -7
  150. package/node_modules/pi-web-access/chrome-cookies.ts +96 -20
  151. package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
  152. package/node_modules/pi-web-access/extract.ts +12 -2
  153. package/node_modules/pi-web-access/firecrawl.ts +18 -2
  154. package/node_modules/pi-web-access/gemini-search.ts +28 -10
  155. package/node_modules/pi-web-access/github-extract.ts +65 -36
  156. package/node_modules/pi-web-access/index.ts +43 -14
  157. package/node_modules/pi-web-access/openai-search.ts +17 -6
  158. package/node_modules/pi-web-access/package.json +1 -1
  159. package/node_modules/pi-web-access/pdf-extract.ts +18 -5
  160. package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
  161. package/package.json +19 -18
@@ -2678,6 +2678,23 @@ function isLegacyGlobalState(value) {
2678
2678
  function getRegisteredLogFiles() {
2679
2679
  return registeredLogFiles;
2680
2680
  }
2681
+ function getSinkWriteFailures() {
2682
+ if (!ndjsonGlobalState)
2683
+ return [];
2684
+ const result = [];
2685
+ for (const state2 of ndjsonGlobalState.writers.values()) {
2686
+ if (state2.writeFailures > 0) {
2687
+ result.push({ file: state2.file, droppedCount: state2.writeFailures });
2688
+ }
2689
+ }
2690
+ return result;
2691
+ }
2692
+ function resetSinkWriteFailures() {
2693
+ if (!ndjsonGlobalState)
2694
+ return;
2695
+ for (const state2 of ndjsonGlobalState.writers.values())
2696
+ state2.writeFailures = 0;
2697
+ }
2681
2698
  function requireCurrentGlobalState() {
2682
2699
  if (ndjsonGlobalState)
2683
2700
  return ndjsonGlobalState;
@@ -2703,16 +2720,28 @@ function assertCompatibleWriterOptions(existing, maxBytes, backupPath) {
2703
2720
  return;
2704
2721
  throw new Error(`createNdjsonLogger: incompatible options for shared path ${existing.file}; the first writer's maxBytes/backupPath must be reused`);
2705
2722
  }
2723
+ function writeQueueItemSync(state2, item) {
2724
+ if (item.kind === "truncate") {
2725
+ fs.writeFileSync(state2.file, "");
2726
+ } else {
2727
+ rotateIfNeeded(state2);
2728
+ fs.appendFileSync(state2.file, item.line);
2729
+ }
2730
+ }
2706
2731
  function applyQueueItemSync(state2, item) {
2707
2732
  ensureDirSync(state2);
2708
- runBestEffort(() => {
2709
- if (item.kind === "truncate") {
2710
- fs.writeFileSync(state2.file, "");
2711
- } else {
2712
- rotateIfNeeded(state2);
2713
- fs.appendFileSync(state2.file, item.line);
2714
- }
2715
- });
2733
+ try {
2734
+ writeQueueItemSync(state2, item);
2735
+ return;
2736
+ } catch {
2737
+ }
2738
+ state2.ensuredDir = false;
2739
+ ensureDirSync(state2);
2740
+ try {
2741
+ writeQueueItemSync(state2, item);
2742
+ } catch {
2743
+ state2.writeFailures += 1;
2744
+ }
2716
2745
  }
2717
2746
  function flushStateSync(state2) {
2718
2747
  const inFlight2 = state2.inFlightBatch;
@@ -2735,6 +2764,8 @@ function createWriterState(file, maxBytes, backupPath) {
2735
2764
  assertCompatibleWriterOptions(existing, maxBytes, backupPath);
2736
2765
  if (!exitFlushers.has(existing.exitFlusher))
2737
2766
  registerWriter(existing);
2767
+ if (typeof existing.writeFailures !== "number")
2768
+ existing.writeFailures = 0;
2738
2769
  return existing;
2739
2770
  }
2740
2771
  const state2 = {};
@@ -2747,6 +2778,7 @@ function createWriterState(file, maxBytes, backupPath) {
2747
2778
  state2.syncRepairItems = null;
2748
2779
  state2.ensuredDir = false;
2749
2780
  state2.exitFlusher = () => flushStateSync(state2);
2781
+ state2.writeFailures = 0;
2750
2782
  globalState2.writers.set(file, state2);
2751
2783
  registerWriter(state2);
2752
2784
  return state2;
@@ -2781,17 +2813,28 @@ function rotateIfNeeded(state2) {
2781
2813
  } catch {
2782
2814
  }
2783
2815
  }
2816
+ async function writeQueueItemAsync(state2, item) {
2817
+ if (item.kind === "truncate") {
2818
+ await fs.promises.writeFile(state2.file, "");
2819
+ } else {
2820
+ rotateIfNeeded(state2);
2821
+ await fs.promises.appendFile(state2.file, item.line);
2822
+ }
2823
+ }
2784
2824
  async function applyQueueItemAsync(state2, item) {
2785
2825
  await ensureDirAsync(state2);
2786
2826
  try {
2787
- if (item.kind === "truncate") {
2788
- await fs.promises.writeFile(state2.file, "");
2789
- } else {
2790
- rotateIfNeeded(state2);
2791
- await fs.promises.appendFile(state2.file, item.line);
2792
- }
2827
+ await writeQueueItemAsync(state2, item);
2828
+ return;
2793
2829
  } catch {
2794
2830
  }
2831
+ state2.ensuredDir = false;
2832
+ await ensureDirAsync(state2);
2833
+ try {
2834
+ await writeQueueItemAsync(state2, item);
2835
+ } catch {
2836
+ state2.writeFailures += 1;
2837
+ }
2795
2838
  }
2796
2839
  async function drainLoop(state2) {
2797
2840
  while (state2.queue.length > 0) {
@@ -2801,14 +2844,24 @@ async function drainLoop(state2) {
2801
2844
  const pendingEnd = truncateIndex === -1 ? state2.queue.length : truncateIndex;
2802
2845
  const pending = item.kind === "truncate" ? [item] : state2.queue.slice(0, pendingEnd);
2803
2846
  state2.inFlightBatch = pending;
2804
- try {
2847
+ const writeBatch = async () => {
2805
2848
  if (item.kind === "truncate") {
2806
2849
  await fs.promises.writeFile(state2.file, "");
2807
2850
  } else {
2808
2851
  rotateIfNeeded(state2);
2809
2852
  await fs.promises.appendFile(state2.file, pending.map((queued) => queued.line).join(""));
2810
2853
  }
2854
+ };
2855
+ try {
2856
+ await writeBatch();
2811
2857
  } catch {
2858
+ state2.ensuredDir = false;
2859
+ await ensureDirAsync(state2);
2860
+ try {
2861
+ await writeBatch();
2862
+ } catch {
2863
+ state2.writeFailures += pending.length;
2864
+ }
2812
2865
  }
2813
2866
  for (const written of pending) {
2814
2867
  if (state2.queue[0] !== written)
@@ -3315,12 +3368,25 @@ function boundedKind(value) {
3315
3368
  return groups.size < MAX_DISTINCT_KINDS - 1 ? kind : OVERFLOW_KIND;
3316
3369
  }
3317
3370
  function getDegradationSummary() {
3318
- return [...groups.entries()].map(([kind, group]) => ({
3371
+ const summary = [...groups.entries()].map(([kind, group]) => ({
3319
3372
  kind,
3320
3373
  count: group.count,
3321
3374
  droppedCount: group.count - group.entries.length,
3322
3375
  latestReasons: group.entries.map((entry) => ({ ...entry }))
3323
3376
  }));
3377
+ const sinkFailures = getSinkWriteFailures();
3378
+ if (sinkFailures.length > 0) {
3379
+ summary.push({
3380
+ kind: "log-sink-write-failure",
3381
+ count: sinkFailures.reduce((total, sink) => total + sink.droppedCount, 0),
3382
+ droppedCount: 0,
3383
+ latestReasons: sinkFailures.map((sink) => ({
3384
+ subject: truncateForLedger(sink.file),
3385
+ reason: truncateForLedger(`${sink.droppedCount} dropped write(s) after reopen-retry failed`)
3386
+ }))
3387
+ });
3388
+ }
3389
+ return summary;
3324
3390
  }
3325
3391
  function debugLedgerFailure(operation, error) {
3326
3392
  try {
@@ -3337,6 +3403,7 @@ function resetDegradationLedger() {
3337
3403
  onceKeys.clear();
3338
3404
  tallies.clear();
3339
3405
  ledgerGeneration++;
3406
+ resetSinkWriteFailures();
3340
3407
  }
3341
3408
  var ENTRIES_PER_KIND, MAX_DISTINCT_KINDS, OVERFLOW_KIND, groups, onceKeys, tallies, ledgerGeneration;
3342
3409
  var init_degradation_ledger = __esm({
@@ -3345,6 +3412,7 @@ var init_degradation_ledger = __esm({
3345
3412
  init_extension_log();
3346
3413
  init_ledger_bounds();
3347
3414
  init_latency_logger();
3415
+ init_ndjson_logger();
3348
3416
  ENTRIES_PER_KIND = 20;
3349
3417
  MAX_DISTINCT_KINDS = 32;
3350
3418
  OVERFLOW_KIND = "other";
@@ -22617,11 +22685,11 @@ function classifyEntry(entry, fullPath, cfg, probeCache, skipCounters) {
22617
22685
  return { recurseInto: fullPath };
22618
22686
  }
22619
22687
  if (entry.isFile()) {
22620
- if (ignoreMatcher.isIgnored(fullPath, false))
22621
- return {};
22622
22688
  const ext2 = path38.extname(entry.name).toLowerCase();
22623
22689
  if (!extensions.has(ext2))
22624
22690
  return {};
22691
+ if (ignoreMatcher.isIgnored(fullPath, false))
22692
+ return {};
22625
22693
  if (isBuildArtifact(fullPath, probeCache)) {
22626
22694
  if (skipCounters)
22627
22695
  skipCounters.buildArtifactSkips += 1;
@@ -28510,6 +28578,46 @@ function skipUnlessToolRan(tool, input, fields) {
28510
28578
  });
28511
28579
  return { status: "skipped", diagnostics: [], semantic: "none" };
28512
28580
  }
28581
+ function recordParsedNothing(input) {
28582
+ const { tool, status, output, parsedCount } = input;
28583
+ if (parsedCount > 0)
28584
+ return false;
28585
+ if (!status)
28586
+ return false;
28587
+ const text = output ?? "";
28588
+ if (!text.trim())
28589
+ return false;
28590
+ const entries = Object.entries(input.fields ?? {}).filter(([, value]) => value !== void 0 && value !== "");
28591
+ const identity = entries.length ? `${tool} (${entries.map(([key2, value]) => `${key2}=${value}`).join(", ")})` : tool;
28592
+ incrementDegradationCount({
28593
+ kind: "runner-parsed-nothing",
28594
+ subject: tool,
28595
+ reason: truncateForLedger(`${identity} exited ${status} with ${text.length} chars of output but the parser read 0 diagnostics: ${firstOutputLine(text) || "no first line"}`)
28596
+ });
28597
+ return true;
28598
+ }
28599
+ function parseToolRun(tool, input, parse6, options = {}) {
28600
+ const skipped = skipUnlessToolRan(tool, input, options.fields);
28601
+ if (skipped)
28602
+ return { skipped, diagnostics: [], parsedNothing: false };
28603
+ const output = options.parseOutput ?? input.output ?? input.result.stdout ?? "";
28604
+ const diagnostics = [...parse6(output)];
28605
+ const parsedNothing = recordParsedNothing({
28606
+ tool,
28607
+ status: input.result.status ?? null,
28608
+ output,
28609
+ parsedCount: diagnostics.length,
28610
+ fields: options.fields
28611
+ });
28612
+ if (parsedNothing && options.skipWhenParsedNothing) {
28613
+ return {
28614
+ skipped: { status: "skipped", diagnostics: [], semantic: "none" },
28615
+ diagnostics: [],
28616
+ parsedNothing
28617
+ };
28618
+ }
28619
+ return { skipped: null, diagnostics, parsedNothing };
28620
+ }
28513
28621
  var init_tool_failure = __esm({
28514
28622
  "dist/clients/dispatch/runners/utils/tool-failure.js"() {
28515
28623
  "use strict";
@@ -29413,11 +29521,11 @@ var init_jscpd_client = __esm({
29413
29521
  }
29414
29522
  if (!entry.isFile())
29415
29523
  return "skip";
29416
- if (ignoreMatcher.isIgnored(fullPath, false))
29417
- return "skip";
29418
29524
  if (/\.(ts|tsx|js|jsx|mjs|cjs|py|pyi|java|go|rs|rb|php|swift|kt|kts|dart|lua|scala|c|h|cpp|cc|cxx|hpp|hxx|cs|m|mm)$/.test(entry.name)) {
29419
29525
  if (entry.name.endsWith(".d.ts"))
29420
29526
  return "skip";
29527
+ if (ignoreMatcher.isIgnored(fullPath, false))
29528
+ return "skip";
29421
29529
  return "stop";
29422
29530
  }
29423
29531
  return "skip";
@@ -31609,7 +31717,8 @@ function logTreeSitterCacheStats(options) {
31609
31717
  maxSize: options.stats.maxSize,
31610
31718
  totalBytes: options.stats.totalBytes,
31611
31719
  totalLines: options.stats.totalLines
31612
- }
31720
+ },
31721
+ ...options.astGrep ? { astGrep: options.astGrep } : {}
31613
31722
  }
31614
31723
  });
31615
31724
  }
@@ -38909,7 +39018,11 @@ function shouldLogEvent(event) {
38909
39018
  return LOG_ALLOWED_EDITS;
38910
39019
  if (event === "range_snapshot_validation")
38911
39020
  return LOG_SNAPSHOT_VALIDATION;
38912
- return event === "edit_blocked" || event === "edit_warned" || event === "exemption_added" || event === "oldtext_not_found" || event === "oldtext_duplicate" || event === "oldtext_indent_autopatched" || event === "oldtext_trailing_ws_autopatched" || event === "oldtext_escape_autopatched" || event === "edit_range_relocated" || event === "edit_preflight_blocked" || event === "edit_partial_apply" || event === "edit_partial_apply_skipped" || event === "edit_post_edit_pipeline_failed" || event === "edit_batch_summary" || event === "edit_batch_summary_overflow" || event === "touched_lines_missing";
39021
+ return event === "edit_blocked" || event === "edit_warned" || event === "exemption_added" || event === "oldtext_not_found" || event === "oldtext_duplicate" || event === "oldtext_indent_autopatched" || event === "oldtext_trailing_ws_autopatched" || event === "oldtext_escape_autopatched" || event === "edit_range_relocated" || event === "edit_preflight_blocked" || event === "edit_partial_apply" || event === "edit_partial_apply_skipped" || event === "edit_post_edit_pipeline_failed" || event === "edit_batch_summary" || event === "edit_batch_summary_overflow" || event === "touched_lines_missing" || // #1913: read-guard record-cap evictions are rare by construction (only
39022
+ // READ_GUARD_MAX_RECORDS_PER_FILE overflow triggers one), so this trim
39023
+ // record bypasses the per-read verbosity gate — a live eviction
39024
+ // regression must be visible without PI_LENS_READ_GUARD_VERBOSE=1.
39025
+ event === "read_cap_trimmed";
38913
39026
  }
38914
39027
  function boundTelemetryValue(value, pathName, depth) {
38915
39028
  if (typeof value === "string") {
@@ -39700,11 +39813,11 @@ function getModuleSourceFiles(moduleRoot, maxFiles = 20) {
39700
39813
  }
39701
39814
  if (!entry.isFile())
39702
39815
  continue;
39816
+ if (!SOURCE_EXTS.has(path72.extname(entry.name).toLowerCase()))
39817
+ continue;
39703
39818
  if (ignoreMatcher.isIgnored(fullPath, false))
39704
39819
  continue;
39705
- if (SOURCE_EXTS.has(path72.extname(entry.name).toLowerCase())) {
39706
- files2.push(normalizeMapKey(fullPath));
39707
- }
39820
+ files2.push(normalizeMapKey(fullPath));
39708
39821
  }
39709
39822
  };
39710
39823
  visit2(root, 0);
@@ -41834,12 +41947,26 @@ var init_bounded_telemetry = __esm({
41834
41947
  "lsp_client_skipped_broken",
41835
41948
  /** #1743 review: the same skip, for a temporarily unavailable command. */
41836
41949
  "lsp_client_skipped_unavailable_command",
41950
+ /**
41951
+ * #1934: `getWarmClientForFile` found no live client for a file that has at
41952
+ * least one language server with a resolvable root. Its callers run per
41953
+ * file (the cascade quiet window, read expansion, `hasWarmLSP`), so it is
41954
+ * rising-edge per candidate (server, root) set, with the ledger holding the
41955
+ * exact count.
41956
+ */
41957
+ "lsp_warm_client_missing",
41837
41958
  /**
41838
41959
  * #1723: an event-loop block at or above the floor. Not a degradation, so
41839
41960
  * no ledger kind; bounded by call cadence (one `turn_end` runs it once per
41840
41961
  * turn) rather than by an option here.
41841
41962
  */
41842
- "loop_block"
41963
+ "loop_block",
41964
+ /**
41965
+ * #1925: a session event skipped because its ctx was invalidated by a
41966
+ * session replacement or reload. A replaced session can drain a whole
41967
+ * queue of them, so it is rising-edge per event name.
41968
+ */
41969
+ "session_event_stale_ctx_skip"
41843
41970
  ];
41844
41971
  REGISTERED_PHASES = new Set(BOUNDED_TELEMETRY_PHASES);
41845
41972
  turnCounts = /* @__PURE__ */ new Map();
@@ -48819,6 +48946,19 @@ function setupConnectionLifecycle(state2, recentStderr) {
48819
48946
  });
48820
48947
  }
48821
48948
  });
48949
+ state2.lspProcess.process.on("close", (code, signal) => {
48950
+ if (state2.shutdownRequested)
48951
+ return;
48952
+ try {
48953
+ const stderrTail = recentStderr(20);
48954
+ incrementDegradationCount({
48955
+ kind: "lsp-server-unexpected-close",
48956
+ subject: state2.serverId,
48957
+ reason: `closed unprompted: code=${code ?? "none"} signal=${signal ?? "none"} stderr=${stderrTail.length > 0 ? `present(${stderrTail.length}b)` : "empty"} root=${state2.root}`
48958
+ });
48959
+ } catch {
48960
+ }
48961
+ });
48822
48962
  }
48823
48963
  async function clientRequestPullDiagnostics(state2, filePath, budgetMs = PULL_REQUEST_TIMEOUT_MS) {
48824
48964
  if (!isClientAlive(state2))
@@ -49590,13 +49730,58 @@ function armNavLateAnswerTelemetry(args) {
49590
49730
  }, () => {
49591
49731
  });
49592
49732
  }
49733
+ function chooseLivenessProbe(state2) {
49734
+ if (state2.operationSupport?.workspaceSymbol === true) {
49735
+ return {
49736
+ method: "workspace/symbol",
49737
+ params: { query: LIVENESS_PING_QUERY }
49738
+ };
49739
+ }
49740
+ const openUri = firstOpenDocumentUri(state2);
49741
+ if (openUri === void 0)
49742
+ return void 0;
49743
+ if (state2.operationSupport?.documentSymbol === true) {
49744
+ return {
49745
+ method: "textDocument/documentSymbol",
49746
+ params: { textDocument: { uri: openUri } }
49747
+ };
49748
+ }
49749
+ if (state2.operationSupport?.hover === true) {
49750
+ return {
49751
+ method: "textDocument/hover",
49752
+ params: {
49753
+ textDocument: { uri: openUri },
49754
+ position: { line: 0, character: 0 }
49755
+ }
49756
+ };
49757
+ }
49758
+ return void 0;
49759
+ }
49760
+ function firstOpenDocumentUri(state2) {
49761
+ for (const normalizedPath of state2.openDocuments) {
49762
+ const uri = state2.openDocumentUris?.get(normalizedPath);
49763
+ if (uri !== void 0)
49764
+ return uri;
49765
+ }
49766
+ return void 0;
49767
+ }
49593
49768
  async function clientPingLiveness(state2, timeoutMs = LIVENESS_PING_TIMEOUT_MS) {
49594
49769
  if (!isClientAlive(state2))
49595
49770
  return false;
49771
+ const probe = chooseLivenessProbe(state2);
49772
+ if (probe === void 0) {
49773
+ try {
49774
+ incrementDegradationCount({
49775
+ kind: "lsp-liveness-probe-unsupported",
49776
+ subject: state2.serverId,
49777
+ reason: "no advertised request method to probe with (no workspaceSymbolProvider, and no open document with documentSymbolProvider or hoverProvider); liveness from process and connection state only"
49778
+ });
49779
+ } catch {
49780
+ }
49781
+ return isClientAlive(state2);
49782
+ }
49596
49783
  try {
49597
- await withTimeout(safeSendRequest(state2.connection, "workspace/symbol", {
49598
- query: LIVENESS_PING_QUERY
49599
- }), timeoutMs);
49784
+ await withTimeout(safeSendRequest(state2.connection, probe.method, probe.params), timeoutMs);
49600
49785
  } catch (err) {
49601
49786
  if (err instanceof Error && err.message.startsWith("Timeout after")) {
49602
49787
  return false;
@@ -49872,6 +50057,7 @@ async function createLSPClient(options) {
49872
50057
  initializationOptions: initialization
49873
50058
  }), initializeTimeoutMs);
49874
50059
  } catch (err) {
50060
+ state2.shutdownRequested = true;
49875
50061
  const pid = lspProcess.pid;
49876
50062
  void killProcessTree(lspProcess.process, pid);
49877
50063
  void removeLspChild(pid, extractSpawnMarker(lspProcess.args)).catch((err2) => {
@@ -51533,7 +51719,14 @@ async function collectWorkspaceDiagnosticFiles(root, maxFiles = getMaxWorkspaceD
51533
51719
  if (ignoreMatcher.isIgnored(full, true))
51534
51720
  continue;
51535
51721
  await walk2(full);
51536
- } else if (entry.isFile() && !ignoreMatcher.isIgnored(full, false) && getServersForFileWithConfig(full).length > 0) {
51722
+ } else if (
51723
+ // #1974: the getServersForFileWithConfig lookup (an in-memory
51724
+ // extension/pathFilter match against the registered LSP servers) is
51725
+ // cheap relative to isIgnored's per-call minimatch pattern compile,
51726
+ // so it gates first — the same order-independent shape as the four
51727
+ // walkers fixed for #1974.
51728
+ entry.isFile() && getServersForFileWithConfig(full).length > 0 && !ignoreMatcher.isIgnored(full, false)
51729
+ ) {
51537
51730
  files2.push(full);
51538
51731
  }
51539
51732
  }
@@ -51647,6 +51840,21 @@ var init_lsp = __esm({
51647
51840
  /** Server/root pairs that already emitted unavailable for the current occurrence. */
51648
51841
  unavailableLogged = /* @__PURE__ */ new Set();
51649
51842
  optionalDisabled = /* @__PURE__ */ new Set();
51843
+ /**
51844
+ * #1934 review F1: what the last COMPLETED `spawnClient` call for a
51845
+ * (server, root) key decided, written by that call at every point it
51846
+ * returns without a client. This is a direct signal, deliberately NOT an
51847
+ * inference from breaker state: the "binary unavailable while installs are
51848
+ * disabled" branch sets a cooldown yet is a POLICY decline by its own
51849
+ * comment, so reading the cooldown mislabels it as a server failure.
51850
+ *
51851
+ * Not a latch, and nothing re-arms it at `session_start`. Every read sits
51852
+ * in the same microtask as the `await` of the spawn promise that just
51853
+ * wrote it, so a stale entry is unreachable: a read is always preceded by
51854
+ * its own attempt's write. Cardinality matches `state.clients` — one entry
51855
+ * per (server, root) — and a successful spawn deletes its entry.
51856
+ */
51857
+ lastSpawnVerdict = /* @__PURE__ */ new Map();
51650
51858
  /** Consecutive failure counts for exponential backoff circuit breaker */
51651
51859
  failureCounts = /* @__PURE__ */ new Map();
51652
51860
  /**
@@ -52558,8 +52766,14 @@ var init_lsp = __esm({
52558
52766
  const withBudget2 = async () => {
52559
52767
  if (servers.length === 0)
52560
52768
  return void 0;
52769
+ let erroredServerId;
52561
52770
  for (const server of servers) {
52562
- const spawned = await this.ensureClientForServer(filePath, server, resolvedRoots, noteSpawnInFlight);
52771
+ const acquisition = {
52772
+ outcome: "declined"
52773
+ };
52774
+ const spawned = await this.ensureClientForServer(filePath, server, resolvedRoots, noteSpawnInFlight, (reported) => {
52775
+ acquisition.outcome = reported;
52776
+ });
52563
52777
  if (spawned) {
52564
52778
  logLatency({
52565
52779
  type: "phase",
@@ -52568,11 +52782,32 @@ var init_lsp = __esm({
52568
52782
  durationMs: 0,
52569
52783
  metadata: {
52570
52784
  serverId: server.id,
52571
- candidateCount: servers.length
52785
+ candidateCount: servers.length,
52786
+ // Emitted RAW, never coerced to a "safe" value: a served
52787
+ // client always reports `warm-reuse` or `cold-spawn`, so a
52788
+ // `declined` here would be a real reporting bug and must be
52789
+ // visible in the log rather than laundered into a lie.
52790
+ outcome: acquisition.outcome
52572
52791
  }
52573
52792
  });
52574
52793
  return spawned;
52575
52794
  }
52795
+ if (acquisition.outcome === "spawn-failure") {
52796
+ erroredServerId ??= server.id;
52797
+ }
52798
+ }
52799
+ if (erroredServerId !== void 0) {
52800
+ logLatency({
52801
+ type: "phase",
52802
+ phase: "lsp_client_selected",
52803
+ filePath,
52804
+ durationMs: 0,
52805
+ metadata: {
52806
+ serverId: erroredServerId,
52807
+ candidateCount: servers.length,
52808
+ outcome: "spawn-failure"
52809
+ }
52810
+ });
52576
52811
  }
52577
52812
  const unavailable = (await Promise.all(servers.map(async (server) => {
52578
52813
  const root = await server.root(filePath);
@@ -52697,6 +52932,7 @@ var init_lsp = __esm({
52697
52932
  if (this.checkDestroyed())
52698
52933
  return void 0;
52699
52934
  const servers = getServersForFileWithConfig(filePath);
52935
+ const missed = [];
52700
52936
  for (const server of servers) {
52701
52937
  const root = await this.resolveServerRoot(server, filePath);
52702
52938
  if (!root)
@@ -52706,6 +52942,21 @@ var init_lsp = __esm({
52706
52942
  if (existing?.isAlive()) {
52707
52943
  return { client: existing, info: server };
52708
52944
  }
52945
+ missed.push(key2);
52946
+ }
52947
+ if (missed.length > 0) {
52948
+ emitBounded("lsp_warm_client_missing", missed.join(","), {
52949
+ filePath,
52950
+ durationMs: 0,
52951
+ metadata: {
52952
+ serverIds: missed.map((key2) => key2.slice(0, key2.indexOf(":"))),
52953
+ roots: missed.map((key2) => key2.slice(key2.indexOf(":") + 1))
52954
+ }
52955
+ }, {
52956
+ ledgerKind: "lsp-warm-client-missing",
52957
+ risingEdgePer: "identity",
52958
+ reason: `no warm client for ${missed.join(",")}`
52959
+ });
52709
52960
  }
52710
52961
  return void 0;
52711
52962
  }
@@ -52881,7 +53132,22 @@ var init_lsp = __esm({
52881
53132
  }
52882
53133
  this.documentDrift.recordSynced(filePath, content, at);
52883
53134
  }
52884
- async ensureClientForServer(filePath, server, resolvedRoots, onSpawnInFlight) {
53135
+ /**
53136
+ * #1934 review F1: record what a `spawnClient` call decided, at the point
53137
+ * it decides it. Called on EVERY path that returns without a client, so
53138
+ * "no verdict" cannot silently mean "failed".
53139
+ *
53140
+ * `"failed"` is a server failure: the spawn or the initialize handshake
53141
+ * went wrong. `"declined"` is policy or lifecycle: host trust refused the
53142
+ * binary, the service shut down mid-spawn, or the binary is absent while
53143
+ * installs are disabled. That last one sets a breaker cooldown but is NOT
53144
+ * a failure, which is exactly why the outcome cannot be inferred from
53145
+ * breaker state.
53146
+ */
53147
+ noteSpawnVerdict(key2, verdict) {
53148
+ this.lastSpawnVerdict.set(key2, verdict);
53149
+ }
53150
+ async ensureClientForServer(filePath, server, resolvedRoots, onSpawnInFlight, onOutcome) {
52885
53151
  const handoff = this.generationHandoff;
52886
53152
  if (handoff) {
52887
53153
  await handoff;
@@ -52944,6 +53210,7 @@ var init_lsp = __esm({
52944
53210
  logSessionStart(`lsp warm-start ${server.id}: reused root=${root} file=${filePath}`);
52945
53211
  this.warmStartLogged.add(key2);
52946
53212
  }
53213
+ onOutcome?.("warm-reuse");
52947
53214
  return { client: existing, info: server };
52948
53215
  }
52949
53216
  const spawnedAt = this.state.clientSpawnedAt.get(key2);
@@ -53051,7 +53318,12 @@ var init_lsp = __esm({
53051
53318
  }
53052
53319
  onSpawnInFlight?.(server.id);
53053
53320
  try {
53054
- return await spawnPromise;
53321
+ const spawned = await spawnPromise;
53322
+ onOutcome?.(spawned ? "cold-spawn" : this.lastSpawnVerdict.get(key2) === "failed" ? "spawn-failure" : "declined");
53323
+ return spawned;
53324
+ } catch (err) {
53325
+ onOutcome?.("spawn-failure");
53326
+ throw err;
53055
53327
  } finally {
53056
53328
  if (this.state.inFlight.get(key2) === spawnPromise) {
53057
53329
  this.state.inFlight.delete(key2);
@@ -53069,6 +53341,7 @@ var init_lsp = __esm({
53069
53341
  async spawnClient(server, root, key2, filePath, allowInstall) {
53070
53342
  if (!isLspSpawnAllowedByTrust()) {
53071
53343
  logSessionStart(`lsp spawn ${server.id}: refused \u2014 ${projectTrustDenialReason()}`);
53344
+ this.noteSpawnVerdict(key2, "declined");
53072
53345
  return void 0;
53073
53346
  }
53074
53347
  const isOptionalServer = OPTIONAL_LSP_SERVER_IDS.has(server.id);
@@ -53083,6 +53356,7 @@ var init_lsp = __esm({
53083
53356
  } catch {
53084
53357
  }
53085
53358
  logSessionStart(`lsp spawn ${server.id}: aborted (service shut down mid-spawn)`);
53359
+ this.noteSpawnVerdict(key2, "declined");
53086
53360
  return void 0;
53087
53361
  }
53088
53362
  if (!spawned) {
@@ -53091,6 +53365,7 @@ var init_lsp = __esm({
53091
53365
  if (!allowInstall) {
53092
53366
  logSessionStart(`lsp spawn ${server.id}: unavailable with install disabled; temporary cooldown only`);
53093
53367
  this.state.broken.set(key2, Date.now() + BROKEN_BASE_COOLDOWN_MS);
53368
+ this.noteSpawnVerdict(key2, "declined");
53094
53369
  return void 0;
53095
53370
  }
53096
53371
  const uCount = (this.failureCounts.get(key2) ?? 0) + 1;
@@ -53102,6 +53377,7 @@ var init_lsp = __esm({
53102
53377
  this.recordBreaker(key2, `permanently disabled after ${uCount} unavailable spawns`);
53103
53378
  logSessionStart(`lsp spawn ${server.id}: permanently disabled after ${uCount} failures`);
53104
53379
  }
53380
+ this.noteSpawnVerdict(key2, "failed");
53105
53381
  return void 0;
53106
53382
  }
53107
53383
  const override = getServerInitOverride(server.id, filePath);
@@ -53118,6 +53394,7 @@ var init_lsp = __esm({
53118
53394
  client.shutdown({ fast: true }).catch(() => {
53119
53395
  });
53120
53396
  logSessionStart(`lsp spawn ${server.id}: aborted (service shut down mid-initialize)`);
53397
+ this.noteSpawnVerdict(key2, "declined");
53121
53398
  return void 0;
53122
53399
  }
53123
53400
  const wsDiag = typeof client.getWorkspaceDiagnosticsSupport === "function" ? client.getWorkspaceDiagnosticsSupport() : {
@@ -53127,6 +53404,7 @@ var init_lsp = __esm({
53127
53404
  };
53128
53405
  this.state.clients.set(key2, client);
53129
53406
  this.unavailableLogged.delete(key2);
53407
+ this.lastSpawnVerdict.delete(key2);
53130
53408
  this.state.clientSpawnedAt.set(key2, Date.now());
53131
53409
  this.clientLastUsedAt.set(key2, Date.now());
53132
53410
  this.scheduleTypeScriptIdleEviction(key2);
@@ -53164,6 +53442,7 @@ var init_lsp = __esm({
53164
53442
  if (isOptionalServer) {
53165
53443
  this.optionalDisabled.add(key2);
53166
53444
  }
53445
+ this.noteSpawnVerdict(key2, "failed");
53167
53446
  return void 0;
53168
53447
  }
53169
53448
  }
@@ -55714,6 +55993,7 @@ var init_lsp = __esm({
55714
55993
  });
55715
55994
  this.state.clients.clear();
55716
55995
  this.state.broken.clear();
55996
+ this.lastSpawnVerdict.clear();
55717
55997
  this.outstandingAuxNotifyWrites.clear();
55718
55998
  this.auxNotifyInflight.clear();
55719
55999
  this.documentDrift.clear();
@@ -56933,12 +57213,15 @@ Please read the file again before modifying it.`, absolutePath, "modified");
56933
57213
  import * as fs72 from "node:fs";
56934
57214
  function enforceRecordCapForFile(records) {
56935
57215
  if (records.length <= READ_GUARD_MAX_RECORDS_PER_FILE)
56936
- return 0;
57216
+ return NO_TRIM;
56937
57217
  const excess = records.length - READ_GUARD_MAX_RECORDS_PER_FILE;
56938
57218
  const evicted = /* @__PURE__ */ new Set();
57219
+ let evictedCreditCount = 0;
56939
57220
  for (let i = 0; i < records.length && evicted.size < excess; i++) {
56940
- if (records[i].searchCredit !== void 0)
57221
+ if (records[i].searchCredit !== void 0) {
56941
57222
  evicted.add(i);
57223
+ evictedCreditCount++;
57224
+ }
56942
57225
  }
56943
57226
  for (let i = 0; i < records.length && evicted.size < excess; i++) {
56944
57227
  evicted.add(i);
@@ -56947,7 +57230,11 @@ function enforceRecordCapForFile(records) {
56947
57230
  records.length = 0;
56948
57231
  for (const record2 of kept)
56949
57232
  records.push(record2);
56950
- return excess;
57233
+ return {
57234
+ evictedCount: excess,
57235
+ evictedCreditCount,
57236
+ evictedGenuineCount: excess - evictedCreditCount
57237
+ };
56951
57238
  }
56952
57239
  function captureReadContentBinding(filePath, offset, limit) {
56953
57240
  try {
@@ -57090,10 +57377,11 @@ function mapVerdictAction(action) {
57090
57377
  return "warned";
57091
57378
  }
57092
57379
  }
57093
- var READ_GUARD_STATE_VERSION, DEFAULT_CONFIG, OWN_EDIT_STALE_GRACE_MS, READ_HASH_MAX_LINES, READ_BINDING_MAX_BYTES, READ_GUARD_MAX_FILES, READ_GUARD_MAX_RECORDS_PER_FILE, READ_GUARD_MAX_EDITS_PER_FILE, READ_GUARD_MAX_UNCONSUMED_FILES, READ_GUARD_IDLE_EVICT_MS_DEFAULT, currentContentMatchesBinding, RELOCATION_WINDOW_MIN, RELOCATION_WINDOW_PER_EDIT, RELOCATION_WINDOW_MAX, ReadGuard;
57380
+ var READ_GUARD_STATE_VERSION, DEFAULT_CONFIG, OWN_EDIT_STALE_GRACE_MS, READ_HASH_MAX_LINES, READ_BINDING_MAX_BYTES, READ_GUARD_MAX_FILES, READ_GUARD_MAX_RECORDS_PER_FILE, NO_TRIM, READ_GUARD_MAX_EDITS_PER_FILE, READ_GUARD_MAX_UNCONSUMED_FILES, READ_GUARD_IDLE_EVICT_MS_DEFAULT, currentContentMatchesBinding, RELOCATION_WINDOW_MIN, RELOCATION_WINDOW_PER_EDIT, RELOCATION_WINDOW_MAX, ReadGuard;
57094
57381
  var init_read_guard = __esm({
57095
57382
  "dist/clients/read-guard.js"() {
57096
57383
  "use strict";
57384
+ init_degradation_ledger();
57097
57385
  init_file_time();
57098
57386
  init_diagnostic_binding();
57099
57387
  init_path_utils();
@@ -57114,6 +57402,11 @@ var init_read_guard = __esm({
57114
57402
  READ_BINDING_MAX_BYTES = 4 * 1024 * 1024;
57115
57403
  READ_GUARD_MAX_FILES = 256;
57116
57404
  READ_GUARD_MAX_RECORDS_PER_FILE = 128;
57405
+ NO_TRIM = {
57406
+ evictedCount: 0,
57407
+ evictedCreditCount: 0,
57408
+ evictedGenuineCount: 0
57409
+ };
57117
57410
  READ_GUARD_MAX_EDITS_PER_FILE = 256;
57118
57411
  READ_GUARD_MAX_UNCONSUMED_FILES = 4096;
57119
57412
  READ_GUARD_IDLE_EVICT_MS_DEFAULT = 30 * 6e4;
@@ -57155,6 +57448,8 @@ var init_read_guard = __esm({
57155
57448
  // happened to the file since. Pruned inside `evictFile` so it never
57156
57449
  // outlives the record it points at.
57157
57450
  knownPathIndex = /* @__PURE__ */ new Map();
57451
+ /** Running per-file record-cap trim totals for this session (#1913 F1). */
57452
+ trimAccumulators = /* @__PURE__ */ new Map();
57158
57453
  sessionId;
57159
57454
  sessionStartMs;
57160
57455
  constructor(sessionId, config = {}) {
@@ -57247,7 +57542,7 @@ var init_read_guard = __esm({
57247
57542
  arr.push(storedRecord);
57248
57543
  this.reads.set(storedRecord.filePath, arr);
57249
57544
  const rawReadCountForFile = arr.length;
57250
- const evictedRecordCount = enforceRecordCapForFile(arr);
57545
+ const { evictedCount: evictedRecordCount, evictedCreditCount, evictedGenuineCount } = enforceRecordCapForFile(arr);
57251
57546
  this.touchFile(storedRecord.filePath);
57252
57547
  this.enforceFileCap();
57253
57548
  logReadGuardEvent({
@@ -57282,6 +57577,39 @@ var init_read_guard = __esm({
57282
57577
  }
57283
57578
  }
57284
57579
  });
57580
+ if (evictedRecordCount > 0) {
57581
+ const key2 = storedRecord.filePath;
57582
+ const acc = this.trimAccumulators.get(key2) ?? {
57583
+ totalEvicted: 0,
57584
+ evictedCreditCount: 0,
57585
+ evictedGenuineCount: 0,
57586
+ trimEventCount: 0
57587
+ };
57588
+ acc.totalEvicted += evictedRecordCount;
57589
+ acc.evictedCreditCount += evictedCreditCount;
57590
+ acc.evictedGenuineCount += evictedGenuineCount;
57591
+ acc.trimEventCount += 1;
57592
+ this.trimAccumulators.set(key2, acc);
57593
+ const isRisingEdge = incrementDegradationCount({
57594
+ kind: "read-guard-record-cap-trim",
57595
+ subject: key2,
57596
+ reason: `trim #${acc.trimEventCount}: evicted ${evictedRecordCount} (credit ${evictedCreditCount}, genuine ${evictedGenuineCount})`
57597
+ });
57598
+ if (isRisingEdge) {
57599
+ logReadGuardEvent({
57600
+ event: "read_cap_trimmed",
57601
+ sessionId: this.sessionId,
57602
+ filePath: storedRecord.filePath,
57603
+ metadata: {
57604
+ trimEventCount: acc.trimEventCount,
57605
+ evictedRecordCount: acc.totalEvicted,
57606
+ evictedCreditCount: acc.evictedCreditCount,
57607
+ evictedGenuineCount: acc.evictedGenuineCount,
57608
+ rawReadCountForFile
57609
+ }
57610
+ });
57611
+ }
57612
+ }
57285
57613
  this.fileTime.read(storedRecord.filePath);
57286
57614
  }
57287
57615
  /**
@@ -57641,6 +57969,18 @@ Re-read the relevant section, then retry the edit using the current line range/c
57641
57969
  this.touchFile(key2);
57642
57970
  return this.reads.get(key2) ?? [];
57643
57971
  }
57972
+ /**
57973
+ * Session-lifetime record-cap trim totals for a file (#1913 review F1).
57974
+ * `recordRead` only writes ONE `read_cap_trimmed` read-guard.log line per
57975
+ * file per session (on the first trim), so this is the running-totals
57976
+ * surface for everything after that — the exact split this class already
57977
+ * computes on every trim, not re-derivable from `getReadHistory` alone
57978
+ * (which only shows the SURVIVING records, not what was evicted).
57979
+ */
57980
+ getTrimStats(filePath) {
57981
+ const stats = this.trimAccumulators.get(this.key(filePath));
57982
+ return stats ? { ...stats } : void 0;
57983
+ }
57644
57984
  /**
57645
57985
  * Snapshot the read-set for persistence across a session resume (#1041).
57646
57986
  * Mirrors widget-state's `exportWidgetState`: the Map is emitted as
@@ -58437,11 +58777,11 @@ async function collectSourceFilesForWarmup(rootDir, maxFiles = MAX_WARMUP_SOURCE
58437
58777
  }
58438
58778
  stack.push(fullPath);
58439
58779
  } else if (entry.isFile()) {
58440
- if (ignoreMatcher.isIgnored(fullPath, false))
58441
- continue;
58442
58780
  const ext2 = path90.extname(entry.name).toLowerCase();
58443
58781
  if (!WARMUP_SOURCE_EXTS.has(ext2))
58444
58782
  continue;
58783
+ if (ignoreMatcher.isIgnored(fullPath, false))
58784
+ continue;
58445
58785
  matchedSeen += 1;
58446
58786
  const bucket = WARMUP_CODE_EXTS.has(ext2) ? codeOut : nonCodeOut;
58447
58787
  if (bucket.length < maxFiles)
@@ -61302,6 +61642,11 @@ function recordOutstandingCascadeTouch(entry) {
61302
61642
  });
61303
61643
  }
61304
61644
  }
61645
+ function resetCascadeTierSessionState() {
61646
+ _outstandingTouches.clear();
61647
+ _expiredSinceLastSweep = 0;
61648
+ _evictedSinceLastSweep = 0;
61649
+ }
61305
61650
  async function reconcileOutstandingCascadeTouches(lspService) {
61306
61651
  const outcomes = [];
61307
61652
  pruneExpiredOutstandingTouches(Date.now());
@@ -62555,6 +62900,16 @@ var init_git_identity = __esm({
62555
62900
  }
62556
62901
  });
62557
62902
 
62903
+ // dist/clients/review-graph/revision-drift.js
62904
+ function formatReviewGraphRevisionDriftNote(drift) {
62905
+ return `Graph was built at commit ${drift.stampedHead.slice(0, 8)}; HEAD is now ${drift.currentHead.slice(0, 8)}. Results below reflect the earlier revision \u2014 run pilens_rebuild or re-analyze to refresh.`;
62906
+ }
62907
+ var init_revision_drift = __esm({
62908
+ "dist/clients/review-graph/revision-drift.js"() {
62909
+ "use strict";
62910
+ }
62911
+ });
62912
+
62558
62913
  // dist/clients/review-graph/symbol-id.js
62559
62914
  function buildSymbolId(filePath, name, kind, startLine) {
62560
62915
  return `${filePath}:${name}:${kind}:${startLine}`;
@@ -63190,6 +63545,7 @@ __export(builder_exports, {
63190
63545
  flushReviewGraphPersist: () => flushReviewGraphPersist,
63191
63546
  flushReviewGraphPersistsForExitForTests: () => flushReviewGraphPersistsForExitForTests,
63192
63547
  flushReviewGraphPersistsForTests: () => flushReviewGraphPersistsForTests,
63548
+ formatReviewGraphRevisionDriftNote: () => formatReviewGraphRevisionDriftNote,
63193
63549
  getCachedReviewGraph: () => getCachedReviewGraph,
63194
63550
  getCheckpointOffloadCountForTests: () => getCheckpointOffloadCountForTests,
63195
63551
  getGraphBuildInfoForGraph: () => getGraphBuildInfoForGraph,
@@ -63199,10 +63555,12 @@ __export(builder_exports, {
63199
63555
  getLastReviewGraphBuildAttempt: () => getLastReviewGraphBuildAttempt,
63200
63556
  getReviewGraphCacheIdentity: () => getReviewGraphCacheIdentity,
63201
63557
  getReviewGraphMaxFiles: () => getReviewGraphMaxFiles,
63558
+ getReviewGraphRevisionDrift: () => getReviewGraphRevisionDrift,
63202
63559
  getReviewGraphSizeSkipVerdict: () => getReviewGraphSizeSkipVerdict,
63203
63560
  getReviewGraphWorkerFallbackReasonForTests: () => getReviewGraphWorkerFallbackReasonForTests,
63204
63561
  getReviewGraphWorkspaceCacheSnapshot: () => getReviewGraphWorkspaceCacheSnapshot,
63205
63562
  graphBuildInfoIsTrustworthy: () => graphBuildInfoIsTrustworthy,
63563
+ isGraphBuildInFlight: () => isGraphBuildInFlight,
63206
63564
  isReviewGraphMigrationNeeded: () => isReviewGraphMigrationNeeded,
63207
63565
  resetReviewGraphPersistWorkerForTests: () => resetReviewGraphPersistWorkerForTests,
63208
63566
  reviewGraphCachePath: () => reviewGraphCachePath,
@@ -63227,13 +63585,20 @@ function clearWorkspaceGraphTimer(entry) {
63227
63585
  clearTimeout(entry.idleTimer);
63228
63586
  entry.idleTimer = void 0;
63229
63587
  }
63588
+ function buildCacheWorkspaceKey(cwd) {
63589
+ return normalizeMapKey(path104.resolve(cwd));
63590
+ }
63591
+ function buildCacheKeyWorkspace(buildKey) {
63592
+ const separator = buildKey.indexOf("|");
63593
+ return separator >= 0 ? buildKey.slice(0, separator) : void 0;
63594
+ }
63230
63595
  function evictWorkspaceGraph(key2, entry) {
63231
63596
  if (_workspaceGraphCache.get(key2) !== entry)
63232
63597
  return;
63233
63598
  clearWorkspaceGraphTimer(entry);
63599
+ const buildWorkspace = buildCacheWorkspaceKey(key2);
63234
63600
  for (const buildKey of _buildCache.keys()) {
63235
- const separator = buildKey.indexOf("|");
63236
- if (separator >= 0 && normalizeMapKey(buildKey.slice(0, separator)) === key2) {
63601
+ if (buildCacheKeyWorkspace(buildKey) === buildWorkspace) {
63237
63602
  _buildCache.delete(buildKey);
63238
63603
  }
63239
63604
  }
@@ -63308,8 +63673,9 @@ function clearReviewGraphWorkspaceCache(cwd) {
63308
63673
  _sizeSkipVerdicts.clear();
63309
63674
  } else {
63310
63675
  const normalized = normalizeMapKey(cwd);
63676
+ const buildWorkspace = buildCacheWorkspaceKey(cwd);
63311
63677
  for (const key2 of _buildCache.keys()) {
63312
- if (normalizeMapKey(key2).startsWith(`${normalized}|`)) {
63678
+ if (buildCacheKeyWorkspace(key2) === buildWorkspace) {
63313
63679
  _buildCache.delete(key2);
63314
63680
  }
63315
63681
  }
@@ -63415,7 +63781,7 @@ function getCachedReviewGraph(cwd) {
63415
63781
  return cached.graph;
63416
63782
  }
63417
63783
  const disk = loadPersistedGraph(cwd, {
63418
- verifyGitStamp: true,
63784
+ verifyWorktreeIdentity: true,
63419
63785
  allowPartial: true
63420
63786
  });
63421
63787
  if (!disk)
@@ -63424,10 +63790,23 @@ function getCachedReviewGraph(cwd) {
63424
63790
  signature: disk.signature,
63425
63791
  fileSignatures: disk.fileSignatures,
63426
63792
  fileHashes: disk.fileHashes,
63427
- graph: disk.graph
63793
+ graph: disk.graph,
63794
+ // Durable half of the drift fact. The current-HEAD half is never cached
63795
+ // beside it — see getReviewGraphRevisionDrift (#1961 review F3).
63796
+ ...disk.stampedHead ? { snapshotStampedHead: disk.stampedHead } : {}
63428
63797
  });
63429
63798
  return disk.graph;
63430
63799
  }
63800
+ function getReviewGraphRevisionDrift(cwd) {
63801
+ const entry = _workspaceGraphCache.get(normalizeMapKey(cwd));
63802
+ const stampedHead = entry?.snapshotStampedHead;
63803
+ if (!stampedHead)
63804
+ return void 0;
63805
+ const current = resolveGitIdentity(cwd);
63806
+ if (!current || current.headCommit === stampedHead)
63807
+ return void 0;
63808
+ return { stampedHead, currentHead: current.headCommit };
63809
+ }
63431
63810
  function makeCtx(filePath, cwd, facts) {
63432
63811
  return {
63433
63812
  filePath,
@@ -63724,6 +64103,19 @@ function rebuildIndexes(graph) {
63724
64103
  graph.edgesByTo.set(edge.to, to);
63725
64104
  }
63726
64105
  }
64106
+ function logSnapshotReadVerdict(cwd, phase, reason) {
64107
+ const verdict = phase === "snapshot_read_dropped" ? "dropped" : "drifted";
64108
+ const isRisingEdge = incrementDegradationCount({
64109
+ kind: "review-graph-snapshot-read",
64110
+ // Subject keeps BOTH discriminators, so aggregation still answers which
64111
+ // workspace and which verdict after the detailed records stop.
64112
+ subject: `${verdict}:${normalizeMapKey(cwd)}`,
64113
+ reason
64114
+ });
64115
+ if (!isRisingEdge)
64116
+ return;
64117
+ logReviewGraph({ cwd, phase, reason });
64118
+ }
63727
64119
  function loadPersistedGraph(cwd, opts) {
63728
64120
  const cacheDir = path104.join(getProjectDataDir(cwd), "cache");
63729
64121
  const cachePath3 = path104.join(cacheDir, GRAPH_CACHE_FILENAME);
@@ -63735,11 +64127,17 @@ function loadPersistedGraph(cwd, opts) {
63735
64127
  return null;
63736
64128
  if (data.coverage?.partial && !opts?.allowPartial)
63737
64129
  return null;
63738
- if (opts?.verifyGitStamp && data.gitStamp) {
64130
+ let stampedHead;
64131
+ if (opts?.verifyWorktreeIdentity && data.gitStamp) {
63739
64132
  const current = resolveGitIdentity(cwd);
63740
- if (current && (current.headCommit !== data.gitStamp.headCommit || current.worktreeRoot !== data.gitStamp.worktreeRoot)) {
64133
+ if (current && current.worktreeRoot !== data.gitStamp.worktreeRoot) {
64134
+ logSnapshotReadVerdict(cwd, "snapshot_read_dropped", "worktree_mismatch");
63741
64135
  return null;
63742
64136
  }
64137
+ if (current && current.headCommit !== data.gitStamp.headCommit) {
64138
+ stampedHead = data.gitStamp.headCommit;
64139
+ logSnapshotReadVerdict(cwd, "snapshot_read_drifted", "head_moved");
64140
+ }
63743
64141
  }
63744
64142
  const graph = {
63745
64143
  version: data.version,
@@ -63758,7 +64156,8 @@ function loadPersistedGraph(cwd, opts) {
63758
64156
  signature: data.signature,
63759
64157
  fileSignatures: new Map(data.fileSignatures ?? []),
63760
64158
  fileHashes: new Map(data.fileHashes ?? []),
63761
- graph
64159
+ graph,
64160
+ ...stampedHead ? { stampedHead } : {}
63762
64161
  };
63763
64162
  } catch {
63764
64163
  return null;
@@ -64543,11 +64942,11 @@ function loadReviewGraphCheckpoint(cwd) {
64543
64942
  }
64544
64943
  if (data.gitStamp) {
64545
64944
  const current = resolveGitIdentity(cwd);
64546
- if (current && (current.headCommit !== data.gitStamp.headCommit || current.worktreeRoot !== data.gitStamp.worktreeRoot)) {
64945
+ if (current && current.worktreeRoot !== data.gitStamp.worktreeRoot) {
64547
64946
  logReviewGraph({
64548
64947
  cwd,
64549
64948
  phase: "checkpoint_discarded",
64550
- reason: "git_stamp_mismatch"
64949
+ reason: "worktree_mismatch"
64551
64950
  });
64552
64951
  deleteReviewGraphCheckpoint(cwd);
64553
64952
  return null;
@@ -66038,6 +66437,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
66038
66437
  const graph = resumed?.graph ?? createEmptyGraph();
66039
66438
  const filesToExtract = resumed?.remaining ?? filesToBuild;
66040
66439
  const treeSitterClient = getSharedTreeSitterClient();
66440
+ treeSitterClient?.ensureTreeCacheCapacity(filesToExtract.length);
66041
66441
  const extractionStartedAt = Date.now();
66042
66442
  const fileHashes = resumed?.fileHashes ?? /* @__PURE__ */ new Map();
66043
66443
  const checkpointEvery = graphCheckpointEveryFiles();
@@ -66145,8 +66545,14 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
66145
66545
  facts.setSessionFact("session.reviewGraph", graph);
66146
66546
  return graph;
66147
66547
  }
66548
+ function buildCacheKey(cwd, changedFiles) {
66549
+ return `${buildCacheWorkspaceKey(cwd)}|${[...changedFiles].sort((a, b) => a.localeCompare(b)).join(",")}`;
66550
+ }
66551
+ function isGraphBuildInFlight(cwd, changedFiles = []) {
66552
+ return _buildCache.has(buildCacheKey(cwd, changedFiles));
66553
+ }
66148
66554
  function buildOrUpdateGraph(cwd, changedFiles, facts, seqHint) {
66149
- const cacheKey = `${cwd}|${[...changedFiles].sort((a, b) => a.localeCompare(b)).join(",")}`;
66555
+ const cacheKey = buildCacheKey(cwd, changedFiles);
66150
66556
  const cached = _buildCache.get(cacheKey);
66151
66557
  if (cached)
66152
66558
  return cached;
@@ -66212,7 +66618,6 @@ function buildOrUpdateGraph(cwd, changedFiles, facts, seqHint) {
66212
66618
  }
66213
66619
  return graph;
66214
66620
  }).catch((err) => {
66215
- _buildCache.delete(cacheKey);
66216
66621
  const reason = err instanceof Error ? err.message : String(err);
66217
66622
  recordBuildAttempt(cwd, "failed", reason, buildId);
66218
66623
  logReviewGraph({
@@ -66233,6 +66638,11 @@ function buildOrUpdateGraph(cwd, changedFiles, facts, seqHint) {
66233
66638
  throw err;
66234
66639
  });
66235
66640
  _buildCache.set(cacheKey, promise);
66641
+ const release = () => {
66642
+ if (_buildCache.get(cacheKey) === promise)
66643
+ _buildCache.delete(cacheKey);
66644
+ };
66645
+ promise.then(release, release);
66236
66646
  return promise;
66237
66647
  }
66238
66648
  function extractSymbolsAndRefsFromGraph(graph) {
@@ -66389,7 +66799,9 @@ var init_builder = __esm({
66389
66799
  init_tree_sitter_shared();
66390
66800
  init_tree_sitter_symbol_extractor();
66391
66801
  init_tree_sitter_shared();
66802
+ init_degradation_ledger();
66392
66803
  init_git_identity();
66804
+ init_revision_drift();
66393
66805
  init_symbol_id();
66394
66806
  init_shared_extraction_ir();
66395
66807
  REVIEW_GRAPH_VERSION = "v8";
@@ -68003,7 +68415,7 @@ var init_detekt = __esm({
68003
68415
  init_tool_policy();
68004
68416
  init_priorities();
68005
68417
  init_runner_helpers();
68006
- init_spawn_outcome();
68418
+ init_tool_failure();
68007
68419
  detekt = createAvailabilityChecker("detekt", ".bat");
68008
68420
  DETEKT_CONFIG_CANDIDATES = [
68009
68421
  "detekt.yml",
@@ -68096,10 +68508,10 @@ var init_detekt = __esm({
68096
68508
  const absPath = path112.resolve(cwd, ctx.filePath);
68097
68509
  const result = await safeSpawnAsync(cmd, ["--input", absPath, "--config", configPath], { cwd, timeout: 6e4 });
68098
68510
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
68099
- if (spawnFailedWithNoOutput(result, raw)) {
68100
- return { status: "skipped", diagnostics: [], semantic: "none" };
68101
- }
68102
- const diagnostics = parseDetektOutput(raw, ctx.filePath);
68511
+ const run = parseToolRun("detekt", { result, output: raw }, (out) => parseDetektOutput(out, ctx.filePath));
68512
+ if (run.skipped)
68513
+ return run.skipped;
68514
+ const diagnostics = run.diagnostics;
68103
68515
  if (diagnostics.length === 0) {
68104
68516
  return { status: "succeeded", diagnostics: [], semantic: "none" };
68105
68517
  }
@@ -68998,7 +69410,7 @@ var init_hadolint = __esm({
68998
69410
  init_tool_policy();
68999
69411
  init_priorities();
69000
69412
  init_runner_helpers();
69001
- init_spawn_outcome();
69413
+ init_tool_failure();
69002
69414
  hadolint = createAvailabilityChecker("hadolint", ".exe");
69003
69415
  hadolintRunner = {
69004
69416
  id: "hadolint",
@@ -69022,11 +69434,10 @@ var init_hadolint = __esm({
69022
69434
  return { status: "skipped", diagnostics: [], semantic: "none" };
69023
69435
  }
69024
69436
  const result = await safeSpawnAsync(cmd, ["--format", "json", "--no-fail", path116.resolve(cwd, ctx.filePath)], { cwd });
69025
- if (spawnFailedWithNoOutput(result)) {
69026
- return { status: "skipped", diagnostics: [], semantic: "none" };
69027
- }
69028
- const output = result.stdout || "";
69029
- const diagnostics = parseHadolintOutput(output, ctx.filePath);
69437
+ const run = parseToolRun("hadolint", { result }, (out) => parseHadolintOutput(out, ctx.filePath));
69438
+ if (run.skipped)
69439
+ return run.skipped;
69440
+ const diagnostics = run.diagnostics;
69030
69441
  if (diagnostics.length === 0) {
69031
69442
  return { status: "succeeded", diagnostics: [], semantic: "none" };
69032
69443
  }
@@ -69825,7 +70236,7 @@ var init_htmlhint = __esm({
69825
70236
  init_tool_policy();
69826
70237
  init_priorities();
69827
70238
  init_runner_helpers();
69828
- init_spawn_outcome();
70239
+ init_tool_failure();
69829
70240
  htmlhint = createAvailabilityChecker("htmlhint");
69830
70241
  HTMLHINT_RULES = {
69831
70242
  "tag-pair": true,
@@ -69865,10 +70276,10 @@ var init_htmlhint = __esm({
69865
70276
  path119.resolve(cwd, ctx.filePath)
69866
70277
  ], { cwd });
69867
70278
  const output = result.stdout || result.stderr || "";
69868
- if (spawnFailedWithNoOutput(result, output)) {
69869
- return { status: "skipped", diagnostics: [], semantic: "none" };
69870
- }
69871
- const diagnostics = parseHtmlhintOutput(output, ctx.filePath);
70279
+ const run = parseToolRun("htmlhint", { result, output }, (out) => parseHtmlhintOutput(out, ctx.filePath));
70280
+ if (run.skipped)
70281
+ return run.skipped;
70282
+ const diagnostics = run.diagnostics;
69872
70283
  if (diagnostics.length === 0) {
69873
70284
  return { status: "succeeded", diagnostics: [], semantic: "none" };
69874
70285
  }
@@ -70558,14 +70969,10 @@ var init_markdownlint = __esm({
70558
70969
  cwd
70559
70970
  });
70560
70971
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
70561
- const skipped = skipUnlessToolRan("markdownlint", {
70562
- result,
70563
- output: raw,
70564
- exitCodes: MARKDOWNLINT_EXIT_CODES
70565
- });
70566
- if (skipped)
70567
- return skipped;
70568
- const diagnostics = parseMarkdownlintOutput(raw, ctx.filePath);
70972
+ const run = parseToolRun("markdownlint", { result, output: raw, exitCodes: MARKDOWNLINT_EXIT_CODES }, (out) => parseMarkdownlintOutput(out, ctx.filePath));
70973
+ if (run.skipped)
70974
+ return run.skipped;
70975
+ const diagnostics = run.diagnostics;
70569
70976
  if (diagnostics.length === 0) {
70570
70977
  return { status: "succeeded", diagnostics: [], semantic: "none" };
70571
70978
  }
@@ -70637,14 +71044,10 @@ var init_mypy = __esm({
70637
71044
  return { status: "skipped", diagnostics: [], semantic: "none" };
70638
71045
  const result = await safeSpawnAsync(cmd, ["--no-error-summary", "--show-column-numbers", ctx.filePath], { timeout: 3e4, cwd });
70639
71046
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
70640
- const skipped = skipUnlessToolRan("mypy", {
70641
- result,
70642
- output: result.stdout,
70643
- exitCodes: MYPY_EXIT_CODES
70644
- });
70645
- if (skipped)
70646
- return skipped;
70647
- const diagnostics = parseMypyOutput(raw, ctx.filePath, cwd);
71047
+ const run = parseToolRun("mypy", { result, output: result.stdout, exitCodes: MYPY_EXIT_CODES }, (out) => parseMypyOutput(out, ctx.filePath, cwd), { parseOutput: raw });
71048
+ if (run.skipped)
71049
+ return run.skipped;
71050
+ const diagnostics = run.diagnostics;
70648
71051
  if (diagnostics.length === 0) {
70649
71052
  return { status: "succeeded", diagnostics: [], semantic: "none" };
70650
71053
  }
@@ -70794,9 +71197,6 @@ var init_oxlint = __esm({
70794
71197
  const result = await safeSpawnAsync(cmd, args, {
70795
71198
  timeout: 3e4
70796
71199
  });
70797
- if (result.status === 0) {
70798
- return { status: "succeeded", diagnostics: [], semantic: "none" };
70799
- }
70800
71200
  const stdout = result.stdout ?? "";
70801
71201
  const stderr = result.stderr ?? "";
70802
71202
  let diagnostics = parseOxlintJson(stdout, ctx.filePath);
@@ -70808,7 +71208,7 @@ var init_oxlint = __esm({
70808
71208
  }
70809
71209
  const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
70810
71210
  return {
70811
- status: "failed",
71211
+ status: !hasBlocking && result.status === 0 ? "succeeded" : "failed",
70812
71212
  diagnostics,
70813
71213
  semantic: hasBlocking ? "blocking" : "warning"
70814
71214
  };
@@ -70896,7 +71296,7 @@ function parsePhpstanJson(raw, fallbackPath, cwd) {
70896
71296
  const diagnostics = [];
70897
71297
  for (const [file, fileErrors] of Object.entries(output.files ?? {})) {
70898
71298
  const filePath = file && file.trim() ? path126.isAbsolute(file) ? file : path126.resolve(cwd, file) : fallbackPath;
70899
- for (const err of fileErrors.errors ?? []) {
71299
+ for (const err of fileErrors?.messages ?? []) {
70900
71300
  diagnostics.push({
70901
71301
  id: `phpstan:${err.line ?? 1}:${err.message.slice(0, 40)}`,
70902
71302
  message: err.message,
@@ -70906,11 +71306,27 @@ function parsePhpstanJson(raw, fallbackPath, cwd) {
70906
71306
  severity: "error",
70907
71307
  semantic: "blocking",
70908
71308
  tool: "phpstan",
70909
- rule: "phpstan",
71309
+ rule: err.identifier || "phpstan",
70910
71310
  fixable: false
70911
71311
  });
70912
71312
  }
70913
71313
  }
71314
+ for (const message of output.errors ?? []) {
71315
+ if (typeof message !== "string" || !message.trim())
71316
+ continue;
71317
+ diagnostics.push({
71318
+ id: `phpstan:global:${message.slice(0, 40)}`,
71319
+ message,
71320
+ filePath: fallbackPath,
71321
+ line: 1,
71322
+ column: 1,
71323
+ severity: "error",
71324
+ semantic: "blocking",
71325
+ tool: "phpstan",
71326
+ rule: "phpstan/analysis",
71327
+ fixable: false
71328
+ });
71329
+ }
70914
71330
  return diagnostics;
70915
71331
  } catch {
70916
71332
  return [];
@@ -70921,7 +71337,7 @@ async function resolvePhpstan(cwd) {
70921
71337
  return phpstan.getCommand(cwd);
70922
71338
  return resolveVendorToolCommand(cwd, "phpstan", ".bat");
70923
71339
  }
70924
- var phpstan, phpstanRunner, phpstan_default;
71340
+ var PHPSTAN_EXIT_CODES, phpstan, phpstanRunner, phpstan_default;
70925
71341
  var init_phpstan = __esm({
70926
71342
  "dist/clients/dispatch/runners/phpstan.js"() {
70927
71343
  "use strict";
@@ -70929,6 +71345,8 @@ var init_phpstan = __esm({
70929
71345
  init_tool_policy();
70930
71346
  init_priorities();
70931
71347
  init_runner_helpers();
71348
+ init_tool_failure();
71349
+ PHPSTAN_EXIT_CODES = { ran: [1] };
70932
71350
  phpstan = createAvailabilityChecker("phpstan", ".phar");
70933
71351
  phpstanRunner = {
70934
71352
  id: "phpstan",
@@ -70950,13 +71368,10 @@ var init_phpstan = __esm({
70950
71368
  return { status: "skipped", diagnostics: [], semantic: "none" };
70951
71369
  const absPath = path126.resolve(cwd, ctx.filePath);
70952
71370
  const result = await safeSpawnAsync(cmd, ["analyse", "--error-format=json", "--no-progress", absPath], { timeout: 3e4, cwd });
70953
- if (result.status === 2 || result.error) {
70954
- return { status: "skipped", diagnostics: [], semantic: "none" };
70955
- }
70956
- if (result.status === 0) {
70957
- return { status: "succeeded", diagnostics: [], semantic: "none" };
70958
- }
70959
- const diagnostics = parsePhpstanJson(result.stdout ?? "", ctx.filePath, cwd);
71371
+ const run = parseToolRun("phpstan", { result, exitCodes: PHPSTAN_EXIT_CODES }, (out) => parsePhpstanJson(out, ctx.filePath, cwd), { parseOutput: result.stdout ?? "" });
71372
+ if (run.skipped)
71373
+ return run.skipped;
71374
+ const diagnostics = run.diagnostics;
70960
71375
  if (diagnostics.length === 0) {
70961
71376
  return { status: "succeeded", diagnostics: [], semantic: "none" };
70962
71377
  }
@@ -72470,10 +72885,10 @@ var typos, TYPOS_EXIT_CODES, spellcheckRunner, spellcheck_default;
72470
72885
  var init_spellcheck = __esm({
72471
72886
  "dist/clients/dispatch/runners/spellcheck.js"() {
72472
72887
  "use strict";
72473
- init_tool_failure();
72474
72888
  init_safe_spawn();
72475
72889
  init_priorities();
72476
72890
  init_runner_helpers();
72891
+ init_tool_failure();
72477
72892
  typos = createAvailabilityChecker("typos", ".exe");
72478
72893
  TYPOS_EXIT_CODES = { ran: [2] };
72479
72894
  spellcheckRunner = {
@@ -72491,18 +72906,10 @@ var init_spellcheck = __esm({
72491
72906
  const result = await safeSpawnAsync(typos.getCommand(ctx.cwd || process.cwd()), args, {
72492
72907
  timeout: 15e3
72493
72908
  });
72494
- const skipped = skipUnlessToolRan("spellcheck", {
72495
- result,
72496
- exitCodes: TYPOS_EXIT_CODES
72497
- });
72498
- if (skipped)
72499
- return skipped;
72500
- const hasTypos = result.status === 2 || result.stdout?.trim();
72501
- if (!hasTypos) {
72502
- return { status: "succeeded", diagnostics: [], semantic: "none" };
72503
- }
72504
- const raw = result.stdout + result.stderr;
72505
- const diagnostics = parseTyposOutput(raw, ctx.filePath);
72909
+ const run = parseToolRun("spellcheck", { result, exitCodes: TYPOS_EXIT_CODES }, (out) => parseTyposOutput(out, ctx.filePath), { parseOutput: `${result.stdout ?? ""}${result.stderr ?? ""}` });
72910
+ if (run.skipped)
72911
+ return run.skipped;
72912
+ const diagnostics = run.diagnostics;
72506
72913
  if (diagnostics.length === 0) {
72507
72914
  return { status: "succeeded", diagnostics: [], semantic: "none" };
72508
72915
  }
@@ -72631,21 +73038,19 @@ var init_sqlfluff = __esm({
72631
73038
  }
72632
73039
  if (!cmd)
72633
73040
  return { status: "skipped", diagnostics: [], semantic: "none" };
72634
- const args = ["lint", "--format", "json", ctx.filePath];
73041
+ const args = ["lint", "--format", "json"];
72635
73042
  if (!hasConfig) {
72636
- args.splice(2, 0, "--dialect", "ansi");
73043
+ args.push("--dialect", "ansi");
72637
73044
  }
73045
+ args.push(ctx.filePath);
72638
73046
  const result = await safeSpawnAsync(cmd, args, {
72639
73047
  timeout: 2e4,
72640
73048
  cwd
72641
73049
  });
72642
- const skipped = skipUnlessToolRan("sqlfluff", {
72643
- result,
72644
- exitCodes: SQLFLUFF_EXIT_CODES
72645
- });
72646
- if (skipped)
72647
- return skipped;
72648
- const diagnostics = parseSqlfluffOutput(result.stdout ?? "", ctx.filePath);
73050
+ const run = parseToolRun("sqlfluff", { result, exitCodes: SQLFLUFF_EXIT_CODES }, (out) => parseSqlfluffOutput(out, ctx.filePath), { parseOutput: result.stdout ?? "" });
73051
+ if (run.skipped)
73052
+ return run.skipped;
73053
+ const diagnostics = run.diagnostics;
72649
73054
  if (diagnostics.length === 0) {
72650
73055
  return { status: "succeeded", diagnostics: [], semantic: "none" };
72651
73056
  }
@@ -72662,6 +73067,13 @@ var init_sqlfluff = __esm({
72662
73067
 
72663
73068
  // dist/clients/dispatch/runners/stylelint.js
72664
73069
  import * as path132 from "node:path";
73070
+ function stylelintReport(stdout, stderr) {
73071
+ for (const stream of [stdout, stderr]) {
73072
+ if (stream?.trimStart().startsWith("["))
73073
+ return stream;
73074
+ }
73075
+ return "";
73076
+ }
72665
73077
  function parseStylelintJson(raw, filePath) {
72666
73078
  try {
72667
73079
  const results = JSON.parse(raw);
@@ -72758,14 +73170,11 @@ var init_stylelint = __esm({
72758
73170
  if (!cmd)
72759
73171
  return { status: "skipped", diagnostics: [], semantic: "none" };
72760
73172
  const result = await safeSpawnAsync(cmd, ["--formatter", "json", ctx.filePath], { timeout: 2e4, cwd });
72761
- const skipped = skipUnlessToolRan("stylelint", {
72762
- result,
72763
- exitCodes: STYLELINT_EXIT_CODES
72764
- });
72765
- if (skipped)
72766
- return skipped;
72767
- const raw = result.stdout ?? "";
72768
- const diagnostics = parseStylelintJson(raw, ctx.filePath);
73173
+ const raw = stylelintReport(result.stdout, result.stderr);
73174
+ const run = parseToolRun("stylelint", { result, output: raw, exitCodes: STYLELINT_EXIT_CODES }, (out) => parseStylelintJson(out, ctx.filePath));
73175
+ if (run.skipped)
73176
+ return run.skipped;
73177
+ const diagnostics = run.diagnostics;
72769
73178
  if (diagnostics.length === 0) {
72770
73179
  return { status: "succeeded", diagnostics: [], semantic: "none" };
72771
73180
  }
@@ -72905,11 +73314,10 @@ var init_swiftlint = __esm({
72905
73314
  if (!cmd)
72906
73315
  return { status: "skipped", diagnostics: [], semantic: "none" };
72907
73316
  const result = await safeSpawnAsync(cmd, ["--reporter", "json", path133.resolve(cwd, ctx.filePath)], { cwd, timeout: 15e3 });
72908
- const skipped = skipUnlessToolRan("swiftlint", { result });
72909
- if (skipped)
72910
- return skipped;
72911
- const raw = result.stdout || "";
72912
- const diagnostics = parseSwiftLintOutput(raw, ctx.filePath);
73317
+ const run = parseToolRun("swiftlint", { result }, (out) => parseSwiftLintOutput(out, ctx.filePath));
73318
+ if (run.skipped)
73319
+ return run.skipped;
73320
+ const diagnostics = run.diagnostics;
72913
73321
  if (diagnostics.length === 0) {
72914
73322
  return { status: "succeeded", diagnostics: [], semantic: "none" };
72915
73323
  }
@@ -72928,26 +73336,47 @@ var init_swiftlint = __esm({
72928
73336
  // dist/clients/dispatch/runners/taplo.js
72929
73337
  import * as path134 from "node:path";
72930
73338
  function parseTaploOutput(raw, filePath) {
72931
- try {
72932
- const parsed = JSON.parse(raw);
72933
- const errors = parsed.errors ?? [];
72934
- return errors.map((err, idx) => ({
72935
- id: `taplo-${err.kind}-${err.range?.start.line ?? idx}`,
72936
- message: `[${err.kind}] ${err.message}`,
73339
+ const diagnostics = [];
73340
+ const lines = (raw ?? "").split(/\r?\n/);
73341
+ for (let i = 0; i < lines.length; i++) {
73342
+ const header = HEADER_PATTERN.exec(lines[i].trimEnd());
73343
+ if (!header)
73344
+ continue;
73345
+ const severityWord = header[1];
73346
+ const summary = header[2].trim();
73347
+ if (!summary)
73348
+ continue;
73349
+ let line = null;
73350
+ let column = 1;
73351
+ for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
73352
+ if (!lines[j].includes(LOCATION_LEAD))
73353
+ continue;
73354
+ const location = LOCATION_PATTERN.exec(lines[j].trimEnd());
73355
+ if (!location)
73356
+ continue;
73357
+ line = Number.parseInt(location[1], 10) || 1;
73358
+ column = Number.parseInt(location[2], 10) || 1;
73359
+ break;
73360
+ }
73361
+ if (line === null)
73362
+ continue;
73363
+ const severity = severityWord === "warning" ? "warning" : "error";
73364
+ diagnostics.push({
73365
+ id: `taplo-${severity}-${line}-${column}`,
73366
+ message: summary,
72937
73367
  filePath,
72938
- line: (err.range?.start.line ?? 0) + 1,
72939
- column: (err.range?.start.col ?? 0) + 1,
72940
- severity: "error",
72941
- semantic: "blocking",
73368
+ line,
73369
+ column,
73370
+ severity,
73371
+ semantic: severity === "error" ? "blocking" : "warning",
72942
73372
  tool: "taplo",
72943
- rule: err.kind,
73373
+ rule: `taplo/${severity}`,
72944
73374
  fixable: false
72945
- }));
72946
- } catch {
72947
- return [];
73375
+ });
72948
73376
  }
73377
+ return diagnostics;
72949
73378
  }
72950
- var taplo, taploRunner, taplo_default;
73379
+ var taplo, TAPLO_EXIT_CODES, HEADER_PATTERN, LOCATION_LEAD, LOCATION_PATTERN, taploRunner, taplo_default;
72951
73380
  var init_taplo = __esm({
72952
73381
  "dist/clients/dispatch/runners/taplo.js"() {
72953
73382
  "use strict";
@@ -72956,8 +73385,12 @@ var init_taplo = __esm({
72956
73385
  init_tool_policy();
72957
73386
  init_priorities();
72958
73387
  init_runner_helpers();
72959
- init_spawn_outcome();
73388
+ init_tool_failure();
72960
73389
  taplo = createAvailabilityChecker("taplo", ".exe");
73390
+ TAPLO_EXIT_CODES = { ran: [1] };
73391
+ HEADER_PATTERN = /^\s*(error|warning):(.*)$/;
73392
+ LOCATION_LEAD = "\u250C\u2500";
73393
+ LOCATION_PATTERN = /:(\d+):(\d+)$/;
72961
73394
  taploRunner = {
72962
73395
  id: "taplo",
72963
73396
  appliesTo: ["toml"],
@@ -72984,11 +73417,12 @@ var init_taplo = __esm({
72984
73417
  if (!cmd)
72985
73418
  return { status: "skipped", diagnostics: [], semantic: "none" };
72986
73419
  const absPath = path134.resolve(cwd, ctx.filePath);
72987
- const result = await safeSpawnAsync(cmd, ["check", "--output=json", absPath], { cwd, timeout: 15e3 });
72988
- if (spawnFailedWithNoOutput(result)) {
72989
- return { status: "skipped", diagnostics: [], semantic: "none" };
72990
- }
72991
- const diagnostics = parseTaploOutput(result.stdout || "", ctx.filePath);
73420
+ const result = await safeSpawnAsync(cmd, ["check", "--colors", "never", absPath], { cwd, timeout: 15e3 });
73421
+ const raw = result.stderr?.trim() ? result.stderr : result.stdout ?? "";
73422
+ const run = parseToolRun("taplo", { result, output: raw, exitCodes: TAPLO_EXIT_CODES }, (out) => parseTaploOutput(out, ctx.filePath), { skipWhenParsedNothing: true });
73423
+ if (run.skipped)
73424
+ return run.skipped;
73425
+ const diagnostics = run.diagnostics;
72992
73426
  if (diagnostics.length === 0) {
72993
73427
  return { status: "succeeded", diagnostics: [], semantic: "none" };
72994
73428
  }
@@ -73150,9 +73584,9 @@ var init_tflint = __esm({
73150
73584
  init_safe_spawn();
73151
73585
  init_tool_policy();
73152
73586
  init_workspace_topology();
73153
- init_runner_helpers();
73154
- init_spawn_outcome();
73155
73587
  init_priorities();
73588
+ init_runner_helpers();
73589
+ init_tool_failure();
73156
73590
  tflint = createAvailabilityChecker("tflint", ".exe");
73157
73591
  TFLINT_CONFIG = ".tflint.hcl";
73158
73592
  tflintRunner = {
@@ -73191,10 +73625,10 @@ var init_tflint = __esm({
73191
73625
  cwd: fileDir,
73192
73626
  timeout: 3e4
73193
73627
  });
73194
- if (spawnFailedWithNoOutput(result)) {
73195
- return { status: "skipped", diagnostics: [], semantic: "none" };
73196
- }
73197
- const diagnostics = parseTflintOutput(result.stdout || "", ctx.filePath);
73628
+ const run = parseToolRun("tflint", { result }, (out) => parseTflintOutput(out, ctx.filePath));
73629
+ if (run.skipped)
73630
+ return run.skipped;
73631
+ const diagnostics = run.diagnostics;
73198
73632
  if (diagnostics.length === 0) {
73199
73633
  return { status: "succeeded", diagnostics: [], semantic: "none" };
73200
73634
  }
@@ -73404,13 +73838,12 @@ function parseValeOutput(raw, filePath) {
73404
73838
  return diagnostics;
73405
73839
  try {
73406
73840
  const parsed = JSON.parse(raw);
73407
- const files2 = parsed?.Data?.Files;
73408
- if (!files2)
73841
+ if (!parsed || typeof parsed !== "object")
73409
73842
  return diagnostics;
73410
- for (const file of files2) {
73411
- if (!file.Alerts)
73843
+ for (const alerts of Object.values(parsed)) {
73844
+ if (!Array.isArray(alerts))
73412
73845
  continue;
73413
- for (const alert of file.Alerts) {
73846
+ for (const alert of alerts) {
73414
73847
  if (!alert.Message)
73415
73848
  continue;
73416
73849
  const severityMap = {
@@ -73425,7 +73858,7 @@ function parseValeOutput(raw, filePath) {
73425
73858
  message: `[${alert.Check ?? "vale"}] ${alert.Message}`,
73426
73859
  filePath,
73427
73860
  line: alert.Line ?? 1,
73428
- column: alert.Column ?? 1,
73861
+ column: alert.Span?.[0] ?? 1,
73429
73862
  severity,
73430
73863
  semantic: severity === "error" ? "blocking" : "warning",
73431
73864
  tool: "vale",
@@ -73468,11 +73901,10 @@ var init_vale = __esm({
73468
73901
  if (!cmd)
73469
73902
  return { status: "skipped", diagnostics: [], semantic: "none" };
73470
73903
  const result = await safeSpawnAsync(cmd, ["--output", "JSON", ctx.filePath], { cwd, timeout: 15e3 });
73471
- const skipped = skipUnlessToolRan("vale", { result });
73472
- if (skipped)
73473
- return skipped;
73474
- const raw = result.stdout || "";
73475
- const diagnostics = parseValeOutput(raw, ctx.filePath);
73904
+ const run = parseToolRun("vale", { result }, (raw) => parseValeOutput(raw, ctx.filePath));
73905
+ if (run.skipped)
73906
+ return run.skipped;
73907
+ const diagnostics = run.diagnostics;
73476
73908
  if (diagnostics.length === 0) {
73477
73909
  return { status: "succeeded", diagnostics: [], semantic: "none" };
73478
73910
  }
@@ -74194,13 +74626,10 @@ var init_yamllint = __esm({
74194
74626
  timeout: 15e3
74195
74627
  });
74196
74628
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
74197
- const skipped = skipUnlessToolRan("yamllint", {
74198
- result,
74199
- output: result.stdout
74200
- });
74201
- if (skipped)
74202
- return skipped;
74203
- const diagnostics = parseYamllintParsable(raw, ctx.filePath);
74629
+ const run = parseToolRun("yamllint", { result, output: result.stdout }, (out) => parseYamllintParsable(out, ctx.filePath), { parseOutput: raw });
74630
+ if (run.skipped)
74631
+ return run.skipped;
74632
+ const diagnostics = run.diagnostics;
74204
74633
  if (diagnostics.length === 0) {
74205
74634
  return { status: "succeeded", diagnostics: [], semantic: "none" };
74206
74635
  }
@@ -77975,7 +78404,7 @@ function makeSourceCountVisitor(state2, ignoreMatcher, limit, maxEntries) {
77975
78404
  }
77976
78405
  return recurse ? "recurse" : "skip";
77977
78406
  }
77978
- if (entry.isFile() && !ignoreMatcher.isIgnored(fullPath, false) && SOURCE_FILE_PATTERN.test(entry.name)) {
78407
+ if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name) && !ignoreMatcher.isIgnored(fullPath, false)) {
77979
78408
  state2.count += 1;
77980
78409
  if (state2.count > limit)
77981
78410
  return "stop";
@@ -83501,10 +83930,10 @@ function tokenizeShellCommand(command) {
83501
83930
  word = "";
83502
83931
  atTokenStart = true;
83503
83932
  };
83504
- const flushSegment = () => {
83933
+ const flushSegment = (terminator) => {
83505
83934
  flushWord();
83506
83935
  if (tokens.length > 0 || unsupported)
83507
- segments.push({ tokens, unsupported });
83936
+ segments.push({ tokens, unsupported, terminator });
83508
83937
  tokens = [];
83509
83938
  unsupported = false;
83510
83939
  atTokenStart = true;
@@ -83569,13 +83998,17 @@ function tokenizeShellCommand(command) {
83569
83998
  }
83570
83999
  if (ch === ";" || ch === "\n" || ch === "|" || ch === "&") {
83571
84000
  flushWord();
84001
+ let terminator;
83572
84002
  if (ch === "|" && next === "|")
83573
84003
  i++;
83574
84004
  else if (ch === "&" && next === "&")
83575
84005
  i++;
83576
- else if (ch === "|" || ch === "&")
84006
+ else if (ch === "|") {
84007
+ unsupported = true;
84008
+ terminator = "pipe";
84009
+ } else if (ch === "&")
83577
84010
  unsupported = true;
83578
- flushSegment();
84011
+ flushSegment(terminator);
83579
84012
  continue;
83580
84013
  }
83581
84014
  if (ch === "<" || ch === ">") {
@@ -83926,11 +84359,44 @@ function parseGrepLineWithoutFile(line, file) {
83926
84359
  return void 0;
83927
84360
  return { file, startLine: lineNumber, endLine: lineNumber };
83928
84361
  }
84362
+ var LINE_DROPPING_TAIL_VERBS = /* @__PURE__ */ new Set(["head", "tail", "uniq"]);
84363
+ function isSedQuitCommand(args) {
84364
+ for (const arg of args) {
84365
+ const token = stripQuotes(arg);
84366
+ if (token.startsWith("-"))
84367
+ continue;
84368
+ if (/(^|[;\n])\s*(\d+|\$)?\s*q\b/.test(token))
84369
+ return true;
84370
+ }
84371
+ return false;
84372
+ }
84373
+ function isLineDroppingTailCommand(tokens) {
84374
+ const verb = path69.basename(stripQuotes(tokens[0] ?? ""));
84375
+ if (LINE_DROPPING_TAIL_VERBS.has(verb))
84376
+ return true;
84377
+ if (verb === "sed")
84378
+ return isSedQuitCommand(tokens.slice(1));
84379
+ return false;
84380
+ }
84381
+ function grepPipesIntoTruncatingTail(segments, index) {
84382
+ if (segments[index]?.terminator !== "pipe")
84383
+ return false;
84384
+ for (let j = index + 1; j < segments.length; j++) {
84385
+ if (isLineDroppingTailCommand(segments[j].tokens))
84386
+ return true;
84387
+ if (segments[j].terminator !== "pipe")
84388
+ return false;
84389
+ }
84390
+ return false;
84391
+ }
83929
84392
  function collectGrepCommandFiles(command, cwd) {
83930
84393
  const files2 = /* @__PURE__ */ new Set();
83931
84394
  let hasLineNumberGrep = false;
83932
84395
  let context;
83933
- for (const tokens of commandSegments(command)) {
84396
+ let truncated = false;
84397
+ const segments = tokenizeShellCommand(command);
84398
+ for (let i = 0; i < segments.length; i++) {
84399
+ const tokens = segments[i].tokens;
83934
84400
  const verb = path69.basename(stripQuotes(tokens[0] ?? ""));
83935
84401
  if (verb !== "grep" && verb !== "egrep" && verb !== "fgrep")
83936
84402
  continue;
@@ -83945,11 +84411,13 @@ function collectGrepCommandFiles(command, cwd) {
83945
84411
  } : segmentContext;
83946
84412
  for (const file of extractGrepSearchFiles(args, cwd))
83947
84413
  files2.add(file);
84414
+ if (grepPipesIntoTruncatingTail(segments, i))
84415
+ truncated = true;
83948
84416
  }
83949
84417
  return {
83950
84418
  hasLineNumberGrep,
83951
84419
  files: files2,
83952
- context: context ?? { before: 0, after: 0 }
84420
+ context: truncated ? { before: 0, after: 0 } : context ?? { before: 0, after: 0 }
83953
84421
  };
83954
84422
  }
83955
84423
  function dedupePushSearchRead(out, seen, loc) {
@@ -89534,6 +90002,47 @@ var RuntimeCoordinator = class {
89534
90002
  });
89535
90003
  return true;
89536
90004
  }
90005
+ /**
90006
+ * #1944: retire a past-EOF demotion after its ONE degraded delivery.
90007
+ *
90008
+ * The past-EOF gate demotes and re-arms, but nothing ever retired the
90009
+ * record, so a blocker whose file shrank past the cited lines re-served on
90010
+ * every turn end for the rest of the session — measured live at 80+
90011
+ * minutes on session 01a0234c. The re-serve is unbounded, not the six
90012
+ * turns the first evidence window suggested.
90013
+ *
90014
+ * It is unbounded because the record cannot resolve itself. The two
90015
+ * clearing events (`clearInlineBlockers` on a later dispatch of the same
90016
+ * path, `retireInlineBlockerOnConfirmedClean` on a fresh clean verdict)
90017
+ * both need the file to be looked at again, and the agent has no reason to
90018
+ * look: the coordinates it was handed do not exist. "Re-run to confirm" is
90019
+ * an instruction this record makes impossible to follow.
90020
+ *
90021
+ * So the delivery surface retires it after serving it once, degraded. That
90022
+ * is a DROP, which #1419's demote-not-drop rule normally forbids; the
90023
+ * exception is narrow and stated here. The finding was already delivered
90024
+ * this turn with its dead coordinates annotated, and what remains is a
90025
+ * message pinned to a line the file does not have. `deadLines` is required
90026
+ * and must be non-empty, so a record stale for any OTHER reason, or
90027
+ * past-EOF with no identified dead line, still stands.
90028
+ *
90029
+ * @returns true when an entry was retired — the caller records it, so the
90030
+ * suppression is never silent (#1432 Gap 1).
90031
+ */
90032
+ retireDemotedPastEofBlocker(filePath, deadLines) {
90033
+ if (deadLines.length === 0)
90034
+ return false;
90035
+ const key2 = path152.resolve(filePath);
90036
+ const existing = this._pendingInlineBlockers.get(key2);
90037
+ if (!existing)
90038
+ return false;
90039
+ if (!existing.stale)
90040
+ return false;
90041
+ if ((existing.staleReason ?? "past-eof") !== "past-eof")
90042
+ return false;
90043
+ this._pendingInlineBlockers.delete(key2);
90044
+ return true;
90045
+ }
89537
90046
  /**
89538
90047
  * Retire a file's inline blocker because a FRESH, content-bound diagnostic
89539
90048
  * verdict proved it gone (#1561).
@@ -89926,6 +90435,7 @@ init_language_policy();
89926
90435
  init_language_profile();
89927
90436
  init_latency_logger();
89928
90437
  init_log_cleanup();
90438
+ init_cascade_tier();
89929
90439
  init_config();
89930
90440
  init_workspace_diagnostics_session();
89931
90441
  init_server();
@@ -91079,6 +91589,7 @@ async function handleSessionStart(deps) {
91079
91589
  resetSmellsSessionState();
91080
91590
  resetWorkspaceDiagnosticsCacheSession(sessionStartMs);
91081
91591
  knipClient.resetSessionState?.();
91592
+ resetCascadeTierSessionState();
91082
91593
  runtime2.resetForSession(sessionStartMs);
91083
91594
  logLatency({
91084
91595
  type: "phase",
@@ -91218,6 +91729,32 @@ async function handleSessionStart(deps) {
91218
91729
  log2(`Active tools: ${quickTools.join(", ")}`);
91219
91730
  dbg2(`session_start tools: ${quickTools.join(", ") || "deferred (quick mode)"}`);
91220
91731
  dbg2("session_start: quick mode active - skipping slow tool probes, language profiling, preinstall, scans, and error debt baseline");
91732
+ logLatency({
91733
+ type: "phase",
91734
+ phase: "session_start_skipped_steps",
91735
+ filePath: cwd,
91736
+ durationMs: 0,
91737
+ metadata: {
91738
+ mode: startupMode,
91739
+ reason: deps.sessionReason,
91740
+ // #1911 review F3: this `if (quickMode)` branch is an early return —
91741
+ // the whole quick path — so there is no enumerable list of "steps
91742
+ // run vs. skipped" this array could be derived from mechanically. It
91743
+ // is DESCRIPTIVE documentation of what the early return skips,
91744
+ // matching the `dbg()` line above word for word; keep both in sync by
91745
+ // hand if either changes. It also does NOT narrow under
91746
+ // `getFlag("no-lsp")` — quick mode skips the same five steps either
91747
+ // way; the LSP flag instead affects `quickTools` above, a separate
91748
+ // record.
91749
+ steps: [
91750
+ "slow_tool_probes",
91751
+ "language_profiling",
91752
+ "tool_preinstall",
91753
+ "startup_scans",
91754
+ "error_debt_baseline"
91755
+ ]
91756
+ }
91757
+ });
91221
91758
  const totalDurationMs2 = Date.now() - sessionStartMs;
91222
91759
  dbg2(`session_start total: ${totalDurationMs2}ms (interactive path)`);
91223
91760
  logLatency({
@@ -93655,6 +94192,61 @@ ${partial.postEditOutput}`;
93655
94192
  }
93656
94193
  }
93657
94194
 
94195
+ // dist/clients/session-event-guard.js
94196
+ init_bounded_telemetry();
94197
+ init_session_lifecycle();
94198
+ function isStaleExtensionCtxError(err) {
94199
+ return err instanceof Error && err.message.includes("stale after session replacement or reload");
94200
+ }
94201
+ function recordStaleSkip(eventName, detectedAt) {
94202
+ emitBounded("session_event_stale_ctx_skip", eventName, {
94203
+ durationMs: 0,
94204
+ metadata: { event: eventName, detectedAt }
94205
+ }, {
94206
+ ledgerKind: "extension-ctx-stale",
94207
+ risingEdgePer: "identity",
94208
+ reason: `${eventName} skipped: extension ctx is stale (${detectedAt})`
94209
+ });
94210
+ }
94211
+ function isThenable(value) {
94212
+ return typeof value?.then === "function";
94213
+ }
94214
+ function guardSessionEvent(eventName, handler, onStaleResult, options) {
94215
+ const skip = (event, detectedAt) => {
94216
+ recordStaleSkip(eventName, detectedAt);
94217
+ try {
94218
+ options.dbg?.(`${eventName} skipped: extension ctx is stale after session replacement or reload`);
94219
+ } catch {
94220
+ }
94221
+ return onStaleResult(event);
94222
+ };
94223
+ return (event, ctx) => {
94224
+ if (probeCtxActive(ctx) === false)
94225
+ return skip(event, "pre-dispatch");
94226
+ try {
94227
+ const result = handler(event, ctx);
94228
+ if (isThenable(result)) {
94229
+ return Promise.resolve(result).catch((err) => {
94230
+ if (isStaleExtensionCtxError(err))
94231
+ return skip(event, "mid-handler");
94232
+ throw err;
94233
+ });
94234
+ }
94235
+ return result;
94236
+ } catch (err) {
94237
+ if (isStaleExtensionCtxError(err))
94238
+ return skip(event, "mid-handler");
94239
+ throw err;
94240
+ }
94241
+ };
94242
+ }
94243
+ function wrapSessionEventHandler(eventName, handler, options = {}) {
94244
+ return guardSessionEvent(eventName, handler, () => void 0, options);
94245
+ }
94246
+ function wrapSessionEventHandlerWithResult(eventName, handler, options) {
94247
+ return guardSessionEvent(eventName, handler, options.onStaleResult, options);
94248
+ }
94249
+
93658
94250
  // dist/index.js
93659
94251
  init_session_lifecycle();
93660
94252
 
@@ -94110,7 +94702,7 @@ function sweepInlineBlockerPastEof(runtime2, cwd, options) {
94110
94702
  total: 0,
94111
94703
  checked: 0,
94112
94704
  demoted: 0,
94113
- healed: 0
94705
+ deadLinesByPath: /* @__PURE__ */ new Map()
94114
94706
  };
94115
94707
  const resync = options?.resync ?? resyncDocumentOnPastEof;
94116
94708
  let entries;
@@ -94141,20 +94733,80 @@ function sweepInlineBlockerPastEof(runtime2, cwd, options) {
94141
94733
  resync
94142
94734
  });
94143
94735
  const isPastEof = diagnostics.some((d) => d.stale);
94736
+ if (isPastEof) {
94737
+ counts.deadLinesByPath.set(entry.filePath, diagnostics.filter((d) => d.stale).map((d) => d.line).filter((line) => typeof line === "number"));
94738
+ }
94144
94739
  const transitioned = runtime2.setInlineBlockerPastEofStale(entry.filePath, isPastEof);
94145
94740
  if (!transitioned)
94146
94741
  continue;
94147
- if (isPastEof) {
94742
+ if (isPastEof)
94148
94743
  counts.demoted += 1;
94149
- } else {
94150
- counts.healed += 1;
94151
- }
94152
94744
  } catch {
94153
94745
  }
94154
94746
  }
94155
94747
  return counts;
94156
94748
  }
94157
94749
 
94750
+ // dist/clients/runtime-turn.js
94751
+ init_degradation_ledger();
94752
+
94753
+ // dist/clients/demoted-finding-render.js
94754
+ var DEAD_LINE_ANNOTATION = "(line no longer exists)";
94755
+ var AUTHORITY_REWRITES = [
94756
+ [
94757
+ /[^\S\n]*(?:🔴|⛔|❌)?[^\S\n]*STOP[^\S\n]*[—–-][^\S\n]*(\d+)[^\S\n]+issue\(s\)[^\S\n]+must be fixed:?/g,
94758
+ "$1 issue(s) were flagged before this file changed; the coordinates below may no longer exist:"
94759
+ ],
94760
+ [/(?:🔴|⛔|❌)[^\S\n]*STOP\b[^\S\n]*[—–-]?/g, "Previously flagged \u2014"],
94761
+ [/\bSTOP\b/g, "Previously flagged"],
94762
+ [/\bmust be fixed\b/g, "were flagged"]
94763
+ ];
94764
+ var CITED_LINE_RE = /^([^\S\n]*)L(\d+)\b/;
94765
+ function splitBannerRegion(lines) {
94766
+ const firstCitedRow = lines.findIndex((line) => CITED_LINE_RE.test(line));
94767
+ if (firstCitedRow !== -1)
94768
+ return { bannerEnd: firstCitedRow };
94769
+ const firstNonBlank = lines.findIndex((line) => line.trim() !== "");
94770
+ return { bannerEnd: firstNonBlank === -1 ? 0 : firstNonBlank + 1 };
94771
+ }
94772
+ function degradeDemotedFindingBody(summary, options = {}) {
94773
+ const deadLines = new Set(options.deadLines ?? []);
94774
+ let authorityMarkersRemoved = 0;
94775
+ const sourceLines = summary.split("\n");
94776
+ const { bannerEnd } = splitBannerRegion(sourceLines);
94777
+ const degradedLines = sourceLines.map((line, index) => {
94778
+ if (index >= bannerEnd)
94779
+ return line;
94780
+ let rewritten = line;
94781
+ for (const [pattern, replacement] of AUTHORITY_REWRITES) {
94782
+ rewritten = rewritten.replace(pattern, (...args) => {
94783
+ authorityMarkersRemoved += 1;
94784
+ return replacement.replace(/\$(\d)/g, (_, digit) => String(args[Number(digit)] ?? ""));
94785
+ });
94786
+ }
94787
+ return rewritten;
94788
+ });
94789
+ let body = degradedLines.join("\n");
94790
+ const deadLinesAnnotated = [];
94791
+ if (deadLines.size > 0) {
94792
+ body = degradedLines.map((line) => {
94793
+ const hit = CITED_LINE_RE.exec(line);
94794
+ if (!hit)
94795
+ return line;
94796
+ const cited = Number.parseInt(hit[2] ?? "", 10);
94797
+ if (!Number.isFinite(cited) || !deadLines.has(cited))
94798
+ return line;
94799
+ deadLinesAnnotated.push(cited);
94800
+ return `${hit[1] ?? ""}L${cited} ${DEAD_LINE_ANNOTATION}${line.slice(hit[0].length)}`;
94801
+ }).join("\n");
94802
+ }
94803
+ return { body, authorityMarkersRemoved, deadLinesAnnotated };
94804
+ }
94805
+ function formatRetirementNote(deadLines) {
94806
+ const cited = deadLines.length > 0 ? ` (cited ${deadLines.length === 1 ? "line" : "lines"} ${deadLines.join(", ")})` : "";
94807
+ return `Retired after this delivery${cited}: the file no longer has these lines, so this finding cannot be re-confirmed and will not be served again.`;
94808
+ }
94809
+
94158
94810
  // dist/clients/runtime-turn.js
94159
94811
  init_stale_marker();
94160
94812
  init_widget_state();
@@ -94331,8 +94983,10 @@ async function handleTurnEnd(deps) {
94331
94983
  metadata: {
94332
94984
  total: blockerPastEof.total,
94333
94985
  checked: blockerPastEof.checked,
94334
- demoted: blockerPastEof.demoted,
94335
- healed: blockerPastEof.healed
94986
+ demoted: blockerPastEof.demoted
94987
+ // #1944 review F3: `healed` is gone. Retirement makes the falling edge
94988
+ // unreachable on this store, so the field could only ever log zero —
94989
+ // see `BlockerPastEofCounts`.
94336
94990
  }
94337
94991
  });
94338
94992
  const blockerFreshnessStart = Date.now();
@@ -94358,11 +95012,24 @@ async function handleTurnEnd(deps) {
94358
95012
  }
94359
95013
  });
94360
95014
  const unresolvedBlockers = runtime2.getInlineBlockersSnapshot();
95015
+ let demotedFindingsRetired = 0;
94361
95016
  for (const { filePath: bPath, summary, stale } of unresolvedBlockers) {
94362
95017
  const displayPath = toRunnerDisplayPath(cwd, bPath);
94363
95018
  if (stale) {
95019
+ const deadLines = blockerPastEof.deadLinesByPath.get(bPath) ?? [];
95020
+ const degraded = degradeDemotedFindingBody(summary, { deadLines });
95021
+ const retired = runtime2.retireDemotedPastEofBlocker(bPath, deadLines);
95022
+ if (retired) {
95023
+ demotedFindingsRetired += 1;
95024
+ incrementDegradationCount({
95025
+ kind: "demoted-finding-retired",
95026
+ subject: `inline-blocker:${displayPath}`,
95027
+ reason: `file shrank past cited line(s) ${deadLines.join(", ")}; retired after one degraded delivery`
95028
+ });
95029
+ }
94364
95030
  advisoryParts.push(`${STALE_LINE_MARKER} ${displayPath}:
94365
- ${summary}`);
95031
+ ${degraded.body}` + (retired ? `
95032
+ ${formatRetirementNote(deadLines)}` : ""));
94366
95033
  } else {
94367
95034
  blockerParts.push(`Unresolved from this turn \u2014 ${displayPath}:
94368
95035
  ${summary}`);
@@ -95408,6 +96075,9 @@ ${p}`);
95408
96075
  runtime2.fixedThisTurn.clear();
95409
96076
  runtime2.clearActionableWarnings();
95410
96077
  runtime2.clearCodeQualityWarnings();
96078
+ if (demotedFindingsRetired > 0) {
96079
+ cacheManager.clearCache("turn-end-findings-last", cwd);
96080
+ }
95411
96081
  logLatency({
95412
96082
  type: "tool_result",
95413
96083
  toolName: "turn_end",
@@ -95421,7 +96091,12 @@ ${p}`);
95421
96091
  fileCount: files2.length,
95422
96092
  blockerSections: blockerParts.length,
95423
96093
  staleSecretSections: staleSecretParts.length,
95424
- advisorySections: advisoryParts.length
96094
+ advisorySections: advisoryParts.length,
96095
+ // #1944 AC3: an empty advisory section on its own cannot say whether
96096
+ // the turn had nothing to report or dropped something. This counter
96097
+ // answers that from latency.log even when the payload is empty, and
96098
+ // the payload itself carries the retirement note when it is not.
96099
+ demotedFindingsRetired
95425
96100
  }
95426
96101
  });
95427
96102
  resetFormatService2();
@@ -96651,17 +97326,15 @@ async function scanFileMajorRules(cwd, files2, signal) {
96651
97326
  // subtract its share so this metric stays comparable to the historical
96652
97327
  // phase-major scans.
96653
97328
  durationMs: Date.now() - startedAt - astGrepDurationMs,
96654
- stats
96655
- });
96656
- });
96657
- await client.withParseCacheMeasurement(async () => {
96658
- }, (stats) => {
96659
- logTreeSitterCacheStats({
96660
- scope: "project_diagnostics_ast_grep_scan",
96661
- filePath: cwd,
96662
- fileCount: astGrepFilesScanned,
96663
- durationMs: astGrepDurationMs,
96664
- stats
97329
+ stats,
97330
+ // #1935 review: ast-grep's own cost is often a scan's most expensive
97331
+ // phase (production evidence: 13168ms over 86 files, the single
97332
+ // biggest contributor). Its duration is subtracted above so it stays
97333
+ // comparable to the historical phase-major scans; carry it here too
97334
+ // so it stays visible SOMEWHERE instead of disappearing when the
97335
+ // vacuous `project_diagnostics_ast_grep_scan` cache_stats record
97336
+ // (below) was removed.
97337
+ astGrep: { durationMs: astGrepDurationMs, fileCount: astGrepFilesScanned }
96665
97338
  });
96666
97339
  });
96667
97340
  return { treeSitter, factRules, astGrep, filesScanned, wasmAborted };
@@ -97999,10 +98672,14 @@ async function moduleReport(file, cwd, options) {
97999
98672
  let graph;
98000
98673
  let graphFileCap;
98001
98674
  let callGraphIdentity;
98675
+ let revisionDriftNote;
98002
98676
  try {
98003
- const { getCachedReviewGraph: getCachedReviewGraph2, getReviewGraphCacheIdentity: getReviewGraphCacheIdentity2, getReviewGraphSizeSkipVerdict: getReviewGraphSizeSkipVerdict2 } = await Promise.resolve().then(() => (init_builder(), builder_exports));
98677
+ const { getCachedReviewGraph: getCachedReviewGraph2, getReviewGraphCacheIdentity: getReviewGraphCacheIdentity2, getReviewGraphSizeSkipVerdict: getReviewGraphSizeSkipVerdict2, getReviewGraphRevisionDrift: getReviewGraphRevisionDrift2, formatReviewGraphRevisionDriftNote: formatReviewGraphRevisionDriftNote2 } = await Promise.resolve().then(() => (init_builder(), builder_exports));
98004
98678
  graph = getCachedReviewGraph2(cwd);
98005
98679
  graphFileCap = getReviewGraphSizeSkipVerdict2(cwd)?.maxFileCount;
98680
+ const drift = graph ? getReviewGraphRevisionDrift2(cwd) : void 0;
98681
+ if (drift)
98682
+ revisionDriftNote = formatReviewGraphRevisionDriftNote2(drift);
98006
98683
  callGraphIdentity = graph ? (() => {
98007
98684
  const identity = getReviewGraphCacheIdentity2(cwd, graph);
98008
98685
  return identity ? {
@@ -98021,6 +98698,8 @@ async function moduleReport(file, cwd, options) {
98021
98698
  const api = topLevel.filter((entry) => entry.exported);
98022
98699
  const internal = topLevel.filter((entry) => !entry.exported);
98023
98700
  const warnings = [...extractionWarnings ?? []];
98701
+ if (revisionDriftNote)
98702
+ warnings.push(revisionDriftNote);
98024
98703
  if (callbackError) {
98025
98704
  warnings.push(`Failed to extract callbacks: ${callbackError}`);
98026
98705
  logLatency({
@@ -98639,6 +99318,7 @@ async function readEnclosing(file, line, cwd, options) {
98639
99318
  // dist/clients/project-report.js
98640
99319
  init_path_utils();
98641
99320
  init_project_snapshot();
99321
+ init_revision_drift();
98642
99322
  import * as path179 from "node:path";
98643
99323
  var DEFAULT_LIMIT = 10;
98644
99324
  var STALE_THRESHOLD_MS = 15 * 6e4;
@@ -98691,7 +99371,7 @@ function buildFileDegrees(graph) {
98691
99371
  }
98692
99372
  return { fanIn, fanOut };
98693
99373
  }
98694
- function computeTrust(graph, cwd) {
99374
+ function computeTrust(graph, cwd, drift) {
98695
99375
  const filesCovered = graph.fileNodes.size;
98696
99376
  const snapshot = loadProjectSnapshotWithoutWordIndex(cwd);
98697
99377
  const snapshotFileCount = snapshot ? Object.keys(snapshot.files).length : 0;
@@ -98728,6 +99408,8 @@ function computeTrust(graph, cwd) {
98728
99408
  const stale = Number.isFinite(ageMs) && ageMs > STALE_THRESHOLD_MS;
98729
99409
  const lowCoverage = coverage < LOW_COVERAGE_THRESHOLD || graph.persistCoverage?.partial === true;
98730
99410
  const notes = [];
99411
+ if (drift)
99412
+ notes.push(formatReviewGraphRevisionDriftNote(drift));
98731
99413
  if (stale) {
98732
99414
  const ageMin = Math.round(ageMs / 6e4);
98733
99415
  notes.push(`Graph is stale: built ${ageMin}m ago. Sections below may miss recent edits \u2014 run pilens_rebuild or re-analyze to refresh.`);
@@ -98759,6 +99441,7 @@ function computeTrust(graph, cwd) {
98759
99441
  sampleSize
98760
99442
  },
98761
99443
  stale,
99444
+ ...drift ? { revisionDrift: drift } : {},
98762
99445
  lowCoverage,
98763
99446
  persistCoverage: graph.persistCoverage?.partial ? graph.persistCoverage : void 0,
98764
99447
  notes
@@ -99016,28 +99699,22 @@ function computeDeadWeight(graph, degrees, entryPointFiles, cwd, limit) {
99016
99699
  };
99017
99700
  }
99018
99701
  var inFlightGraphBuilds = /* @__PURE__ */ new Set();
99019
- function triggerBackgroundGraphBuild(cwd) {
99702
+ function triggerBackgroundGraphBuild(cwd, deps) {
99020
99703
  const key2 = normalizeMapKey(path179.resolve(cwd));
99021
99704
  if (inFlightGraphBuilds.has(key2))
99022
- return false;
99705
+ return "already_running";
99706
+ const deduped = deps.isGraphBuildInFlight(key2, []);
99023
99707
  inFlightGraphBuilds.add(key2);
99024
- void (async () => {
99025
- try {
99026
- const { buildOrUpdateGraph: buildOrUpdateGraph3 } = await Promise.resolve().then(() => (init_builder(), builder_exports));
99027
- const { FactStore: FactStore2 } = await Promise.resolve().then(() => (init_fact_store(), fact_store_exports));
99028
- await buildOrUpdateGraph3(key2, [], new FactStore2());
99029
- } catch {
99030
- } finally {
99031
- inFlightGraphBuilds.delete(key2);
99032
- }
99033
- })();
99034
- return true;
99708
+ const build2 = deps.buildOrUpdateGraph(key2, [], new deps.FactStore()).catch(() => {
99709
+ });
99710
+ void build2.finally(() => inFlightGraphBuilds.delete(key2));
99711
+ return deduped ? "already_running" : "started";
99035
99712
  }
99036
99713
  async function projectReport(cwd, options) {
99037
99714
  const limit = clampLimit(options?.limit);
99038
99715
  const focusTerms = normalizeFocus2(options?.focus);
99039
99716
  const view = options?.view;
99040
- const { getCachedReviewGraph: getCachedReviewGraph2, getReviewGraphSizeSkipVerdict: getReviewGraphSizeSkipVerdict2, getLastReviewGraphBuildAttempt: getLastReviewGraphBuildAttempt2 } = await Promise.resolve().then(() => (init_builder(), builder_exports));
99717
+ const { getCachedReviewGraph: getCachedReviewGraph2, getReviewGraphSizeSkipVerdict: getReviewGraphSizeSkipVerdict2, getLastReviewGraphBuildAttempt: getLastReviewGraphBuildAttempt2, buildOrUpdateGraph: buildOrUpdateGraph3, isGraphBuildInFlight: isGraphBuildInFlight2, getReviewGraphRevisionDrift: getReviewGraphRevisionDrift2 } = await Promise.resolve().then(() => (init_builder(), builder_exports));
99041
99718
  let graph;
99042
99719
  try {
99043
99720
  graph = getCachedReviewGraph2(cwd);
@@ -99061,17 +99738,25 @@ async function projectReport(cwd, options) {
99061
99738
  };
99062
99739
  }
99063
99740
  const previousAttempt = getLastReviewGraphBuildAttempt2(cwd);
99064
- const kickedOff = triggerBackgroundGraphBuild(cwd);
99065
- const lastBuildAttempt2 = previousAttempt ?? getLastReviewGraphBuildAttempt2(cwd);
99741
+ const { FactStore: FactStore2 } = await Promise.resolve().then(() => (init_fact_store(), fact_store_exports));
99742
+ const trigger = triggerBackgroundGraphBuild(cwd, {
99743
+ buildOrUpdateGraph: buildOrUpdateGraph3,
99744
+ isGraphBuildInFlight: isGraphBuildInFlight2,
99745
+ FactStore: FactStore2
99746
+ });
99747
+ const started = trigger === "started";
99748
+ const lastBuildAttempt2 = started ? getLastReviewGraphBuildAttempt2(cwd) ?? previousAttempt : previousAttempt;
99749
+ const terminal = previousAttempt?.outcome === "failed" || previousAttempt?.outcome === "skipped";
99750
+ const cause = terminal ? `Review graph unavailable: ${previousAttempt?.reason ?? previousAttempt?.outcome}. ` : "";
99066
99751
  return {
99067
99752
  available: false,
99068
- hint: lastBuildAttempt2?.outcome === "failed" || lastBuildAttempt2?.outcome === "skipped" ? `Review graph unavailable: ${lastBuildAttempt2.reason ?? lastBuildAttempt2.outcome}. A retry was ${kickedOff ? "started" : "not started"}.` : kickedOff ? "No review graph cached for this workspace yet \u2014 a build was kicked off in the background; retry this call shortly." : "No review graph cached for this workspace yet \u2014 the background build is still running; retry this call shortly.",
99753
+ hint: started ? terminal ? `${cause}A retry was started.` : "No review graph cached for this workspace yet \u2014 a build was kicked off in the background; retry this call shortly." : `${cause}A build is already running for this workspace; retry this call shortly.`,
99069
99754
  ...lastBuildAttempt2 ? { lastBuildAttempt: lastBuildAttempt2 } : {},
99070
99755
  ...view ? { view } : {}
99071
99756
  };
99072
99757
  }
99073
99758
  const degrees = buildFileDegrees(graph);
99074
- const trust = computeTrust(graph, cwd);
99759
+ const trust = computeTrust(graph, cwd, getReviewGraphRevisionDrift2(cwd));
99075
99760
  const hubs = await computeHubs(graph, degrees, cwd, limit, focusTerms);
99076
99761
  const { entryPoints, entryPointFiles } = computeEntryPoints(graph, degrees, cwd, limit, focusTerms);
99077
99762
  const subsystems = computeSubsystems(graph, cwd, limit);
@@ -99507,7 +100192,7 @@ function createLensDiagnosticsTool(cacheManager, getCwd, getLspService = getLSPS
99507
100192
  };
99508
100193
  }
99509
100194
  function formatProjectDeltaDiagnostic(diagnostic, stale) {
99510
- const marker = diagnostic.semantic === "blocking" || diagnostic.severity === "error" ? "\u{1F534}" : "\u2139";
100195
+ const marker = stale ? "\u25CB" : diagnostic.semantic === "blocking" || diagnostic.severity === "error" ? "\u{1F534}" : "\u2139";
99511
100196
  const rule = diagnostic.rule ?? diagnostic.code ?? diagnostic.runner;
99512
100197
  const where = stale ? STALE_LINE_MARKER : `L${diagnostic.line ?? "?"}`;
99513
100198
  return ` ${marker} ${where} ${rule} ${diagnostic.message}`;
@@ -104856,6 +105541,7 @@ function recordToolSetMutation(mutation) {
104856
105541
  }
104857
105542
 
104858
105543
  // dist/clients/cache-observability.js
105544
+ init_env_utils();
104859
105545
  init_latency_logger();
104860
105546
  import { createHash as createHash15 } from "node:crypto";
104861
105547
  var PROCESS_TURN_SCOPE = "process-global-runtime";
@@ -104866,6 +105552,93 @@ var MAX_REPORTED_MESSAGES = 1e4;
104866
105552
  var MAX_INJECTED_CHARS = 16384;
104867
105553
  var MAX_INJECTED_BYTES = 65536;
104868
105554
  var contextObservationCounter = 0;
105555
+ var CHARS_PER_ESTIMATED_TOKEN = 4;
105556
+ var DEFAULT_PROVIDER_CACHE_TTL_MS = 6e4;
105557
+ var _providerCacheTtl = lazyEnvNumber("PI_LENS_PROVIDER_CACHE_TTL_MS", DEFAULT_PROVIDER_CACHE_TTL_MS);
105558
+ var getProviderCacheTtlMs = _providerCacheTtl.get;
105559
+ var _resetProviderCacheTtlForTests = _providerCacheTtl._resetForTests;
105560
+ var LOW_CACHE_READ_RATIO = 0.5;
105561
+ var PARTIAL_EVICTION_INPUT_FACTOR = 4;
105562
+ var PARTIAL_EVICTION_MIN_FRESH_TOKENS = 512;
105563
+ var MAX_ATTRIBUTION_CHARS = 262144;
105564
+ var MAX_ATTRIBUTION_BYTES = 1048576;
105565
+ var attributionBySession = /* @__PURE__ */ new Map();
105566
+ function newAttributionState() {
105567
+ return {
105568
+ prefixBrokeSinceLastUsage: false,
105569
+ injectedCharsSinceLastUsage: 0,
105570
+ newTranscriptCharsSinceLastUsage: 0,
105571
+ attributionCharsCapped: false
105572
+ };
105573
+ }
105574
+ function attributionFor(key2) {
105575
+ const existing = attributionBySession.get(key2);
105576
+ const state2 = existing ?? newAttributionState();
105577
+ attributionBySession.delete(key2);
105578
+ attributionBySession.set(key2, state2);
105579
+ while (attributionBySession.size > MAX_TRACKED_SESSIONS) {
105580
+ const oldest = attributionBySession.keys().next().value;
105581
+ if (oldest === void 0)
105582
+ break;
105583
+ attributionBySession.delete(oldest);
105584
+ }
105585
+ return state2;
105586
+ }
105587
+ function recordContextAttribution(key2, existingMessages, injectedSize) {
105588
+ const state2 = attributionFor(key2);
105589
+ const baseline = Math.min(state2.lastObservedMessageCount ?? existingMessages.length, existingMessages.length);
105590
+ const grown = measureTranscriptGrowth(existingMessages.slice(baseline));
105591
+ state2.injectedCharsSinceLastUsage = Math.min(MAX_ATTRIBUTION_CHARS, state2.injectedCharsSinceLastUsage + injectedSize.chars);
105592
+ state2.newTranscriptCharsSinceLastUsage = Math.min(MAX_ATTRIBUTION_CHARS, state2.newTranscriptCharsSinceLastUsage + grown.chars);
105593
+ if (injectedSize.capped || grown.capped || state2.injectedCharsSinceLastUsage >= MAX_ATTRIBUTION_CHARS || state2.newTranscriptCharsSinceLastUsage >= MAX_ATTRIBUTION_CHARS) {
105594
+ state2.attributionCharsCapped = true;
105595
+ }
105596
+ state2.lastObservedMessageCount = existingMessages.length;
105597
+ state2.lastContextAtMs = Date.now();
105598
+ }
105599
+ function resolveInterTurnGap(state2, nowMs) {
105600
+ if (state2.lastUsageAtMs === void 0) {
105601
+ return { gapMs: null, basis: "no-prior-turn" };
105602
+ }
105603
+ if (state2.lastContextAtMs !== void 0) {
105604
+ return {
105605
+ gapMs: Math.max(0, state2.lastContextAtMs - state2.lastUsageAtMs),
105606
+ basis: "request-time"
105607
+ };
105608
+ }
105609
+ return {
105610
+ gapMs: Math.max(0, nowMs - state2.lastUsageAtMs),
105611
+ basis: "message-end-fallback"
105612
+ };
105613
+ }
105614
+ function estimateTokens2(chars) {
105615
+ return Math.ceil(chars / CHARS_PER_ESTIMATED_TOKEN);
105616
+ }
105617
+ function classifyCacheMiss(args) {
105618
+ const { cacheRead, priorCacheRead } = args;
105619
+ if (typeof cacheRead !== "number" || !Number.isFinite(cacheRead))
105620
+ return null;
105621
+ if (typeof priorCacheRead !== "number" || !Number.isFinite(priorCacheRead)) {
105622
+ return null;
105623
+ }
105624
+ let kind;
105625
+ if (cacheRead === 0) {
105626
+ kind = "zero-read";
105627
+ } else if (priorCacheRead > 0 && cacheRead < priorCacheRead * LOW_CACHE_READ_RATIO) {
105628
+ kind = "low-read";
105629
+ } else {
105630
+ return null;
105631
+ }
105632
+ if (args.prefixBroke)
105633
+ return { kind, cause: "prefix-broke" };
105634
+ if (args.interTurnGapMs !== null && args.interTurnGapMs >= args.ttlMs) {
105635
+ return { kind, cause: "ttl-expired" };
105636
+ }
105637
+ if (kind === "low-read" && !args.attributionCharsCapped && args.input > Math.max(PARTIAL_EVICTION_MIN_FRESH_TOKENS, args.estimatedNewTokens * PARTIAL_EVICTION_INPUT_FACTOR)) {
105638
+ return { kind, cause: "partial-eviction" };
105639
+ }
105640
+ return { kind, cause: "unknown" };
105641
+ }
104869
105642
  function boundedHashScalar(value, state2) {
104870
105643
  if (value === null)
104871
105644
  return "null";
@@ -104938,87 +105711,78 @@ function hashMessageSequence(messages) {
104938
105711
  contentTruncated: state2.contentTruncated
104939
105712
  };
104940
105713
  }
104941
- function messageTextSize(content) {
105714
+ var INJECTED_LIMITS = {
105715
+ maxChars: MAX_INJECTED_CHARS,
105716
+ maxBytes: MAX_INJECTED_BYTES
105717
+ };
105718
+ var TRANSCRIPT_LIMITS = {
105719
+ maxChars: MAX_ATTRIBUTION_CHARS,
105720
+ maxBytes: MAX_ATTRIBUTION_BYTES
105721
+ };
105722
+ function messageTextSize(content, limits) {
104942
105723
  if (typeof content === "string") {
104943
- const chars = Math.min(content.length, MAX_INJECTED_CHARS);
105724
+ const chars = Math.min(content.length, limits.maxChars);
104944
105725
  return {
104945
105726
  chars,
104946
- bytes: Math.min(Buffer.byteLength(content.slice(0, chars), "utf8"), MAX_INJECTED_BYTES)
105727
+ bytes: Math.min(Buffer.byteLength(content.slice(0, chars), "utf8"), limits.maxBytes)
104947
105728
  };
104948
105729
  }
104949
105730
  const bounded = boundedHashValue(content);
104950
105731
  return {
104951
- chars: Math.min(bounded.length, MAX_INJECTED_CHARS),
104952
- bytes: Math.min(Buffer.byteLength(bounded.slice(0, MAX_INJECTED_CHARS), "utf8"), MAX_INJECTED_BYTES)
105732
+ chars: Math.min(bounded.length, limits.maxChars),
105733
+ bytes: Math.min(Buffer.byteLength(bounded.slice(0, limits.maxChars), "utf8"), limits.maxBytes)
104953
105734
  };
104954
105735
  }
104955
- function measureInjectedMessages(messages) {
105736
+ function measureMessages(messages, limits) {
104956
105737
  let chars = 0;
104957
105738
  let bytes = 0;
104958
105739
  const measuredCount = Math.min(messages.length, MAX_REPORTED_MESSAGES);
104959
105740
  for (let i = 0; i < measuredCount; i++) {
104960
105741
  const message = messages[i];
104961
- const size = messageTextSize(message?.content);
104962
- chars = Math.min(MAX_INJECTED_CHARS, chars + size.chars);
104963
- bytes = Math.min(MAX_INJECTED_BYTES, bytes + size.bytes);
104964
- if (chars === MAX_INJECTED_CHARS || bytes === MAX_INJECTED_BYTES)
105742
+ const size = messageTextSize(message?.content, limits);
105743
+ chars = Math.min(limits.maxChars, chars + size.chars);
105744
+ bytes = Math.min(limits.maxBytes, bytes + size.bytes);
105745
+ if (chars === limits.maxChars || bytes === limits.maxBytes)
104965
105746
  break;
104966
105747
  }
104967
105748
  return {
104968
105749
  chars,
104969
105750
  bytes,
104970
- capped: chars >= MAX_INJECTED_CHARS || bytes >= MAX_INJECTED_BYTES
105751
+ capped: chars >= limits.maxChars || bytes >= limits.maxBytes
104971
105752
  };
104972
105753
  }
104973
- function sessionKey(sessionId) {
104974
- return sessionId?.trim() ? sessionId.trim() : NO_SESSION_KEY;
104975
- }
104976
- function prefixLengthForPlacement(placement, messageCount) {
104977
- if (placement === "insert-before-final") {
104978
- return Math.max(0, messageCount - 1);
104979
- }
104980
- if (placement === "prepend")
104981
- return 0;
104982
- return messageCount;
104983
- }
104984
- function resolvePrefixObservation(truncated, observation) {
104985
- if (truncated)
104986
- return "unknown";
104987
- return observation ?? "empty";
105754
+ function measureInjectedMessages(messages) {
105755
+ return measureMessages(messages, INJECTED_LIMITS);
104988
105756
  }
104989
- function prefixBaselineForObservation(observation) {
104990
- if (observation === "baseline")
104991
- return true;
104992
- if (observation === "empty" || observation === "unknown")
104993
- return null;
104994
- return false;
105757
+ function measureTranscriptGrowth(messages) {
105758
+ return measureMessages(messages, TRANSCRIPT_LIMITS);
104995
105759
  }
104996
- function firstMessageChangeFor(truncated, before, after) {
104997
- if (truncated)
104998
- return "unknown";
104999
- return before !== after ? "changed" : "unchanged";
105760
+ function sessionKey(sessionId) {
105761
+ return sessionId?.trim() ? sessionId.trim() : NO_SESSION_KEY;
105000
105762
  }
105001
105763
  function observeCacheContext(args) {
105002
105764
  try {
105003
105765
  const existingMessages = args.existingMessages ?? [];
105004
105766
  const resultMessages = args.resultMessages ?? existingMessages;
105005
- const injectedMessages = args.injectedMessages ?? [];
105767
+ const injectionSlices = (args.injectionSlices ?? []).filter((slice) => slice.messages.length > 0);
105768
+ const injectedMessages = injectionSlices.flatMap((slice) => slice.messages);
105006
105769
  const placement = args.placement ?? "none";
105007
105770
  const beforeSequence = hashMessageSequence(existingMessages);
105008
105771
  const afterSequence = hashMessageSequence(resultMessages);
105009
- const beforePrefixLength = prefixLengthForPlacement(placement, existingMessages.length);
105010
- const afterPrefixLength = Math.min(beforePrefixLength, resultMessages.length);
105011
- const beforePrefix = hashMessageSequence(existingMessages.slice(0, beforePrefixLength));
105012
- const afterPrefix = hashMessageSequence(resultMessages.slice(0, afterPrefixLength));
105013
- const beforeFirstSequence = existingMessages.length ? hashMessageSequence([existingMessages[0]]) : void 0;
105014
- const afterFirstSequence = resultMessages.length ? hashMessageSequence([resultMessages[0]]) : void 0;
105015
- const beforeFirst = beforeFirstSequence?.hash ?? null;
105016
- const afterFirst = afterFirstSequence?.hash ?? null;
105017
- const firstMessageHashTruncated = beforeFirstSequence?.truncated === true || beforeFirstSequence?.contentTruncated === true || afterFirstSequence?.truncated === true || afterFirstSequence?.contentTruncated === true;
105018
- const prefixHashTruncated = beforePrefix.truncated || afterPrefix.truncated || beforePrefix.contentTruncated || afterPrefix.contentTruncated;
105019
- const prefixObservation = resolvePrefixObservation(prefixHashTruncated, args.prefixObservation);
105020
105772
  const sizes = measureInjectedMessages(injectedMessages);
105773
+ const sourceBreakdown = injectionSlices.map((slice) => {
105774
+ const sliceSize = measureInjectedMessages(slice.messages);
105775
+ return {
105776
+ source: slice.source,
105777
+ messageCount: Math.min(slice.messages.length, MAX_REPORTED_MESSAGES),
105778
+ chars: sliceSize.chars,
105779
+ bytes: sliceSize.bytes,
105780
+ estimatedTokens: estimateTokens2(sliceSize.chars),
105781
+ countsCapped: sliceSize.capped
105782
+ };
105783
+ });
105021
105784
  const messageCountCapped = existingMessages.length > MAX_REPORTED_MESSAGES || resultMessages.length > MAX_REPORTED_MESSAGES;
105785
+ recordContextAttribution(sessionKey(args.sessionId), existingMessages, sizes);
105022
105786
  logLatency({
105023
105787
  type: "phase",
105024
105788
  filePath: "<pi-lens>",
@@ -105032,34 +105796,25 @@ function observeCacheContext(args) {
105032
105796
  sessionRole: args.sessionRole,
105033
105797
  ...args.sessionRole === "concurrent-secondary" ? { turnScope: SECONDARY_TURN_SCOPE } : { turnIndex: args.turnIndex, turnScope: PROCESS_TURN_SCOPE },
105034
105798
  injectionEnabled: args.injectionEnabled,
105035
- injectionSources: Array.from(args.injectionSources ?? []),
105799
+ injectionSources: sourceBreakdown.map((entry) => entry.source),
105800
+ injectionSourceBreakdown: sourceBreakdown,
105801
+ injectionOccurred: injectedMessages.length > 0,
105036
105802
  injectedMessageCount: Math.min(injectedMessages.length, MAX_REPORTED_MESSAGES),
105037
105803
  injectedMessageCountCapped: injectedMessages.length > MAX_REPORTED_MESSAGES,
105038
105804
  injectedChars: sizes.chars,
105039
105805
  injectedBytes: sizes.bytes,
105806
+ injectedEstimatedTokens: estimateTokens2(sizes.chars),
105807
+ injectedTokenBasis: "chars-per-token-4-estimate-not-provider-measured",
105040
105808
  injectedCountsCapped: sizes.capped,
105041
105809
  existingMessageCount: Math.min(existingMessages.length, MAX_REPORTED_MESSAGES),
105042
105810
  resultMessageCount: Math.min(resultMessages.length, MAX_REPORTED_MESSAGES),
105043
105811
  messageCountCapped,
105044
105812
  placement,
105045
- prefixObservation,
105046
- prefixObservationUnknown: prefixObservation === "unknown",
105047
- prefixBaseline: prefixBaselineForObservation(prefixObservation),
105048
- firstMessageChanged: firstMessageHashTruncated ? null : beforeFirst !== afterFirst,
105049
- firstMessageChange: firstMessageChangeFor(firstMessageHashTruncated, beforeFirst, afterFirst),
105050
- firstMessageHashTruncated,
105051
- beforeFirstMessageHash: beforeFirst,
105052
- afterFirstMessageHash: afterFirst,
105053
105813
  beforeSequenceHash: beforeSequence.hash,
105054
105814
  afterSequenceHash: afterSequence.hash,
105055
- beforePrefixHash: beforePrefix.hash,
105056
- afterPrefixHash: afterPrefix.hash,
105057
105815
  sequenceHashTruncated: beforeSequence.truncated || afterSequence.truncated || beforeSequence.contentTruncated || afterSequence.contentTruncated,
105058
105816
  sequenceMessageCountTruncated: beforeSequence.truncated || afterSequence.truncated,
105059
- sequenceContentHashTruncated: beforeSequence.contentTruncated || afterSequence.contentTruncated,
105060
- prefixHashTruncated,
105061
- prefixMessageCountTruncated: beforePrefix.truncated || afterPrefix.truncated,
105062
- prefixContentHashTruncated: beforePrefix.contentTruncated || afterPrefix.contentTruncated
105817
+ sequenceContentHashTruncated: beforeSequence.contentTruncated || afterSequence.contentTruncated
105063
105818
  }
105064
105819
  });
105065
105820
  } catch (err) {
@@ -105077,6 +105832,34 @@ function logCacheUsage(message, dbg2, context) {
105077
105832
  if (!usage || typeof usage !== "object")
105078
105833
  return;
105079
105834
  const u = usage;
105835
+ const attributionKey = sessionKey(context?.sessionId);
105836
+ const state2 = attributionFor(attributionKey);
105837
+ const nowMs = Date.now();
105838
+ const gap = resolveInterTurnGap(state2, nowMs);
105839
+ const interTurnGapMs = gap.gapMs;
105840
+ const ttlMs = getProviderCacheTtlMs();
105841
+ const estimatedNewTokens = estimateTokens2(state2.injectedCharsSinceLastUsage + state2.newTranscriptCharsSinceLastUsage);
105842
+ const verdict = classifyCacheMiss({
105843
+ cacheRead: u.cacheRead,
105844
+ input: typeof u.input === "number" ? u.input : 0,
105845
+ priorCacheRead: state2.lastCacheRead,
105846
+ interTurnGapMs,
105847
+ ttlMs,
105848
+ prefixBroke: state2.prefixBrokeSinceLastUsage,
105849
+ estimatedNewTokens,
105850
+ attributionCharsCapped: state2.attributionCharsCapped
105851
+ });
105852
+ const priorCacheRead = typeof state2.lastCacheRead === "number" ? state2.lastCacheRead : null;
105853
+ const injectedCharsSinceLastTurn = state2.injectedCharsSinceLastUsage;
105854
+ const newTranscriptCharsSinceLastTurn = state2.newTranscriptCharsSinceLastUsage;
105855
+ const attributionCharsCapped = state2.attributionCharsCapped;
105856
+ state2.lastUsageAtMs = nowMs;
105857
+ state2.lastCacheRead = typeof u.cacheRead === "number" && Number.isFinite(u.cacheRead) ? u.cacheRead : void 0;
105858
+ state2.prefixBrokeSinceLastUsage = false;
105859
+ state2.injectedCharsSinceLastUsage = 0;
105860
+ state2.newTranscriptCharsSinceLastUsage = 0;
105861
+ state2.attributionCharsCapped = false;
105862
+ state2.lastContextAtMs = void 0;
105080
105863
  logLatency({
105081
105864
  type: "phase",
105082
105865
  filePath: "<pi-lens>",
@@ -105091,6 +105874,20 @@ function logCacheUsage(message, dbg2, context) {
105091
105874
  output: u.output,
105092
105875
  // `Usage.cost` is a breakdown object; the total is the headline number.
105093
105876
  cost: u.cost?.total,
105877
+ // Idle milliseconds before this turn's request, or null for the first
105878
+ // record in the session. This is the field the 2026-08-21
105879
+ // investigation had to reconstruct by hand-joining timestamps.
105880
+ interTurnGapMs,
105881
+ gapBasis: gap.basis,
105882
+ // null means "nothing to explain", not "cause unknown"; `unknown` is
105883
+ // the explicit no-local-explanation verdict.
105884
+ cacheMissCause: verdict?.cause ?? null,
105885
+ cacheMissKind: verdict?.kind ?? null,
105886
+ cacheTtlThresholdMs: ttlMs,
105887
+ priorCacheRead,
105888
+ injectedCharsSinceLastTurn,
105889
+ newTranscriptCharsSinceLastTurn,
105890
+ attributionCharsCapped,
105094
105891
  ...context ? {
105095
105892
  // MessageEndEvent has no request/context id in the host API. These
105096
105893
  // fields permit session correlation without inventing an exact
@@ -105158,6 +105955,7 @@ function observeCachePrefix(messages, turnIndex, sessionId, sessionRole, dbg2) {
105158
105955
  return "baseline";
105159
105956
  }
105160
105957
  if (currentHash !== previousHash) {
105958
+ attributionFor(key2).prefixBrokeSinceLastUsage = true;
105161
105959
  logLatency({
105162
105960
  type: "phase",
105163
105961
  filePath: "<pi-lens>",
@@ -105182,6 +105980,7 @@ function observeCachePrefix(messages, turnIndex, sessionId, sessionRole, dbg2) {
105182
105980
  function clearCachePrefixSession(sessionId) {
105183
105981
  const key2 = sessionKey(sessionId);
105184
105982
  prefixHashBySession.delete(key2);
105983
+ attributionBySession.delete(key2);
105185
105984
  }
105186
105985
 
105187
105986
  // dist/clients/startup-timing.js
@@ -105478,9 +106277,6 @@ logLatency({
105478
106277
  });
105479
106278
  function log(_msg) {
105480
106279
  }
105481
- function isStaleExtensionCtxError(err) {
105482
- return err instanceof Error && err.message.includes("stale after session replacement or reload");
105483
- }
105484
106280
  var runtime = new RuntimeCoordinator();
105485
106281
  var _readBridgeRegistered = false;
105486
106282
  var _readBridgeGetFlag;
@@ -106139,7 +106935,7 @@ ${degradations.map((group) => ` ${group.kind}: ${group.count} (${group.latestRe
106139
106935
  skillPaths: [skillsDir]
106140
106936
  };
106141
106937
  });
106142
- pi.on("session_start", async (event, ctx) => {
106938
+ pi.on("session_start", wrapSessionEventHandler("session_start", async (event, ctx) => {
106143
106939
  const sessionStartMonotonicAt = performance4.now();
106144
106940
  resetVerifiedPathAttributionGuessCount();
106145
106941
  warmDispatchAtSessionStart();
@@ -106314,10 +107110,12 @@ ${degradations.map((group) => ` ${group.kind}: ${group.count} (${group.latestRe
106314
107110
  mountLensWidget(ctx.ui, readExtensionMode(ctx));
106315
107111
  }
106316
107112
  } catch (sessionErr) {
107113
+ if (isStaleExtensionCtxError(sessionErr))
107114
+ throw sessionErr;
106317
107115
  dbg(`session_start crashed: ${sessionErr}`);
106318
107116
  dbg(`session_start crash stack: ${sessionErr.stack}`);
106319
107117
  }
106320
- });
107118
+ }, { dbg }));
106321
107119
  pi.on("session_before_fork", () => {
106322
107120
  try {
106323
107121
  pendingForkSnapshot = exportWidgetState();
@@ -106341,7 +107139,7 @@ ${degradations.map((group) => ` ${group.kind}: ${group.count} (${group.latestRe
106341
107139
  resetLSPService
106342
107140
  });
106343
107141
  });
106344
- pi.on("tool_result", async (event, ctx) => {
107142
+ const onToolResult = async (event, ctx) => {
106345
107143
  rememberOwnEventCtx(ctx);
106346
107144
  if (!lensEnabled)
106347
107145
  return;
@@ -106382,8 +107180,9 @@ ${degradations.map((group) => ` ${group.kind}: ${group.count} (${group.latestRe
106382
107180
  } finally {
106383
107181
  setAmbientAbortSignal(void 0);
106384
107182
  }
106385
- });
106386
- pi.on("turn_start", (_event, ctx) => {
107183
+ };
107184
+ pi.on("tool_result", wrapSessionEventHandler("tool_result", onToolResult, { dbg }));
107185
+ const onTurnStart = (_event, ctx) => {
106387
107186
  rememberOwnEventCtx(ctx);
106388
107187
  adoptProjectTrustFromPorts(hostPorts);
106389
107188
  if (lensWidgetVisible && ctx?.ui && (mountedLensWidgetUi === void 0 || mountedLensWidgetUi !== ctx.ui)) {
@@ -106400,7 +107199,8 @@ ${degradations.map((group) => ` ${group.kind}: ${group.count} (${group.latestRe
106400
107199
  }).catch((err) => {
106401
107200
  dbg(`turn_start: cross-process nudge read failed: ${err}`);
106402
107201
  });
106403
- });
107202
+ };
107203
+ pi.on("turn_start", wrapSessionEventHandler("turn_start", onTurnStart, { dbg }));
106404
107204
  async function runDeferredMutationDrain(ctx) {
106405
107205
  const currentSessionId = getStableSessionId(ctx);
106406
107206
  const emission = classifyCurrentSessionEmission(ctx, currentSessionId);
@@ -106434,25 +107234,28 @@ ${degradations.map((group) => ` ${group.kind}: ${group.count} (${group.latestRe
106434
107234
  updateLspStatus(ctx.ui.setStatus, ctx.ui.theme);
106435
107235
  }
106436
107236
  }
106437
- pi.on("agent_end", async (_event, ctx) => {
107237
+ const onAgentEnd = async (_event, ctx) => {
106438
107238
  if (!lensEnabled)
106439
107239
  return;
106440
- setAmbientAbortSignal(ctx?.signal);
106441
107240
  try {
107241
+ setAmbientAbortSignal(ctx?.signal);
106442
107242
  await flushDebouncedToolResults();
106443
107243
  ctx.ui && updateLspStatus(ctx.ui.setStatus, ctx.ui.theme);
106444
107244
  } catch (agentEndErr) {
107245
+ if (isStaleExtensionCtxError(agentEndErr))
107246
+ throw agentEndErr;
106445
107247
  dbg(`agent_end crashed: ${agentEndErr}`);
106446
107248
  dbg(`agent_end crash stack: ${agentEndErr.stack}`);
106447
107249
  } finally {
106448
107250
  setAmbientAbortSignal(void 0);
106449
107251
  }
106450
- });
106451
- pi.on("turn_end", async (_event, ctx) => {
107252
+ };
107253
+ pi.on("agent_end", wrapSessionEventHandler("agent_end", onAgentEnd, { dbg }));
107254
+ const onTurnEnd = async (_event, ctx) => {
106452
107255
  if (!lensEnabled)
106453
107256
  return;
106454
- setAmbientAbortSignal(ctx?.signal);
106455
107257
  try {
107258
+ setAmbientAbortSignal(ctx?.signal);
106456
107259
  const repaintLspStatus = captureLspStatusRepaint(ctx);
106457
107260
  const elStats = getEventLoopStats();
106458
107261
  const loopMaxMs = elStats?.maxMs ?? 0;
@@ -106551,12 +107354,15 @@ ${degradations.map((group) => ` ${group.kind}: ${group.count} (${group.latestRe
106551
107354
  );
106552
107355
  }
106553
107356
  } catch (turnEndErr) {
107357
+ if (isStaleExtensionCtxError(turnEndErr))
107358
+ throw turnEndErr;
106554
107359
  dbg(`turn_end crashed: ${turnEndErr}`);
106555
107360
  dbg(`turn_end crash stack: ${turnEndErr.stack}`);
106556
107361
  } finally {
106557
107362
  setAmbientAbortSignal(void 0);
106558
107363
  }
106559
- });
107364
+ };
107365
+ pi.on("turn_end", wrapSessionEventHandler("turn_end", onTurnEnd, { dbg }));
106560
107366
  registerBuiltinQuietWindowTasks(() => runtime);
106561
107367
  registerCascadeTierReconcileTask(() => getLSPService(), {
106562
107368
  onResolvedFound: ({ filePath, diagnostics }) => {
@@ -106644,27 +107450,36 @@ ${degradations.map((group) => ` ${group.kind}: ${group.count} (${group.latestRe
106644
107450
  });
106645
107451
  });
106646
107452
  }
106647
- try {
106648
- pi.on("agent_settled", async (_event, ctx) => {
106649
- if (!lensEnabled)
106650
- return;
107453
+ const onAgentSettled = async (_event, ctx) => {
107454
+ if (!lensEnabled)
107455
+ return;
107456
+ try {
106651
107457
  setAmbientAbortSignal(ctx?.signal);
106652
107458
  try {
106653
107459
  await runDeferredMutationDrain(ctx);
106654
107460
  } catch (drainErr) {
107461
+ if (isStaleExtensionCtxError(drainErr))
107462
+ throw drainErr;
106655
107463
  dbg(`agent_settled deferred_mutation_drain crashed: ${drainErr}`);
106656
107464
  } finally {
106657
107465
  setAmbientAbortSignal(void 0);
106658
107466
  }
107467
+ const cwd = ctx?.cwd;
106659
107468
  void runQuietWindow({
106660
107469
  runtime,
106661
107470
  dbg,
106662
- cwd: ctx?.cwd
107471
+ cwd
106663
107472
  }).catch((err) => {
106664
107473
  dbg(`quiet_window crashed: ${err}`);
106665
107474
  });
106666
107475
  dumpActiveHandles("agent_settled");
106667
- });
107476
+ } catch (settledErr) {
107477
+ setAmbientAbortSignal(void 0);
107478
+ throw settledErr;
107479
+ }
107480
+ };
107481
+ try {
107482
+ pi.on("agent_settled", wrapSessionEventHandler("agent_settled", onAgentSettled, { dbg }));
106668
107483
  } catch (registerErr) {
106669
107484
  dbg(`agent_settled registration failed (older pi host?): ${registerErr}`);
106670
107485
  }
@@ -106719,89 +107534,105 @@ ${degradations.map((group) => ` ${group.kind}: ${group.count} (${group.latestRe
106719
107534
  return true;
106720
107535
  return !msg.content.some((block) => typeof block === "object" && block !== null && block.type === "tool_result");
106721
107536
  };
106722
- pi.on("context", async (event, ctx) => {
106723
- const existingMessages = event?.messages ?? [];
106724
- const prefixSessionId = getStableSessionId(ctx);
106725
- const sessionRole = classifyCurrentSessionEmission(ctx, prefixSessionId);
106726
- const prefixObservation = observeCachePrefix(existingMessages, runtime.turnIndex, prefixSessionId, sessionRole, dbg);
106727
- const effectiveInjectionEnabled = lensEnabled && contextInjectionEnabled;
106728
- let telemetryLogged = false;
106729
- const logContextObservation = (resultMessages, placement, injectionSources, injectedMessages) => {
106730
- if (telemetryLogged)
106731
- return;
106732
- telemetryLogged = true;
106733
- observeCacheContext({
106734
- existingMessages,
106735
- resultMessages,
106736
- sessionId: prefixSessionId,
106737
- sessionRole,
106738
- turnIndex: runtime.turnIndex,
106739
- injectionEnabled: effectiveInjectionEnabled,
106740
- injectionSources,
106741
- injectedMessages,
106742
- placement,
106743
- prefixObservation,
106744
- dbg
106745
- });
106746
- };
106747
- try {
106748
- const cwd = ctx.cwd ?? process.cwd();
106749
- if (!effectiveInjectionEnabled) {
106750
- logContextObservation(existingMessages, "none", [], []);
106751
- return;
106752
- }
106753
- const turnEndFindings = consumeTurnEndFindings(cacheManager, cwd, runtime);
106754
- const sessionGuidance = consumeSessionStartGuidance(cacheManager, cwd);
106755
- const testFindings = consumeTestFindings(cacheManager, cwd, runtime);
106756
- const agentNudge = consumeAgentNudge(dbg);
106757
- const sourceMessages = [
106758
- {
106759
- source: "session-guidance",
106760
- messages: sessionGuidance?.messages ?? []
106761
- },
106762
- {
106763
- source: "turn-findings",
106764
- messages: turnEndFindings?.messages ?? []
106765
- },
106766
- {
106767
- source: "test-findings",
106768
- messages: testFindings?.messages ?? []
106769
- },
106770
- {
106771
- source: "agent-nudge",
106772
- messages: agentNudge?.messages ?? []
106773
- }
106774
- ].filter((source) => source.messages.length > 0);
106775
- const injectedMessages = sourceMessages.flatMap((source) => source.messages);
106776
- const injectionSources = sourceMessages.map((source) => source.source);
106777
- if (injectedMessages.length === 0) {
106778
- logContextObservation(existingMessages, "none", [], []);
106779
- return;
107537
+ pi.on(
107538
+ "context",
107539
+ // #1929: `context` is reachable with a ctx the SDK already invalidated,
107540
+ // exactly like the five handlers #1925 wrapped. It could not use the void
107541
+ // wrapper because the host CONSUMES its return value on the live path, so
107542
+ // it uses the value-returning variant and states its own stale-path answer
107543
+ // below. Before this, a stale ctx surfaced as `context event error: …`,
107544
+ // indistinguishable from a real handler bug, and counted nothing.
107545
+ wrapSessionEventHandlerWithResult("context", async (event, ctx) => {
107546
+ const existingMessages = event?.messages ?? [];
107547
+ const prefixSessionId = getStableSessionId(ctx);
107548
+ const sessionRole = classifyCurrentSessionEmission(ctx, prefixSessionId);
107549
+ observeCachePrefix(existingMessages, runtime.turnIndex, prefixSessionId, sessionRole, dbg);
107550
+ const effectiveInjectionEnabled = lensEnabled && contextInjectionEnabled;
107551
+ let telemetryLogged = false;
107552
+ const logContextObservation = (resultMessages, placement, injectionSlices) => {
107553
+ if (telemetryLogged)
107554
+ return;
107555
+ telemetryLogged = true;
107556
+ observeCacheContext({
107557
+ existingMessages,
107558
+ resultMessages,
107559
+ sessionId: prefixSessionId,
107560
+ sessionRole,
107561
+ turnIndex: runtime.turnIndex,
107562
+ injectionEnabled: effectiveInjectionEnabled,
107563
+ injectionSlices,
107564
+ placement,
107565
+ dbg
107566
+ });
107567
+ };
107568
+ try {
107569
+ const cwd = ctx.cwd ?? process.cwd();
107570
+ if (!effectiveInjectionEnabled) {
107571
+ logContextObservation(existingMessages, "none", []);
107572
+ return;
107573
+ }
107574
+ const turnEndFindings = consumeTurnEndFindings(cacheManager, cwd, runtime);
107575
+ const sessionGuidance = consumeSessionStartGuidance(cacheManager, cwd);
107576
+ const testFindings = consumeTestFindings(cacheManager, cwd, runtime);
107577
+ const agentNudge = consumeAgentNudge(dbg);
107578
+ const sourceMessages = [
107579
+ {
107580
+ source: "session-guidance",
107581
+ messages: sessionGuidance?.messages ?? []
107582
+ },
107583
+ {
107584
+ source: "turn-findings",
107585
+ messages: turnEndFindings?.messages ?? []
107586
+ },
107587
+ {
107588
+ source: "test-findings",
107589
+ messages: testFindings?.messages ?? []
107590
+ },
107591
+ {
107592
+ source: "agent-nudge",
107593
+ messages: agentNudge?.messages ?? []
107594
+ }
107595
+ ].filter((source) => source.messages.length > 0);
107596
+ const injectedMessages = sourceMessages.flatMap((source) => source.messages);
107597
+ if (injectedMessages.length === 0) {
107598
+ logContextObservation(existingMessages, "none", []);
107599
+ return;
107600
+ }
107601
+ if (existingMessages.length === 0) {
107602
+ const resultMessages2 = [...injectedMessages];
107603
+ logContextObservation(resultMessages2, "prepend", sourceMessages);
107604
+ return { messages: resultMessages2 };
107605
+ }
107606
+ const lastMessage = existingMessages[existingMessages.length - 1];
107607
+ if (!isPlainUserPrompt(lastMessage)) {
107608
+ const resultMessages2 = [...existingMessages, ...injectedMessages];
107609
+ logContextObservation(resultMessages2, "append", sourceMessages);
107610
+ return { messages: resultMessages2 };
107611
+ }
107612
+ const resultMessages = [
107613
+ ...existingMessages.slice(0, -1),
107614
+ ...injectedMessages,
107615
+ lastMessage
107616
+ ];
107617
+ logContextObservation(resultMessages, "insert-before-final", sourceMessages);
107618
+ return { messages: resultMessages };
107619
+ } catch (err) {
107620
+ if (isStaleExtensionCtxError(err))
107621
+ throw err;
107622
+ if (!telemetryLogged)
107623
+ logContextObservation(existingMessages, "none", []);
107624
+ dbg(`context event error: ${err}`);
106780
107625
  }
106781
- if (existingMessages.length === 0) {
106782
- const resultMessages2 = [...injectedMessages];
106783
- logContextObservation(resultMessages2, "prepend", injectionSources, injectedMessages);
106784
- return { messages: resultMessages2 };
106785
- }
106786
- const lastMessage = existingMessages[existingMessages.length - 1];
106787
- if (!isPlainUserPrompt(lastMessage)) {
106788
- const resultMessages2 = [...existingMessages, ...injectedMessages];
106789
- logContextObservation(resultMessages2, "append", injectionSources, injectedMessages);
106790
- return { messages: resultMessages2 };
106791
- }
106792
- const resultMessages = [
106793
- ...existingMessages.slice(0, -1),
106794
- ...injectedMessages,
106795
- lastMessage
106796
- ];
106797
- logContextObservation(resultMessages, "insert-before-final", injectionSources, injectedMessages);
106798
- return { messages: resultMessages };
106799
- } catch (err) {
106800
- if (!telemetryLogged)
106801
- logContextObservation(existingMessages, "none", [], []);
106802
- dbg(`context event error: ${err}`);
106803
- }
106804
- });
107626
+ }, {
107627
+ dbg,
107628
+ // #1929: a dead ctx means pi-lens contributes NOTHING to this
107629
+ // request. `undefined` is that answer, and is what the four
107630
+ // non-injecting paths above already return, so the host keeps its
107631
+ // own message list byte-for-byte. Never a half-built injection: the
107632
+ // consume* calls that feed one have side effects and may not have run.
107633
+ onStaleResult: () => void 0
107634
+ })
107635
+ );
106805
107636
  }
106806
107637
  function index_default(pi) {
106807
107638
  return runInConsoleCaptureWindow(() => activateExtension(pi));