ast-mcp-server 0.11.1 → 0.12.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/CHANGELOG.md +35 -2
- package/README.md +32 -2
- package/SECURITY.md +1 -1
- package/dist/batch/schema.js +7 -16
- package/dist/compiler-worker-entry.js +51 -0
- package/dist/index.js +20 -120
- package/dist/server.js +2 -32
- package/dist/services/agent-targets.js +2 -14
- package/dist/services/compiler-worker-host.js +422 -0
- package/dist/services/compiler-worker-protocol.js +95 -0
- package/dist/services/diagnostics.js +55 -0
- package/dist/services/operations.js +3 -1
- package/dist/services/project.js +7 -0
- package/dist/services/public-errors.js +6 -3
- package/dist/services/runtime-logger.js +5 -0
- package/dist/services/runtime-policy.js +61 -0
- package/dist/tools/apply_operation.js +5 -2
- package/dist/tools/catalog.js +80 -0
- package/dist/tools/explore.js +5 -2
- package/dist/tools/find_references.js +5 -2
- package/dist/tools/find_test_candidates.js +5 -2
- package/dist/tools/get_diagnostics.js +36 -4
- package/dist/tools/get_file.js +5 -2
- package/dist/tools/get_impact.js +5 -2
- package/dist/tools/get_operation_preview.js +5 -2
- package/dist/tools/get_outline.js +5 -2
- package/dist/tools/get_project_status.js +5 -2
- package/dist/tools/get_symbol_source.js +5 -2
- package/dist/tools/list_files.js +5 -2
- package/dist/tools/rename_symbol.js +5 -2
- package/dist/tools/replace_symbol_body.js +5 -2
- package/dist/tools/scaffold_class.js +5 -2
- package/dist/tools/search_symbols.js +5 -2
- package/dist/worker-runtime.js +126 -0
- package/docs/support.md +22 -6
- package/package.json +3 -3
- package/skills/structural-code-editing/SKILL.md +14 -2
- package/skills/structural-code-editing/releases.json +17 -2
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { decodeCompilerWorkerEnvelope, startCompilerWorker } from "./compiler-worker-protocol.js";
|
|
8
|
+
import { createCompilerWorkerSpawnSpec, readRuntimePolicy, } from "./runtime-policy.js";
|
|
9
|
+
import { emitCompilerWorkerEvent } from "./runtime-logger.js";
|
|
10
|
+
import { PublicOperationalError, renderPublicError } from "./public-errors.js";
|
|
11
|
+
export class CompilerWorkerHostError extends Error {
|
|
12
|
+
kind;
|
|
13
|
+
recovery;
|
|
14
|
+
constructor(kind, recovery) {
|
|
15
|
+
super("Compiler worker request failed.");
|
|
16
|
+
this.kind = kind;
|
|
17
|
+
this.recovery = recovery;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const bounded = (value) => Buffer.byteLength(JSON.stringify(value)) <= 256 * 1024;
|
|
21
|
+
const within = (promise, ms) => Promise.race([
|
|
22
|
+
promise,
|
|
23
|
+
delay(ms, undefined, { ref: false }).then(() => {
|
|
24
|
+
throw new CompilerWorkerHostError("startup");
|
|
25
|
+
}),
|
|
26
|
+
]);
|
|
27
|
+
function deferred() {
|
|
28
|
+
let resolve;
|
|
29
|
+
let reject;
|
|
30
|
+
const promise = new Promise((yes, no) => {
|
|
31
|
+
resolve = yes;
|
|
32
|
+
reject = no;
|
|
33
|
+
});
|
|
34
|
+
return { promise, resolve, reject };
|
|
35
|
+
}
|
|
36
|
+
export function spawnCompilerWorkerProcess(options) {
|
|
37
|
+
const spec = createCompilerWorkerSpawnSpec(options.workerEntryPath, options.environment);
|
|
38
|
+
if (!spec.ok || !spec.command)
|
|
39
|
+
throw new CompilerWorkerHostError("startup");
|
|
40
|
+
const child = spawn(spec.command, spec.args, {
|
|
41
|
+
...spec.options,
|
|
42
|
+
stdio: ["pipe", "pipe", "inherit", "ipc"],
|
|
43
|
+
});
|
|
44
|
+
const ready = deferred();
|
|
45
|
+
const closed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
46
|
+
const pending = new Map();
|
|
47
|
+
const snapshots = [];
|
|
48
|
+
const acknowledgements = new Map();
|
|
49
|
+
let outbound = 0;
|
|
50
|
+
let inbound = 0;
|
|
51
|
+
let controlInbound = 0;
|
|
52
|
+
let writes = 0;
|
|
53
|
+
const fail = (error) => {
|
|
54
|
+
ready.reject(error);
|
|
55
|
+
for (const request of pending.values())
|
|
56
|
+
request.reject(error);
|
|
57
|
+
pending.clear();
|
|
58
|
+
for (const snapshot of snapshots.splice(0))
|
|
59
|
+
snapshot.reject(error);
|
|
60
|
+
for (const acknowledged of acknowledgements.values())
|
|
61
|
+
acknowledged.reject(error);
|
|
62
|
+
acknowledgements.clear();
|
|
63
|
+
};
|
|
64
|
+
child.once("close", () => fail(new CompilerWorkerHostError("worker_exit")));
|
|
65
|
+
child.on("message", (message) => {
|
|
66
|
+
const frame = JSON.stringify(message);
|
|
67
|
+
const decoded = decodeCompilerWorkerEnvelope(frame);
|
|
68
|
+
if (!decoded.ok ||
|
|
69
|
+
decoded.value.generation !== options.generation ||
|
|
70
|
+
decoded.value.sequence <= controlInbound ||
|
|
71
|
+
!["ready", "snapshot", "settled"].includes(decoded.value.type))
|
|
72
|
+
return fail(new CompilerWorkerHostError("protocol"));
|
|
73
|
+
controlInbound = decoded.value.sequence;
|
|
74
|
+
if (decoded.value.type === "settled") {
|
|
75
|
+
const writeSequence = decoded.value.payload.write_sequence;
|
|
76
|
+
if (!Number.isSafeInteger(writeSequence))
|
|
77
|
+
return fail(new CompilerWorkerHostError("protocol"));
|
|
78
|
+
acknowledgements.get(writeSequence)?.resolve();
|
|
79
|
+
acknowledgements.delete(writeSequence);
|
|
80
|
+
}
|
|
81
|
+
else if (decoded.value.type === "ready") {
|
|
82
|
+
inbound = 1;
|
|
83
|
+
ready.resolve(frame);
|
|
84
|
+
}
|
|
85
|
+
else
|
|
86
|
+
snapshots.shift()?.resolve(decoded.value);
|
|
87
|
+
});
|
|
88
|
+
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
89
|
+
if (Buffer.byteLength(line) > 256 * 1024)
|
|
90
|
+
return fail(new CompilerWorkerHostError("protocol"));
|
|
91
|
+
let message;
|
|
92
|
+
try {
|
|
93
|
+
message = JSON.parse(line);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return fail(new CompilerWorkerHostError("protocol"));
|
|
97
|
+
}
|
|
98
|
+
if (!("id" in message) || message.id === undefined)
|
|
99
|
+
return;
|
|
100
|
+
const request = pending.get(message.id);
|
|
101
|
+
if (!request)
|
|
102
|
+
return;
|
|
103
|
+
pending.delete(message.id);
|
|
104
|
+
request.resolve({
|
|
105
|
+
generation: options.generation,
|
|
106
|
+
sequence: ++inbound,
|
|
107
|
+
payload: { request_id: message.id, message },
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
const write = (message, acknowledge = false) => {
|
|
111
|
+
if (!bounded(message) || !child.stdin?.writable)
|
|
112
|
+
return Promise.reject(new CompilerWorkerHostError("protocol"));
|
|
113
|
+
options.beforeWrite?.(message);
|
|
114
|
+
const writeSequence = ++writes, processed = acknowledge ? deferred() : undefined;
|
|
115
|
+
if (processed) {
|
|
116
|
+
acknowledgements.set(writeSequence, processed);
|
|
117
|
+
void processed.promise.catch(() => undefined);
|
|
118
|
+
}
|
|
119
|
+
const written = new Promise((resolve, reject) => child.stdin.write(`${JSON.stringify(message)}\n`, (error) => error ? (acknowledgements.delete(writeSequence), reject(error)) : resolve()));
|
|
120
|
+
return processed ? written.then(() => processed.promise) : written;
|
|
121
|
+
};
|
|
122
|
+
const control = (type, payload) => {
|
|
123
|
+
const frame = { v: 1, generation: options.generation, sequence: ++outbound, type, payload };
|
|
124
|
+
if (!bounded(frame) || !child.connected)
|
|
125
|
+
return Promise.reject(new CompilerWorkerHostError("protocol"));
|
|
126
|
+
return new Promise((resolve, reject) => child.send(frame, (error) => (error ? reject(error) : resolve())));
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
generation: options.generation,
|
|
130
|
+
ready: ready.promise,
|
|
131
|
+
async replay(frames) {
|
|
132
|
+
await control("handshake", {});
|
|
133
|
+
await ready.promise;
|
|
134
|
+
for (const frame of frames)
|
|
135
|
+
await write(frame);
|
|
136
|
+
},
|
|
137
|
+
forward(message) {
|
|
138
|
+
if (!("id" in message))
|
|
139
|
+
return write(message, true).then(() => undefined);
|
|
140
|
+
if (message.id === undefined)
|
|
141
|
+
return Promise.reject(new CompilerWorkerHostError("protocol"));
|
|
142
|
+
const result = deferred();
|
|
143
|
+
const id = message.id;
|
|
144
|
+
pending.set(id, result);
|
|
145
|
+
void write(message).catch((error) => {
|
|
146
|
+
pending.delete(id);
|
|
147
|
+
result.reject(error);
|
|
148
|
+
});
|
|
149
|
+
return result.promise;
|
|
150
|
+
},
|
|
151
|
+
cancel(id) {
|
|
152
|
+
if (!pending.has(id))
|
|
153
|
+
return false;
|
|
154
|
+
void write({ jsonrpc: "2.0", method: "notifications/cancelled", params: { requestId: id } }, true).catch(() => undefined);
|
|
155
|
+
return true;
|
|
156
|
+
},
|
|
157
|
+
async snapshot() {
|
|
158
|
+
await Promise.all([...acknowledgements.values()].map(({ promise }) => promise));
|
|
159
|
+
const result = deferred();
|
|
160
|
+
snapshots.push(result);
|
|
161
|
+
await control("snapshot", {});
|
|
162
|
+
return (await result.promise).payload;
|
|
163
|
+
},
|
|
164
|
+
terminate: () => {
|
|
165
|
+
child.kill("SIGKILL");
|
|
166
|
+
},
|
|
167
|
+
reap: () => closed,
|
|
168
|
+
async shutdown(trigger) {
|
|
169
|
+
await control("shutdown", { trigger }).catch(() => undefined);
|
|
170
|
+
child.stdin?.end();
|
|
171
|
+
await closed;
|
|
172
|
+
return "complete";
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export class CompilerWorkerHost {
|
|
177
|
+
spawn;
|
|
178
|
+
timeoutMs;
|
|
179
|
+
lifecycle;
|
|
180
|
+
generation = 0;
|
|
181
|
+
sequence = 0;
|
|
182
|
+
child;
|
|
183
|
+
starting;
|
|
184
|
+
active = new Map();
|
|
185
|
+
leases = new Map();
|
|
186
|
+
initialization = [];
|
|
187
|
+
activity = 0;
|
|
188
|
+
parentWork = 0;
|
|
189
|
+
parentAdmission = "open";
|
|
190
|
+
idleTimer;
|
|
191
|
+
retiring;
|
|
192
|
+
constructor(spawn, timeoutMs = 10_000, lifecycle = {}) {
|
|
193
|
+
this.spawn = spawn;
|
|
194
|
+
this.timeoutMs = timeoutMs;
|
|
195
|
+
this.lifecycle = lifecycle;
|
|
196
|
+
}
|
|
197
|
+
now() {
|
|
198
|
+
return (this.lifecycle.now ?? Date.now)();
|
|
199
|
+
}
|
|
200
|
+
// prettier-ignore
|
|
201
|
+
pruneLeases() { const now = this.now(); for (const [id, expiry] of this.leases)
|
|
202
|
+
if (expiry <= now)
|
|
203
|
+
this.leases.delete(id); }
|
|
204
|
+
// prettier-ignore
|
|
205
|
+
addLease(id) { if (typeof id !== "string" || !id || !bounded(id))
|
|
206
|
+
return; this.pruneLeases(); this.leases.set(id, this.now() + (this.lifecycle.leaseTtlMs ?? 15 * 60_000)); while (this.leases.size > (this.lifecycle.leaseLimit ?? 128))
|
|
207
|
+
this.leases.delete(this.leases.keys().next().value); }
|
|
208
|
+
// prettier-ignore
|
|
209
|
+
scheduleIdle() { clearTimeout(this.idleTimer); const ttl = this.lifecycle.idleTtlMs ?? 0; if (ttl > 0 && this.child)
|
|
210
|
+
this.idleTimer = setTimeout(() => { this.retiring = this.recycle(this.activity).catch(async () => { const child = this.child; await child?.reap().catch(() => undefined); if (this.child === child)
|
|
211
|
+
this.child = undefined; }).finally(() => { this.retiring = undefined; }); }, ttl); }
|
|
212
|
+
// prettier-ignore
|
|
213
|
+
static quiescent(value) { return value.runtime_admission === "open" && value.project_admission === "open" && ["active_requests", "active_sends", "active_operations", "queued_operations", "completion_critical_operations"].every((key) => value[key] === 0) && value.mutation_history === false; }
|
|
214
|
+
// prettier-ignore
|
|
215
|
+
async recycle(activity) { this.pruneLeases(); const child = this.child, generation = this.generation, sequence = this.sequence; if (!child || this.parentWork || this.leases.size || child.generation !== generation)
|
|
216
|
+
return this.scheduleIdle(); const first = await child.snapshot(); if (!CompilerWorkerHost.quiescent(first) || activity !== this.activity || sequence !== this.sequence)
|
|
217
|
+
return this.scheduleIdle(); this.parentAdmission = "closed"; const second = await child.snapshot(); if (child !== this.child || generation !== this.generation || sequence !== this.sequence || activity !== this.activity || !CompilerWorkerHost.quiescent(second)) {
|
|
218
|
+
this.parentAdmission = "open";
|
|
219
|
+
return this.scheduleIdle();
|
|
220
|
+
} await this.stopChild(child, "requested"); emitCompilerWorkerEvent({ kind: "idle", generation }); this.parentAdmission = "open"; }
|
|
221
|
+
// prettier-ignore
|
|
222
|
+
async stopChild(child, trigger) { const completion = child.shutdown(trigger); try {
|
|
223
|
+
await within(completion, this.timeoutMs);
|
|
224
|
+
if (this.child === child)
|
|
225
|
+
this.child = undefined;
|
|
226
|
+
return "complete";
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
const snapshot = await child.snapshot().catch(() => undefined);
|
|
230
|
+
if (snapshot?.completion_critical_operations) {
|
|
231
|
+
await completion;
|
|
232
|
+
if (this.child === child)
|
|
233
|
+
this.child = undefined;
|
|
234
|
+
return "complete";
|
|
235
|
+
}
|
|
236
|
+
child.terminate();
|
|
237
|
+
await child.reap().catch(() => undefined);
|
|
238
|
+
if (this.child === child)
|
|
239
|
+
this.child = undefined;
|
|
240
|
+
return "forced";
|
|
241
|
+
} }
|
|
242
|
+
async start(initialization) {
|
|
243
|
+
if (this.child)
|
|
244
|
+
return { generation: this.child.generation, sequence: this.sequence };
|
|
245
|
+
if (this.starting)
|
|
246
|
+
return this.starting;
|
|
247
|
+
this.starting = (async () => {
|
|
248
|
+
let child;
|
|
249
|
+
try {
|
|
250
|
+
const envelope = initialization.length === 2
|
|
251
|
+
? await startCompilerWorker({
|
|
252
|
+
frames: initialization,
|
|
253
|
+
timeoutMs: this.timeoutMs,
|
|
254
|
+
spawn: () => (child = this.spawn(++this.generation)),
|
|
255
|
+
})
|
|
256
|
+
: await (async () => {
|
|
257
|
+
child = this.spawn(++this.generation);
|
|
258
|
+
await child.replay([]);
|
|
259
|
+
const decoded = decodeCompilerWorkerEnvelope(await within(child.ready, this.timeoutMs));
|
|
260
|
+
if (!decoded.ok || decoded.value.type !== "ready")
|
|
261
|
+
throw new Error();
|
|
262
|
+
return decoded.value;
|
|
263
|
+
})();
|
|
264
|
+
if (!child || envelope.generation !== child.generation)
|
|
265
|
+
throw new Error();
|
|
266
|
+
this.child = child;
|
|
267
|
+
this.sequence = envelope.sequence;
|
|
268
|
+
this.parentAdmission = "open";
|
|
269
|
+
this.scheduleIdle();
|
|
270
|
+
return { generation: child.generation, sequence: this.sequence };
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
child?.terminate();
|
|
274
|
+
await child?.reap().catch(() => undefined);
|
|
275
|
+
throw new CompilerWorkerHostError("startup");
|
|
276
|
+
}
|
|
277
|
+
})().finally(() => {
|
|
278
|
+
this.starting = undefined;
|
|
279
|
+
});
|
|
280
|
+
return this.starting;
|
|
281
|
+
}
|
|
282
|
+
async forward(message) {
|
|
283
|
+
if (!bounded(message))
|
|
284
|
+
throw new CompilerWorkerHostError("protocol");
|
|
285
|
+
await this.retiring;
|
|
286
|
+
this.activity += 1;
|
|
287
|
+
await this.start(this.initialization.length === 2 ? this.initialization : []);
|
|
288
|
+
this.parentWork += 1;
|
|
289
|
+
const id = "id" in message ? message.id : undefined;
|
|
290
|
+
if (id !== undefined)
|
|
291
|
+
this.active.set(id, this.generation);
|
|
292
|
+
try {
|
|
293
|
+
const settled = await this.child.forward(message);
|
|
294
|
+
if ("method" in message && message.method === "initialize")
|
|
295
|
+
this.initialization = [message];
|
|
296
|
+
if ("method" in message &&
|
|
297
|
+
message.method === "notifications/initialized" &&
|
|
298
|
+
this.initialization.length === 1)
|
|
299
|
+
this.initialization.push(message);
|
|
300
|
+
if (!settled)
|
|
301
|
+
return undefined;
|
|
302
|
+
const payload = settled.payload;
|
|
303
|
+
if (settled.generation !== this.generation ||
|
|
304
|
+
settled.sequence <= this.sequence ||
|
|
305
|
+
payload.request_id !== id ||
|
|
306
|
+
!payload.message)
|
|
307
|
+
throw new CompilerWorkerHostError("protocol");
|
|
308
|
+
this.sequence = settled.sequence;
|
|
309
|
+
const response = payload.message;
|
|
310
|
+
const result = "result" in response ? response.result : undefined;
|
|
311
|
+
this.addLease(result?.operation_id ??
|
|
312
|
+
result?.structuredContent?.operation_id);
|
|
313
|
+
return response;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
if (error instanceof CompilerWorkerHostError && error.kind === "worker_exit") {
|
|
317
|
+
const child = this.child;
|
|
318
|
+
await child?.reap().catch(() => undefined);
|
|
319
|
+
if (this.child === child)
|
|
320
|
+
this.child = undefined;
|
|
321
|
+
emitCompilerWorkerEvent({
|
|
322
|
+
kind: "crash",
|
|
323
|
+
generation: child?.generation ?? this.generation,
|
|
324
|
+
});
|
|
325
|
+
const params = "params" in message
|
|
326
|
+
? message.params
|
|
327
|
+
: undefined;
|
|
328
|
+
const operationId = params?.name === "ast_apply_operation" ? params.arguments?.operation_id : undefined;
|
|
329
|
+
if (typeof operationId === "string") {
|
|
330
|
+
this.addLease(operationId);
|
|
331
|
+
const correlationId = randomUUID();
|
|
332
|
+
emitCompilerWorkerEvent({
|
|
333
|
+
kind: "ambiguity",
|
|
334
|
+
generation: child?.generation ?? this.generation,
|
|
335
|
+
correlationId,
|
|
336
|
+
count: this.leases.size,
|
|
337
|
+
});
|
|
338
|
+
throw new CompilerWorkerHostError("ambiguous_apply", {
|
|
339
|
+
correlation_id: correlationId,
|
|
340
|
+
expires_at: this.leases.get(operationId),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
throw error instanceof CompilerWorkerHostError
|
|
345
|
+
? error
|
|
346
|
+
: new CompilerWorkerHostError("worker_exit");
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
if (id !== undefined)
|
|
350
|
+
this.active.delete(id);
|
|
351
|
+
this.parentWork -= 1;
|
|
352
|
+
this.scheduleIdle();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
cancel(id) {
|
|
356
|
+
if (this.active.get(id) !== this.generation || !this.child)
|
|
357
|
+
return "not_found";
|
|
358
|
+
return this.child.cancel(id) ? "forwarded" : "not_found";
|
|
359
|
+
}
|
|
360
|
+
async shutdown(trigger) {
|
|
361
|
+
clearTimeout(this.idleTimer);
|
|
362
|
+
await this.starting?.catch(() => undefined);
|
|
363
|
+
if (!this.child)
|
|
364
|
+
return "complete";
|
|
365
|
+
const child = this.child;
|
|
366
|
+
this.parentAdmission = "closed";
|
|
367
|
+
return this.stopChild(child, trigger);
|
|
368
|
+
}
|
|
369
|
+
snapshot() {
|
|
370
|
+
this.pruneLeases();
|
|
371
|
+
return {
|
|
372
|
+
generation: this.generation,
|
|
373
|
+
sequence: this.sequence,
|
|
374
|
+
state: this.child ? "ready" : this.starting ? "starting" : "idle",
|
|
375
|
+
active_requests: this.active.size,
|
|
376
|
+
parent_work: this.parentWork,
|
|
377
|
+
parent_admission: this.parentAdmission,
|
|
378
|
+
lease_tombstones: this.leases.size,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
export async function runSupervisedStdioServer() {
|
|
383
|
+
const entry = fileURLToPath(new URL("../compiler-worker-entry.js", import.meta.url));
|
|
384
|
+
const policy = readRuntimePolicy(process.env);
|
|
385
|
+
const host = new CompilerWorkerHost((generation) => spawnCompilerWorkerProcess({ generation, workerEntryPath: entry, environment: process.env }), 10_000, { idleTtlMs: policy.compilerWorkerIdleTtlMs });
|
|
386
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
387
|
+
const relay = async (line) => {
|
|
388
|
+
let message;
|
|
389
|
+
try {
|
|
390
|
+
message = JSON.parse(line);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// prettier-ignore
|
|
396
|
+
if ("method" in message && message.method === "notifications/cancelled") {
|
|
397
|
+
const id = message.params?.requestId;
|
|
398
|
+
if ((typeof id === "string" || typeof id === "number") && bounded(id))
|
|
399
|
+
host.cancel(id);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const response = await host.forward(message);
|
|
404
|
+
if (response)
|
|
405
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if ("id" in message) {
|
|
409
|
+
const ambiguous = error instanceof CompilerWorkerHostError &&
|
|
410
|
+
error.kind === "ambiguous_apply" &&
|
|
411
|
+
error.recovery;
|
|
412
|
+
const rendered = ambiguous
|
|
413
|
+
? renderPublicError(new PublicOperationalError("AMBIGUOUS_APPLY", "The apply outcome is ambiguous."), ambiguous.correlation_id).envelope.error
|
|
414
|
+
: undefined;
|
|
415
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, error: { code: -32603, message: rendered?.message ?? "Compiler worker request failed.", ...(ambiguous ? { data: { code: "AMBIGUOUS_APPLY", ...ambiguous } } : {}) } })}\n`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
lines.on("line", (line) => void relay(line));
|
|
420
|
+
lines.once("close", () => void host.shutdown("parent_exit"));
|
|
421
|
+
return { shutdown: () => host.shutdown("requested"), snapshot: () => host.snapshot() };
|
|
422
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
const FIELDS = new Set(["v", "generation", "sequence", "type", "payload"]);
|
|
3
|
+
const TYPES = new Set(["handshake", "snapshot", "shutdown", "ready", "settled", "lease", "exit"]);
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
export function decodeCompilerWorkerEnvelope(frame) {
|
|
8
|
+
if (Buffer.byteLength(frame) > 256 * 1024) {
|
|
9
|
+
return { ok: false, reason: "oversized" };
|
|
10
|
+
}
|
|
11
|
+
let value;
|
|
12
|
+
try {
|
|
13
|
+
value = JSON.parse(frame);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { ok: false, reason: "malformed" };
|
|
17
|
+
}
|
|
18
|
+
if (!isRecord(value))
|
|
19
|
+
return { ok: false, reason: "invalid_envelope" };
|
|
20
|
+
if (Object.keys(value).some((key) => !FIELDS.has(key)))
|
|
21
|
+
return { ok: false, reason: "unknown_field" };
|
|
22
|
+
if (value.v !== 1)
|
|
23
|
+
return { ok: false, reason: "version_drift" };
|
|
24
|
+
if (typeof value.type !== "string" || !TYPES.has(value.type)) {
|
|
25
|
+
return { ok: false, reason: "unknown_type" };
|
|
26
|
+
}
|
|
27
|
+
if (!Number.isSafeInteger(value.generation) ||
|
|
28
|
+
value.generation < 1 ||
|
|
29
|
+
!Number.isSafeInteger(value.sequence) ||
|
|
30
|
+
value.sequence < 1 ||
|
|
31
|
+
!isRecord(value.payload)) {
|
|
32
|
+
return { ok: false, reason: "invalid_envelope" };
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, value };
|
|
35
|
+
}
|
|
36
|
+
export function validateInitializationReplay(frames) {
|
|
37
|
+
if (frames.length < 2)
|
|
38
|
+
return { ok: false, reason: "missing_replay" };
|
|
39
|
+
if (frames.length > 2)
|
|
40
|
+
return { ok: false, reason: "unexpected_replay" };
|
|
41
|
+
const [request, notification] = frames;
|
|
42
|
+
if (!isRecord(request) ||
|
|
43
|
+
request.method !== "initialize" ||
|
|
44
|
+
!("id" in request) ||
|
|
45
|
+
!isRecord(notification) ||
|
|
46
|
+
notification.method !== "notifications/initialized" ||
|
|
47
|
+
"id" in notification) {
|
|
48
|
+
return { ok: false, reason: "missing_replay" };
|
|
49
|
+
}
|
|
50
|
+
if (Buffer.byteLength(JSON.stringify(frames)) > 256 * 1024) {
|
|
51
|
+
return { ok: false, reason: "oversized_replay" };
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, frames: [request, notification] };
|
|
54
|
+
}
|
|
55
|
+
export class CompilerWorkerStartupError extends Error {
|
|
56
|
+
reason;
|
|
57
|
+
constructor(reason) {
|
|
58
|
+
super("Compiler worker startup failed.");
|
|
59
|
+
this.reason = reason;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function attempt(options, frames) {
|
|
63
|
+
const child = await options.spawn();
|
|
64
|
+
let timeout;
|
|
65
|
+
try {
|
|
66
|
+
await child.replay(frames);
|
|
67
|
+
const timedOut = new Promise((_, reject) => {
|
|
68
|
+
timeout = setTimeout(() => reject(new CompilerWorkerStartupError("timeout")), options.timeoutMs ?? 10_000);
|
|
69
|
+
});
|
|
70
|
+
const decoded = decodeCompilerWorkerEnvelope(await Promise.race([child.ready, timedOut]));
|
|
71
|
+
if (!decoded.ok || decoded.value.type !== "ready")
|
|
72
|
+
throw new CompilerWorkerStartupError("protocol");
|
|
73
|
+
return decoded.value;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
child.terminate();
|
|
77
|
+
await child.reap();
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (timeout !== undefined)
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function startCompilerWorker(options) {
|
|
86
|
+
const replay = validateInitializationReplay(options.frames);
|
|
87
|
+
if (!replay.ok)
|
|
88
|
+
throw new CompilerWorkerStartupError(replay.reason);
|
|
89
|
+
try {
|
|
90
|
+
return await attempt(options, replay.frames);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return attempt(options, replay.frames);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { DiagnosticCategory, ts } from "ts-morph";
|
|
3
3
|
import { NO_REQUEST_CONTEXT } from "./request-context.js";
|
|
4
|
+
export const DIAGNOSTIC_AGGREGATE_GROUP_LIMIT = 20;
|
|
5
|
+
function safeCount(value) {
|
|
6
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
7
|
+
throw new RangeError("Diagnostic aggregate count is outside the safe integer range.");
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function increment(counts, key) {
|
|
12
|
+
counts.set(key, safeCount((counts.get(key) ?? 0) + 1));
|
|
13
|
+
}
|
|
14
|
+
function rankGroups(counts, createGroup, compareKey, groupedDiagnosticCount, requestContext) {
|
|
15
|
+
const groups = [];
|
|
16
|
+
for (const [key, count] of counts) {
|
|
17
|
+
requestContext.checkpoint();
|
|
18
|
+
groups.push(createGroup(key, safeCount(count)));
|
|
19
|
+
}
|
|
20
|
+
groups.sort((left, right) => {
|
|
21
|
+
requestContext.checkpoint();
|
|
22
|
+
return right.count - left.count || compareKey(left, right);
|
|
23
|
+
});
|
|
24
|
+
const selected = groups.slice(0, DIAGNOSTIC_AGGREGATE_GROUP_LIMIT);
|
|
25
|
+
const covered = safeCount(selected.reduce((total, group) => safeCount(total + group.count), 0));
|
|
26
|
+
const omittedGroups = safeCount(groups.length - selected.length);
|
|
27
|
+
return {
|
|
28
|
+
groups: selected,
|
|
29
|
+
total_group_count: safeCount(groups.length),
|
|
30
|
+
omitted_group_count: omittedGroups,
|
|
31
|
+
covered_diagnostic_count: covered,
|
|
32
|
+
omitted_diagnostic_count: safeCount(groupedDiagnosticCount - covered),
|
|
33
|
+
truncated: omittedGroups > 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function buildDiagnosticAggregates(diagnostics, requestContext = NO_REQUEST_CONTEXT) {
|
|
37
|
+
requestContext.checkpoint();
|
|
38
|
+
const codeCounts = new Map();
|
|
39
|
+
const fileCounts = new Map();
|
|
40
|
+
let unfiled = 0;
|
|
41
|
+
for (const diagnostic of diagnostics) {
|
|
42
|
+
requestContext.checkpoint();
|
|
43
|
+
increment(codeCounts, diagnostic.code);
|
|
44
|
+
if (diagnostic.file === null) {
|
|
45
|
+
unfiled = safeCount(unfiled + 1);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
increment(fileCounts, diagnostic.file);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const codes = rankGroups(codeCounts, (code, count) => ({ code, count }), (left, right) => left.code - right.code, safeCount(diagnostics.length), requestContext);
|
|
52
|
+
const files = rankGroups(fileCounts, (file, count) => ({ file, count }), (left, right) => (left.file < right.file ? -1 : left.file > right.file ? 1 : 0), safeCount(diagnostics.length - unfiled), requestContext);
|
|
53
|
+
return {
|
|
54
|
+
group_limit: DIAGNOSTIC_AGGREGATE_GROUP_LIMIT,
|
|
55
|
+
codes,
|
|
56
|
+
files: { ...files, unfiled_diagnostic_count: unfiled },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
4
59
|
export function normalizeDiagnostic(diagnostic, projectRoot, requestContext = NO_REQUEST_CONTEXT) {
|
|
5
60
|
requestContext.checkpoint();
|
|
6
61
|
const sourceFile = diagnostic.getSourceFile();
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { createTwoFilesPatch } from "diff";
|
|
6
6
|
import { LanguageVariant, Node, ScriptTarget, SyntaxKind, ts } from "ts-morph";
|
|
7
7
|
import { compareDiagnostics, normalizeDiagnostic, } from "./diagnostics.js";
|
|
8
|
-
import { createFreshProject, findDeclarationByName, getSourceFileOrThrow, invalidateProjectIfIdle, withProjectOperation, } from "./project.js";
|
|
8
|
+
import { createFreshProject, findDeclarationByName, getSourceFileOrThrow, invalidateProjectIfIdle, recordProjectMutationHistory, withProjectOperation, } from "./project.js";
|
|
9
9
|
import { NO_REQUEST_CONTEXT } from "./request-context.js";
|
|
10
10
|
import { executableDeclaration } from "./symbols.js";
|
|
11
11
|
import { buildClassScaffold } from "./scaffold.js";
|
|
@@ -290,6 +290,7 @@ async function createPlan(projectRootInput, kind, allowNewErrors, mutate, reques
|
|
|
290
290
|
};
|
|
291
291
|
operationContext.checkpoint();
|
|
292
292
|
operations.set(operationId, record);
|
|
293
|
+
recordProjectMutationHistory();
|
|
293
294
|
retainedOperationId = operationId;
|
|
294
295
|
await operationTestHooks.afterRetain?.(operationId);
|
|
295
296
|
return publicOperation(record);
|
|
@@ -846,6 +847,7 @@ export function importOperationRecord(persisted, expectedPlanHash) {
|
|
|
846
847
|
}
|
|
847
848
|
pruneOperations();
|
|
848
849
|
operations.set(record.operation_id, record);
|
|
850
|
+
recordProjectMutationHistory();
|
|
849
851
|
return publicOperation(record);
|
|
850
852
|
}
|
|
851
853
|
export function setOperationTestHooksForTests(hooks) {
|
package/dist/services/project.js
CHANGED
|
@@ -23,6 +23,7 @@ const projectRoots = new WeakMap();
|
|
|
23
23
|
let projectSessionAccessSequence = 0n;
|
|
24
24
|
let projectRuntimePolicy;
|
|
25
25
|
let projectRuntimeAdmission = "open";
|
|
26
|
+
let projectMutationHistory = false;
|
|
26
27
|
function nextProjectSessionAccessSequence() {
|
|
27
28
|
projectSessionAccessSequence += 1n;
|
|
28
29
|
return projectSessionAccessSequence;
|
|
@@ -791,12 +792,17 @@ export function clearProjectSessions() {
|
|
|
791
792
|
projectSessionAccessSequence = 0n;
|
|
792
793
|
projectRuntimePolicy = undefined;
|
|
793
794
|
projectRuntimeAdmission = "open";
|
|
795
|
+
projectMutationHistory = false;
|
|
796
|
+
}
|
|
797
|
+
export function recordProjectMutationHistory() {
|
|
798
|
+
projectMutationHistory = true;
|
|
794
799
|
}
|
|
795
800
|
export function prepareProjectRuntimeForStartup() {
|
|
796
801
|
if (projectSessions.size !== 0) {
|
|
797
802
|
throw new Error("Cannot start a project runtime while sessions remain open.");
|
|
798
803
|
}
|
|
799
804
|
projectRuntimeAdmission = "open";
|
|
805
|
+
projectMutationHistory = false;
|
|
800
806
|
}
|
|
801
807
|
export function getProjectRuntimeShutdownSnapshot() {
|
|
802
808
|
let activeOperations = 0;
|
|
@@ -814,6 +820,7 @@ export function getProjectRuntimeShutdownSnapshot() {
|
|
|
814
820
|
active_operations: activeOperations,
|
|
815
821
|
queued_operations: queuedOperations,
|
|
816
822
|
completion_critical_operations: completionCriticalOperations,
|
|
823
|
+
mutation_history: projectMutationHistory,
|
|
817
824
|
});
|
|
818
825
|
}
|
|
819
826
|
export function beginProjectShutdown() {
|
|
@@ -15,6 +15,7 @@ export const PUBLIC_ERROR_CODES = Object.freeze([
|
|
|
15
15
|
"STALE_WORKSPACE",
|
|
16
16
|
"INCOMPLETE_EVIDENCE",
|
|
17
17
|
"MUTATION_BLOCKED",
|
|
18
|
+
"AMBIGUOUS_APPLY",
|
|
18
19
|
"CONFLICT",
|
|
19
20
|
"INTERNAL_ERROR",
|
|
20
21
|
]);
|
|
@@ -38,6 +39,7 @@ const FIXED_PUBLIC_MESSAGES = Object.freeze({
|
|
|
38
39
|
STALE_WORKSPACE: "The workspace changed. Retry the operation.",
|
|
39
40
|
INCOMPLETE_EVIDENCE: "The required compiler evidence is incomplete.",
|
|
40
41
|
MUTATION_BLOCKED: "The mutation is blocked.",
|
|
42
|
+
AMBIGUOUS_APPLY: "The apply outcome is ambiguous. Inspect the operation receipt before retrying.",
|
|
41
43
|
CONFLICT: "The operation conflicts with current state.",
|
|
42
44
|
INTERNAL_ERROR: INTERNAL_ERROR_MESSAGE,
|
|
43
45
|
});
|
|
@@ -178,7 +180,9 @@ function codedError(error) {
|
|
|
178
180
|
return null;
|
|
179
181
|
return {
|
|
180
182
|
code: code,
|
|
181
|
-
message:
|
|
183
|
+
message: code === "AMBIGUOUS_APPLY"
|
|
184
|
+
? FIXED_PUBLIC_MESSAGES.AMBIGUOUS_APPLY
|
|
185
|
+
: sanitizePublicText(message),
|
|
182
186
|
};
|
|
183
187
|
}
|
|
184
188
|
if (!(error instanceof Error))
|
|
@@ -236,9 +240,8 @@ function serializePublicError(code, message, correlationId) {
|
|
|
236
240
|
};
|
|
237
241
|
return { envelope, text: JSON.stringify(envelope) };
|
|
238
242
|
}
|
|
239
|
-
export function renderPublicError(error) {
|
|
243
|
+
export function renderPublicError(error, correlationId = randomUUID()) {
|
|
240
244
|
const classified = classifyPublicError(error);
|
|
241
|
-
const correlationId = randomUUID();
|
|
242
245
|
const initial = serializePublicError(classified.code, classified.message, correlationId);
|
|
243
246
|
if (Buffer.byteLength(initial.text, "utf8") <= MAX_PUBLIC_ERROR_RESPONSE_BYTES) {
|
|
244
247
|
return initial;
|
|
@@ -48,3 +48,8 @@ export function renderToolFailureEvent(input) {
|
|
|
48
48
|
export function emitToolFailureEvent(input) {
|
|
49
49
|
process.stderr.write(`${renderToolFailureEvent(input)}\n`);
|
|
50
50
|
}
|
|
51
|
+
// prettier-ignore
|
|
52
|
+
export function emitCompilerWorkerEvent(input) {
|
|
53
|
+
const event = { event: "compiler_worker", version: 1, kind: input.kind, generation: Number.isSafeInteger(input.generation) ? input.generation : 0, correlation_id: input.correlationId && UUID_PATTERN.test(input.correlationId) ? input.correlationId : undefined, count: input.count === undefined ? undefined : Math.max(0, Math.min(128, Math.trunc(input.count))) };
|
|
54
|
+
process.stderr.write(`${JSON.stringify(event)}\n`);
|
|
55
|
+
}
|