@adhisang/minecraft-modding-mcp 6.1.1 → 6.2.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 +30 -0
- package/README.md +4 -2
- package/dist/cache-registry.d.ts +1 -1
- package/dist/cache-registry.js +3 -0
- package/dist/entry-tools/analyze-mod-service.d.ts +12 -6
- package/dist/entry-tools/analyze-mod-service.js +37 -3
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -0
- package/dist/entry-tools/analyze-symbol-service.js +37 -2
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +4 -0
- package/dist/entry-tools/inspect-minecraft/internal.js +28 -0
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/error-mapping.d.ts +13 -0
- package/dist/error-mapping.js +35 -2
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/index.js +37 -17
- package/dist/mapping/internal-types.d.ts +7 -0
- package/dist/mapping/types.d.ts +18 -0
- package/dist/mapping-service.js +16 -3
- package/dist/minecraft-explorer-service.d.ts +4 -0
- package/dist/minecraft-explorer-service.js +156 -17
- package/dist/mod-analyzer.d.ts +7 -0
- package/dist/mod-analyzer.js +28 -7
- package/dist/source/artifact-resolver.d.ts +2 -0
- package/dist/source/artifact-resolver.js +24 -1
- package/dist/source/class-source/members-builder.d.ts +4 -0
- package/dist/source/class-source/members-builder.js +3 -1
- package/dist/source/class-source.d.ts +2 -1
- package/dist/source/class-source.js +154 -17
- package/dist/source/did-you-mean.d.ts +14 -0
- package/dist/source/did-you-mean.js +79 -0
- package/dist/source/file-access.js +159 -3
- package/dist/source/indexer.js +72 -2
- package/dist/source/lifecycle/runtime-check.js +9 -5
- package/dist/source/nested-jars.d.ts +59 -0
- package/dist/source/nested-jars.js +198 -0
- package/dist/source/workspace-target.js +5 -2
- package/dist/source-jar-reader.d.ts +16 -0
- package/dist/source-jar-reader.js +82 -0
- package/dist/source-service.d.ts +36 -0
- package/dist/source-service.js +49 -6
- package/dist/stage-emitter.js +24 -8
- package/dist/stdio-supervisor.d.ts +92 -9
- package/dist/stdio-supervisor.js +915 -103
- package/dist/tool-contract-manifest.js +1 -1
- package/dist/tool-guidance.js +3 -1
- package/dist/tool-schemas.d.ts +1337 -149
- package/dist/tool-schemas.js +38 -7
- package/dist/types.d.ts +23 -0
- package/dist/workspace-mapping-service.d.ts +1 -0
- package/dist/workspace-mapping-service.js +120 -8
- package/docs/tool-reference.md +19 -3
- package/package.json +1 -1
package/dist/stdio-supervisor.js
CHANGED
|
@@ -16,6 +16,90 @@ const PRESERVED_PATH_KEYS = new Set(["projectPath", "sourcePath", "mixinConfigPa
|
|
|
16
16
|
const MAX_STRING_BYTES = 256;
|
|
17
17
|
const MAX_ARRAY_LENGTH = 8;
|
|
18
18
|
const MAX_OBJECT_KEYS = 16;
|
|
19
|
+
const DEFAULT_VALIDATE_PROJECT_TIMEOUT_MS = 120_000;
|
|
20
|
+
const MIN_VALIDATE_PROJECT_TIMEOUT_MS = 10_000;
|
|
21
|
+
const MAX_VALIDATE_PROJECT_TIMEOUT_MS = 600_000;
|
|
22
|
+
const MAX_WORKER_STARTUP_WATCHDOG_MS = 30_000;
|
|
23
|
+
const MAX_SUPERVISOR_QUEUE = 2;
|
|
24
|
+
const DEFAULT_TREE_CLEANUP_TIMEOUT_MS = 5_000;
|
|
25
|
+
export function loadValidateProjectTimeoutMs(value = process.env.MCP_VALIDATE_PROJECT_TIMEOUT_MS) {
|
|
26
|
+
if (!/^[0-9]+$/.test(value ?? "")) {
|
|
27
|
+
return DEFAULT_VALIDATE_PROJECT_TIMEOUT_MS;
|
|
28
|
+
}
|
|
29
|
+
const parsed = Number(value);
|
|
30
|
+
if (!Number.isSafeInteger(parsed) ||
|
|
31
|
+
parsed < MIN_VALIDATE_PROJECT_TIMEOUT_MS ||
|
|
32
|
+
parsed > MAX_VALIDATE_PROJECT_TIMEOUT_MS) {
|
|
33
|
+
return DEFAULT_VALIDATE_PROJECT_TIMEOUT_MS;
|
|
34
|
+
}
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
export function computeWorkerStartupWatchdogMs(validateProjectTimeoutMs) {
|
|
38
|
+
return Math.min(validateProjectTimeoutMs, MAX_WORKER_STARTUP_WATCHDOG_MS);
|
|
39
|
+
}
|
|
40
|
+
export function computeRestartBackoffMs(retryIndex) {
|
|
41
|
+
if (!Number.isFinite(retryIndex) || retryIndex <= 0) {
|
|
42
|
+
return 100;
|
|
43
|
+
}
|
|
44
|
+
return Math.min(100 * (2 ** Math.floor(retryIndex)), MAX_WORKER_STARTUP_WATCHDOG_MS);
|
|
45
|
+
}
|
|
46
|
+
export function terminatePosixProcessGroup(pid, kill = process.kill) {
|
|
47
|
+
try {
|
|
48
|
+
kill(-pid, "SIGKILL");
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error?.code === "ESRCH") {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function buildWindowsTreeKillArgs(pid) {
|
|
59
|
+
return ["/PID", String(pid), "/T", "/F"];
|
|
60
|
+
}
|
|
61
|
+
export function shouldRetainUnavailableNotification(method) {
|
|
62
|
+
return method === "notifications/initialized";
|
|
63
|
+
}
|
|
64
|
+
export class RestartBackoffState {
|
|
65
|
+
retryIndex = 0;
|
|
66
|
+
epoch = 0;
|
|
67
|
+
reserve(now) {
|
|
68
|
+
const delayMs = computeRestartBackoffMs(this.retryIndex);
|
|
69
|
+
this.retryIndex += 1;
|
|
70
|
+
return {
|
|
71
|
+
epoch: ++this.epoch,
|
|
72
|
+
notBefore: now + delayMs,
|
|
73
|
+
delayMs
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
reset() {
|
|
77
|
+
this.retryIndex = 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function retryPosixTreeToken(pid, kill = process.kill) {
|
|
81
|
+
return terminatePosixProcessGroup(pid, kill);
|
|
82
|
+
}
|
|
83
|
+
export async function settleTreeCleanupWithin(operation, timeoutMs, onTimeout = () => { }) {
|
|
84
|
+
return await new Promise((resolve) => {
|
|
85
|
+
let settled = false;
|
|
86
|
+
const finish = (result) => {
|
|
87
|
+
if (settled)
|
|
88
|
+
return;
|
|
89
|
+
settled = true;
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
resolve(result);
|
|
92
|
+
};
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
try {
|
|
95
|
+
onTimeout();
|
|
96
|
+
}
|
|
97
|
+
catch { /* best effort */ }
|
|
98
|
+
finish(false);
|
|
99
|
+
}, timeoutMs);
|
|
100
|
+
operation.then(finish, () => finish(false));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
19
103
|
function isRequest(message) {
|
|
20
104
|
return "method" in message && "id" in message;
|
|
21
105
|
}
|
|
@@ -110,6 +194,109 @@ export function redactToolArgs(args) {
|
|
|
110
194
|
}
|
|
111
195
|
return { args: {}, modified: counter.modified };
|
|
112
196
|
}
|
|
197
|
+
function redactDiagnosticValue(value) {
|
|
198
|
+
const counter = { modified: false };
|
|
199
|
+
return redactValue(value, counter) ?? null;
|
|
200
|
+
}
|
|
201
|
+
function buildSyntheticToolResult(id, structuredContent) {
|
|
202
|
+
return {
|
|
203
|
+
jsonrpc: "2.0",
|
|
204
|
+
id,
|
|
205
|
+
result: {
|
|
206
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
|
|
207
|
+
isError: true,
|
|
208
|
+
structuredContent
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export function buildSupervisorQueueLimitReply(id, method) {
|
|
213
|
+
if (method !== "tools/call") {
|
|
214
|
+
return {
|
|
215
|
+
jsonrpc: "2.0",
|
|
216
|
+
id,
|
|
217
|
+
error: {
|
|
218
|
+
code: -32000,
|
|
219
|
+
message: "MCP supervisor request queue is full."
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return buildSyntheticToolResult(id, {
|
|
224
|
+
error: {
|
|
225
|
+
type: "about:blank/mcp/limit-exceeded",
|
|
226
|
+
title: "Supervisor queue limit exceeded",
|
|
227
|
+
detail: "The MCP supervisor request queue is full.",
|
|
228
|
+
status: 413,
|
|
229
|
+
code: "ERR_LIMIT_EXCEEDED",
|
|
230
|
+
instance: `urn:mcp:request:${String(id)}`,
|
|
231
|
+
retryClass: "transient",
|
|
232
|
+
issueOrigin: "tool_issue",
|
|
233
|
+
hints: ["Retry after the supervisor queue drains."]
|
|
234
|
+
},
|
|
235
|
+
meta: {
|
|
236
|
+
synthetic: true,
|
|
237
|
+
syntheticSource: "supervisor",
|
|
238
|
+
queue: {
|
|
239
|
+
reason: "supervisor-request-queue",
|
|
240
|
+
maxQueued: MAX_SUPERVISOR_QUEUE,
|
|
241
|
+
queuedCount: MAX_SUPERVISOR_QUEUE
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
export function buildValidateProjectTimeoutReply(input) {
|
|
247
|
+
const { request, phase, deadlineMs, now, workerRestartInitiated } = input;
|
|
248
|
+
const retryRecommendation = decideRetryRecommendation({
|
|
249
|
+
toolName: "validate-project",
|
|
250
|
+
lastStage: request.lastStage,
|
|
251
|
+
lastStageMeta: request.lastStageMeta,
|
|
252
|
+
exit: { code: null, signal: null }
|
|
253
|
+
}, []);
|
|
254
|
+
const error = {
|
|
255
|
+
type: "about:blank/mcp/tool-timeout",
|
|
256
|
+
title: "MCP tool timed out",
|
|
257
|
+
detail: `validate-project exceeded its ${deadlineMs} ms deadline.`,
|
|
258
|
+
status: 408,
|
|
259
|
+
code: "ERR_TOOL_TIMEOUT",
|
|
260
|
+
instance: `urn:mcp:request:${String(request.id)}`,
|
|
261
|
+
retryClass: "transient",
|
|
262
|
+
issueOrigin: "tool_issue",
|
|
263
|
+
hints: ["Retry the request or narrow the validation scope."]
|
|
264
|
+
};
|
|
265
|
+
if (request.toolName === "validate-project" &&
|
|
266
|
+
request.toolArgsRedacted !== undefined &&
|
|
267
|
+
!request.toolArgsRedactedModified &&
|
|
268
|
+
getToolSchema("validate-project") !== undefined) {
|
|
269
|
+
const gated = buildSuggestedCall({
|
|
270
|
+
tool: "validate-project",
|
|
271
|
+
params: request.toolArgsRedacted
|
|
272
|
+
});
|
|
273
|
+
if (gated.suggestedCall) {
|
|
274
|
+
error.suggestedCall = gated.suggestedCall;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return buildSyntheticToolResult(request.id, {
|
|
278
|
+
error,
|
|
279
|
+
meta: {
|
|
280
|
+
synthetic: true,
|
|
281
|
+
syntheticSource: "supervisor",
|
|
282
|
+
timeout: {
|
|
283
|
+
tool: "validate-project",
|
|
284
|
+
phase,
|
|
285
|
+
durationMs: Math.max(0, now - request.startedAt),
|
|
286
|
+
deadlineMs,
|
|
287
|
+
lastStage: request.lastStage ?? null,
|
|
288
|
+
lastStageElapsedMs: request.lastStageStartedAt === undefined
|
|
289
|
+
? null
|
|
290
|
+
: Math.max(0, now - request.lastStageStartedAt),
|
|
291
|
+
lastStageMeta: redactDiagnosticValue(request.lastStageMeta),
|
|
292
|
+
redactedToolArgs: request.toolArgsRedacted ?? {},
|
|
293
|
+
redactedToolArgsModified: request.toolArgsRedactedModified ?? false,
|
|
294
|
+
retryRecommendation,
|
|
295
|
+
workerRestartInitiated
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
113
300
|
export function decideRetryRecommendation(ctx, recentRestartTimestamps) {
|
|
114
301
|
if (ctx.lastStage === "target-lookup") {
|
|
115
302
|
const meta = ctx.lastStageMeta;
|
|
@@ -297,15 +484,40 @@ function debugSupervisor(event, details) {
|
|
|
297
484
|
}
|
|
298
485
|
export class StdioSupervisor {
|
|
299
486
|
entryFile;
|
|
487
|
+
validateProjectTimeoutMs;
|
|
488
|
+
workerStartupWatchdogMs;
|
|
489
|
+
clientWriter;
|
|
490
|
+
treeTokenRetrier;
|
|
491
|
+
treeCleanupTimeoutMs;
|
|
492
|
+
eventWriter;
|
|
493
|
+
monotonicNow;
|
|
494
|
+
timerScheduler;
|
|
495
|
+
timerClearer;
|
|
496
|
+
workerSpawner;
|
|
497
|
+
treeTerminator;
|
|
300
498
|
clientReader = new JsonRpcFrameReader();
|
|
301
|
-
|
|
302
|
-
|
|
499
|
+
workerReaders = new Map();
|
|
500
|
+
queuedRequests = [];
|
|
501
|
+
queuedNotifications = [];
|
|
303
502
|
pendingRequests = new Map();
|
|
304
503
|
recentRestarts = new Map();
|
|
504
|
+
liveChildren = new Set();
|
|
505
|
+
staleChildren = new Set();
|
|
506
|
+
cleanupStates = new Map();
|
|
507
|
+
unresolvedTreeTokens = new Set();
|
|
508
|
+
restartBackoff = new RestartBackoffState();
|
|
509
|
+
terminalChildren = new WeakSet();
|
|
305
510
|
child;
|
|
306
511
|
childReady = false;
|
|
307
512
|
shuttingDown = false;
|
|
308
513
|
restartTimer;
|
|
514
|
+
startupWatchdog;
|
|
515
|
+
validateBarrierKey;
|
|
516
|
+
runningValidateKey;
|
|
517
|
+
attemptToken = 0;
|
|
518
|
+
currentRetryEpoch;
|
|
519
|
+
currentRetryReservation;
|
|
520
|
+
retryPaused = false;
|
|
309
521
|
workerStderrBuffer = "";
|
|
310
522
|
clientMode = DEFAULT_CLIENT_MODE;
|
|
311
523
|
initializeRequest;
|
|
@@ -315,6 +527,24 @@ export class StdioSupervisor {
|
|
|
315
527
|
initializeSentToWorker = false;
|
|
316
528
|
constructor(options) {
|
|
317
529
|
this.entryFile = options.entryFile;
|
|
530
|
+
this.validateProjectTimeoutMs = options.validateProjectTimeoutMs ?? loadValidateProjectTimeoutMs();
|
|
531
|
+
this.workerStartupWatchdogMs = computeWorkerStartupWatchdogMs(this.validateProjectTimeoutMs);
|
|
532
|
+
this.clientWriter = options.clientWriter;
|
|
533
|
+
this.treeTokenRetrier = options.treeTokenRetrier;
|
|
534
|
+
this.treeCleanupTimeoutMs = options.treeCleanupTimeoutMs ?? DEFAULT_TREE_CLEANUP_TIMEOUT_MS;
|
|
535
|
+
this.eventWriter = options.eventWriter ?? log;
|
|
536
|
+
this.monotonicNow = options.monotonicNow ?? (() => performance.now());
|
|
537
|
+
this.timerScheduler = options.timerScheduler ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
538
|
+
this.timerClearer = options.timerClearer ?? ((timer) => clearTimeout(timer));
|
|
539
|
+
this.workerSpawner = options.workerSpawner ?? (() => spawn(process.execPath, [...process.execArgv, this.entryFile], {
|
|
540
|
+
env: {
|
|
541
|
+
...process.env,
|
|
542
|
+
[WORKER_MODE_ENV]: "1"
|
|
543
|
+
},
|
|
544
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
545
|
+
...(process.platform === "win32" ? {} : { detached: true })
|
|
546
|
+
}));
|
|
547
|
+
this.treeTerminator = options.treeTerminator;
|
|
318
548
|
}
|
|
319
549
|
async start() {
|
|
320
550
|
process.stdin.on("data", this.handleClientData);
|
|
@@ -360,127 +590,355 @@ export class StdioSupervisor {
|
|
|
360
590
|
else if (isNotification(message) && message.method === "notifications/initialized") {
|
|
361
591
|
this.initializedNotification = message;
|
|
362
592
|
}
|
|
363
|
-
if (
|
|
364
|
-
|
|
593
|
+
if (isNotification(message)) {
|
|
594
|
+
if (message.method === "notifications/cancelled") {
|
|
595
|
+
this.handleCancellation(message);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (!this.childReady) {
|
|
599
|
+
if (!shouldRetainUnavailableNotification(message.method)) {
|
|
600
|
+
this.eventWriter("warn", "supervisor.notification_dropped", {
|
|
601
|
+
method: message.method,
|
|
602
|
+
reason: this.liveCapOccupancy() >= 2 ? "live-cap-blocked" : "worker-unavailable"
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (this.child && !this.child.stdin.destroyed) {
|
|
608
|
+
this.writeToWorker(this.child, message);
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (!isRequest(message)) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (message.method === "initialize") {
|
|
616
|
+
if (!this.child && this.liveCapOccupancy() >= 2) {
|
|
617
|
+
this.clearInitialInitializationState();
|
|
618
|
+
this.writeToClient(buildLegacyJsonRpcError(message.id));
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (this.childReady) {
|
|
622
|
+
this.forwardRequest(message, this.createPendingRequest(message));
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
const existing = this.queuedNotifications.findIndex((entry) => isRequest(entry) && entry.method === "initialize");
|
|
626
|
+
if (existing >= 0)
|
|
627
|
+
this.queuedNotifications.splice(existing, 1);
|
|
628
|
+
this.queuedNotifications.push(message);
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const pending = this.createPendingRequest(message);
|
|
633
|
+
if (!this.child && this.liveCapOccupancy() >= 2) {
|
|
634
|
+
const { reply } = buildWorkerRestartReply(pending, { code: null, signal: null }, this.monotonicNow(), [], { structuredRestartDisabled: STRUCTURED_RESTART_DISABLED });
|
|
635
|
+
this.writeToClient(reply);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const isValidate = pending.toolName === "validate-project";
|
|
639
|
+
const dispatchImmediately = this.canDispatchImmediately(pending);
|
|
640
|
+
if (!dispatchImmediately && this.queuedRequests.length >= MAX_SUPERVISOR_QUEUE) {
|
|
641
|
+
this.writeToClient(buildSupervisorQueueLimitReply(pending.id, pending.method ?? message.method));
|
|
642
|
+
this.drainQueue();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (isValidate) {
|
|
646
|
+
pending.timeoutPhase = "queue";
|
|
647
|
+
const elapsedAtAdmission = Math.max(0, this.monotonicNow() - pending.startedAt);
|
|
648
|
+
pending.deadlineTimer = this.timerScheduler(() => this.handleValidateProjectDeadline(requestKey(pending.id)), Math.max(0, this.validateProjectTimeoutMs - elapsedAtAdmission));
|
|
649
|
+
pending.deadlineTimer.unref();
|
|
650
|
+
if (!this.validateBarrierKey) {
|
|
651
|
+
this.validateBarrierKey = requestKey(pending.id);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (dispatchImmediately) {
|
|
655
|
+
this.forwardRequest(message, pending);
|
|
365
656
|
return;
|
|
366
657
|
}
|
|
367
|
-
this.
|
|
658
|
+
this.queuedRequests.push({ message, pending });
|
|
659
|
+
}
|
|
660
|
+
createPendingRequest(message) {
|
|
661
|
+
const pending = {
|
|
662
|
+
id: message.id,
|
|
663
|
+
method: message.method,
|
|
664
|
+
startedAt: this.monotonicNow()
|
|
665
|
+
};
|
|
666
|
+
if (message.method === "tools/call") {
|
|
667
|
+
const params = (message.params ?? {});
|
|
668
|
+
if (typeof params.name === "string")
|
|
669
|
+
pending.toolName = params.name;
|
|
670
|
+
const redacted = redactToolArgs(params.arguments);
|
|
671
|
+
pending.toolArgsRedacted = redacted.args;
|
|
672
|
+
pending.toolArgsRedactedModified = redacted.modified;
|
|
673
|
+
}
|
|
674
|
+
return pending;
|
|
368
675
|
}
|
|
369
|
-
|
|
676
|
+
canDispatchImmediately(pending) {
|
|
677
|
+
if (!this.childReady || !this.child || this.child.stdin.destroyed)
|
|
678
|
+
return false;
|
|
679
|
+
if (pending.toolName === "validate-project") {
|
|
680
|
+
return (this.pendingRequests.size === 0 &&
|
|
681
|
+
this.runningValidateKey === undefined &&
|
|
682
|
+
this.staleChildren.size === 0 &&
|
|
683
|
+
this.unresolvedTreeTokens.size === 0 &&
|
|
684
|
+
this.queuedRequests.length === 0);
|
|
685
|
+
}
|
|
686
|
+
return this.validateBarrierKey === undefined;
|
|
687
|
+
}
|
|
688
|
+
forwardRequest(message, pending) {
|
|
370
689
|
const child = this.child;
|
|
371
690
|
if (!child || child.stdin.destroyed) {
|
|
372
|
-
this.
|
|
373
|
-
|
|
374
|
-
this.scheduleRestart();
|
|
691
|
+
if (this.queuedRequests.length < MAX_SUPERVISOR_QUEUE) {
|
|
692
|
+
this.queuedRequests.push({ message, pending });
|
|
375
693
|
}
|
|
694
|
+
else {
|
|
695
|
+
if (pending.deadlineTimer)
|
|
696
|
+
this.timerClearer(pending.deadlineTimer);
|
|
697
|
+
this.writeToClient(buildSupervisorQueueLimitReply(pending.id, pending.method ?? message.method));
|
|
698
|
+
}
|
|
699
|
+
this.scheduleRestart();
|
|
376
700
|
return;
|
|
377
701
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
method: message.method,
|
|
384
|
-
startedAt: performance.now()
|
|
385
|
-
};
|
|
386
|
-
if (message.method === "tools/call") {
|
|
387
|
-
const params = (message.params ?? {});
|
|
388
|
-
if (typeof params.name === "string") {
|
|
389
|
-
pending.toolName = params.name;
|
|
390
|
-
}
|
|
391
|
-
const redacted = redactToolArgs(params.arguments);
|
|
392
|
-
pending.toolArgsRedacted = redacted.args;
|
|
393
|
-
pending.toolArgsRedactedModified = redacted.modified;
|
|
394
|
-
}
|
|
395
|
-
this.pendingRequests.set(requestKey(id), pending);
|
|
396
|
-
}
|
|
397
|
-
if (message.method === "initialize") {
|
|
398
|
-
this.initializeSentToWorker = true;
|
|
399
|
-
}
|
|
702
|
+
this.pendingRequests.set(requestKey(pending.id), pending);
|
|
703
|
+
if (pending.toolName === "validate-project") {
|
|
704
|
+
pending.timeoutPhase = "running";
|
|
705
|
+
this.runningValidateKey = requestKey(pending.id);
|
|
706
|
+
this.validateBarrierKey = requestKey(pending.id);
|
|
400
707
|
}
|
|
708
|
+
if (message.method === "initialize")
|
|
709
|
+
this.initializeSentToWorker = true;
|
|
401
710
|
debugSupervisor("forward_to_worker", {
|
|
402
711
|
method: "method" in message ? message.method : undefined,
|
|
403
712
|
id: "id" in message ? message.id : undefined
|
|
404
713
|
});
|
|
714
|
+
this.writeToWorker(child, message);
|
|
715
|
+
}
|
|
716
|
+
writeToWorker(child, message) {
|
|
405
717
|
child.stdin.write(encodeJsonRpcMessage(message, "content-length"));
|
|
406
718
|
}
|
|
407
|
-
|
|
408
|
-
|
|
719
|
+
handleCancellation(message) {
|
|
720
|
+
const params = (message.params ?? {});
|
|
721
|
+
const targetId = params.requestId;
|
|
722
|
+
if (typeof targetId !== "string" && typeof targetId !== "number") {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const key = requestKey(targetId);
|
|
726
|
+
const queuedIndex = this.queuedRequests.findIndex((entry) => requestKey(entry.pending.id) === key);
|
|
727
|
+
if (queuedIndex >= 0) {
|
|
728
|
+
const [{ pending }] = this.queuedRequests.splice(queuedIndex, 1);
|
|
729
|
+
if (pending.deadlineTimer)
|
|
730
|
+
this.timerClearer(pending.deadlineTimer);
|
|
731
|
+
if (this.validateBarrierKey === key)
|
|
732
|
+
this.validateBarrierKey = undefined;
|
|
733
|
+
this.drainQueue();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const pending = this.pendingRequests.get(key);
|
|
737
|
+
if (pending?.toolName === "validate-project") {
|
|
738
|
+
pending.clientCancelled = true;
|
|
739
|
+
}
|
|
740
|
+
const child = this.child;
|
|
741
|
+
if (child && !child.stdin.destroyed) {
|
|
742
|
+
this.writeToWorker(child, message);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
handleValidateProjectDeadline(key) {
|
|
746
|
+
const queuedIndex = this.queuedRequests.findIndex((entry) => requestKey(entry.pending.id) === key);
|
|
747
|
+
if (queuedIndex >= 0) {
|
|
748
|
+
const [{ pending }] = this.queuedRequests.splice(queuedIndex, 1);
|
|
749
|
+
if (pending.deadlineTimer)
|
|
750
|
+
this.timerClearer(pending.deadlineTimer);
|
|
751
|
+
pending.deadlineTimer = undefined;
|
|
752
|
+
if (this.validateBarrierKey === key)
|
|
753
|
+
this.validateBarrierKey = undefined;
|
|
754
|
+
if (!pending.clientCancelled) {
|
|
755
|
+
this.writeToClient(buildValidateProjectTimeoutReply({
|
|
756
|
+
request: pending,
|
|
757
|
+
phase: "queue",
|
|
758
|
+
deadlineMs: this.validateProjectTimeoutMs,
|
|
759
|
+
now: this.monotonicNow(),
|
|
760
|
+
workerRestartInitiated: false
|
|
761
|
+
}));
|
|
762
|
+
}
|
|
763
|
+
this.drainQueue();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const pending = this.pendingRequests.get(key);
|
|
767
|
+
if (!pending || pending.toolName !== "validate-project")
|
|
409
768
|
return;
|
|
769
|
+
this.pendingRequests.delete(key);
|
|
770
|
+
pending.deadlineTimer = undefined;
|
|
771
|
+
this.runningValidateKey = undefined;
|
|
772
|
+
this.validateBarrierKey = undefined;
|
|
773
|
+
if (!pending.clientCancelled) {
|
|
774
|
+
this.writeToClient(buildValidateProjectTimeoutReply({
|
|
775
|
+
request: pending,
|
|
776
|
+
phase: "running",
|
|
777
|
+
deadlineMs: this.validateProjectTimeoutMs,
|
|
778
|
+
now: this.monotonicNow(),
|
|
779
|
+
workerRestartInitiated: true
|
|
780
|
+
}));
|
|
410
781
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
782
|
+
this.recoverTimedOutWorker();
|
|
783
|
+
}
|
|
784
|
+
drainQueue() {
|
|
785
|
+
if (!this.childReady || !this.child || this.child.stdin.destroyed)
|
|
786
|
+
return;
|
|
787
|
+
while (this.queuedRequests.length > 0) {
|
|
788
|
+
if (this.runningValidateKey)
|
|
789
|
+
return;
|
|
790
|
+
const next = this.queuedRequests[0];
|
|
791
|
+
const nextKey = requestKey(next.pending.id);
|
|
792
|
+
if (next.pending.toolName === "validate-project") {
|
|
793
|
+
this.validateBarrierKey = nextKey;
|
|
794
|
+
if (this.pendingRequests.size > 0 ||
|
|
795
|
+
this.staleChildren.size > 0 ||
|
|
796
|
+
this.unresolvedTreeTokens.size > 0) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
this.queuedRequests.shift();
|
|
800
|
+
this.forwardRequest(next.message, next.pending);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (this.validateBarrierKey && this.validateBarrierKey !== nextKey) {
|
|
804
|
+
const barrierIndex = this.queuedRequests.findIndex((entry) => requestKey(entry.pending.id) === this.validateBarrierKey);
|
|
805
|
+
if (barrierIndex === 0)
|
|
806
|
+
return;
|
|
416
807
|
}
|
|
808
|
+
this.queuedRequests.shift();
|
|
809
|
+
this.forwardRequest(next.message, next.pending);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
spawnWorker() {
|
|
813
|
+
if (this.shuttingDown || this.child || this.liveCapOccupancy() >= 2) {
|
|
814
|
+
this.retryPaused = this.liveCapOccupancy() >= 2;
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
this.currentRetryEpoch = undefined;
|
|
818
|
+
this.currentRetryReservation = undefined;
|
|
819
|
+
this.retryPaused = false;
|
|
820
|
+
const token = ++this.attemptToken;
|
|
821
|
+
let child;
|
|
822
|
+
try {
|
|
823
|
+
child = this.workerSpawner();
|
|
824
|
+
}
|
|
825
|
+
catch (error) {
|
|
826
|
+
log("error", "supervisor.worker_spawn_throw", {
|
|
827
|
+
message: error instanceof Error ? error.message : String(error)
|
|
828
|
+
});
|
|
829
|
+
this.handleStartupFailure(token, { code: null, signal: null });
|
|
830
|
+
return;
|
|
417
831
|
}
|
|
418
|
-
const child = spawn(process.execPath, [...process.execArgv, this.entryFile], {
|
|
419
|
-
env: {
|
|
420
|
-
...process.env,
|
|
421
|
-
[WORKER_MODE_ENV]: "1"
|
|
422
|
-
},
|
|
423
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
424
|
-
});
|
|
425
832
|
this.child = child;
|
|
833
|
+
this.liveChildren.add(child);
|
|
834
|
+
this.workerReaders.set(child, new JsonRpcFrameReader());
|
|
426
835
|
this.childReady = false;
|
|
427
836
|
this.initializeSentToWorker = false;
|
|
428
|
-
this.workerReader.clear();
|
|
429
837
|
this.workerStderrBuffer = "";
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
838
|
+
this.clearStartupWatchdog();
|
|
839
|
+
this.startupWatchdog = this.timerScheduler(() => {
|
|
840
|
+
if (token !== this.attemptToken || child !== this.child || this.childReady)
|
|
841
|
+
return;
|
|
842
|
+
log("warn", "supervisor.worker_startup_timeout", {
|
|
843
|
+
pid: child.pid,
|
|
844
|
+
timeoutMs: this.workerStartupWatchdogMs
|
|
845
|
+
});
|
|
846
|
+
this.invalidateCurrentChild(child);
|
|
847
|
+
this.beginTreeTermination(child);
|
|
848
|
+
this.handleStartupFailure(token, { code: null, signal: "SIGKILL" });
|
|
849
|
+
}, this.workerStartupWatchdogMs);
|
|
850
|
+
child.stdout.on("data", (chunk) => this.handleWorkerData(child, chunk));
|
|
851
|
+
child.stderr.on("data", (chunk) => this.handleWorkerStderr(child, chunk));
|
|
852
|
+
child.stdin.on("error", (error) => this.handleWorkerStdinError(child, error));
|
|
433
853
|
child.once("error", (error) => this.handleWorkerProcessError(child, error));
|
|
434
854
|
child.once("exit", (code, signal) => this.handleWorkerExit(child, code, signal));
|
|
855
|
+
child.once("close", (code, signal) => this.handleWorkerExit(child, code, signal));
|
|
435
856
|
log("info", "supervisor.worker_spawn", { pid: child.pid });
|
|
436
857
|
}
|
|
437
|
-
handleWorkerData
|
|
438
|
-
this.
|
|
858
|
+
handleWorkerData(child, chunk) {
|
|
859
|
+
if (child !== this.child)
|
|
860
|
+
return;
|
|
861
|
+
const reader = this.workerReaders.get(child);
|
|
862
|
+
if (!reader)
|
|
863
|
+
return;
|
|
864
|
+
reader.processChunk(chunk, {
|
|
439
865
|
onFrame: ({ message }) => {
|
|
440
|
-
this.handleWorkerMessage(message);
|
|
866
|
+
this.handleWorkerMessage(child, message);
|
|
441
867
|
},
|
|
442
868
|
onError: (error) => {
|
|
443
869
|
log("warn", "supervisor.worker_parse_error", { message: error.message });
|
|
444
870
|
}
|
|
445
871
|
});
|
|
446
|
-
}
|
|
447
|
-
handleWorkerStdinError
|
|
872
|
+
}
|
|
873
|
+
handleWorkerStdinError(child, error) {
|
|
874
|
+
if (child !== this.child)
|
|
875
|
+
return;
|
|
448
876
|
if (error.code === "EPIPE") {
|
|
449
877
|
return;
|
|
450
878
|
}
|
|
451
879
|
log("warn", "supervisor.worker_stdin_error", { message: error.message });
|
|
452
|
-
}
|
|
453
|
-
handleWorkerStderr
|
|
880
|
+
}
|
|
881
|
+
handleWorkerStderr(child, chunk) {
|
|
882
|
+
if (child !== this.child)
|
|
883
|
+
return;
|
|
454
884
|
this.workerStderrBuffer += chunk.toString();
|
|
455
885
|
const lines = this.workerStderrBuffer.split(/\r?\n/);
|
|
456
886
|
this.workerStderrBuffer = lines.pop() ?? "";
|
|
457
887
|
for (const line of lines) {
|
|
458
888
|
if (line === WORKER_READY_MARKER) {
|
|
459
|
-
this.handleWorkerReady();
|
|
889
|
+
this.handleWorkerReady(child);
|
|
460
890
|
continue;
|
|
461
891
|
}
|
|
462
892
|
process.stderr.write(`${line}\n`);
|
|
463
893
|
}
|
|
464
|
-
}
|
|
894
|
+
}
|
|
465
895
|
handleWorkerProcessError(child, error) {
|
|
466
896
|
log("error", "supervisor.worker_process_error", { message: error.message });
|
|
467
897
|
if (child !== this.child) {
|
|
468
898
|
return;
|
|
469
899
|
}
|
|
470
|
-
|
|
471
|
-
|
|
900
|
+
const wasReady = this.childReady;
|
|
901
|
+
this.invalidateCurrentChild(child);
|
|
902
|
+
this.beginTreeTermination(child);
|
|
903
|
+
if (!wasReady) {
|
|
904
|
+
this.handleStartupFailure(this.attemptToken, { code: null, signal: null });
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
this.failPendingRequestsOnWorkerExit({ code: null, signal: null });
|
|
908
|
+
this.scheduleRestart(true);
|
|
472
909
|
}
|
|
473
|
-
this.handleWorkerExit(child, null, null);
|
|
474
910
|
}
|
|
475
911
|
handleWorkerExit(child, code, signal) {
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
|
|
912
|
+
if (this.terminalChildren.has(child))
|
|
913
|
+
return;
|
|
914
|
+
this.terminalChildren.add(child);
|
|
915
|
+
const cleanup = this.cleanupStates.get(child);
|
|
916
|
+
if (cleanup) {
|
|
917
|
+
cleanup.parentExited = true;
|
|
918
|
+
this.liveChildren.delete(child);
|
|
919
|
+
this.workerReaders.delete(child);
|
|
920
|
+
this.staleChildren.delete(child);
|
|
921
|
+
if (cleanup.status === "pending" || cleanup.status === "unresolved") {
|
|
922
|
+
this.unresolvedTreeTokens.add(cleanup.pid);
|
|
479
923
|
}
|
|
924
|
+
else {
|
|
925
|
+
this.cleanupStates.delete(child);
|
|
926
|
+
}
|
|
927
|
+
this.resumePausedRestart();
|
|
928
|
+
this.drainQueue();
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
this.liveChildren.delete(child);
|
|
932
|
+
this.workerReaders.delete(child);
|
|
933
|
+
this.staleChildren.delete(child);
|
|
934
|
+
if (child !== this.child) {
|
|
935
|
+
this.resumePausedRestart();
|
|
936
|
+
this.drainQueue();
|
|
480
937
|
return;
|
|
481
938
|
}
|
|
482
939
|
const childPid = this.child?.pid;
|
|
483
|
-
this.
|
|
940
|
+
const wasReady = this.childReady;
|
|
941
|
+
this.detachCurrentChild();
|
|
484
942
|
if (this.shuttingDown) {
|
|
485
943
|
return;
|
|
486
944
|
}
|
|
@@ -490,10 +948,16 @@ export class StdioSupervisor {
|
|
|
490
948
|
signal,
|
|
491
949
|
pendingRequests: this.pendingRequests.size
|
|
492
950
|
});
|
|
951
|
+
if (!wasReady) {
|
|
952
|
+
this.handleStartupFailure(this.attemptToken, { code, signal });
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
493
955
|
this.failPendingRequestsOnWorkerExit({ code, signal });
|
|
494
|
-
this.scheduleRestart();
|
|
956
|
+
this.scheduleRestart(true);
|
|
495
957
|
}
|
|
496
|
-
handleWorkerMessage(message) {
|
|
958
|
+
handleWorkerMessage(child, message) {
|
|
959
|
+
if (child !== this.child)
|
|
960
|
+
return;
|
|
497
961
|
debugSupervisor("worker_message", {
|
|
498
962
|
hasMethod: "method" in message,
|
|
499
963
|
method: "method" in message ? message.method : undefined,
|
|
@@ -511,17 +975,32 @@ export class StdioSupervisor {
|
|
|
511
975
|
if (id !== undefined) {
|
|
512
976
|
this.pendingRequests.delete(requestKey(id));
|
|
513
977
|
}
|
|
978
|
+
if ("error" in message) {
|
|
979
|
+
if (!this.replayingInitialization && id !== undefined) {
|
|
980
|
+
this.writeToClient(buildLegacyJsonRpcError(id));
|
|
981
|
+
const retainedIndex = this.queuedNotifications.findIndex((entry) => isRequest(entry) && requestKey(entry.id) === requestKey(id));
|
|
982
|
+
if (retainedIndex >= 0)
|
|
983
|
+
this.queuedNotifications.splice(retainedIndex, 1);
|
|
984
|
+
}
|
|
985
|
+
const active = this.child;
|
|
986
|
+
if (active) {
|
|
987
|
+
this.invalidateCurrentChild(active);
|
|
988
|
+
this.beginTreeTermination(active);
|
|
989
|
+
}
|
|
990
|
+
this.handleStartupFailure(this.attemptToken, { code: null, signal: null });
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
514
993
|
if (this.replayingInitialization) {
|
|
515
994
|
this.replayingInitialization = false;
|
|
516
995
|
if (this.initializedNotification) {
|
|
517
|
-
this.
|
|
996
|
+
this.writeToWorker(child, this.initializedNotification);
|
|
518
997
|
}
|
|
519
|
-
this.
|
|
998
|
+
this.adoptActiveChild();
|
|
520
999
|
this.flushQueue();
|
|
521
1000
|
return;
|
|
522
1001
|
}
|
|
523
1002
|
this.clientInitialized = true;
|
|
524
|
-
this.
|
|
1003
|
+
this.adoptActiveChild();
|
|
525
1004
|
this.writeToClient(message);
|
|
526
1005
|
this.flushQueue();
|
|
527
1006
|
return;
|
|
@@ -529,10 +1008,24 @@ export class StdioSupervisor {
|
|
|
529
1008
|
if (isResponse(message)) {
|
|
530
1009
|
const id = getTrackedRequestId(message);
|
|
531
1010
|
if (id !== undefined) {
|
|
532
|
-
|
|
1011
|
+
const key = requestKey(id);
|
|
1012
|
+
const pending = this.pendingRequests.get(key);
|
|
1013
|
+
this.pendingRequests.delete(key);
|
|
1014
|
+
if (pending?.deadlineTimer)
|
|
1015
|
+
this.timerClearer(pending.deadlineTimer);
|
|
1016
|
+
if (pending?.toolName === "validate-project") {
|
|
1017
|
+
this.runningValidateKey = undefined;
|
|
1018
|
+
if (this.validateBarrierKey === key)
|
|
1019
|
+
this.validateBarrierKey = undefined;
|
|
1020
|
+
if (pending.clientCancelled) {
|
|
1021
|
+
this.drainQueue();
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
533
1025
|
}
|
|
534
1026
|
}
|
|
535
1027
|
this.writeToClient(message);
|
|
1028
|
+
this.drainQueue();
|
|
536
1029
|
}
|
|
537
1030
|
applyStageUpdate(params) {
|
|
538
1031
|
if (params === null || params === undefined || typeof params !== "object") {
|
|
@@ -553,42 +1046,48 @@ export class StdioSupervisor {
|
|
|
553
1046
|
// restart envelopes carry the latest progress payload.
|
|
554
1047
|
if (typeof p.stage === "string" && p.stage !== pending.lastStage) {
|
|
555
1048
|
pending.lastStage = p.stage;
|
|
556
|
-
pending.lastStageStartedAt =
|
|
1049
|
+
pending.lastStageStartedAt = this.monotonicNow();
|
|
557
1050
|
}
|
|
558
1051
|
else if (pending.lastStageStartedAt === undefined) {
|
|
559
1052
|
// First emit for this request; bootstrap the stage timer.
|
|
560
|
-
pending.lastStageStartedAt =
|
|
1053
|
+
pending.lastStageStartedAt = this.monotonicNow();
|
|
561
1054
|
}
|
|
562
1055
|
pending.lastStageMeta = p.meta;
|
|
563
1056
|
}
|
|
564
|
-
handleWorkerReady() {
|
|
1057
|
+
handleWorkerReady(child) {
|
|
1058
|
+
if (child !== this.child)
|
|
1059
|
+
return;
|
|
565
1060
|
debugSupervisor("worker_ready", {
|
|
566
1061
|
hasInitializeRequest: this.initializeRequest !== undefined,
|
|
567
1062
|
clientInitialized: this.clientInitialized
|
|
568
1063
|
});
|
|
569
1064
|
if (!this.initializeRequest) {
|
|
570
|
-
this.
|
|
1065
|
+
this.adoptActiveChild();
|
|
571
1066
|
this.flushQueue();
|
|
572
1067
|
return;
|
|
573
1068
|
}
|
|
574
1069
|
this.replayingInitialization = this.clientInitialized;
|
|
575
|
-
this.
|
|
1070
|
+
this.forwardRequest(this.initializeRequest, this.createPendingRequest(this.initializeRequest));
|
|
576
1071
|
}
|
|
577
1072
|
isInitializationResponse(message) {
|
|
578
1073
|
const id = isResponse(message) ? getTrackedRequestId(message) : undefined;
|
|
579
1074
|
const initializeId = this.initializeRequest
|
|
580
1075
|
? getTrackedRequestId(this.initializeRequest)
|
|
581
1076
|
: undefined;
|
|
1077
|
+
const initializePending = initializeId !== undefined
|
|
1078
|
+
? this.pendingRequests.get(requestKey(initializeId))?.method === "initialize"
|
|
1079
|
+
: false;
|
|
582
1080
|
return (id !== undefined &&
|
|
583
1081
|
initializeId !== undefined &&
|
|
1082
|
+
initializePending &&
|
|
584
1083
|
requestKey(id) === requestKey(initializeId));
|
|
585
1084
|
}
|
|
586
1085
|
flushQueue() {
|
|
587
|
-
if (!this.childReady || this.
|
|
1086
|
+
if (!this.childReady || !this.child) {
|
|
588
1087
|
return;
|
|
589
1088
|
}
|
|
590
|
-
const
|
|
591
|
-
for (const message of
|
|
1089
|
+
const controls = this.queuedNotifications.splice(0, this.queuedNotifications.length);
|
|
1090
|
+
for (const message of controls) {
|
|
592
1091
|
if (this.initializeSentToWorker &&
|
|
593
1092
|
isRequest(message) &&
|
|
594
1093
|
message.method === "initialize" &&
|
|
@@ -596,14 +1095,20 @@ export class StdioSupervisor {
|
|
|
596
1095
|
getTrackedRequestId(message) === getTrackedRequestId(this.initializeRequest)) {
|
|
597
1096
|
continue;
|
|
598
1097
|
}
|
|
599
|
-
|
|
1098
|
+
if (isRequest(message)) {
|
|
1099
|
+
this.forwardRequest(message, this.createPendingRequest(message));
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
this.writeToWorker(this.child, message);
|
|
1103
|
+
}
|
|
600
1104
|
}
|
|
1105
|
+
this.drainQueue();
|
|
601
1106
|
}
|
|
602
1107
|
failPendingRequestsOnWorkerExit(exit) {
|
|
603
1108
|
const preservedInitializeKey = this.initializeRequest
|
|
604
1109
|
? requestKey(this.initializeRequest.id)
|
|
605
1110
|
: undefined;
|
|
606
|
-
const now =
|
|
1111
|
+
const now = this.monotonicNow();
|
|
607
1112
|
// Record this exit once per toolName regardless of how many concurrent
|
|
608
1113
|
// pending requests it killed; per-request recording would inflate the
|
|
609
1114
|
// count to N and falsely escalate retryRecommendation to "report-bug".
|
|
@@ -621,6 +1126,14 @@ export class StdioSupervisor {
|
|
|
621
1126
|
if (key === preservedInitializeKey)
|
|
622
1127
|
continue;
|
|
623
1128
|
this.pendingRequests.delete(key);
|
|
1129
|
+
if (pending.deadlineTimer)
|
|
1130
|
+
this.timerClearer(pending.deadlineTimer);
|
|
1131
|
+
if (this.runningValidateKey === key)
|
|
1132
|
+
this.runningValidateKey = undefined;
|
|
1133
|
+
if (this.validateBarrierKey === key)
|
|
1134
|
+
this.validateBarrierKey = undefined;
|
|
1135
|
+
if (pending.clientCancelled)
|
|
1136
|
+
continue;
|
|
624
1137
|
const toolName = pending.toolName ?? "unknown";
|
|
625
1138
|
const pruned = prunedByTool.get(toolName) ?? [];
|
|
626
1139
|
const { reply } = buildWorkerRestartReply(pending, exit, now, pruned, { structuredRestartDisabled: STRUCTURED_RESTART_DISABLED });
|
|
@@ -634,31 +1147,316 @@ export class StdioSupervisor {
|
|
|
634
1147
|
id: "id" in message ? message.id : undefined,
|
|
635
1148
|
clientMode: this.clientMode
|
|
636
1149
|
});
|
|
637
|
-
|
|
638
|
-
|
|
1150
|
+
try {
|
|
1151
|
+
if (this.clientWriter) {
|
|
1152
|
+
this.clientWriter(message);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const frame = encodeJsonRpcMessage(message, this.clientMode);
|
|
1156
|
+
process.stdout.write(frame);
|
|
1157
|
+
}
|
|
1158
|
+
catch (error) {
|
|
1159
|
+
this.eventWriter("warn", "supervisor.client_write_error", {
|
|
1160
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
639
1163
|
}
|
|
640
|
-
|
|
641
|
-
if (this.
|
|
1164
|
+
clearInitialInitializationState() {
|
|
1165
|
+
if (this.clientInitialized)
|
|
642
1166
|
return;
|
|
1167
|
+
this.initializeRequest = undefined;
|
|
1168
|
+
this.initializedNotification = undefined;
|
|
1169
|
+
this.replayingInitialization = false;
|
|
1170
|
+
this.initializeSentToWorker = false;
|
|
1171
|
+
}
|
|
1172
|
+
liveCapOccupancy() {
|
|
1173
|
+
return this.liveChildren.size + this.unresolvedTreeTokens.size;
|
|
1174
|
+
}
|
|
1175
|
+
clearStartupWatchdog() {
|
|
1176
|
+
if (this.startupWatchdog) {
|
|
1177
|
+
this.timerClearer(this.startupWatchdog);
|
|
1178
|
+
this.startupWatchdog = undefined;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
adoptActiveChild() {
|
|
1182
|
+
this.clearStartupWatchdog();
|
|
1183
|
+
this.childReady = true;
|
|
1184
|
+
this.restartBackoff.reset();
|
|
1185
|
+
this.currentRetryEpoch = undefined;
|
|
1186
|
+
this.currentRetryReservation = undefined;
|
|
1187
|
+
this.retryPaused = false;
|
|
1188
|
+
}
|
|
1189
|
+
invalidateCurrentChild(child) {
|
|
1190
|
+
if (child !== this.child)
|
|
1191
|
+
return;
|
|
1192
|
+
this.clearStartupWatchdog();
|
|
1193
|
+
this.child = undefined;
|
|
1194
|
+
this.childReady = false;
|
|
1195
|
+
this.replayingInitialization = false;
|
|
1196
|
+
this.initializeSentToWorker = false;
|
|
1197
|
+
this.staleChildren.add(child);
|
|
1198
|
+
this.attemptToken += 1;
|
|
1199
|
+
child.stdout.removeAllListeners("data");
|
|
1200
|
+
child.stderr.removeAllListeners("data");
|
|
1201
|
+
child.stdin.removeAllListeners("error");
|
|
1202
|
+
}
|
|
1203
|
+
detachCurrentChild() {
|
|
1204
|
+
const child = this.child;
|
|
1205
|
+
this.clearStartupWatchdog();
|
|
1206
|
+
this.child = undefined;
|
|
1207
|
+
this.childReady = false;
|
|
1208
|
+
this.replayingInitialization = false;
|
|
1209
|
+
this.initializeSentToWorker = false;
|
|
1210
|
+
if (child) {
|
|
1211
|
+
child.stdout.removeAllListeners("data");
|
|
1212
|
+
child.stderr.removeAllListeners("data");
|
|
1213
|
+
child.stdin.removeAllListeners("error");
|
|
643
1214
|
}
|
|
644
|
-
this.restartTimer = setTimeout(() => {
|
|
645
|
-
this.restartTimer = undefined;
|
|
646
|
-
this.spawnWorker();
|
|
647
|
-
}, 100);
|
|
648
1215
|
}
|
|
649
|
-
|
|
1216
|
+
recoverTimedOutWorker() {
|
|
650
1217
|
const child = this.child;
|
|
651
1218
|
if (!child) {
|
|
652
|
-
this.
|
|
1219
|
+
this.scheduleRestart(true);
|
|
653
1220
|
return;
|
|
654
1221
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1222
|
+
this.invalidateCurrentChild(child);
|
|
1223
|
+
this.beginTreeTermination(child);
|
|
1224
|
+
this.spawnWorker();
|
|
1225
|
+
}
|
|
1226
|
+
beginTreeTermination(child) {
|
|
1227
|
+
const pid = child.pid;
|
|
1228
|
+
if (pid === undefined) {
|
|
1229
|
+
try {
|
|
1230
|
+
child.kill("SIGKILL");
|
|
1231
|
+
}
|
|
1232
|
+
catch { /* best effort */ }
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (!this.cleanupStates.has(child)) {
|
|
1236
|
+
this.cleanupStates.set(child, { pid, status: "pending", parentExited: false });
|
|
1237
|
+
}
|
|
1238
|
+
if (this.treeTerminator) {
|
|
1239
|
+
let operation;
|
|
1240
|
+
try {
|
|
1241
|
+
operation = this.treeTerminator(pid);
|
|
1242
|
+
}
|
|
1243
|
+
catch {
|
|
1244
|
+
operation = false;
|
|
1245
|
+
}
|
|
1246
|
+
if (typeof operation === "boolean") {
|
|
1247
|
+
if (!operation) {
|
|
1248
|
+
try {
|
|
1249
|
+
child.kill("SIGKILL");
|
|
1250
|
+
}
|
|
1251
|
+
catch { /* best effort */ }
|
|
1252
|
+
}
|
|
1253
|
+
this.finishTreeTermination(child, operation);
|
|
1254
|
+
}
|
|
1255
|
+
else {
|
|
1256
|
+
void settleTreeCleanupWithin(operation, this.treeCleanupTimeoutMs).then((success) => {
|
|
1257
|
+
if (!success) {
|
|
1258
|
+
try {
|
|
1259
|
+
child.kill("SIGKILL");
|
|
1260
|
+
}
|
|
1261
|
+
catch { /* best effort */ }
|
|
1262
|
+
}
|
|
1263
|
+
this.finishTreeTermination(child, success);
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (process.platform !== "win32") {
|
|
1269
|
+
if (terminatePosixProcessGroup(pid)) {
|
|
1270
|
+
this.finishTreeTermination(child, true);
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
try {
|
|
1274
|
+
child.kill("SIGKILL");
|
|
1275
|
+
}
|
|
1276
|
+
catch { /* best effort */ }
|
|
1277
|
+
this.finishTreeTermination(child, false);
|
|
1278
|
+
}
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
let taskkill;
|
|
1282
|
+
try {
|
|
1283
|
+
taskkill = spawn("taskkill", buildWindowsTreeKillArgs(pid), {
|
|
1284
|
+
stdio: "ignore",
|
|
1285
|
+
windowsHide: true
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
catch {
|
|
1289
|
+
try {
|
|
1290
|
+
child.kill("SIGKILL");
|
|
1291
|
+
}
|
|
1292
|
+
catch { /* best effort */ }
|
|
1293
|
+
this.finishTreeTermination(child, false);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const operation = new Promise((resolve) => {
|
|
1297
|
+
let settled = false;
|
|
1298
|
+
const finish = (success) => {
|
|
1299
|
+
if (settled)
|
|
1300
|
+
return;
|
|
1301
|
+
settled = true;
|
|
1302
|
+
resolve(success);
|
|
1303
|
+
};
|
|
1304
|
+
taskkill.once("error", () => finish(false));
|
|
1305
|
+
taskkill.once("exit", (code) => finish(code === 0));
|
|
1306
|
+
});
|
|
1307
|
+
void settleTreeCleanupWithin(operation, this.treeCleanupTimeoutMs, () => {
|
|
1308
|
+
try {
|
|
1309
|
+
taskkill.kill("SIGKILL");
|
|
1310
|
+
}
|
|
1311
|
+
catch { /* best effort */ }
|
|
1312
|
+
}).then((success) => {
|
|
1313
|
+
if (!success) {
|
|
1314
|
+
try {
|
|
1315
|
+
child.kill("SIGKILL");
|
|
1316
|
+
}
|
|
1317
|
+
catch { /* best effort */ }
|
|
1318
|
+
}
|
|
1319
|
+
this.finishTreeTermination(child, success);
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
finishTreeTermination(child, success) {
|
|
1323
|
+
const cleanup = this.cleanupStates.get(child);
|
|
1324
|
+
if (!cleanup)
|
|
1325
|
+
return;
|
|
1326
|
+
cleanup.status = success ? "accepted" : "unresolved";
|
|
1327
|
+
if (!cleanup.parentExited)
|
|
1328
|
+
return;
|
|
1329
|
+
if (success) {
|
|
1330
|
+
this.unresolvedTreeTokens.delete(cleanup.pid);
|
|
1331
|
+
this.cleanupStates.delete(child);
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
this.unresolvedTreeTokens.add(cleanup.pid);
|
|
1335
|
+
}
|
|
1336
|
+
this.resumePausedRestart();
|
|
1337
|
+
this.drainQueue();
|
|
1338
|
+
}
|
|
1339
|
+
async retryUnresolvedTreeToken(pid) {
|
|
1340
|
+
let success = false;
|
|
1341
|
+
if (this.treeTokenRetrier) {
|
|
1342
|
+
success = await settleTreeCleanupWithin(Promise.resolve().then(() => this.treeTokenRetrier(pid)), this.treeCleanupTimeoutMs);
|
|
1343
|
+
}
|
|
1344
|
+
else if (process.platform !== "win32") {
|
|
1345
|
+
success = retryPosixTreeToken(pid);
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
let taskkill;
|
|
1349
|
+
const operation = new Promise((resolve) => {
|
|
1350
|
+
try {
|
|
1351
|
+
taskkill = spawn("taskkill", buildWindowsTreeKillArgs(pid), {
|
|
1352
|
+
stdio: "ignore",
|
|
1353
|
+
windowsHide: true
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
catch {
|
|
1357
|
+
resolve(false);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
let settled = false;
|
|
1361
|
+
const finish = (value) => {
|
|
1362
|
+
if (settled)
|
|
1363
|
+
return;
|
|
1364
|
+
settled = true;
|
|
1365
|
+
resolve(value);
|
|
1366
|
+
};
|
|
1367
|
+
taskkill.once("error", () => finish(false));
|
|
1368
|
+
taskkill.once("exit", (code) => finish(code === 0));
|
|
1369
|
+
});
|
|
1370
|
+
success = await settleTreeCleanupWithin(operation, this.treeCleanupTimeoutMs, () => {
|
|
1371
|
+
try {
|
|
1372
|
+
taskkill?.kill("SIGKILL");
|
|
1373
|
+
}
|
|
1374
|
+
catch { /* best effort */ }
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
if (!success)
|
|
1378
|
+
return;
|
|
1379
|
+
this.unresolvedTreeTokens.delete(pid);
|
|
1380
|
+
for (const [child, cleanup] of this.cleanupStates) {
|
|
1381
|
+
if (cleanup.pid === pid && cleanup.parentExited) {
|
|
1382
|
+
this.cleanupStates.delete(child);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
handleStartupFailure(token, exit) {
|
|
1387
|
+
if (token !== this.attemptToken && this.child !== undefined)
|
|
1388
|
+
return;
|
|
1389
|
+
this.clearStartupWatchdog();
|
|
1390
|
+
this.failQueuedRequestsOnStartupFailure(exit);
|
|
1391
|
+
this.scheduleRestart(true);
|
|
1392
|
+
}
|
|
1393
|
+
failQueuedRequestsOnStartupFailure(exit) {
|
|
1394
|
+
const now = this.monotonicNow();
|
|
1395
|
+
const queued = this.queuedRequests.splice(0, this.queuedRequests.length);
|
|
1396
|
+
for (const { pending } of queued) {
|
|
1397
|
+
if (pending.deadlineTimer)
|
|
1398
|
+
this.timerClearer(pending.deadlineTimer);
|
|
1399
|
+
const { reply } = buildWorkerRestartReply(pending, exit, now, [], {
|
|
1400
|
+
structuredRestartDisabled: STRUCTURED_RESTART_DISABLED
|
|
1401
|
+
});
|
|
1402
|
+
this.writeToClient(reply);
|
|
1403
|
+
}
|
|
1404
|
+
const retainedInitialize = this.queuedNotifications.find((message) => isRequest(message) && message.method === "initialize");
|
|
1405
|
+
this.queuedNotifications.splice(0, this.queuedNotifications.length);
|
|
1406
|
+
if (retainedInitialize && isRequest(retainedInitialize)) {
|
|
1407
|
+
this.writeToClient(buildLegacyJsonRpcError(retainedInitialize.id));
|
|
1408
|
+
}
|
|
1409
|
+
this.clearInitialInitializationState();
|
|
1410
|
+
this.validateBarrierKey = undefined;
|
|
1411
|
+
this.runningValidateKey = undefined;
|
|
1412
|
+
}
|
|
1413
|
+
scheduleRestart(_failedAttempt = false) {
|
|
1414
|
+
if (this.restartTimer || this.shuttingDown || this.child || this.currentRetryReservation) {
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const reservation = this.restartBackoff.reserve(this.monotonicNow());
|
|
1418
|
+
this.currentRetryReservation = reservation;
|
|
1419
|
+
this.currentRetryEpoch = reservation.epoch;
|
|
1420
|
+
if (this.liveCapOccupancy() >= 2) {
|
|
1421
|
+
this.retryPaused = true;
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
this.armRestartReservation(reservation);
|
|
1425
|
+
}
|
|
1426
|
+
armRestartReservation(reservation) {
|
|
1427
|
+
if (this.restartTimer || this.shuttingDown)
|
|
1428
|
+
return;
|
|
1429
|
+
const remaining = Math.max(0, reservation.notBefore - this.monotonicNow());
|
|
1430
|
+
this.restartTimer = this.timerScheduler(() => {
|
|
1431
|
+
this.restartTimer = undefined;
|
|
1432
|
+
if (this.shuttingDown ||
|
|
1433
|
+
reservation.epoch !== this.currentRetryEpoch ||
|
|
1434
|
+
this.child ||
|
|
1435
|
+
this.liveCapOccupancy() >= 2) {
|
|
1436
|
+
if (!this.shuttingDown &&
|
|
1437
|
+
reservation.epoch === this.currentRetryEpoch &&
|
|
1438
|
+
this.liveCapOccupancy() >= 2) {
|
|
1439
|
+
this.retryPaused = true;
|
|
1440
|
+
}
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
this.currentRetryEpoch = undefined;
|
|
1444
|
+
this.currentRetryReservation = undefined;
|
|
1445
|
+
this.spawnWorker();
|
|
1446
|
+
}, remaining);
|
|
1447
|
+
}
|
|
1448
|
+
resumePausedRestart() {
|
|
1449
|
+
if (!this.retryPaused || this.shuttingDown || this.child || this.liveCapOccupancy() >= 2)
|
|
1450
|
+
return;
|
|
1451
|
+
this.retryPaused = false;
|
|
1452
|
+
const reservation = this.currentRetryReservation;
|
|
1453
|
+
if (!reservation) {
|
|
1454
|
+
this.scheduleRestart(true);
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
if (reservation.epoch !== this.currentRetryEpoch)
|
|
1458
|
+
return;
|
|
1459
|
+
this.armRestartReservation(reservation);
|
|
662
1460
|
}
|
|
663
1461
|
async shutdown() {
|
|
664
1462
|
if (this.shuttingDown) {
|
|
@@ -666,20 +1464,34 @@ export class StdioSupervisor {
|
|
|
666
1464
|
}
|
|
667
1465
|
this.shuttingDown = true;
|
|
668
1466
|
if (this.restartTimer) {
|
|
669
|
-
|
|
1467
|
+
this.timerClearer(this.restartTimer);
|
|
670
1468
|
this.restartTimer = undefined;
|
|
671
1469
|
}
|
|
1470
|
+
this.currentRetryEpoch = undefined;
|
|
1471
|
+
this.currentRetryReservation = undefined;
|
|
1472
|
+
this.retryPaused = false;
|
|
1473
|
+
this.clearStartupWatchdog();
|
|
1474
|
+
for (const pending of this.pendingRequests.values()) {
|
|
1475
|
+
if (pending.deadlineTimer)
|
|
1476
|
+
this.timerClearer(pending.deadlineTimer);
|
|
1477
|
+
}
|
|
1478
|
+
for (const { pending } of this.queuedRequests) {
|
|
1479
|
+
if (pending.deadlineTimer)
|
|
1480
|
+
this.timerClearer(pending.deadlineTimer);
|
|
1481
|
+
}
|
|
1482
|
+
this.pendingRequests.clear();
|
|
1483
|
+
this.queuedRequests.splice(0, this.queuedRequests.length);
|
|
672
1484
|
process.stdin.off("data", this.handleClientData);
|
|
673
1485
|
process.stdin.off("error", this.handleClientError);
|
|
674
1486
|
process.stdin.off("end", this.handleClientClosed);
|
|
675
1487
|
process.stdin.off("close", this.handleClientClosed);
|
|
676
1488
|
process.off("SIGINT", this.handleTerminateSignal);
|
|
677
1489
|
process.off("SIGTERM", this.handleTerminateSignal);
|
|
678
|
-
|
|
679
|
-
this.
|
|
680
|
-
|
|
681
|
-
child.kill("SIGTERM");
|
|
1490
|
+
this.detachCurrentChild();
|
|
1491
|
+
for (const child of this.liveChildren) {
|
|
1492
|
+
this.beginTreeTermination(child);
|
|
682
1493
|
}
|
|
1494
|
+
await Promise.all([...this.unresolvedTreeTokens].map((pid) => this.retryUnresolvedTreeToken(pid)));
|
|
683
1495
|
}
|
|
684
1496
|
}
|
|
685
1497
|
export const STDIO_WORKER_MODE_ENV = WORKER_MODE_ENV;
|