@adhisang/minecraft-modding-mcp 3.2.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +52 -32
- package/dist/build-suggested-call.d.ts +29 -0
- package/dist/build-suggested-call.js +58 -0
- package/dist/cache-registry.d.ts +3 -1
- package/dist/cache-registry.js +59 -7
- package/dist/config.d.ts +10 -1
- package/dist/config.js +52 -1
- package/dist/entry-tools/analyze-symbol-service.d.ts +18 -18
- package/dist/entry-tools/analyze-symbol-service.js +13 -2
- package/dist/entry-tools/batch-class-members-service.d.ts +34 -0
- package/dist/entry-tools/batch-class-members-service.js +97 -0
- package/dist/entry-tools/batch-class-source-service.d.ts +37 -0
- package/dist/entry-tools/batch-class-source-service.js +100 -0
- package/dist/entry-tools/batch-mappings-service.d.ts +36 -0
- package/dist/entry-tools/batch-mappings-service.js +66 -0
- package/dist/entry-tools/batch-runner.d.ts +72 -0
- package/dist/entry-tools/batch-runner.js +90 -0
- package/dist/entry-tools/batch-symbol-exists-service.d.ts +46 -0
- package/dist/entry-tools/batch-symbol-exists-service.js +113 -0
- package/dist/entry-tools/compare-minecraft-service.d.ts +6 -6
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +83 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.d.ts +6 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +80 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +248 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +60 -0
- package/dist/entry-tools/inspect-minecraft/handlers/file.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +54 -0
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +100 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.js +155 -0
- package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +6 -0
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +49 -0
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +1042 -0
- package/dist/entry-tools/inspect-minecraft/internal.js +448 -0
- package/dist/entry-tools/inspect-minecraft-service.d.ts +213 -328
- package/dist/entry-tools/inspect-minecraft-service.js +20 -1238
- package/dist/entry-tools/manage-cache-service.d.ts +16 -16
- package/dist/entry-tools/validate-project/cases/access-transformer.d.ts +6 -0
- package/dist/entry-tools/validate-project/cases/access-transformer.js +106 -0
- package/dist/entry-tools/validate-project/cases/access-widener.d.ts +6 -0
- package/dist/entry-tools/validate-project/cases/access-widener.js +86 -0
- package/dist/entry-tools/validate-project/cases/mixin.d.ts +6 -0
- package/dist/entry-tools/validate-project/cases/mixin.js +90 -0
- package/dist/entry-tools/validate-project/cases/project-summary.d.ts +97 -0
- package/dist/entry-tools/validate-project/cases/project-summary.js +346 -0
- package/dist/entry-tools/validate-project/internal.d.ts +135 -0
- package/dist/entry-tools/validate-project/internal.js +287 -0
- package/dist/entry-tools/validate-project-service.d.ts +63 -47
- package/dist/entry-tools/validate-project-service.js +12 -482
- package/dist/entry-tools/verify-mixin-target-service.d.ts +133 -0
- package/dist/entry-tools/verify-mixin-target-service.js +323 -0
- package/dist/error-mapping.d.ts +40 -0
- package/dist/error-mapping.js +139 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +170 -1314
- package/dist/lru-list.d.ts +31 -0
- package/dist/lru-list.js +102 -0
- package/dist/mapping/internal-types.d.ts +54 -0
- package/dist/mapping/internal-types.js +14 -0
- package/dist/mapping/loaders/mojang.d.ts +2 -0
- package/dist/mapping/loaders/mojang.js +64 -0
- package/dist/mapping/loaders/tiny-loom.d.ts +2 -0
- package/dist/mapping/loaders/tiny-loom.js +73 -0
- package/dist/mapping/loaders/tiny-maven.d.ts +2 -0
- package/dist/mapping/loaders/tiny-maven.js +104 -0
- package/dist/mapping/loaders/types.d.ts +14 -0
- package/dist/mapping/loaders/types.js +2 -0
- package/dist/mapping/lookup.d.ts +52 -0
- package/dist/mapping/lookup.js +496 -0
- package/dist/mapping/parsers/normalize.d.ts +10 -0
- package/dist/mapping/parsers/normalize.js +52 -0
- package/dist/mapping/parsers/proguard.d.ts +20 -0
- package/dist/mapping/parsers/proguard.js +138 -0
- package/dist/mapping/parsers/symbol-records.d.ts +27 -0
- package/dist/mapping/parsers/symbol-records.js +216 -0
- package/dist/mapping/parsers/tiny.d.ts +9 -0
- package/dist/mapping/parsers/tiny.js +96 -0
- package/dist/mapping/types.d.ts +147 -0
- package/dist/mapping/types.js +2 -0
- package/dist/mapping-pipeline-service.d.ts +10 -1
- package/dist/mapping-pipeline-service.js +16 -3
- package/dist/mapping-service.d.ts +15 -144
- package/dist/mapping-service.js +179 -1119
- package/dist/mixin/access-validators.d.ts +9 -0
- package/dist/mixin/access-validators.js +257 -0
- package/dist/mixin/annotation-validators.d.ts +5 -0
- package/dist/mixin/annotation-validators.js +162 -0
- package/dist/mixin/helpers.d.ts +28 -0
- package/dist/mixin/helpers.js +315 -0
- package/dist/mixin/parsed-validator.d.ts +8 -0
- package/dist/mixin/parsed-validator.js +337 -0
- package/dist/mixin/types.d.ts +208 -0
- package/dist/mixin/types.js +28 -0
- package/dist/mixin-validator.d.ts +9 -201
- package/dist/mixin-validator.js +8 -1005
- package/dist/observability.d.ts +18 -1
- package/dist/observability.js +44 -1
- package/dist/response-utils.d.ts +44 -10
- package/dist/response-utils.js +131 -17
- package/dist/source/access-validate.d.ts +4 -0
- package/dist/source/access-validate.js +254 -0
- package/dist/source/artifact-resolver.d.ts +110 -0
- package/dist/source/artifact-resolver.js +1174 -0
- package/dist/source/cache-metrics.d.ts +26 -0
- package/dist/source/cache-metrics.js +172 -0
- package/dist/source/class-source/members-builder.d.ts +34 -0
- package/dist/source/class-source/members-builder.js +46 -0
- package/dist/source/class-source/snippet-builder.d.ts +19 -0
- package/dist/source/class-source/snippet-builder.js +46 -0
- package/dist/source/class-source-helpers.d.ts +34 -0
- package/dist/source/class-source-helpers.js +140 -0
- package/dist/source/class-source.d.ts +42 -0
- package/dist/source/class-source.js +883 -0
- package/dist/source/descriptor-utils.d.ts +6 -0
- package/dist/source/descriptor-utils.js +37 -0
- package/dist/source/file-access.d.ts +4 -0
- package/dist/source/file-access.js +102 -0
- package/dist/source/indexer.d.ts +82 -0
- package/dist/source/indexer.js +505 -0
- package/dist/source/lifecycle/diff-utils.d.ts +9 -0
- package/dist/source/lifecycle/diff-utils.js +107 -0
- package/dist/source/lifecycle/diff.d.ts +2 -0
- package/dist/source/lifecycle/diff.js +265 -0
- package/dist/source/lifecycle/mapping-helpers.d.ts +22 -0
- package/dist/source/lifecycle/mapping-helpers.js +327 -0
- package/dist/source/lifecycle/runtime-check.d.ts +2 -0
- package/dist/source/lifecycle/runtime-check.js +142 -0
- package/dist/source/lifecycle/trace.d.ts +2 -0
- package/dist/source/lifecycle/trace.js +231 -0
- package/dist/source/lifecycle.d.ts +4 -0
- package/dist/source/lifecycle.js +5 -0
- package/dist/source/search.d.ts +51 -0
- package/dist/source/search.js +676 -0
- package/dist/source/shared-utils.d.ts +6 -0
- package/dist/source/shared-utils.js +55 -0
- package/dist/source/state.d.ts +21 -0
- package/dist/source/state.js +19 -0
- package/dist/source/symbol-resolver.d.ts +3 -0
- package/dist/source/symbol-resolver.js +212 -0
- package/dist/source/validate-mixin/pipeline/mapping-health.d.ts +3 -0
- package/dist/source/validate-mixin/pipeline/mapping-health.js +41 -0
- package/dist/source/validate-mixin/pipeline/parse.d.ts +2 -0
- package/dist/source/validate-mixin/pipeline/parse.js +10 -0
- package/dist/source/validate-mixin/pipeline/resolve.d.ts +3 -0
- package/dist/source/validate-mixin/pipeline/resolve.js +78 -0
- package/dist/source/validate-mixin/pipeline/target-lookup.d.ts +6 -0
- package/dist/source/validate-mixin/pipeline/target-lookup.js +260 -0
- package/dist/source/validate-mixin/pipeline-context.d.ts +72 -0
- package/dist/source/validate-mixin/pipeline-context.js +93 -0
- package/dist/source/validate-mixin.d.ts +22 -0
- package/dist/source/validate-mixin.js +799 -0
- package/dist/source/workspace-target.d.ts +18 -0
- package/dist/source/workspace-target.js +305 -0
- package/dist/source-resolver.d.ts +9 -1
- package/dist/source-resolver.js +14 -6
- package/dist/source-service.d.ts +178 -105
- package/dist/source-service.js +72 -5312
- package/dist/stage-emitter.d.ts +13 -0
- package/dist/stage-emitter.js +30 -0
- package/dist/stdio-supervisor.d.ts +61 -0
- package/dist/stdio-supervisor.js +326 -9
- package/dist/storage/artifacts-repo.d.ts +4 -1
- package/dist/storage/artifacts-repo.js +33 -5
- package/dist/storage/files-repo.d.ts +0 -2
- package/dist/storage/files-repo.js +0 -11
- package/dist/storage/migrations.d.ts +1 -1
- package/dist/storage/migrations.js +10 -2
- package/dist/storage/schema.d.ts +2 -0
- package/dist/storage/schema.js +25 -0
- package/dist/tool-contract-manifest.d.ts +1 -1
- package/dist/tool-contract-manifest.js +23 -6
- package/dist/tool-guidance.d.ts +82 -0
- package/dist/tool-guidance.js +734 -0
- package/dist/tool-schema-registry.d.ts +16 -0
- package/dist/tool-schema-registry.js +37 -0
- package/dist/tool-schemas.d.ts +3518 -0
- package/dist/tool-schemas.js +813 -0
- package/dist/types.d.ts +39 -0
- package/dist/version-service.js +7 -6
- package/dist/workspace-context-cache.d.ts +32 -0
- package/dist/workspace-context-cache.js +66 -0
- package/dist/workspace-mapping-service.d.ts +16 -0
- package/dist/workspace-mapping-service.js +173 -1
- package/docs/README-ja.md +414 -0
- package/docs/examples.md +483 -0
- package/docs/tool-reference.md +459 -0
- package/package.json +5 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type StageEmitter = (stage: string, meta?: Record<string, unknown>) => Promise<void>;
|
|
2
|
+
export declare const NOOP_STAGE_EMITTER: StageEmitter;
|
|
3
|
+
export type StageEmitterExtra = {
|
|
4
|
+
requestId?: string | number;
|
|
5
|
+
sendNotification?: (notification: {
|
|
6
|
+
method: string;
|
|
7
|
+
params?: unknown;
|
|
8
|
+
}) => Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
export type StageEmitterOptions = {
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare function makeStageEmitter(extra: StageEmitterExtra | undefined, options?: StageEmitterOptions): StageEmitter;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
const STAGE_PROGRESS_DISABLED = process.env.MIXIN_STAGE_PROGRESS_OFF === "1";
|
|
3
|
+
export const NOOP_STAGE_EMITTER = async () => {
|
|
4
|
+
/* noop */
|
|
5
|
+
};
|
|
6
|
+
export function makeStageEmitter(extra, options = {}) {
|
|
7
|
+
if (options.disabled ?? STAGE_PROGRESS_DISABLED) {
|
|
8
|
+
return NOOP_STAGE_EMITTER;
|
|
9
|
+
}
|
|
10
|
+
if (!extra) {
|
|
11
|
+
return NOOP_STAGE_EMITTER;
|
|
12
|
+
}
|
|
13
|
+
const requestId = extra.requestId;
|
|
14
|
+
const sendNotification = extra.sendNotification;
|
|
15
|
+
if (requestId === undefined || typeof sendNotification !== "function") {
|
|
16
|
+
return NOOP_STAGE_EMITTER;
|
|
17
|
+
}
|
|
18
|
+
return async (stage, meta) => {
|
|
19
|
+
await sendNotification({
|
|
20
|
+
method: "$/stageUpdate",
|
|
21
|
+
params: {
|
|
22
|
+
stage,
|
|
23
|
+
meta: meta ?? null,
|
|
24
|
+
t: performance.now(),
|
|
25
|
+
requestId
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=stage-emitter.js.map
|
|
@@ -1,12 +1,72 @@
|
|
|
1
|
+
import type { JSONRPCResponse } from "@modelcontextprotocol/sdk/types.js";
|
|
1
2
|
type SupervisorOptions = {
|
|
2
3
|
entryFile: string;
|
|
3
4
|
};
|
|
5
|
+
type RequestId = string | number;
|
|
6
|
+
export type ExitInfo = {
|
|
7
|
+
code: number | null;
|
|
8
|
+
signal: NodeJS.Signals | null;
|
|
9
|
+
};
|
|
10
|
+
export type RetryRecommendation = "narrow-query" | "clear-cache" | "report-bug" | "same-request";
|
|
11
|
+
export type RestartContext = {
|
|
12
|
+
toolName: string | undefined;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
lastStage: string | undefined;
|
|
15
|
+
lastStageElapsedMs: number | undefined;
|
|
16
|
+
lastStageMeta: unknown;
|
|
17
|
+
exit: ExitInfo;
|
|
18
|
+
toolArgsRedacted: Record<string, unknown> | undefined;
|
|
19
|
+
/** `true` when redaction replaced any value with a sentinel placeholder; the synthetic envelope omits `suggestedCall` in that case. */
|
|
20
|
+
toolArgsRedactedModified: boolean;
|
|
21
|
+
retryRecommendation: RetryRecommendation;
|
|
22
|
+
};
|
|
23
|
+
export type PendingRequestSnapshot = {
|
|
24
|
+
id: RequestId;
|
|
25
|
+
method?: string;
|
|
26
|
+
toolName?: string;
|
|
27
|
+
toolArgsRedacted?: Record<string, unknown>;
|
|
28
|
+
/** `true` when toolArgsRedacted contains sentinel placeholders (truncate/redact/overflow). */
|
|
29
|
+
toolArgsRedactedModified?: boolean;
|
|
30
|
+
startedAt: number;
|
|
31
|
+
lastStage?: string;
|
|
32
|
+
lastStageStartedAt?: number;
|
|
33
|
+
lastStageMeta?: unknown;
|
|
34
|
+
};
|
|
35
|
+
export declare function buildLegacyJsonRpcError(id: RequestId): JSONRPCResponse;
|
|
36
|
+
export type RedactedToolArgs = {
|
|
37
|
+
args: Record<string, unknown>;
|
|
38
|
+
/** `true` when redaction replaced any value with a sentinel; supervisor uses this to decide whether to surface `suggestedCall`. */
|
|
39
|
+
modified: boolean;
|
|
40
|
+
};
|
|
41
|
+
export declare function redactToolArgs(args: unknown): RedactedToolArgs;
|
|
42
|
+
export declare function decideRetryRecommendation(ctx: Pick<RestartContext, "toolName" | "lastStage" | "lastStageMeta" | "exit">, recentRestartTimestamps: number[]): RetryRecommendation;
|
|
43
|
+
export declare function buildSyntheticCallToolResult(id: RequestId, ctx: RestartContext): JSONRPCResponse;
|
|
44
|
+
export declare function pruneRestartTimestamps(timestamps: number[], now: number, windowMs?: number): number[];
|
|
45
|
+
/**
|
|
46
|
+
* Group a worker-exit batch by toolName and produce the per-tool pruned
|
|
47
|
+
* timestamp set plus the post-exit snapshot. Grouping ensures one exit
|
|
48
|
+
* counts as one death per tool regardless of how many concurrent pending
|
|
49
|
+
* requests it killed. `prunedByTool` feeds `buildWorkerRestartReply`;
|
|
50
|
+
* `updatedByTool` is what the caller writes back to `recentRestarts`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function buildExitTimestampGroups(pendingToolNames: Iterable<string | undefined>, recentRestarts: Map<string, number[]>, now: number): {
|
|
53
|
+
prunedByTool: Map<string, number[]>;
|
|
54
|
+
updatedByTool: Map<string, number[]>;
|
|
55
|
+
};
|
|
56
|
+
export type BuildWorkerRestartReplyOptions = {
|
|
57
|
+
structuredRestartDisabled?: boolean;
|
|
58
|
+
};
|
|
59
|
+
export declare function buildWorkerRestartReply(req: PendingRequestSnapshot, exit: ExitInfo, now: number, recentRestartTimestamps: number[], options?: BuildWorkerRestartReplyOptions): {
|
|
60
|
+
reply: JSONRPCResponse;
|
|
61
|
+
updatedTimestamps: number[];
|
|
62
|
+
};
|
|
4
63
|
export declare class StdioSupervisor {
|
|
5
64
|
private readonly entryFile;
|
|
6
65
|
private readonly clientReader;
|
|
7
66
|
private readonly workerReader;
|
|
8
67
|
private readonly queuedMessages;
|
|
9
68
|
private readonly pendingRequests;
|
|
69
|
+
private readonly recentRestarts;
|
|
10
70
|
private child;
|
|
11
71
|
private childReady;
|
|
12
72
|
private shuttingDown;
|
|
@@ -33,6 +93,7 @@ export declare class StdioSupervisor {
|
|
|
33
93
|
private readonly handleWorkerProcessError;
|
|
34
94
|
private readonly handleWorkerExit;
|
|
35
95
|
private handleWorkerMessage;
|
|
96
|
+
private applyStageUpdate;
|
|
36
97
|
private handleWorkerReady;
|
|
37
98
|
private isInitializationResponse;
|
|
38
99
|
private flushQueue;
|
package/dist/stdio-supervisor.js
CHANGED
|
@@ -2,10 +2,20 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { encodeJsonRpcMessage, JsonRpcFrameReader } from "./json-rpc-framing.js";
|
|
4
4
|
import { log } from "./logger.js";
|
|
5
|
+
import { buildSuggestedCall } from "./build-suggested-call.js";
|
|
6
|
+
import { getToolSchema } from "./tool-schema-registry.js";
|
|
5
7
|
const DEFAULT_CLIENT_MODE = "line";
|
|
6
8
|
const WORKER_MODE_ENV = "MCP_STDIO_WORKER_MODE";
|
|
7
9
|
const WORKER_READY_MARKER = "__MCP_STDIO_WORKER_READY__";
|
|
8
10
|
const SUPERVISOR_DEBUG_ENABLED = process.env.MCP_SUPERVISOR_DEBUG === "1";
|
|
11
|
+
const STRUCTURED_RESTART_DISABLED = process.env.SUPERVISOR_STRUCTURED_RESTART_OFF === "1";
|
|
12
|
+
const RESTART_WINDOW_MS = 60_000;
|
|
13
|
+
const RESTART_REPEAT_THRESHOLD = 3;
|
|
14
|
+
const REDACT_KEY_PATTERNS = [/secret/i, /token/i, /apikey/i, /password/i];
|
|
15
|
+
const PRESERVED_PATH_KEYS = new Set(["projectPath", "sourcePath", "mixinConfigPath"]);
|
|
16
|
+
const MAX_STRING_BYTES = 256;
|
|
17
|
+
const MAX_ARRAY_LENGTH = 8;
|
|
18
|
+
const MAX_OBJECT_KEYS = 16;
|
|
9
19
|
function isRequest(message) {
|
|
10
20
|
return "method" in message && "id" in message;
|
|
11
21
|
}
|
|
@@ -23,7 +33,7 @@ function getTrackedRequestId(message) {
|
|
|
23
33
|
function requestKey(id) {
|
|
24
34
|
return `${typeof id}:${String(id)}`;
|
|
25
35
|
}
|
|
26
|
-
function
|
|
36
|
+
export function buildLegacyJsonRpcError(id) {
|
|
27
37
|
return {
|
|
28
38
|
jsonrpc: "2.0",
|
|
29
39
|
id,
|
|
@@ -33,6 +43,252 @@ function buildWorkerRestartError(id) {
|
|
|
33
43
|
}
|
|
34
44
|
};
|
|
35
45
|
}
|
|
46
|
+
function byteLengthUtf8(value) {
|
|
47
|
+
return Buffer.byteLength(value, "utf8");
|
|
48
|
+
}
|
|
49
|
+
function isRedactKey(key) {
|
|
50
|
+
return REDACT_KEY_PATTERNS.some((re) => re.test(key));
|
|
51
|
+
}
|
|
52
|
+
function redactValue(value, counter, keyName) {
|
|
53
|
+
if (keyName !== undefined && !PRESERVED_PATH_KEYS.has(keyName) && isRedactKey(keyName)) {
|
|
54
|
+
counter.modified = true;
|
|
55
|
+
return "<redacted>";
|
|
56
|
+
}
|
|
57
|
+
if (value === null || value === undefined) {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
const valueType = typeof value;
|
|
61
|
+
if (valueType === "string") {
|
|
62
|
+
const str = value;
|
|
63
|
+
if (keyName !== undefined && PRESERVED_PATH_KEYS.has(keyName)) {
|
|
64
|
+
return str;
|
|
65
|
+
}
|
|
66
|
+
const byteLen = byteLengthUtf8(str);
|
|
67
|
+
if (byteLen > MAX_STRING_BYTES) {
|
|
68
|
+
counter.modified = true;
|
|
69
|
+
return `<truncated:${byteLen} bytes>`;
|
|
70
|
+
}
|
|
71
|
+
return str;
|
|
72
|
+
}
|
|
73
|
+
if (valueType === "number" || valueType === "boolean") {
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
if (value.length > MAX_ARRAY_LENGTH) {
|
|
78
|
+
counter.modified = true;
|
|
79
|
+
const head = value
|
|
80
|
+
.slice(0, MAX_ARRAY_LENGTH)
|
|
81
|
+
.map((item) => redactValue(item, counter));
|
|
82
|
+
head.push(`<+${value.length - MAX_ARRAY_LENGTH} more>`);
|
|
83
|
+
return head;
|
|
84
|
+
}
|
|
85
|
+
return value.map((item) => redactValue(item, counter));
|
|
86
|
+
}
|
|
87
|
+
if (valueType === "object") {
|
|
88
|
+
const entries = Object.entries(value);
|
|
89
|
+
const limited = entries.slice(0, MAX_OBJECT_KEYS);
|
|
90
|
+
const result = {};
|
|
91
|
+
for (const [key, child] of limited) {
|
|
92
|
+
result[key] = redactValue(child, counter, key);
|
|
93
|
+
}
|
|
94
|
+
if (entries.length > MAX_OBJECT_KEYS) {
|
|
95
|
+
counter.modified = true;
|
|
96
|
+
result["<+more>"] = `<+${entries.length - MAX_OBJECT_KEYS} more keys>`;
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
export function redactToolArgs(args) {
|
|
103
|
+
if (args === null || args === undefined || typeof args !== "object" || Array.isArray(args)) {
|
|
104
|
+
return { args: {}, modified: false };
|
|
105
|
+
}
|
|
106
|
+
const counter = { modified: false };
|
|
107
|
+
const redacted = redactValue(args, counter);
|
|
108
|
+
if (redacted && typeof redacted === "object" && !Array.isArray(redacted)) {
|
|
109
|
+
return { args: redacted, modified: counter.modified };
|
|
110
|
+
}
|
|
111
|
+
return { args: {}, modified: counter.modified };
|
|
112
|
+
}
|
|
113
|
+
export function decideRetryRecommendation(ctx, recentRestartTimestamps) {
|
|
114
|
+
if (ctx.lastStage === "target-lookup") {
|
|
115
|
+
const meta = ctx.lastStageMeta;
|
|
116
|
+
const targetTotal = typeof meta?.targetTotal === "number" ? meta.targetTotal : undefined;
|
|
117
|
+
if (targetTotal !== undefined && targetTotal > 5) {
|
|
118
|
+
return "narrow-query";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (ctx.exit.signal === "SIGABRT" || ctx.exit.signal === "SIGSEGV" || ctx.exit.signal === "SIGKILL") {
|
|
122
|
+
return "clear-cache";
|
|
123
|
+
}
|
|
124
|
+
if (recentRestartTimestamps.length >= RESTART_REPEAT_THRESHOLD) {
|
|
125
|
+
return "report-bug";
|
|
126
|
+
}
|
|
127
|
+
return "same-request";
|
|
128
|
+
}
|
|
129
|
+
export function buildSyntheticCallToolResult(id, ctx) {
|
|
130
|
+
const tool = ctx.toolName ?? "unknown";
|
|
131
|
+
const hints = [];
|
|
132
|
+
if (ctx.lastStage) {
|
|
133
|
+
const indexNote = (() => {
|
|
134
|
+
const meta = ctx.lastStageMeta;
|
|
135
|
+
if (meta &&
|
|
136
|
+
typeof meta.targetIndex === "number" &&
|
|
137
|
+
typeof meta.targetTotal === "number") {
|
|
138
|
+
return `at index ${meta.targetIndex}/${meta.targetTotal}`;
|
|
139
|
+
}
|
|
140
|
+
return "";
|
|
141
|
+
})();
|
|
142
|
+
const recommendation = (() => {
|
|
143
|
+
switch (ctx.retryRecommendation) {
|
|
144
|
+
case "narrow-query":
|
|
145
|
+
return "narrow mixinConfigPath to one file and retry";
|
|
146
|
+
case "clear-cache":
|
|
147
|
+
return "clear cache and retry";
|
|
148
|
+
case "report-bug":
|
|
149
|
+
return "report a bug — repeated worker exits";
|
|
150
|
+
case "same-request":
|
|
151
|
+
default:
|
|
152
|
+
return "retry the same request";
|
|
153
|
+
}
|
|
154
|
+
})();
|
|
155
|
+
hints.push(`lastStage=${ctx.lastStage}${indexNote ? ` ${indexNote}` : ""} — ${recommendation}`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
hints.push("worker exited before stage tracking began");
|
|
159
|
+
}
|
|
160
|
+
const error = {
|
|
161
|
+
type: "about:blank/mcp/worker-restart",
|
|
162
|
+
title: "MCP worker restarted",
|
|
163
|
+
detail: `The worker process exited while handling tools/call (${tool}).`,
|
|
164
|
+
status: 503,
|
|
165
|
+
code: "ERR_WORKER_RESTART",
|
|
166
|
+
instance: `urn:mcp:request:${String(id)}`,
|
|
167
|
+
hints
|
|
168
|
+
};
|
|
169
|
+
// Mutated args carry sentinel placeholders (`<truncated:…>`, `<redacted>`)
|
|
170
|
+
// that are not safe to retry. Surface them on `meta.restart.redactedToolArgs`
|
|
171
|
+
// for diagnostics instead.
|
|
172
|
+
if (ctx.toolArgsRedacted !== undefined && !ctx.toolArgsRedactedModified) {
|
|
173
|
+
// Only emit `suggestedCall` when the resolved tool name is a
|
|
174
|
+
// currently-registered public tool. The "unknown" fallback above and
|
|
175
|
+
// unregistered names (typo, disabled tool, version-skewed) cannot
|
|
176
|
+
// produce a re-callable payload; buildSuggestedCall fails open for
|
|
177
|
+
// unregistered names so the registration check belongs here. The
|
|
178
|
+
// diagnostic `restart.redactedToolArgs` field below still surfaces the
|
|
179
|
+
// args.
|
|
180
|
+
if (ctx.toolName !== undefined &&
|
|
181
|
+
ctx.toolName.length > 0 &&
|
|
182
|
+
getToolSchema(ctx.toolName) !== undefined) {
|
|
183
|
+
const gated = buildSuggestedCall({
|
|
184
|
+
tool: ctx.toolName,
|
|
185
|
+
params: ctx.toolArgsRedacted
|
|
186
|
+
});
|
|
187
|
+
if (gated.suggestedCall) {
|
|
188
|
+
error.suggestedCall = gated.suggestedCall;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (ctx.lastStage !== undefined) {
|
|
193
|
+
error.failedStage = ctx.lastStage;
|
|
194
|
+
}
|
|
195
|
+
const restart = {
|
|
196
|
+
tool,
|
|
197
|
+
durationMs: ctx.durationMs,
|
|
198
|
+
lastStage: ctx.lastStage ?? null,
|
|
199
|
+
lastStageElapsedMs: ctx.lastStageElapsedMs ?? null,
|
|
200
|
+
lastStageMeta: ctx.lastStageMeta ?? null,
|
|
201
|
+
exit: {
|
|
202
|
+
code: ctx.exit.code,
|
|
203
|
+
signal: ctx.exit.signal
|
|
204
|
+
},
|
|
205
|
+
retryRecommendation: ctx.retryRecommendation
|
|
206
|
+
};
|
|
207
|
+
// Diagnostic echo (paired with the modified flag); not retryable.
|
|
208
|
+
if (ctx.toolArgsRedacted !== undefined) {
|
|
209
|
+
restart.redactedToolArgs = ctx.toolArgsRedacted;
|
|
210
|
+
restart.redactedToolArgsModified = ctx.toolArgsRedactedModified;
|
|
211
|
+
}
|
|
212
|
+
const meta = {
|
|
213
|
+
synthetic: true,
|
|
214
|
+
syntheticSource: "supervisor",
|
|
215
|
+
restart
|
|
216
|
+
};
|
|
217
|
+
const structuredContent = { error, meta };
|
|
218
|
+
return {
|
|
219
|
+
jsonrpc: "2.0",
|
|
220
|
+
id,
|
|
221
|
+
result: {
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: JSON.stringify(structuredContent)
|
|
226
|
+
}
|
|
227
|
+
],
|
|
228
|
+
isError: true,
|
|
229
|
+
structuredContent
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
export function pruneRestartTimestamps(timestamps, now, windowMs = RESTART_WINDOW_MS) {
|
|
234
|
+
const cutoff = now - windowMs;
|
|
235
|
+
return timestamps.filter((t) => t >= cutoff);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Group a worker-exit batch by toolName and produce the per-tool pruned
|
|
239
|
+
* timestamp set plus the post-exit snapshot. Grouping ensures one exit
|
|
240
|
+
* counts as one death per tool regardless of how many concurrent pending
|
|
241
|
+
* requests it killed. `prunedByTool` feeds `buildWorkerRestartReply`;
|
|
242
|
+
* `updatedByTool` is what the caller writes back to `recentRestarts`.
|
|
243
|
+
*/
|
|
244
|
+
export function buildExitTimestampGroups(pendingToolNames, recentRestarts, now) {
|
|
245
|
+
const prunedByTool = new Map();
|
|
246
|
+
const updatedByTool = new Map();
|
|
247
|
+
for (const raw of pendingToolNames) {
|
|
248
|
+
const toolName = raw ?? "unknown";
|
|
249
|
+
if (prunedByTool.has(toolName))
|
|
250
|
+
continue;
|
|
251
|
+
const pruned = pruneRestartTimestamps(recentRestarts.get(toolName) ?? [], now);
|
|
252
|
+
prunedByTool.set(toolName, pruned);
|
|
253
|
+
updatedByTool.set(toolName, [...pruned, now]);
|
|
254
|
+
}
|
|
255
|
+
return { prunedByTool, updatedByTool };
|
|
256
|
+
}
|
|
257
|
+
export function buildWorkerRestartReply(req, exit, now, recentRestartTimestamps, options = {}) {
|
|
258
|
+
if (options.structuredRestartDisabled || req.method !== "tools/call") {
|
|
259
|
+
return {
|
|
260
|
+
reply: buildLegacyJsonRpcError(req.id),
|
|
261
|
+
updatedTimestamps: recentRestartTimestamps
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const pruned = pruneRestartTimestamps(recentRestartTimestamps, now);
|
|
265
|
+
// Include the current restart in the count so "3 deaths in 60s →
|
|
266
|
+
// report-bug" fires on the third event, not the fourth.
|
|
267
|
+
const updatedTimestamps = [...pruned, now];
|
|
268
|
+
const recommendation = decideRetryRecommendation({
|
|
269
|
+
toolName: req.toolName,
|
|
270
|
+
lastStage: req.lastStage,
|
|
271
|
+
lastStageMeta: req.lastStageMeta,
|
|
272
|
+
exit
|
|
273
|
+
}, updatedTimestamps);
|
|
274
|
+
const ctx = {
|
|
275
|
+
toolName: req.toolName,
|
|
276
|
+
durationMs: Math.max(0, now - req.startedAt),
|
|
277
|
+
lastStage: req.lastStage,
|
|
278
|
+
lastStageElapsedMs: req.lastStageStartedAt !== undefined
|
|
279
|
+
? Math.max(0, now - req.lastStageStartedAt)
|
|
280
|
+
: undefined,
|
|
281
|
+
lastStageMeta: req.lastStageMeta,
|
|
282
|
+
exit,
|
|
283
|
+
toolArgsRedacted: req.toolArgsRedacted,
|
|
284
|
+
toolArgsRedactedModified: req.toolArgsRedactedModified ?? false,
|
|
285
|
+
retryRecommendation: recommendation
|
|
286
|
+
};
|
|
287
|
+
return {
|
|
288
|
+
reply: buildSyntheticCallToolResult(req.id, ctx),
|
|
289
|
+
updatedTimestamps
|
|
290
|
+
};
|
|
291
|
+
}
|
|
36
292
|
function debugSupervisor(event, details) {
|
|
37
293
|
if (!SUPERVISOR_DEBUG_ENABLED) {
|
|
38
294
|
return;
|
|
@@ -45,6 +301,7 @@ export class StdioSupervisor {
|
|
|
45
301
|
workerReader = new JsonRpcFrameReader();
|
|
46
302
|
queuedMessages = [];
|
|
47
303
|
pendingRequests = new Map();
|
|
304
|
+
recentRestarts = new Map();
|
|
48
305
|
child;
|
|
49
306
|
childReady = false;
|
|
50
307
|
shuttingDown = false;
|
|
@@ -121,10 +378,21 @@ export class StdioSupervisor {
|
|
|
121
378
|
if (isRequest(message)) {
|
|
122
379
|
const id = getTrackedRequestId(message);
|
|
123
380
|
if (id !== undefined) {
|
|
124
|
-
|
|
381
|
+
const pending = {
|
|
125
382
|
id,
|
|
126
|
-
method: message.method
|
|
127
|
-
|
|
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);
|
|
128
396
|
}
|
|
129
397
|
if (message.method === "initialize") {
|
|
130
398
|
this.initializeSentToWorker = true;
|
|
@@ -202,7 +470,7 @@ export class StdioSupervisor {
|
|
|
202
470
|
signal,
|
|
203
471
|
pendingRequests: this.pendingRequests.size
|
|
204
472
|
});
|
|
205
|
-
this.failPendingRequestsOnWorkerExit();
|
|
473
|
+
this.failPendingRequestsOnWorkerExit({ code, signal });
|
|
206
474
|
this.scheduleRestart();
|
|
207
475
|
};
|
|
208
476
|
handleWorkerMessage(message) {
|
|
@@ -212,6 +480,12 @@ export class StdioSupervisor {
|
|
|
212
480
|
id: "id" in message ? message.id : undefined,
|
|
213
481
|
replayingInitialization: this.replayingInitialization
|
|
214
482
|
});
|
|
483
|
+
if (isNotification(message) && typeof message.method === "string" && message.method.startsWith("$/")) {
|
|
484
|
+
if (message.method === "$/stageUpdate") {
|
|
485
|
+
this.applyStageUpdate(message.params);
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
215
489
|
if (this.isInitializationResponse(message)) {
|
|
216
490
|
const id = getTrackedRequestId(message);
|
|
217
491
|
if (id !== undefined) {
|
|
@@ -240,6 +514,33 @@ export class StdioSupervisor {
|
|
|
240
514
|
}
|
|
241
515
|
this.writeToClient(message);
|
|
242
516
|
}
|
|
517
|
+
applyStageUpdate(params) {
|
|
518
|
+
if (params === null || params === undefined || typeof params !== "object") {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const p = params;
|
|
522
|
+
const requestId = typeof p.requestId === "string" || typeof p.requestId === "number"
|
|
523
|
+
? p.requestId
|
|
524
|
+
: undefined;
|
|
525
|
+
if (requestId === undefined)
|
|
526
|
+
return;
|
|
527
|
+
const pending = this.pendingRequests.get(requestKey(requestId));
|
|
528
|
+
if (!pending)
|
|
529
|
+
return;
|
|
530
|
+
// Refresh `lastStageStartedAt` only on stage transitions so
|
|
531
|
+
// `lastStageElapsedMs` measures from stage entry, not from the latest
|
|
532
|
+
// per-target emit. `lastStageMeta` still updates on every emit so
|
|
533
|
+
// restart envelopes carry the latest progress payload.
|
|
534
|
+
if (typeof p.stage === "string" && p.stage !== pending.lastStage) {
|
|
535
|
+
pending.lastStage = p.stage;
|
|
536
|
+
pending.lastStageStartedAt = performance.now();
|
|
537
|
+
}
|
|
538
|
+
else if (pending.lastStageStartedAt === undefined) {
|
|
539
|
+
// First emit for this request; bootstrap the stage timer.
|
|
540
|
+
pending.lastStageStartedAt = performance.now();
|
|
541
|
+
}
|
|
542
|
+
pending.lastStageMeta = p.meta;
|
|
543
|
+
}
|
|
243
544
|
handleWorkerReady() {
|
|
244
545
|
debugSupervisor("worker_ready", {
|
|
245
546
|
hasInitializeRequest: this.initializeRequest !== undefined,
|
|
@@ -278,16 +579,32 @@ export class StdioSupervisor {
|
|
|
278
579
|
this.forwardToWorker(message);
|
|
279
580
|
}
|
|
280
581
|
}
|
|
281
|
-
failPendingRequestsOnWorkerExit() {
|
|
582
|
+
failPendingRequestsOnWorkerExit(exit) {
|
|
282
583
|
const preservedInitializeKey = this.initializeRequest && !this.clientInitialized
|
|
283
584
|
? requestKey(this.initializeRequest.id)
|
|
284
585
|
: undefined;
|
|
586
|
+
const now = performance.now();
|
|
587
|
+
// Record this exit once per toolName regardless of how many concurrent
|
|
588
|
+
// pending requests it killed; per-request recording would inflate the
|
|
589
|
+
// count to N and falsely escalate retryRecommendation to "report-bug".
|
|
590
|
+
const pendingToolNames = [];
|
|
591
|
+
for (const [key, pending] of this.pendingRequests.entries()) {
|
|
592
|
+
if (key === preservedInitializeKey)
|
|
593
|
+
continue;
|
|
594
|
+
pendingToolNames.push(pending.toolName);
|
|
595
|
+
}
|
|
596
|
+
const { prunedByTool, updatedByTool } = buildExitTimestampGroups(pendingToolNames, this.recentRestarts, now);
|
|
597
|
+
for (const [toolName, updated] of updatedByTool) {
|
|
598
|
+
this.recentRestarts.set(toolName, updated);
|
|
599
|
+
}
|
|
285
600
|
for (const [key, pending] of [...this.pendingRequests.entries()]) {
|
|
286
|
-
if (key === preservedInitializeKey)
|
|
601
|
+
if (key === preservedInitializeKey)
|
|
287
602
|
continue;
|
|
288
|
-
}
|
|
289
603
|
this.pendingRequests.delete(key);
|
|
290
|
-
|
|
604
|
+
const toolName = pending.toolName ?? "unknown";
|
|
605
|
+
const pruned = prunedByTool.get(toolName) ?? [];
|
|
606
|
+
const { reply } = buildWorkerRestartReply(pending, exit, now, pruned, { structuredRestartDisabled: STRUCTURED_RESTART_DISABLED });
|
|
607
|
+
this.writeToClient(reply);
|
|
291
608
|
}
|
|
292
609
|
}
|
|
293
610
|
writeToClient(message) {
|
|
@@ -3,6 +3,7 @@ import type { ArtifactProvenance, ArtifactRow, SourceMapping, SourceOrigin } fro
|
|
|
3
3
|
type SqliteDatabase = InstanceType<typeof Database>;
|
|
4
4
|
interface UpsertArtifactInput {
|
|
5
5
|
artifactId: string;
|
|
6
|
+
alias?: string;
|
|
6
7
|
origin: SourceOrigin;
|
|
7
8
|
coordinate?: string;
|
|
8
9
|
version?: string;
|
|
@@ -27,6 +28,7 @@ export declare class ArtifactsRepo {
|
|
|
27
28
|
private readonly upsertStmt;
|
|
28
29
|
private readonly getStmt;
|
|
29
30
|
private readonly touchStmt;
|
|
31
|
+
private readonly setAliasStmt;
|
|
30
32
|
private readonly deleteStmt;
|
|
31
33
|
private readonly listStmt;
|
|
32
34
|
private readonly countStmt;
|
|
@@ -34,8 +36,9 @@ export declare class ArtifactsRepo {
|
|
|
34
36
|
private readonly listLruWithContentBytesStmt;
|
|
35
37
|
constructor(db: SqliteDatabase);
|
|
36
38
|
upsertArtifact(input: UpsertArtifactInput): void;
|
|
37
|
-
getArtifact(
|
|
39
|
+
getArtifact(artifactIdOrAlias: string): ArtifactRow | undefined;
|
|
38
40
|
touchArtifact(artifactId: string, timestamp: string): void;
|
|
41
|
+
setAlias(artifactId: string, alias: string): void;
|
|
39
42
|
deleteArtifact(artifactId: string): void;
|
|
40
43
|
listArtifactsByLru(limit: number): ArtifactRow[];
|
|
41
44
|
countArtifacts(): number;
|
|
@@ -35,6 +35,7 @@ function parseQualityFlags(value) {
|
|
|
35
35
|
function toArtifactRow(record) {
|
|
36
36
|
return {
|
|
37
37
|
artifactId: record.artifact_id,
|
|
38
|
+
alias: record.alias ?? undefined,
|
|
38
39
|
origin: record.origin,
|
|
39
40
|
coordinate: record.coordinate ?? undefined,
|
|
40
41
|
version: record.version ?? undefined,
|
|
@@ -59,6 +60,7 @@ export class ArtifactsRepo {
|
|
|
59
60
|
upsertStmt;
|
|
60
61
|
getStmt;
|
|
61
62
|
touchStmt;
|
|
63
|
+
setAliasStmt;
|
|
62
64
|
deleteStmt;
|
|
63
65
|
listStmt;
|
|
64
66
|
countStmt;
|
|
@@ -68,11 +70,12 @@ export class ArtifactsRepo {
|
|
|
68
70
|
this.db = db;
|
|
69
71
|
this.upsertStmt = this.db.prepare(`
|
|
70
72
|
INSERT INTO artifacts (
|
|
71
|
-
artifact_id, origin, coordinate, version, binary_jar_path, source_jar_path, repo_url, requested_mapping, mapping_applied, provenance_json, quality_flags_json, artifact_signature, is_decompiled, created_at, updated_at
|
|
73
|
+
artifact_id, alias, origin, coordinate, version, binary_jar_path, source_jar_path, repo_url, requested_mapping, mapping_applied, provenance_json, quality_flags_json, artifact_signature, is_decompiled, created_at, updated_at
|
|
72
74
|
) VALUES (
|
|
73
|
-
@artifact_id, @origin, @coordinate, @version, @binary_jar_path, @source_jar_path, @repo_url, @requested_mapping, @mapping_applied, @provenance_json, @quality_flags_json, @artifact_signature, @is_decompiled, @created_at, @updated_at
|
|
75
|
+
@artifact_id, @alias, @origin, @coordinate, @version, @binary_jar_path, @source_jar_path, @repo_url, @requested_mapping, @mapping_applied, @provenance_json, @quality_flags_json, @artifact_signature, @is_decompiled, @created_at, @updated_at
|
|
74
76
|
)
|
|
75
77
|
ON CONFLICT(artifact_id) DO UPDATE SET
|
|
78
|
+
alias = excluded.alias,
|
|
76
79
|
origin = excluded.origin,
|
|
77
80
|
coordinate = excluded.coordinate,
|
|
78
81
|
version = excluded.version,
|
|
@@ -87,9 +90,12 @@ export class ArtifactsRepo {
|
|
|
87
90
|
is_decompiled = excluded.is_decompiled,
|
|
88
91
|
updated_at = excluded.updated_at
|
|
89
92
|
`);
|
|
93
|
+
// artifact_id is a 64-char SHA hex; alias is `<type>-<...>-<6charhex>` containing
|
|
94
|
+
// dashes/letters. The two namespaces cannot collide, so OR-matching is unambiguous.
|
|
90
95
|
this.getStmt = this.db.prepare(`
|
|
91
96
|
SELECT
|
|
92
97
|
artifact_id,
|
|
98
|
+
alias,
|
|
93
99
|
origin,
|
|
94
100
|
coordinate,
|
|
95
101
|
version,
|
|
@@ -105,17 +111,28 @@ export class ArtifactsRepo {
|
|
|
105
111
|
created_at,
|
|
106
112
|
updated_at
|
|
107
113
|
FROM artifacts
|
|
108
|
-
WHERE artifact_id = ?
|
|
114
|
+
WHERE artifact_id = ? OR alias = ?
|
|
115
|
+
LIMIT 1
|
|
109
116
|
`);
|
|
110
117
|
this.touchStmt = this.db.prepare(`
|
|
111
118
|
UPDATE artifacts
|
|
112
119
|
SET updated_at = ?
|
|
113
120
|
WHERE artifact_id = ?
|
|
121
|
+
`);
|
|
122
|
+
// Persists alias on cache-hit / migrated-row paths where upsertArtifact would
|
|
123
|
+
// otherwise be skipped. Conditional WHERE keeps it idempotent and avoids a
|
|
124
|
+
// pointless write when alias is already correct.
|
|
125
|
+
this.setAliasStmt = this.db.prepare(`
|
|
126
|
+
UPDATE artifacts
|
|
127
|
+
SET alias = ?
|
|
128
|
+
WHERE artifact_id = ?
|
|
129
|
+
AND (alias IS NULL OR alias <> ?)
|
|
114
130
|
`);
|
|
115
131
|
this.deleteStmt = this.db.prepare(`DELETE FROM artifacts WHERE artifact_id = ?`);
|
|
116
132
|
this.listStmt = this.db.prepare(`
|
|
117
133
|
SELECT
|
|
118
134
|
artifact_id,
|
|
135
|
+
alias,
|
|
119
136
|
origin,
|
|
120
137
|
coordinate,
|
|
121
138
|
version,
|
|
@@ -157,6 +174,7 @@ export class ArtifactsRepo {
|
|
|
157
174
|
upsertArtifact(input) {
|
|
158
175
|
this.upsertStmt.run({
|
|
159
176
|
artifact_id: input.artifactId,
|
|
177
|
+
alias: input.alias ?? null,
|
|
160
178
|
origin: input.origin,
|
|
161
179
|
coordinate: input.coordinate ?? null,
|
|
162
180
|
version: input.version ?? null,
|
|
@@ -173,8 +191,11 @@ export class ArtifactsRepo {
|
|
|
173
191
|
updated_at: input.timestamp
|
|
174
192
|
});
|
|
175
193
|
}
|
|
176
|
-
|
|
177
|
-
|
|
194
|
+
// Accepts either an artifact_id (64-char SHA hex) or a human-readable alias
|
|
195
|
+
// (e.g. `mc-1.21.10-mojang-merged-5ad2e7`). The two key namespaces cannot
|
|
196
|
+
// collide because aliases always contain `-` and a non-hex prefix.
|
|
197
|
+
getArtifact(artifactIdOrAlias) {
|
|
198
|
+
const row = this.getStmt.get([artifactIdOrAlias, artifactIdOrAlias]);
|
|
178
199
|
if (!row) {
|
|
179
200
|
return undefined;
|
|
180
201
|
}
|
|
@@ -183,6 +204,13 @@ export class ArtifactsRepo {
|
|
|
183
204
|
touchArtifact(artifactId, timestamp) {
|
|
184
205
|
this.touchStmt.run([timestamp, artifactId]);
|
|
185
206
|
}
|
|
207
|
+
// Backfills or rotates the alias for an existing row. Used by warm-cache
|
|
208
|
+
// resolveArtifact paths where upsertArtifact is skipped, so a freshly
|
|
209
|
+
// computed alias still reaches the DB and stays in sync with the value the
|
|
210
|
+
// caller just received in the response.
|
|
211
|
+
setAlias(artifactId, alias) {
|
|
212
|
+
this.setAliasStmt.run([alias, artifactId, alias]);
|
|
213
|
+
}
|
|
186
214
|
deleteArtifact(artifactId) {
|
|
187
215
|
this.deleteStmt.run([artifactId]);
|
|
188
216
|
}
|
|
@@ -43,8 +43,6 @@ export declare class FilesRepo {
|
|
|
43
43
|
private readonly db;
|
|
44
44
|
private readonly deleteStmt;
|
|
45
45
|
private readonly insertFilesStmt;
|
|
46
|
-
private readonly insertFtsStmt;
|
|
47
|
-
private readonly deleteFtsStmt;
|
|
48
46
|
private readonly getContentStmt;
|
|
49
47
|
private readonly listStmt;
|
|
50
48
|
private readonly listRowsStmt;
|