@akira-tl/forgerelay 0.4.4 → 0.4.5
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/CHANGELOG.md +13 -0
- package/capabilities/code-intelligence/GUIDE.md +6 -0
- package/dist/capability-registry.js +2 -2
- package/dist/logger.js +11 -1
- package/dist/lsp/code-intelligence.js +69 -97
- package/dist/lsp/runtime/diagnostic-snapshots.js +8 -0
- package/dist/lsp/runtime/document-synchronizer.js +102 -0
- package/dist/lsp/runtime/manager.js +203 -22
- package/dist/lsp/runtime/semantic-requests.js +116 -0
- package/dist/lsp/test-support/server-fixture.js +1 -1
- package/dist/server.js +27 -6
- package/docs/configuration.md +19 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.4.5] - 2026-08-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added Host-to-LSP cancellation propagation, bounded per-service semantic concurrency/queueing, and stable request timeout/cancellation/capacity errors without exposing arbitrary Agent-controlled timeout values.
|
|
12
|
+
- Added aggregate Language-service runtime telemetry for service/process/request/document/diagnostic/stderr retention without logging source contents or paths.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Language services now retry one unexpected server crash, enter a bounded cooldown after repeated crashes, and invalidate only affected services when the effective server-definition fingerprint changes.
|
|
17
|
+
- Language-service lifecycle now distinguishes truly idle services from cancellation-ignoring requests, evicts only the least-recently-used safe idle service at capacity, shares one service across logical workspaces on the same physical project, and releases managed-worktree services before finalization.
|
|
18
|
+
- Idle shutdown and repeated open/query/config/crash cycles now explicitly release and bound server processes, synchronized documents, diagnostic snapshots, request state, stderr tails, and service counts.
|
|
19
|
+
|
|
7
20
|
## [0.4.4] - 2026-08-11
|
|
8
21
|
|
|
9
22
|
### Added
|
|
@@ -15,3 +15,9 @@ Diagnostics use the same normalized result contract for push and pull providers.
|
|
|
15
15
|
Semantic locations may identify External code locations outside the Workspace, but that does not expand ForgeRelay's allowed roots or grant the file tools permission to read those paths.
|
|
16
16
|
|
|
17
17
|
Language-server definitions use structured process configuration rather than shell command strings. A project configuration entry may contain `command`, `args`, `env`, `languages`, `extensions`, `languageIdByExtension`, `projectMarkers`, and `enabled` fields. Use `languageIdByExtension` when one server definition covers multiple language IDs whose extensions do not map one-to-one by array position. The server command is launched directly without a shell.
|
|
18
|
+
|
|
19
|
+
Semantic Language-server requests use an internal bounded deadline; the Agent contract does not expose arbitrary timeout controls. Host cancellation is propagated to in-flight LSP requests, and ForgeRelay bounds both per-service semantic concurrency and queued work. Document synchronization is serialized before semantic requests cross the concurrency gate, while independent semantic reads may execute concurrently after that barrier. A server that ignores cancellation keeps its underlying request slot occupied until it actually settles, preventing repeated cancellations from creating an unbounded set of pending JSON-RPC requests.
|
|
20
|
+
|
|
21
|
+
An unexpected Language-server process exit fails its pending JSON-RPC work immediately instead of waiting for the semantic deadline. ForgeRelay discards that service and retries the current semantic operation at most once. A second consecutive crash for the same Language project/server-definition fingerprint enters a short cooldown before another process may be started; a successful retry clears the crash state. Effective server definitions are fingerprinted, so a changed project/global/built-in definition selects a new service identity on the next code-intelligence resolution and retires only an idle service with the same project root/server id but the old fingerprint. ForgeRelay does not add a recursive config watcher.
|
|
22
|
+
|
|
23
|
+
Language services are shared by physical Language project identity rather than logical Workspace id, so multiple conversations over the same checkout do not multiply server processes. Idle services are reclaimed after a bounded TTL, and the global service cap evicts the least-recently-used truly idle service; a request that has returned to the Host but whose server ignored cancellation still counts as active until the underlying LSP request settles. Managed-worktree finalization releases idle Language services rooted in that worktree before removing it and refuses finalization while semantic work is still active. Debug runtime telemetry reports only aggregate service/process/request/document/diagnostic/stderr counts and never source contents.
|
|
@@ -79,7 +79,7 @@ export class CapabilityRegistry {
|
|
|
79
79
|
throw new CapabilityError("invalid_arguments", `Invalid arguments for capability ${name}: ${details}`);
|
|
80
80
|
}
|
|
81
81
|
try {
|
|
82
|
-
return await definition.run(parsed.data, context);
|
|
82
|
+
return await definition.run(parsed.data, context, options);
|
|
83
83
|
}
|
|
84
84
|
catch (error) {
|
|
85
85
|
if (error instanceof CapabilityError)
|
|
@@ -186,7 +186,7 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
186
186
|
available: dependencies.codeIntelligence?.available ?? false,
|
|
187
187
|
reason: dependencies.codeIntelligence?.unavailableReason,
|
|
188
188
|
}),
|
|
189
|
-
run: async (input, context) => dependencies.codeIntelligence.run(input, context),
|
|
189
|
+
run: async (input, context, options) => dependencies.codeIntelligence.run(input, context, options),
|
|
190
190
|
}]
|
|
191
191
|
: []),
|
|
192
192
|
...(dependencies.downloadArtifact
|
package/dist/logger.js
CHANGED
|
@@ -223,7 +223,17 @@ function formatRuntimeResources(entry) {
|
|
|
223
223
|
const completed = numberField(entry.processesCompleted) ?? 0;
|
|
224
224
|
const workspaces = numberField(entry.cachedWorkspaces) ?? 0;
|
|
225
225
|
const reviewStates = numberField(entry.reviewStates) ?? 0;
|
|
226
|
-
|
|
226
|
+
const languageServices = numberField(entry.languageServices) ?? 0;
|
|
227
|
+
const languageServicesActive = numberField(entry.languageServicesActive) ?? 0;
|
|
228
|
+
const languageProcesses = numberField(entry.languageProcessesRunning) ?? 0;
|
|
229
|
+
const languageRequestsActive = numberField(entry.languageRequestsActive) ?? 0;
|
|
230
|
+
const languageRequestsQueued = numberField(entry.languageRequestsQueued) ?? 0;
|
|
231
|
+
const languageDocuments = numberField(entry.languageOpenDocuments) ?? 0;
|
|
232
|
+
const languageDiagnosticSnapshots = numberField(entry.languageDiagnosticSnapshots) ?? 0;
|
|
233
|
+
const languageDiagnosticsRetained = numberField(entry.languageDiagnosticsRetained) ?? 0;
|
|
234
|
+
const languageStderrBytes = numberField(entry.languageStderrBytes) ?? 0;
|
|
235
|
+
const languageCooldowns = numberField(entry.languageCrashCooldowns) ?? 0;
|
|
236
|
+
return `runtime rss=${rssMb}MB heap=${heapUsedMb}/${heapTotalMb}MB transports=${transports} processes=${running} running/${completed} completed workspaces=${workspaces} review=${reviewStates} lsp=${languageServices} services/${languageServicesActive} active/${languageProcesses} processes requests=${languageRequestsActive} active/${languageRequestsQueued} queued docs=${languageDocuments} diagnostics=${languageDiagnosticSnapshots} snapshots/${languageDiagnosticsRetained} retained stderr=${languageStderrBytes}B cooldowns=${languageCooldowns}`;
|
|
227
237
|
}
|
|
228
238
|
function bytesToMegabytes(value) {
|
|
229
239
|
return value === undefined ? 0 : Math.round(value / (1024 * 1024));
|
|
@@ -3,7 +3,7 @@ import { readFile, realpath } from "node:fs/promises";
|
|
|
3
3
|
import { basename, resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { createMessageConnection } from "vscode-jsonrpc/node";
|
|
6
|
-
import { DefinitionRequest, DocumentDiagnosticReportKind, DocumentDiagnosticRequest, DocumentSymbolRequest,
|
|
6
|
+
import { DefinitionRequest, DocumentDiagnosticReportKind, DocumentDiagnosticRequest, DocumentSymbolRequest, ExitNotification, HoverRequest, InitializeRequest, ReferencesRequest, InitializedNotification, MarkupKind, PositionEncodingKind, PublishDiagnosticsNotification, ShutdownRequest, WorkspaceSymbolRequest, } from "vscode-languageserver-protocol";
|
|
7
7
|
import { LanguageServerConfigurationError, } from "./language-server-config.js";
|
|
8
8
|
import { terminateProcessTree } from "../process-platform.js";
|
|
9
9
|
import { CodeIntelligenceError } from "./code-intelligence-error.js";
|
|
@@ -11,8 +11,10 @@ import { DEFAULT_CODE_INTELLIGENCE_RESULT_LIMIT } from "./code-intelligence-type
|
|
|
11
11
|
import { normalizeHoverContents } from "./normalization/hover.js";
|
|
12
12
|
import { normalizeDocumentSymbols, normalizeWorkspaceSymbols, } from "./normalization/symbols.js";
|
|
13
13
|
import { isWithin, locationEntries, normalizeLocations, workspaceDisplayPath, } from "./normalization/locations.js";
|
|
14
|
-
import { lspPositionFromUser, rangeFromLsp
|
|
14
|
+
import { lspPositionFromUser, rangeFromLsp } from "./position-encoding.js";
|
|
15
15
|
import { DiagnosticSnapshotStore } from "./runtime/diagnostic-snapshots.js";
|
|
16
|
+
import { DocumentSynchronizer } from "./runtime/document-synchronizer.js";
|
|
17
|
+
import { SemanticRequestCoordinator } from "./runtime/semantic-requests.js";
|
|
16
18
|
export { CodeIntelligenceError } from "./code-intelligence-error.js";
|
|
17
19
|
const STDERR_TAIL_BYTES = 64 * 1024;
|
|
18
20
|
export class LanguageService {
|
|
@@ -27,8 +29,9 @@ export class LanguageService {
|
|
|
27
29
|
initializePromise;
|
|
28
30
|
positionEncoding = PositionEncodingKind.UTF16;
|
|
29
31
|
capabilities;
|
|
30
|
-
documents
|
|
32
|
+
documents;
|
|
31
33
|
diagnosticSnapshots;
|
|
34
|
+
semanticRequests;
|
|
32
35
|
stderrTail = Buffer.alloc(0);
|
|
33
36
|
closed = false;
|
|
34
37
|
constructor(workspaceRoot, project, policy) {
|
|
@@ -36,7 +39,13 @@ export class LanguageService {
|
|
|
36
39
|
this.project = project;
|
|
37
40
|
this.policy = policy;
|
|
38
41
|
this.key = languageServiceKey(project);
|
|
42
|
+
this.documents = new DocumentSynchronizer(project.definition);
|
|
39
43
|
this.diagnosticSnapshots = new DiagnosticSnapshotStore(policy.maxDiagnosticDocuments, policy.maxDiagnosticsPerDocument);
|
|
44
|
+
this.semanticRequests = new SemanticRequestCoordinator({
|
|
45
|
+
maxConcurrent: policy.maxConcurrentSemanticRequests,
|
|
46
|
+
maxQueued: policy.maxQueuedSemanticRequests,
|
|
47
|
+
deadlineMs: policy.requestTimeoutMs,
|
|
48
|
+
});
|
|
40
49
|
}
|
|
41
50
|
acquire() {
|
|
42
51
|
this.inFlight += 1;
|
|
@@ -46,19 +55,37 @@ export class LanguageService {
|
|
|
46
55
|
this.inFlight = Math.max(0, this.inFlight - 1);
|
|
47
56
|
this.lastUsedAt = Date.now();
|
|
48
57
|
}
|
|
49
|
-
|
|
58
|
+
get isIdle() {
|
|
59
|
+
return this.inFlight === 0 &&
|
|
60
|
+
this.semanticRequests.activeCount === 0 &&
|
|
61
|
+
this.semanticRequests.queuedCount === 0;
|
|
62
|
+
}
|
|
63
|
+
get runtimeStats() {
|
|
64
|
+
const child = this.child;
|
|
65
|
+
return {
|
|
66
|
+
operationInFlight: this.inFlight,
|
|
67
|
+
semanticRequestsActive: this.semanticRequests.activeCount,
|
|
68
|
+
semanticRequestsQueued: this.semanticRequests.queuedCount,
|
|
69
|
+
openDocuments: this.documents.size,
|
|
70
|
+
diagnosticSnapshots: this.diagnosticSnapshots.size,
|
|
71
|
+
diagnosticsRetained: this.diagnosticSnapshots.retainedDiagnostics,
|
|
72
|
+
stderrBytes: this.stderrTail.length,
|
|
73
|
+
processRunning: Boolean(child && child.exitCode === null && child.signalCode === null),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async definition(input, signal) {
|
|
50
77
|
try {
|
|
51
78
|
await this.ensureStarted();
|
|
52
79
|
if (!this.capabilities?.definitionProvider) {
|
|
53
80
|
throw new CodeIntelligenceError("code.operation_unsupported", `Language server ${this.project.definition.id} does not advertise definition support.`);
|
|
54
81
|
}
|
|
55
82
|
const sourcePath = await workspaceSourcePath(this.workspaceRoot, input.path);
|
|
56
|
-
const document = await this.
|
|
83
|
+
const document = await this.syncDocumentOrdered(sourcePath);
|
|
57
84
|
const position = lspPositionFromUser(document.text, input.line, input.column, this.positionEncoding);
|
|
58
|
-
const response = await
|
|
85
|
+
const response = await this.semanticRequests.run(`Definition request for ${input.path}`, signal, (token) => this.connection.sendRequest(DefinitionRequest.type, {
|
|
59
86
|
textDocument: { uri: document.uri },
|
|
60
87
|
position,
|
|
61
|
-
}
|
|
88
|
+
}, token));
|
|
62
89
|
const locations = await normalizeLocations(locationEntries(response), this.workspaceRoot, this.positionEncoding);
|
|
63
90
|
return {
|
|
64
91
|
operation: "definition",
|
|
@@ -76,19 +103,19 @@ export class LanguageService {
|
|
|
76
103
|
throw new CodeIntelligenceError("code.server_crashed", `Language server ${this.project.definition.id} failed: ${errorMessage(error)}${this.stderrSuffix()}`);
|
|
77
104
|
}
|
|
78
105
|
}
|
|
79
|
-
async hover(input) {
|
|
106
|
+
async hover(input, signal) {
|
|
80
107
|
try {
|
|
81
108
|
await this.ensureStarted();
|
|
82
109
|
if (!this.capabilities?.hoverProvider) {
|
|
83
110
|
throw new CodeIntelligenceError("code.operation_unsupported", `Language server ${this.project.definition.id} does not advertise hover support.`);
|
|
84
111
|
}
|
|
85
112
|
const sourcePath = await workspaceSourcePath(this.workspaceRoot, input.path);
|
|
86
|
-
const document = await this.
|
|
113
|
+
const document = await this.syncDocumentOrdered(sourcePath);
|
|
87
114
|
const position = lspPositionFromUser(document.text, input.line, input.column, this.positionEncoding);
|
|
88
|
-
const response = await
|
|
115
|
+
const response = await this.semanticRequests.run(`Hover request for ${input.path}`, signal, (token) => this.connection.sendRequest(HoverRequest.type, {
|
|
89
116
|
textDocument: { uri: document.uri },
|
|
90
117
|
position,
|
|
91
|
-
}
|
|
118
|
+
}, token));
|
|
92
119
|
const common = {
|
|
93
120
|
operation: "hover",
|
|
94
121
|
selectedServer: this.project.definition.id,
|
|
@@ -114,20 +141,20 @@ export class LanguageService {
|
|
|
114
141
|
throw new CodeIntelligenceError("code.server_crashed", `Language server ${this.project.definition.id} failed: ${errorMessage(error)}${this.stderrSuffix()}`);
|
|
115
142
|
}
|
|
116
143
|
}
|
|
117
|
-
async references(input) {
|
|
144
|
+
async references(input, signal) {
|
|
118
145
|
try {
|
|
119
146
|
await this.ensureStarted();
|
|
120
147
|
if (!this.capabilities?.referencesProvider) {
|
|
121
148
|
throw new CodeIntelligenceError("code.operation_unsupported", `Language server ${this.project.definition.id} does not advertise references support.`);
|
|
122
149
|
}
|
|
123
150
|
const sourcePath = await workspaceSourcePath(this.workspaceRoot, input.path);
|
|
124
|
-
const document = await this.
|
|
151
|
+
const document = await this.syncDocumentOrdered(sourcePath);
|
|
125
152
|
const position = lspPositionFromUser(document.text, input.line, input.column, this.positionEncoding);
|
|
126
|
-
const response = await
|
|
153
|
+
const response = await this.semanticRequests.run(`References request for ${input.path}`, signal, (token) => this.connection.sendRequest(ReferencesRequest.type, {
|
|
127
154
|
textDocument: { uri: document.uri },
|
|
128
155
|
position,
|
|
129
156
|
context: { includeDeclaration: true },
|
|
130
|
-
}
|
|
157
|
+
}, token));
|
|
131
158
|
const entries = locationEntries(response ?? []);
|
|
132
159
|
const limit = input.limit ?? DEFAULT_CODE_INTELLIGENCE_RESULT_LIMIT;
|
|
133
160
|
const selected = entries.slice(0, limit);
|
|
@@ -151,17 +178,17 @@ export class LanguageService {
|
|
|
151
178
|
throw new CodeIntelligenceError("code.server_crashed", `Language server ${this.project.definition.id} failed: ${errorMessage(error)}${this.stderrSuffix()}`);
|
|
152
179
|
}
|
|
153
180
|
}
|
|
154
|
-
async documentSymbols(input) {
|
|
181
|
+
async documentSymbols(input, signal) {
|
|
155
182
|
try {
|
|
156
183
|
await this.ensureStarted();
|
|
157
184
|
if (!this.capabilities?.documentSymbolProvider) {
|
|
158
185
|
throw new CodeIntelligenceError("code.operation_unsupported", `Language server ${this.project.definition.id} does not advertise document-symbol support.`);
|
|
159
186
|
}
|
|
160
187
|
const sourcePath = await workspaceSourcePath(this.workspaceRoot, input.path);
|
|
161
|
-
const document = await this.
|
|
162
|
-
const response = await
|
|
188
|
+
const document = await this.syncDocumentOrdered(sourcePath);
|
|
189
|
+
const response = await this.semanticRequests.run(`Document-symbol request for ${input.path}`, signal, (token) => this.connection.sendRequest(DocumentSymbolRequest.type, {
|
|
163
190
|
textDocument: { uri: document.uri },
|
|
164
|
-
}
|
|
191
|
+
}, token));
|
|
165
192
|
const normalized = normalizeDocumentSymbols(response, document.text, this.positionEncoding, input.limit ?? DEFAULT_CODE_INTELLIGENCE_RESULT_LIMIT);
|
|
166
193
|
return {
|
|
167
194
|
operation: "documentSymbols",
|
|
@@ -176,15 +203,15 @@ export class LanguageService {
|
|
|
176
203
|
throw new CodeIntelligenceError("code.server_crashed", `Language server ${this.project.definition.id} failed: ${errorMessage(error)}${this.stderrSuffix()}`);
|
|
177
204
|
}
|
|
178
205
|
}
|
|
179
|
-
async workspaceSymbols(input) {
|
|
206
|
+
async workspaceSymbols(input, signal) {
|
|
180
207
|
try {
|
|
181
208
|
await this.ensureStarted();
|
|
182
209
|
if (!this.capabilities?.workspaceSymbolProvider) {
|
|
183
210
|
throw new CodeIntelligenceError("code.operation_unsupported", `Language server ${this.project.definition.id} does not advertise workspace-symbol support.`);
|
|
184
211
|
}
|
|
185
212
|
const sourcePath = await workspaceSourcePath(this.workspaceRoot, input.path);
|
|
186
|
-
await this.
|
|
187
|
-
const response = await
|
|
213
|
+
await this.syncDocumentOrdered(sourcePath);
|
|
214
|
+
const response = await this.semanticRequests.run(`Workspace-symbol request for ${JSON.stringify(input.query)}`, signal, (token) => this.connection.sendRequest(WorkspaceSymbolRequest.type, { query: input.query }, token));
|
|
188
215
|
const normalized = await normalizeWorkspaceSymbols(response, this.workspaceRoot, this.positionEncoding, input.limit ?? DEFAULT_CODE_INTELLIGENCE_RESULT_LIMIT);
|
|
189
216
|
return {
|
|
190
217
|
operation: "workspaceSymbols",
|
|
@@ -199,11 +226,11 @@ export class LanguageService {
|
|
|
199
226
|
throw new CodeIntelligenceError("code.server_crashed", `Language server ${this.project.definition.id} failed: ${errorMessage(error)}${this.stderrSuffix()}`);
|
|
200
227
|
}
|
|
201
228
|
}
|
|
202
|
-
async diagnostics(input) {
|
|
229
|
+
async diagnostics(input, signal) {
|
|
203
230
|
try {
|
|
204
231
|
await this.ensureStarted();
|
|
205
232
|
const sourcePath = await workspaceSourcePath(this.workspaceRoot, input.path);
|
|
206
|
-
const document = await this.
|
|
233
|
+
const document = await this.syncDocumentOrdered(sourcePath);
|
|
207
234
|
const limit = input.limit ?? DEFAULT_CODE_INTELLIGENCE_RESULT_LIMIT;
|
|
208
235
|
const common = {
|
|
209
236
|
operation: "diagnostics",
|
|
@@ -214,11 +241,11 @@ export class LanguageService {
|
|
|
214
241
|
const diagnosticProvider = this.capabilities?.diagnosticProvider;
|
|
215
242
|
if (diagnosticProvider) {
|
|
216
243
|
const previousResultId = this.diagnosticSnapshots.previousPullResultId(document.uri);
|
|
217
|
-
const response = await
|
|
244
|
+
const response = await this.semanticRequests.run(`Diagnostic request for ${input.path}`, signal, (token) => this.connection.sendRequest(DocumentDiagnosticRequest.type, {
|
|
218
245
|
textDocument: { uri: document.uri },
|
|
219
246
|
...(diagnosticProvider.identifier === undefined ? {} : { identifier: diagnosticProvider.identifier }),
|
|
220
247
|
...(previousResultId === undefined ? {} : { previousResultId }),
|
|
221
|
-
}
|
|
248
|
+
}, token));
|
|
222
249
|
if (response.kind === DocumentDiagnosticReportKind.Full) {
|
|
223
250
|
this.diagnosticSnapshots.capturePull(response.items, document, this.positionEncoding, response.resultId);
|
|
224
251
|
}
|
|
@@ -254,18 +281,7 @@ export class LanguageService {
|
|
|
254
281
|
const connection = this.connection;
|
|
255
282
|
const child = this.child;
|
|
256
283
|
if (connection) {
|
|
257
|
-
|
|
258
|
-
if (!document.openNotified)
|
|
259
|
-
continue;
|
|
260
|
-
try {
|
|
261
|
-
await connection.sendNotification(DidCloseTextDocumentNotification.type, {
|
|
262
|
-
textDocument: { uri: document.uri },
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
// The server may already be gone.
|
|
267
|
-
}
|
|
268
|
-
}
|
|
284
|
+
await this.documents.closeAll(connection);
|
|
269
285
|
try {
|
|
270
286
|
await withTimeout(connection.sendRequest(ShutdownRequest.type), this.policy.shutdownTimeoutMs, () => new Error("Language-server shutdown timed out."));
|
|
271
287
|
await connection.sendNotification(ExitNotification.type);
|
|
@@ -286,6 +302,9 @@ export class LanguageService {
|
|
|
286
302
|
this.child = undefined;
|
|
287
303
|
this.connection = undefined;
|
|
288
304
|
this.initializePromise = undefined;
|
|
305
|
+
this.capabilities = undefined;
|
|
306
|
+
this.positionEncoding = PositionEncodingKind.UTF16;
|
|
307
|
+
this.stderrTail = Buffer.alloc(0);
|
|
289
308
|
}
|
|
290
309
|
async ensureStarted() {
|
|
291
310
|
if (this.closed) {
|
|
@@ -335,6 +354,16 @@ export class LanguageService {
|
|
|
335
354
|
onExitDuringInitialization = (code, signal) => reject(new CodeIntelligenceError("code.language_service_start_failed", `Language server ${definition.id} exited during initialization (${signal ?? code ?? "unknown"}).${this.stderrSuffix()}`));
|
|
336
355
|
child.once("exit", onExitDuringInitialization);
|
|
337
356
|
});
|
|
357
|
+
child.on("exit", () => {
|
|
358
|
+
if (this.closed || this.child !== child)
|
|
359
|
+
return;
|
|
360
|
+
try {
|
|
361
|
+
connection.dispose();
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// Pending semantic requests will surface the unexpected process exit.
|
|
365
|
+
}
|
|
366
|
+
});
|
|
338
367
|
const rootUri = pathToFileURL(this.project.projectRoot).href;
|
|
339
368
|
const initializeParams = {
|
|
340
369
|
processId: process.pid,
|
|
@@ -418,49 +447,8 @@ export class LanguageService {
|
|
|
418
447
|
this.diagnosticSnapshots.capturePush(params, this.documents.get(params.uri), this.positionEncoding);
|
|
419
448
|
});
|
|
420
449
|
}
|
|
421
|
-
async
|
|
422
|
-
|
|
423
|
-
const text = await readFile(sourcePath, "utf8");
|
|
424
|
-
const existing = this.documents.get(uri);
|
|
425
|
-
const languageId = languageIdForPath(this.project.definition, sourcePath);
|
|
426
|
-
const synchronization = textDocumentSynchronization(this.capabilities?.textDocumentSync);
|
|
427
|
-
if (!existing) {
|
|
428
|
-
const document = { uri, languageId, version: 1, text, openNotified: false };
|
|
429
|
-
this.documents.set(uri, document);
|
|
430
|
-
if (synchronization.openClose) {
|
|
431
|
-
await this.connection.sendNotification(DidOpenTextDocumentNotification.type, {
|
|
432
|
-
textDocument: {
|
|
433
|
-
uri: document.uri,
|
|
434
|
-
languageId: document.languageId,
|
|
435
|
-
version: document.version,
|
|
436
|
-
text: document.text,
|
|
437
|
-
},
|
|
438
|
-
});
|
|
439
|
-
document.openNotified = true;
|
|
440
|
-
}
|
|
441
|
-
return document;
|
|
442
|
-
}
|
|
443
|
-
if (existing.text !== text) {
|
|
444
|
-
const previousText = existing.text;
|
|
445
|
-
existing.version += 1;
|
|
446
|
-
existing.text = text;
|
|
447
|
-
if (synchronization.change === TextDocumentSyncKind.Full) {
|
|
448
|
-
await this.connection.sendNotification(DidChangeTextDocumentNotification.type, {
|
|
449
|
-
textDocument: { uri, version: existing.version },
|
|
450
|
-
contentChanges: [{ text }],
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
else if (synchronization.change === TextDocumentSyncKind.Incremental) {
|
|
454
|
-
await this.connection.sendNotification(DidChangeTextDocumentNotification.type, {
|
|
455
|
-
textDocument: { uri, version: existing.version },
|
|
456
|
-
contentChanges: [{
|
|
457
|
-
range: wholeDocumentRange(previousText, this.positionEncoding),
|
|
458
|
-
text,
|
|
459
|
-
}],
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
return existing;
|
|
450
|
+
async syncDocumentOrdered(sourcePath) {
|
|
451
|
+
return this.documents.sync(sourcePath, this.connection, this.capabilities?.textDocumentSync, this.positionEncoding);
|
|
464
452
|
}
|
|
465
453
|
appendStderr(chunk) {
|
|
466
454
|
this.stderrTail = Buffer.concat([this.stderrTail, chunk]);
|
|
@@ -480,22 +468,6 @@ export function languageServiceKey(project) {
|
|
|
480
468
|
project.definition.fingerprint,
|
|
481
469
|
]);
|
|
482
470
|
}
|
|
483
|
-
function languageIdForPath(definition, path) {
|
|
484
|
-
const extension = path.slice(path.lastIndexOf(".")).toLowerCase();
|
|
485
|
-
return definition.languageIdByExtension[extension] ?? definition.languages[0];
|
|
486
|
-
}
|
|
487
|
-
function textDocumentSynchronization(value) {
|
|
488
|
-
if (typeof value === "number") {
|
|
489
|
-
return {
|
|
490
|
-
openClose: value !== TextDocumentSyncKind.None,
|
|
491
|
-
change: value,
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
return {
|
|
495
|
-
openClose: value?.openClose === true,
|
|
496
|
-
change: value?.change ?? TextDocumentSyncKind.None,
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
471
|
async function workspaceSourcePath(workspaceRoot, inputPath) {
|
|
500
472
|
const root = resolve(workspaceRoot);
|
|
501
473
|
const path = resolve(root, inputPath);
|
|
@@ -52,6 +52,14 @@ export class DiagnosticSnapshotStore {
|
|
|
52
52
|
get size() {
|
|
53
53
|
return this.pushSnapshots.size + this.pullSnapshots.size;
|
|
54
54
|
}
|
|
55
|
+
get retainedDiagnostics() {
|
|
56
|
+
let total = 0;
|
|
57
|
+
for (const snapshot of this.pushSnapshots.values())
|
|
58
|
+
total += snapshot.diagnostics.length;
|
|
59
|
+
for (const snapshot of this.pullSnapshots.values())
|
|
60
|
+
total += snapshot.diagnostics.length;
|
|
61
|
+
return total;
|
|
62
|
+
}
|
|
55
63
|
normalizeSnapshot(diagnostics, document, encoding, metadata) {
|
|
56
64
|
return {
|
|
57
65
|
diagnostics: diagnostics
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { DidChangeTextDocumentNotification, DidCloseTextDocumentNotification, DidOpenTextDocumentNotification, TextDocumentSyncKind, } from "vscode-languageserver-protocol";
|
|
4
|
+
import { wholeDocumentRange } from "../position-encoding.js";
|
|
5
|
+
export class DocumentSynchronizer {
|
|
6
|
+
definition;
|
|
7
|
+
documents = new Map();
|
|
8
|
+
queue = Promise.resolve();
|
|
9
|
+
constructor(definition) {
|
|
10
|
+
this.definition = definition;
|
|
11
|
+
}
|
|
12
|
+
get(uri) {
|
|
13
|
+
return this.documents.get(uri);
|
|
14
|
+
}
|
|
15
|
+
get size() {
|
|
16
|
+
return this.documents.size;
|
|
17
|
+
}
|
|
18
|
+
async sync(sourcePath, connection, synchronizationValue, positionEncoding) {
|
|
19
|
+
const operation = this.queue.then(() => this.syncNow(sourcePath, connection, synchronizationValue, positionEncoding), () => this.syncNow(sourcePath, connection, synchronizationValue, positionEncoding));
|
|
20
|
+
this.queue = operation.then(() => undefined, () => undefined);
|
|
21
|
+
return operation;
|
|
22
|
+
}
|
|
23
|
+
async closeAll(connection) {
|
|
24
|
+
await this.queue;
|
|
25
|
+
for (const document of this.documents.values()) {
|
|
26
|
+
if (!document.openNotified)
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
await connection.sendNotification(DidCloseTextDocumentNotification.type, {
|
|
30
|
+
textDocument: { uri: document.uri },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// The server may already be gone.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
clear() {
|
|
39
|
+
this.documents.clear();
|
|
40
|
+
this.queue = Promise.resolve();
|
|
41
|
+
}
|
|
42
|
+
async syncNow(sourcePath, connection, synchronizationValue, positionEncoding) {
|
|
43
|
+
const uri = pathToFileURL(sourcePath).href;
|
|
44
|
+
const text = await readFile(sourcePath, "utf8");
|
|
45
|
+
const existing = this.documents.get(uri);
|
|
46
|
+
const languageId = languageIdForPath(this.definition, sourcePath);
|
|
47
|
+
const synchronization = textDocumentSynchronization(synchronizationValue);
|
|
48
|
+
if (!existing) {
|
|
49
|
+
const document = { uri, languageId, version: 1, text, openNotified: false };
|
|
50
|
+
this.documents.set(uri, document);
|
|
51
|
+
if (synchronization.openClose) {
|
|
52
|
+
await connection.sendNotification(DidOpenTextDocumentNotification.type, {
|
|
53
|
+
textDocument: {
|
|
54
|
+
uri: document.uri,
|
|
55
|
+
languageId: document.languageId,
|
|
56
|
+
version: document.version,
|
|
57
|
+
text: document.text,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
document.openNotified = true;
|
|
61
|
+
}
|
|
62
|
+
return document;
|
|
63
|
+
}
|
|
64
|
+
if (existing.text !== text) {
|
|
65
|
+
const previousText = existing.text;
|
|
66
|
+
existing.version += 1;
|
|
67
|
+
existing.text = text;
|
|
68
|
+
if (synchronization.change === TextDocumentSyncKind.Full) {
|
|
69
|
+
await connection.sendNotification(DidChangeTextDocumentNotification.type, {
|
|
70
|
+
textDocument: { uri, version: existing.version },
|
|
71
|
+
contentChanges: [{ text }],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else if (synchronization.change === TextDocumentSyncKind.Incremental) {
|
|
75
|
+
await connection.sendNotification(DidChangeTextDocumentNotification.type, {
|
|
76
|
+
textDocument: { uri, version: existing.version },
|
|
77
|
+
contentChanges: [{
|
|
78
|
+
range: wholeDocumentRange(previousText, positionEncoding),
|
|
79
|
+
text,
|
|
80
|
+
}],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return existing;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function languageIdForPath(definition, path) {
|
|
88
|
+
const extension = path.slice(path.lastIndexOf(".")).toLowerCase();
|
|
89
|
+
return definition.languageIdByExtension[extension] ?? definition.languages[0];
|
|
90
|
+
}
|
|
91
|
+
function textDocumentSynchronization(value) {
|
|
92
|
+
if (typeof value === "number") {
|
|
93
|
+
return {
|
|
94
|
+
openClose: value !== TextDocumentSyncKind.None,
|
|
95
|
+
change: value,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
openClose: value?.openClose === true,
|
|
100
|
+
change: value?.change ?? TextDocumentSyncKind.None,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { resolve, sep } from "node:path";
|
|
3
3
|
import { CodeIntelligenceError, LanguageService, languageServiceKey, } from "../code-intelligence.js";
|
|
4
4
|
import { LanguageServerConfigurationError, resolveLanguageProject, } from "../language-server-config.js";
|
|
5
5
|
const LANGUAGE_SERVICE_IDLE_MS = 10 * 60 * 1_000;
|
|
@@ -8,17 +8,25 @@ const MAX_LANGUAGE_SERVICES = 16;
|
|
|
8
8
|
const LANGUAGE_SERVICE_START_TIMEOUT_MS = 15_000;
|
|
9
9
|
const LANGUAGE_REQUEST_TIMEOUT_MS = 10_000;
|
|
10
10
|
const LANGUAGE_SERVICE_SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
11
|
+
const MAX_CONCURRENT_SEMANTIC_REQUESTS = 4;
|
|
12
|
+
const MAX_QUEUED_SEMANTIC_REQUESTS = 16;
|
|
11
13
|
const MAX_DIAGNOSTIC_DOCUMENTS = 128;
|
|
12
14
|
const MAX_DIAGNOSTICS_PER_DOCUMENT = 1000;
|
|
15
|
+
const LANGUAGE_SERVICE_CRASH_COOLDOWN_MS = 5_000;
|
|
13
16
|
export class CodeIntelligenceManager {
|
|
14
17
|
config;
|
|
15
18
|
services = new Map();
|
|
16
19
|
serviceCreations = new Map();
|
|
20
|
+
invalidatedServiceKeys = new Set();
|
|
21
|
+
crashStates = new Map();
|
|
22
|
+
retiredWorkspaceRoots = new Set();
|
|
17
23
|
serviceCreationQueue = Promise.resolve();
|
|
18
24
|
cleanupTimer;
|
|
19
25
|
policy;
|
|
26
|
+
crashCooldownMs;
|
|
20
27
|
constructor(config, options = {}) {
|
|
21
28
|
this.config = config;
|
|
29
|
+
this.crashCooldownMs = positiveInteger(options.crashCooldownMs, LANGUAGE_SERVICE_CRASH_COOLDOWN_MS, "crashCooldownMs");
|
|
22
30
|
this.policy = {
|
|
23
31
|
idleMs: positiveInteger(options.idleMs, LANGUAGE_SERVICE_IDLE_MS, "idleMs"),
|
|
24
32
|
cleanupIntervalMs: positiveInteger(options.cleanupIntervalMs, LANGUAGE_SERVICE_CLEANUP_INTERVAL_MS, "cleanupIntervalMs"),
|
|
@@ -26,6 +34,8 @@ export class CodeIntelligenceManager {
|
|
|
26
34
|
startTimeoutMs: positiveInteger(options.startTimeoutMs, LANGUAGE_SERVICE_START_TIMEOUT_MS, "startTimeoutMs"),
|
|
27
35
|
requestTimeoutMs: positiveInteger(options.requestTimeoutMs, LANGUAGE_REQUEST_TIMEOUT_MS, "requestTimeoutMs"),
|
|
28
36
|
shutdownTimeoutMs: positiveInteger(options.shutdownTimeoutMs, LANGUAGE_SERVICE_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs"),
|
|
37
|
+
maxConcurrentSemanticRequests: positiveInteger(options.maxConcurrentSemanticRequests, MAX_CONCURRENT_SEMANTIC_REQUESTS, "maxConcurrentSemanticRequests"),
|
|
38
|
+
maxQueuedSemanticRequests: positiveInteger(options.maxQueuedSemanticRequests, MAX_QUEUED_SEMANTIC_REQUESTS, "maxQueuedSemanticRequests"),
|
|
29
39
|
maxDiagnosticDocuments: positiveInteger(options.maxDiagnosticDocuments, MAX_DIAGNOSTIC_DOCUMENTS, "maxDiagnosticDocuments"),
|
|
30
40
|
maxDiagnosticsPerDocument: positiveInteger(options.maxDiagnosticsPerDocument, MAX_DIAGNOSTICS_PER_DOCUMENT, "maxDiagnosticsPerDocument"),
|
|
31
41
|
};
|
|
@@ -34,11 +44,12 @@ export class CodeIntelligenceManager {
|
|
|
34
44
|
}, this.policy.cleanupIntervalMs);
|
|
35
45
|
this.cleanupTimer.unref();
|
|
36
46
|
}
|
|
37
|
-
async run(workspaceRoot, input) {
|
|
47
|
+
async run(workspaceRoot, input, options = {}) {
|
|
38
48
|
let project;
|
|
39
49
|
let canonicalWorkspaceRoot;
|
|
40
50
|
try {
|
|
41
51
|
canonicalWorkspaceRoot = await realpath(resolve(workspaceRoot));
|
|
52
|
+
this.assertWorkspaceRootAvailable(canonicalWorkspaceRoot);
|
|
42
53
|
project = await resolveLanguageProject({
|
|
43
54
|
workspaceRoot: canonicalWorkspaceRoot,
|
|
44
55
|
sourcePath: input.path,
|
|
@@ -51,26 +62,42 @@ export class CodeIntelligenceManager {
|
|
|
51
62
|
}
|
|
52
63
|
throw error;
|
|
53
64
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
await this.invalidateChangedServices(project);
|
|
66
|
+
const identity = languageServiceKey(project);
|
|
67
|
+
this.assertNotCoolingDown(identity, project);
|
|
68
|
+
let lastCrash;
|
|
69
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
70
|
+
this.assertWorkspaceRootAvailable(canonicalWorkspaceRoot);
|
|
71
|
+
const service = await this.acquireService(canonicalWorkspaceRoot, project);
|
|
72
|
+
let crashed = false;
|
|
73
|
+
try {
|
|
74
|
+
const result = await this.executeOperation(service, input, options.signal);
|
|
75
|
+
this.crashStates.delete(identity);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (!(error instanceof CodeIntelligenceError) || error.code !== "code.server_crashed") {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
crashed = true;
|
|
83
|
+
lastCrash = error;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await this.releaseService(service);
|
|
87
|
+
}
|
|
88
|
+
if (crashed) {
|
|
89
|
+
await this.discardService(service);
|
|
90
|
+
const failures = (this.crashStates.get(identity)?.failures ?? 0) + 1;
|
|
91
|
+
if (attempt === 0) {
|
|
92
|
+
this.crashStates.set(identity, { failures, cooldownUntil: 0 });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const cooldownUntil = Date.now() + this.crashCooldownMs;
|
|
96
|
+
this.crashStates.set(identity, { failures, cooldownUntil });
|
|
97
|
+
throw new CodeIntelligenceError("code.language_service_cooldown", `Language server ${project.definition.id} crashed repeatedly; retry after ${this.crashCooldownMs}ms. Last error: ${lastCrash.message}`);
|
|
69
98
|
}
|
|
70
99
|
}
|
|
71
|
-
|
|
72
|
-
service.release();
|
|
73
|
-
}
|
|
100
|
+
throw lastCrash ?? new CodeIntelligenceError("code.server_crashed", `Language server ${project.definition.id} failed without a recoverable result.`);
|
|
74
101
|
}
|
|
75
102
|
async shutdown() {
|
|
76
103
|
clearInterval(this.cleanupTimer);
|
|
@@ -78,11 +105,150 @@ export class CodeIntelligenceManager {
|
|
|
78
105
|
this.serviceCreations.clear();
|
|
79
106
|
const services = [...this.services.values()];
|
|
80
107
|
this.services.clear();
|
|
108
|
+
this.invalidatedServiceKeys.clear();
|
|
109
|
+
this.crashStates.clear();
|
|
110
|
+
this.retiredWorkspaceRoots.clear();
|
|
81
111
|
await Promise.allSettled(services.map((service) => service.shutdown()));
|
|
82
112
|
}
|
|
83
113
|
get size() {
|
|
84
114
|
return this.services.size;
|
|
85
115
|
}
|
|
116
|
+
stats() {
|
|
117
|
+
const services = [...this.services.values()];
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
return services.reduce((stats, service) => {
|
|
120
|
+
const serviceStats = service.runtimeStats;
|
|
121
|
+
stats.servicesActive += service.isIdle ? 0 : 1;
|
|
122
|
+
stats.servicesIdle += service.isIdle ? 1 : 0;
|
|
123
|
+
stats.processesRunning += serviceStats.processRunning ? 1 : 0;
|
|
124
|
+
stats.operationsInFlight += serviceStats.operationInFlight;
|
|
125
|
+
stats.semanticRequestsActive += serviceStats.semanticRequestsActive;
|
|
126
|
+
stats.semanticRequestsQueued += serviceStats.semanticRequestsQueued;
|
|
127
|
+
stats.openDocuments += serviceStats.openDocuments;
|
|
128
|
+
stats.diagnosticSnapshots += serviceStats.diagnosticSnapshots;
|
|
129
|
+
stats.diagnosticsRetained += serviceStats.diagnosticsRetained;
|
|
130
|
+
stats.stderrBytes += serviceStats.stderrBytes;
|
|
131
|
+
return stats;
|
|
132
|
+
}, {
|
|
133
|
+
servicesTotal: services.length,
|
|
134
|
+
servicesActive: 0,
|
|
135
|
+
servicesIdle: 0,
|
|
136
|
+
processesRunning: 0,
|
|
137
|
+
operationsInFlight: 0,
|
|
138
|
+
semanticRequestsActive: 0,
|
|
139
|
+
semanticRequestsQueued: 0,
|
|
140
|
+
openDocuments: 0,
|
|
141
|
+
diagnosticSnapshots: 0,
|
|
142
|
+
diagnosticsRetained: 0,
|
|
143
|
+
stderrBytes: 0,
|
|
144
|
+
pendingCreations: this.serviceCreations.size,
|
|
145
|
+
crashCooldowns: [...this.crashStates.values()].filter((state) => state.cooldownUntil > now).length,
|
|
146
|
+
invalidatedServices: this.invalidatedServiceKeys.size,
|
|
147
|
+
retiredWorkspaceRoots: this.retiredWorkspaceRoots.size,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async retireWorkspaceRoot(workspaceRoot) {
|
|
151
|
+
const canonicalRoot = await realpath(resolve(workspaceRoot));
|
|
152
|
+
this.retiredWorkspaceRoots.add(canonicalRoot);
|
|
153
|
+
await Promise.allSettled(this.serviceCreations.values());
|
|
154
|
+
const matching = [...this.services.entries()].filter(([, service]) => resolve(service.workspaceRoot) === canonicalRoot);
|
|
155
|
+
const active = matching.filter(([, service]) => !service.isIdle);
|
|
156
|
+
if (active.length > 0) {
|
|
157
|
+
this.retiredWorkspaceRoots.delete(canonicalRoot);
|
|
158
|
+
throw new CodeIntelligenceError("code.language_service_busy", `Cannot finalize Workspace ${canonicalRoot} while ${active.length} Language service request(s) are still active.`);
|
|
159
|
+
}
|
|
160
|
+
let releasedServices = 0;
|
|
161
|
+
for (const [key, service] of matching) {
|
|
162
|
+
if (this.services.get(key) === service)
|
|
163
|
+
this.services.delete(key);
|
|
164
|
+
this.invalidatedServiceKeys.delete(key);
|
|
165
|
+
this.crashStates.delete(key);
|
|
166
|
+
await service.shutdown();
|
|
167
|
+
releasedServices += 1;
|
|
168
|
+
}
|
|
169
|
+
this.clearIdentityStateForWorkspaceRoot(canonicalRoot);
|
|
170
|
+
return { root: canonicalRoot, releasedServices };
|
|
171
|
+
}
|
|
172
|
+
restoreWorkspaceRoot(root) {
|
|
173
|
+
this.retiredWorkspaceRoots.delete(resolve(root));
|
|
174
|
+
}
|
|
175
|
+
assertWorkspaceRootAvailable(workspaceRoot) {
|
|
176
|
+
if (!this.retiredWorkspaceRoots.has(resolve(workspaceRoot)))
|
|
177
|
+
return;
|
|
178
|
+
throw new CodeIntelligenceError("code.language_service_unavailable", "Code intelligence is unavailable because this managed-worktree Workspace is being finalized.");
|
|
179
|
+
}
|
|
180
|
+
clearIdentityStateForWorkspaceRoot(workspaceRoot) {
|
|
181
|
+
for (const key of [...this.crashStates.keys()]) {
|
|
182
|
+
if (identityBelongsToWorkspaceRoot(key, workspaceRoot))
|
|
183
|
+
this.crashStates.delete(key);
|
|
184
|
+
}
|
|
185
|
+
for (const key of [...this.invalidatedServiceKeys]) {
|
|
186
|
+
if (identityBelongsToWorkspaceRoot(key, workspaceRoot))
|
|
187
|
+
this.invalidatedServiceKeys.delete(key);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async executeOperation(service, input, signal) {
|
|
191
|
+
switch (input.operation) {
|
|
192
|
+
case "definition":
|
|
193
|
+
return service.definition(input, signal);
|
|
194
|
+
case "hover":
|
|
195
|
+
return service.hover(input, signal);
|
|
196
|
+
case "references":
|
|
197
|
+
return service.references(input, signal);
|
|
198
|
+
case "documentSymbols":
|
|
199
|
+
return service.documentSymbols(input, signal);
|
|
200
|
+
case "workspaceSymbols":
|
|
201
|
+
return service.workspaceSymbols(input, signal);
|
|
202
|
+
case "diagnostics":
|
|
203
|
+
return service.diagnostics(input, signal);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
assertNotCoolingDown(identity, project) {
|
|
207
|
+
const state = this.crashStates.get(identity);
|
|
208
|
+
if (!state?.cooldownUntil)
|
|
209
|
+
return;
|
|
210
|
+
const remaining = state.cooldownUntil - Date.now();
|
|
211
|
+
if (remaining <= 0) {
|
|
212
|
+
this.crashStates.delete(identity);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
throw new CodeIntelligenceError("code.language_service_cooldown", `Language server ${project.definition.id} is cooling down after repeated crashes; retry in ${remaining}ms.`);
|
|
216
|
+
}
|
|
217
|
+
async invalidateChangedServices(project) {
|
|
218
|
+
const root = resolve(project.projectRoot);
|
|
219
|
+
const invalidated = [];
|
|
220
|
+
for (const [key, service] of this.services) {
|
|
221
|
+
if (resolve(service.project.projectRoot) === root &&
|
|
222
|
+
service.project.definition.id === project.definition.id &&
|
|
223
|
+
service.project.definition.fingerprint !== project.definition.fingerprint) {
|
|
224
|
+
this.invalidatedServiceKeys.add(key);
|
|
225
|
+
this.crashStates.delete(key);
|
|
226
|
+
if (service.isIdle)
|
|
227
|
+
invalidated.push([key, service]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const [key, service] of invalidated) {
|
|
231
|
+
if (this.services.get(key) === service)
|
|
232
|
+
this.services.delete(key);
|
|
233
|
+
this.invalidatedServiceKeys.delete(key);
|
|
234
|
+
await service.shutdown();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async releaseService(service) {
|
|
238
|
+
service.release();
|
|
239
|
+
if (!this.invalidatedServiceKeys.has(service.key) || !service.isIdle)
|
|
240
|
+
return;
|
|
241
|
+
if (this.services.get(service.key) === service)
|
|
242
|
+
this.services.delete(service.key);
|
|
243
|
+
this.invalidatedServiceKeys.delete(service.key);
|
|
244
|
+
await service.shutdown();
|
|
245
|
+
}
|
|
246
|
+
async discardService(service) {
|
|
247
|
+
if (this.services.get(service.key) === service)
|
|
248
|
+
this.services.delete(service.key);
|
|
249
|
+
this.invalidatedServiceKeys.delete(service.key);
|
|
250
|
+
await service.shutdown();
|
|
251
|
+
}
|
|
86
252
|
async acquireService(workspaceRoot, project) {
|
|
87
253
|
const key = languageServiceKey(project);
|
|
88
254
|
const existing = this.services.get(key);
|
|
@@ -133,7 +299,7 @@ export class CodeIntelligenceManager {
|
|
|
133
299
|
}
|
|
134
300
|
}
|
|
135
301
|
async closeIdle(now = Date.now()) {
|
|
136
|
-
const stale = [...this.services.entries()].filter(([, service]) => service.
|
|
302
|
+
const stale = [...this.services.entries()].filter(([, service]) => service.isIdle && now - service.lastUsedAt >= this.policy.idleMs);
|
|
137
303
|
for (const [key, service] of stale) {
|
|
138
304
|
this.services.delete(key);
|
|
139
305
|
await service.shutdown();
|
|
@@ -143,7 +309,7 @@ export class CodeIntelligenceManager {
|
|
|
143
309
|
if (this.services.size < this.policy.maxServices)
|
|
144
310
|
return;
|
|
145
311
|
const idle = [...this.services.entries()]
|
|
146
|
-
.filter(([, service]) => service.
|
|
312
|
+
.filter(([, service]) => service.isIdle)
|
|
147
313
|
.sort((left, right) => left[1].lastUsedAt - right[1].lastUsedAt);
|
|
148
314
|
const candidate = idle[0];
|
|
149
315
|
if (!candidate) {
|
|
@@ -153,6 +319,21 @@ export class CodeIntelligenceManager {
|
|
|
153
319
|
await candidate[1].shutdown();
|
|
154
320
|
}
|
|
155
321
|
}
|
|
322
|
+
function identityBelongsToWorkspaceRoot(identity, workspaceRoot) {
|
|
323
|
+
try {
|
|
324
|
+
const parsed = JSON.parse(identity);
|
|
325
|
+
const projectRoot = Array.isArray(parsed) && typeof parsed[0] === "string"
|
|
326
|
+
? resolve(parsed[0])
|
|
327
|
+
: undefined;
|
|
328
|
+
if (!projectRoot)
|
|
329
|
+
return false;
|
|
330
|
+
const root = resolve(workspaceRoot);
|
|
331
|
+
return projectRoot === root || projectRoot.startsWith(`${root}${sep}`);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
156
337
|
function positiveInteger(value, fallback, label) {
|
|
157
338
|
const resolvedValue = value ?? fallback;
|
|
158
339
|
if (!Number.isInteger(resolvedValue) || resolvedValue < 1) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { CancellationTokenSource } from "vscode-jsonrpc";
|
|
2
|
+
import { CodeIntelligenceError } from "../code-intelligence-error.js";
|
|
3
|
+
export class SemanticRequestCoordinator {
|
|
4
|
+
options;
|
|
5
|
+
active = 0;
|
|
6
|
+
queue = [];
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
async run(label, signal, operation) {
|
|
11
|
+
const startedAt = Date.now();
|
|
12
|
+
await this.acquire(label, signal, startedAt);
|
|
13
|
+
const source = new CancellationTokenSource();
|
|
14
|
+
let settledForCaller = false;
|
|
15
|
+
let timeout;
|
|
16
|
+
let onAbort;
|
|
17
|
+
const cancellation = new Promise((_resolve, reject) => {
|
|
18
|
+
const rejectOnce = (error) => {
|
|
19
|
+
if (settledForCaller)
|
|
20
|
+
return;
|
|
21
|
+
settledForCaller = true;
|
|
22
|
+
try {
|
|
23
|
+
source.cancel();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// The Language-server connection may already have closed or crashed.
|
|
27
|
+
}
|
|
28
|
+
reject(error);
|
|
29
|
+
};
|
|
30
|
+
const remaining = Math.max(1, this.options.deadlineMs - (Date.now() - startedAt));
|
|
31
|
+
timeout = setTimeout(() => rejectOnce(new CodeIntelligenceError("code.request_timeout", `${label} exceeded the ${this.options.deadlineMs}ms semantic request deadline.`)), remaining);
|
|
32
|
+
timeout.unref();
|
|
33
|
+
if (signal) {
|
|
34
|
+
onAbort = () => rejectOnce(new CodeIntelligenceError("code.request_cancelled", `${label} was cancelled by the Host.`));
|
|
35
|
+
if (signal.aborted)
|
|
36
|
+
onAbort();
|
|
37
|
+
else
|
|
38
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const underlying = operation(source.token);
|
|
42
|
+
underlying.finally(() => {
|
|
43
|
+
source.dispose();
|
|
44
|
+
this.release();
|
|
45
|
+
}).catch(() => undefined);
|
|
46
|
+
try {
|
|
47
|
+
const value = await Promise.race([underlying, cancellation]);
|
|
48
|
+
settledForCaller = true;
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (timeout)
|
|
53
|
+
clearTimeout(timeout);
|
|
54
|
+
if (signal && onAbort)
|
|
55
|
+
signal.removeEventListener("abort", onAbort);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
get activeCount() {
|
|
59
|
+
return this.active;
|
|
60
|
+
}
|
|
61
|
+
get queuedCount() {
|
|
62
|
+
return this.queue.length;
|
|
63
|
+
}
|
|
64
|
+
async acquire(label, signal, startedAt) {
|
|
65
|
+
if (signal?.aborted) {
|
|
66
|
+
throw new CodeIntelligenceError("code.request_cancelled", `${label} was cancelled by the Host.`);
|
|
67
|
+
}
|
|
68
|
+
if (this.active < this.options.maxConcurrent) {
|
|
69
|
+
this.active += 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (this.queue.length >= this.options.maxQueued) {
|
|
73
|
+
throw new CodeIntelligenceError("code.request_capacity", `Semantic request queue capacity reached (${this.options.maxQueued}) for this Language service.`);
|
|
74
|
+
}
|
|
75
|
+
await new Promise((resolve, reject) => {
|
|
76
|
+
const waiter = { resolve, reject, signal };
|
|
77
|
+
const remaining = Math.max(1, this.options.deadlineMs - (Date.now() - startedAt));
|
|
78
|
+
const remove = () => {
|
|
79
|
+
const index = this.queue.indexOf(waiter);
|
|
80
|
+
if (index >= 0)
|
|
81
|
+
this.queue.splice(index, 1);
|
|
82
|
+
};
|
|
83
|
+
waiter.timer = setTimeout(() => {
|
|
84
|
+
remove();
|
|
85
|
+
reject(new CodeIntelligenceError("code.request_timeout", `${label} exceeded the ${this.options.deadlineMs}ms semantic request deadline while queued.`));
|
|
86
|
+
}, remaining);
|
|
87
|
+
waiter.timer.unref();
|
|
88
|
+
if (signal) {
|
|
89
|
+
waiter.onAbort = () => {
|
|
90
|
+
remove();
|
|
91
|
+
if (waiter.timer)
|
|
92
|
+
clearTimeout(waiter.timer);
|
|
93
|
+
reject(new CodeIntelligenceError("code.request_cancelled", `${label} was cancelled by the Host.`));
|
|
94
|
+
};
|
|
95
|
+
signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
96
|
+
}
|
|
97
|
+
this.queue.push(waiter);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
release() {
|
|
101
|
+
this.active = Math.max(0, this.active - 1);
|
|
102
|
+
while (this.queue.length > 0 && this.active < this.options.maxConcurrent) {
|
|
103
|
+
const waiter = this.queue.shift();
|
|
104
|
+
if (waiter.timer)
|
|
105
|
+
clearTimeout(waiter.timer);
|
|
106
|
+
if (waiter.signal && waiter.onAbort)
|
|
107
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
108
|
+
if (waiter.signal?.aborted) {
|
|
109
|
+
waiter.reject(new CodeIntelligenceError("code.request_cancelled", "Semantic request was cancelled by the Host."));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
this.active += 1;
|
|
113
|
+
waiter.resolve();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -53,7 +53,7 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
|
|
|
53
53
|
await close();
|
|
54
54
|
await rm(root, { recursive: true, force: true });
|
|
55
55
|
});
|
|
56
|
-
return { client, project, close };
|
|
56
|
+
return { client, project, codeIntelligence, close };
|
|
57
57
|
}
|
|
58
58
|
export async function callOpen(client, path, conversationScopeId) {
|
|
59
59
|
return client.callTool({
|
package/dist/server.js
CHANGED
|
@@ -755,10 +755,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
755
755
|
inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.hooks),
|
|
756
756
|
codeIntelligence: {
|
|
757
757
|
available: true,
|
|
758
|
-
run: async (input, context) => {
|
|
758
|
+
run: async (input, context, options) => {
|
|
759
759
|
try {
|
|
760
760
|
return {
|
|
761
|
-
value: await codeIntelligence.run(context.workspaceRoot, input),
|
|
761
|
+
value: await codeIntelligence.run(context.workspaceRoot, input, { signal: options.signal }),
|
|
762
762
|
};
|
|
763
763
|
}
|
|
764
764
|
catch (error) {
|
|
@@ -1254,7 +1254,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1254
1254
|
idempotentHint: false,
|
|
1255
1255
|
openWorldHint: true,
|
|
1256
1256
|
},
|
|
1257
|
-
}, async ({ workspaceId, name, action, arguments: capabilityArguments, file }) => {
|
|
1257
|
+
}, async ({ workspaceId, name, action, arguments: capabilityArguments, file }, extra) => {
|
|
1258
1258
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1259
1259
|
let changedPaths = [];
|
|
1260
1260
|
return runToolWithHooks(hooks, {
|
|
@@ -1289,7 +1289,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1289
1289
|
});
|
|
1290
1290
|
return result;
|
|
1291
1291
|
}
|
|
1292
|
-
const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), { nativeFile: file });
|
|
1292
|
+
const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), { nativeFile: file, signal: extra.signal });
|
|
1293
1293
|
changedPaths = execution.changedPaths ?? [];
|
|
1294
1294
|
const result = {
|
|
1295
1295
|
content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
|
|
@@ -1389,7 +1389,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1389
1389
|
throw new Error(`Cannot close this worktree-backed workspace while logical workspace processes are still running or awaiting completion delivery: ${busyWorkspaceIds.join(", ")}.`);
|
|
1390
1390
|
}
|
|
1391
1391
|
const startedAt = performance.now();
|
|
1392
|
-
const
|
|
1392
|
+
const retirement = await codeIntelligence.retireWorkspaceRoot(workspace.root);
|
|
1393
|
+
let closed;
|
|
1394
|
+
try {
|
|
1395
|
+
closed = await workspaces.closeWorktree(workspaceId, commitMessage);
|
|
1396
|
+
}
|
|
1397
|
+
finally {
|
|
1398
|
+
codeIntelligence.restoreWorkspaceRoot(retirement.root);
|
|
1399
|
+
}
|
|
1393
1400
|
await Promise.all(physicalWorkspaceIds.map((id) => reviewCheckpoints.releaseWorkspace(id)));
|
|
1394
1401
|
const result = [
|
|
1395
1402
|
`Closed managed-worktree-backed workspace ${workspaceId}.`,
|
|
@@ -2161,6 +2168,7 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
2161
2168
|
const logRuntimeResources = () => {
|
|
2162
2169
|
const memory = process.memoryUsage();
|
|
2163
2170
|
const processStats = processSessions.stats();
|
|
2171
|
+
const codeStats = codeIntelligence.stats();
|
|
2164
2172
|
logEvent(config.logging, "debug", "runtime_resources", {
|
|
2165
2173
|
rssBytes: memory.rss,
|
|
2166
2174
|
heapUsedBytes: memory.heapUsed,
|
|
@@ -2173,7 +2181,20 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
2173
2181
|
processesCompleted: processStats.completed,
|
|
2174
2182
|
cachedWorkspaces: workspaces.cachedWorkspaceCount,
|
|
2175
2183
|
reviewStates: reviewCheckpoints.stateCount,
|
|
2176
|
-
languageServices:
|
|
2184
|
+
languageServices: codeStats.servicesTotal,
|
|
2185
|
+
languageServicesActive: codeStats.servicesActive,
|
|
2186
|
+
languageProcessesRunning: codeStats.processesRunning,
|
|
2187
|
+
languageOperationsInFlight: codeStats.operationsInFlight,
|
|
2188
|
+
languageRequestsActive: codeStats.semanticRequestsActive,
|
|
2189
|
+
languageRequestsQueued: codeStats.semanticRequestsQueued,
|
|
2190
|
+
languageOpenDocuments: codeStats.openDocuments,
|
|
2191
|
+
languageDiagnosticSnapshots: codeStats.diagnosticSnapshots,
|
|
2192
|
+
languageDiagnosticsRetained: codeStats.diagnosticsRetained,
|
|
2193
|
+
languageStderrBytes: codeStats.stderrBytes,
|
|
2194
|
+
languagePendingCreations: codeStats.pendingCreations,
|
|
2195
|
+
languageCrashCooldowns: codeStats.crashCooldowns,
|
|
2196
|
+
languageInvalidatedServices: codeStats.invalidatedServices,
|
|
2197
|
+
languageRetiredWorkspaceRoots: codeStats.retiredWorkspaceRoots,
|
|
2177
2198
|
});
|
|
2178
2199
|
};
|
|
2179
2200
|
const transportCleanupTimer = setInterval(() => {
|
package/docs/configuration.md
CHANGED
|
@@ -154,7 +154,7 @@ force a Host to invalidate its cached schema.
|
|
|
154
154
|
### LSP code intelligence
|
|
155
155
|
|
|
156
156
|
ForgeRelay advertises `code.intelligence` through the Capability Gateway; it does
|
|
157
|
-
not add language-specific top-level MCP tools. ForgeRelay 0.4.
|
|
157
|
+
not add language-specific top-level MCP tools. ForgeRelay 0.4.5 supports
|
|
158
158
|
`definition`, `hover`, `references`, `documentSymbols`, `workspaceSymbols`, and
|
|
159
159
|
`diagnostics`.
|
|
160
160
|
Position-based operations accept the same workspace-relative source position. Hover
|
|
@@ -175,6 +175,24 @@ and the real `total` when the complete Language-server response makes it known.
|
|
|
175
175
|
dependencies: ForgeRelay may discover an executable already installed on the
|
|
176
176
|
machine, but it never downloads or installs one automatically.
|
|
177
177
|
|
|
178
|
+
ForgeRelay 0.4.5 hardens this shared Language-service runtime. Semantic requests
|
|
179
|
+
have one internal bounded deadline, Host cancellation propagates to LSP cancellation,
|
|
180
|
+
and each service has finite concurrent and queued request budgets rather than
|
|
181
|
+
Agent-configurable timeouts. An unexpected server crash is retried at most once;
|
|
182
|
+
repeated crashes enter a short cooldown. Effective server-definition fingerprints
|
|
183
|
+
invalidate only the affected project/service on the next resolution without adding
|
|
184
|
+
a recursive filesystem watcher.
|
|
185
|
+
|
|
186
|
+
Language services are keyed by physical Language project identity, so logical
|
|
187
|
+
workspaces over the same checkout reuse one process. Truly idle services are
|
|
188
|
+
reclaimed after a bounded TTL and the global service cap evicts the least-recently-
|
|
189
|
+
used safe idle service. A server request that ignores cancellation still counts as
|
|
190
|
+
active until the underlying JSON-RPC request settles. Managed-worktree finalization
|
|
191
|
+
releases services rooted in that worktree before removal and refuses finalization
|
|
192
|
+
while semantic work is active. Debug `runtime_resources` telemetry includes only
|
|
193
|
+
aggregate Language-service/process/request/document/diagnostic/stderr counts; it
|
|
194
|
+
does not log source contents or source paths.
|
|
195
|
+
|
|
178
196
|
Effective Language-server definitions resolve in this order:
|
|
179
197
|
|
|
180
198
|
1. project configuration in `.forgerelay/language-servers.json`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Local development control plane for MCP coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Akira-TL/forgerelay#readme",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"debug:accept": "node scripts/debug/accept.mjs",
|
|
43
43
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
44
44
|
"start": "node dist/cli.js serve",
|
|
45
|
-
"test": "tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspace-store.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
45
|
+
"test": "tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspace-store.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
46
46
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
47
47
|
"release:check": "node scripts/release-version.mjs check",
|
|
48
48
|
"release:tag-check": "node scripts/release-version.mjs tag",
|