@adhisang/minecraft-modding-mcp 6.1.0 → 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.
Files changed (54) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +4 -2
  3. package/dist/cache-registry.d.ts +1 -1
  4. package/dist/cache-registry.js +3 -0
  5. package/dist/entry-tools/analyze-mod-service.d.ts +12 -6
  6. package/dist/entry-tools/analyze-mod-service.js +37 -3
  7. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -0
  8. package/dist/entry-tools/analyze-symbol-service.js +37 -2
  9. package/dist/entry-tools/inspect-minecraft/internal.d.ts +4 -0
  10. package/dist/entry-tools/inspect-minecraft/internal.js +28 -0
  11. package/dist/entry-tools/manage-cache-service.d.ts +4 -4
  12. package/dist/error-mapping.d.ts +13 -0
  13. package/dist/error-mapping.js +35 -2
  14. package/dist/errors.d.ts +2 -0
  15. package/dist/errors.js +2 -0
  16. package/dist/index.js +37 -17
  17. package/dist/mapping/internal-types.d.ts +7 -0
  18. package/dist/mapping/types.d.ts +18 -0
  19. package/dist/mapping-service.js +16 -3
  20. package/dist/minecraft-explorer-service.d.ts +4 -0
  21. package/dist/minecraft-explorer-service.js +156 -17
  22. package/dist/mod-analyzer.d.ts +7 -0
  23. package/dist/mod-analyzer.js +28 -7
  24. package/dist/nbt/typed-json.js +4 -1
  25. package/dist/source/artifact-resolver.d.ts +2 -0
  26. package/dist/source/artifact-resolver.js +24 -1
  27. package/dist/source/class-source/members-builder.d.ts +4 -0
  28. package/dist/source/class-source/members-builder.js +3 -1
  29. package/dist/source/class-source.d.ts +2 -1
  30. package/dist/source/class-source.js +154 -17
  31. package/dist/source/did-you-mean.d.ts +14 -0
  32. package/dist/source/did-you-mean.js +79 -0
  33. package/dist/source/file-access.js +159 -3
  34. package/dist/source/indexer.js +72 -2
  35. package/dist/source/lifecycle/runtime-check.js +15 -7
  36. package/dist/source/nested-jars.d.ts +59 -0
  37. package/dist/source/nested-jars.js +198 -0
  38. package/dist/source/workspace-target.js +5 -2
  39. package/dist/source-jar-reader.d.ts +16 -0
  40. package/dist/source-jar-reader.js +82 -0
  41. package/dist/source-service.d.ts +36 -0
  42. package/dist/source-service.js +49 -6
  43. package/dist/stage-emitter.js +24 -8
  44. package/dist/stdio-supervisor.d.ts +92 -9
  45. package/dist/stdio-supervisor.js +915 -103
  46. package/dist/tool-contract-manifest.js +1 -1
  47. package/dist/tool-guidance.js +3 -1
  48. package/dist/tool-schemas.d.ts +1337 -149
  49. package/dist/tool-schemas.js +38 -7
  50. package/dist/types.d.ts +23 -0
  51. package/dist/workspace-mapping-service.d.ts +1 -0
  52. package/dist/workspace-mapping-service.js +120 -8
  53. package/docs/tool-reference.md +19 -3
  54. package/package.json +1 -1
@@ -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
- workerReader = new JsonRpcFrameReader();
302
- queuedMessages = [];
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 (!this.childReady) {
364
- this.queuedMessages.push(message);
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.forwardToWorker(message);
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
- forwardToWorker(message) {
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.queuedMessages.push(message);
373
- if (!this.shuttingDown) {
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
- if (isRequest(message)) {
379
- const id = getTrackedRequestId(message);
380
- if (id !== undefined) {
381
- const pending = {
382
- id,
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
- spawnWorker() {
408
- if (this.shuttingDown) {
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
- const stale = this.child;
412
- if (stale) {
413
- this.detachChild();
414
- if (stale.exitCode === null) {
415
- stale.kill("SIGTERM");
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
- child.stdout.on("data", this.handleWorkerData);
431
- child.stderr.on("data", this.handleWorkerStderr);
432
- child.stdin.on("error", this.handleWorkerStdinError);
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 = (chunk) => {
438
- this.workerReader.processChunk(chunk, {
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 = (error) => {
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 = (chunk) => {
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
- if (child.exitCode === null && child.signalCode === null) {
471
- child.kill("SIGTERM");
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 (child !== this.child) {
477
- if (child.exitCode === null && child.signalCode === null) {
478
- child.kill("SIGTERM");
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.detachChild();
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.forwardToWorker(this.initializedNotification);
996
+ this.writeToWorker(child, this.initializedNotification);
518
997
  }
519
- this.childReady = true;
998
+ this.adoptActiveChild();
520
999
  this.flushQueue();
521
1000
  return;
522
1001
  }
523
1002
  this.clientInitialized = true;
524
- this.childReady = true;
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
- this.pendingRequests.delete(requestKey(id));
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 = performance.now();
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 = performance.now();
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.childReady = true;
1065
+ this.adoptActiveChild();
571
1066
  this.flushQueue();
572
1067
  return;
573
1068
  }
574
1069
  this.replayingInitialization = this.clientInitialized;
575
- this.forwardToWorker(this.initializeRequest);
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.queuedMessages.length === 0) {
1086
+ if (!this.childReady || !this.child) {
588
1087
  return;
589
1088
  }
590
- const pending = this.queuedMessages.splice(0, this.queuedMessages.length);
591
- for (const message of pending) {
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
- this.forwardToWorker(message);
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 = performance.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
- const frame = encodeJsonRpcMessage(message, this.clientMode);
638
- process.stdout.write(frame);
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
- scheduleRestart() {
641
- if (this.restartTimer || this.shuttingDown) {
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
- detachChild() {
1216
+ recoverTimedOutWorker() {
650
1217
  const child = this.child;
651
1218
  if (!child) {
652
- this.childReady = false;
1219
+ this.scheduleRestart(true);
653
1220
  return;
654
1221
  }
655
- child.stdout.off("data", this.handleWorkerData);
656
- child.stderr.off("data", this.handleWorkerStderr);
657
- child.stdin.off("error", this.handleWorkerStdinError);
658
- child.removeAllListeners("error");
659
- child.removeAllListeners("exit");
660
- this.child = undefined;
661
- this.childReady = false;
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
- clearTimeout(this.restartTimer);
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
- const child = this.child;
679
- this.detachChild();
680
- if (child && child.exitCode === null) {
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;