@adhisang/minecraft-modding-mcp 7.0.0-rc.2 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import fastGlob from "fast-glob";
2
+ import { DECOMPILE_SIGNATURE_QUALIFIER, jarArtifactIdentity } from "../artifact-identity.js";
2
3
  import { buildArtifactAlias } from "../config.js";
3
4
  import { buildSuggestedCall } from "../build-suggested-call.js";
4
5
  import { ERROR_CODES, createError, isAppError } from "../errors.js";
@@ -7,9 +8,8 @@ import { log } from "../logger.js";
7
8
  import { applyMappingPipeline } from "../mapping-pipeline-service.js";
8
9
  import { parseCoordinate } from "../maven-resolver.js";
9
10
  import { resolveMojangTinyFile } from "../mojang-tiny-mapping-service.js";
10
- import { artifactSignatureFromFile } from "../path-resolver.js";
11
11
  import { detectFabricLikeInputNamespace, listJavaEntries } from "../source-jar-reader.js";
12
- import { artifactIdForJar, resolveSourceTarget as resolveSourceTargetInternal } from "../source-resolver.js";
12
+ import { resolveSourceTarget as resolveSourceTargetInternal } from "../source-resolver.js";
13
13
  import { resolveTinyRemapperJar } from "../tiny-remapper-resolver.js";
14
14
  import { isUnobfuscatedVersion } from "../version-service.js";
15
15
  import { dedupeQualityFlags, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
@@ -562,8 +562,10 @@ export async function probeMinecraftArtifact(svc, input) {
562
562
  });
563
563
  }
564
564
  const selectedSourceJarPath = versionSourceDiscovery.selectedSourceJarPath;
565
- const sourceSignature = artifactSignatureFromFile(selectedSourceJarPath).signature;
566
- const artifactId = artifactIdForJar("jar", selectedSourceJarPath, sourceSignature);
565
+ // The same helper `resolveSourceTarget`'s jar branch uses, so the id this
566
+ // probe publishes and the id a full resolve mints for the same jar are
567
+ // derived and composed by one piece of code.
568
+ const { artifactId } = await jarArtifactIdentity(selectedSourceJarPath);
567
569
  warnings.push(`Resolved source-backed artifact from Loom cache candidate: ${selectedSourceJarPath}.`);
568
570
  if (versionSourceDiscovery.selectedHasMinecraftNamespace === false) {
569
571
  warnings.push(`Source coverage does not include net.minecraft for ${selectedSourceJarPath}; class lookups may fall back to the binary artifact.`);
@@ -577,8 +579,14 @@ export async function probeMinecraftArtifact(svc, input) {
577
579
  ...(warnings.length > 0 ? { warnings } : {})
578
580
  };
579
581
  }
