@adhisang/minecraft-modding-mcp 7.0.0 → 7.1.1
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 +72 -0
- package/README.md +4 -3
- package/dist/access-transformer-parser.d.ts +8 -0
- package/dist/access-transformer-parser.js +8 -1
- package/dist/access-widener-parser.d.ts +17 -0
- package/dist/access-widener-parser.js +12 -1
- package/dist/cache-registry.js +19 -6
- package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
- package/dist/entry-tools/batch-class-members-service.js +20 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
- package/dist/entry-tools/batch-class-source-service.js +10 -0
- package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
- package/dist/entry-tools/compare-minecraft-service.js +65 -4
- package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +15 -9
- package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
- package/dist/entry-tools/manage-cache-service.d.ts +2 -2
- package/dist/entry-tools/manage-cache-service.js +10 -14
- package/dist/entry-tools/validate-project/cases/access-transformer.js +22 -3
- package/dist/entry-tools/validate-project/cases/access-widener.js +31 -4
- package/dist/entry-tools/validate-project/cases/mixin.js +11 -3
- package/dist/entry-tools/validate-project/cases/project-summary.js +94 -16
- package/dist/entry-tools/validate-project-service.d.ts +2 -2
- package/dist/entry-tools/verify-mixin-target-service.js +19 -8
- package/dist/index.js +38 -15
- package/dist/java-process.d.ts +1 -0
- package/dist/java-process.js +14 -0
- package/dist/mapping/lookup.js +16 -1
- package/dist/mapping-service.d.ts +14 -0
- package/dist/mapping-service.js +35 -15
- package/dist/minecraft-explorer-service.js +70 -8
- package/dist/mixin/access-validators.js +38 -2
- package/dist/mixin/annotation-validators.js +137 -43
- package/dist/mixin/parsed-validator.js +21 -7
- package/dist/mixin-parser.d.ts +52 -0
- package/dist/mixin-parser.js +709 -130
- package/dist/mod-decompile-service.js +11 -1
- package/dist/mod-remap-service.js +6 -6
- package/dist/nbt/java-nbt-codec.js +7 -1
- package/dist/source/access-validate.js +10 -0
- package/dist/source/artifact-resolver.d.ts +27 -3
- package/dist/source/artifact-resolver.js +235 -30
- package/dist/source/class-source/members-builder.d.ts +7 -0
- package/dist/source/class-source/members-builder.js +4 -1
- package/dist/source/class-source.d.ts +9 -2
- package/dist/source/class-source.js +186 -27
- package/dist/source/indexer.js +69 -1
- package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
- package/dist/source/lifecycle/mapping-helpers.js +29 -3
- package/dist/source/lifecycle/runtime-check.d.ts +25 -0
- package/dist/source/lifecycle/runtime-check.js +68 -39
- package/dist/source/nested-jars.d.ts +15 -1
- package/dist/source/nested-jars.js +14 -5
- package/dist/source/search.d.ts +10 -2
- package/dist/source/search.js +60 -13
- package/dist/source/symbol-resolver.js +88 -0
- package/dist/source/validate-mixin/pipeline/mapping-health.js +20 -1
- package/dist/source/validate-mixin/pipeline/target-lookup.js +16 -7
- package/dist/source/validate-mixin.d.ts +5 -0
- package/dist/source/validate-mixin.js +136 -21
- package/dist/source/workspace-target.js +75 -7
- package/dist/source-jar-reader.d.ts +48 -1
- package/dist/source-jar-reader.js +93 -3
- package/dist/source-resolver.d.ts +7 -0
- package/dist/source-resolver.js +22 -14
- package/dist/source-service.d.ts +5 -0
- package/dist/source-service.js +7 -0
- package/dist/stdio-supervisor.d.ts +35 -1
- package/dist/stdio-supervisor.js +77 -2
- package/dist/storage/db.d.ts +62 -2
- package/dist/storage/db.js +181 -20
- package/dist/storage/files-repo.d.ts +7 -0
- package/dist/storage/files-repo.js +17 -4
- package/dist/storage/sqlite.d.ts +31 -1
- package/dist/storage/sqlite.js +125 -16
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +2 -1
- package/dist/tool-guidance.js +4 -1
- package/dist/tool-schemas.d.ts +64 -52
- package/dist/tool-schemas.js +9 -7
- package/dist/types.d.ts +9 -0
- package/dist/v1-parity-schemas.js +36 -2
- package/dist/version-diff-service.d.ts +23 -0
- package/dist/version-diff-service.js +101 -0
- package/dist/version-service.d.ts +14 -0
- package/dist/version-service.js +52 -3
- package/dist/workspace-context-cache.d.ts +25 -0
- package/dist/workspace-context-cache.js +52 -2
- package/dist/workspace-mapping-service.d.ts +8 -0
- package/dist/workspace-mapping-service.js +151 -21
- package/docs/README-ja.md +3 -1
- package/docs/tool-reference.md +69 -22
- package/package.json +1 -1
package/dist/source-resolver.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join, resolve as resolvePath, sep } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import fastGlob from "fast-glob";
|
|
5
4
|
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
5
|
+
import { resolveGradleUserHomePath } from "./gradle-paths.js";
|
|
6
6
|
import { buildRemoteBinaryUrls, buildRemoteSourceUrls, groupToPath, hasExistingJar, isMutableMavenCoordinate, parseCoordinate, normalizedCoordinateValue } from "./maven-resolver.js";
|
|
7
7
|
import { defaultDownloadPath, discardCachedDownload, resolveCachedDownload } from "./repo-downloader.js";
|
|
8
8
|
import { normalizeJarPath } from "./path-resolver.js";
|
|
9
9
|
import { composeArtifactId, contentDigestSignature, contentSignature, jarArtifactIdentity, DECOMPILE_SIGNATURE_QUALIFIER } from "./artifact-identity.js";
|
|
10
|
-
import { hasAnyJarEntry, hasJavaSourceExtension } from "./source-jar-reader.js";
|
|
10
|
+
import { hasAnyJarEntry, hasJavaSourceExtension, scanJarForJavaSources } from "./source-jar-reader.js";
|
|
11
11
|
/**
|
|
12
12
|
* Whether a jar contains java sources, with archive errors deliberately
|
|
13
13
|
* propagating.
|
|
14
14
|
*
|
|
15
15
|
* This is the check for a jar that IS the subject of the request - the
|
|
16
|
-
* `input.kind === "jar"` branch of `resolveSourceTarget`
|
|
17
|
-
* `resolvedJarPath
|
|
18
|
-
*
|
|
16
|
+
* `input.kind === "jar"` branch of `resolveSourceTarget` runs it on
|
|
17
|
+
* `resolvedJarPath` as `scanSubjectJarForJavaSources`, the same walk that also
|
|
18
|
+
* collects Minecraft runtime signals. What propagates is the reader's refusal
|
|
19
|
+
* to OPEN the archive: a truncated file, a non-zip file, an unreadable central directory,
|
|
19
20
|
* an I/O failure. On the subject jar that refusal is the verdict itself, and
|
|
20
21
|
* swallowing it would downgrade "this archive could not be read" into "this
|
|
21
22
|
* archive has no sources" - handing back a sibling `-sources.jar` as if the
|
|
@@ -73,6 +74,18 @@ async function candidateHasJavaSources(jarPath) {
|
|
|
73
74
|
return false;
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* `hasJavaSources` for the subject jar of a `kind: "jar"` request, returning the
|
|
79
|
+
* Minecraft runtime signals the same walk collected. Same contract on both counts
|
|
80
|
+
* that matter: an absent file answers "no sources" without opening anything, and
|
|
81
|
+
* an archive that cannot be opened propagates.
|
|
82
|
+
*/
|
|
83
|
+
async function scanSubjectJarForJavaSources(jarPath) {
|
|
84
|
+
if (!hasExistingJar(jarPath)) {
|
|
85
|
+
return { hasJavaSources: false };
|
|
86
|
+
}
|
|
87
|
+
return await scanJarForJavaSources(jarPath);
|
|
88
|
+
}
|
|
76
89
|
function resolveExactJarSourceCandidate(inputJarPath) {
|
|
77
90
|
const directory = dirname(inputJarPath);
|
|
78
91
|
const jarName = basename(inputJarPath);
|
|
@@ -331,16 +344,9 @@ async function firstReadableJarArchive(candidates) {
|
|
|
331
344
|
}
|
|
332
345
|
return undefined;
|
|
333
346
|
}
|
|
334
|
-
function resolveGradleUserHome() {
|
|
335
|
-
const configured = process.env.GRADLE_USER_HOME?.trim();
|
|
336
|
-
if (configured) {
|
|
337
|
-
return configured;
|
|
338
|
-
}
|
|
339
|
-
return resolvePath(homedir(), ".gradle");
|
|
340
|
-
}
|
|
341
347
|
async function resolveGradleCacheCoordinateCandidate(coordinate) {
|
|
342
348
|
const parsed = parseCoordinate(coordinate);
|
|
343
|
-
const baseDir = resolvePath(
|
|
349
|
+
const baseDir = resolvePath(resolveGradleUserHomePath(), "caches", "modules-2", "files-2.1", parsed.groupId, parsed.artifactId, parsed.version);
|
|
344
350
|
const base = `${parsed.artifactId}-${parsed.version}`;
|
|
345
351
|
const classifierSuffix = parsed.classifier ? `-${parsed.classifier}` : "";
|
|
346
352
|
const preferredSourceNames = [
|
|
@@ -526,7 +532,9 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
|
|
|
526
532
|
const adjacentSourceCandidates = await listAdjacentJarSourceCandidates(resolvedJarPath);
|
|
527
533
|
const maybeAdjacentSourceCandidates = adjacentSourceCandidates.length > 0 ? adjacentSourceCandidates : undefined;
|
|
528
534
|
const preferBinaryOnly = options.preferBinaryOnly ?? false;
|
|
529
|
-
|
|
535
|
+
const subjectScan = await scanSubjectJarForJavaSources(resolvedJarPath);
|
|
536
|
+
options.onSubjectJarScanned?.(subjectScan);
|
|
537
|
+
if (subjectScan.hasJavaSources) {
|
|
530
538
|
const siblingBinaryJarPath = resolveSiblingBinaryJarCandidate(resolvedJarPath);
|
|
531
539
|
const binaryJarPath = siblingBinaryJarPath ??
|
|
532
540
|
(basename(resolvedJarPath).endsWith("-sources.jar") ? undefined : resolvedJarPath);
|
package/dist/source-service.d.ts
CHANGED
|
@@ -671,6 +671,11 @@ export declare class SourceService {
|
|
|
671
671
|
listVersions(input?: ListVersionsInput): Promise<ListVersionsOutput>;
|
|
672
672
|
getRegistryData(input: GetRegistryDataInput): Promise<GetRegistryDataOutput>;
|
|
673
673
|
compareVersions(input: CompareVersionsInput): Promise<CompareVersionsOutput>;
|
|
674
|
+
/**
|
|
675
|
+
* Thin passthrough so entry tools (migration-overview) can diff a version
|
|
676
|
+
* pair's `libraries` without reaching into VersionService directly.
|
|
677
|
+
*/
|
|
678
|
+
getVersionLibraries(version: string): Promise<string[]>;
|
|
674
679
|
decompileModJar(input: DecompileModJarInput): Promise<DecompileModJarOutput>;
|
|
675
680
|
/**
|
|
676
681
|
* Member-level view of a third-party mod jar class, read from bytecode
|
package/dist/source-service.js
CHANGED
|
@@ -123,6 +123,13 @@ export class SourceService {
|
|
|
123
123
|
async compareVersions(input) {
|
|
124
124
|
return this.versionDiffService.compareVersions(input);
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Thin passthrough so entry tools (migration-overview) can diff a version
|
|
128
|
+
* pair's `libraries` without reaching into VersionService directly.
|
|
129
|
+
*/
|
|
130
|
+
async getVersionLibraries(version) {
|
|
131
|
+
return this.versionService.getVersionLibraries(version);
|
|
132
|
+
}
|
|
126
133
|
async decompileModJar(input) {
|
|
127
134
|
return this.modDecompileService.decompileModJar(input);
|
|
128
135
|
}
|
|
@@ -204,6 +204,15 @@ export declare class StdioSupervisor {
|
|
|
204
204
|
private readonly unresolvedTreeTokens;
|
|
205
205
|
private readonly restartBackoff;
|
|
206
206
|
private readonly terminalChildren;
|
|
207
|
+
/**
|
|
208
|
+
* Workers started by the DEFAULT spawner on POSIX, which spawns them
|
|
209
|
+
* detached — so each one leads its own process group and `-pid` names
|
|
210
|
+
* exactly that group. Only these are group-signalled after an unexpected
|
|
211
|
+
* exit: a child handed in by an injected spawner carries no such guarantee,
|
|
212
|
+
* and its pid may not be a group id this supervisor owns.
|
|
213
|
+
*/
|
|
214
|
+
private readonly processGroupLeaders;
|
|
215
|
+
private readonly spawnsProcessGroupLeaders;
|
|
207
216
|
/**
|
|
208
217
|
* Monotonic worker-generation counter (incremented per successful spawn).
|
|
209
218
|
* Recorded into finality tombstones so retention can be bounded per
|
|
@@ -310,7 +319,8 @@ export declare class StdioSupervisor {
|
|
|
310
319
|
* Number of queued requests that arrived before an initial initialize while
|
|
311
320
|
* the worker was unavailable. Only era-neutral modern discovers can occupy
|
|
312
321
|
* this prefix; readiness forwards it before initialize, then keeps the queue
|
|
313
|
-
* suffix gated until the initialization response.
|
|
322
|
+
* suffix gated until the initialization response. A RE-initialize (the era
|
|
323
|
+
* is already legacy) has no prefix at all: nothing queued may overtake it.
|
|
314
324
|
*/
|
|
315
325
|
private initializePredecessorCount;
|
|
316
326
|
private clientInitialized;
|
|
@@ -739,6 +749,30 @@ export declare class StdioSupervisor {
|
|
|
739
749
|
private adoptActiveChild;
|
|
740
750
|
private invalidateCurrentChild;
|
|
741
751
|
private detachCurrentChild;
|
|
752
|
+
/**
|
|
753
|
+
* Signals the process group of a worker that exited UNEXPECTEDLY, so the
|
|
754
|
+
* descendants it leaves behind (a Java grandchild above all) are not
|
|
755
|
+
* orphaned.
|
|
756
|
+
*
|
|
757
|
+
* PID reuse: this runs from the worker's `exit` event, after the worker
|
|
758
|
+
* itself was reaped, so its pid is free again. The kernel still refuses to
|
|
759
|
+
* hand that number out as a new pid or pgid while ANY member of the old
|
|
760
|
+
* group is alive — so while there is something left to reap, `-pid` names
|
|
761
|
+
* exactly that group. The residual race (the group already empty AND the
|
|
762
|
+
* number reused by a new group leader in between) is accepted as negligible.
|
|
763
|
+
* pidfd-based group signalling would close it but needs Linux 6.9+.
|
|
764
|
+
*
|
|
765
|
+
* Fire-and-forget by design: it is not recorded as an in-flight cleanup, so
|
|
766
|
+
* it never occupies a live-generation slot or delays the replacement.
|
|
767
|
+
*
|
|
768
|
+
* Windows: `taskkill /T` walks the tree from its root, and the root has
|
|
769
|
+
* already exited here, so there is nothing it can find. Descendants of a
|
|
770
|
+
* crashed worker are not reaped on Windows (known limitation).
|
|
771
|
+
*
|
|
772
|
+
* Logged `result`: "signalled" (the group had members and was signalled),
|
|
773
|
+
* "group-empty" (ESRCH — nothing was left), or "failed".
|
|
774
|
+
*/
|
|
775
|
+
private reapExitedWorkerGroup;
|
|
742
776
|
private recoverTimedOutWorker;
|
|
743
777
|
private beginTreeTermination;
|
|
744
778
|
private finishTreeTermination;
|
package/dist/stdio-supervisor.js
CHANGED
|
@@ -670,6 +670,15 @@ export class StdioSupervisor {
|
|
|
670
670
|
unresolvedTreeTokens = new Set();
|
|
671
671
|
restartBackoff = new RestartBackoffState();
|
|
672
672
|
terminalChildren = new WeakSet();
|
|
673
|
+
/**
|
|
674
|
+
* Workers started by the DEFAULT spawner on POSIX, which spawns them
|
|
675
|
+
* detached — so each one leads its own process group and `-pid` names
|
|
676
|
+
* exactly that group. Only these are group-signalled after an unexpected
|
|
677
|
+
* exit: a child handed in by an injected spawner carries no such guarantee,
|
|
678
|
+
* and its pid may not be a group id this supervisor owns.
|
|
679
|
+
*/
|
|
680
|
+
processGroupLeaders = new WeakSet();
|
|
681
|
+
spawnsProcessGroupLeaders;
|
|
673
682
|
/**
|
|
674
683
|
* Monotonic worker-generation counter (incremented per successful spawn).
|
|
675
684
|
* Recorded into finality tombstones so retention can be bounded per
|
|
@@ -776,7 +785,8 @@ export class StdioSupervisor {
|
|
|
776
785
|
* Number of queued requests that arrived before an initial initialize while
|
|
777
786
|
* the worker was unavailable. Only era-neutral modern discovers can occupy
|
|
778
787
|
* this prefix; readiness forwards it before initialize, then keeps the queue
|
|
779
|
-
* suffix gated until the initialization response.
|
|
788
|
+
* suffix gated until the initialization response. A RE-initialize (the era
|
|
789
|
+
* is already legacy) has no prefix at all: nothing queued may overtake it.
|
|
780
790
|
*/
|
|
781
791
|
initializePredecessorCount = 0;
|
|
782
792
|
clientInitialized = false;
|
|
@@ -818,6 +828,7 @@ export class StdioSupervisor {
|
|
|
818
828
|
this.monotonicNow = options.monotonicNow ?? (() => performance.now());
|
|
819
829
|
this.timerScheduler = options.timerScheduler ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
820
830
|
this.timerClearer = options.timerClearer ?? ((timer) => clearTimeout(timer));
|
|
831
|
+
this.spawnsProcessGroupLeaders = options.workerSpawner === undefined && process.platform !== "win32";
|
|
821
832
|
this.workerSpawner = options.workerSpawner ?? (() => spawn(process.execPath, [...process.execArgv, this.entryFile], {
|
|
822
833
|
env: {
|
|
823
834
|
...process.env,
|
|
@@ -1246,6 +1257,7 @@ export class StdioSupervisor {
|
|
|
1246
1257
|
this.writeToClient(buildInvalidInitializeRejection(message.id), this.modeForMessage(message));
|
|
1247
1258
|
return;
|
|
1248
1259
|
}
|
|
1260
|
+
const eraBeforeInitialize = this.era;
|
|
1249
1261
|
this.era = "legacy";
|
|
1250
1262
|
// The SDK's opening classifier treats an initialize that carries a
|
|
1251
1263
|
// valid modern era claim as MODERN, diverging from this admission rule
|
|
@@ -1270,7 +1282,13 @@ export class StdioSupervisor {
|
|
|
1270
1282
|
this.forwardRequest(message, this.createPendingRequest(message));
|
|
1271
1283
|
}
|
|
1272
1284
|
else {
|
|
1273
|
-
|
|
1285
|
+
// Only an INITIAL initialize owns a releasable prefix: while the era
|
|
1286
|
+
// was unselected the sole request that can have queued is an
|
|
1287
|
+
// era-neutral discover. Once legacy is locked ordinary requests queue
|
|
1288
|
+
// too, and letting them overtake a RE-sent initialize would hand them
|
|
1289
|
+
// to an un-handshaken replacement worker.
|
|
1290
|
+
this.initializePredecessorCount =
|
|
1291
|
+
eraBeforeInitialize === "unselected" ? this.queuedRequests.length : 0;
|
|
1274
1292
|
const existing = this.queuedNotifications.findIndex((entry) => isRequest(entry) && entry.method === "initialize");
|
|
1275
1293
|
if (existing >= 0)
|
|
1276
1294
|
this.queuedNotifications.splice(existing, 1);
|
|
@@ -2009,6 +2027,8 @@ export class StdioSupervisor {
|
|
|
2009
2027
|
}
|
|
2010
2028
|
this.child = child;
|
|
2011
2029
|
this.liveChildren.add(child);
|
|
2030
|
+
if (this.spawnsProcessGroupLeaders)
|
|
2031
|
+
this.processGroupLeaders.add(child);
|
|
2012
2032
|
this.workerReaders.set(child, new JsonRpcFrameReader());
|
|
2013
2033
|
const generation = ++this.workerGeneration;
|
|
2014
2034
|
// "close" fires only once the process has ended AND its stdio streams are
|
|
@@ -2407,6 +2427,10 @@ export class StdioSupervisor {
|
|
|
2407
2427
|
const wasReady = this.childReady;
|
|
2408
2428
|
const readyAt = this.childReadyAt;
|
|
2409
2429
|
this.detachCurrentChild();
|
|
2430
|
+
// The worker ended on its own (crash, OOM, an external kill of the worker
|
|
2431
|
+
// alone): no supervisor-initiated termination ran, so any descendant it
|
|
2432
|
+
// forked is still alive in its process group with no other owner.
|
|
2433
|
+
this.reapExitedWorkerGroup(child, childPid);
|
|
2410
2434
|
if (this.shuttingDown) {
|
|
2411
2435
|
return;
|
|
2412
2436
|
}
|
|
@@ -2925,6 +2949,57 @@ export class StdioSupervisor {
|
|
|
2925
2949
|
child.stdin.removeAllListeners("error");
|
|
2926
2950
|
}
|
|
2927
2951
|
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Signals the process group of a worker that exited UNEXPECTEDLY, so the
|
|
2954
|
+
* descendants it leaves behind (a Java grandchild above all) are not
|
|
2955
|
+
* orphaned.
|
|
2956
|
+
*
|
|
2957
|
+
* PID reuse: this runs from the worker's `exit` event, after the worker
|
|
2958
|
+
* itself was reaped, so its pid is free again. The kernel still refuses to
|
|
2959
|
+
* hand that number out as a new pid or pgid while ANY member of the old
|
|
2960
|
+
* group is alive — so while there is something left to reap, `-pid` names
|
|
2961
|
+
* exactly that group. The residual race (the group already empty AND the
|
|
2962
|
+
* number reused by a new group leader in between) is accepted as negligible.
|
|
2963
|
+
* pidfd-based group signalling would close it but needs Linux 6.9+.
|
|
2964
|
+
*
|
|
2965
|
+
* Fire-and-forget by design: it is not recorded as an in-flight cleanup, so
|
|
2966
|
+
* it never occupies a live-generation slot or delays the replacement.
|
|
2967
|
+
*
|
|
2968
|
+
* Windows: `taskkill /T` walks the tree from its root, and the root has
|
|
2969
|
+
* already exited here, so there is nothing it can find. Descendants of a
|
|
2970
|
+
* crashed worker are not reaped on Windows (known limitation).
|
|
2971
|
+
*
|
|
2972
|
+
* Logged `result`: "signalled" (the group had members and was signalled),
|
|
2973
|
+
* "group-empty" (ESRCH — nothing was left), or "failed".
|
|
2974
|
+
*/
|
|
2975
|
+
reapExitedWorkerGroup(child, pid) {
|
|
2976
|
+
if (pid === undefined)
|
|
2977
|
+
return;
|
|
2978
|
+
const report = (result) => {
|
|
2979
|
+
this.eventWriter(result === "failed" ? "warn" : "info", "supervisor.worker_exit_group_cleanup", {
|
|
2980
|
+
pid,
|
|
2981
|
+
result
|
|
2982
|
+
});
|
|
2983
|
+
};
|
|
2984
|
+
if (process.platform === "win32") {
|
|
2985
|
+
debugSupervisor("worker_exit_group_unreachable", { pid });
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
if (!this.processGroupLeaders.has(child))
|
|
2989
|
+
return;
|
|
2990
|
+
let groupEmpty = false;
|
|
2991
|
+
const signalled = terminatePosixProcessGroup(pid, (target, signal) => {
|
|
2992
|
+
try {
|
|
2993
|
+
process.kill(target, signal);
|
|
2994
|
+
}
|
|
2995
|
+
catch (error) {
|
|
2996
|
+
if (error?.code === "ESRCH")
|
|
2997
|
+
groupEmpty = true;
|
|
2998
|
+
throw error;
|
|
2999
|
+
}
|
|
3000
|
+
});
|
|
3001
|
+
report(!signalled ? "failed" : groupEmpty ? "group-empty" : "signalled");
|
|
3002
|
+
}
|
|
2928
3003
|
recoverTimedOutWorker() {
|
|
2929
3004
|
const child = this.child;
|
|
2930
3005
|
if (!child) {
|
package/dist/storage/db.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import Database from "./sqlite.js";
|
|
1
|
+
import Database, { isRawSqliteCorruptionError } from "./sqlite.js";
|
|
2
2
|
import type { Config } from "../types.js";
|
|
3
|
+
export { isRawSqliteCorruptionError };
|
|
3
4
|
type SqliteDatabase = InstanceType<typeof Database>;
|
|
4
5
|
type Logger = {
|
|
5
6
|
warn: (message: string, details?: Record<string, unknown>) => void;
|
|
@@ -11,5 +12,64 @@ export interface InitializedDatabase {
|
|
|
11
12
|
schemaVersion: number;
|
|
12
13
|
}
|
|
13
14
|
type DatabaseConfig = Pick<Config, "sqlitePath"> & Partial<Pick<Config, "sqliteCacheKb" | "sqliteMmapSize">>;
|
|
15
|
+
/**
|
|
16
|
+
* Best-effort request that the NEXT `openDatabase` for `sqlitePath` run the
|
|
17
|
+
* full `PRAGMA integrity_check` instead of the default `quick_check`. Creates
|
|
18
|
+
* a new, uniquely-named request file (see the block comment above) with
|
|
19
|
+
* exclusive-create so it can never collide with or overwrite another
|
|
20
|
+
* pending request. Never throws: a failure to write the file only means the
|
|
21
|
+
* escalation is missed, which is logged rather than allowed to mask the
|
|
22
|
+
* caller's real error. Returns whether the file was actually written, so a
|
|
23
|
+
* caller that cannot schedule the follow-up check can say so instead of
|
|
24
|
+
* promising one.
|
|
25
|
+
*/
|
|
26
|
+
export declare function requestFullIntegrityCheck(sqlitePath: string, logger?: Logger): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Lists the full paths of every currently pending full-integrity-check
|
|
29
|
+
* request file for `sqlitePath`. Best-effort: a missing or unreadable
|
|
30
|
+
* directory is treated as "no requests pending" rather than as an error,
|
|
31
|
+
* since the fallback (a quick_check) is always safe to run.
|
|
32
|
+
*/
|
|
33
|
+
export declare function listIntegrityCheckRequestFiles(sqlitePath: string): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Deletes EXACTLY the given request file paths - the ones an earlier
|
|
36
|
+
* `listIntegrityCheckRequestFiles` call returned, never a fresh listing - so
|
|
37
|
+
* a request file created after that decision point is never deleted by a
|
|
38
|
+
* clear that only ever meant to honor the requests it actually acted on.
|
|
39
|
+
* Best-effort per file: ENOENT (already gone) is ignored, anything else is
|
|
40
|
+
* logged, not thrown.
|
|
41
|
+
*
|
|
42
|
+
* Exported so a unit test can exercise the race guard directly:
|
|
43
|
+
* `openDatabase` lists-then-clears synchronously with no `await` in between,
|
|
44
|
+
* so there is no way to interleave a concurrent request file appearing
|
|
45
|
+
* between "decide" and "clear" from outside that call.
|
|
46
|
+
*/
|
|
47
|
+
export declare function clearIntegrityCheckRequestFiles(requestFiles: string[], logger?: Logger): void;
|
|
48
|
+
/**
|
|
49
|
+
* Wires a Database instance's corruption observer (see sqlite.ts) so that a
|
|
50
|
+
* raw corruption error thrown by ANY later statement on this handle - no
|
|
51
|
+
* matter what catches and wraps it further up the call stack, including a
|
|
52
|
+
* tool-specific error wrapper that never reaches runTool's own catch block -
|
|
53
|
+
* still schedules the next open's full integrity_check. Exported so a unit
|
|
54
|
+
* test can attach the same wiring to a Database instance opened directly
|
|
55
|
+
* (bypassing openDatabase's own open-time check, which would otherwise catch
|
|
56
|
+
* a test's injected corruption before a "successfully opened" handle ever
|
|
57
|
+
* existed to query against).
|
|
58
|
+
*/
|
|
59
|
+
export declare function attachRuntimeCorruptionObserver(db: SqliteDatabase, sqlitePath: string, logger?: Logger): void;
|
|
60
|
+
/**
|
|
61
|
+
* runTool catch-path helper: when `caughtError` is a raw SQLite corruption
|
|
62
|
+
* error observed while serving a tool call (not at open time - open-time
|
|
63
|
+
* corruption is already an AppError by the time it reaches here), this
|
|
64
|
+
* requests a full integrity check on the next open and returns a typed
|
|
65
|
+
* ERR_DB_FAILURE the caller should report instead of the raw error. Any other
|
|
66
|
+
* error is returned unchanged so non-corruption failures are unaffected.
|
|
67
|
+
*
|
|
68
|
+
* The marker request here is redundant with the Database corruption observer
|
|
69
|
+
* (attachRuntimeCorruptionObserver) for an UNWRAPPED raw error, since that
|
|
70
|
+
* observer already fired deeper in the call stack; it is kept because
|
|
71
|
+
* `convertRuntimeSqliteCorruption` is the only place with reliable access to
|
|
72
|
+
* whether scheduling succeeded, which decides which `nextAction` to publish.
|
|
73
|
+
*/
|
|
74
|
+
export declare function convertRuntimeSqliteCorruption(error: unknown, sqlitePath: string, logger?: Logger): unknown;
|
|
14
75
|
export declare function openDatabase(config: DatabaseConfig, logger?: Logger): InitializedDatabase;
|
|
15
|
-
export {};
|
package/dist/storage/db.js
CHANGED
|
@@ -1,22 +1,131 @@
|
|
|
1
|
-
import { existsSync, renameSync } from "node:fs";
|
|
2
|
-
import { dirname } from "node:path";
|
|
1
|
+
import { existsSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import { mkdirSync } from "node:fs";
|
|
4
|
-
import
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import Database, { isRawSqliteCorruptionError } from "./sqlite.js";
|
|
5
6
|
import { runMigrations } from "./migrations.js";
|
|
6
7
|
import { createError, ERROR_CODES, isAppError } from "../errors.js";
|
|
7
8
|
import { log } from "../logger.js";
|
|
9
|
+
// Re-exported for callers that already import the predicate from this module
|
|
10
|
+
// (it now lives in ./sqlite.js so the Database wrapper can call it directly
|
|
11
|
+
// without a module cycle back to this file).
|
|
12
|
+
export { isRawSqliteCorruptionError };
|
|
8
13
|
const DEFAULT_SQLITE_CACHE_KB = 8_000;
|
|
9
14
|
const DEFAULT_SQLITE_MMAP_SIZE = 268_435_456;
|
|
15
|
+
// Per-request escalation files created by the runtime-corruption recovery
|
|
16
|
+
// path (see attachRuntimeCorruptionObserver / convertRuntimeSqliteCorruption
|
|
17
|
+
// below): a SQLite error surfaced while serving a tool call - not at open
|
|
18
|
+
// time - means quick_check already passed for this file, so the NEXT open
|
|
19
|
+
// must pay for the full integrity_check instead of trusting quick_check
|
|
20
|
+
// again.
|
|
21
|
+
//
|
|
22
|
+
// Each request is its OWN immutable, uniquely-named file (exclusive-create,
|
|
23
|
+
// never overwritten), rather than one shared marker whose content gets
|
|
24
|
+
// replaced. That is what makes two requests race-free even across processes
|
|
25
|
+
// sharing one cache directory: an open lists whatever request files exist at
|
|
26
|
+
// its OWN decision point and remembers those exact names; after honoring
|
|
27
|
+
// them, it deletes only those names. A request file created by someone else
|
|
28
|
+
// AFTER that decision (a concurrent process, or a second corruption event)
|
|
29
|
+
// is simply a name that open never saw, so it can never be swept up by a
|
|
30
|
+
// clear meant for a different, earlier decision - it waits for the NEXT
|
|
31
|
+
// open. (An earlier single shared marker file, `<sqlitePath>.integrity-check-
|
|
32
|
+
// requested`, was never released, so there is no old name to keep reading
|
|
33
|
+
// for compatibility; it is dropped outright rather than special-cased.)
|
|
34
|
+
const INTEGRITY_CHECK_REQUEST_INFIX = ".integrity-check-requested.";
|
|
10
35
|
function ensureParentDirectory(path) {
|
|
11
36
|
mkdirSync(dirname(path), { recursive: true });
|
|
12
37
|
}
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
38
|
+
function integrityCheckRequestFileName(sqlitePath, token) {
|
|
39
|
+
return `${basename(sqlitePath)}${INTEGRITY_CHECK_REQUEST_INFIX}${token}`;
|
|
40
|
+
}
|
|
41
|
+
function integrityCheckRequestFileNamePrefix(sqlitePath) {
|
|
42
|
+
return `${basename(sqlitePath)}${INTEGRITY_CHECK_REQUEST_INFIX}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Best-effort request that the NEXT `openDatabase` for `sqlitePath` run the
|
|
46
|
+
* full `PRAGMA integrity_check` instead of the default `quick_check`. Creates
|
|
47
|
+
* a new, uniquely-named request file (see the block comment above) with
|
|
48
|
+
* exclusive-create so it can never collide with or overwrite another
|
|
49
|
+
* pending request. Never throws: a failure to write the file only means the
|
|
50
|
+
* escalation is missed, which is logged rather than allowed to mask the
|
|
51
|
+
* caller's real error. Returns whether the file was actually written, so a
|
|
52
|
+
* caller that cannot schedule the follow-up check can say so instead of
|
|
53
|
+
* promising one.
|
|
54
|
+
*/
|
|
55
|
+
export function requestFullIntegrityCheck(sqlitePath, logger = buildDefaultLogger()) {
|
|
56
|
+
const token = `${Date.now()}-${process.pid}-${randomUUID()}`;
|
|
57
|
+
const requestPath = join(dirname(sqlitePath), integrityCheckRequestFileName(sqlitePath, token));
|
|
58
|
+
try {
|
|
59
|
+
ensureParentDirectory(sqlitePath);
|
|
60
|
+
writeFileSync(requestPath, "", { flag: "wx" });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch (writeError) {
|
|
64
|
+
logger.warn("Failed to request a full SQLite integrity check", {
|
|
65
|
+
sqlitePath,
|
|
66
|
+
reason: writeError instanceof Error ? writeError.message : String(writeError)
|
|
67
|
+
});
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Lists the full paths of every currently pending full-integrity-check
|
|
73
|
+
* request file for `sqlitePath`. Best-effort: a missing or unreadable
|
|
74
|
+
* directory is treated as "no requests pending" rather than as an error,
|
|
75
|
+
* since the fallback (a quick_check) is always safe to run.
|
|
76
|
+
*/
|
|
77
|
+
export function listIntegrityCheckRequestFiles(sqlitePath) {
|
|
78
|
+
const dir = dirname(sqlitePath);
|
|
79
|
+
const prefix = integrityCheckRequestFileNamePrefix(sqlitePath);
|
|
80
|
+
try {
|
|
81
|
+
return readdirSync(dir)
|
|
82
|
+
.filter((name) => name.startsWith(prefix))
|
|
83
|
+
.map((name) => join(dir, name));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Deletes EXACTLY the given request file paths - the ones an earlier
|
|
91
|
+
* `listIntegrityCheckRequestFiles` call returned, never a fresh listing - so
|
|
92
|
+
* a request file created after that decision point is never deleted by a
|
|
93
|
+
* clear that only ever meant to honor the requests it actually acted on.
|
|
94
|
+
* Best-effort per file: ENOENT (already gone) is ignored, anything else is
|
|
95
|
+
* logged, not thrown.
|
|
96
|
+
*
|
|
97
|
+
* Exported so a unit test can exercise the race guard directly:
|
|
98
|
+
* `openDatabase` lists-then-clears synchronously with no `await` in between,
|
|
99
|
+
* so there is no way to interleave a concurrent request file appearing
|
|
100
|
+
* between "decide" and "clear" from outside that call.
|
|
101
|
+
*/
|
|
102
|
+
export function clearIntegrityCheckRequestFiles(requestFiles, logger = buildDefaultLogger()) {
|
|
103
|
+
for (const requestFile of requestFiles) {
|
|
104
|
+
try {
|
|
105
|
+
unlinkSync(requestFile);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error?.code === "ENOENT") {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
logger.warn("Failed to clear a SQLite full-integrity-check request file", {
|
|
112
|
+
requestFile,
|
|
113
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function runIntegrityCheck(db, logger, mode = "quick") {
|
|
119
|
+
const resultKey = mode === "full" ? "integrity_check" : "quick_check";
|
|
120
|
+
const startedAt = Date.now();
|
|
121
|
+
const result = db.prepare(`PRAGMA ${resultKey}`).get();
|
|
122
|
+
const durationMs = Date.now() - startedAt;
|
|
123
|
+
logger.info("SQLite consistency check completed", { durationMs, check: resultKey });
|
|
124
|
+
if (!result || result[resultKey] !== "ok") {
|
|
16
125
|
throw createError({
|
|
17
126
|
code: ERROR_CODES.DB_FAILURE,
|
|
18
127
|
message: "SQLite integrity check failed.",
|
|
19
|
-
details: { reason: "integrity_check_failed",
|
|
128
|
+
details: { reason: "integrity_check_failed", check: resultKey, checkResult: result }
|
|
20
129
|
});
|
|
21
130
|
}
|
|
22
131
|
}
|
|
@@ -60,21 +169,59 @@ function isSchemaVersionMismatchError(error) {
|
|
|
60
169
|
return (error.details?.reason === "schema_version_unsupported" ||
|
|
61
170
|
error.details?.reason === "schema_version_invalid");
|
|
62
171
|
}
|
|
63
|
-
const SQLITE_CORRUPT_ERRCODE = 11;
|
|
64
|
-
const SQLITE_NOTADB_ERRCODE = 26;
|
|
65
172
|
function isCorruptionError(error) {
|
|
66
173
|
if (isAppError(error)) {
|
|
67
174
|
return (error.code === ERROR_CODES.DB_FAILURE && error.details?.reason === "integrity_check_failed");
|
|
68
175
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
176
|
+
return isRawSqliteCorruptionError(error);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Wires a Database instance's corruption observer (see sqlite.ts) so that a
|
|
180
|
+
* raw corruption error thrown by ANY later statement on this handle - no
|
|
181
|
+
* matter what catches and wraps it further up the call stack, including a
|
|
182
|
+
* tool-specific error wrapper that never reaches runTool's own catch block -
|
|
183
|
+
* still schedules the next open's full integrity_check. Exported so a unit
|
|
184
|
+
* test can attach the same wiring to a Database instance opened directly
|
|
185
|
+
* (bypassing openDatabase's own open-time check, which would otherwise catch
|
|
186
|
+
* a test's injected corruption before a "successfully opened" handle ever
|
|
187
|
+
* existed to query against).
|
|
188
|
+
*/
|
|
189
|
+
export function attachRuntimeCorruptionObserver(db, sqlitePath, logger = buildDefaultLogger()) {
|
|
190
|
+
db.setCorruptionObserver(() => {
|
|
191
|
+
requestFullIntegrityCheck(sqlitePath, logger);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* runTool catch-path helper: when `caughtError` is a raw SQLite corruption
|
|
196
|
+
* error observed while serving a tool call (not at open time - open-time
|
|
197
|
+
* corruption is already an AppError by the time it reaches here), this
|
|
198
|
+
* requests a full integrity check on the next open and returns a typed
|
|
199
|
+
* ERR_DB_FAILURE the caller should report instead of the raw error. Any other
|
|
200
|
+
* error is returned unchanged so non-corruption failures are unaffected.
|
|
201
|
+
*
|
|
202
|
+
* The marker request here is redundant with the Database corruption observer
|
|
203
|
+
* (attachRuntimeCorruptionObserver) for an UNWRAPPED raw error, since that
|
|
204
|
+
* observer already fired deeper in the call stack; it is kept because
|
|
205
|
+
* `convertRuntimeSqliteCorruption` is the only place with reliable access to
|
|
206
|
+
* whether scheduling succeeded, which decides which `nextAction` to publish.
|
|
207
|
+
*/
|
|
208
|
+
export function convertRuntimeSqliteCorruption(error, sqlitePath, logger = buildDefaultLogger()) {
|
|
209
|
+
if (!isRawSqliteCorruptionError(error)) {
|
|
210
|
+
return error;
|
|
75
211
|
}
|
|
76
|
-
const
|
|
77
|
-
|
|
212
|
+
const scheduled = requestFullIntegrityCheck(sqlitePath, logger);
|
|
213
|
+
const nextAction = scheduled
|
|
214
|
+
? "Restart the MCP server so the cache database is fully checked and rebuilt if necessary, then retry the request."
|
|
215
|
+
: `The automatic full check could not be scheduled. Stop every MCP server process that uses this cache, delete the cache database at ${sqlitePath} together with its -wal and -shm files, then restart; the cache is rebuilt on the next start.`;
|
|
216
|
+
return createError({
|
|
217
|
+
code: ERROR_CODES.DB_FAILURE,
|
|
218
|
+
message: "A SQLite consistency error occurred while serving this request.",
|
|
219
|
+
details: {
|
|
220
|
+
sqlitePath,
|
|
221
|
+
reason: "runtime_corruption",
|
|
222
|
+
nextAction
|
|
223
|
+
}
|
|
224
|
+
});
|
|
78
225
|
}
|
|
79
226
|
function buildDefaultLogger() {
|
|
80
227
|
return {
|
|
@@ -100,12 +247,21 @@ function buildDefaultLogger() {
|
|
|
100
247
|
}
|
|
101
248
|
export function openDatabase(config, logger = buildDefaultLogger()) {
|
|
102
249
|
let db;
|
|
250
|
+
const pendingRequestFiles = listIntegrityCheckRequestFiles(config.sqlitePath);
|
|
103
251
|
try {
|
|
104
252
|
ensureParentDirectory(config.sqlitePath);
|
|
105
253
|
db = new Database(config.sqlitePath);
|
|
106
254
|
applyPragmas(db, config);
|
|
107
255
|
const schemaVersion = runMigrations(db);
|
|
108
|
-
runIntegrityCheck(db);
|
|
256
|
+
runIntegrityCheck(db, logger, pendingRequestFiles.length > 0 ? "full" : "quick");
|
|
257
|
+
if (pendingRequestFiles.length > 0) {
|
|
258
|
+
clearIntegrityCheckRequestFiles(pendingRequestFiles, logger);
|
|
259
|
+
}
|
|
260
|
+
// Only attach the runtime-corruption observer once the open itself
|
|
261
|
+
// (migrations + consistency check) has fully succeeded: open-time
|
|
262
|
+
// failures must keep going through the backup/rebuild path below, not
|
|
263
|
+
// silently re-request a check that is about to happen anyway.
|
|
264
|
+
attachRuntimeCorruptionObserver(db, config.sqlitePath, logger);
|
|
109
265
|
return { db, schemaVersion };
|
|
110
266
|
}
|
|
111
267
|
catch (caughtError) {
|
|
@@ -152,12 +308,17 @@ export function openDatabase(config, logger = buildDefaultLogger()) {
|
|
|
152
308
|
const backupPath = backupCorruptedDb(config.sqlitePath);
|
|
153
309
|
logger.warn("SQLite database integrity check failed. Recreated database after backup", {
|
|
154
310
|
sqlitePath: config.sqlitePath,
|
|
155
|
-
backupPath
|
|
311
|
+
backupPath,
|
|
312
|
+
reason: errorMessage
|
|
156
313
|
});
|
|
157
314
|
rebuilt = new Database(config.sqlitePath);
|
|
158
315
|
applyPragmas(rebuilt, config);
|
|
159
316
|
const schemaVersion = runMigrations(rebuilt);
|
|
160
|
-
runIntegrityCheck(rebuilt);
|
|
317
|
+
runIntegrityCheck(rebuilt, logger);
|
|
318
|
+
if (pendingRequestFiles.length > 0) {
|
|
319
|
+
clearIntegrityCheckRequestFiles(pendingRequestFiles, logger);
|
|
320
|
+
}
|
|
321
|
+
attachRuntimeCorruptionObserver(rebuilt, config.sqlitePath, logger);
|
|
161
322
|
return { db: rebuilt, schemaVersion };
|
|
162
323
|
}
|
|
163
324
|
catch (rebuildError) {
|
|
@@ -19,6 +19,13 @@ export interface SearchFilesOptions {
|
|
|
19
19
|
cursor?: string;
|
|
20
20
|
mode?: "mixed" | "text" | "path";
|
|
21
21
|
fetchLimitOverride?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Literal file_path prefix (e.g. `net/minecraft/`) that narrows the candidate
|
|
24
|
+
* queries through an escaped, ASCII case-insensitive `LIKE prefix%`. That match
|
|
25
|
+
* set is a superset of a case-sensitive startsWith, so callers keep their own
|
|
26
|
+
* exact scope check on the returned candidates.
|
|
27
|
+
*/
|
|
28
|
+
pathPrefix?: string;
|
|
22
29
|
}
|
|
23
30
|
export interface SearchFilesResult {
|
|
24
31
|
filePath: string;
|