@mrclrchtr/supi-code-intelligence 6.2.0 → 6.3.0

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 (51) hide show
  1. package/README.md +2 -0
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-lsp/README.md +2 -0
  5. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  6. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  7. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/README.md +52 -0
  8. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +785 -129
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +660 -98
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +10 -4
  11. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  12. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +6 -2
  13. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +106 -0
  14. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +4 -4
  15. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +348 -99
  16. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +43 -15
  17. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +23 -32
  18. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +4 -4
  19. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +1 -1
  20. package/node_modules/@mrclrchtr/supi-lsp/src/workspace-path-policy.ts +18 -9
  21. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  22. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  23. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/README.md +29 -1
  24. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.cjs +99 -19
  25. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.cjs.map +4 -4
  26. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.js +94 -18
  27. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.js.map +4 -4
  28. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
  29. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +14 -14
  30. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +13 -8
  31. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.cjs +97 -19
  32. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.cjs.map +4 -4
  33. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.cts +28 -8
  34. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.cts.map +1 -1
  35. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.ts +28 -8
  36. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.ts.map +1 -1
  37. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.js +92 -18
  38. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.js.map +4 -4
  39. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
  40. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +17 -17
  41. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +7 -4
  42. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm +0 -0
  43. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +2 -2
  44. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm +0 -0
  45. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +2 -2
  46. package/package.json +5 -5
  47. package/src/session/health-refresh.ts +4 -2
  48. package/src/session/health-types.ts +3 -3
  49. package/src/substrate/lsp/settings.ts +2 -1
  50. package/src/tool/code_health/refresh-outcome.ts +37 -7
  51. package/src/tool/code_health/refresh-status.ts +58 -12
@@ -65,8 +65,8 @@ import {
65
65
  } from "../diagnostics/workspace-sentinels.ts";
66
66
  import { raceRequestControl } from "../session/readiness.ts";