580
- const binarySignature = artifactSignatureFromFile(versionJar.jarPath).signature;
581
- const artifactId = artifactIdForJar("jar", versionJar.jarPath, `${binarySignature}:decompile`);
582
+ // "Lightweight" describes what this probe SKIPS: it neither decompiles nor
583
+ // rebuilds the index. It does read the version jar's bytes, because that is
584
+ // what the jar route derives an id from, so the first probe of a given jar in
585
+ // a process pays for one hash of it; the digest memo in
586
+ // `src/artifact-identity.ts` covers the ones after that.
587
+ const { artifactId } = await jarArtifactIdentity(versionJar.jarPath, {
588
+ signatureQualifier: DECOMPILE_SIGNATURE_QUALIFIER
589
+ });
582
590
  return {
583
591
  artifactId,
584
592
  mappingApplied: effectiveMapping,
@@ -9,6 +9,7 @@ import { remapAndCountMembers, sliceMembersWithLimit, projectMembersForWire, pro
9
9
  import { collectDidYouMeanCandidates } from "./did-you-mean.js";
10
10
  import { matchesMemberPattern } from "./member-pattern.js";
11
11
  import { findNestedJarClasses, resolveUniqueNestedJarForClass } from "./nested-jars.js";
12
+ import { extractSymbolsFromSource } from "../symbols/symbol-extractor.js";
12
13
  import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "../page-cursor.js";
13
14
  import { dedupeQualityFlags, inheritArtifactMapping, normalizeMapping, normalizeOptionalString, normalizePathStyle } from "./shared-utils.js";
14
15
  import { isUnobfuscatedVersion } from "../version-service.js";
@@ -407,6 +408,50 @@ function projectDecompiledFallback(fallback, level) {
407
408
  // The class-like symbol kinds findClass returns. MUST stay in sync with the JS-side
408
409
  // isTypeSymbol checks below; pushed down to SQL so non-type rows are never fetched.
409
410
  const TYPE_SYMBOL_KINDS = ["class", "interface", "enum", "record"];
411
+ /**
412
+ * Reconstruct the full nesting chain between a file's top-level type and a
413
+ * type declared two or more levels deeper inside it. The extractor stores
414
+ * only ONE qualifiedName per FILE (the top-level type), so naively
415
+ * concatenating `<topLevelFQN>.<symbolName>` is only correct for exactly one
416
+ * level of nesting — it silently drops every intermediate enclosing type.
417
+ *
418
+ * Walks the file's own type declarations from the top-level type downward,
419
+ * using brace-range containment to find which declaration directly encloses
420
+ * `targetLine` at each level, collecting each intermediate simple name.
421
+ *
422
+ * Returns the intermediate simple names (outermost first, target excluded)
423
+ * or undefined when the chain cannot be reconstructed — e.g. the file's
424
+ * content is unavailable — in which case the caller falls back to the
425
+ * single-level formula.
426
+ */
427
+ function resolveNestedTypeChain(svc, artifactId, filePath, topSimpleName, targetLine) {
428
+ const fileRow = svc.filesRepo.getFileContent(artifactId, filePath);
429
+ if (!fileRow)
430
+ return undefined;
431
+ const lines = fileRow.content.split(/\r?\n/);
432
+ const fileSymbols = extractSymbolsFromSource(filePath, fileRow.content);
433
+ let currentBody = classSourceHelpers.computeBraceRange(lines, fileSymbols, topSimpleName);
434
+ if (!currentBody)
435
+ return undefined;
436
+ const chain = [];
437
+ // Bounded by the number of type symbols in the file; guards against ever
438
+ // looping on a malformed structure.
439
+ for (let step = 0; step <= fileSymbols.length; step += 1) {
440
+ const childRanges = classSourceHelpers.computeNestedTypeRanges(lines, fileSymbols, currentBody);
441
+ if (childRanges.some((range) => range.declarationLine === targetLine)) {
442
+ return chain;
443
+ }
444
+ const containingChild = childRanges.find((range) => range.declarationLine < targetLine && targetLine <= range.endLine);
445
+ if (!containingChild)
446
+ return undefined;
447
+ const childSymbol = fileSymbols.find((symbol) => symbol.line === containingChild.declarationLine);
448
+ if (!childSymbol)
449
+ return undefined;
450
+ chain.push(childSymbol.symbolName);
451
+ currentBody = containingChild;
452
+ }
453
+ return undefined;
454
+ }
410
455
  export function findClass(svc, input) {
411
456
  const className = input.className.trim();
412
457
  if (!className) {
@@ -500,8 +545,18 @@ export function findClass(svc, input) {
500
545
  const enclosingQualifiedName = row.qualifiedName ?? row.filePath.replace(/\.java$/, "").replaceAll("/", ".");
501
546
  const enclosingSimpleName = enclosingQualifiedName.split(".").at(-1) ?? enclosingQualifiedName;
502
547
  const nested = enclosingSimpleName !== row.symbolName;
548
+ let qualifiedName = nested ? `${enclosingQualifiedName}.${row.symbolName}` : enclosingQualifiedName;
549
+ if (nested) {
550
+ // The formula above assumes exactly one level of nesting. When the type
551
+ // is nested two or more levels deep, reconstruct the real chain so the
552
+ // intermediate enclosing type(s) are not silently dropped.
553
+ const intermediateChain = resolveNestedTypeChain(svc, artifactId, row.filePath, enclosingSimpleName, row.line);
554
+ if (intermediateChain && intermediateChain.length > 0) {
555
+ qualifiedName = `${enclosingQualifiedName}.${[...intermediateChain, row.symbolName].join(".")}`;
556
+ }
557
+ }
503
558
  candidates.push({
504
- qualifiedName: nested ? `${enclosingQualifiedName}.${row.symbolName}` : enclosingQualifiedName,
559
+ qualifiedName,
505
560
  filePath: row.filePath,
506
561
  line: row.line,
507
562
  symbolKind: row.symbolKind,
@@ -48,7 +48,6 @@ export declare function localM2CoordinateCandidatePaths(localM2Path: string, par
48
48
  */
49
49
  export declare const BINARY_JAR_NO_CLASSES_FLAG = "binary-jar-no-classes";
50
50
  export type { MappingVariant } from "./types.js";
51
- export declare function artifactIdForJar(inputKind: string, artifactPath: string, signature: string, suffix?: string, mappingVariant?: MappingVariant): string;
52
51
  export interface ResolveSourceTargetOptions {
53
52
  allowDecompile: boolean;
54
53
  preferBinaryOnly?: boolean;
@@ -1,78 +1,13 @@
1
- import { statSync } from "node:fs";
2
1
  import { readdir } from "node:fs/promises";
3
2
  import { basename, dirname, join, resolve as resolvePath, sep } from "node:path";
4
3
  import { homedir } from "node:os";
5
4
  import fastGlob from "fast-glob";
6
5
  import { createError, ERROR_CODES, isAppError } from "./errors.js";
7
6
  import { buildRemoteBinaryUrls, buildRemoteSourceUrls, groupToPath, hasExistingJar, isMutableMavenCoordinate, parseCoordinate, normalizedCoordinateValue } from "./maven-resolver.js";
8
- import { defaultDownloadPath, digestFile, discardCachedDownload, resolveCachedDownload } from "./repo-downloader.js";
9
- import { artifactSignatureFromFile, normalizeJarPath } from "./path-resolver.js";
10
- import { stableArtifactId } from "./config.js";
7
+ import { defaultDownloadPath, discardCachedDownload, resolveCachedDownload } from "./repo-downloader.js";
8
+ import { normalizeJarPath } from "./path-resolver.js";
9
+ import { composeArtifactId, contentDigestSignature, contentSignature, jarArtifactIdentity, DECOMPILE_SIGNATURE_QUALIFIER } from "./artifact-identity.js";
11
10
  import { hasAnyJarEntry, hasJavaSourceExtension } from "./source-jar-reader.js";
12
- function readStatsSignature(filePath) {
13
- const stats = artifactSignatureFromFile(filePath);
14
- return stats.signature;
15
- }
16
- /**
17
- * Digests already derived from a local jar, each pinned to the stat that
18
- * produced it.
19
- *
20
- * Keyed by the symlink-resolved path, so a jar reached through two names is
21
- * hashed once. The entry is only ever *reused*, never trusted on its own: a
22
- * mismatched mtime or size discards it, so the map cannot serve a digest for
23
- * bytes that have since been replaced.
24
- *
25
- * Bounded, like the helper caches in `src/source/artifact-resolver.ts`: this map
26
- * is module-level and lives as long as the process, and a long-running server
27
- * walks this cascade once per target-driven tool call, so an unbounded map grows
28
- * with every distinct jar path the server has ever seen. Eviction costs at most
29
- * one re-hash, which is exactly what a cache miss already costs.
30
- */
31
- const contentSignatureCache = new Map();
32
- const MAX_CONTENT_SIGNATURE_CACHE = 512;
33
- /** Insert, dropping the oldest key first when the bound is reached. */
34
- function rememberContentSignature(resolvedPath, entry) {
35
- if (!contentSignatureCache.has(resolvedPath) && contentSignatureCache.size >= MAX_CONTENT_SIGNATURE_CACHE) {
36
- const oldestKey = contentSignatureCache.keys().next().value;
37
- if (oldestKey) {
38
- contentSignatureCache.delete(oldestKey);
39
- }
40
- }
41
- contentSignatureCache.set(resolvedPath, entry);
42
- }
43
- /**
44
- * The identity of a jar sitting on local disk: a sha256 of its bytes.
45
- *
46
- * `~/.m2` and the Gradle module cache move a file's mtime for reasons that have
47
- * nothing to do with its contents - an eviction followed by a re-fetch of
48
- * byte-identical bytes, a filesystem restore, a plain `touch`. An `mtimeMs:size`
49
- * signature turns every one of those into a fresh artifactId and a fresh
50
- * decompile, which is exactly the instability the download cache's
51
- * content-addressed identity removed from the remote half of this cascade.
52
- *
53
- * Hashing is not free and this cascade is re-walked on every target-driven tool
54
- * call, so the digest is memoized against the stat that produced it. The stat is
55
- * taken *before* the digest on purpose: bytes replaced mid-hash are recorded
56
- * against a stat they no longer have, so the entry is rejected on the next call
57
- * and re-derived - a wasted hash, never a wrong identity.
58
- */
59
- async function contentSignature(jarPath) {
60
- // The same normalization `artifactSignatureFromFile` applied, kept so this
61
- // path still refuses a vanished or non-jar file the way it always has.
62
- const resolvedPath = normalizeJarPath(jarPath);
63
- const stats = statSync(resolvedPath);
64
- const cached = contentSignatureCache.get(resolvedPath);
65
- if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
66
- return cached.sha256;
67
- }
68
- const { contentSha256 } = await digestFile(resolvedPath);
69
- rememberContentSignature(resolvedPath, {
70
- mtimeMs: stats.mtimeMs,
71
- size: stats.size,
72
- sha256: contentSha256
73
- });
74
- return contentSha256;
75
- }
76
11
  /**
77
12
  * Whether a jar contains java sources, with archive errors deliberately
78
13
  * propagating.
@@ -445,28 +380,20 @@ async function resolveGradleCacheCoordinateCandidate(coordinate) {
445
380
  function resolveRemoteBinaryCandidate(coordinate, repos) {
446
381
  return buildRemoteBinaryUrls(repos, coordinate);
447
382
  }
448
- export function artifactIdForJar(inputKind, artifactPath, signature, suffix, mappingVariant = "pass") {
449
- const parts = [inputKind, artifactPath, signature, suffix ?? "source"];
450
- if (mappingVariant === "mojang-remapped") {
451
- parts.push("mojang-remapped");
452
- }
453
- return stableArtifactId(parts);
454
- }
455
- function artifactIdForCoordinate(coordinate, source, signature, mappingVariant = "pass") {
456
- const parts = ["coord", coordinate, source, signature];
457
- if (mappingVariant === "mojang-remapped") {
458
- parts.push("mojang-remapped");
459
- }
460
- return stableArtifactId(parts);
461
- }
462
383
  function resolvedAtNow() {
463
384
  return new Date().toISOString();
464
385
  }
465
386
  /** Shared shape for every artifact the coordinate cascade can return. */
466
387
  function coordinateArtifact(spec) {
467
388
  return {
468
- artifactId: artifactIdForCoordinate(spec.coordinate, spec.idSource, spec.signature, spec.mappingVariant ?? "pass"),
469
- artifactSignature: spec.signature,
389
+ artifactId: composeArtifactId({
390
+ space: "coordinate",
391
+ coordinate: spec.coordinate,
392
+ idSource: spec.idSource,
393
+ signature: spec.signature,
394
+ mappingVariant: spec.mappingVariant
395
+ }),
396
+ artifactSignature: spec.signature.value,
470
397
  origin: spec.origin,
471
398
  sourceJarPath: spec.sourceJarPath,
472
399
  binaryJarPath: spec.binaryJarPath,
@@ -595,7 +522,6 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
595
522
  };
596
523
  if (input.kind === "jar") {
597
524
  const resolvedJarPath = normalizeJarPath(input.value);
598
- const binarySignature = readStatsSignature(resolvedJarPath);
599
525
  const exactSourceJarPath = resolveExactJarSourceCandidate(resolvedJarPath);
600
526
  const adjacentSourceCandidates = await listAdjacentJarSourceCandidates(resolvedJarPath);
601
527
  const maybeAdjacentSourceCandidates = adjacentSourceCandidates.length > 0 ? adjacentSourceCandidates : undefined;
@@ -604,9 +530,10 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
604
530
  const siblingBinaryJarPath = resolveSiblingBinaryJarCandidate(resolvedJarPath);
605
531
  const binaryJarPath = siblingBinaryJarPath ??
606
532
  (basename(resolvedJarPath).endsWith("-sources.jar") ? undefined : resolvedJarPath);
533
+ const identity = await jarArtifactIdentity(resolvedJarPath);
607
534
  return {
608
- artifactId: artifactIdForJar("jar", resolvedJarPath, binarySignature),
609
- artifactSignature: binarySignature,
535
+ artifactId: identity.artifactId,
536
+ artifactSignature: identity.signature,
610
537
  origin: "local-jar",
611
538
  binaryJarPath,
612
539
  sourceJarPath: resolvedJarPath,
@@ -616,10 +543,10 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
616
543
  };
617
544
  }
618
545
  if (!preferBinaryOnly && await candidateHasJavaSources(exactSourceJarPath)) {
619
- const sourceSignature = readStatsSignature(exactSourceJarPath);
546
+ const identity = await jarArtifactIdentity(exactSourceJarPath);
620
547
  return {
621
- artifactId: artifactIdForJar("jar", exactSourceJarPath, sourceSignature),
622
- artifactSignature: sourceSignature,
548
+ artifactId: identity.artifactId,
549
+ artifactSignature: identity.signature,
623
550
  origin: "local-jar",
624
551
  binaryJarPath: resolvedJarPath,
625
552
  sourceJarPath: exactSourceJarPath,
@@ -648,9 +575,13 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
648
575
  // no `.java` entries.
649
576
  const subjectArchive = await inspectSubjectJarArchive(resolvedJarPath);
650
577
  const subjectQualityFlags = subjectArchive ? binaryJarQualityFlags(subjectArchive) : [];
578
+ const identity = await jarArtifactIdentity(resolvedJarPath, {
579
+ signatureQualifier: DECOMPILE_SIGNATURE_QUALIFIER,
580
+ mappingVariant: options.mappingVariant
581
+ });
651
582
  return {
652
- artifactId: artifactIdForJar("jar", resolvedJarPath, `${binarySignature}:decompile`, undefined, options.mappingVariant ?? "pass"),
653
- artifactSignature: `${binarySignature}:decompile`,
583
+ artifactId: identity.artifactId,
584
+ artifactSignature: identity.signature,
654
585
  origin: "decompiled",
655
586
  binaryJarPath: resolvedJarPath,
656
587
  adjacentSourceCandidates: maybeAdjacentSourceCandidates,
@@ -843,7 +774,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
843
774
  // Identity follows the bytes. The HTTP validators that used to form this
844
775
  // signature rotate whenever a CDN or repository migration happens, even
845
776
  // when the jar is byte-identical.
846
- const signature = download.contentSha256;
777
+ const signature = contentDigestSignature(download.contentSha256);
847
778
  return coordinateArtifact({
848
779
  coordinate,
849
780
  idSource: "remote-repo",
@@ -983,7 +914,7 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
983
914
  }
984
915
  // A file in the URL-keyed download cache is identified by its bytes, not
985
916
  // by a stat signature that a re-download would change for free.
986
- const signature = downloaded.contentSha256;
917
+ const signature = contentDigestSignature(downloaded.contentSha256);
987
918
  return coordinateArtifact({
988
919
  coordinate,
989
920
  idSource: "decompiled",
@@ -238,6 +238,22 @@ export declare class StdioSupervisor {
238
238
  */
239
239
  private readonly syntheticTombstones;
240
240
  private child;
241
+ /**
242
+ * The one response `handleWorkerMessage` is part-way through settling.
243
+ *
244
+ * It removes a response's pending entry BEFORE writing the reply, so between
245
+ * those two statements the id is owed a response that NOTHING records: the
246
+ * entry is gone, no deadline is armed for anything but validate-project, and
247
+ * the worker considers the request answered. A fault in that window used to
248
+ * be indistinguishable from an id already settled, and the request stayed
249
+ * unanswered for the life of an otherwise healthy session.
250
+ *
251
+ * Set the moment the entry is deleted and cleared the moment the write
252
+ * returns, so it is defined only inside that window and only ever names one
253
+ * request. `answerFaultedWorkerResponse` is the sole reader, and it consumes
254
+ * the marker rather than merely reading it.
255
+ */
256
+ private settlingWorkerResponse;
241
257
  private childReady;
242
258
  /**
243
259
  * Monotonic timestamp of the current generation's adoption, or undefined
@@ -338,17 +354,47 @@ export declare class StdioSupervisor {
338
354
  */
339
355
  private readonly handleFatalError;
340
356
  /**
341
- * Admission entry point, wrapped so a fault can never make a request vanish.
357
+ * Admission entry point, wrapped so a fault cannot silently drop a request.
342
358
  *
343
359
  * Anything thrown while classifying, queueing or forwarding a client frame
344
360
  * propagates out of the frame reader's `onFrame`, where the reader swallows
345
361
  * it as a parse error — the request then gets NO reply and the client waits
346
362
  * on that id forever (and on a modern-era request the one-way era lock has
347
- * already happened). Answering the id with -32603 keeps the
348
- * exactly-one-response guarantee; id-less frames (notifications, malformed
349
- * ids) have nothing to answer and are only logged.
363
+ * already happened). The catch below answers the id with -32603 instead.
364
+ *
365
+ * The reply is conditional, and on exactly one thing: that nothing this
366
+ * admission installed is still live at the id. A surviving instance is
367
+ * already tracked and will be settled by an ordinary path, so answering
368
+ * alongside it would make two terminal replies for one request; the catch
369
+ * reports and stands down in that case rather than answering. Id-less frames
370
+ * (notifications, malformed ids) have nothing to answer and are only logged.
350
371
  */
351
372
  private handleClientMessage;
373
+ /**
374
+ * Runs one step of a fault-recovery path so that it cannot throw.
375
+ *
376
+ * The two frame-reader callers — the admission catch in handleClientMessage
377
+ * and the worker-frame catch in handleWorkerData — execute inside a
378
+ * {@link JsonRpcFrameReader}'s `onFrame`, where the reader deliberately
379
+ * converts an escaping throw into a plain parse error: the frame is then
380
+ * silently dropped, which for a recovery path means the request it was
381
+ * rescuing is lost after all. `dispatchQueuedRequest` is the third caller,
382
+ * and its exposure is wider rather than narrower: a drain is reached from
383
+ * those two `onFrame` paths, but also from timer callbacks
384
+ * (handleValidateProjectDeadline) and from process-event handlers
385
+ * (handleWorkerExit), where nothing above it is prepared to contain a throw
386
+ * at all.
387
+ *
388
+ * Reporting goes through `log` rather than the injected `eventWriter` so
389
+ * that an INJECTED writer — the collaborator most likely to have thrown — is
390
+ * not also the reporter. That NARROWS the failure; it does not remove it.
391
+ * `eventWriter` defaults to `log` (see the constructor), so in the default
392
+ * configuration, where nothing is injected, the reporter IS the writer that
393
+ * just faulted. The report therefore carries its own try/catch whose handler
394
+ * does nothing: past this point there is no reporting channel left, and a
395
+ * throw here would drop the frame this method exists to save.
396
+ */
397
+ private runRecoveryStep;
352
398
  /**
353
399
  * Every request INSTANCE live at `key` right now: the forwarded entry plus any
354
400
  * queued ones. Captured before admission runs so a rollback can tell the state
@@ -374,6 +420,20 @@ export declare class StdioSupervisor {
374
420
  * Returns whether anything was rolled back.
375
421
  */
376
422
  private rollbackFailedAdmission;
423
+ /**
424
+ * Removes every QUEUED instance at `key` that is absent from `preexisting`.
425
+ *
426
+ * Split out of rollbackFailedAdmission so the admission catch's finality
427
+ * check can repeat the removal without re-entering the method that may just
428
+ * have thrown. Returns whether anything was removed; calling it twice is
429
+ * harmless, because the second call finds nothing.
430
+ */
431
+ private dropQueuedInstances;
432
+ /**
433
+ * Whether any request instance live at `key` is absent from `preexisting` —
434
+ * that is, whether the admission being rolled back still owns its id.
435
+ */
436
+ private hasInstanceOutside;
377
437
  private routeClientMessage;
378
438
  /**
379
439
  * Era-aware notification dispatch. notifications/cancelled stays
@@ -499,8 +559,94 @@ export declare class StdioSupervisor {
499
559
  */
500
560
  private handleValidateProjectDeadline;
501
561
  private drainQueue;
562
+ /**
563
+ * Forwards one queued entry, containing a fault in the forward itself.
564
+ *
565
+ * `forwardRequest` installs the pending entry and takes the validate
566
+ * slot/barrier BEFORE it writes to the worker's stdin, so a throw from the
567
+ * encode or the write leaves the request holding an id it never reached the
568
+ * worker on. Unguarded, that request is unreachable: it has been shifted out
569
+ * of the queue, so no later drain can dispatch it, and no worker will answer
570
+ * an id it never saw — and only validate-project arms a deadline, so nothing
571
+ * else settles it either. The one containing guard above this
572
+ * (`runRecoveryStep` around the recovery drain) contains the exception
573
+ * without recovering the request, and stops the drain on top of that,
574
+ * leaving everything behind it parked as well.
575
+ *
576
+ * Both are repaired here: the entry is released — which records the finality
577
+ * tombstone, so a worker that did somehow see the bytes cannot answer over
578
+ * the reply below — terminally answered, and the drain carries on to the
579
+ * next entry. Containment is per-entry because the fault this catches is
580
+ * per-message: an encode failure on one payload, or a stdin `write` that
581
+ * throws. A stdin that is merely destroyed never reaches this catch at all —
582
+ * `forwardRequest` tests for that first and takes its own no-child fallback,
583
+ * which does not throw.
584
+ *
585
+ * The `initialize` carve-out in `releaseForwardedRequest` is honoured: if it
586
+ * declines, nothing is answered here and the handshake lifecycle keeps the
587
+ * entry. Admission parks `initialize` in `queuedNotifications` rather than in
588
+ * `queuedRequests`, so the ordinary path never puts one in front of this
589
+ * drain; `forwardRequest`'s own no-child fallback is the one way an
590
+ * initialize can end up queued, and the carve-out is what covers it.
591
+ *
592
+ * Returns whether the entry reached the worker.
593
+ */
594
+ private dispatchQueuedRequest;
502
595
  private spawnWorker;
503
596
  private handleWorkerData;
597
+ /**
598
+ * Rescues the request a faulted worker frame was answering.
599
+ *
600
+ * `handleWorkerMessage` runs inside the worker reader's `onFrame`, where a
601
+ * throw becomes a parse error and the frame is simply dropped. For a
602
+ * RESPONSE frame that left the pending entry live with no reply and nothing
603
+ * left to settle it — deadlines are armed for validate-project only, so any
604
+ * other tool had no rescue at all and the client waited on that id for the
605
+ * life of the session. The id is released and answered here instead.
606
+ *
607
+ * The queue drain is the OTHER half, and it is why the answering half lives
608
+ * in its own method: `handleWorkerMessage` deletes a response's pending
609
+ * entry before the `writeToClient` that can throw, and its `drainQueue()` is
610
+ * the last statement of all. So the commonest fault arrives here with the
611
+ * entry already gone AND the queue undrained, and every early return in the
612
+ * answering path is a case where the drain is the only rescue left.
613
+ *
614
+ * What this recovery is worth, stated exactly. It answers the id when this
615
+ * supervisor is the one entitled to (see `answerFaultedWorkerResponse`), and
616
+ * every id it answers is tombstoned, so the worker's own answer for that id
617
+ * cannot become a second reply. It does NOT make one-response-per-id a
618
+ * property of the whole file: an id whose entry could not be released is
619
+ * deliberately left unanswered here rather than answered twice, the
620
+ * `initialize` carve-out is settled by the handshake lifecycle instead, and
621
+ * tombstone retention is capped at {@link MAX_SYNTHETIC_TOMBSTONES} — past
622
+ * 1024 live tombstones in one worker generation the oldest is evicted, and a
623
+ * very old worker's late answer for an evicted id would pass through.
624
+ *
625
+ * What this deliberately does NOT do: tear the session down or touch framing
626
+ * state. One frame the supervisor could not handle is not evidence that the
627
+ * worker's stream desynchronized (that is `supervisor.worker_framing_fatal`,
628
+ * reported by the reader itself), and the reader's own state already
629
+ * describes the stream correctly. The one exception is an in-flight
630
+ * `initialize`, whose generation cannot finish its handshake once its answer
631
+ * has been lost — see `answerFaultedWorkerResponse`.
632
+ */
633
+ private recoverFaultedWorkerMessage;
634
+ /**
635
+ * Terminally answers the client request a faulted worker RESPONSE frame was
636
+ * carrying the answer for, if this supervisor is still the one entitled to
637
+ * answer it. Returns without replying otherwise; the caller's drain runs
638
+ * either way.
639
+ */
640
+ private answerFaultedWorkerResponse;
641
+ /**
642
+ * Answers a request whose pending entry `handleWorkerMessage` had already
643
+ * removed when it faulted, and which therefore never reached the client.
644
+ *
645
+ * Consumed once: the marker is cleared before the reply is attempted, so a
646
+ * fault in the reply cannot leave a stale claim on the id for a later,
647
+ * unrelated fault to act on.
648
+ */
649
+ private answerUndeliveredWorkerResponse;
504
650
  private handleWorkerStdinError;
505
651
  /**
506
652
  * Reassembles the worker's stderr into lines (the ready marker may be split