@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.
- package/README.md +164 -82
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
- package/node_modules/@mrclrchtr/supi-core/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
- package/node_modules/@mrclrchtr/supi-lsp/README.md +35 -13
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +3 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +3 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +6 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +43 -35
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +83 -17
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +108 -92
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +44 -82
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +3 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +9 -68
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +45 -58
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +230 -331
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +201 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +129 -70
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-typescript.ts +395 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +8 -12
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +685 -183
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +60 -60
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-request-enrollment.ts +75 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-barrier.ts +443 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-errors.ts +30 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-file.ts +15 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +290 -37
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +72 -16
- package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +1 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +30 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +10 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +50 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +11 -7
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +608 -45
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +3 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +37 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +31 -11
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +19 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callee-display-name.ts +165 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +67 -9
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +2 -0
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/package.json +1 -1
- package/package.json +5 -5
- package/src/analysis/health/diagnostics.ts +1 -2
- package/src/analysis/health/file-scope.ts +4 -1
- package/src/analysis/health/server-status.ts +1 -1
- package/src/analysis/relations/callees.ts +17 -8
- package/src/analysis/relations/implementations.ts +1 -2
- package/src/analysis/relations/references.ts +8 -4
- package/src/analysis/relations/types.ts +6 -30
- package/src/analysis/target/anchored.ts +9 -1
- package/src/analysis/target/symbol.ts +29 -11
- package/src/analysis/target/types.ts +14 -6
- package/src/api.ts +2 -0
- package/src/session/graph/collect.ts +18 -3
- package/src/session/health-refresh.ts +12 -1
- package/src/session/health-types.ts +18 -6
- package/src/session/health-workflow.ts +21 -19
- package/src/session/orientation-types.ts +5 -6
- package/src/session/orientation-workflow.ts +10 -2
- package/src/session/target-workflow.ts +9 -6
- package/src/substrate/lsp/source-tracking.ts +11 -6
- package/src/tool/code_find/guidance.ts +2 -2
- package/src/tool/code_find/spec.ts +5 -4
- package/src/tool/code_find/tui.ts +7 -1
- package/src/tool/code_graph/details.ts +32 -0
- package/src/tool/code_graph/display.ts +54 -0
- package/src/tool/code_graph/format.ts +60 -0
- package/src/tool/code_graph/guidance.ts +1 -1
- package/src/tool/code_graph/markdown.ts +76 -198
- package/src/tool/code_graph/read-next.ts +65 -0
- package/src/tool/code_graph/result.ts +118 -83
- package/src/tool/code_graph/tui.ts +216 -54
- package/src/tool/code_health/guidance.ts +2 -4
- package/src/tool/code_health/markdown.ts +75 -58
- package/src/tool/code_health/refresh-outcome.ts +41 -1
- package/src/tool/code_health/refresh-status.ts +114 -48
- package/src/tool/code_health/scope.ts +78 -0
- package/src/tool/code_health/spec.ts +1 -1
- package/src/tool/code_health/tui.ts +25 -38
- package/src/tool/code_inspect/tui.ts +7 -1
- package/src/tool/code_orientation/result.ts +31 -8
- package/src/tool/code_orientation/tui.ts +26 -5
- package/src/tool/code_refactor_apply/guidance.ts +1 -1
- package/src/tool/code_refactor_apply/tui.ts +7 -3
- package/src/tool/code_refactor_plan/guidance.ts +1 -1
- package/src/tool/code_refactor_plan/tui.ts +7 -1
- package/src/tool/code_resolve/guidance.ts +2 -4
- package/src/tool/code_resolve/result.ts +13 -10
- package/src/tool/code_resolve/tui.ts +18 -1
- package/src/tool/infra/truncate.ts +39 -13
- package/src/tool/result/candidate-row.ts +27 -0
- package/src/tool/result/errors.ts +21 -6
- package/src/tool/result/types.ts +7 -3
- package/src/tool/schemas.ts +5 -0
- package/src/types/execution.ts +2 -0
- package/src/types/index.ts +2 -0
- package/src/ui/tui/candidate-legacy-body.ts +69 -0
- package/src/ui/tui/candidate-selection.ts +366 -0
- package/src/ui/tui/common.ts +26 -31
- 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
|
-
|
|
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
|
-
|
|
598
|
-
recovery.statusReason === "process-crashed" &&
|
|
599
|
-
|
|
600
|
-
|
|
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;
|
|
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.
|
|
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
|
-
|
|
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)
|
|
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 {
|
|
1788
|
+
return {
|
|
1789
|
+
client: null,
|
|
1790
|
+
processCrashRecovery: failedRecovery,
|
|
1791
|
+
startupRetry: emptyStartupRetryReport(),
|
|
1792
|
+
};
|
|
1294
1793
|
}
|
|
1295
1794
|
throw error;
|
|
1296
1795
|
}
|
|
1297
|
-
return {
|
|
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,
|
|
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: "
|
|
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: "
|
|
2029
|
+
return { file: resolvedPath, kind: "skipped", reason: "missing" };
|
|
1481
2030
|
}
|
|
1482
2031
|
if (fileStatus === "not-automatic-source") {
|
|
1483
|
-
return { file: resolvedPath, kind: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
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
|
-
{
|
|
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
|
|
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
|
}
|