@minhspark/codex-mcp-bridge 1.13.9 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.mjs CHANGED
@@ -4,6 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { z } from "zod";
5
5
  import path from "node:path";
6
6
  import { realpathSync } from "node:fs";
7
+ import { AsyncLocalStorage } from "node:async_hooks";
7
8
 
8
9
  import { CodexAppServerClient, writerLockWarning } from "./app-server-client.mjs";
9
10
  import {
@@ -26,10 +27,13 @@ import { DesktopTaskDelivery, DESKTOP_TOOL_BUDGET_MS } from "./thread-delivery.m
26
27
  import { desktopTasksConfigured } from "./native-relay.mjs";
27
28
  import { exitForVersionRequest } from "./cli-version.mjs";
28
29
  import { createRuntimeState } from "./runtime-state.mjs";
30
+ import { assertRoutingReload, clientReloadReason, createReloadControl } from "./reload-control.mjs";
31
+ import { accountIdentity, assertAccountIdentity, publicAccountState, readBridgeAccounts, requireBridgeAccounts } from "./bridge-account-context.mjs";
32
+ import { assertClaudeSenderContext, readClaudeSenderContext, requireClaudeSenderContext } from "./claude-sender-context.mjs";
29
33
 
30
34
  exitForVersionRequest(import.meta.url);
31
35
 
32
- const VERSION = "1.13.9";
36
+ const VERSION = "1.15.1";
33
37
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
34
38
 
35
39
  /**
@@ -50,13 +54,61 @@ const RELEASE_TURN_STATUSES = TERMINAL_TURN_STATUSES;
50
54
  const security = new BridgeSecurityPolicy();
51
55
  const desktopTasksEnabled = desktopTasksConfigured();
52
56
  const runtime = createRuntimeState({ configuration: desktopTasksConfigured });
53
- const desktopTasks = new DesktopTaskDelivery({ security });
57
+ const desktopOperation = new AsyncLocalStorage();
58
+ const desktopTasks = new DesktopTaskDelivery({ security, beforeRequest: beforeDesktopRequest, accountContext: () => desktopOperation.getStore()?.accounts });
59
+
60
+ async function assertDesktopOperation(context, { verifyProcess = false } = {}) {
61
+ if (!context || context.diagnostic) return;
62
+ runtime.assertCurrent();
63
+ const accounts = readBridgeAccounts();
64
+ assertAccountIdentity(context.accounts, accounts);
65
+ await assertClaudeSenderContext(context.caller, {
66
+ account: accounts.claude,
67
+ ...(!verifyProcess ? { readAncestry: async () => context.caller.lineage } : {}),
68
+ });
69
+ assertAccountIdentity(context.accounts);
70
+ }
71
+
72
+ async function beforeDesktopRequest({ operation, args, phase }) {
73
+ const context = desktopOperation.getStore();
74
+ if (context?.diagnostic) return;
75
+ if (!context) throw new Error("Desktop operations require a verified calling Code session.");
76
+ const mutating = ["create_thread", "send_message_to_thread", "set_thread_title", "navigate_to_codex_page"].includes(operation);
77
+ await assertDesktopOperation(context, { verifyProcess: mutating && phase === "write" });
78
+ if (mutating) {
79
+ context.dispatched = true;
80
+ if (args.threadId) context.threadId = args.threadId;
81
+ }
82
+ }
83
+
84
+ function withheldDesktopResult(error, context) {
85
+ if (context.dispatched) reload.defer("A Desktop operation was dispatched without a fully verified outcome");
86
+ const reason = (error?.message ?? "The calling session or accounts could not be reverified.").replace(/\s*No message was sent\./g, "");
87
+ return {
88
+ ...textResult(`${reason} ${context.dispatched ? "A Desktop operation may already have been dispatched. Its original destination and any creation receipt were preserved." : "No Desktop mutation was dispatched."} Reply content was withheld. Inspect the original task and do not resend automatically.`, true),
89
+ structuredContent: { accountContext: context.accounts, operation: { state: context.dispatched ? "dispatched_outcome_unverified" : "blocked", threadId: context.threadId ?? null } },
90
+ };
91
+ }
54
92
 
55
93
  const client = desktopTasksEnabled ? null : new CodexAppServerClient({
56
94
  clientInfo: { name: "codex-mcp-bridge", title: "Codex MCP Bridge", version: VERSION },
57
95
  log,
58
96
  });
59
97
 
98
+ const reload = createReloadControl({
99
+ entry: "index.mjs",
100
+ inspect: () => desktopTasks.threadOperations.size ? "Desktop task operations are still active" : clientReloadReason(client),
101
+ quiesce: () => { client?.close(); },
102
+ exportState: () => ({ desktopTasksEnabled, ownedThreadIds: [...security.ownedThreadIds] }),
103
+ restore: (state) => {
104
+ assertRoutingReload(state.desktopTasksEnabled, desktopTasksEnabled);
105
+ if (!Array.isArray(state.ownedThreadIds)
106
+ || state.ownedThreadIds.some((id) => typeof id !== "string" || !id.trim())
107
+ || new Set(state.ownedThreadIds).size !== state.ownedThreadIds.length) throw new Error("Invalid or incompatible Codex worker reload state");
108
+ security.ownedThreadIds = new Set(state.ownedThreadIds);
109
+ },
110
+ });
111
+
60
112
  const textResult = (text, isError = false) => ({
61
113
  content: [{ type: "text", text }],
62
114
  ...(isError ? { isError: true } : {}),
@@ -116,7 +168,7 @@ function threadNameFor({ cwd, prompt, name }) {
116
168
  }
117
169
 
118
170
  async function delegateDesktopTask({ cwd, prompt, name, model, effort, timeoutSec, openInApp, waitForReply = true }) {
119
- const deadline = Date.now() + Math.min((timeoutSec ?? 40) * 1000, DESKTOP_TOOL_BUDGET_MS);
171
+ const deadline = desktopOperation.getStore()?.deadline ?? Date.now() + Math.min((timeoutSec ?? 40) * 1000, DESKTOP_TOOL_BUDGET_MS);
120
172
  const workspace = resolveWorkspacePath(cwd);
121
173
  const created = await desktopTasks.create({
122
174
  cwd: workspace.path, prompt, name: threadNameFor({ cwd: workspace.path, prompt, name }),
@@ -293,16 +345,39 @@ const server = new McpServer(
293
345
  function registerTool(name, definition, handler) {
294
346
  server.registerTool(name, definition, async (...args) => {
295
347
  try {
348
+ return await reload.run(async () => {
296
349
  if (!definition.annotations?.readOnlyHint) runtime.assertCurrent();
297
350
  if (name === "codex_bridge_status") {
298
351
  const state = runtime.status();
299
352
  if (!state.current) return { ...failure(new Error(`${state.reason}; reconnect this MCP server in the existing task.`)), structuredContent: { runtime: state } };
300
- const result = await handler(...args);
353
+ const accounts = desktopTasksEnabled ? readBridgeAccounts() : null;
354
+ const result = await desktopOperation.run({ diagnostic: true, accounts: accountIdentity(accounts) }, () => handler(...args));
301
355
  result.content.push({ type: "text", text: `runtime pid: ${state.pid}\nloaded source: ${state.revision}\nruntime state: current` });
302
356
  result.structuredContent = { ...result.structuredContent, runtime: state };
357
+ if (desktopTasksEnabled) {
358
+ result.structuredContent.accounts = { claude: publicAccountState(accounts.claude), codex: publicAccountState(accounts.codex) };
359
+ }
303
360
  return result;
304
361
  }
362
+ if (desktopTasksEnabled) {
363
+ const deadline = Date.now() + Math.min((args[0]?.timeoutSec ?? 40) * 1000, DESKTOP_TOOL_BUDGET_MS);
364
+ const accounts = readBridgeAccounts();
365
+ const identity = requireBridgeAccounts(accounts);
366
+ const caller = requireClaudeSenderContext(await readClaudeSenderContext({ account: accounts.claude }));
367
+ const context = { accounts: identity, caller, dispatched: false, deadline };
368
+ return await desktopOperation.run(context, async () => {
369
+ try {
370
+ await assertDesktopOperation(context);
371
+ const result = await handler(...args);
372
+ await assertDesktopOperation(context);
373
+ return result;
374
+ } catch (error) {
375
+ return withheldDesktopResult(error, context);
376
+ }
377
+ });
378
+ }
305
379
  return await handler(...args);
380
+ });
306
381
  } catch (err) {
307
382
  return failure(err);
308
383
  }
@@ -406,7 +481,10 @@ registerTool(
406
481
  title: "Send a prompt to a Codex thread",
407
482
  description:
408
483
  "Send a prompt as a new user turn inside an existing Codex thread and wait for Codex to answer. " +
409
- "The thread keeps its full history, cwd and model. Use list_codex_threads first if you do not know the threadId.",
484
+ "The thread keeps its full history, cwd and model. Use list_codex_threads first if you do not know the threadId. " +
485
+ "Desktop-owned tasks must use Desktop native delivery; an open task is a valid destination. " +
486
+ "If legacy delivery reports an active writer, inspect codex_bridge_status and repair the native relay/configuration. " +
487
+ "Do not close the task, create a replacement, or ask the user to copy the message manually.",
410
488
  inputSchema: {
411
489
  threadId: z.string().describe("Codex thread id (UUID) - get it from list_codex_threads"),
412
490
  prompt: z.string().describe("The message to send to Codex, exactly as a user would type it"),
@@ -441,7 +519,7 @@ registerTool(
441
519
  },
442
520
  },
443
521
  async ({ threadId, prompt, timeoutSec, cwd, model, effort, name, openInApp, releaseAfterTurn }) => {
444
- const deadline = desktopTasksEnabled ? Date.now() + Math.min((timeoutSec ?? 40) * 1000, DESKTOP_TOOL_BUDGET_MS) : undefined;
522
+ const deadline = desktopTasksEnabled ? desktopOperation.getStore()?.deadline ?? Date.now() + Math.min((timeoutSec ?? 40) * 1000, DESKTOP_TOOL_BUDGET_MS) : undefined;
445
523
  return (desktopTasksEnabled ? desktopTasks : client).withThread(threadId, async () => {
446
524
  const notes = [];
447
525
  const shouldOpen = openInApp ?? DEFAULT_OPEN_IN_APP;
@@ -670,7 +748,7 @@ registerTool(
670
748
  async ({ threadId, limit }) => {
671
749
  try {
672
750
  if (desktopTasksEnabled) {
673
- const deadline = Date.now() + DESKTOP_TOOL_BUDGET_MS;
751
+ const deadline = desktopOperation.getStore()?.deadline ?? Date.now() + DESKTOP_TOOL_BUDGET_MS;
674
752
  await desktopTasks.inspect(threadId, undefined, { deadline });
675
753
  const response = await desktopTasks.request("read_thread", { threadId, hostId: "local", turnLimit: Math.min(limit ?? 10, 10) }, { deadline });
676
754
  return textResult(JSON.stringify(response, null, 2));
@@ -758,9 +836,12 @@ registerTool(
758
836
  async ({ threadId, background }) => {
759
837
  try {
760
838
  if (desktopTasksEnabled) {
761
- const deadline = Date.now() + DESKTOP_TOOL_BUDGET_MS;
839
+ const deadline = desktopOperation.getStore()?.deadline ?? Date.now() + DESKTOP_TOOL_BUDGET_MS;
762
840
  await desktopTasks.inspect(threadId, undefined, { deadline });
763
- if (background) await openThreadInCodexApp(threadId, { activate: false });
841
+ if (background) {
842
+ await beforeDesktopRequest({ operation: "navigate_to_codex_page", args: { threadId }, phase: "write" });
843
+ await openThreadInCodexApp(threadId, { activate: false });
844
+ }
764
845
  else await desktopTasks.open(threadId, { deadline });
765
846
  return textResult(`Opened ${codexThreadUrl(threadId)} in Codex Desktop.`);
766
847
  }
@@ -894,4 +975,5 @@ registerTool(
894
975
 
895
976
  const transport = new StdioServerTransport();
896
977
  await server.connect(transport);
978
+ reload.listen();
897
979
  log(desktopTasksEnabled ? `ready on ${PLATFORM_LABEL} (Codex Desktop only; external app-server disabled)` : `ready on ${PLATFORM_LABEL} (app-server endpoint: ${client.url}, codex: ${client.codexBin})`);
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ import { fork } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createReleaseSnapshot, sourceRevision } from "./release-snapshot.mjs";
7
+ import { desktopTasksConfigured } from "./native-relay.mjs";
8
+
9
+ const ENTRIES = new Set(["index.mjs", "claude-bridge.mjs", "native-relay-companion.mjs"]);
10
+ const MAX_FRAME_BYTES = 16 * 1024 * 1024;
11
+ const MAX_QUEUE = 256;
12
+ const hasId = (message) => Object.hasOwn(message, "id");
13
+ const idKey = (id) => JSON.stringify(id);
14
+ const errorMessage = (id, message) => ({ jsonrpc: "2.0", id, error: { code: -32603, message } });
15
+
16
+ function lines(stream, receive, failed) {
17
+ let buffer = Buffer.alloc(0);
18
+ stream.on("data", (chunk) => {
19
+ buffer = Buffer.concat([buffer, chunk]);
20
+ if (buffer.length > MAX_FRAME_BYTES) { failed(new Error("MCP frame exceeds the buffer limit")); return; }
21
+ let index;
22
+ while ((index = buffer.indexOf(10)) !== -1) {
23
+ const frame = buffer.subarray(0, index).toString("utf8").trim();
24
+ buffer = buffer.subarray(index + 1);
25
+ if (!frame) continue;
26
+ try { receive(JSON.parse(frame)); }
27
+ catch (error) { failed(error); }
28
+ }
29
+ });
30
+ }
31
+
32
+ class Worker {
33
+ constructor(release, entry, env, handlers) {
34
+ this.release = release;
35
+ this.handlers = handlers;
36
+ this.rpcPending = new Map();
37
+ this.controlPending = new Map();
38
+ this.clientPending = new Map();
39
+ this.sequence = 0;
40
+ this.retiring = false;
41
+ this.closed = false;
42
+ this.process = fork(path.join(release.directory, "src", entry), [], {
43
+ execPath: process.execPath, execArgv: [], cwd: process.cwd(), windowsHide: true,
44
+ env: { ...env, CODEX_BRIDGE_WORKER: "1", CODEX_BRIDGE_STAGED: "1" },
45
+ stdio: ["pipe", "pipe", "pipe", "ipc"], serialization: "json",
46
+ });
47
+ this.process.stderr.on("data", (data) => process.stderr.write(data));
48
+ this.process.stdin.on("error", (error) => this.fail(error));
49
+ this.process.on("error", (error) => this.fail(error));
50
+ this.process.on("exit", (code, signal) => this.fail(new Error(`Bridge worker exited (${signal ?? code})`)));
51
+ this.process.on("message", (message) => {
52
+ if (message?.type !== "bridge:control-result") return;
53
+ const pending = this.controlPending.get(message.id);
54
+ if (!pending) return;
55
+ this.controlPending.delete(message.id);
56
+ clearTimeout(pending.timer);
57
+ if (message.error) pending.reject(new Error(message.error));
58
+ else pending.resolve(message.result);
59
+ });
60
+ lines(this.process.stdout, (message) => {
61
+ if (!message || message.jsonrpc !== "2.0") throw new Error("Invalid worker JSON-RPC message");
62
+ if (!message.method && hasId(message) && this.rpcPending.has(message.id)) {
63
+ const pending = this.rpcPending.get(message.id);
64
+ this.rpcPending.delete(message.id);
65
+ clearTimeout(pending.timer);
66
+ if (message.error) pending.reject(new Error(message.error.message));
67
+ else pending.resolve(message.result);
68
+ } else handlers.message(this, message);
69
+ }, (error) => this.fail(error));
70
+ }
71
+
72
+ write(message) {
73
+ if (this.closed) throw new Error("The bridge worker is unavailable");
74
+ this.process.stdin.write(`${JSON.stringify(message)}\n`);
75
+ }
76
+
77
+ rpc(method, params, timeout = 15000) {
78
+ const id = `supervisor:${++this.sequence}`;
79
+ return new Promise((resolve, reject) => {
80
+ const timer = setTimeout(() => { this.rpcPending.delete(id); reject(new Error(`Candidate ${method} timed out`)); }, timeout);
81
+ this.rpcPending.set(id, { resolve, reject, timer });
82
+ this.write({ jsonrpc: "2.0", id, method, ...(params === undefined ? {} : { params }) });
83
+ });
84
+ }
85
+
86
+ control(action, data, timeout = 15000) {
87
+ const id = ++this.sequence;
88
+ return new Promise((resolve, reject) => {
89
+ const timer = setTimeout(() => { this.controlPending.delete(id); reject(new Error(`Worker ${action} timed out`)); }, timeout);
90
+ this.controlPending.set(id, { resolve, reject, timer });
91
+ this.process.send({ type: "bridge:control", id, action, data }, (error) => {
92
+ if (!error) return;
93
+ const pending = this.controlPending.get(id);
94
+ if (pending) { clearTimeout(timer); this.controlPending.delete(id); reject(error); }
95
+ });
96
+ });
97
+ }
98
+
99
+ fail(error) {
100
+ if (this.closed) return;
101
+ this.closed = true;
102
+ for (const pending of [...this.rpcPending.values(), ...this.controlPending.values()]) {
103
+ clearTimeout(pending.timer);
104
+ pending.reject(error);
105
+ }
106
+ this.rpcPending.clear();
107
+ this.controlPending.clear();
108
+ this.handlers.exit(this, error);
109
+ }
110
+
111
+ async stop() {
112
+ this.retiring = true;
113
+ if (this.process.exitCode !== null || this.process.signalCode) return;
114
+ await new Promise((resolve) => {
115
+ const timer = setTimeout(() => { this.process.kill(); resolve(); }, 1000);
116
+ this.process.once("exit", () => { clearTimeout(timer); resolve(); });
117
+ this.process.stdin.end();
118
+ if (this.process.connected) this.process.disconnect();
119
+ });
120
+ }
121
+ }
122
+
123
+ export async function runSupervisor(entry, options = {}) {
124
+ if (!ENTRIES.has(entry)) throw new Error("Expected index.mjs, claude-bridge.mjs, or native-relay-companion.mjs");
125
+ const root = fs.realpathSync.native(options.root ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
126
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
127
+ process.stdout.write(`${JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version}\n`);
128
+ return;
129
+ }
130
+ const log = (message) => process.stderr.write(`[bridge-supervisor] ${message}\n`);
131
+ const output = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
132
+ const env = { ...process.env, CODEX_BRIDGE_SOURCE_ROOT: root };
133
+ const routingConfiguration = () => entry === "native-relay-companion.mjs" ? null : desktopTasksConfigured(env);
134
+ if (entry === "native-relay-companion.mjs" && !env.CODEX_APP_TOOLS_PIPE_PATH) {
135
+ const { resolveNativeToolsPipePath } = await import("./native-relay.mjs");
136
+ const pipe = await resolveNativeToolsPipePath();
137
+ if (pipe) env.CODEX_APP_TOOLS_PIPE_PATH = pipe;
138
+ }
139
+ const pollMs = Math.max(100, Number(env.CODEX_BRIDGE_RELOAD_POLL_MS) || 1000);
140
+ const settleMs = Math.max(100, Number(env.CODEX_BRIDGE_RELOAD_SETTLE_MS) || 1500);
141
+ let active;
142
+ let initializeParams;
143
+ let initializeResult;
144
+ let ready = false;
145
+ let switching = false;
146
+ let stopping = false;
147
+ let failure = null;
148
+ let sequence = 0;
149
+ let observed = null;
150
+ let observedRouting = null;
151
+ let observedAt = 0;
152
+ let retryAt = 0;
153
+ let reason = null;
154
+ let reloads = 0;
155
+ const queue = [];
156
+ const requests = new Map();
157
+ const reverse = new Map();
158
+ const workers = new Set();
159
+ const diagnostics = () => ({ enabled: true, supervisorPid: process.pid, workerPid: active?.process.pid ?? null,
160
+ revision: active?.release.revision ?? null, availableRevision: observed,
161
+ routingConfiguration: active?.routingConfiguration ?? null, availableRoutingConfiguration: observedRouting,
162
+ pending: Boolean(observed && (observed !== active?.release.revision || observedRouting !== active?.routingConfiguration)),
163
+ state: failure ? "failed" : switching ? "reloading" : reason ? "deferred" : "current", reason: failure ?? reason, reloads });
164
+
165
+ const handlers = {
166
+ message(worker, message) {
167
+ if (message.method && hasId(message)) {
168
+ const id = `bridge-server:${++sequence}`;
169
+ reverse.set(idKey(id), { worker, id: message.id });
170
+ output({ ...message, id });
171
+ return;
172
+ }
173
+ if (message.method) {
174
+ if (worker === active) output(message);
175
+ return;
176
+ }
177
+ const pending = worker.clientPending.get(message.id);
178
+ if (!pending) return;
179
+ worker.clientPending.delete(message.id);
180
+ requests.delete(idKey(pending.id));
181
+ const respond = async () => {
182
+ if (pending.method === "initialize" && !message.error) {
183
+ initializeResult = message.result;
184
+ await worker.control("activate");
185
+ ready = true;
186
+ }
187
+ if (message.result && ["codex_bridge_status", "claude_bridge_status", "native_relay_status"].includes(pending.tool)) {
188
+ message.result.structuredContent = { ...message.result.structuredContent, autoReload: diagnostics() };
189
+ message.result.content = [...(message.result.content ?? []), { type: "text", text: `automatic reload: ${diagnostics().state}\nsupervisor pid: ${process.pid}\nworker pid: ${worker.process.pid}\nreload count: ${reloads}${reason ? `\nreload detail: ${reason}` : ""}` }];
190
+ }
191
+ output({ ...message, id: pending.id });
192
+ };
193
+ void respond().catch((error) => { failure = error.message; output(errorMessage(pending.id, error.message)); });
194
+ },
195
+ exit(worker, error) {
196
+ for (const [id, pending] of worker.clientPending) {
197
+ requests.delete(idKey(pending.id));
198
+ output(errorMessage(pending.id, `${error.message}. The operation may already have been dispatched; it was not retried. Retain the original task and receipts.`));
199
+ worker.clientPending.delete(id);
200
+ }
201
+ for (const [id, pending] of reverse) if (pending.worker === worker) reverse.delete(id);
202
+ if (worker === active && !worker.retiring && !stopping) { failure = `${error.message}; automatic replay is disabled because delivery state may be unknown`; log(failure); }
203
+ },
204
+ };
205
+ const spawn = (release, routing) => {
206
+ if (stopping) throw new Error("The MCP client disconnected before activation");
207
+ const worker = new Worker(release, entry, env, handlers);
208
+ worker.routingConfiguration = routing;
209
+ workers.add(worker);
210
+ return worker;
211
+ };
212
+ const check = async () => {
213
+ if (switching || !ready || stopping || failure) return;
214
+ let revision;
215
+ let routing;
216
+ try { revision = sourceRevision(root); routing = routingConfiguration(); }
217
+ catch (error) { reason = `Installation is not ready: ${error.message}`; return; }
218
+ if (observed !== revision || observedRouting !== routing) { observed = revision; observedRouting = routing; observedAt = Date.now(); retryAt = 0; }
219
+ if (revision === active.release.revision && routing === active.routingConfiguration) { reason = null; return; }
220
+ if (Date.now() - observedAt < settleMs || Date.now() < retryAt) { reason ??= "Waiting for a complete, stable installation"; return; }
221
+ if (active.clientPending.size || reverse.size) { reason = "A request is still running; its worker is retained"; return; }
222
+ switching = true;
223
+ let candidate;
224
+ let quiesced = false;
225
+ const previous = active;
226
+ try {
227
+ const state = await previous.control("inspect");
228
+ if (!state.reloadable) { reason = state.reason ?? "Pending deliveries prevent a safe reload"; return; }
229
+ const release = createReleaseSnapshot(root, { expectedRevision: revision });
230
+ candidate = spawn(release, routing);
231
+ const initialized = await candidate.rpc("initialize", initializeParams);
232
+ if (initialized.protocolVersion !== initializeResult.protocolVersion || JSON.stringify(initialized.capabilities) !== JSON.stringify(initializeResult.capabilities)) {
233
+ throw new Error("The new MCP protocol or capabilities require an explicit client reconnect");
234
+ }
235
+ candidate.write({ jsonrpc: "2.0", method: "notifications/initialized" });
236
+ const catalog = await candidate.rpc("tools/list", {});
237
+ if (!Array.isArray(catalog.tools) || !catalog.tools.length || catalog.nextCursor) throw new Error("The candidate tool catalog is incomplete");
238
+ const oldCatalog = await previous.rpc("tools/list", {});
239
+ if (sourceRevision(root) !== revision) throw new Error("The installation changed before activation");
240
+ if (routingConfiguration() !== routing) throw new Error("The routing configuration changed before activation");
241
+ quiesced = true;
242
+ const exported = await previous.control("quiesce");
243
+ await candidate.control("restore", exported.state);
244
+ await candidate.control("activate");
245
+ if (stopping) throw new Error("The MCP client disconnected during activation");
246
+ active = candidate;
247
+ candidate = null;
248
+ reloads++;
249
+ reason = null;
250
+ if (JSON.stringify(catalog) !== JSON.stringify(oldCatalog)) output({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
251
+ await previous.stop();
252
+ workers.delete(previous);
253
+ log(`activated ${entry} revision ${revision.slice(0, 12)}; client connection retained`);
254
+ } catch (error) {
255
+ reason = `Update deferred: ${error.message}`;
256
+ retryAt = Date.now() + Math.max(pollMs, 5000);
257
+ if (candidate) { await candidate.stop(); workers.delete(candidate); }
258
+ if (quiesced && active === previous && !previous.closed) {
259
+ try { await previous.control("resume"); }
260
+ catch (resumeError) { failure = `Previous runtime could not resume: ${resumeError.message}`; }
261
+ }
262
+ log(reason);
263
+ } finally {
264
+ switching = false;
265
+ for (const message of queue.splice(0)) dispatch(message);
266
+ }
267
+ };
268
+ const dispatch = (message) => {
269
+ if (stopping) return;
270
+ if (!message.method && hasId(message)) {
271
+ const pending = reverse.get(idKey(message.id));
272
+ if (pending) { reverse.delete(idKey(message.id)); pending.worker.write({ ...message, id: pending.id }); }
273
+ return;
274
+ }
275
+ if (message.method === "notifications/cancelled") {
276
+ const pending = requests.get(idKey(message.params?.requestId));
277
+ if (pending) pending.worker.write({ ...message, params: { ...message.params, requestId: pending.workerId } });
278
+ else {
279
+ const index = queue.findIndex((queued) => hasId(queued) && idKey(queued.id) === idKey(message.params?.requestId));
280
+ if (index !== -1) { const [cancelled] = queue.splice(index, 1); output({ jsonrpc: "2.0", id: cancelled.id, error: { code: -32800, message: "Request cancelled before dispatch" } }); }
281
+ }
282
+ return;
283
+ }
284
+ if (failure) { if (hasId(message)) output(errorMessage(message.id, failure)); return; }
285
+ if (switching || !active) {
286
+ const bytes = queue.reduce((size, queued) => size + Buffer.byteLength(JSON.stringify(queued)), 0);
287
+ if (queue.length >= MAX_QUEUE || bytes + Buffer.byteLength(JSON.stringify(message)) > MAX_FRAME_BYTES) { if (hasId(message)) output(errorMessage(message.id, "Reload request queue is full; no operation was dispatched")); return; }
288
+ queue.push(message);
289
+ return;
290
+ }
291
+ if (message.method === "initialize") initializeParams = message.params;
292
+ if (hasId(message)) {
293
+ if (requests.has(idKey(message.id))) { output(errorMessage(message.id, "Duplicate active JSON-RPC request ID")); return; }
294
+ const id = `client:${++sequence}`;
295
+ active.clientPending.set(id, { id: message.id, method: message.method, tool: message.params?.name });
296
+ requests.set(idKey(message.id), { worker: active, workerId: id });
297
+ active.write({ ...message, id });
298
+ } else active.write(message);
299
+ };
300
+ const shutdown = async () => {
301
+ if (stopping) return;
302
+ stopping = true;
303
+ clearInterval(timer);
304
+ await Promise.allSettled([...workers].map((worker) => worker.stop()));
305
+ process.stdin.pause();
306
+ };
307
+ const preparationStarted = performance.now();
308
+ log(`preparing ${entry} runtime`);
309
+ const initial = createReleaseSnapshot(root);
310
+ log(`prepared ${entry} runtime in ${Math.round(performance.now() - preparationStarted)} ms`);
311
+ observed = initial.revision;
312
+ observedRouting = routingConfiguration();
313
+ active = spawn(initial, observedRouting);
314
+ const timer = setInterval(() => { void check(); }, pollMs);
315
+ lines(process.stdin, (message) => {
316
+ if (!message || message.jsonrpc !== "2.0" || (!message.method && !hasId(message))) throw new Error("Invalid client JSON-RPC message");
317
+ dispatch(message);
318
+ }, (error) => { log(error.message); void shutdown(); });
319
+ process.stdin.on("end", () => { void shutdown(); });
320
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void shutdown(); });
321
+ log(`supervising ${entry}; installed updates activate after pending work completes`);
322
+ }
323
+
324
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
325
+ await runSupervisor(process.argv[2]);
326
+ }