@mrclrchtr/supi-code-intelligence 6.4.0 → 7.0.1

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 (129) hide show
  1. package/README.md +164 -82
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  4. package/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  5. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  6. package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  7. package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  9. package/node_modules/@mrclrchtr/supi-lsp/README.md +35 -13
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +3 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +3 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +1 -1
  20. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  21. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +6 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +43 -35
  23. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +83 -17
  24. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +108 -92
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +44 -82
  26. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +3 -4
  27. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +9 -68
  28. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +45 -58
  29. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +230 -331
  30. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +201 -1
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +129 -70
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-typescript.ts +395 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +8 -12
  34. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +685 -183
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +2 -2
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +60 -60
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-request-enrollment.ts +75 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-barrier.ts +443 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-errors.ts +30 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-file.ts +15 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +290 -37
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +72 -16
  43. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +1 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +30 -4
  45. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +1 -1
  46. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +10 -4
  47. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +50 -1
  48. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +11 -7
  49. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +608 -45
  50. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +3 -1
  51. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +37 -4
  52. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +31 -11
  53. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +19 -3
  54. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  55. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  56. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  57. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  58. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  59. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  60. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  61. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  62. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +2 -0
  63. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callee-display-name.ts +165 -0
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +67 -9
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +2 -0
  66. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  67. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  68. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  69. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  70. package/node_modules/yaml/package.json +1 -1
  71. package/package.json +5 -5
  72. package/src/analysis/health/diagnostics.ts +1 -2
  73. package/src/analysis/health/file-scope.ts +4 -1
  74. package/src/analysis/health/server-status.ts +1 -1
  75. package/src/analysis/relations/callees.ts +17 -8
  76. package/src/analysis/relations/implementations.ts +1 -2
  77. package/src/analysis/relations/references.ts +8 -4
  78. package/src/analysis/relations/types.ts +6 -30
  79. package/src/analysis/target/anchored.ts +9 -1
  80. package/src/analysis/target/symbol.ts +29 -11
  81. package/src/analysis/target/types.ts +14 -6
  82. package/src/api.ts +2 -0
  83. package/src/session/graph/collect.ts +18 -3
  84. package/src/session/health-refresh.ts +12 -1
  85. package/src/session/health-types.ts +18 -6
  86. package/src/session/health-workflow.ts +21 -19
  87. package/src/session/orientation-types.ts +5 -6
  88. package/src/session/orientation-workflow.ts +10 -2
  89. package/src/session/target-workflow.ts +9 -6
  90. package/src/substrate/lsp/source-tracking.ts +11 -6
  91. package/src/tool/code_find/guidance.ts +2 -2
  92. package/src/tool/code_find/spec.ts +5 -4
  93. package/src/tool/code_find/tui.ts +7 -1
  94. package/src/tool/code_graph/details.ts +32 -0
  95. package/src/tool/code_graph/display.ts +54 -0
  96. package/src/tool/code_graph/format.ts +60 -0
  97. package/src/tool/code_graph/guidance.ts +1 -1
  98. package/src/tool/code_graph/markdown.ts +76 -198
  99. package/src/tool/code_graph/read-next.ts +65 -0
  100. package/src/tool/code_graph/result.ts +118 -83
  101. package/src/tool/code_graph/tui.ts +216 -54
  102. package/src/tool/code_health/guidance.ts +2 -4
  103. package/src/tool/code_health/markdown.ts +75 -58
  104. package/src/tool/code_health/refresh-outcome.ts +41 -1
  105. package/src/tool/code_health/refresh-status.ts +114 -48
  106. package/src/tool/code_health/scope.ts +78 -0
  107. package/src/tool/code_health/spec.ts +1 -1
  108. package/src/tool/code_health/tui.ts +25 -38
  109. package/src/tool/code_inspect/tui.ts +7 -1
  110. package/src/tool/code_orientation/result.ts +31 -8
  111. package/src/tool/code_orientation/tui.ts +26 -5
  112. package/src/tool/code_refactor_apply/guidance.ts +1 -1
  113. package/src/tool/code_refactor_apply/tui.ts +7 -3
  114. package/src/tool/code_refactor_plan/guidance.ts +1 -1
  115. package/src/tool/code_refactor_plan/tui.ts +7 -1
  116. package/src/tool/code_resolve/guidance.ts +2 -4
  117. package/src/tool/code_resolve/result.ts +13 -10
  118. package/src/tool/code_resolve/tui.ts +18 -1
  119. package/src/tool/infra/truncate.ts +39 -13
  120. package/src/tool/result/candidate-row.ts +27 -0
  121. package/src/tool/result/errors.ts +21 -6
  122. package/src/tool/result/types.ts +7 -3
  123. package/src/tool/schemas.ts +5 -0
  124. package/src/types/execution.ts +2 -0
  125. package/src/types/index.ts +2 -0
  126. package/src/ui/tui/candidate-legacy-body.ts +69 -0
  127. package/src/ui/tui/candidate-selection.ts +366 -0
  128. package/src/ui/tui/common.ts +26 -31
  129. package/src/ui/tui/execution-error.ts +92 -0
@@ -33,6 +33,7 @@ import {
33
33
  invalidateTsconfigCacheForConfig,
34
34
  invalidateTsconfigCacheForConfigDir,
35
35
  isProjectConfigFileName,
36
+ isTsconfigApplicableFile,
36
37
  } from "../config/tsconfig-scope.ts";