67
67
  import {
68
- emptyProcessCrashRecoverySummary,
69
- type ProcessCrashRecoverySummary,
68
+ emptyProcessCrashRecoveryReport,
69
+ type ProcessCrashRecoveryReport,
70
70
  type ScopeDecisionEntry,
71
71
  type ScopeDecisionSummary,
72
72
  type WorkspaceDiagnosticReport,
@@ -94,6 +94,11 @@ import {
94
94
  syncClientFileAndGetDiagnostics,
95
95
  } from "./manager-diagnostics.ts";
96
96
  import { clientKey, rememberKnownRoot, resolveRootForFile } from "./manager-helpers.ts";
97
+ import {
98
+ boundProcessCrashFailureMessage,
99
+ buildProcessCrashRecoveryReport,
100
+ type ProcessCrashRecoveryRouteResult,
101
+ } from "./manager-process-crash-report.ts";
97
102
  import { buildProjectServerInfo } from "./manager-project-info.ts";
98
103
  import type {
99
104
  ActiveCoverageSummaryEntry,
@@ -180,7 +185,7 @@ type FileClientRequestOptions = {
180
185
 
181
186
  interface FileReadinessResult {
182
187
  client: LspClient | null;
183
- processCrashRecovery: ProcessCrashRecoverySummary;
188
+ processCrashRecovery: ProcessCrashRecoveryReport;
184
189
  }
185
190
 
186
191
  interface ProcessCrashRecoveryState {
@@ -196,40 +201,56 @@ interface ProcessCrashRecoveryState {
196
201
  pending: Promise<LspClient | null> | null;
197
202
  /** Start time for the current replacement attempt, when one exists. */
198
203
  attemptStartedAt?: number;
204
+ /** Bounded caught Error.message from a failed replacement startup. */
205
+ failureMessage?: string;
199
206
  }
200
207
 
201
- type ProcessCrashRecoveryOutcome = "attempt" | "success" | "failure" | "exhausted" | "cancelled";
202
-
203
208
  type ProcessCrashDemandResult = {
204
- /** Whether at least one required crashed route supports the operation. */
209
+ /** Whether at least one crashed route supports the operation. */
205
210
  hasSupport: boolean;
211
+ /** Whether replacement work changed the route set and needs a fresh pass. */
212
+ requiresFreshEvidence: boolean;
206
213
  /** Separate route-level result for this explicit process-crash demand. */
207
- processCrashRecovery: ProcessCrashRecoverySummary;
214
+ processCrashRecovery: ProcessCrashRecoveryReport;
208
215
  /** Required routes that remain unavailable after shared recovery settles. */
209
216
  failures: string[];
210
217
  /** In-scope tracked files from required routes that remain unavailable. */
211
218
  failedFiles: string[];
212
219
  };
213
220
 
221
+ type ProcessCrashRecoverySnapshot = {
222
+ readonly key: string;
223
+ /** Mutable state handle used only to await the shared replacement. */
224
+ readonly recovery: ProcessCrashRecoveryState;
225
+ readonly failedClient: LspClient;
226
+ readonly name: string;
227
+ readonly root: string;
228
+ readonly statusReason: NonNullable<ProcessCrashRecoveryState["statusReason"]>;
229
+ readonly files: readonly string[];
230
+ };
231
+
214
232
  type RequiredProcessCrashDemand = {
215
- key: string;
216
- recovery: ProcessCrashRecoveryState;
233
+ snapshot: ProcessCrashRecoverySnapshot;
217
234
  failedFiles: readonly string[];
218
235
  };
219
236
 
220
- type ProcessCrashRecoveryRouteOutcome = "pending" | "recovered" | "failed";
237
+ type ImmediateProcessCrashDemand = {
238
+ route: ProcessCrashRecoveryRouteResult;
239
+ failedFiles: readonly string[];
240
+ reportOnly: boolean;
241
+ };
221
242
 
222
- function summarizeProcessCrashRecoveryRoutes(
223
- outcomes: Iterable<ProcessCrashRecoveryRouteOutcome>,
224
- ): ProcessCrashRecoverySummary {
225
- const summary = emptyProcessCrashRecoverySummary();
226
- for (const outcome of outcomes) {
227
- summary.attemptedRoutes++;
228
- if (outcome === "recovered") summary.recoveredRoutes++;
229
- if (outcome === "failed") summary.failedRoutes++;
230
- }
231
- return summary;
232
- }
243
+ type ProcessCrashDemandSelection = {
244
+ immediate: ImmediateProcessCrashDemand[];
245
+ required: RequiredProcessCrashDemand[];
246
+ };
247
+
248
+ type ProcessCrashRecoveryTelemetryOutcome =
249
+ | "attempt"
250
+ | "success"
251
+ | "failure"
252
+ | "exhausted"
253
+ | "cancelled";
233
254
 
234
255
  // ── LspManager ────────────────────────────────────────────────────────
235
256
  export class LspManager {
@@ -292,20 +313,24 @@ export class LspManager {
292
313
  }
293
314
 
294
315
  /**
295
- * Return the consumed process-crash outcome for a file readiness result.
296
- * The result supports unavailable and non-resolved waits; it is null before
297
- * an attempt is consumed and after successful recovery, so old success is
298
- * not reported as a new outcome. This is a passive read and never starts
299
- * recovery.
316
+ * Return an exhausted process-crash outcome for a file readiness result.
317
+ * This is a passive read and never starts recovery. A crashed or pending
318
+ * route has no final result until an explicit operation observes it.
300
319
  */
301
- getProcessCrashRecoverySummaryForFile(filePath: string): ProcessCrashRecoverySummary | null {
320
+ getProcessCrashRecoveryReportForFile(filePath: string): ProcessCrashRecoveryReport | null {
302
321
  const route = this.resolveFileRoute(resolveSessionPath(this.cwd, filePath));
303
322
  const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
304
- if (!recovery?.attemptConsumed || recovery.statusReason === undefined) return null;
305
-
306
- const outcome: ProcessCrashRecoveryRouteOutcome =
307
- recovery.statusReason === "process-crash-recovery-exhausted" ? "failed" : "pending";
308
- return summarizeProcessCrashRecoveryRoutes([outcome]);
323
+ if (!route || recovery?.statusReason !== "process-crash-recovery-exhausted") return null;
324
+ return buildProcessCrashRecoveryReport(
325
+ [
326
+ {
327
+ name: route.serverName,
328
+ root: route.root,
329
+ outcome: "recovery-exhausted",
330
+ },
331
+ ],
332
+ this.cwd,
333
+ );
309
334
  }
310
335
 
311
336
  /**
@@ -509,6 +534,7 @@ export class LspManager {
509
534
  failedClient: client,
510
535
  files: this.trackedFilesForClient(client),
511
536
  pending: null,
537
+ failureMessage: undefined,
512
538
  });
513
539
  }
514
540
 
@@ -547,66 +573,192 @@ export class LspManager {
547
573
  /**
548
574
  * Recover all crashed routes selected by one evidence-producing operation.
549
575
  * Each route keeps its independent attempt budget and shared replacement.
576
+ * Selection uses one immutable route-fact snapshot so scope and retention
577
+ * facts cannot change halfway through the demand.
550
578
  */
551
579
  private async recoverProcessCrashDemand(
552
- selectFiles: (recovery: ProcessCrashRecoveryState) => readonly string[] | null,
580
+ selectFiles: (snapshot: ProcessCrashRecoverySnapshot) => readonly string[] | null,
553
581
  control?: CodeRequestControl,
582
+ options: { skipWhenNoRetainedFile?: boolean } = {},
554
583
  ): Promise<ProcessCrashDemandResult> {
555
- const required = Array.from(this.processCrashRecoveries.entries()).flatMap(
556
- ([key, recovery]): RequiredProcessCrashDemand[] => {
557
- if (recovery.statusReason === undefined) return [];
558
- const failedFiles = selectFiles(recovery);
559
- return failedFiles === null ? [] : [{ key, recovery, failedFiles }];
560
- },
584
+ const { immediate, required } = this.selectProcessCrashDemandRoutes(
585
+ this.snapshotProcessCrashRoutes(),
586
+ selectFiles,
587
+ options,
561
588
  );
589
+ const hasSupport = immediate.length > 0 || required.length > 0;
562
590
  if (required.length === 0) {
591
+ const failures = immediate
592
+ .filter((entry) => !entry.reportOnly)
593
+ .map((entry) => this.formatProcessCrashDemandFailure(entry.route, entry.route.outcome));
563
594
  return {
564
- hasSupport: false,
565
- processCrashRecovery: emptyProcessCrashRecoverySummary(),
566
- failures: [],
567
- failedFiles: [],
595
+ hasSupport,
596
+ requiresFreshEvidence: false,
597
+ processCrashRecovery: buildProcessCrashRecoveryReport(
598
+ immediate.map(({ route }) => route),
599
+ this.cwd,
600
+ ),
601
+ failures,
602
+ failedFiles: Array.from(
603
+ new Set(
604
+ immediate
605
+ .filter((entry) => !entry.reportOnly)
606
+ .flatMap(({ failedFiles }) => failedFiles),
607
+ ),
608
+ ),
568
609
  };
569
610
  }
570
611
 
571
612
  await Promise.all(
572
- required.map(async ({ key, recovery }) => {
573
- const serverConfig = this.config.servers[recovery.failedClient.name];
613
+ required.map(async ({ snapshot }) => {
614
+ const serverConfig = this.config.servers[snapshot.name];
574
615
  if (!serverConfig) return;
575
616
  await this.acquireProcessCrashReplacement(
576
- recovery,
617
+ snapshot.recovery,
577
618
  {
578
- serverName: recovery.failedClient.name,
619
+ serverName: snapshot.name,
579
620
  serverConfig,
580
- root: recovery.failedClient.root,
581
- key,
621
+ root: snapshot.root,
622
+ key: snapshot.key,
582
623
  },
583
624
  control,
584
625
  );
585
626
  }),
586
627
  );
587
628
 
588
- const failedDemands = required.filter(({ recovery }) => recovery.statusReason !== undefined);
589
- const outcomes = required.map(
590
- ({ recovery }): ProcessCrashRecoveryRouteOutcome =>
591
- recovery.statusReason === undefined ? "recovered" : "failed",
629
+ const observedRequired = required.filter(
630
+ ({ snapshot }) => !this.isProcessCrashDemandCancelled(snapshot),
592
631
  );
632
+ const failedDemands = observedRequired.filter(
633
+ ({ snapshot }) => snapshot.recovery.statusReason !== undefined,
634
+ );
635
+ const recoveredRoutes = observedRequired.map(
636
+ ({ snapshot }): ProcessCrashRecoveryRouteResult => ({
637
+ name: snapshot.name,
638
+ root: snapshot.root,
639
+ outcome: snapshot.recovery.statusReason === undefined ? "recovered" : "recovery-failed",
640
+ ...(snapshot.recovery.statusReason === undefined || !snapshot.recovery.failureMessage
641
+ ? {}
642
+ : { failureMessage: snapshot.recovery.failureMessage }),
643
+ }),
644
+ );
645
+ const failedImmediate = immediate.filter((entry) => !entry.reportOnly);
593
646
  return {
594
- hasSupport: true,
595
- processCrashRecovery: summarizeProcessCrashRecoveryRoutes(outcomes),
596
- failures: failedDemands.map(({ recovery }) => this.formatProcessCrashDemandFailure(recovery)),
597
- failedFiles: Array.from(new Set(failedDemands.flatMap(({ failedFiles }) => failedFiles))),
647
+ hasSupport,
648
+ requiresFreshEvidence: observedRequired.length > 0,
649
+ processCrashRecovery: buildProcessCrashRecoveryReport(
650
+ [...immediate.map(({ route }) => route), ...recoveredRoutes],
651
+ this.cwd,
652
+ ),
653
+ failures: [
654
+ ...failedImmediate.map((entry) =>
655
+ this.formatProcessCrashDemandFailure(entry.route, entry.route.outcome),
656
+ ),
657
+ ...failedDemands.map(({ snapshot }) =>
658
+ this.formatProcessCrashDemandFailure(snapshot.recovery, "recovery-failed"),
659
+ ),
660
+ ],
661
+ failedFiles: Array.from(
662
+ new Set([
663
+ ...failedImmediate.flatMap(({ failedFiles }) => failedFiles),
664
+ ...failedDemands.flatMap(({ failedFiles }) => failedFiles),
665
+ ]),
666
+ ),
598
667
  };
599
668
  }
600
669
 
601
- private formatProcessCrashDemandFailure(recovery: ProcessCrashRecoveryState): string {
602
- const root = (path.relative(this.cwd, recovery.failedClient.root) || ".").replaceAll("\\", "/");
670
+ private isProcessCrashDemandCancelled(snapshot: ProcessCrashRecoverySnapshot): boolean {
671
+ return (
672
+ snapshot.recovery.statusReason !== undefined &&
673
+ (this.shuttingDown || this.processCrashRecoveries.get(snapshot.key) !== snapshot.recovery)
674
+ );
675
+ }
676
+
677
+ private selectProcessCrashDemandRoutes(
678
+ snapshots: readonly ProcessCrashRecoverySnapshot[],
679
+ selectFiles: (snapshot: ProcessCrashRecoverySnapshot) => readonly string[] | null,
680
+ options: { skipWhenNoRetainedFile?: boolean },
681
+ ): ProcessCrashDemandSelection {
682
+ const immediate: ImmediateProcessCrashDemand[] = [];
683
+ const required: RequiredProcessCrashDemand[] = [];
684
+
685
+ for (const snapshot of snapshots) {
686
+ const failedFiles = selectFiles(snapshot);
687
+ if (failedFiles === null) continue;
688
+
689
+ const exhausted = snapshot.statusReason === "process-crash-recovery-exhausted";
690
+ const noRetainedFile = failedFiles.length === 0;
691
+ if (exhausted) {
692
+ immediate.push({
693
+ route: {
694
+ name: snapshot.name,
695
+ root: snapshot.root,
696
+ outcome: "recovery-exhausted",
697
+ },
698
+ failedFiles,
699
+ reportOnly: options.skipWhenNoRetainedFile === true && noRetainedFile,
700
+ });
701
+ continue;
702
+ }
703
+ if (
704
+ options.skipWhenNoRetainedFile === true &&
705
+ noRetainedFile &&
706
+ snapshot.statusReason === "process-crashed"
707
+ ) {
708
+ immediate.push({
709
+ route: {
710
+ name: snapshot.name,
711
+ root: snapshot.root,
712
+ outcome: "skipped-no-retained-file",
713
+ },
714
+ failedFiles,
715
+ reportOnly: true,
716
+ });
717
+ continue;
718
+ }
719
+ required.push({ snapshot, failedFiles });
720
+ }
721
+ return { immediate, required };
722
+ }
723
+
724
+ private snapshotProcessCrashRoutes(): ProcessCrashRecoverySnapshot[] {
725
+ return Array.from(this.processCrashRecoveries.entries()).flatMap(([key, recovery]) => {
726
+ if (recovery.statusReason === undefined) return [];
727
+ return [
728
+ {
729
+ key,
730
+ recovery,
731
+ failedClient: recovery.failedClient,
732
+ name: recovery.failedClient.name,
733
+ root: recovery.failedClient.root,
734
+ statusReason: recovery.statusReason,
735
+ files: [...recovery.files],
736
+ },
737
+ ];
738
+ });
739
+ }
740
+
741
+ private formatProcessCrashDemandFailure(
742
+ recovery: ProcessCrashRecoveryState | ProcessCrashRecoveryRouteResult,
743
+ outcome?: ProcessCrashRecoveryRouteResult["outcome"],
744
+ ): string {
745
+ const isRouteResult = "outcome" in recovery;
746
+ const name = isRouteResult ? recovery.name : recovery.failedClient.name;
747
+ const root = isRouteResult
748
+ ? recovery.root
749
+ : (path.relative(this.cwd, recovery.failedClient.root) || ".").replaceAll("\\", "/");
750
+ const routeOutcome =
751
+ outcome ??
752
+ (isRouteResult
753
+ ? recovery.outcome
754
+ : recovery.statusReason === "process-crash-recovery-exhausted"
755
+ ? "recovery-exhausted"
756
+ : "recovery-failed");
603
757
  const reason =
604
- recovery.statusReason === "process-crash-recovery-exhausted"
758
+ routeOutcome === "recovery-exhausted"
605
759
  ? "process recovery exhausted; reload required"
606
- : recovery.statusReason === "process-crash-recovery-pending"
607
- ? "process recovery in progress"
608
- : "process crashed";
609
- return `${recovery.failedClient.name} @ ${root} is unavailable — ${reason}.`;
760
+ : "process recovery failed";
761
+ return `${name} @ ${root} is unavailable — ${reason}.`;
610
762
  }
611
763
 
612
764
  private startProcessCrashRecovery(
@@ -670,6 +822,7 @@ export class LspManager {
670
822
 
671
823
  this.unavailable.delete(key);
672
824
  recovery.statusReason = undefined;
825
+ recovery.failureMessage = undefined;
673
826
  this.publishLifecycle("recovery");
674
827
  this.restoreTrackedDocuments(started, recovery.files);
675
828
  this.recordProcessCrashRecoveryEvent(
@@ -716,7 +869,7 @@ export class LspManager {
716
869
  // LspClient's initialize request already has the 30-second transport
717
870
  // bound. Do not apply the 5-second diagnostic-restart bound here.
718
871
  await replacement.start();
719
- } catch {
872
+ } catch (error) {
720
873
  if (this.isProcessCrashRecoveryCancelled(recovery, route.key)) {
721
874
  await replacement.forceKill().catch(() => {});
722
875
  this.recordProcessCrashRecoveryEvent(
@@ -728,7 +881,7 @@ export class LspManager {
728
881
  return null;
729
882
  }
730
883
  if (recovery.statusReason !== "process-crash-recovery-exhausted") {
731
- this.markProcessCrashRecoveryFailed(recovery, route.key, replacement);
884
+ this.markProcessCrashRecoveryFailed(recovery, route.key, replacement, error);
732
885
  this.recordProcessCrashRecoveryEvent(
733
886
  route.serverName,
734
887
  route.root,
@@ -776,8 +929,13 @@ export class LspManager {
776
929
  recovery: ProcessCrashRecoveryState,
777
930
  key: string,
778
931
  replacement: LspClient,
932
+ error: unknown,
779
933
  ): void {
780
934
  recovery.statusReason = "process-crash-recovery-exhausted";
935
+ recovery.failureMessage =
936
+ error instanceof Error && error.message
937
+ ? boundProcessCrashFailureMessage(error.message)
938
+ : undefined;
781
939
  this.unavailable.set(key, "start-failed");
782
940
  if (this.clients.get(key) === replacement) this.clients.delete(key);
783
941
  // A failed initialize may still leave a child process alive. The
@@ -789,7 +947,7 @@ export class LspManager {
789
947
  private recordProcessCrashRecoveryEvent(
790
948
  serverName: string,
791
949
  root: string,
792
- outcome: ProcessCrashRecoveryOutcome,
950
+ outcome: ProcessCrashRecoveryTelemetryOutcome,
793
951
  startedAt?: number,
794
952
  ): void {
795
953
  try {
@@ -1065,33 +1223,103 @@ export class LspManager {
1065
1223
  async waitUntilFileReady(
1066
1224
  filePath: string,
1067
1225
  control?: CodeRequestControl,
1068
- onProcessCrashRecovery?: (summary: ProcessCrashRecoverySummary) => void,
1226
+ onProcessCrashRecovery?: (report: ProcessCrashRecoveryReport) => void,
1069
1227
  ): Promise<FileReadinessResult> {
1070
1228
  throwIfCodeRequestInterrupted(control);
1071
1229
  const resolvedPath = resolveSessionPath(this.cwd, filePath);
1072
1230
  const route = this.resolveFileRoute(resolvedPath);
1073
1231
  const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
1074
- const processCrashRecoveryRequested =
1075
- recovery !== undefined && (recovery.pending !== null || recovery.statusReason !== undefined);
1076
- if (processCrashRecoveryRequested) {
1077
- onProcessCrashRecovery?.(summarizeProcessCrashRecoveryRoutes(["pending"]));
1078
- }
1232
+ const statusAtStart = recovery?.statusReason;
1233
+ const processCrashRecoveryRequested = statusAtStart !== undefined;
1079
1234
  const client = await this.getClientForFile(resolvedPath, {
1080
1235
  recoverProcessCrash: true,
1081
1236
  control,
1082
1237
  });
1083
- const processCrashRecovery = processCrashRecoveryRequested
1084
- ? summarizeProcessCrashRecoveryRoutes([client ? "recovered" : "failed"])
1085
- : emptyProcessCrashRecoverySummary();
1238
+ const processCrashRecovery = this.buildFileProcessCrashRecoveryReport(
1239
+ route,
1240
+ recovery,
1241
+ statusAtStart,
1242
+ client,
1243
+ );
1244
+ if (processCrashRecoveryRequested) onProcessCrashRecovery?.(processCrashRecovery);
1086
1245
  if (!client) return { client: null, processCrashRecovery };
1087
- // The caller's wait stops on cancellation even though the shared
1088
- // readiness state keeps its own lifecycle.
1089
- await client.getReady(control);
1090
- await this.warmSemanticProject(client, resolvedPath, true, control);
1091
- throwIfCodeRequestInterrupted(control);
1246
+ try {
1247
+ await client.getReady(control);
1248
+ await this.warmSemanticProject(client, resolvedPath, true, control);
1249
+ throwIfCodeRequestInterrupted(control);
1250
+ } catch (error) {
1251
+ const failedRecovery = this.buildFileProcessCrashFailureReport(
1252
+ route,
1253
+ recovery,
1254
+ statusAtStart,
1255
+ error,
1256
+ );
1257
+ if (failedRecovery) {
1258
+ onProcessCrashRecovery?.(failedRecovery);
1259
+ return { client: null, processCrashRecovery: failedRecovery };
1260
+ }
1261
+ throw error;
1262
+ }
1092
1263
  return { client, processCrashRecovery };
1093
1264
  }
1094
1265
 
1266
+ private buildFileProcessCrashRecoveryReport(
1267
+ route: FileRoute | null,
1268
+ recovery: ProcessCrashRecoveryState | undefined,
1269
+ statusAtStart: ProcessCrashRecoveryState["statusReason"],
1270
+ client: LspClient | null,
1271
+ ): ProcessCrashRecoveryReport {
1272
+ if (statusAtStart === undefined) return emptyProcessCrashRecoveryReport();
1273
+ return buildProcessCrashRecoveryReport(
1274
+ [
1275
+ {
1276
+ name: route?.serverName ?? "unknown",
1277
+ root: route?.root ?? this.cwd,
1278
+ outcome:
1279
+ statusAtStart === "process-crash-recovery-exhausted"
1280
+ ? "recovery-exhausted"
1281
+ : client
1282
+ ? "recovered"
1283
+ : "recovery-failed",
1284
+ ...(client || !recovery?.failureMessage
1285
+ ? {}
1286
+ : { failureMessage: recovery.failureMessage }),
1287
+ },
1288
+ ],
1289
+ this.cwd,
1290
+ );
1291
+ }
1292
+
1293
+ private buildFileProcessCrashFailureReport(
1294
+ route: FileRoute | null,
1295
+ recovery: ProcessCrashRecoveryState | undefined,
1296
+ statusAtStart: ProcessCrashRecoveryState["statusReason"],
1297
+ error: unknown,
1298
+ ): ProcessCrashRecoveryReport | null {
1299
+ if (
1300
+ statusAtStart === undefined ||
1301
+ statusAtStart === "process-crash-recovery-exhausted" ||
1302
+ recovery?.statusReason !== "process-crash-recovery-exhausted"
1303
+ ) {
1304
+ return null;
1305
+ }
1306
+ return buildProcessCrashRecoveryReport(
1307
+ [
1308
+ {
1309
+ name: route?.serverName ?? "unknown",
1310
+ root: route?.root ?? this.cwd,
1311
+ outcome: "recovery-failed",
1312
+ ...(error instanceof Error && error.message
1313
+ ? { failureMessage: boundProcessCrashFailureMessage(error.message) }
1314
+ : recovery.failureMessage
1315
+ ? { failureMessage: recovery.failureMessage }
1316
+ : {}),
1317
+ },
1318
+ ],
1319
+ this.cwd,
1320
+ );
1321
+ }
1322
+
1095
1323
  /**
1096
1324
  * Wait until one started client is query-ready, then warm its project.
1097
1325
  * Returns the number of concrete clients that are ready after the warm-up.
@@ -1245,18 +1473,26 @@ export class LspManager {
1245
1473
  processCrashDemand?: { scopes?: readonly string[] };
1246
1474
  control?: CodeRequestControl;
1247
1475
  }): Promise<WorkspaceRecoveryResult> {
1248
- const demand = options?.processCrashDemand
1249
- ? await this.recoverProcessCrashDemand((recovery) => {
1250
- const files = this.processCrashDiagnosticFiles(recovery, options.processCrashDemand);
1251
- return files.length > 0 ? files : null;
1252
- }, options.control)
1476
+ const processCrashDemand = options?.processCrashDemand;
1477
+ const demand = processCrashDemand
1478
+ ? await this.recoverProcessCrashDemand(
1479
+ (snapshot) => {
1480
+ if (!this.processCrashDiagnosticRouteInScope(snapshot, processCrashDemand)) {
1481
+ return null;
1482
+ }
1483
+ return this.processCrashDiagnosticFiles(snapshot, processCrashDemand);
1484
+ },
1485
+ options?.control,
1486
+ { skipWhenNoRetainedFile: true },
1487
+ )
1253
1488
  : {
1254
1489
  hasSupport: false,
1255
- processCrashRecovery: emptyProcessCrashRecoverySummary(),
1490
+ requiresFreshEvidence: false,
1491
+ processCrashRecovery: emptyProcessCrashRecoveryReport(),
1256
1492
  failures: [],
1257
1493
  failedFiles: [],
1258
1494
  };
1259
- const recoveryOptions = demand.hasSupport
1495
+ const recoveryOptions = demand.requiresFreshEvidence
1260
1496
  ? { ...options, initialEvidence: undefined }
1261
1497
  : options;
1262
1498
  const result = await recoverWorkspaceDiagnosticsImpl(this, recoveryOptions);
@@ -1266,21 +1502,34 @@ export class LspManager {
1266
1502
  );
1267
1503
  }
1268
1504
 
1505
+ private processCrashDiagnosticRouteInScope(
1506
+ snapshot: ProcessCrashRecoverySnapshot,
1507
+ demand: { scopes?: readonly string[] },
1508
+ ): boolean {
1509
+ return this.processCrashDiagnosticScopes(demand).some(
1510
+ (scope) =>
1511
+ projectRoots.isWithinOrEqual(scope, snapshot.root) ||
1512
+ projectRoots.isWithinOrEqual(snapshot.root, scope),
1513
+ );
1514
+ }
1515
+
1269
1516
  private processCrashDiagnosticFiles(
1270
- recovery: ProcessCrashRecoveryState,
1271
- demand: { scopes?: readonly string[] } | undefined,
1517
+ snapshot: ProcessCrashRecoverySnapshot,
1518
+ demand: { scopes?: readonly string[] },
1272
1519
  ): string[] {
1273
- if (!demand) return [];
1274
- const scopes = demand.scopes?.map((scope) => resolveSessionPath(this.cwd, scope));
1275
- return recovery.files.filter(
1520
+ const scopes = this.processCrashDiagnosticScopes(demand);
1521
+ return snapshot.files.filter(
1276
1522
  (file) =>
1277
1523
  this.isDiagnosticFile(file) &&
1278
- (!scopes ||
1279
- scopes.length === 0 ||
1280
- scopes.some((scope) => projectRoots.isWithinOrEqual(scope, file))),
1524
+ scopes.some((scope) => projectRoots.isWithinOrEqual(scope, file)),
1281
1525
  );
1282
1526
  }
1283
1527
 
1528
+ private processCrashDiagnosticScopes(demand: { scopes?: readonly string[] }): string[] {
1529
+ const scopes = demand.scopes?.map((scope) => resolveSessionPath(this.cwd, scope));
1530
+ return scopes && scopes.length > 0 ? scopes : [this.cwd];
1531
+ }
1532
+
1284
1533
  private addProcessCrashDiagnosticEvidence(
1285
1534
  result: WorkspaceRecoveryResult,
1286
1535
  demand: ProcessCrashDemandResult,
@@ -1665,9 +1914,9 @@ export class LspManager {
1665
1914
  );
1666
1915
  }
1667
1916
  const demand = await this.recoverProcessCrashDemand(
1668
- (recovery) =>
1669
- recovery.failedClient.serverCapabilities?.workspaceSymbolProvider &&
1670
- routeMatches(recovery.failedClient)
1917
+ (snapshot) =>
1918
+ snapshot.failedClient.serverCapabilities?.workspaceSymbolProvider &&
1919
+ routeMatches(snapshot.failedClient)
1671
1920
  ? []
1672
1921
  : null,
1673
1922
  control,