@mrclrchtr/supi-code-intelligence 6.3.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +16 -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/diagnostics/workspace-sentinels.ts +4 -32
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sources.ts +108 -0
- 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 +770 -56
- 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 +48 -12
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +50 -4
- 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 +27 -11
- package/src/session/health-types.ts +21 -4
- package/src/session/health-workflow.ts +28 -22
- package/src/session/orientation-types.ts +5 -6
- package/src/session/orientation-workflow.ts +10 -2
- package/src/session/session.ts +12 -5
- package/src/session/target-workflow.ts +9 -6
- package/src/substrate/lsp/maintenance.ts +53 -47
- package/src/substrate/lsp/source-tracking.ts +252 -0
- package/src/tool/code_find/guidance.ts +3 -3
- 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 +2 -2
- 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 +3 -5
- package/src/tool/code_health/markdown.ts +90 -57
- package/src/tool/code_health/refresh-outcome.ts +51 -0
- package/src/tool/code_health/refresh-status.ts +130 -43
- 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 +29 -38
- package/src/tool/code_inspect/guidance.ts +5 -3
- package/src/tool/code_inspect/tui.ts +7 -1
- package/src/tool/code_orientation/guidance.ts +3 -4
- 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 +3 -5
- package/src/tool/code_refactor_plan/tui.ts +7 -1
- package/src/tool/code_resolve/guidance.ts +3 -5
- package/src/tool/code_resolve/result.ts +13 -10
- package/src/tool/code_resolve/tui.ts +18 -1
- package/src/tool/guidance.ts +11 -9
- 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
|
@@ -26,12 +26,14 @@ import {
|
|
|
26
26
|
truncateDebugIdentity as truncateIdentity,
|
|
27
27
|
} from "@mrclrchtr/supi-core/debug";
|
|
28
28
|
import { resolveToolPath as resolveSessionPath, uriToFile } from "@mrclrchtr/supi-core/path";
|
|
29
|
+
import { isMissingFileError } from "../client/client-file-state.ts";
|
|
29
30
|
import { getServerForFile } from "../config/config.ts";
|
|
30
31
|
import {
|
|
31
32
|
getFileScopeDecision,
|
|
32
33
|
invalidateTsconfigCacheForConfig,
|
|
33
34
|
invalidateTsconfigCacheForConfigDir,
|
|
34
35
|
isProjectConfigFileName,
|
|
36
|
+
isTsconfigApplicableFile,
|
|
35
37
|
} from "../config/tsconfig-scope.ts";
|
|
36
38
|
import type {
|
|
37
39
|
DetectedProjectServer,
|
|
@@ -63,14 +65,26 @@ import {
|
|
|
63
65
|
type WorkspaceSentinelScanOptions,
|
|
64
66
|
type WorkspaceSentinelSyncResult,
|
|
65
67
|
} from "../diagnostics/workspace-sentinels.ts";
|
|
68
|
+
import {
|
|
69
|
+
scanWorkspaceSources as scanAutomaticWorkspaceSources,
|
|
70
|
+
type WorkspaceSourceInventory,
|
|
71
|
+
} from "../diagnostics/workspace-sources.ts";
|
|
66
72
|
import { raceRequestControl } from "../session/readiness.ts";
|
|
67
73
|
import {
|
|
68
74
|
emptyProcessCrashRecoveryReport,
|
|
75
|
+
emptyStartupRetryReport,
|
|
69
76
|
type ProcessCrashRecoveryReport,
|
|
70
77
|
type ScopeDecisionEntry,
|
|
71
78
|
type ScopeDecisionSummary,
|
|
79
|
+
type StartupRetryReport,
|
|
72
80
|
type WorkspaceDiagnosticReport,
|
|
73
81
|
} from "../session/runtime-diagnostics.ts";
|
|
82
|
+
import { scanProjectCapabilities } from "../session/scanner.ts";
|
|
83
|
+
import {
|
|
84
|
+
type BulkTrackFileOutcome,
|
|
85
|
+
type BulkTrackFilesResult,
|
|
86
|
+
MAX_BULK_TRACK_FILES,
|
|
87
|
+
} from "../session/workspace-lsp-runtime.ts";
|
|
74
88
|
import {
|
|
75
89
|
displayRelativeFilePath,
|
|
76
90
|
formatCoverageSummaryText,
|
|
@@ -97,7 +111,9 @@ import { clientKey, rememberKnownRoot, resolveRootForFile } from "./manager-help
|
|
|
97
111
|
import {
|
|
98
112
|
boundProcessCrashFailureMessage,
|
|
99
113
|
buildProcessCrashRecoveryReport,
|
|
114
|
+
buildStartupRetryReport,
|
|
100
115
|
type ProcessCrashRecoveryRouteResult,
|
|
116
|
+
type StartupRetryRouteResult,
|
|
101
117
|
} from "./manager-process-crash-report.ts";
|
|
102
118
|
import { buildProjectServerInfo } from "./manager-project-info.ts";
|
|
103
119
|
import type {
|
|
@@ -124,6 +140,7 @@ type UnavailableReason = "missing-command" | "start-failed" | "runtime-error";
|
|
|
124
140
|
|
|
125
141
|
/** Maximum tracked-file entries retained in one scope-decision telemetry summary. */
|
|
126
142
|
const SCOPE_DECISION_MAX_ENTRIES = 24;
|
|
143
|
+
const MAX_CONCURRENT_BULK_TRACKS = 4;
|
|
127
144
|
|
|
128
145
|
interface ScopeDecisionAccumulator {
|
|
129
146
|
entries: ScopeDecisionEntry[];
|
|
@@ -186,6 +203,45 @@ type FileClientRequestOptions = {
|
|
|
186
203
|
interface FileReadinessResult {
|
|
187
204
|
client: LspClient | null;
|
|
188
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[];
|
|
189
245
|
}
|
|
190
246
|
|
|
191
247
|
interface ProcessCrashRecoveryState {
|
|
@@ -212,6 +268,8 @@ type ProcessCrashDemandResult = {
|
|
|
212
268
|
requiresFreshEvidence: boolean;
|
|
213
269
|
/** Separate route-level result for this explicit process-crash demand. */
|
|
214
270
|
processCrashRecovery: ProcessCrashRecoveryReport;
|
|
271
|
+
/** Separate result for explicit initial-start retries. */
|
|
272
|
+
startupRetry: StartupRetryReport;
|
|
215
273
|
/** Required routes that remain unavailable after shared recovery settles. */
|
|
216
274
|
failures: string[];
|
|
217
275
|
/** In-scope tracked files from required routes that remain unavailable. */
|
|
@@ -258,6 +316,10 @@ export class LspManager {
|
|
|
258
316
|
private clients = new Map<string, LspClient>();
|
|
259
317
|
/** Per-root startup failures keyed by "serverName:root" */
|
|
260
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>();
|
|
261
323
|
/** Memoized per-command availability of LSP server binaries on PATH */
|
|
262
324
|
private commandAvailability = new Map<string, boolean>();
|
|
263
325
|
/** Guards against concurrent client creation for the same server:root key. */
|
|
@@ -286,6 +348,9 @@ export class LspManager {
|
|
|
286
348
|
private handledCrashGenerations = new Map<string, number>();
|
|
287
349
|
/** Prevent late startup and lifecycle callbacks from republishing after shutdown. */
|
|
288
350
|
private shuttingDown = false;
|
|
351
|
+
/** Coalesce per-file tracking transitions into one bulk transition. */
|
|
352
|
+
private trackedFilesBatchDepth = 0;
|
|
353
|
+
private trackedFilesBatchChanged = false;
|
|
289
354
|
constructor(
|
|
290
355
|
private readonly config: LspConfig,
|
|
291
356
|
private readonly cwd: string,
|
|
@@ -343,7 +408,7 @@ export class LspManager {
|
|
|
343
408
|
return this.canServeFile(filePath);
|
|
344
409
|
}
|
|
345
410
|
|
|
346
|
-
/** Inventory automatic workspace
|
|
411
|
+
/** Inventory automatic workspace sentinels with the runtime policy. */
|
|
347
412
|
scanWorkspaceSentinels(options: WorkspaceSentinelScanOptions = {}): Map<string, number> {
|
|
348
413
|
return scanAutomaticWorkspaceSentinels(this.cwd, {
|
|
349
414
|
...options,
|
|
@@ -351,7 +416,16 @@ export class LspManager {
|
|
|
351
416
|
});
|
|
352
417
|
}
|
|
353
418
|
|
|
354
|
-
/**
|
|
419
|
+
/** Scan configured LSP source extensions with the runtime path policy. */
|
|
420
|
+
scanWorkspaceSources(control?: CodeRequestControl): Promise<WorkspaceSourceInventory> {
|
|
421
|
+
return scanAutomaticWorkspaceSources(this.cwd, {
|
|
422
|
+
fileTypes: Object.values(this.config.servers).flatMap((server) => server.fileTypes),
|
|
423
|
+
policy: this.automaticPathPolicy,
|
|
424
|
+
control,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Refresh the automatic workspace sentinel with the runtime policy. */
|
|
355
429
|
syncWorkspaceSentinelSnapshot(
|
|
356
430
|
previous: Map<string, number>,
|
|
357
431
|
options: WorkspaceSentinelScanOptions = {},
|
|
@@ -417,14 +491,22 @@ export class LspManager {
|
|
|
417
491
|
// A recovered route keeps its consumed budget but has no active reason.
|
|
418
492
|
// Passive callers can use its running client normally.
|
|
419
493
|
|
|
420
|
-
return this.startServerForRoot(route.serverName, route.root);
|
|
494
|
+
return this.startServerForRoot(route.serverName, route.root, options.control);
|
|
421
495
|
}
|
|
422
|
-
async startServerForRoot(
|
|
496
|
+
async startServerForRoot(
|
|
497
|
+
serverName: string,
|
|
498
|
+
root: string,
|
|
499
|
+
control?: CodeRequestControl,
|
|
500
|
+
options: { retryFailedStart?: boolean } = {},
|
|
501
|
+
): Promise<LspClient | null> {
|
|
502
|
+
throwIfCodeRequestInterrupted(control);
|
|
423
503
|
if (this.shuttingDown) return null;
|
|
424
504
|
const serverConfig = this.config.servers[serverName];
|
|
425
505
|
if (!serverConfig) return null;
|
|
426
506
|
const key = clientKey(serverName, root);
|
|
427
|
-
|
|
507
|
+
const retryFailedStart = options.retryFailedStart === true;
|
|
508
|
+
if (retryFailedStart) this.unavailable.delete(key);
|
|
509
|
+
if (!retryFailedStart && this.getUnavailableReason(key, serverConfig.command)) return null;
|
|
428
510
|
|
|
429
511
|
// Return existing client
|
|
430
512
|
const existing = this.clients.get(key);
|
|
@@ -440,27 +522,34 @@ export class LspManager {
|
|
|
440
522
|
if (existing && existing.status === "error") {
|
|
441
523
|
this.clients.delete(key);
|
|
442
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
|
+
});
|
|
443
530
|
this.clearWarmedWorkspaceSymbolProjects(existing.name, existing.root);
|
|
444
531
|
this.clearWarmedSemanticProjects(existing.name, existing.root);
|
|
445
532
|
this.clearPendingWarmProbes(existing.name, existing.root);
|
|
446
|
-
return null;
|
|
533
|
+
if (!retryFailedStart) return null;
|
|
534
|
+
this.unavailable.delete(key);
|
|
447
535
|
}
|
|
448
536
|
|
|
449
537
|
// Deduplicate concurrent starts for the same server:root pair.
|
|
450
538
|
// This prevents spawning duplicate server processes when two
|
|
451
539
|
// callers race through getClientForFile before either await yields.
|
|
452
540
|
const pending = this.pendingStarts.get(key);
|
|
453
|
-
if (pending) return pending;
|
|
541
|
+
if (pending) return raceRequestControl(pending, control);
|
|
454
542
|
|
|
455
543
|
const startPromise = this.performStart(serverName, serverConfig, root, key);
|
|
456
544
|
this.pendingStarts.set(key, startPromise);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
this.pendingStarts.delete(key);
|
|
462
|
-
}
|
|
463
|
-
|
|
545
|
+
// Keep the shared startup registered until it settles. A cancelled
|
|
546
|
+
// caller only stops waiting; it must not allow a duplicate route start.
|
|
547
|
+
void startPromise
|
|
548
|
+
.finally(() => {
|
|
549
|
+
if (this.pendingStarts.get(key) === startPromise) this.pendingStarts.delete(key);
|
|
550
|
+
})
|
|
551
|
+
.catch(() => {});
|
|
552
|
+
return raceRequestControl(startPromise, control);
|
|
464
553
|
}
|
|
465
554
|
|
|
466
555
|
private createClient(
|
|
@@ -505,6 +594,11 @@ export class LspManager {
|
|
|
505
594
|
}
|
|
506
595
|
}
|
|
507
596
|
|
|
597
|
+
if (kind === "tracked-files" && this.trackedFilesBatchDepth > 0) {
|
|
598
|
+
this.trackedFilesBatchChanged = true;
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
508
602
|
const aggregateKind =
|
|
509
603
|
kind === "readiness" && generation > 1 && client.ready ? "recovery" : kind;
|
|
510
604
|
this.publishLifecycle(aggregateKind);
|
|
@@ -558,18 +652,318 @@ export class LspManager {
|
|
|
558
652
|
recovery: ProcessCrashRecoveryState,
|
|
559
653
|
route: FileRoute,
|
|
560
654
|
control?: CodeRequestControl,
|
|
655
|
+
options: { allowExhausted?: boolean } = {},
|
|
561
656
|
): Promise<LspClient | null> {
|
|
562
657
|
if (recovery.pending) return this.waitForProcessCrashRecovery(recovery, control);
|
|
563
|
-
|
|
564
|
-
recovery.statusReason === "process-crashed" &&
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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) {
|
|
568
664
|
return this.startProcessCrashRecovery(recovery, route, control);
|
|
569
665
|
}
|
|
570
666
|
return Promise.resolve(null);
|
|
571
667
|
}
|
|
572
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
|
+
|
|
573
967
|
/**
|
|
574
968
|
* Recover all crashed routes selected by one evidence-producing operation.
|
|
575
969
|
* Each route keeps its independent attempt budget and shared replacement.
|
|
@@ -594,6 +988,7 @@ export class LspManager {
|
|
|
594
988
|
return {
|
|
595
989
|
hasSupport,
|
|
596
990
|
requiresFreshEvidence: false,
|
|
991
|
+
startupRetry: emptyStartupRetryReport(),
|
|
597
992
|
processCrashRecovery: buildProcessCrashRecoveryReport(
|
|
598
993
|
immediate.map(({ route }) => route),
|
|
599
994
|
this.cwd,
|
|
@@ -646,6 +1041,7 @@ export class LspManager {
|
|
|
646
1041
|
return {
|
|
647
1042
|
hasSupport,
|
|
648
1043
|
requiresFreshEvidence: observedRequired.length > 0,
|
|
1044
|
+
startupRetry: emptyStartupRetryReport(),
|
|
649
1045
|
processCrashRecovery: buildProcessCrashRecoveryReport(
|
|
650
1046
|
[...immediate.map(({ route }) => route), ...recoveredRoutes],
|
|
651
1047
|
this.cwd,
|
|
@@ -756,8 +1152,8 @@ export class LspManager {
|
|
|
756
1152
|
: "recovery-failed");
|
|
757
1153
|
const reason =
|
|
758
1154
|
routeOutcome === "recovery-exhausted"
|
|
759
|
-
? "process recovery exhausted;
|
|
760
|
-
: "process recovery failed";
|
|
1155
|
+
? "process recovery exhausted; try another explicit health refresh"
|
|
1156
|
+
: "process recovery failed; try another explicit health refresh";
|
|
761
1157
|
return `${name} @ ${root} is unavailable — ${reason}.`;
|
|
762
1158
|
}
|
|
763
1159
|
|
|
@@ -996,6 +1392,11 @@ export class LspManager {
|
|
|
996
1392
|
// Validate command exists
|
|
997
1393
|
if (!commandExists(serverConfig.command)) {
|
|
998
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
|
+
});
|
|
999
1400
|
return null;
|
|
1000
1401
|
}
|
|
1001
1402
|
|
|
@@ -1014,9 +1415,15 @@ export class LspManager {
|
|
|
1014
1415
|
return null;
|
|
1015
1416
|
}
|
|
1016
1417
|
this.unavailable.delete(key);
|
|
1418
|
+
this.failedStarts.delete(key);
|
|
1017
1419
|
return client;
|
|
1018
|
-
} catch {
|
|
1420
|
+
} catch (error) {
|
|
1019
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
|
+
});
|
|
1020
1427
|
if (this.clients.get(key) === client) this.clients.delete(key);
|
|
1021
1428
|
return null;
|
|
1022
1429
|
}
|
|
@@ -1095,7 +1502,7 @@ export class LspManager {
|
|
|
1095
1502
|
seen: Set<string>,
|
|
1096
1503
|
): { client: LspClient; key: string } | null {
|
|
1097
1504
|
const client = this.getExistingClientForFile(filePath);
|
|
1098
|
-
if (!client || (pushOnly && client.
|
|
1505
|
+
if (!client || (pushOnly && client.hasDiagnosticRequestAdapter)) return null;
|
|
1099
1506
|
|
|
1100
1507
|
const key = clientKey(client.name, client.root);
|
|
1101
1508
|
if (this.processCrashRecoveries.get(key)?.pending || seen.has(key)) return null;
|
|
@@ -1196,6 +1603,17 @@ export class LspManager {
|
|
|
1196
1603
|
fileTypes: [...(this.config.servers[client.name]?.fileTypes ?? [])],
|
|
1197
1604
|
});
|
|
1198
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
|
+
}
|
|
1199
1617
|
// Keep an exhausted recovery route visible after its failed replacement
|
|
1200
1618
|
// is removed from the active client pool.
|
|
1201
1619
|
for (const recovery of this.processCrashRecoveries.values()) {
|
|
@@ -1223,10 +1641,115 @@ export class LspManager {
|
|
|
1223
1641
|
async waitUntilFileReady(
|
|
1224
1642
|
filePath: string,
|
|
1225
1643
|
control?: CodeRequestControl,
|
|
1226
|
-
|
|
1644
|
+
options: { retryFailedRoute?: boolean } & FileReadinessCallbacks = {},
|
|
1227
1645
|
): Promise<FileReadinessResult> {
|
|
1228
1646
|
throwIfCodeRequestInterrupted(control);
|
|
1229
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> {
|
|
1230
1753
|
const route = this.resolveFileRoute(resolvedPath);
|
|
1231
1754
|
const recovery = route ? this.processCrashRecoveries.get(route.key) : undefined;
|
|
1232
1755
|
const statusAtStart = recovery?.statusReason;
|
|
@@ -1242,7 +1765,13 @@ export class LspManager {
|
|
|
1242
1765
|
client,
|
|
1243
1766
|
);
|
|
1244
1767
|
if (processCrashRecoveryRequested) onProcessCrashRecovery?.(processCrashRecovery);
|
|
1245
|
-
if (!client)
|
|
1768
|
+
if (!client) {
|
|
1769
|
+
return {
|
|
1770
|
+
client: null,
|
|
1771
|
+
processCrashRecovery,
|
|
1772
|
+
startupRetry: emptyStartupRetryReport(),
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1246
1775
|
try {
|
|
1247
1776
|
await client.getReady(control);
|
|
1248
1777
|
await this.warmSemanticProject(client, resolvedPath, true, control);
|
|
@@ -1256,11 +1785,62 @@ export class LspManager {
|
|
|
1256
1785
|
);
|
|
1257
1786
|
if (failedRecovery) {
|
|
1258
1787
|
onProcessCrashRecovery?.(failedRecovery);
|
|
1259
|
-
return {
|
|
1788
|
+
return {
|
|
1789
|
+
client: null,
|
|
1790
|
+
processCrashRecovery: failedRecovery,
|
|
1791
|
+
startupRetry: emptyStartupRetryReport(),
|
|
1792
|
+
};
|
|
1260
1793
|
}
|
|
1261
1794
|
throw error;
|
|
1262
1795
|
}
|
|
1263
|
-
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
|
+
);
|
|
1264
1844
|
}
|
|
1265
1845
|
|
|
1266
1846
|
private buildFileProcessCrashRecoveryReport(
|
|
@@ -1377,7 +1957,10 @@ export class LspManager {
|
|
|
1377
1957
|
return unavailableCodeQuery(`No LSP client can collect diagnostics for ${resolvedPath}.`);
|
|
1378
1958
|
}
|
|
1379
1959
|
try {
|
|
1380
|
-
return await syncClientFileAndGetDiagnostics(client, resolvedPath, maxSeverity,
|
|
1960
|
+
return await syncClientFileAndGetDiagnostics(client, resolvedPath, maxSeverity, {
|
|
1961
|
+
control,
|
|
1962
|
+
contentIsAuthoritative: false,
|
|
1963
|
+
});
|
|
1381
1964
|
} catch (error) {
|
|
1382
1965
|
// Cancellation must propagate; a cancelled caller no longer awaits a
|
|
1383
1966
|
// diagnostic result or the file cleanup side effects.
|
|
@@ -1387,6 +1970,118 @@ export class LspManager {
|
|
|
1387
1970
|
return unavailableCodeQuery(`Diagnostic collection failed for ${resolvedPath}: ${detail}`);
|
|
1388
1971
|
}
|
|
1389
1972
|
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Track a bounded set of automatic source files with shared route starts.
|
|
1975
|
+
* Inputs after the first 256 unique paths are ignored.
|
|
1976
|
+
*/
|
|
1977
|
+
async bulkTrackFiles(
|
|
1978
|
+
filePaths: readonly string[],
|
|
1979
|
+
control?: CodeRequestControl,
|
|
1980
|
+
): Promise<BulkTrackFilesResult> {
|
|
1981
|
+
throwIfCodeRequestInterrupted(control);
|
|
1982
|
+
const selected = Array.from(
|
|
1983
|
+
new Set(filePaths.map((filePath) => resolveSessionPath(this.cwd, filePath))),
|
|
1984
|
+
).slice(0, MAX_BULK_TRACK_FILES);
|
|
1985
|
+
if (selected.length === 0) return { outcomes: [] };
|
|
1986
|
+
|
|
1987
|
+
this.trackedFilesBatchDepth++;
|
|
1988
|
+
const outcomes = new Array<BulkTrackFileOutcome>(selected.length);
|
|
1989
|
+
const workerCount = Math.min(MAX_CONCURRENT_BULK_TRACKS, selected.length);
|
|
1990
|
+
let nextIndex = 0;
|
|
1991
|
+
const worker = async (): Promise<void> => {
|
|
1992
|
+
for (;;) {
|
|
1993
|
+
throwIfCodeRequestInterrupted(control);
|
|
1994
|
+
const index = nextIndex++;
|
|
1995
|
+
if (index >= selected.length) return;
|
|
1996
|
+
outcomes[index] = await this.trackBulkFile(selected[index], control);
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
const workers = Array.from({ length: workerCount }, () => worker());
|
|
2000
|
+
try {
|
|
2001
|
+
await Promise.all(workers);
|
|
2002
|
+
return { outcomes };
|
|
2003
|
+
} catch (error) {
|
|
2004
|
+
// Let every worker observe cancellation before the batch transition is
|
|
2005
|
+
// published. Shared route startup may continue, but no worker may open
|
|
2006
|
+
// another document after this caller stops waiting.
|
|
2007
|
+
await Promise.allSettled(workers);
|
|
2008
|
+
throw error;
|
|
2009
|
+
} finally {
|
|
2010
|
+
this.trackedFilesBatchDepth--;
|
|
2011
|
+
if (this.trackedFilesBatchDepth === 0 && this.trackedFilesBatchChanged) {
|
|
2012
|
+
this.trackedFilesBatchChanged = false;
|
|
2013
|
+
this.publishLifecycle("tracked-files");
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
private async trackBulkFile(
|
|
2019
|
+
filePath: string,
|
|
2020
|
+
control?: CodeRequestControl,
|
|
2021
|
+
): Promise<BulkTrackFileOutcome> {
|
|
2022
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
2023
|
+
if (!this.isConfiguredAutomaticSourceFile(resolvedPath)) {
|
|
2024
|
+
return { file: resolvedPath, kind: "skipped", reason: "not-automatic-source" };
|
|
2025
|
+
}
|
|
2026
|
+
const fileStatus = this.inspectSourceFile(resolvedPath);
|
|
2027
|
+
if (fileStatus !== "regular") {
|
|
2028
|
+
if (fileStatus === "missing") {
|
|
2029
|
+
return { file: resolvedPath, kind: "skipped", reason: "missing" };
|
|
2030
|
+
}
|
|
2031
|
+
if (fileStatus === "not-automatic-source") {
|
|
2032
|
+
return { file: resolvedPath, kind: "skipped", reason: "not-automatic-source" };
|
|
2033
|
+
}
|
|
2034
|
+
return {
|
|
2035
|
+
file: resolvedPath,
|
|
2036
|
+
kind: "unavailable",
|
|
2037
|
+
reason: "The source file could not be inspected.",
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
if (this.isFileTracked(resolvedPath)) {
|
|
2041
|
+
return { file: resolvedPath, kind: "already-tracked" };
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
throwIfCodeRequestInterrupted(control);
|
|
2045
|
+
// Recheck the automatic source-support rule after the inventory scan. A
|
|
2046
|
+
// config, exclusion, or route can change while a refresh is in flight.
|
|
2047
|
+
if (!this.isSupportedSourceFile(resolvedPath)) {
|
|
2048
|
+
return {
|
|
2049
|
+
file: resolvedPath,
|
|
2050
|
+
kind: "unavailable",
|
|
2051
|
+
reason: "No available LSP route can serve this source file.",
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
const client = await this.ensureFileOpen(resolvedPath, { control });
|
|
2056
|
+
if (!client) {
|
|
2057
|
+
return {
|
|
2058
|
+
file: resolvedPath,
|
|
2059
|
+
kind: "unavailable",
|
|
2060
|
+
reason: "The LSP route could not open this source file.",
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
return { file: resolvedPath, kind: "tracked" };
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
private isConfiguredAutomaticSourceFile(filePath: string): boolean {
|
|
2067
|
+
return this.isAutomaticScopePath(filePath) && getServerForFile(this.config, filePath) !== null;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
private inspectSourceFile(
|
|
2071
|
+
filePath: string,
|
|
2072
|
+
): "regular" | "missing" | "not-automatic-source" | "unavailable" {
|
|
2073
|
+
try {
|
|
2074
|
+
return fs.statSync(filePath).isFile() ? "regular" : "not-automatic-source";
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
if (isMissingFileError(error)) return "missing";
|
|
2077
|
+
return "unavailable";
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
private isFileTracked(filePath: string): boolean {
|
|
2082
|
+
return this.getExistingClientForFile(filePath)?.openFiles.includes(filePath) ?? false;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
1390
2085
|
/** Close a file across any active LSP clients and clear its cached diagnostics. */
|
|
1391
2086
|
closeFile(filePath: string): void {
|
|
1392
2087
|
closeFileAcrossClients(this.clients.values(), resolveSessionPath(this.cwd, filePath));
|
|
@@ -1434,13 +2129,13 @@ export class LspManager {
|
|
|
1434
2129
|
/** Return per-route diagnostic evidence capability for recovery targeting. */
|
|
1435
2130
|
getClientDiagnosticRoutes(): Array<{
|
|
1436
2131
|
key: string;
|
|
1437
|
-
|
|
2132
|
+
hasDiagnosticRequestAdapter: boolean;
|
|
1438
2133
|
unconfirmedFiles: string[];
|
|
1439
2134
|
stallSignal: ReturnType<LspClient["getRecoveryStallSignal"]>;
|
|
1440
2135
|
}> {
|
|
1441
2136
|
const routes: Array<{
|
|
1442
2137
|
key: string;
|
|
1443
|
-
|
|
2138
|
+
hasDiagnosticRequestAdapter: boolean;
|
|
1444
2139
|
unconfirmedFiles: string[];
|
|
1445
2140
|
stallSignal: ReturnType<LspClient["getRecoveryStallSignal"]>;
|
|
1446
2141
|
}> = [];
|
|
@@ -1453,7 +2148,7 @@ export class LspManager {
|
|
|
1453
2148
|
.filter((file) => this.isDiagnosticFile(file));
|
|
1454
2149
|
routes.push({
|
|
1455
2150
|
key,
|
|
1456
|
-
|
|
2151
|
+
hasDiagnosticRequestAdapter: client.hasDiagnosticRequestAdapter,
|
|
1457
2152
|
unconfirmedFiles,
|
|
1458
2153
|
stallSignal: client.getRecoveryStallSignal(),
|
|
1459
2154
|
});
|
|
@@ -1469,35 +2164,46 @@ export class LspManager {
|
|
|
1469
2164
|
quietMs?: number;
|
|
1470
2165
|
/** Evidence from a refresh the caller already completed; skips this pass's own refresh when no watched-file changes apply. */
|
|
1471
2166
|
initialEvidence?: DiagnosticEvidenceSummary;
|
|
1472
|
-
/** Explicit demand to
|
|
1473
|
-
processCrashDemand?: { scopes?: readonly string[] };
|
|
2167
|
+
/** Explicit demand to retry crashed and failed routes in scope. */
|
|
2168
|
+
processCrashDemand?: { scopes?: readonly string[]; explicit?: boolean };
|
|
1474
2169
|
control?: CodeRequestControl;
|
|
1475
2170
|
}): Promise<WorkspaceRecoveryResult> {
|
|
1476
2171
|
const processCrashDemand = options?.processCrashDemand;
|
|
1477
|
-
const demand = processCrashDemand
|
|
1478
|
-
? await this.
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
+
};
|
|
1495
2193
|
const recoveryOptions = demand.requiresFreshEvidence
|
|
1496
2194
|
? { ...options, initialEvidence: undefined }
|
|
1497
2195
|
: options;
|
|
1498
2196
|
const result = await recoverWorkspaceDiagnosticsImpl(this, recoveryOptions);
|
|
2197
|
+
if (processCrashDemand?.explicit) {
|
|
2198
|
+
throwIfCodeRequestInterrupted(options?.control);
|
|
2199
|
+
this.observeExplicitReadyRoutes(this.processCrashDiagnosticScopes(processCrashDemand));
|
|
2200
|
+
}
|
|
1499
2201
|
return this.addProcessCrashDiagnosticEvidence(
|
|
1500
|
-
{
|
|
2202
|
+
{
|
|
2203
|
+
...result,
|
|
2204
|
+
processCrashRecovery: demand.processCrashRecovery,
|
|
2205
|
+
startupRetry: demand.startupRetry,
|
|
2206
|
+
},
|
|
1501
2207
|
demand,
|
|
1502
2208
|
);
|
|
1503
2209
|
}
|
|
@@ -1575,6 +2281,8 @@ export class LspManager {
|
|
|
1575
2281
|
this.initializedClientGenerations.clear();
|
|
1576
2282
|
this.handledCrashGenerations.clear();
|
|
1577
2283
|
this.processCrashRecoveries.clear();
|
|
2284
|
+
this.failedStarts.clear();
|
|
2285
|
+
this.explicitRouteAttempts.clear();
|
|
1578
2286
|
this.recoveryRestartEpochs.clear();
|
|
1579
2287
|
this.unavailable.clear();
|
|
1580
2288
|
this.knownRoots.clear();
|
|
@@ -1851,7 +2559,8 @@ export class LspManager {
|
|
|
1851
2559
|
}
|
|
1852
2560
|
|
|
1853
2561
|
/**
|
|
1854
|
-
* Aggregate the tsconfig scope decision for every tracked
|
|
2562
|
+
* Aggregate the tsconfig scope decision for every tracked TypeScript or
|
|
2563
|
+
* JavaScript-family file.
|
|
1855
2564
|
*
|
|
1856
2565
|
* The recovery telemetry surface uses this to report why tracked files are
|
|
1857
2566
|
* in or out of scope without re-deriving the scope logic. The returned
|
|
@@ -1869,7 +2578,7 @@ export class LspManager {
|
|
|
1869
2578
|
for (const client of this.clients.values()) {
|
|
1870
2579
|
for (const document of client.getDiagnosticSnapshot().documents) {
|
|
1871
2580
|
const file = relativeFilePathFromUri(document.uri, this.cwd);
|
|
1872
|
-
if (!this.isAutomaticScopePath(file)) continue;
|
|
2581
|
+
if (!this.isAutomaticScopePath(file) || !isTsconfigApplicableFile(file)) continue;
|
|
1873
2582
|
totalFiles++;
|
|
1874
2583
|
accumulateScopeDecision(accumulator, file, getFileScopeDecision(file, this.cwd));
|
|
1875
2584
|
}
|
|
@@ -1994,9 +2703,14 @@ export class LspManager {
|
|
|
1994
2703
|
const client = await this.getClientForFile(resolvedPath, options);
|
|
1995
2704
|
if (!client) return null;
|
|
1996
2705
|
try {
|
|
1997
|
-
|
|
2706
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
2707
|
+
// Route startup can be shared. The document open belongs to this
|
|
2708
|
+
// caller, so check again after the shared wait before sending didOpen.
|
|
2709
|
+
throwIfCodeRequestInterrupted(options.control);
|
|
2710
|
+
client.didOpen(resolvedPath, content);
|
|
1998
2711
|
return client;
|
|
1999
|
-
} catch {
|
|
2712
|
+
} catch (error) {
|
|
2713
|
+
if (isCodeRequestInterruption(error, options.control)) throw error;
|
|
2000
2714
|
this.closeFile(resolvedPath);
|
|
2001
2715
|
return null;
|
|
2002
2716
|
}
|