37
38
  import type {
38
39
  DetectedProjectServer,
@@ -71,11 +72,14 @@ import {
71
72
  import { raceRequestControl } from "../session/readiness.ts";
72
73
  import {
73
74
  emptyProcessCrashRecoveryReport,
75
+ emptyStartupRetryReport,
74
76
  type ProcessCrashRecoveryReport,
75
77
  type ScopeDecisionEntry,
76
78
  type ScopeDecisionSummary,
79
+ type StartupRetryReport,
77
80
  type WorkspaceDiagnosticReport,
78
81
  } from "../session/runtime-diagnostics.ts";
82
+ import { scanProjectCapabilities } from "../session/scanner.ts";
79
83
  import {
80
84
  type BulkTrackFileOutcome,
81
85
  type BulkTrackFilesResult,
@@ -107,7 +111,9 @@ import { clientKey, rememberKnownRoot, resolveRootForFile } from "./manager-help
107
111
  import {
108
112
  boundProcessCrashFailureMessage,
109
113
  buildProcessCrashRecoveryReport,
114
+ buildStartupRetryReport,
110
115
  type ProcessCrashRecoveryRouteResult,
116
+ type StartupRetryRouteResult,
111
117
  } from "./manager-process-crash-report.ts";
112
118
  import { buildProjectServerInfo } from "./manager-project-info.ts";
113
119
  import type {
@@ -197,6 +203,45 @@ type FileClientRequestOptions = {
197
203
  interface FileReadinessResult {
198
204
  client: LspClient | null;
199
205
  processCrashRecovery: ProcessCrashRecoveryReport;
206
+ startupRetry: StartupRetryReport;
207
+ }
208
+
209
+ interface FileReadinessCallbacks {
210
+ readonly onProcessCrashRecovery?: (report: ProcessCrashRecoveryReport) => void;
211
+ readonly onStartupRetry?: (report: StartupRetryReport) => void;
212
+ }
213
+
214
+ interface FailedStartState {
215
+ readonly serverName: string;
216
+ readonly root: string;
217
+ readonly failureMessage?: string;
218
+ }
219
+
220
+ interface ExplicitRouteCandidate {
221
+ readonly route: FileRoute;
222
+ readonly processCrashRecovery?: ProcessCrashRecoveryState;
223
+ readonly startupFailure?: FailedStartState;
224
+ }
225
+
226
+ interface ExplicitRouteAttemptResult {
227
+ readonly candidate: ExplicitRouteCandidate;
228
+ readonly kind: "startup" | "discovery" | "process-crash" | "none";
229
+ readonly client: LspClient | null;
230
+ readonly attempted: boolean;
231
+ readonly failureMessage?: string;
232
+ }
233
+
234
+ interface ExplicitRouteAttemptLease {
235
+ readonly promise: Promise<ExplicitRouteAttemptResult>;
236
+ users: number;
237
+ settled: boolean;
238
+ }
239
+
240
+ interface ExplicitRouteClassification {
241
+ startupResult?: StartupRetryRouteResult;
242
+ processResult?: ProcessCrashRecoveryRouteResult;
243
+ failure?: string;
244
+ failedFiles: string[];
200
245
  }
201
246
 
202
247
  interface ProcessCrashRecoveryState {
@@ -223,6 +268,8 @@ type ProcessCrashDemandResult = {
223
268
  requiresFreshEvidence: boolean;
224
269
  /** Separate route-level result for this explicit process-crash demand. */
225
270
  processCrashRecovery: ProcessCrashRecoveryReport;
271
+ /** Separate result for explicit initial-start retries. */
272
+ startupRetry: StartupRetryReport;
226
273
  /** Required routes that remain unavailable after shared recovery settles. */
227
274
  failures: string[];
228
275
  /** In-scope tracked files from required routes that remain unavailable. */
@@ -269,6 +316,10 @@ export class LspManager {
269
316
  private clients = new Map<string, LspClient>();
270
317
  /** Per-root startup failures keyed by "serverName:root" */
271
318
  private unavailable = new Map<string, UnavailableReason>();
319
+ /** Initial-start failures that an explicit health refresh may retry. */
320
+ private failedStarts = new Map<string, FailedStartState>();
321
+ /** Shared explicit route attempts for concurrent health refresh calls. */
322
+ private explicitRouteAttempts = new Map<string, ExplicitRouteAttemptLease>();
272
323
  /** Memoized per-command availability of LSP server binaries on PATH */
273
324
  private commandAvailability = new Map<string, boolean>();
274
325
  /** Guards against concurrent client creation for the same server:root key. */
@@ -446,13 +497,16 @@ export class LspManager {
446
497
  serverName: string,
447
498
  root: string,
448
499
  control?: CodeRequestControl,
500
+ options: { retryFailedStart?: boolean } = {},
449
501
  ): Promise<LspClient | null> {
450
502
  throwIfCodeRequestInterrupted(control);
451
503
  if (this.shuttingDown) return null;
452
504
  const serverConfig = this.config.servers[serverName];
453
505
  if (!serverConfig) return null;
454
506
  const key = clientKey(serverName, root);
455
- if (this.getUnavailableReason(key, serverConfig.command)) return null;
507
+ const retryFailedStart = options.retryFailedStart === true;
508
+ if (retryFailedStart) this.unavailable.delete(key);
509
+ if (!retryFailedStart && this.getUnavailableReason(key, serverConfig.command)) return null;
456
510
 
457
511
  // Return existing client
458
512
  const existing = this.clients.get(key);
@@ -468,10 +522,16 @@ export class LspManager {
468
522
  if (existing && existing.status === "error") {
469
523
  this.clients.delete(key);
470
524
  this.unavailable.set(key, "runtime-error");
525
+ this.failedStarts.set(key, {
526
+ serverName,
527
+ root,
528
+ failureMessage: "The LSP client is not running.",
529
+ });
471
530
  this.clearWarmedWorkspaceSymbolProjects(existing.name, existing.root);
472
531
  this.clearWarmedSemanticProjects(existing.name, existing.root);
473
532
  this.clearPendingWarmProbes(existing.name, existing.root);
474
- return null;
533
+ if (!retryFailedStart) return null;
534
+ this.unavailable.delete(key);
475
535
  }
476
536
 
477
537
  // Deduplicate concurrent starts for the same server:root pair.
@@ -592,18 +652,318 @@ export class LspManager {
592
652
  recovery: ProcessCrashRecoveryState,
593
653
  route: FileRoute,
594
654
  control?: CodeRequestControl,
655
+ options: { allowExhausted?: boolean } = {},
595
656
  ): Promise<LspClient | null> {
596
657
  if (recovery.pending) return this.waitForProcessCrashRecovery(recovery, control);
597
- if (
598
- recovery.statusReason === "process-crashed" &&
599
- !recovery.attemptConsumed &&
600
- !this.shuttingDown
601
- ) {
658
+ const automaticAttempt =
659
+ recovery.statusReason === "process-crashed" && !recovery.attemptConsumed;
660
+ const explicitAttempt =
661
+ options.allowExhausted === true &&
662
+ recovery.statusReason === "process-crash-recovery-exhausted";
663
+ if ((automaticAttempt || explicitAttempt) && !this.shuttingDown) {
602
664
  return this.startProcessCrashRecovery(recovery, route, control);
603
665
  }
604
666
  return Promise.resolve(null);
605
667
  }
606
668
 
669
+ /** Retry failed routes selected by one explicit health refresh. */
670
+ private async recoverExplicitRouteDemand(
671
+ demand: { scopes?: readonly string[] },
672
+ control?: CodeRequestControl,
673
+ ): Promise<ProcessCrashDemandResult> {
674
+ throwIfCodeRequestInterrupted(control);
675
+ const scopes = this.processCrashDiagnosticScopes(demand);
676
+ const candidates = this.selectExplicitRouteCandidates(scopes);
677
+ return this.withExplicitRouteAttempts(candidates, control, async (results) => {
678
+ throwIfCodeRequestInterrupted(control);
679
+ for (const result of results) {
680
+ if (result.kind === "process-crash" && result.attempted) {
681
+ this.recoveryRestartEpochs.set(result.candidate.route.key, this.invalidationEpoch);
682
+ }
683
+ }
684
+ this.observeExplicitReadyRoutes(scopes);
685
+ const classified = this.classifyExplicitRouteAttempts(results, scopes);
686
+ return {
687
+ hasSupport: candidates.length > 0,
688
+ requiresFreshEvidence: results.some((result) => result.attempted),
689
+ startupRetry: buildStartupRetryReport(classified.startupResults, this.cwd),
690
+ processCrashRecovery: buildProcessCrashRecoveryReport(classified.processResults, this.cwd),
691
+ failures: classified.failures,
692
+ failedFiles: Array.from(new Set(classified.failedFiles)),
693
+ };
694
+ });
695
+ }
696
+
697
+ private classifyExplicitRouteAttempts(
698
+ results: readonly ExplicitRouteAttemptResult[],
699
+ scopes: readonly string[],
700
+ ): {
701
+ startupResults: StartupRetryRouteResult[];
702
+ processResults: ProcessCrashRecoveryRouteResult[];
703
+ failures: string[];
704
+ failedFiles: string[];
705
+ } {
706
+ const classified: {
707
+ startupResults: StartupRetryRouteResult[];
708
+ processResults: ProcessCrashRecoveryRouteResult[];
709
+ failures: string[];
710
+ failedFiles: string[];
711
+ } = {
712
+ startupResults: [],
713
+ processResults: [],
714
+ failures: [],
715
+ failedFiles: [],
716
+ } satisfies {
717
+ startupResults: StartupRetryRouteResult[];
718
+ processResults: ProcessCrashRecoveryRouteResult[];
719
+ failures: string[];
720
+ failedFiles: string[];
721
+ };
722
+ for (const result of results) {
723
+ const outcome = this.classifyExplicitRouteAttempt(result, scopes);
724
+ if (outcome.startupResult) classified.startupResults.push(outcome.startupResult);
725
+ if (outcome.processResult) classified.processResults.push(outcome.processResult);
726
+ if (outcome.failure) classified.failures.push(outcome.failure);
727
+ classified.failedFiles.push(...outcome.failedFiles);
728
+ }
729
+ return classified;
730
+ }
731
+
732
+ private classifyExplicitRouteAttempt(
733
+ result: ExplicitRouteAttemptResult,
734
+ scopes: readonly string[],
735
+ ): ExplicitRouteClassification {
736
+ if (!result.attempted) return { failedFiles: [] };
737
+ const recovered = result.client?.status === "running";
738
+ const startup = this.classifyExplicitStartupAttempt(result, recovered);
739
+ if (startup) return startup;
740
+ if (result.kind !== "process-crash") return { failedFiles: [] };
741
+ return this.classifyExplicitProcessAttempt(result, scopes, recovered);
742
+ }
743
+
744
+ private classifyExplicitStartupAttempt(
745
+ result: ExplicitRouteAttemptResult,
746
+ recovered: boolean,
747
+ ): ExplicitRouteClassification | null {
748
+ if (result.kind !== "startup" && !(result.kind === "discovery" && !recovered)) {
749
+ return null;
750
+ }
751
+ const { route } = result.candidate;
752
+ return {
753
+ startupResult: {
754
+ name: route.serverName,
755
+ root: route.root,
756
+ outcome: recovered ? "recovered" : "retry-failed",
757
+ ...(recovered || !result.failureMessage ? {} : { failureMessage: result.failureMessage }),
758
+ },
759
+ ...(recovered
760
+ ? {}
761
+ : { failure: this.formatStartupRetryFailure(route, result.failureMessage) }),
762
+ failedFiles: [],
763
+ };
764
+ }
765
+
766
+ private classifyExplicitProcessAttempt(
767
+ result: ExplicitRouteAttemptResult,
768
+ scopes: readonly string[],
769
+ recovered: boolean,
770
+ ): ExplicitRouteClassification {
771
+ const { route } = result.candidate;
772
+ const processResult: ProcessCrashRecoveryRouteResult = {
773
+ name: route.serverName,
774
+ root: route.root,
775
+ outcome: recovered ? "recovered" : "recovery-failed",
776
+ ...(!recovered && result.failureMessage ? { failureMessage: result.failureMessage } : {}),
777
+ };
778
+ return {
779
+ processResult,
780
+ ...(recovered
781
+ ? {}
782
+ : {
783
+ failure: this.formatProcessCrashDemandFailure(
784
+ { name: route.serverName, root: route.root, outcome: "recovery-failed" },
785
+ "recovery-failed",
786
+ ),
787
+ }),
788
+ failedFiles: recovered ? [] : this.explicitFailedFiles(result.candidate, scopes),
789
+ };
790
+ }
791
+
792
+ private selectExplicitRouteCandidates(scopes: readonly string[]): ExplicitRouteCandidate[] {
793
+ const candidates = new Map<string, ExplicitRouteCandidate>();
794
+ const add = (serverName: string, root: string): void => {
795
+ if (!this.automaticPathPolicy.isEligible(root, "directory")) return;
796
+ const serverConfig = this.config.servers[serverName];
797
+ if (!serverConfig) return;
798
+ const route = { serverName, serverConfig, root, key: clientKey(serverName, root) };
799
+ if (!this.explicitRouteInScope(root, scopes)) return;
800
+ const recovery = this.processCrashRecoveries.get(route.key);
801
+ const startupFailure = this.failedStarts.get(route.key);
802
+ if (this.clients.get(route.key)?.status === "running" && !recovery?.statusReason) {
803
+ candidates.set(route.key, { route });
804
+ return;
805
+ }
806
+ candidates.set(route.key, {
807
+ route,
808
+ ...(recovery ? { processCrashRecovery: recovery } : {}),
809
+ ...(startupFailure ? { startupFailure } : {}),
810
+ });
811
+ };
812
+
813
+ const exactFile =
814
+ scopes.length === 1 && this.inspectSourceFile(scopes[0]) === "regular" ? scopes[0] : null;
815
+ const exactRoute = exactFile ? this.resolveFileRoute(exactFile) : null;
816
+ if (exactRoute) {
817
+ add(exactRoute.serverName, exactRoute.root);
818
+ return Array.from(candidates.values());
819
+ }
820
+
821
+ const detected = scanProjectCapabilities(
822
+ this.config,
823
+ this.cwd,
824
+ undefined,
825
+ this.automaticPathPolicy,
826
+ );
827
+ for (const entry of detected) {
828
+ rememberKnownRoot(this.knownRoots, entry.name, entry.root);
829
+ add(entry.name, entry.root);
830
+ }
831
+ for (const failure of this.failedStarts.values()) add(failure.serverName, failure.root);
832
+ for (const snapshot of this.snapshotProcessCrashRoutes()) add(snapshot.name, snapshot.root);
833
+ return Array.from(candidates.values()).sort(
834
+ (first, second) =>
835
+ first.route.root.localeCompare(second.route.root) ||
836
+ first.route.serverName.localeCompare(second.route.serverName),
837
+ );
838
+ }
839
+
840
+ private explicitRouteInScope(root: string, scopes: readonly string[]): boolean {
841
+ return scopes.some(
842
+ (scope) =>
843
+ projectRoots.isWithinOrEqual(scope, root) || projectRoots.isWithinOrEqual(root, scope),
844
+ );
845
+ }
846
+
847
+ private async withExplicitRouteAttempts<T>(
848
+ candidates: readonly ExplicitRouteCandidate[],
849
+ control: CodeRequestControl | undefined,
850
+ callback: (results: ExplicitRouteAttemptResult[]) => Promise<T>,
851
+ ): Promise<T> {
852
+ throwIfCodeRequestInterrupted(control);
853
+ const leased: string[] = [];
854
+ try {
855
+ const promises = candidates.map((candidate) => {
856
+ throwIfCodeRequestInterrupted(control);
857
+ const key = candidate.route.key;
858
+ const existing = this.explicitRouteAttempts.get(key);
859
+ if (existing) {
860
+ existing.users++;
861
+ leased.push(key);
862
+ return raceRequestControl(existing.promise, control);
863
+ }
864
+ const promise = this.performExplicitRouteAttempt(candidate);
865
+ const lease: ExplicitRouteAttemptLease = { promise, users: 1, settled: false };
866
+ this.explicitRouteAttempts.set(key, lease);
867
+ void promise
868
+ .finally(() => {
869
+ lease.settled = true;
870
+ if (lease.users === 0 && this.explicitRouteAttempts.get(key) === lease) {
871
+ this.explicitRouteAttempts.delete(key);
872
+ }
873
+ })
874
+ .catch(() => {});
875
+ leased.push(key);
876
+ return raceRequestControl(promise, control);
877
+ });
878
+ const results = await Promise.all(promises);
879
+ return await callback(results);
880
+ } finally {
881
+ for (const key of leased) {
882
+ const lease = this.explicitRouteAttempts.get(key);
883
+ if (!lease) continue;
884
+ lease.users--;
885
+ if (lease.users === 0 && lease.settled) this.explicitRouteAttempts.delete(key);
886
+ }
887
+ }
888
+ }
889
+
890
+ private async performExplicitRouteAttempt(
891
+ candidate: ExplicitRouteCandidate,
892
+ ): Promise<ExplicitRouteAttemptResult> {
893
+ const { route } = candidate;
894
+ const recovery = this.processCrashRecoveries.get(route.key) ?? candidate.processCrashRecovery;
895
+ if (recovery?.statusReason) {
896
+ const client = await this.acquireProcessCrashReplacement(recovery, route, undefined, {
897
+ allowExhausted: true,
898
+ });
899
+ return {
900
+ candidate,
901
+ kind: "process-crash",
902
+ client,
903
+ attempted: true,
904
+ ...(recovery.failureMessage ? { failureMessage: recovery.failureMessage } : {}),
905
+ };
906
+ }
907
+
908
+ const existing = this.clients.get(route.key);
909
+ if (existing?.status === "running") {
910
+ return { candidate, kind: "none", client: existing, attempted: false };
911
+ }
912
+ const retryStartup = candidate.startupFailure !== undefined || existing?.status === "error";
913
+ const client = await this.startServerForRoot(route.serverName, route.root, undefined, {
914
+ retryFailedStart: true,
915
+ });
916
+ const failureMessage =
917
+ this.failedStarts.get(route.key)?.failureMessage ?? candidate.startupFailure?.failureMessage;
918
+ return {
919
+ candidate,
920
+ kind: retryStartup ? "startup" : "discovery",
921
+ client,
922
+ attempted: true,
923
+ ...(failureMessage ? { failureMessage } : {}),
924
+ };
925
+ }
926
+
927
+ private observeExplicitReadyRoutes(scopes: readonly string[]): void {
928
+ for (const client of this.clients.values()) {
929
+ if (client.status !== "running" || !client.ready) continue;
930
+ if (!this.explicitRouteInScope(client.root, scopes)) continue;
931
+ this.observeExplicitRouteReady(client);
932
+ }
933
+ }
934
+
935
+ private observeExplicitRouteReady(client: LspClient): void {
936
+ if (client.status !== "running" || !client.ready) return;
937
+ const key = clientKey(client.name, client.root);
938
+ const recovery = this.processCrashRecoveries.get(key);
939
+ if (recovery) {
940
+ recovery.attemptConsumed = false;
941
+ recovery.statusReason = undefined;
942
+ recovery.failureMessage = undefined;
943
+ this.processCrashRecoveries.delete(key);
944
+ }
945
+ this.failedStarts.delete(key);
946
+ this.unavailable.delete(key);
947
+ }
948
+
949
+ private explicitFailedFiles(
950
+ candidate: ExplicitRouteCandidate,
951
+ scopes: readonly string[],
952
+ ): string[] {
953
+ const files = candidate.processCrashRecovery?.files ?? [];
954
+ return files.filter(
955
+ (file) =>
956
+ this.isDiagnosticFile(file) &&
957
+ scopes.some((scope) => projectRoots.isWithinOrEqual(scope, file)),
958
+ );
959
+ }
960
+
961
+ private formatStartupRetryFailure(route: FileRoute, failureMessage?: string): string {
962
+ const root = (path.relative(this.cwd, route.root) || ".").replaceAll("\\", "/");
963
+ const detail = failureMessage ? `: ${boundProcessCrashFailureMessage(failureMessage)}` : "";
964
+ return `${route.serverName} @ ${root} is unavailable — startup retry failed${detail}; try another explicit health refresh.`;
965
+ }
966
+
607
967
  /**
608
968
  * Recover all crashed routes selected by one evidence-producing operation.
609
969
  * Each route keeps its independent attempt budget and shared replacement.
@@ -628,6 +988,7 @@ export class LspManager {
628
988
  return {
629
989
  hasSupport,
630
990
  requiresFreshEvidence: false,
991
+ startupRetry: emptyStartupRetryReport(),
631
992
  processCrashRecovery: buildProcessCrashRecoveryReport(
632
993
  immediate.map(({ route }) => route),
633
994
  this.cwd,
@@ -680,6 +1041,7 @@ export class LspManager {
680
1041
  return {
681
1042
  hasSupport,
682
1043
  requiresFreshEvidence: observedRequired.length > 0,
1044
+ startupRetry: emptyStartupRetryReport(),
683
1045
  processCrashRecovery: buildProcessCrashRecoveryReport(
684
1046
  [...immediate.map(({ route }) => route), ...recoveredRoutes],
685
1047
  this.cwd,
@@ -790,8 +1152,8 @@ export class LspManager {
790
1152
  : "recovery-failed");
791
1153
  const reason =
792
1154
  routeOutcome === "recovery-exhausted"
793
- ? "process recovery exhausted; reload required"
794
- : "process recovery failed";
1155
+ ? "process recovery exhausted; try another explicit health refresh"
1156
+ : "process recovery failed; try another explicit health refresh";
795
1157
  return `${name} @ ${root} is unavailable — ${reason}.`;
796
1158
  }
797
1159
 
@@ -1030,6 +1392,11 @@ export class LspManager {
1030
1392
  // Validate command exists
1031
1393
  if (!commandExists(serverConfig.command)) {
1032
1394
  this.unavailable.set(key, "missing-command");
1395
+ this.failedStarts.set(key, {
1396
+ serverName,
1397
+ root,
1398
+ failureMessage: `Configured server command is not available: ${serverConfig.command}`,
1399
+ });
1033
1400
  return null;
1034
1401
  }
1035
1402
 
@@ -1048,9 +1415,15 @@ export class LspManager {
1048
1415
  return null;
1049
1416
  }
1050
1417
  this.unavailable.delete(key);
1418
+ this.failedStarts.delete(key);
1051
1419
  return client;
1052
- } catch {
1420
+ } catch (error) {
1053
1421
  this.unavailable.set(key, "start-failed");
1422
+ this.failedStarts.set(key, {
1423
+ serverName,
1424
+ root,
1425
+ ...(error instanceof Error && error.message ? { failureMessage: error.message } : {}),
1426
+ });
1054
1427
  if (this.clients.get(key) === client) this.clients.delete(key);
1055
1428
  return null;
1056
1429
  }
@@ -1129,7 +1502,7 @@ export class LspManager {
1129
1502
  seen: Set<string>,
1130
1503
  ): { client: LspClient; key: string } | null {
1131
1504
  const client = this.getExistingClientForFile(filePath);
1132
- if (!client || (pushOnly && client.hasDiagnosticProvider)) return null;
1505
+ if (!client || (pushOnly && client.hasDiagnosticRequestAdapter)) return null;
1133
1506
 
1134
1507
  const key = clientKey(client.name, client.root);
1135
1508
  if (this.processCrashRecoveries.get(key)?.pending || seen.has(key)) return null;
@@ -1230,6 +1603,17 @@ export class LspManager {
1230
1603
  fileTypes: [...(this.config.servers[client.name]?.fileTypes ?? [])],
1231
1604
  });
1232
1605
  }
1606
+ // Keep an initial-start failure visible after its failed client is
1607
+ // removed from the active client pool.
1608
+ for (const failure of this.failedStarts.values()) {
1609
+ const key = clientKey(failure.serverName, failure.root);
1610
+ if (known.has(key)) continue;
1611
+ known.set(key, {
1612
+ name: failure.serverName,
1613
+ root: failure.root,
1614
+ fileTypes: [...(this.config.servers[failure.serverName]?.fileTypes ?? [])],
1615
+ });
1616
+ }
1233
1617
  // Keep an exhausted recovery route visible after its failed replacement
1234
1618
  // is removed from the active client pool.
1235
1619
  for (const recovery of this.processCrashRecoveries.values()) {
@@ -1257,10 +1641,115 @@ export class LspManager {
1257
1641
  async waitUntilFileReady(
1258
1642
  filePath: string,
1259
1643
  control?: CodeRequestControl,
1260
- onProcessCrashRecovery?: (report: ProcessCrashRecoveryReport) => void,
1644
+ options: { retryFailedRoute?: boolean } & FileReadinessCallbacks = {},
1261
1645
  ): Promise<FileReadinessResult> {
1262
1646
  throwIfCodeRequestInterrupted(control);
1263
1647
  const resolvedPath = resolveSessionPath(this.cwd, filePath);
1648
+ if (options.retryFailedRoute) {
1649
+ const route = this.resolveFileRoute(resolvedPath);
1650
+ if (!route || !this.automaticPathPolicy.isEligible(route.root, "directory")) {
1651
+ return {
1652
+ client: null,
1653
+ processCrashRecovery: emptyProcessCrashRecoveryReport(),
1654
+ startupRetry: emptyStartupRetryReport(),
1655
+ };
1656
+ }
1657
+ const candidates: ExplicitRouteCandidate[] = [
1658
+ {
1659
+ route,
1660
+ ...(this.processCrashRecoveries.get(route.key)
1661
+ ? { processCrashRecovery: this.processCrashRecoveries.get(route.key) }
1662
+ : {}),
1663
+ ...(this.failedStarts.get(route.key)
1664
+ ? { startupFailure: this.failedStarts.get(route.key) }
1665
+ : {}),
1666
+ },
1667
+ ];
1668
+ return this.withExplicitRouteAttempts(candidates, control, (results) =>
1669
+ this.completeExplicitFileReadiness(resolvedPath, control, options, results[0]),
1670
+ );
1671
+ }
1672
+ return this.waitUntilFileReadyInternal(resolvedPath, control, options.onProcessCrashRecovery);
1673
+ }
1674
+
1675
+ private async completeExplicitFileReadiness(
1676
+ resolvedPath: string,
1677
+ control: CodeRequestControl | undefined,
1678
+ callbacks: FileReadinessCallbacks,
1679
+ result: ExplicitRouteAttemptResult | undefined,
1680
+ ): Promise<FileReadinessResult> {
1681
+ const startupRetry = result
1682
+ ? this.buildExplicitFileStartupReport(result)
1683
+ : emptyStartupRetryReport();
1684
+ if (startupRetry.entries.length > 0) callbacks.onStartupRetry?.(startupRetry);
1685
+ let readiness: FileReadinessResult;
1686
+ try {
1687
+ readiness = await this.waitUntilFileReadyInternal(
1688
+ resolvedPath,
1689
+ control,
1690
+ callbacks.onProcessCrashRecovery,
1691
+ );
1692
+ } catch (error) {
1693
+ if (result?.kind !== "process-crash" || isCodeRequestInterruption(error, control)) {
1694
+ throw error;
1695
+ }
1696
+ const processCrashRecovery = this.buildExplicitFileProcessCrashReport(result, null, error);
1697
+ callbacks.onProcessCrashRecovery?.(processCrashRecovery);
1698
+ return {
1699
+ client: null,
1700
+ processCrashRecovery,
1701
+ startupRetry,
1702
+ };
1703
+ }
1704
+ if (!result) return readiness;
1705
+ if (readiness.client?.ready) this.observeExplicitRouteReady(readiness.client);
1706
+ const processCrashRecovery =
1707
+ result.kind === "process-crash"
1708
+ ? this.mergeExplicitFileProcessCrashReports(
1709
+ this.buildExplicitFileProcessCrashReport(result, readiness.client),
1710
+ readiness.processCrashRecovery,
1711
+ )
1712
+ : readiness.processCrashRecovery;
1713
+ if (processCrashRecovery.entries.length > 0) {
1714
+ callbacks.onProcessCrashRecovery?.(processCrashRecovery);
1715
+ }
1716
+ return {
1717
+ client: readiness.client,
1718
+ processCrashRecovery,
1719
+ startupRetry,
1720
+ };
1721
+ }
1722
+
1723
+ private mergeExplicitFileProcessCrashReports(
1724
+ explicit: ProcessCrashRecoveryReport,
1725
+ observed: ProcessCrashRecoveryReport,
1726
+ ): ProcessCrashRecoveryReport {
1727
+ if (observed.failedRoutes === 0) return explicit;
1728
+ const observedFailure = observed.entries.find(
1729
+ (entry) => entry.outcome === "recovery-failed" && entry.failureMessage,
1730
+ );
1731
+ if (!observedFailure?.failureMessage) return explicit;
1732
+ return {
1733
+ ...explicit,
1734
+ entries: explicit.entries.map((entry) =>
1735
+ entry.name === observedFailure.name && entry.root === observedFailure.root
1736
+ ? { ...entry, failureMessage: observedFailure.failureMessage }
1737
+ : entry,
1738
+ ),
1739
+ };
1740
+ }
1741
+
1742
+ private buildExplicitFileStartupReport(result: ExplicitRouteAttemptResult): StartupRetryReport {
1743
+ return result.kind === "startup" || (result.kind === "discovery" && !result.client)
1744
+ ? this.buildExplicitFileStartupRetryReport(result)
1745
+ : emptyStartupRetryReport();
1746
+ }
1747
+
1748
+ private async waitUntilFileReadyInternal(
1749
+ resolvedPath: string,
1750
+ control: CodeRequestControl | undefined,
1751
+ onProcessCrashRecovery?: (report: ProcessCrashRecoveryReport) => void,
1752
+ ): Promise<FileReadinessResult> {
1264
1753
  const route = this.resolveFileRoute(resolvedPath);
1265
1754
  const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
1266
1755
  const statusAtStart = recovery?.statusReason;
@@ -1276,7 +1765,13 @@ export class LspManager {
1276
1765
  client,
1277
1766
  );
1278
1767
  if (processCrashRecoveryRequested) onProcessCrashRecovery?.(processCrashRecovery);
1279
- if (!client) return { client: null, processCrashRecovery };
1768
+ if (!client) {
1769
+ return {
1770
+ client: null,
1771
+ processCrashRecovery,
1772
+ startupRetry: emptyStartupRetryReport(),
1773
+ };
1774
+ }
1280
1775
  try {
1281
1776
  await client.getReady(control);
1282
1777
  await this.warmSemanticProject(client, resolvedPath, true, control);
@@ -1290,11 +1785,62 @@ export class LspManager {
1290
1785
  );
1291
1786
  if (failedRecovery) {
1292
1787
  onProcessCrashRecovery?.(failedRecovery);
1293
- return { client: null, processCrashRecovery: failedRecovery };
1788
+ return {
1789
+ client: null,
1790
+ processCrashRecovery: failedRecovery,
1791
+ startupRetry: emptyStartupRetryReport(),
1792
+ };
1294
1793
  }
1295
1794
  throw error;
1296
1795
  }
1297
- return { client, processCrashRecovery };
1796
+ return {
1797
+ client,
1798
+ processCrashRecovery,
1799
+ startupRetry: emptyStartupRetryReport(),
1800
+ };
1801
+ }
1802
+
1803
+ private buildExplicitFileStartupRetryReport(
1804
+ result: ExplicitRouteAttemptResult,
1805
+ ): StartupRetryReport {
1806
+ const recovered = result.client?.status === "running";
1807
+ return buildStartupRetryReport(
1808
+ [
1809
+ {
1810
+ name: result.candidate.route.serverName,
1811
+ root: result.candidate.route.root,
1812
+ outcome: recovered ? "recovered" : "retry-failed",
1813
+ ...(recovered || !result.failureMessage ? {} : { failureMessage: result.failureMessage }),
1814
+ },
1815
+ ],
1816
+ this.cwd,
1817
+ );
1818
+ }
1819
+
1820
+ private buildExplicitFileProcessCrashReport(
1821
+ result: ExplicitRouteAttemptResult,
1822
+ client: LspClient | null,
1823
+ failure?: unknown,
1824
+ ): ProcessCrashRecoveryReport {
1825
+ const failureMessage =
1826
+ failure instanceof Error && failure.message
1827
+ ? boundProcessCrashFailureMessage(failure.message)
1828
+ : result.failureMessage;
1829
+ return buildProcessCrashRecoveryReport(
1830
+ [
1831
+ {
1832
+ name: result.candidate.route.serverName,
1833
+ root: result.candidate.route.root,
1834
+ outcome: client?.status === "running" ? "recovered" : "recovery-failed",
1835
+ ...(!client?.status || client.status !== "running"
1836
+ ? failureMessage
1837
+ ? { failureMessage }
1838
+ : {}
1839
+ : {}),
1840
+ },
1841
+ ],
1842
+ this.cwd,
1843
+ );
1298
1844
  }
1299
1845
 
1300
1846
  private buildFileProcessCrashRecoveryReport(
@@ -1411,7 +1957,10 @@ export class LspManager {
1411
1957
  return unavailableCodeQuery(`No LSP client can collect diagnostics for ${resolvedPath}.`);
1412
1958
  }
1413
1959
  try {
1414
- return await syncClientFileAndGetDiagnostics(client, resolvedPath, maxSeverity, control);
1960
+ return await syncClientFileAndGetDiagnostics(client, resolvedPath, maxSeverity, {
1961
+ control,
1962
+ contentIsAuthoritative: false,
1963
+ });
1415
1964
  } catch (error) {
1416
1965
  // Cancellation must propagate; a cancelled caller no longer awaits a
1417
1966
  // diagnostic result or the file cleanup side effects.
@@ -1472,15 +2021,15 @@ export class LspManager {
1472
2021
  ): Promise<BulkTrackFileOutcome> {
1473
2022
  const resolvedPath = resolveSessionPath(this.cwd, filePath);
1474
2023
  if (!this.isConfiguredAutomaticSourceFile(resolvedPath)) {
1475
- return { file: resolvedPath, kind: "unsupported", reason: "not-automatic-source" };
2024
+ return { file: resolvedPath, kind: "skipped", reason: "not-automatic-source" };
1476
2025
  }
1477
2026
  const fileStatus = this.inspectSourceFile(resolvedPath);
1478
2027
  if (fileStatus !== "regular") {
1479
2028
  if (fileStatus === "missing") {
1480
- return { file: resolvedPath, kind: "unsupported", reason: "missing" };
2029
+ return { file: resolvedPath, kind: "skipped", reason: "missing" };
1481
2030
  }
1482
2031
  if (fileStatus === "not-automatic-source") {
1483
- return { file: resolvedPath, kind: "unsupported", reason: "not-automatic-source" };
2032
+ return { file: resolvedPath, kind: "skipped", reason: "not-automatic-source" };
1484
2033
  }
1485
2034
  return {
1486
2035
  file: resolvedPath,
@@ -1580,13 +2129,13 @@ export class LspManager {
1580
2129
  /** Return per-route diagnostic evidence capability for recovery targeting. */
1581
2130
  getClientDiagnosticRoutes(): Array<{
1582
2131
  key: string;
1583
- supportsPull: boolean;
2132
+ hasDiagnosticRequestAdapter: boolean;
1584
2133
  unconfirmedFiles: string[];
1585
2134
  stallSignal: ReturnType<LspClient["getRecoveryStallSignal"]>;
1586
2135
  }> {
1587
2136
  const routes: Array<{
1588
2137
  key: string;
1589
- supportsPull: boolean;
2138
+ hasDiagnosticRequestAdapter: boolean;
1590
2139
  unconfirmedFiles: string[];
1591
2140
  stallSignal: ReturnType<LspClient["getRecoveryStallSignal"]>;
1592
2141
  }> = [];
@@ -1599,7 +2148,7 @@ export class LspManager {
1599
2148
  .filter((file) => this.isDiagnosticFile(file));
1600
2149
  routes.push({
1601
2150
  key,
1602
- supportsPull: client.hasDiagnosticProvider,
2151
+ hasDiagnosticRequestAdapter: client.hasDiagnosticRequestAdapter,
1603
2152
  unconfirmedFiles,
1604
2153
  stallSignal: client.getRecoveryStallSignal(),
1605
2154
  });
@@ -1615,35 +2164,46 @@ export class LspManager {
1615
2164
  quietMs?: number;
1616
2165
  /** Evidence from a refresh the caller already completed; skips this pass's own refresh when no watched-file changes apply. */
1617
2166
  initialEvidence?: DiagnosticEvidenceSummary;
1618
- /** Explicit demand to recover crashed routes with tracked files in scope. */
1619
- processCrashDemand?: { scopes?: readonly string[] };
2167
+ /** Explicit demand to retry crashed and failed routes in scope. */
2168
+ processCrashDemand?: { scopes?: readonly string[]; explicit?: boolean };
1620
2169
  control?: CodeRequestControl;
1621
2170
  }): Promise<WorkspaceRecoveryResult> {
1622
2171
  const processCrashDemand = options?.processCrashDemand;
1623
- const demand = processCrashDemand
1624
- ? await this.recoverProcessCrashDemand(
1625
- (snapshot) => {
1626
- if (!this.processCrashDiagnosticRouteInScope(snapshot, processCrashDemand)) {
1627
- return null;
1628
- }
1629
- return this.processCrashDiagnosticFiles(snapshot, processCrashDemand);
1630
- },
1631
- options?.control,
1632
- { skipWhenNoRetainedFile: true },
1633
- )
1634
- : {
1635
- hasSupport: false,
1636
- requiresFreshEvidence: false,
1637
- processCrashRecovery: emptyProcessCrashRecoveryReport(),
1638
- failures: [],
1639
- failedFiles: [],
1640
- };
2172
+ const demand = processCrashDemand?.explicit
2173
+ ? await this.recoverExplicitRouteDemand(processCrashDemand, options?.control)
2174
+ : processCrashDemand
2175
+ ? await this.recoverProcessCrashDemand(
2176
+ (snapshot) => {
2177
+ if (!this.processCrashDiagnosticRouteInScope(snapshot, processCrashDemand)) {
2178
+ return null;
2179
+ }
2180
+ return this.processCrashDiagnosticFiles(snapshot, processCrashDemand);
2181
+ },
2182
+ options?.control,
2183
+ { skipWhenNoRetainedFile: true },
2184
+ )
2185
+ : {
2186
+ hasSupport: false,
2187
+ requiresFreshEvidence: false,
2188
+ processCrashRecovery: emptyProcessCrashRecoveryReport(),
2189
+ startupRetry: emptyStartupRetryReport(),
2190
+ failures: [],
2191
+ failedFiles: [],
2192
+ };
1641
2193
  const recoveryOptions = demand.requiresFreshEvidence
1642
2194
  ? { ...options, initialEvidence: undefined }
1643
2195
  : options;
1644
2196
  const result = await recoverWorkspaceDiagnosticsImpl(this, recoveryOptions);
2197
+ if (processCrashDemand?.explicit) {
2198
+ throwIfCodeRequestInterrupted(options?.control);
2199
+ this.observeExplicitReadyRoutes(this.processCrashDiagnosticScopes(processCrashDemand));
2200
+ }
1645
2201
  return this.addProcessCrashDiagnosticEvidence(
1646
- { ...result, processCrashRecovery: demand.processCrashRecovery },
2202
+ {
2203
+ ...result,
2204
+ processCrashRecovery: demand.processCrashRecovery,
2205
+ startupRetry: demand.startupRetry,
2206
+ },
1647
2207
  demand,
1648
2208
  );
1649
2209
  }
@@ -1721,6 +2281,8 @@ export class LspManager {
1721
2281
  this.initializedClientGenerations.clear();
1722
2282
  this.handledCrashGenerations.clear();
1723
2283
  this.processCrashRecoveries.clear();
2284
+ this.failedStarts.clear();
2285
+ this.explicitRouteAttempts.clear();
1724
2286
  this.recoveryRestartEpochs.clear();
1725
2287
  this.unavailable.clear();
1726
2288
  this.knownRoots.clear();
@@ -1997,7 +2559,8 @@ export class LspManager {
1997
2559
  }
1998
2560
 
1999
2561
  /**
2000
- * Aggregate the tsconfig scope decision for every tracked file.
2562
+ * Aggregate the tsconfig scope decision for every tracked TypeScript or
2563
+ * JavaScript-family file.
2001
2564
  *
2002
2565
  * The recovery telemetry surface uses this to report why tracked files are
2003
2566
  * in or out of scope without re-deriving the scope logic. The returned
@@ -2015,7 +2578,7 @@ export class LspManager {
2015
2578
  for (const client of this.clients.values()) {
2016
2579
  for (const document of client.getDiagnosticSnapshot().documents) {
2017
2580
  const file = relativeFilePathFromUri(document.uri, this.cwd);
2018
- if (!this.isAutomaticScopePath(file)) continue;
2581
+ if (!this.isAutomaticScopePath(file) || !isTsconfigApplicableFile(file)) continue;
2019
2582
  totalFiles++;
2020
2583
  accumulateScopeDecision(accumulator, file, getFileScopeDecision(file, this.cwd));
2021
2584
  }