ast-mcp-server 0.11.1 → 0.11.2
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 +20 -2
- package/README.md +14 -2
- package/SECURITY.md +1 -1
- package/dist/compiler-worker-entry.js +51 -0
- package/dist/index.js +20 -120
- package/dist/services/compiler-worker-host.js +422 -0
- package/dist/services/compiler-worker-protocol.js +95 -0
- package/dist/services/operations.js +3 -1
- package/dist/services/project.js +7 -0
- package/dist/services/public-errors.js +6 -3
- package/dist/services/runtime-logger.js +5 -0
- package/dist/services/runtime-policy.js +61 -0
- package/dist/worker-runtime.js +126 -0
- package/docs/support.md +22 -6
- package/package.json +3 -3
- package/skills/structural-code-editing/SKILL.md +1 -1
- package/skills/structural-code-editing/releases.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [0.11.
|
|
9
|
+
## [0.11.2] — local release candidate, pending release (2026-08-25)
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added an explicit per-connection supervised compiler-worker mode that can recycle an idle compiler child while the local stdio parent remains connected; `in_process` remains the default and rollback, and TTL `0` disables recycling.
|
|
14
|
+
- Added scoped Linux evidence on exact Node.js 22.13.0 and Node.js 24 for PSS reclamation, stable compiler fingerprints, SQLite reuse, bounded redaction, and parent-death cleanup without orphans.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Compiler-worker startup now waits for `ready` before replaying initialization, preventing cross-channel settlement ordering from rejecting the first forwarded request as stale.
|
|
19
|
+
- CLI project-discovery fixtures now create the external project directory before concurrent configuration writes, eliminating a nondeterministic `ENOENT` setup race.
|
|
20
|
+
- Public-registry verification now copies the isolated runner's complete local runtime dependency closure, including the shared managed-skill validator, before launching it outside the source tree.
|
|
21
|
+
- A deterministic copied-runner regression executes the isolated entry point and rejects any local import omitted by its production bootstrap.
|
|
22
|
+
|
|
23
|
+
### Release status
|
|
24
|
+
|
|
25
|
+
- This local release candidate succeeds immutable npm versions `0.11.0` and `0.11.1`, neither of which was promotable: `0.11.0` verification exposed stale managed-skill predicates, and `0.11.1` verification exposed the copied runner's missing validator module. Version `0.11.2` remains pending its exact-tree matrix, CI, and separately authorized delivery sequence; it has not been published or assigned to any dist-tag.
|
|
26
|
+
|
|
27
|
+
## [0.11.1] — published to `next`, verification failed, not promoted (2026-08-23)
|
|
10
28
|
|
|
11
29
|
### Fixed
|
|
12
30
|
|
|
@@ -15,7 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
15
33
|
|
|
16
34
|
### Release status
|
|
17
35
|
|
|
18
|
-
-
|
|
36
|
+
- npm published these immutable bytes under `next` with provenance and the expected Git commit. Public-registry verification then failed because the isolated registry runner omitted its newly imported managed-skill validator module; `0.11.1` was never promoted to `latest`, tagged in Git, or released on GitHub.
|
|
19
37
|
|
|
20
38
|
## [0.11.0] — published to `next`, verification failed, not promoted (2026-08-23)
|
|
21
39
|
|
package/README.md
CHANGED
|
@@ -84,11 +84,23 @@ The included batch benchmark records a 50% reduction in model round-trips and a
|
|
|
84
84
|
|
|
85
85
|
## Supported environment and trust boundary
|
|
86
86
|
|
|
87
|
-
The local `0.11.
|
|
87
|
+
The local `0.11.2` release candidate requires Node.js `>=22.13.0`; its evidence matrix targets exact Node.js 22.13.0 and the current Node.js 24 line. Published v0.10.0 retains its immutable historical Node.js 22.13.0/24 evidence. Managed setup-file publication additionally requires GNU coreutils 9.7 `mv` supporting `--update=none-fail`, `--exchange`, `--no-copy`, and `--no-target-directory`, GNU coreutils `ln -L -T`, procfs descriptor paths at `/proc/self/fd`, and `O_DIRECTORY`/`O_NOFOLLOW`. Other Linux architectures or systems without those filesystem primitives, macOS, and Windows remain unverified.
|
|
88
88
|
|
|
89
89
|
This is a local stdio server. It runs with the invoking user's filesystem permissions, and clients may request any `project_root` that user can access. It does not provide HTTP authentication, sandboxing, tenant isolation, or a remote-service security boundary. Remote, untrusted, and multi-tenant operation is unsupported.
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
### Optional supervised compiler worker
|
|
92
|
+
|
|
93
|
+
The compiler runs in process by default. Linux operators may explicitly keep the stdio parent connected while allowing an idle compiler child to exit and lazily respawn:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
AST_COMPILER_WORKER_MODE=supervised ast-mcp-server
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The parent waits for child readiness before replaying bounded initialization state. Requests and cancellation remain generation-affine; mutation history, live operation leases, and completion-critical apply work prevent unsafe recycling. Set `AST_COMPILER_WORKER_MODE=in_process` for the full rollback, or set `AST_COMPILER_WORKER_IDLE_TTL_MS=0` to retain the relay while disabling idle recycling.
|
|
100
|
+
|
|
101
|
+
The scoped Linux canary passed on exact Node.js 22.13.0 and Node.js 24 with repeatable PSS reclamation, stable compiler fingerprints, unchanged SQLite reuse, bounded redacted diagnostics, and no orphan after parent death. This is one child per connection, not a shared daemon, pool, or new default. See [ADR 0014](docs/adr/0014-supervised-compiler-worker.md).
|
|
102
|
+
|
|
103
|
+
In published v0.10.0 and the local `0.11.2` release candidate, an absent `AST_SYMBOL_INDEX_PERSISTENCE` or explicit `enabled` selects the private SQLite symbol-index cache. `disabled` is the immediate memory-only rollback. `canary` requires an explicit absolute normalized `AST_SYMBOL_INDEX_CACHE_ROOT`. Invalid policy or storage fails closed to compiler-authoritative memory reads with bounded path-free status.
|
|
92
104
|
|
|
93
105
|
The default cache root is selected from `AST_SYMBOL_INDEX_CACHE_ROOT`, then `XDG_CACHE_HOME`, then `HOME`. Inspect or clear only derived cache artifacts through the bounded CLI:
|
|
94
106
|
|
package/SECURITY.md
CHANGED
|
@@ -11,7 +11,7 @@ Security fixes are provided for the exact version currently identified by the np
|
|
|
11
11
|
| Earlier npm versions | No |
|
|
12
12
|
| Unpublished release candidates | No |
|
|
13
13
|
|
|
14
|
-
This project supports trusted single-user local stdio use; the supported operating-system and architecture boundary remains Linux x64 only. The local `0.11.
|
|
14
|
+
This project supports trusted single-user local stdio use; the supported operating-system and architecture boundary remains Linux x64 only. The local `0.11.2` release candidate requires Node.js `>=22.13.0`, with evidence targeted at exact Node.js 22.13.0 and the governed Node.js 24 major. Published v0.10.0 retains its immutable Node.js 22.13.0/24 release matrix. Managed setup mutation requires GNU coreutils 9.7 `mv --update=none-fail --exchange --no-copy --no-target-directory`, GNU coreutils `ln -L -T`, and procfs descriptor-relative paths at `/proc/self/fd` with `O_DIRECTORY`/`O_NOFOLLOW`. The candidate's default SQLite cache also relies on Linux ownership, mode, no-follow, link-count and inode-identity checks; it does not defend against a malicious same-UID process. Other Linux architectures and systems without those primitives are unverified, as are macOS and Windows. Remote, untrusted, and multi-tenant use is outside the supported security boundary.
|
|
15
15
|
|
|
16
16
|
## Reporting a vulnerability
|
|
17
17
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { PassThrough } from "node:stream";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { decodeCompilerWorkerEnvelope } from "./services/compiler-worker-protocol.js";
|
|
7
|
+
async function runDefaultCompilerRuntime(input = process.stdin) {
|
|
8
|
+
return (await import("./worker-runtime.js")).runStdioServer({
|
|
9
|
+
transport: new StdioServerTransport(input, process.stdout),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function runCompilerWorkerChild(runRuntime = runDefaultCompilerRuntime) {
|
|
13
|
+
return runRuntime();
|
|
14
|
+
}
|
|
15
|
+
// prettier-ignore
|
|
16
|
+
export function installPrivateControl(runtime, input) {
|
|
17
|
+
let sequence = 0, generation = 0, writeSequence = 0;
|
|
18
|
+
const send = (generation, type, payload) => process.send?.({ v: 1, generation, sequence: ++sequence, type, payload });
|
|
19
|
+
const arm = (handle) => { const force = () => { if (handle.snapshot?.().completion_critical_operations > 0)
|
|
20
|
+
setTimeout(force, 50).unref();
|
|
21
|
+
else
|
|
22
|
+
process.exit(1); }; setTimeout(force, (handle.shutdownGraceMs ?? 10_000) + 2_000).unref(); };
|
|
23
|
+
const shutdown = (handle, trigger) => { if (trigger === "parent_exit")
|
|
24
|
+
arm(handle); void handle.shutdown(trigger).finally(() => process.disconnect()); };
|
|
25
|
+
process.on("message", (message) => { const decoded = decodeCompilerWorkerEnvelope(JSON.stringify(message)); if (!decoded.ok)
|
|
26
|
+
return; if (decoded.value.type === "handshake") {
|
|
27
|
+
generation = decoded.value.generation;
|
|
28
|
+
void runtime.then(() => send(generation, "ready", {}));
|
|
29
|
+
} if (decoded.value.type === "snapshot")
|
|
30
|
+
void runtime.then((handle) => send(generation, "snapshot", (handle.snapshot?.() ?? {}))); if (decoded.value.type === "shutdown") {
|
|
31
|
+
const trigger = decoded.value.payload.trigger === "parent_exit" ? "parent_exit" : "requested";
|
|
32
|
+
void runtime.then((handle) => shutdown(handle, trigger));
|
|
33
|
+
} });
|
|
34
|
+
if (input) {
|
|
35
|
+
const lines = createInterface({ input: process.stdin });
|
|
36
|
+
lines.on("line", (line) => input.write(`${line}\n`, () => send(generation, "settled", { write_sequence: ++writeSequence })));
|
|
37
|
+
lines.once("close", () => input.end());
|
|
38
|
+
}
|
|
39
|
+
process.once("disconnect", () => void runtime.then((handle) => { arm(handle); void handle.shutdown("parent_exit").then(() => process.exit(0), () => process.exit(1)); }));
|
|
40
|
+
}
|
|
41
|
+
const entry = process.argv[1];
|
|
42
|
+
if (entry && fs.realpathSync(entry) === fs.realpathSync(fileURLToPath(import.meta.url))) {
|
|
43
|
+
const input = process.send ? new PassThrough() : undefined;
|
|
44
|
+
const runtime = input ? runDefaultCompilerRuntime(input) : runCompilerWorkerChild();
|
|
45
|
+
if (process.send)
|
|
46
|
+
installPrivateControl(runtime, input);
|
|
47
|
+
runtime.catch(() => {
|
|
48
|
+
process.stderr.write("Fatal compiler worker startup error.\n");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
|
51
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,131 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import { createServer } from "./server.js";
|
|
7
|
-
import { beginProjectShutdown, closeDrainedProjectSessions, getProjectRuntimeShutdownSnapshot, prepareProjectRuntimeForStartup, waitForProjectCompletionCriticalOperationsToDrain, waitForProjectOperationsToDrain, } from "./services/project.js";
|
|
8
|
-
import { RuntimeActivityTracker } from "./services/runtime-activity.js";
|
|
9
4
|
import { readRuntimePolicy } from "./services/runtime-policy.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
export async function loadRuntimeModule(policy, loaders) {
|
|
6
|
+
return {
|
|
7
|
+
mode: policy.compilerWorkerMode,
|
|
8
|
+
module: await (policy.compilerWorkerMode === "supervised"
|
|
9
|
+
? loaders.supervised()
|
|
10
|
+
: loaders.inProcess()),
|
|
11
|
+
};
|
|
13
12
|
}
|
|
14
13
|
export async function runStdioServer(options = {}) {
|
|
15
|
-
|
|
16
|
-
throw new Error("An injected MCP server requires its runtime activity tracker.");
|
|
17
|
-
}
|
|
18
|
-
prepareProjectRuntimeForStartup();
|
|
19
|
-
const runtimeActivity = options.runtimeActivity ?? new RuntimeActivityTracker();
|
|
20
|
-
runtimeActivity.prepareForStartup();
|
|
21
|
-
const server = options.server ?? createServer({ runtimeActivity });
|
|
22
|
-
const transport = options.transport ?? new StdioServerTransport();
|
|
23
|
-
const send = transport.send.bind(transport);
|
|
24
|
-
transport.send = (message, sendOptions) => runtimeActivity.trackSend(() => send(message, sendOptions));
|
|
25
|
-
const runtimePolicy = options.runtimePolicy ?? readRuntimePolicy();
|
|
26
|
-
const installProcessHandlers = options.installProcessHandlers ?? true;
|
|
27
|
-
const exit = options.exit ?? ((code) => process.exit(code));
|
|
28
|
-
const protocol = server.server;
|
|
29
|
-
const previousTransportClose = protocol.onclose;
|
|
30
|
-
let listenersInstalled = false;
|
|
31
|
-
let terminalHandlerInstalled = false;
|
|
32
|
-
const onSigint = () => requestFromRuntime("sigint");
|
|
33
|
-
const onSigterm = () => requestFromRuntime("sigterm");
|
|
34
|
-
const onStdinEnd = () => requestFromRuntime("stdin_eof");
|
|
35
|
-
const onStdinClose = () => requestFromRuntime("stdin_eof");
|
|
36
|
-
const onTransportClose = () => {
|
|
37
|
-
previousTransportClose?.();
|
|
38
|
-
requestFromRuntime("transport_closed");
|
|
39
|
-
};
|
|
40
|
-
const removeLifecycleListeners = () => {
|
|
41
|
-
if (listenersInstalled) {
|
|
42
|
-
process.off("SIGINT", onSigint);
|
|
43
|
-
process.off("SIGTERM", onSigterm);
|
|
44
|
-
process.stdin.off("end", onStdinEnd);
|
|
45
|
-
process.stdin.off("close", onStdinClose);
|
|
46
|
-
listenersInstalled = false;
|
|
47
|
-
}
|
|
48
|
-
if (protocol.onclose === onTransportClose)
|
|
49
|
-
protocol.onclose = previousTransportClose;
|
|
50
|
-
};
|
|
51
|
-
const coordinator = new ShutdownCoordinator({
|
|
52
|
-
drainTimeoutMs: runtimePolicy.shutdownDrainTimeoutMs,
|
|
53
|
-
stopAdmissionAndCancel: () => {
|
|
54
|
-
runtimeActivity.beginShutdown();
|
|
55
|
-
return beginProjectShutdown();
|
|
56
|
-
},
|
|
57
|
-
getDrainSnapshot: getProjectRuntimeShutdownSnapshot,
|
|
58
|
-
waitForDrain: async () => {
|
|
59
|
-
await Promise.all([waitForProjectOperationsToDrain(), runtimeActivity.waitForIdle()]);
|
|
60
|
-
},
|
|
61
|
-
waitForCompletionCriticalOperationsToDrain: waitForProjectCompletionCriticalOperationsToDrain,
|
|
62
|
-
closeMcp: async () => {
|
|
63
|
-
await server.close();
|
|
64
|
-
},
|
|
65
|
-
closeProjects: closeDrainedProjectSessions,
|
|
66
|
-
emitIncompleteShutdown: emitIncompleteShutdownEvent,
|
|
67
|
-
onCompletionCriticalWait: options.onCompletionCriticalWait,
|
|
68
|
-
});
|
|
69
|
-
let lifecycleCompletion;
|
|
70
|
-
function requestShutdown(trigger) {
|
|
71
|
-
lifecycleCompletion ??= coordinator.shutdown(trigger).finally(removeLifecycleListeners);
|
|
72
|
-
return lifecycleCompletion;
|
|
73
|
-
}
|
|
74
|
-
function requestFromRuntime(trigger) {
|
|
75
|
-
try {
|
|
76
|
-
options.onRuntimeTrigger?.(trigger);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// Runtime observation is non-authoritative and must never interrupt shutdown.
|
|
80
|
-
}
|
|
81
|
-
const completion = requestShutdown(trigger);
|
|
82
|
-
if (terminalHandlerInstalled)
|
|
83
|
-
return;
|
|
84
|
-
terminalHandlerInstalled = true;
|
|
85
|
-
void completion
|
|
86
|
-
.then((result) => {
|
|
87
|
-
if (result.state === "forced_noncritical")
|
|
88
|
-
exit(1);
|
|
89
|
-
})
|
|
90
|
-
.catch(() => {
|
|
91
|
-
emitShutdownFailureEvent();
|
|
92
|
-
exit(1);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
protocol.onclose = onTransportClose;
|
|
96
|
-
if (installProcessHandlers) {
|
|
97
|
-
process.on("SIGINT", onSigint);
|
|
98
|
-
process.on("SIGTERM", onSigterm);
|
|
99
|
-
process.stdin.once("end", onStdinEnd);
|
|
100
|
-
process.stdin.once("close", onStdinClose);
|
|
101
|
-
listenersInstalled = true;
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
await server.connect(transport);
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
await requestShutdown("requested").catch(() => undefined);
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
if (options.logStartup ?? true)
|
|
111
|
-
process.stderr.write("ast-mcp-server running over stdio.\n");
|
|
112
|
-
return Object.freeze({
|
|
113
|
-
shutdown: (trigger = "requested") => requestShutdown(trigger),
|
|
114
|
-
});
|
|
14
|
+
return (await import("./worker-runtime.js")).runStdioServer(options);
|
|
115
15
|
}
|
|
116
|
-
function
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
16
|
+
async function runConfiguredStdioServer() {
|
|
17
|
+
const policy = readRuntimePolicy();
|
|
18
|
+
const selected = await loadRuntimeModule(policy, {
|
|
19
|
+
inProcess: async () => ({ run: () => runStdioServer({ runtimePolicy: policy }) }),
|
|
20
|
+
supervised: async () => ({
|
|
21
|
+
run: () => import("./services/compiler-worker-host.js").then((host) => host.runSupervisedStdioServer()),
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
await selected.module.run();
|
|
126
25
|
}
|
|
127
|
-
|
|
128
|
-
|
|
26
|
+
const entry = process.argv[1];
|
|
27
|
+
if (entry && fs.realpathSync(entry) === fs.realpathSync(fileURLToPath(import.meta.url))) {
|
|
28
|
+
runConfiguredStdioServer().catch(() => {
|
|
129
29
|
process.stderr.write("Fatal ast-mcp-server startup error.\n");
|
|
130
30
|
process.exit(1);
|
|
131
31
|
});
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { decodeCompilerWorkerEnvelope, startCompilerWorker } from "./compiler-worker-protocol.js";
|
|
8
|
+
import { createCompilerWorkerSpawnSpec, readRuntimePolicy, } from "./runtime-policy.js";
|
|
9
|
+
import { emitCompilerWorkerEvent } from "./runtime-logger.js";
|
|
10
|
+
import { PublicOperationalError, renderPublicError } from "./public-errors.js";
|
|
11
|
+
export class CompilerWorkerHostError extends Error {
|
|
12
|
+
kind;
|
|
13
|
+
recovery;
|
|
14
|
+
constructor(kind, recovery) {
|
|
15
|
+
super("Compiler worker request failed.");
|
|
16
|
+
this.kind = kind;
|
|
17
|
+
this.recovery = recovery;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const bounded = (value) => Buffer.byteLength(JSON.stringify(value)) <= 256 * 1024;
|
|
21
|
+
const within = (promise, ms) => Promise.race([
|
|
22
|
+
promise,
|
|
23
|
+
delay(ms, undefined, { ref: false }).then(() => {
|
|
24
|
+
throw new CompilerWorkerHostError("startup");
|
|
25
|
+
}),
|
|
26
|
+
]);
|
|
27
|
+
function deferred() {
|
|
28
|
+
let resolve;
|
|
29
|
+
let reject;
|
|
30
|
+
const promise = new Promise((yes, no) => {
|
|
31
|
+
resolve = yes;
|
|
32
|
+
reject = no;
|
|
33
|
+
});
|
|
34
|
+
return { promise, resolve, reject };
|
|
35
|
+
}
|
|
36
|
+
export function spawnCompilerWorkerProcess(options) {
|
|
37
|
+
const spec = createCompilerWorkerSpawnSpec(options.workerEntryPath, options.environment);
|
|
38
|
+
if (!spec.ok || !spec.command)
|
|
39
|
+
throw new CompilerWorkerHostError("startup");
|
|
40
|
+
const child = spawn(spec.command, spec.args, {
|
|
41
|
+
...spec.options,
|
|
42
|
+
stdio: ["pipe", "pipe", "inherit", "ipc"],
|
|
43
|
+
});
|
|
44
|
+
const ready = deferred();
|
|
45
|
+
const closed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
46
|
+
const pending = new Map();
|
|
47
|
+
const snapshots = [];
|
|
48
|
+
const acknowledgements = new Map();
|
|
49
|
+
let outbound = 0;
|
|
50
|
+
let inbound = 0;
|
|
51
|
+
let controlInbound = 0;
|
|
52
|
+
let writes = 0;
|
|
53
|
+
const fail = (error) => {
|
|
54
|
+
ready.reject(error);
|
|
55
|
+
for (const request of pending.values())
|
|
56
|
+
request.reject(error);
|
|
57
|
+
pending.clear();
|
|
58
|
+
for (const snapshot of snapshots.splice(0))
|
|
59
|
+
snapshot.reject(error);
|
|
60
|
+
for (const acknowledged of acknowledgements.values())
|
|
61
|
+
acknowledged.reject(error);
|
|
62
|
+
acknowledgements.clear();
|
|
63
|
+
};
|
|
64
|
+
child.once("close", () => fail(new CompilerWorkerHostError("worker_exit")));
|
|
65
|
+
child.on("message", (message) => {
|
|
66
|
+
const frame = JSON.stringify(message);
|
|
67
|
+
const decoded = decodeCompilerWorkerEnvelope(frame);
|
|
68
|
+
if (!decoded.ok ||
|
|
69
|
+
decoded.value.generation !== options.generation ||
|
|
70
|
+
decoded.value.sequence <= controlInbound ||
|
|
71
|
+
!["ready", "snapshot", "settled"].includes(decoded.value.type))
|
|
72
|
+
return fail(new CompilerWorkerHostError("protocol"));
|
|
73
|
+
controlInbound = decoded.value.sequence;
|
|
74
|
+
if (decoded.value.type === "settled") {
|
|
75
|
+
const writeSequence = decoded.value.payload.write_sequence;
|
|
76
|
+
if (!Number.isSafeInteger(writeSequence))
|
|
77
|
+
return fail(new CompilerWorkerHostError("protocol"));
|
|
78
|
+
acknowledgements.get(writeSequence)?.resolve();
|
|
79
|
+
acknowledgements.delete(writeSequence);
|
|
80
|
+
}
|
|
81
|
+
else if (decoded.value.type === "ready") {
|
|
82
|
+
inbound = 1;
|
|
83
|
+
ready.resolve(frame);
|
|
84
|
+
}
|
|
85
|
+
else
|
|
86
|
+
snapshots.shift()?.resolve(decoded.value);
|
|
87
|
+
});
|
|
88
|
+
createInterface({ input: child.stdout }).on("line", (line) => {
|
|
89
|
+
if (Buffer.byteLength(line) > 256 * 1024)
|
|
90
|
+
return fail(new CompilerWorkerHostError("protocol"));
|
|
91
|
+
let message;
|
|
92
|
+
try {
|
|
93
|
+
message = JSON.parse(line);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return fail(new CompilerWorkerHostError("protocol"));
|
|
97
|
+
}
|
|
98
|
+
if (!("id" in message) || message.id === undefined)
|
|
99
|
+
return;
|
|
100
|
+
const request = pending.get(message.id);
|
|
101
|
+
if (!request)
|
|
102
|
+
return;
|
|
103
|
+
pending.delete(message.id);
|
|
104
|
+
request.resolve({
|
|
105
|
+
generation: options.generation,
|
|
106
|
+
sequence: ++inbound,
|
|
107
|
+
payload: { request_id: message.id, message },
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
const write = (message, acknowledge = false) => {
|
|
111
|
+
if (!bounded(message) || !child.stdin?.writable)
|
|
112
|
+
return Promise.reject(new CompilerWorkerHostError("protocol"));
|
|
113
|
+
options.beforeWrite?.(message);
|
|
114
|
+
const writeSequence = ++writes, processed = acknowledge ? deferred() : undefined;
|
|
115
|
+
if (processed) {
|
|
116
|
+
acknowledgements.set(writeSequence, processed);
|
|
117
|
+
void processed.promise.catch(() => undefined);
|
|
118
|
+
}
|
|
119
|
+
const written = new Promise((resolve, reject) => child.stdin.write(`${JSON.stringify(message)}\n`, (error) => error ? (acknowledgements.delete(writeSequence), reject(error)) : resolve()));
|
|
120
|
+
return processed ? written.then(() => processed.promise) : written;
|
|
121
|
+
};
|
|
122
|
+
const control = (type, payload) => {
|
|
123
|
+
const frame = { v: 1, generation: options.generation, sequence: ++outbound, type, payload };
|
|
124
|
+
if (!bounded(frame) || !child.connected)
|
|
125
|
+
return Promise.reject(new CompilerWorkerHostError("protocol"));
|
|
126
|
+
return new Promise((resolve, reject) => child.send(frame, (error) => (error ? reject(error) : resolve())));
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
generation: options.generation,
|
|
130
|
+
ready: ready.promise,
|
|
131
|
+
async replay(frames) {
|
|
132
|
+
await control("handshake", {});
|
|
133
|
+
await ready.promise;
|
|
134
|
+
for (const frame of frames)
|
|
135
|
+
await write(frame);
|
|
136
|
+
},
|
|
137
|
+
forward(message) {
|
|
138
|
+
if (!("id" in message))
|
|
139
|
+
return write(message, true).then(() => undefined);
|
|
140
|
+
if (message.id === undefined)
|
|
141
|
+
return Promise.reject(new CompilerWorkerHostError("protocol"));
|
|
142
|
+
const result = deferred();
|
|
143
|
+
const id = message.id;
|
|
144
|
+
pending.set(id, result);
|
|
145
|
+
void write(message).catch((error) => {
|
|
146
|
+
pending.delete(id);
|
|
147
|
+
result.reject(error);
|
|
148
|
+
});
|
|
149
|
+
return result.promise;
|
|
150
|
+
},
|
|
151
|
+
cancel(id) {
|
|
152
|
+
if (!pending.has(id))
|
|
153
|
+
return false;
|
|
154
|
+
void write({ jsonrpc: "2.0", method: "notifications/cancelled", params: { requestId: id } }, true).catch(() => undefined);
|
|
155
|
+
return true;
|
|
156
|
+
},
|
|
157
|
+
async snapshot() {
|
|
158
|
+
await Promise.all([...acknowledgements.values()].map(({ promise }) => promise));
|
|
159
|
+
const result = deferred();
|
|
160
|
+
snapshots.push(result);
|
|
161
|
+
await control("snapshot", {});
|
|
162
|
+
return (await result.promise).payload;
|
|
163
|
+
},
|
|
164
|
+
terminate: () => {
|
|
165
|
+
child.kill("SIGKILL");
|
|
166
|
+
},
|
|
167
|
+
reap: () => closed,
|
|
168
|
+
async shutdown(trigger) {
|
|
169
|
+
await control("shutdown", { trigger }).catch(() => undefined);
|
|
170
|
+
child.stdin?.end();
|
|
171
|
+
await closed;
|
|
172
|
+
return "complete";
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export class CompilerWorkerHost {
|
|
177
|
+
spawn;
|
|
178
|
+
timeoutMs;
|
|
179
|
+
lifecycle;
|
|
180
|
+
generation = 0;
|
|
181
|
+
sequence = 0;
|
|
182
|
+
child;
|
|
183
|
+
starting;
|
|
184
|
+
active = new Map();
|
|
185
|
+
leases = new Map();
|
|
186
|
+
initialization = [];
|
|
187
|
+
activity = 0;
|
|
188
|
+
parentWork = 0;
|
|
189
|
+
parentAdmission = "open";
|
|
190
|
+
idleTimer;
|
|
191
|
+
retiring;
|
|
192
|
+
constructor(spawn, timeoutMs = 10_000, lifecycle = {}) {
|
|
193
|
+
this.spawn = spawn;
|
|
194
|
+
this.timeoutMs = timeoutMs;
|
|
195
|
+
this.lifecycle = lifecycle;
|
|
196
|
+
}
|
|
197
|
+
now() {
|
|
198
|
+
return (this.lifecycle.now ?? Date.now)();
|
|
199
|
+
}
|
|
200
|
+
// prettier-ignore
|
|
201
|
+
pruneLeases() { const now = this.now(); for (const [id, expiry] of this.leases)
|
|
202
|
+
if (expiry <= now)
|
|
203
|
+
this.leases.delete(id); }
|
|
204
|
+
// prettier-ignore
|
|
205
|
+
addLease(id) { if (typeof id !== "string" || !id || !bounded(id))
|
|
206
|
+
return; this.pruneLeases(); this.leases.set(id, this.now() + (this.lifecycle.leaseTtlMs ?? 15 * 60_000)); while (this.leases.size > (this.lifecycle.leaseLimit ?? 128))
|
|
207
|
+
this.leases.delete(this.leases.keys().next().value); }
|
|
208
|
+
// prettier-ignore
|
|
209
|
+
scheduleIdle() { clearTimeout(this.idleTimer); const ttl = this.lifecycle.idleTtlMs ?? 0; if (ttl > 0 && this.child)
|
|
210
|
+
this.idleTimer = setTimeout(() => { this.retiring = this.recycle(this.activity).catch(async () => { const child = this.child; await child?.reap().catch(() => undefined); if (this.child === child)
|
|
211
|
+
this.child = undefined; }).finally(() => { this.retiring = undefined; }); }, ttl); }
|
|
212
|
+
// prettier-ignore
|
|
213
|
+
static quiescent(value) { return value.runtime_admission === "open" && value.project_admission === "open" && ["active_requests", "active_sends", "active_operations", "queued_operations", "completion_critical_operations"].every((key) => value[key] === 0) && value.mutation_history === false; }
|
|
214
|
+
// prettier-ignore
|
|
215
|
+
async recycle(activity) { this.pruneLeases(); const child = this.child, generation = this.generation, sequence = this.sequence; if (!child || this.parentWork || this.leases.size || child.generation !== generation)
|
|
216
|
+
return this.scheduleIdle(); const first = await child.snapshot(); if (!CompilerWorkerHost.quiescent(first) || activity !== this.activity || sequence !== this.sequence)
|
|
217
|
+
return this.scheduleIdle(); this.parentAdmission = "closed"; const second = await child.snapshot(); if (child !== this.child || generation !== this.generation || sequence !== this.sequence || activity !== this.activity || !CompilerWorkerHost.quiescent(second)) {
|
|
218
|
+
this.parentAdmission = "open";
|
|
219
|
+
return this.scheduleIdle();
|
|
220
|
+
} await this.stopChild(child, "requested"); emitCompilerWorkerEvent({ kind: "idle", generation }); this.parentAdmission = "open"; }
|
|
221
|
+
// prettier-ignore
|
|
222
|
+
async stopChild(child, trigger) { const completion = child.shutdown(trigger); try {
|
|
223
|
+
await within(completion, this.timeoutMs);
|
|
224
|
+
if (this.child === child)
|
|
225
|
+
this.child = undefined;
|
|
226
|
+
return "complete";
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
const snapshot = await child.snapshot().catch(() => undefined);
|
|
230
|
+
if (snapshot?.completion_critical_operations) {
|
|
231
|
+
await completion;
|
|
232
|
+
if (this.child === child)
|
|
233
|
+
this.child = undefined;
|
|
234
|
+
return "complete";
|
|
235
|
+
}
|
|
236
|
+
child.terminate();
|
|
237
|
+
await child.reap().catch(() => undefined);
|
|
238
|
+
if (this.child === child)
|
|
239
|
+
this.child = undefined;
|
|
240
|
+
return "forced";
|
|
241
|
+
} }
|
|
242
|
+
async start(initialization) {
|
|
243
|
+
if (this.child)
|
|
244
|
+
return { generation: this.child.generation, sequence: this.sequence };
|
|
245
|
+
if (this.starting)
|
|
246
|
+
return this.starting;
|
|
247
|
+
this.starting = (async () => {
|
|
248
|
+
let child;
|
|
249
|
+
try {
|
|
250
|
+
const envelope = initialization.length === 2
|
|
251
|
+
? await startCompilerWorker({
|
|
252
|
+
frames: initialization,
|
|
253
|
+
timeoutMs: this.timeoutMs,
|
|
254
|
+
spawn: () => (child = this.spawn(++this.generation)),
|
|
255
|
+
})
|
|
256
|
+
: await (async () => {
|
|
257
|
+
child = this.spawn(++this.generation);
|
|
258
|
+
await child.replay([]);
|
|
259
|
+
const decoded = decodeCompilerWorkerEnvelope(await within(child.ready, this.timeoutMs));
|
|
260
|
+
if (!decoded.ok || decoded.value.type !== "ready")
|
|
261
|
+
throw new Error();
|
|
262
|
+
return decoded.value;
|
|
263
|
+
})();
|
|
264
|
+
if (!child || envelope.generation !== child.generation)
|
|
265
|
+
throw new Error();
|
|
266
|
+
this.child = child;
|
|
267
|
+
this.sequence = envelope.sequence;
|
|
268
|
+
this.parentAdmission = "open";
|
|
269
|
+
this.scheduleIdle();
|
|
270
|
+
return { generation: child.generation, sequence: this.sequence };
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
child?.terminate();
|
|
274
|
+
await child?.reap().catch(() => undefined);
|
|
275
|
+
throw new CompilerWorkerHostError("startup");
|
|
276
|
+
}
|
|
277
|
+
})().finally(() => {
|
|
278
|
+
this.starting = undefined;
|
|
279
|
+
});
|
|
280
|
+
return this.starting;
|
|
281
|
+
}
|
|
282
|
+
async forward(message) {
|
|
283
|
+
if (!bounded(message))
|
|
284
|
+
throw new CompilerWorkerHostError("protocol");
|
|
285
|
+
await this.retiring;
|
|
286
|
+
this.activity += 1;
|
|
287
|
+
await this.start(this.initialization.length === 2 ? this.initialization : []);
|
|
288
|
+
this.parentWork += 1;
|
|
289
|
+
const id = "id" in message ? message.id : undefined;
|
|
290
|
+
if (id !== undefined)
|
|
291
|
+
this.active.set(id, this.generation);
|
|
292
|
+
try {
|
|
293
|
+
const settled = await this.child.forward(message);
|
|
294
|
+
if ("method" in message && message.method === "initialize")
|
|
295
|
+
this.initialization = [message];
|
|
296
|
+
if ("method" in message &&
|
|
297
|
+
message.method === "notifications/initialized" &&
|
|
298
|
+
this.initialization.length === 1)
|
|
299
|
+
this.initialization.push(message);
|
|
300
|
+
if (!settled)
|
|
301
|
+
return undefined;
|
|
302
|
+
const payload = settled.payload;
|
|
303
|
+
if (settled.generation !== this.generation ||
|
|
304
|
+
settled.sequence <= this.sequence ||
|
|
305
|
+
payload.request_id !== id ||
|
|
306
|
+
!payload.message)
|
|
307
|
+
throw new CompilerWorkerHostError("protocol");
|
|
308
|
+
this.sequence = settled.sequence;
|
|
309
|
+
const response = payload.message;
|
|
310
|
+
const result = "result" in response ? response.result : undefined;
|
|
311
|
+
this.addLease(result?.operation_id ??
|
|
312
|
+
result?.structuredContent?.operation_id);
|
|
313
|
+
return response;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
if (error instanceof CompilerWorkerHostError && error.kind === "worker_exit") {
|
|
317
|
+
const child = this.child;
|
|
318
|
+
await child?.reap().catch(() => undefined);
|
|
319
|
+
if (this.child === child)
|
|
320
|
+
this.child = undefined;
|
|
321
|
+
emitCompilerWorkerEvent({
|
|
322
|
+
kind: "crash",
|
|
323
|
+
generation: child?.generation ?? this.generation,
|
|
324
|
+
});
|
|
325
|
+
const params = "params" in message
|
|
326
|
+
? message.params
|
|
327
|
+
: undefined;
|
|
328
|
+
const operationId = params?.name === "ast_apply_operation" ? params.arguments?.operation_id : undefined;
|
|
329
|
+
if (typeof operationId === "string") {
|
|
330
|
+
this.addLease(operationId);
|
|
331
|
+
const correlationId = randomUUID();
|
|
332
|
+
emitCompilerWorkerEvent({
|
|
333
|
+
kind: "ambiguity",
|
|
334
|
+
generation: child?.generation ?? this.generation,
|
|
335
|
+
correlationId,
|
|
336
|
+
count: this.leases.size,
|
|
337
|
+
});
|
|
338
|
+
throw new CompilerWorkerHostError("ambiguous_apply", {
|
|
339
|
+
correlation_id: correlationId,
|
|
340
|
+
expires_at: this.leases.get(operationId),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
throw error instanceof CompilerWorkerHostError
|
|
345
|
+
? error
|
|
346
|
+
: new CompilerWorkerHostError("worker_exit");
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
if (id !== undefined)
|
|
350
|
+
this.active.delete(id);
|
|
351
|
+
this.parentWork -= 1;
|
|
352
|
+
this.scheduleIdle();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
cancel(id) {
|
|
356
|
+
if (this.active.get(id) !== this.generation || !this.child)
|
|
357
|
+
return "not_found";
|
|
358
|
+
return this.child.cancel(id) ? "forwarded" : "not_found";
|
|
359
|
+
}
|
|
360
|
+
async shutdown(trigger) {
|
|
361
|
+
clearTimeout(this.idleTimer);
|
|
362
|
+
await this.starting?.catch(() => undefined);
|
|
363
|
+
if (!this.child)
|
|
364
|
+
return "complete";
|
|
365
|
+
const child = this.child;
|
|
366
|
+
this.parentAdmission = "closed";
|
|
367
|
+
return this.stopChild(child, trigger);
|
|
368
|
+
}
|
|
369
|
+
snapshot() {
|
|
370
|
+
this.pruneLeases();
|
|
371
|
+
return {
|
|
372
|
+
generation: this.generation,
|
|
373
|
+
sequence: this.sequence,
|
|
374
|
+
state: this.child ? "ready" : this.starting ? "starting" : "idle",
|
|
375
|
+
active_requests: this.active.size,
|
|
376
|
+
parent_work: this.parentWork,
|
|
377
|
+
parent_admission: this.parentAdmission,
|
|
378
|
+
lease_tombstones: this.leases.size,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
export async function runSupervisedStdioServer() {
|
|
383
|
+
const entry = fileURLToPath(new URL("../compiler-worker-entry.js", import.meta.url));
|
|
384
|
+
const policy = readRuntimePolicy(process.env);
|
|
385
|
+
const host = new CompilerWorkerHost((generation) => spawnCompilerWorkerProcess({ generation, workerEntryPath: entry, environment: process.env }), 10_000, { idleTtlMs: policy.compilerWorkerIdleTtlMs });
|
|
386
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
387
|
+
const relay = async (line) => {
|
|
388
|
+
let message;
|
|
389
|
+
try {
|
|
390
|
+
message = JSON.parse(line);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// prettier-ignore
|
|
396
|
+
if ("method" in message && message.method === "notifications/cancelled") {
|
|
397
|
+
const id = message.params?.requestId;
|
|
398
|
+
if ((typeof id === "string" || typeof id === "number") && bounded(id))
|
|
399
|
+
host.cancel(id);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const response = await host.forward(message);
|
|
404
|
+
if (response)
|
|
405
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if ("id" in message) {
|
|
409
|
+
const ambiguous = error instanceof CompilerWorkerHostError &&
|
|
410
|
+
error.kind === "ambiguous_apply" &&
|
|
411
|
+
error.recovery;
|
|
412
|
+
const rendered = ambiguous
|
|
413
|
+
? renderPublicError(new PublicOperationalError("AMBIGUOUS_APPLY", "The apply outcome is ambiguous."), ambiguous.correlation_id).envelope.error
|
|
414
|
+
: undefined;
|
|
415
|
+
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, error: { code: -32603, message: rendered?.message ?? "Compiler worker request failed.", ...(ambiguous ? { data: { code: "AMBIGUOUS_APPLY", ...ambiguous } } : {}) } })}\n`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
lines.on("line", (line) => void relay(line));
|
|
420
|
+
lines.once("close", () => void host.shutdown("parent_exit"));
|
|
421
|
+
return { shutdown: () => host.shutdown("requested"), snapshot: () => host.snapshot() };
|
|
422
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
const FIELDS = new Set(["v", "generation", "sequence", "type", "payload"]);
|
|
3
|
+
const TYPES = new Set(["handshake", "snapshot", "shutdown", "ready", "settled", "lease", "exit"]);
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
export function decodeCompilerWorkerEnvelope(frame) {
|
|
8
|
+
if (Buffer.byteLength(frame) > 256 * 1024) {
|
|
9
|
+
return { ok: false, reason: "oversized" };
|
|
10
|
+
}
|
|
11
|
+
let value;
|
|
12
|
+
try {
|
|
13
|
+
value = JSON.parse(frame);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { ok: false, reason: "malformed" };
|
|
17
|
+
}
|
|
18
|
+
if (!isRecord(value))
|
|
19
|
+
return { ok: false, reason: "invalid_envelope" };
|
|
20
|
+
if (Object.keys(value).some((key) => !FIELDS.has(key)))
|
|
21
|
+
return { ok: false, reason: "unknown_field" };
|
|
22
|
+
if (value.v !== 1)
|
|
23
|
+
return { ok: false, reason: "version_drift" };
|
|
24
|
+
if (typeof value.type !== "string" || !TYPES.has(value.type)) {
|
|
25
|
+
return { ok: false, reason: "unknown_type" };
|
|
26
|
+
}
|
|
27
|
+
if (!Number.isSafeInteger(value.generation) ||
|
|
28
|
+
value.generation < 1 ||
|
|
29
|
+
!Number.isSafeInteger(value.sequence) ||
|
|
30
|
+
value.sequence < 1 ||
|
|
31
|
+
!isRecord(value.payload)) {
|
|
32
|
+
return { ok: false, reason: "invalid_envelope" };
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, value };
|
|
35
|
+
}
|
|
36
|
+
export function validateInitializationReplay(frames) {
|
|
37
|
+
if (frames.length < 2)
|
|
38
|
+
return { ok: false, reason: "missing_replay" };
|
|
39
|
+
if (frames.length > 2)
|
|
40
|
+
return { ok: false, reason: "unexpected_replay" };
|
|
41
|
+
const [request, notification] = frames;
|
|
42
|
+
if (!isRecord(request) ||
|
|
43
|
+
request.method !== "initialize" ||
|
|
44
|
+
!("id" in request) ||
|
|
45
|
+
!isRecord(notification) ||
|
|
46
|
+
notification.method !== "notifications/initialized" ||
|
|
47
|
+
"id" in notification) {
|
|
48
|
+
return { ok: false, reason: "missing_replay" };
|
|
49
|
+
}
|
|
50
|
+
if (Buffer.byteLength(JSON.stringify(frames)) > 256 * 1024) {
|
|
51
|
+
return { ok: false, reason: "oversized_replay" };
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, frames: [request, notification] };
|
|
54
|
+
}
|
|
55
|
+
export class CompilerWorkerStartupError extends Error {
|
|
56
|
+
reason;
|
|
57
|
+
constructor(reason) {
|
|
58
|
+
super("Compiler worker startup failed.");
|
|
59
|
+
this.reason = reason;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function attempt(options, frames) {
|
|
63
|
+
const child = await options.spawn();
|
|
64
|
+
let timeout;
|
|
65
|
+
try {
|
|
66
|
+
await child.replay(frames);
|
|
67
|
+
const timedOut = new Promise((_, reject) => {
|
|
68
|
+
timeout = setTimeout(() => reject(new CompilerWorkerStartupError("timeout")), options.timeoutMs ?? 10_000);
|
|
69
|
+
});
|
|
70
|
+
const decoded = decodeCompilerWorkerEnvelope(await Promise.race([child.ready, timedOut]));
|
|
71
|
+
if (!decoded.ok || decoded.value.type !== "ready")
|
|
72
|
+
throw new CompilerWorkerStartupError("protocol");
|
|
73
|
+
return decoded.value;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
child.terminate();
|
|
77
|
+
await child.reap();
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (timeout !== undefined)
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function startCompilerWorker(options) {
|
|
86
|
+
const replay = validateInitializationReplay(options.frames);
|
|
87
|
+
if (!replay.ok)
|
|
88
|
+
throw new CompilerWorkerStartupError(replay.reason);
|
|
89
|
+
try {
|
|
90
|
+
return await attempt(options, replay.frames);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return attempt(options, replay.frames);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { createTwoFilesPatch } from "diff";
|
|
6
6
|
import { LanguageVariant, Node, ScriptTarget, SyntaxKind, ts } from "ts-morph";
|
|
7
7
|
import { compareDiagnostics, normalizeDiagnostic, } from "./diagnostics.js";
|
|
8
|
-
import { createFreshProject, findDeclarationByName, getSourceFileOrThrow, invalidateProjectIfIdle, withProjectOperation, } from "./project.js";
|
|
8
|
+
import { createFreshProject, findDeclarationByName, getSourceFileOrThrow, invalidateProjectIfIdle, recordProjectMutationHistory, withProjectOperation, } from "./project.js";
|
|
9
9
|
import { NO_REQUEST_CONTEXT } from "./request-context.js";
|
|
10
10
|
import { executableDeclaration } from "./symbols.js";
|
|
11
11
|
import { buildClassScaffold } from "./scaffold.js";
|
|
@@ -290,6 +290,7 @@ async function createPlan(projectRootInput, kind, allowNewErrors, mutate, reques
|
|
|
290
290
|
};
|
|
291
291
|
operationContext.checkpoint();
|
|
292
292
|
operations.set(operationId, record);
|
|
293
|
+
recordProjectMutationHistory();
|
|
293
294
|
retainedOperationId = operationId;
|
|
294
295
|
await operationTestHooks.afterRetain?.(operationId);
|
|
295
296
|
return publicOperation(record);
|
|
@@ -846,6 +847,7 @@ export function importOperationRecord(persisted, expectedPlanHash) {
|
|
|
846
847
|
}
|
|
847
848
|
pruneOperations();
|
|
848
849
|
operations.set(record.operation_id, record);
|
|
850
|
+
recordProjectMutationHistory();
|
|
849
851
|
return publicOperation(record);
|
|
850
852
|
}
|
|
851
853
|
export function setOperationTestHooksForTests(hooks) {
|
package/dist/services/project.js
CHANGED
|
@@ -23,6 +23,7 @@ const projectRoots = new WeakMap();
|
|
|
23
23
|
let projectSessionAccessSequence = 0n;
|
|
24
24
|
let projectRuntimePolicy;
|
|
25
25
|
let projectRuntimeAdmission = "open";
|
|
26
|
+
let projectMutationHistory = false;
|
|
26
27
|
function nextProjectSessionAccessSequence() {
|
|
27
28
|
projectSessionAccessSequence += 1n;
|
|
28
29
|
return projectSessionAccessSequence;
|
|
@@ -791,12 +792,17 @@ export function clearProjectSessions() {
|
|
|
791
792
|
projectSessionAccessSequence = 0n;
|
|
792
793
|
projectRuntimePolicy = undefined;
|
|
793
794
|
projectRuntimeAdmission = "open";
|
|
795
|
+
projectMutationHistory = false;
|
|
796
|
+
}
|
|
797
|
+
export function recordProjectMutationHistory() {
|
|
798
|
+
projectMutationHistory = true;
|
|
794
799
|
}
|
|
795
800
|
export function prepareProjectRuntimeForStartup() {
|
|
796
801
|
if (projectSessions.size !== 0) {
|
|
797
802
|
throw new Error("Cannot start a project runtime while sessions remain open.");
|
|
798
803
|
}
|
|
799
804
|
projectRuntimeAdmission = "open";
|
|
805
|
+
projectMutationHistory = false;
|
|
800
806
|
}
|
|
801
807
|
export function getProjectRuntimeShutdownSnapshot() {
|
|
802
808
|
let activeOperations = 0;
|
|
@@ -814,6 +820,7 @@ export function getProjectRuntimeShutdownSnapshot() {
|
|
|
814
820
|
active_operations: activeOperations,
|
|
815
821
|
queued_operations: queuedOperations,
|
|
816
822
|
completion_critical_operations: completionCriticalOperations,
|
|
823
|
+
mutation_history: projectMutationHistory,
|
|
817
824
|
});
|
|
818
825
|
}
|
|
819
826
|
export function beginProjectShutdown() {
|
|
@@ -15,6 +15,7 @@ export const PUBLIC_ERROR_CODES = Object.freeze([
|
|
|
15
15
|
"STALE_WORKSPACE",
|
|
16
16
|
"INCOMPLETE_EVIDENCE",
|
|
17
17
|
"MUTATION_BLOCKED",
|
|
18
|
+
"AMBIGUOUS_APPLY",
|
|
18
19
|
"CONFLICT",
|
|
19
20
|
"INTERNAL_ERROR",
|
|
20
21
|
]);
|
|
@@ -38,6 +39,7 @@ const FIXED_PUBLIC_MESSAGES = Object.freeze({
|
|
|
38
39
|
STALE_WORKSPACE: "The workspace changed. Retry the operation.",
|
|
39
40
|
INCOMPLETE_EVIDENCE: "The required compiler evidence is incomplete.",
|
|
40
41
|
MUTATION_BLOCKED: "The mutation is blocked.",
|
|
42
|
+
AMBIGUOUS_APPLY: "The apply outcome is ambiguous. Inspect the operation receipt before retrying.",
|
|
41
43
|
CONFLICT: "The operation conflicts with current state.",
|
|
42
44
|
INTERNAL_ERROR: INTERNAL_ERROR_MESSAGE,
|
|
43
45
|
});
|
|
@@ -178,7 +180,9 @@ function codedError(error) {
|
|
|
178
180
|
return null;
|
|
179
181
|
return {
|
|
180
182
|
code: code,
|
|
181
|
-
message:
|
|
183
|
+
message: code === "AMBIGUOUS_APPLY"
|
|
184
|
+
? FIXED_PUBLIC_MESSAGES.AMBIGUOUS_APPLY
|
|
185
|
+
: sanitizePublicText(message),
|
|
182
186
|
};
|
|
183
187
|
}
|
|
184
188
|
if (!(error instanceof Error))
|
|
@@ -236,9 +240,8 @@ function serializePublicError(code, message, correlationId) {
|
|
|
236
240
|
};
|
|
237
241
|
return { envelope, text: JSON.stringify(envelope) };
|
|
238
242
|
}
|
|
239
|
-
export function renderPublicError(error) {
|
|
243
|
+
export function renderPublicError(error, correlationId = randomUUID()) {
|
|
240
244
|
const classified = classifyPublicError(error);
|
|
241
|
-
const correlationId = randomUUID();
|
|
242
245
|
const initial = serializePublicError(classified.code, classified.message, correlationId);
|
|
243
246
|
if (Buffer.byteLength(initial.text, "utf8") <= MAX_PUBLIC_ERROR_RESPONSE_BYTES) {
|
|
244
247
|
return initial;
|
|
@@ -48,3 +48,8 @@ export function renderToolFailureEvent(input) {
|
|
|
48
48
|
export function emitToolFailureEvent(input) {
|
|
49
49
|
process.stderr.write(`${renderToolFailureEvent(input)}\n`);
|
|
50
50
|
}
|
|
51
|
+
// prettier-ignore
|
|
52
|
+
export function emitCompilerWorkerEvent(input) {
|
|
53
|
+
const event = { event: "compiler_worker", version: 1, kind: input.kind, generation: Number.isSafeInteger(input.generation) ? input.generation : 0, correlation_id: input.correlationId && UUID_PATTERN.test(input.correlationId) ? input.correlationId : undefined, count: input.count === undefined ? undefined : Math.max(0, Math.min(128, Math.trunc(input.count))) };
|
|
54
|
+
process.stderr.write(`${JSON.stringify(event)}\n`);
|
|
55
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
export const RUNTIME_POLICY_ENV_KEYS = Object.freeze([
|
|
2
3
|
"AST_MAX_PROJECT_SESSIONS",
|
|
3
4
|
"AST_MAX_QUEUED_OPERATIONS_PER_PROJECT",
|
|
4
5
|
"AST_QUEUE_WAIT_TIMEOUT_MS",
|
|
5
6
|
"AST_OPERATION_DEADLINE_MS",
|
|
6
7
|
"AST_SHUTDOWN_DRAIN_TIMEOUT_MS",
|
|
8
|
+
"AST_COMPILER_WORKER_MODE",
|
|
9
|
+
"AST_COMPILER_WORKER_IDLE_TTL_MS",
|
|
7
10
|
]);
|
|
8
11
|
function parseBoundedInteger(rawValue, defaultValue, minimum, maximum) {
|
|
9
12
|
if (rawValue === undefined)
|
|
@@ -26,12 +29,22 @@ export function parseRuntimePolicy(environment) {
|
|
|
26
29
|
const queueWaitTimeoutMs = parseBoundedInteger(environment.AST_QUEUE_WAIT_TIMEOUT_MS, 30_000, 100, 300_000);
|
|
27
30
|
const operationDeadlineMs = parseBoundedInteger(environment.AST_OPERATION_DEADLINE_MS, 120_000, 1_000, 900_000);
|
|
28
31
|
const shutdownDrainTimeoutMs = parseBoundedInteger(environment.AST_SHUTDOWN_DRAIN_TIMEOUT_MS, 10_000, 100, 60_000);
|
|
32
|
+
const rawWorkerMode = environment.AST_COMPILER_WORKER_MODE;
|
|
33
|
+
const compilerWorkerMode = rawWorkerMode === "supervised" ? "supervised" : "in_process";
|
|
34
|
+
const compilerWorkerModeReason = rawWorkerMode === undefined
|
|
35
|
+
? "default"
|
|
36
|
+
: rawWorkerMode === "in_process" || rawWorkerMode === "supervised"
|
|
37
|
+
? "configured"
|
|
38
|
+
: "invalid_mode";
|
|
39
|
+
const compilerWorkerIdleTtlMs = parseBoundedInteger(environment.AST_COMPILER_WORKER_IDLE_TTL_MS, 60_000, 0, 86_400_000);
|
|
29
40
|
const reasons = Object.freeze({
|
|
30
41
|
AST_MAX_PROJECT_SESSIONS: maxProjectSessions.reason,
|
|
31
42
|
AST_MAX_QUEUED_OPERATIONS_PER_PROJECT: maxQueuedOperationsPerProject.reason,
|
|
32
43
|
AST_QUEUE_WAIT_TIMEOUT_MS: queueWaitTimeoutMs.reason,
|
|
33
44
|
AST_OPERATION_DEADLINE_MS: operationDeadlineMs.reason,
|
|
34
45
|
AST_SHUTDOWN_DRAIN_TIMEOUT_MS: shutdownDrainTimeoutMs.reason,
|
|
46
|
+
AST_COMPILER_WORKER_MODE: compilerWorkerModeReason,
|
|
47
|
+
AST_COMPILER_WORKER_IDLE_TTL_MS: compilerWorkerIdleTtlMs.reason,
|
|
35
48
|
});
|
|
36
49
|
return Object.freeze({
|
|
37
50
|
maxProjectSessions: maxProjectSessions.value,
|
|
@@ -39,9 +52,57 @@ export function parseRuntimePolicy(environment) {
|
|
|
39
52
|
queueWaitTimeoutMs: queueWaitTimeoutMs.value,
|
|
40
53
|
operationDeadlineMs: operationDeadlineMs.value,
|
|
41
54
|
shutdownDrainTimeoutMs: shutdownDrainTimeoutMs.value,
|
|
55
|
+
compilerWorkerMode,
|
|
56
|
+
compilerWorkerIdleTtlMs: compilerWorkerIdleTtlMs.value,
|
|
42
57
|
reasons,
|
|
43
58
|
});
|
|
44
59
|
}
|
|
60
|
+
const WORKER_PATH_KEYS = ["HOME", "XDG_CACHE_HOME", "AST_SYMBOL_INDEX_CACHE_ROOT"];
|
|
61
|
+
const WORKER_ENV_KEYS = [
|
|
62
|
+
...WORKER_PATH_KEYS,
|
|
63
|
+
"AST_SYMBOL_INDEX_PERSISTENCE",
|
|
64
|
+
"AST_SYMBOL_INDEX_BUSY_TIMEOUT_MS",
|
|
65
|
+
];
|
|
66
|
+
const MAX_WORKER_ENV_VALUE_LENGTH = 4096;
|
|
67
|
+
function validEnvironmentString(value) {
|
|
68
|
+
return (typeof value === "string" &&
|
|
69
|
+
value.length <= MAX_WORKER_ENV_VALUE_LENGTH &&
|
|
70
|
+
!value.includes("\0"));
|
|
71
|
+
}
|
|
72
|
+
export function createCompilerWorkerSpawnSpec(workerEntryPath, environment) {
|
|
73
|
+
const projected = {};
|
|
74
|
+
for (const key of WORKER_ENV_KEYS) {
|
|
75
|
+
const value = environment[key];
|
|
76
|
+
if (value === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
if (!validEnvironmentString(value))
|
|
79
|
+
return { ok: false, reason: "invalid_environment" };
|
|
80
|
+
const isPath = WORKER_PATH_KEYS.includes(key);
|
|
81
|
+
const invalidMode = key === "AST_SYMBOL_INDEX_PERSISTENCE" && !["disabled", "canary", "enabled"].includes(value);
|
|
82
|
+
const invalidTimeout = key === "AST_SYMBOL_INDEX_BUSY_TIMEOUT_MS" &&
|
|
83
|
+
parseBoundedInteger(value, 1_000, 1, 60_000).reason !== "configured";
|
|
84
|
+
if ((isPath && (!path.isAbsolute(value) || path.normalize(value) !== value)) ||
|
|
85
|
+
invalidMode ||
|
|
86
|
+
invalidTimeout) {
|
|
87
|
+
return { ok: false, reason: "invalid_environment" };
|
|
88
|
+
}
|
|
89
|
+
projected[key] = value;
|
|
90
|
+
}
|
|
91
|
+
const policy = parseRuntimePolicy(environment);
|
|
92
|
+
Object.assign(projected, {
|
|
93
|
+
AST_MAX_PROJECT_SESSIONS: String(policy.maxProjectSessions),
|
|
94
|
+
AST_MAX_QUEUED_OPERATIONS_PER_PROJECT: String(policy.maxQueuedOperationsPerProject),
|
|
95
|
+
AST_QUEUE_WAIT_TIMEOUT_MS: String(policy.queueWaitTimeoutMs),
|
|
96
|
+
AST_OPERATION_DEADLINE_MS: String(policy.operationDeadlineMs),
|
|
97
|
+
AST_SHUTDOWN_DRAIN_TIMEOUT_MS: String(policy.shutdownDrainTimeoutMs),
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
command: process.execPath,
|
|
102
|
+
args: [workerEntryPath],
|
|
103
|
+
options: { shell: false, env: projected },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
45
106
|
export function readRuntimePolicy(environment = process.env) {
|
|
46
107
|
return parseRuntimePolicy(environment);
|
|
47
108
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
import { beginProjectShutdown, closeDrainedProjectSessions, getProjectRuntimeShutdownSnapshot, prepareProjectRuntimeForStartup, waitForProjectCompletionCriticalOperationsToDrain, waitForProjectOperationsToDrain, } from "./services/project.js";
|
|
4
|
+
import { RuntimeActivityTracker } from "./services/runtime-activity.js";
|
|
5
|
+
import { readRuntimePolicy } from "./services/runtime-policy.js";
|
|
6
|
+
import { emitIncompleteShutdownEvent, ShutdownCoordinator, } from "./services/shutdown.js";
|
|
7
|
+
function emitShutdownFailureEvent() {
|
|
8
|
+
process.stderr.write(`${JSON.stringify({ event: "shutdown_failed", version: 1 })}\n`);
|
|
9
|
+
}
|
|
10
|
+
export async function runStdioServer(options = {}) {
|
|
11
|
+
if (options.server && !options.runtimeActivity) {
|
|
12
|
+
throw new Error("An injected MCP server requires its runtime activity tracker.");
|
|
13
|
+
}
|
|
14
|
+
prepareProjectRuntimeForStartup();
|
|
15
|
+
const runtimeActivity = options.runtimeActivity ?? new RuntimeActivityTracker();
|
|
16
|
+
runtimeActivity.prepareForStartup();
|
|
17
|
+
const server = options.server ?? createServer({ runtimeActivity });
|
|
18
|
+
const transport = options.transport ?? new StdioServerTransport();
|
|
19
|
+
const send = transport.send.bind(transport);
|
|
20
|
+
transport.send = (message, sendOptions) => runtimeActivity.trackSend(() => send(message, sendOptions));
|
|
21
|
+
const runtimePolicy = options.runtimePolicy ?? readRuntimePolicy();
|
|
22
|
+
const installProcessHandlers = options.installProcessHandlers ?? true;
|
|
23
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
24
|
+
const protocol = server.server;
|
|
25
|
+
const previousTransportClose = protocol.onclose;
|
|
26
|
+
let listenersInstalled = false;
|
|
27
|
+
let terminalHandlerInstalled = false;
|
|
28
|
+
const onSigint = () => requestFromRuntime("sigint");
|
|
29
|
+
const onSigterm = () => requestFromRuntime("sigterm");
|
|
30
|
+
const onStdinEnd = () => requestFromRuntime("stdin_eof");
|
|
31
|
+
const onStdinClose = () => requestFromRuntime("stdin_eof");
|
|
32
|
+
const onTransportClose = () => {
|
|
33
|
+
previousTransportClose?.();
|
|
34
|
+
requestFromRuntime("transport_closed");
|
|
35
|
+
};
|
|
36
|
+
const removeLifecycleListeners = () => {
|
|
37
|
+
if (listenersInstalled) {
|
|
38
|
+
process.off("SIGINT", onSigint);
|
|
39
|
+
process.off("SIGTERM", onSigterm);
|
|
40
|
+
process.stdin.off("end", onStdinEnd);
|
|
41
|
+
process.stdin.off("close", onStdinClose);
|
|
42
|
+
listenersInstalled = false;
|
|
43
|
+
}
|
|
44
|
+
if (protocol.onclose === onTransportClose)
|
|
45
|
+
protocol.onclose = previousTransportClose;
|
|
46
|
+
};
|
|
47
|
+
const coordinator = new ShutdownCoordinator({
|
|
48
|
+
drainTimeoutMs: runtimePolicy.shutdownDrainTimeoutMs,
|
|
49
|
+
stopAdmissionAndCancel: () => {
|
|
50
|
+
runtimeActivity.beginShutdown();
|
|
51
|
+
return beginProjectShutdown();
|
|
52
|
+
},
|
|
53
|
+
getDrainSnapshot: getProjectRuntimeShutdownSnapshot,
|
|
54
|
+
waitForDrain: async () => {
|
|
55
|
+
await Promise.all([waitForProjectOperationsToDrain(), runtimeActivity.waitForIdle()]);
|
|
56
|
+
},
|
|
57
|
+
waitForCompletionCriticalOperationsToDrain: waitForProjectCompletionCriticalOperationsToDrain,
|
|
58
|
+
closeMcp: async () => {
|
|
59
|
+
await server.close();
|
|
60
|
+
},
|
|
61
|
+
closeProjects: closeDrainedProjectSessions,
|
|
62
|
+
emitIncompleteShutdown: emitIncompleteShutdownEvent,
|
|
63
|
+
onCompletionCriticalWait: options.onCompletionCriticalWait,
|
|
64
|
+
});
|
|
65
|
+
let lifecycleCompletion;
|
|
66
|
+
function requestShutdown(trigger) {
|
|
67
|
+
lifecycleCompletion ??= coordinator.shutdown(trigger).finally(removeLifecycleListeners);
|
|
68
|
+
return lifecycleCompletion;
|
|
69
|
+
}
|
|
70
|
+
function requestFromRuntime(trigger) {
|
|
71
|
+
try {
|
|
72
|
+
options.onRuntimeTrigger?.(trigger);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Runtime observation is non-authoritative and must never interrupt shutdown.
|
|
76
|
+
}
|
|
77
|
+
const completion = requestShutdown(trigger);
|
|
78
|
+
if (terminalHandlerInstalled)
|
|
79
|
+
return;
|
|
80
|
+
terminalHandlerInstalled = true;
|
|
81
|
+
void completion
|
|
82
|
+
.then((result) => {
|
|
83
|
+
if (result.state === "forced_noncritical")
|
|
84
|
+
exit(1);
|
|
85
|
+
})
|
|
86
|
+
.catch(() => {
|
|
87
|
+
emitShutdownFailureEvent();
|
|
88
|
+
exit(1);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
protocol.onclose = onTransportClose;
|
|
92
|
+
if (installProcessHandlers) {
|
|
93
|
+
process.on("SIGINT", onSigint);
|
|
94
|
+
process.on("SIGTERM", onSigterm);
|
|
95
|
+
process.stdin.once("end", onStdinEnd);
|
|
96
|
+
process.stdin.once("close", onStdinClose);
|
|
97
|
+
listenersInstalled = true;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await server.connect(transport);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
await requestShutdown("requested").catch(() => undefined);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
if (options.logStartup ?? true)
|
|
107
|
+
process.stderr.write("ast-mcp-server running over stdio.\n");
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
shutdown: (trigger = "requested") => requestShutdown(trigger),
|
|
110
|
+
shutdownGraceMs: runtimePolicy.shutdownDrainTimeoutMs,
|
|
111
|
+
snapshot: () => {
|
|
112
|
+
const runtime = runtimeActivity.snapshot(), project = getProjectRuntimeShutdownSnapshot();
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
runtime_admission: runtime.admission,
|
|
115
|
+
active_requests: runtime.active_requests,
|
|
116
|
+
active_sends: runtime.active_sends,
|
|
117
|
+
project_admission: project.admission,
|
|
118
|
+
session_count: project.session_count,
|
|
119
|
+
active_operations: project.active_operations,
|
|
120
|
+
queued_operations: project.queued_operations,
|
|
121
|
+
completion_critical_operations: project.completion_critical_operations,
|
|
122
|
+
mutation_history: project.mutation_history,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
package/docs/support.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# Support policy
|
|
2
2
|
|
|
3
|
-
This document defines the supported platform, runtime, persistence, and operational boundary for published `ast-mcp-server` v0.10.0 and the local `0.11.
|
|
3
|
+
This document defines the supported platform, runtime, persistence, and operational boundary for published `ast-mcp-server` v0.10.0 and the local `0.11.2` release candidate.
|
|
4
4
|
|
|
5
5
|
## Supported release target
|
|
6
6
|
|
|
7
7
|
| Environment | Status | Contract |
|
|
8
8
|
| ----------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------- |
|
|
9
9
|
| Linux x64 with required GNU coreutils and procfs, Node.js 22.5.0 | Historical | Immutable v0.8.1 release evidence; not the runtime floor of the published or candidate lines. |
|
|
10
|
-
| Linux x64 with required GNU coreutils and procfs, exact Node.js 22.13.0 | Supported | Published v0.10.0 and local 0.11.
|
|
11
|
-
| Linux x64 with required GNU coreutils and procfs, current Node.js 24 line | Supported | Published v0.10.0 and local 0.11.
|
|
10
|
+
| Linux x64 with required GNU coreutils and procfs, exact Node.js 22.13.0 | Supported | Published v0.10.0 and local 0.11.2 matrices, each bound to its own package and tree bytes. |
|
|
11
|
+
| Linux x64 with required GNU coreutils and procfs, current Node.js 24 line | Supported | Published v0.10.0 and local 0.11.2 matrices, each bound to its own package and tree bytes. |
|
|
12
12
|
| Other Linux architectures or systems without the required filesystem features | Unverified | Not supported until equivalent architecture, mutation, and filesystem-publication gates pass. |
|
|
13
13
|
| macOS | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass. |
|
|
14
14
|
| Windows | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass. |
|
|
15
15
|
| Remote, untrusted, or multi-tenant service use | Unsupported | No network authentication, sandbox, or tenant-isolation boundary is provided. |
|
|
16
16
|
|
|
17
|
-
The local `0.11.
|
|
17
|
+
The local `0.11.2` release candidate requires Node.js `>=22.13.0`; its matrix targets exact Node.js 22.13.0 and the governed Node.js 24 major. Published v0.10.0 retains its immutable `>=22.13.0` metadata and exact Node.js 22.13.0/24 release evidence. The earlier v0.8.1 Node.js 22.5.0/24 matrix remains historical evidence for those bytes only. A future runtime satisfying an engine range is not automatically a verified release target.
|
|
18
18
|
|
|
19
19
|
The checked-evidence freezer requires GNU coreutils 9.7 `mv` with `--update=none-fail`, `--no-copy`, and `--no-target-directory`. Managed setup-file mutation additionally requires that same `mv` with `--exchange`, GNU coreutils `ln -L -T`, procfs descriptor paths at `/proc/self/fd`, `O_DIRECTORY`, and `O_NOFOLLOW`. It fails closed when any verified Linux primitive is unavailable. Equivalent publication, exchange, descriptor-link, and descriptor-relative mutation semantics have not been verified on other operating systems or architectures.
|
|
20
20
|
|
|
@@ -32,9 +32,25 @@ The server does not provide:
|
|
|
32
32
|
|
|
33
33
|
Operation locks coordinate cooperating same-user processes. They do not stop editors, network filesystems, or hostile same-user writers. Report-set freezer coordination likewise assumes cooperating same-UID processes and cannot defend checked evidence against a malicious process with the same filesystem authority. The freezer guarantees no-replace atomic visibility of the checked directory; it does not claim persistence across sudden power loss because parent-directory `fsync` durability is not established.
|
|
34
34
|
|
|
35
|
+
## Optional supervised compiler worker
|
|
36
|
+
|
|
37
|
+
The default and rollback remain `AST_COMPILER_WORKER_MODE=in_process`. Explicit `supervised` mode keeps one lightweight stdio parent connected to one disposable compiler child per connection:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
AST_COMPILER_WORKER_MODE=supervised \
|
|
41
|
+
AST_COMPILER_WORKER_IDLE_TTL_MS=60000 \
|
|
42
|
+
ast-mcp-server
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The parent waits for `ready` before replaying bounded initialization state. Generation-affine forwarding and cancellation reject stale settlements and do not retry forwarded calls. Recycling requires stable parent and child quiescence; mutation history, live leases, and completion-critical work keep the child pinned. Parent death closes admission and permits only completion-critical drain before exit.
|
|
46
|
+
|
|
47
|
+
Set the TTL to `0` to disable recycling without removing the relay, or select `in_process` to remove the worker boundary. No shared daemon, pool, cross-client deduplication, default promotion, or automatic mutation-plan repair is provided.
|
|
48
|
+
|
|
49
|
+
The scoped Linux evidence passed on exact Node.js 22.13.0 and the governed Node.js 24 line: three parents each completed three load/idle/respawn cycles with at least 80% PSS-delta reclamation and no upward trend. Respawns retained the compiler fingerprint, recorded six SQLite hits, reused exactly 400 files, rebuilt zero, and returned equivalent reads. Diagnostics remained bounded and redacted, and parent-death inspection found zero orphan processes. These results do not establish support for macOS, Windows, or other Linux environments.
|
|
50
|
+
|
|
35
51
|
## Symbol-index persistence
|
|
36
52
|
|
|
37
|
-
Published v0.10.0 and the local `0.11.
|
|
53
|
+
Published v0.10.0 and the local `0.11.2` release candidate select native SQLite when the persistence setting is absent or explicitly `enabled`; operators requiring no persistent index state must set `disabled` explicitly:
|
|
38
54
|
|
|
39
55
|
```bash
|
|
40
56
|
AST_SYMBOL_INDEX_PERSISTENCE=disabled \
|
|
@@ -64,7 +80,7 @@ Both commands are bounded and path-free. Clear preflights the tree, refuses unsa
|
|
|
64
80
|
|
|
65
81
|
## Production-readiness acceptance
|
|
66
82
|
|
|
67
|
-
The four retained Linux x64 production-readiness reports cover `ast-mcp-server` and `x-scraper` under Node.js 22.5.0 and Node.js 24 for package version 0.6.0, commit `2d0b21bbb80fae1acfca6a85d5891d87e68b59c1`, and tree `af931d49769fabdf06f623965a6cfe1f9afb8a81`. They are historical integration evidence only: they are not reattributed to v0.8.1, published-next v0.9.0 or v0.9.1, the local 0.9.2 candidate, published v0.10.0, published-next v0.11.0, or the local v0.11.
|
|
83
|
+
The four retained Linux x64 production-readiness reports cover `ast-mcp-server` and `x-scraper` under Node.js 22.5.0 and Node.js 24 for package version 0.6.0, commit `2d0b21bbb80fae1acfca6a85d5891d87e68b59c1`, and tree `af931d49769fabdf06f623965a6cfe1f9afb8a81`. They are historical integration evidence only: they are not reattributed to v0.8.1, published-next v0.9.0 or v0.9.1, the local 0.9.2 candidate, published v0.10.0, published-next v0.11.0 or v0.11.1, or the local v0.11.2 release candidate. The exact 0.9.0 and 0.9.1 release matrices remain historical evidence for their immutable published-next bytes, the exact 0.9.2 matrix remains historical local-candidate evidence, the exact 0.10.0 matrix remains historical published-release evidence, and the exact 0.11.0 and 0.11.1 matrices remain historical evidence for their immutable published-next bytes before public-registry verification exposed their distinct verifier defects. A separate exact 0.11.2 release matrix is the release-candidate gate for current repository bytes under both supported runtime lines, including supervised-worker lifecycle and reclamation evidence, deterministic CLI project-discovery fixture setup, local-registry managed-asset verification, and the public copied-runner dependency-closure regression; none of these matrices rebinds the older retained reports. MCP-PROD-404 remains normative for admission:
|
|
68
84
|
|
|
69
85
|
- zero semantic mismatches across disabled, cold, warm, restart, and rollback reads;
|
|
70
86
|
- byte-identical real-repository status and worktree trees;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ast-mcp-server",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Structural code MCP server and batch CLI for compact TypeScript/JavaScript reads and hash-bound AST edits.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsc",
|
|
51
51
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
52
|
-
"test": "vitest run",
|
|
52
|
+
"test": "vitest run --exclude test/supervised-worker.integration.test.ts && yarn build && vitest run test/supervised-worker.integration.test.ts --no-file-parallelism",
|
|
53
53
|
"test:watch": "vitest",
|
|
54
54
|
"test:mcp": "yarn build && node scripts/mcp-smoke.mjs",
|
|
55
55
|
"test:lifecycle": "yarn build && node scripts/mcp-lifecycle-smoke.mjs",
|
|
@@ -97,5 +97,5 @@
|
|
|
97
97
|
"typescript-eslint": "^8.65.0",
|
|
98
98
|
"vitest": "^4.1.10"
|
|
99
99
|
},
|
|
100
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "f7b429670093bd9f43195979e395ccf418eb4ba2"
|
|
101
101
|
}
|
|
@@ -18,7 +18,7 @@ Cargar este skill antes de navegar semánticamente, analizar impacto o referenci
|
|
|
18
18
|
|
|
19
19
|
No forzar un roundtrip AST para Markdown, configuración, comentarios ni una edición textual trivial en un archivo ya conocido. Si las tools `ast_*` no están disponibles, declarar el fallback y no presentar búsquedas textuales como evidencia compiler-backed.
|
|
20
20
|
|
|
21
|
-
The local 0.11.
|
|
21
|
+
The local 0.11.2 release candidate requires Node.js `>=22.13.0` and is verified with exact Node.js 22.13.0 plus the governed Node.js 24 line. Published v0.10.0 retains its immutable Node.js 22.13.0/24 release evidence.
|
|
22
22
|
|
|
23
23
|
## Hard Rules
|
|
24
24
|
|