@leo000001/codex-mcp 2.1.4 → 2.1.5

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/dist/index.js CHANGED
@@ -392,7 +392,7 @@ var DEFAULT_TERMINAL_CLEANUP_MS = 5 * 60 * 1e3;
392
392
  var CLEANUP_INTERVAL_MS = 6e4;
393
393
 
394
394
  // src/app-server/client.ts
395
- var CLIENT_VERSION = true ? "2.1.4" : "0.0.0-dev";
395
+ var CLIENT_VERSION = true ? "2.1.5" : "0.0.0-dev";
396
396
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
397
397
  var STARTUP_REQUEST_TIMEOUT = 9e4;
398
398
  var MAX_WRITE_QUEUE_BYTES = 5 * 1024 * 1024;
@@ -417,6 +417,9 @@ var AppServerClient = class extends EventEmitter {
417
417
  get supportsTurnOverrides() {
418
418
  return true;
419
419
  }
420
+ get childPid() {
421
+ return this.process?.pid ?? void 0;
422
+ }
420
423
  /**
421
424
  * Spawn codex app-server and perform initialization handshake.
422
425
  */
@@ -877,19 +880,97 @@ function stripShellNoise(delta) {
877
880
  if (cleaned.length === 0) return "";
878
881
  return cleaned.join("\n");
879
882
  }
883
+ var MAX_WAITERS_PER_SESSION = 4;
884
+ var MAX_WAIT_MS = 12e4;
880
885
  var SessionManager = class {
881
886
  sessions = /* @__PURE__ */ new Map();
882
887
  clients = /* @__PURE__ */ new Map();
883
888
  cancellationInFlight = /* @__PURE__ */ new Map();
884
889
  cleanupTimer = null;
885
890
  createClient;
891
+ /** Optional disk persistence adapter. */
892
+ persistence;
893
+ /** Track last persisted status to avoid redundant writes. */
894
+ lastPersistedStatus = /* @__PURE__ */ new Map();
895
+ /** Sessions for which a TTL warning event has already been emitted this cycle. */
896
+ ttlWarningEmitted = /* @__PURE__ */ new Set();
897
+ /** Long-poll notifiers: set of resolve callbacks waiting for any change in a session. */
898
+ sessionNotifiers = /* @__PURE__ */ new Map();
886
899
  constructor(options = {}) {
887
900
  this.createClient = options.createClient ?? (() => new AppServerClient());
901
+ this.persistence = options.persistence ?? null;
888
902
  if (!options.disableCleanup) {
889
903
  this.cleanupTimer = setInterval(() => this.cleanupSessions(), CLEANUP_INTERVAL_MS);
890
904
  if (this.cleanupTimer.unref) this.cleanupTimer.unref();
891
905
  }
892
906
  }
907
+ /**
908
+ * Ingest recovered sessions from disk into the in-memory session store.
909
+ * Marks previously-running sessions as error, preserves completed results.
910
+ */
911
+ ingestRecovered(recovered) {
912
+ for (const rec of recovered) {
913
+ if (this.sessions.has(rec.sessionId)) continue;
914
+ const VALID_STATUSES = /* @__PURE__ */ new Set([
915
+ "running",
916
+ "idle",
917
+ "waiting_approval",
918
+ "error",
919
+ "cancelled"
920
+ ]);
921
+ const wasActive = rec.meta.status === "running" || rec.meta.status === "waiting_approval";
922
+ const resolvedStatus = wasActive ? "error" : VALID_STATUSES.has(rec.meta.status) ? rec.meta.status : "error";
923
+ const now = (/* @__PURE__ */ new Date()).toISOString();
924
+ const session = {
925
+ sessionId: rec.meta.sessionId,
926
+ threadId: rec.meta.threadId,
927
+ status: resolvedStatus,
928
+ lastEventCursor: 0,
929
+ createdAt: rec.meta.createdAt,
930
+ lastActiveAt: now,
931
+ cancelledAt: rec.meta.cancelledAt,
932
+ cancelledReason: wasActive ? "Server restarted while session was active" : rec.meta.cancelledReason,
933
+ cwd: rec.meta.cwd ?? ".",
934
+ model: rec.meta.model,
935
+ approvalPolicy: rec.meta.approvalPolicy,
936
+ sandbox: rec.meta.sandbox,
937
+ eventBuffer: createEventBuffer(),
938
+ pendingRequests: /* @__PURE__ */ new Map(),
939
+ lastResult: rec.result
940
+ };
941
+ this.sessions.set(rec.sessionId, session);
942
+ if (rec.lastSeq >= 0) {
943
+ this.persistence?.setEventLogNextSeq(rec.sessionId, rec.lastSeq + 1);
944
+ }
945
+ if (wasActive) {
946
+ this.persistSessionIfChanged(session);
947
+ }
948
+ }
949
+ }
950
+ /**
951
+ * Best-effort persist session metadata to disk when status changes.
952
+ * Deduplicates writes if status hasn't changed since last persist.
953
+ */
954
+ persistSessionIfChanged(session) {
955
+ if (!this.persistence) return;
956
+ const lastStatus = this.lastPersistedStatus.get(session.sessionId);
957
+ if (lastStatus === session.status) return;
958
+ try {
959
+ this.persistence.writeSessionMeta(session);
960
+ this.lastPersistedStatus.set(session.sessionId, session.status);
961
+ } catch {
962
+ }
963
+ }
964
+ /**
965
+ * Best-effort persist result to disk.
966
+ */
967
+ persistResult(session) {
968
+ if (!this.persistence || !session.lastResult) return;
969
+ try {
970
+ this.persistence.writeResult(session.sessionId, session.lastResult);
971
+ } catch {
972
+ }
973
+ }
893
974
  // ── Session Creation ─────────────────────────────────────────────
894
975
  async createSession(prompt, cwd, spawnOpts, effort, advanced) {
895
976
  const sessionId = `sess_${randomUUID().slice(0, 12)}`;
@@ -915,9 +996,19 @@ var SessionManager = class {
915
996
  };
916
997
  this.sessions.set(sessionId, session);
917
998
  this.clients.set(sessionId, client);
999
+ try {
1000
+ this.persistence?.writeSessionMeta(session);
1001
+ } catch {
1002
+ }
918
1003
  try {
919
1004
  this.registerHandlers(sessionId, client, approvalTimeoutMs);
920
1005
  await client.start(spawnOpts);
1006
+ if (client.childPid !== void 0) {
1007
+ try {
1008
+ this.persistence?.writePidInfo(sessionId, client.childPid, spawnOpts.model);
1009
+ } catch {
1010
+ }
1011
+ }
921
1012
  const threadStartResult = await client.threadStart({
922
1013
  cwd,
923
1014
  model: spawnOpts.model,
@@ -985,6 +1076,7 @@ var SessionManager = class {
985
1076
  clearTerminalEvents(session.eventBuffer);
986
1077
  session.status = "running";
987
1078
  session.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
1079
+ this.persistSessionIfChanged(session);
988
1080
  const input = [{ type: "text", text: prompt }];
989
1081
  const resolvedCwd = overrides?.cwd ? resolveAndValidateCwd(overrides.cwd, session.cwd) : void 0;
990
1082
  const turnParams = {
@@ -1070,6 +1162,18 @@ var SessionManager = class {
1070
1162
  const session = this.getSessionOrThrow(sessionId);
1071
1163
  return includeSensitive ? toSensitiveInfo(session) : toPublicInfo(session);
1072
1164
  }
1165
+ getLastResult(sessionId) {
1166
+ return this.getSessionOrThrow(sessionId).lastResult;
1167
+ }
1168
+ getPendingActionTypes(sessionId) {
1169
+ const session = this.getSessionOrThrow(sessionId);
1170
+ const actionTypes = /* @__PURE__ */ new Set();
1171
+ for (const req of session.pendingRequests.values()) {
1172
+ if (req.resolved) continue;
1173
+ actionTypes.add(req.kind === "user_input" ? "user_input" : "approval");
1174
+ }
1175
+ return Array.from(actionTypes);
1176
+ }
1073
1177
  async cancelSession(sessionId, reason) {
1074
1178
  const existing = this.cancellationInFlight.get(sessionId);
1075
1179
  if (existing) {
@@ -1093,6 +1197,7 @@ var SessionManager = class {
1093
1197
  session.cancelledAt = now;
1094
1198
  session.lastActiveAt = now;
1095
1199
  session.cancelledReason = reason ?? "Cancelled by user";
1200
+ this.persistSessionIfChanged(session);
1096
1201
  for (const [reqId, req] of session.pendingRequests) {
1097
1202
  if (req.timeoutHandle) clearTimeout(req.timeoutHandle);
1098
1203
  if (!req.resolved && req.respond) {
@@ -1129,6 +1234,7 @@ var SessionManager = class {
1129
1234
  { status: "cancelled", reason: session.cancelledReason, turnId: cancelledTurnId },
1130
1235
  true
1131
1236
  );
1237
+ this.notifyWaiters(sessionId);
1132
1238
  if (client) {
1133
1239
  await client.destroy();
1134
1240
  this.clients.delete(sessionId);
@@ -1244,6 +1350,57 @@ var SessionManager = class {
1244
1350
  );
1245
1351
  }
1246
1352
  }
1353
+ // ── Long-poll support ────────────────────────────────────────────
1354
+ /**
1355
+ * Wait until a new event is pushed, a new pending request arrives, a status
1356
+ * change occurs, or `timeoutMs` elapses (whichever comes first).
1357
+ *
1358
+ * Rejects with an error when more than MAX_WAITERS_PER_SESSION concurrent
1359
+ * waiters are already queued for the same session.
1360
+ */
1361
+ waitForChange(sessionId, timeoutMs, signal) {
1362
+ return new Promise((resolve, reject) => {
1363
+ if (signal?.aborted) {
1364
+ resolve();
1365
+ return;
1366
+ }
1367
+ let notifiers = this.sessionNotifiers.get(sessionId);
1368
+ if (!notifiers) {
1369
+ notifiers = /* @__PURE__ */ new Set();
1370
+ this.sessionNotifiers.set(sessionId, notifiers);
1371
+ }
1372
+ if (notifiers.size >= MAX_WAITERS_PER_SESSION) {
1373
+ reject(
1374
+ new Error(
1375
+ `[codex-mcp] Too many concurrent long-poll waiters for session '${sessionId}' (max ${MAX_WAITERS_PER_SESSION})`
1376
+ )
1377
+ );
1378
+ return;
1379
+ }
1380
+ const clampedMs = Math.min(Math.max(0, timeoutMs), MAX_WAIT_MS);
1381
+ const done = () => {
1382
+ notifiers.delete(notifyFn);
1383
+ if (notifiers.size === 0) this.sessionNotifiers.delete(sessionId);
1384
+ clearTimeout(timer);
1385
+ if (signal) signal.removeEventListener("abort", onAbort);
1386
+ resolve();
1387
+ };
1388
+ const notifyFn = done;
1389
+ const timer = setTimeout(done, clampedMs);
1390
+ if (timer.unref) timer.unref();
1391
+ const onAbort = () => done();
1392
+ if (signal) signal.addEventListener("abort", onAbort, { once: true });
1393
+ notifiers.add(notifyFn);
1394
+ });
1395
+ }
1396
+ /** Resolve all waiters for a session immediately (called on any state change). */
1397
+ notifyWaiters(sessionId) {
1398
+ const notifiers = this.sessionNotifiers.get(sessionId);
1399
+ if (!notifiers || notifiers.size === 0) return;
1400
+ for (const fn of Array.from(notifiers)) {
1401
+ fn();
1402
+ }
1403
+ }
1247
1404
  // ── Event Polling ────────────────────────────────────────────────
1248
1405
  pollEvents(sessionId, cursor, maxEvents = DEFAULT_MAX_EVENTS, options = {}) {
1249
1406
  const session = this.getSessionOrThrow(sessionId);
@@ -1466,8 +1623,18 @@ var SessionManager = class {
1466
1623
  }
1467
1624
  req.resolved = true;
1468
1625
  req.decision = decision;
1626
+ try {
1627
+ sendPendingRequestResponseOrThrow(req, response, sessionId, requestId);
1628
+ } catch (error) {
1629
+ req.resolved = false;
1630
+ req.decision = void 0;
1631
+ session.pendingRequests.set(requestId, req);
1632
+ if (session.status !== "cancelled") {
1633
+ session.status = "waiting_approval";
1634
+ }
1635
+ throw error;
1636
+ }
1469
1637
  if (req.timeoutHandle) clearTimeout(req.timeoutHandle);
1470
- sendPendingRequestResponseOrThrow(req, response, sessionId, requestId);
1471
1638
  pushEvent(
1472
1639
  session.eventBuffer,
1473
1640
  "approval_result",
@@ -1484,6 +1651,7 @@ var SessionManager = class {
1484
1651
  if (session.pendingRequests.size === 0 && session.status === "waiting_approval") {
1485
1652
  session.status = "running";
1486
1653
  }
1654
+ this.notifyWaiters(sessionId);
1487
1655
  }
1488
1656
  // ── User Input Response ──────────────────────────────────────────
1489
1657
  resolveUserInput(sessionId, requestId, answers) {
@@ -1495,13 +1663,22 @@ var SessionManager = class {
1495
1663
  );
1496
1664
  }
1497
1665
  req.resolved = true;
1666
+ try {
1667
+ sendPendingRequestResponseOrThrow(
1668
+ req,
1669
+ { answers },
1670
+ sessionId,
1671
+ requestId
1672
+ );
1673
+ } catch (error) {
1674
+ req.resolved = false;
1675
+ session.pendingRequests.set(requestId, req);
1676
+ if (session.status !== "cancelled") {
1677
+ session.status = "waiting_approval";
1678
+ }
1679
+ throw error;
1680
+ }
1498
1681
  if (req.timeoutHandle) clearTimeout(req.timeoutHandle);
1499
- sendPendingRequestResponseOrThrow(
1500
- req,
1501
- { answers },
1502
- sessionId,
1503
- requestId
1504
- );
1505
1682
  pushEvent(
1506
1683
  session.eventBuffer,
1507
1684
  "approval_result",
@@ -1517,6 +1694,7 @@ var SessionManager = class {
1517
1694
  if (session.pendingRequests.size === 0 && session.status === "waiting_approval") {
1518
1695
  session.status = "running";
1519
1696
  }
1697
+ this.notifyWaiters(sessionId);
1520
1698
  }
1521
1699
  // ── Cleanup ──────────────────────────────────────────────────────
1522
1700
  destroy() {
@@ -1625,6 +1803,8 @@ var SessionManager = class {
1625
1803
  },
1626
1804
  true
1627
1805
  );
1806
+ this.persistSessionIfChanged(session);
1807
+ this.persistResult(session);
1628
1808
  break;
1629
1809
  }
1630
1810
  case Methods.ERROR: {
@@ -1651,6 +1831,7 @@ var SessionManager = class {
1651
1831
  );
1652
1832
  } else {
1653
1833
  pushEvent(session.eventBuffer, "error", data, true);
1834
+ this.persistSessionIfChanged(session);
1654
1835
  }
1655
1836
  }
1656
1837
  break;
@@ -1699,6 +1880,7 @@ var SessionManager = class {
1699
1880
  default:
1700
1881
  break;
1701
1882
  }
1883
+ this.notifyWaiters(sessionId);
1702
1884
  });
1703
1885
  client.onServerRequest((id, method, params) => {
1704
1886
  if (session.status === "cancelled" || session.status === "error") {
@@ -1769,6 +1951,7 @@ var SessionManager = class {
1769
1951
  if (session.pendingRequests.size === 0 && session.status === "waiting_approval") {
1770
1952
  session.status = "running";
1771
1953
  }
1954
+ this.notifyWaiters(sessionId);
1772
1955
  }
1773
1956
  }, approvalTimeoutMs);
1774
1957
  session.pendingRequests.set(requestId, pending);
@@ -1836,6 +2019,7 @@ var SessionManager = class {
1836
2019
  if (session.pendingRequests.size === 0 && session.status === "waiting_approval") {
1837
2020
  session.status = "running";
1838
2021
  }
2022
+ this.notifyWaiters(sessionId);
1839
2023
  }
1840
2024
  }, approvalTimeoutMs);
1841
2025
  session.pendingRequests.set(requestId, pending);
@@ -1890,6 +2074,7 @@ var SessionManager = class {
1890
2074
  if (session.pendingRequests.size === 0 && session.status === "waiting_approval") {
1891
2075
  session.status = "running";
1892
2076
  }
2077
+ this.notifyWaiters(sessionId);
1893
2078
  }
1894
2079
  }, approvalTimeoutMs);
1895
2080
  session.pendingRequests.set(requestId, pending);
@@ -1928,6 +2113,7 @@ var SessionManager = class {
1928
2113
  client.respondErrorToServer(id, -32601, `Unhandled server request: ${method}`);
1929
2114
  break;
1930
2115
  }
2116
+ this.notifyWaiters(sessionId);
1931
2117
  });
1932
2118
  client.on("exit", (code) => {
1933
2119
  clearSessionPendingRequests(session);
@@ -1943,6 +2129,9 @@ var SessionManager = class {
1943
2129
  },
1944
2130
  true
1945
2131
  );
2132
+ this.persistSessionIfChanged(session);
2133
+ this.persistResult(session);
2134
+ this.notifyWaiters(sessionId);
1946
2135
  }
1947
2136
  });
1948
2137
  client.on("error", (err) => {
@@ -1959,25 +2148,34 @@ var SessionManager = class {
1959
2148
  },
1960
2149
  true
1961
2150
  );
2151
+ this.persistSessionIfChanged(session);
2152
+ this.persistResult(session);
2153
+ this.notifyWaiters(sessionId);
1962
2154
  }
1963
2155
  });
1964
2156
  }
1965
2157
  cleanupSessions() {
1966
2158
  const now = Date.now();
2159
+ const TTL_WARNING_THRESHOLD_MS = 6e4;
1967
2160
  for (const [id, session] of this.sessions) {
1968
2161
  const lastActive = new Date(session.lastActiveAt).getTime();
1969
2162
  if (Number.isNaN(lastActive)) {
2163
+ this.ttlWarningEmitted.delete(id);
1970
2164
  this.requestCancellation(id, "Invalid timestamp");
1971
2165
  continue;
1972
2166
  }
1973
2167
  const age = now - lastActive;
1974
2168
  if (session.status === "idle" && age > DEFAULT_IDLE_CLEANUP_MS) {
2169
+ this.ttlWarningEmitted.delete(id);
1975
2170
  this.requestCancellation(id, "Idle timeout");
1976
2171
  } else if (session.status === "waiting_approval" && age > DEFAULT_RUNNING_CLEANUP_MS) {
2172
+ this.ttlWarningEmitted.delete(id);
1977
2173
  this.requestCancellation(id, "Approval timeout");
1978
2174
  } else if (session.status === "running" && age > DEFAULT_RUNNING_CLEANUP_MS) {
2175
+ this.ttlWarningEmitted.delete(id);
1979
2176
  this.requestCancellation(id, "Running timeout");
1980
2177
  } else if ((session.status === "cancelled" || session.status === "error") && age > DEFAULT_TERMINAL_CLEANUP_MS) {
2178
+ this.ttlWarningEmitted.delete(id);
1981
2179
  this.clients.get(id)?.destroy().catch((err) => {
1982
2180
  console.error(
1983
2181
  `[codex-mcp] Failed to destroy app-server client during cleanup: session=${id} error=${err instanceof Error ? err.message : String(err)}`
@@ -1985,6 +2183,27 @@ var SessionManager = class {
1985
2183
  });
1986
2184
  this.clients.delete(id);
1987
2185
  this.sessions.delete(id);
2186
+ this.lastPersistedStatus.delete(id);
2187
+ this.ttlWarningEmitted.delete(id);
2188
+ } else {
2189
+ let ttlMs;
2190
+ if (session.status === "idle") {
2191
+ ttlMs = DEFAULT_IDLE_CLEANUP_MS;
2192
+ } else if (session.status === "running" || session.status === "waiting_approval") {
2193
+ ttlMs = DEFAULT_RUNNING_CLEANUP_MS;
2194
+ }
2195
+ if (ttlMs !== void 0 && !this.ttlWarningEmitted.has(id)) {
2196
+ const timeUntilExpiry = ttlMs - age;
2197
+ if (timeUntilExpiry <= TTL_WARNING_THRESHOLD_MS && timeUntilExpiry > 0) {
2198
+ this.ttlWarningEmitted.add(id);
2199
+ pushEvent(session.eventBuffer, "progress", {
2200
+ method: "codex-mcp/ttl_warning",
2201
+ type: "ttl_warning",
2202
+ ttlRemainingMs: timeUntilExpiry,
2203
+ sessionId: id
2204
+ });
2205
+ }
2206
+ }
1988
2207
  }
1989
2208
  }
1990
2209
  }
@@ -2474,17 +2693,117 @@ function extractSpawnOptions(params) {
2474
2693
  };
2475
2694
  }
2476
2695
 
2696
+ // src/utils/execution.ts
2697
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["idle", "error", "cancelled"]);
2698
+ function interactionStateForStatus(status) {
2699
+ if (status === "waiting_approval") return "waiting_input";
2700
+ if (TERMINAL_STATUSES.has(status)) return "finished";
2701
+ return "working";
2702
+ }
2703
+ function recommendedNextActionForStatus(status, actionTypes = []) {
2704
+ if (status === "waiting_approval") {
2705
+ if (actionTypes.includes("user_input")) return "respond_user_input";
2706
+ if (actionTypes.includes("approval")) return "respond_permission";
2707
+ }
2708
+ if (TERMINAL_STATUSES.has(status)) return "none";
2709
+ return "poll";
2710
+ }
2711
+ function buildExecutionInfo(waitForResultMs, status, fallbackReason) {
2712
+ const requested = waitForResultMs && waitForResultMs > 0 ? "foreground" : "background";
2713
+ const effective = requested === "foreground" && TERMINAL_STATUSES.has(status) ? "foreground" : "background";
2714
+ return {
2715
+ requested,
2716
+ effective,
2717
+ waitForResultMs: waitForResultMs && waitForResultMs > 0 ? waitForResultMs : void 0,
2718
+ fallbackReason: effective === "background" ? fallbackReason : void 0
2719
+ };
2720
+ }
2721
+ async function waitForCodexSessionForegroundResult(sessionManager, sessionId, waitForResultMs, signal) {
2722
+ const deadline = Date.now() + Math.min(waitForResultMs, 3e5);
2723
+ while (Date.now() < deadline) {
2724
+ let status2;
2725
+ try {
2726
+ status2 = sessionManager.getSession(sessionId).status;
2727
+ } catch {
2728
+ status2 = "error";
2729
+ }
2730
+ if (TERMINAL_STATUSES.has(status2)) {
2731
+ const finalResult = sessionManager.getLastResult(sessionId);
2732
+ return {
2733
+ status: status2,
2734
+ result: finalResult,
2735
+ completedAt: finalResult?.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
2736
+ };
2737
+ }
2738
+ if (status2 === "waiting_approval") {
2739
+ return {
2740
+ status: status2,
2741
+ pendingActionTypes: sessionManager.getPendingActionTypes(sessionId),
2742
+ fallbackReason: "interactive_poll_required"
2743
+ };
2744
+ }
2745
+ const remainingMs = Math.min(deadline - Date.now(), 5e3);
2746
+ if (remainingMs <= 0) break;
2747
+ try {
2748
+ await sessionManager.waitForChange(sessionId, remainingMs, signal);
2749
+ } catch {
2750
+ break;
2751
+ }
2752
+ }
2753
+ let status = "running";
2754
+ try {
2755
+ status = sessionManager.getSession(sessionId).status;
2756
+ } catch {
2757
+ status = "error";
2758
+ }
2759
+ return { status, fallbackReason: "wait_for_result_timeout" };
2760
+ }
2761
+
2477
2762
  // src/tools/codex.ts
2478
- async function executeCodex(args, sessionManager, serverCwd) {
2763
+ async function executeCodex(args, sessionManager, serverCwd, requestSignal) {
2479
2764
  const cwd = resolveAndValidateCwd(args.cwd, serverCwd);
2480
2765
  const spawnOpts = extractSpawnOptions(args);
2481
2766
  const effort = args.effort ?? DEFAULT_EFFORT_LEVEL;
2482
- return sessionManager.createSession(args.prompt, cwd, spawnOpts, effort, args.advanced);
2767
+ const startResult = await sessionManager.createSession(
2768
+ args.prompt,
2769
+ cwd,
2770
+ spawnOpts,
2771
+ effort,
2772
+ args.advanced
2773
+ );
2774
+ const waitMs = args.advanced?.waitForResult;
2775
+ const baseResult = {
2776
+ ...startResult,
2777
+ execution: buildExecutionInfo(waitMs, "running"),
2778
+ interactionState: interactionStateForStatus("running"),
2779
+ recommendedNextAction: recommendedNextActionForStatus("running")
2780
+ };
2781
+ if (!waitMs || waitMs <= 0) return baseResult;
2782
+ const foreground = await waitForCodexSessionForegroundResult(
2783
+ sessionManager,
2784
+ startResult.sessionId,
2785
+ waitMs,
2786
+ requestSignal
2787
+ );
2788
+ return {
2789
+ sessionId: startResult.sessionId,
2790
+ threadId: startResult.threadId,
2791
+ result: foreground.result,
2792
+ status: foreground.status,
2793
+ completedAt: foreground.completedAt,
2794
+ pollInterval: foreground.status === "waiting_approval" ? WAITING_APPROVAL_POLL_INTERVAL : foreground.status === "running" ? startResult.pollInterval : void 0,
2795
+ execution: buildExecutionInfo(waitMs, foreground.status, foreground.fallbackReason),
2796
+ interactionState: interactionStateForStatus(foreground.status),
2797
+ recommendedNextAction: recommendedNextActionForStatus(
2798
+ foreground.status,
2799
+ foreground.pendingActionTypes ?? []
2800
+ )
2801
+ };
2483
2802
  }
2484
2803
 
2485
2804
  // src/tools/codex-reply.ts
2486
- async function executeCodexReply(args, sessionManager) {
2487
- return sessionManager.replyToSession(args.sessionId, args.prompt, {
2805
+ async function executeCodexReply(args, sessionManager, requestSignal) {
2806
+ const startResult = await sessionManager.replyToSession(args.sessionId, args.prompt, {
2488
2807
  model: args.model,
2489
2808
  approvalPolicy: args.approvalPolicy,
2490
2809
  effort: args.effort,
@@ -2494,6 +2813,36 @@ async function executeCodexReply(args, sessionManager) {
2494
2813
  cwd: args.cwd,
2495
2814
  outputSchema: args.outputSchema
2496
2815
  });
2816
+ const waitMs = args.waitForResult;
2817
+ const baseResult = {
2818
+ ...startResult,
2819
+ execution: buildExecutionInfo(waitMs, "running"),
2820
+ interactionState: interactionStateForStatus("running"),
2821
+ recommendedNextAction: recommendedNextActionForStatus("running")
2822
+ };
2823
+ if (!waitMs || waitMs <= 0) {
2824
+ return baseResult;
2825
+ }
2826
+ const foreground = await waitForCodexSessionForegroundResult(
2827
+ sessionManager,
2828
+ startResult.sessionId,
2829
+ waitMs,
2830
+ requestSignal
2831
+ );
2832
+ return {
2833
+ sessionId: startResult.sessionId,
2834
+ threadId: startResult.threadId,
2835
+ status: foreground.status,
2836
+ pollInterval: foreground.status === "waiting_approval" ? WAITING_APPROVAL_POLL_INTERVAL : foreground.status === "running" ? startResult.pollInterval : void 0,
2837
+ result: foreground.result,
2838
+ completedAt: foreground.completedAt,
2839
+ execution: buildExecutionInfo(waitMs, foreground.status, foreground.fallbackReason),
2840
+ interactionState: interactionStateForStatus(foreground.status),
2841
+ recommendedNextAction: recommendedNextActionForStatus(
2842
+ foreground.status,
2843
+ foreground.pendingActionTypes ?? []
2844
+ )
2845
+ };
2497
2846
  }
2498
2847
 
2499
2848
  // src/tools/codex-session.ts
@@ -2556,7 +2905,7 @@ async function executeCodexSession(args, sessionManager) {
2556
2905
  }
2557
2906
 
2558
2907
  // src/tools/codex-check.ts
2559
- function executeCodexCheck(args, sessionManager) {
2908
+ function executeCodexCheck(args, sessionManager, requestSignal) {
2560
2909
  const responseMode = args.responseMode ?? "minimal";
2561
2910
  const pollOptions = args.pollOptions;
2562
2911
  switch (args.action) {
@@ -2568,10 +2917,34 @@ function executeCodexCheck(args, sessionManager) {
2568
2917
  };
2569
2918
  }
2570
2919
  const maxEvents = typeof args.maxEvents === "number" ? Math.max(POLL_MIN_MAX_EVENTS, Math.floor(args.maxEvents)) : POLL_DEFAULT_MAX_EVENTS;
2571
- return sessionManager.pollEvents(args.sessionId, args.cursor, maxEvents, {
2572
- responseMode,
2573
- pollOptions
2574
- });
2920
+ const waitMs = pollOptions?.waitMs;
2921
+ if (typeof waitMs === "number" && waitMs > 0) {
2922
+ const firstCheck = sessionManager.pollEvents(args.sessionId, args.cursor, maxEvents, {
2923
+ responseMode,
2924
+ pollOptions
2925
+ });
2926
+ const hasData = firstCheck.events.length > 0 || firstCheck.actions !== void 0 && firstCheck.actions.length > 0 || firstCheck.result !== void 0;
2927
+ if (!hasData) {
2928
+ const clampedWait = Math.min(waitMs, 12e4);
2929
+ return sessionManager.waitForChange(args.sessionId, clampedWait, requestSignal).catch(() => void 0).then(
2930
+ () => enrichCheckResult(
2931
+ sessionManager,
2932
+ sessionManager.pollEvents(args.sessionId, args.cursor, maxEvents, {
2933
+ responseMode,
2934
+ pollOptions
2935
+ })
2936
+ )
2937
+ );
2938
+ }
2939
+ return enrichCheckResult(sessionManager, firstCheck);
2940
+ }
2941
+ return enrichCheckResult(
2942
+ sessionManager,
2943
+ sessionManager.pollEvents(args.sessionId, args.cursor, maxEvents, {
2944
+ responseMode,
2945
+ pollOptions
2946
+ })
2947
+ );
2575
2948
  }
2576
2949
  case "respond_permission": {
2577
2950
  if (!args.requestId || !args.decision) {
@@ -2641,10 +3014,13 @@ function executeCodexCheck(args, sessionManager) {
2641
3014
  return { error: message, isError: true };
2642
3015
  }
2643
3016
  const maxEvents = typeof args.maxEvents === "number" ? Math.max(0, Math.floor(args.maxEvents)) : RESPOND_DEFAULT_MAX_EVENTS;
2644
- return sessionManager.pollEventsMonotonic(args.sessionId, args.cursor, maxEvents, {
2645
- responseMode,
2646
- pollOptions
2647
- });
3017
+ return enrichCheckResult(
3018
+ sessionManager,
3019
+ sessionManager.pollEventsMonotonic(args.sessionId, args.cursor, maxEvents, {
3020
+ responseMode,
3021
+ pollOptions
3022
+ })
3023
+ );
2648
3024
  }
2649
3025
  case "respond_user_input": {
2650
3026
  if (!args.requestId || !args.answers) {
@@ -2666,10 +3042,13 @@ function executeCodexCheck(args, sessionManager) {
2666
3042
  return { error: message, isError: true };
2667
3043
  }
2668
3044
  const maxEvents = typeof args.maxEvents === "number" ? Math.max(0, Math.floor(args.maxEvents)) : RESPOND_DEFAULT_MAX_EVENTS;
2669
- return sessionManager.pollEventsMonotonic(args.sessionId, args.cursor, maxEvents, {
2670
- responseMode,
2671
- pollOptions
2672
- });
3045
+ return enrichCheckResult(
3046
+ sessionManager,
3047
+ sessionManager.pollEventsMonotonic(args.sessionId, args.cursor, maxEvents, {
3048
+ responseMode,
3049
+ pollOptions
3050
+ })
3051
+ );
2673
3052
  }
2674
3053
  default:
2675
3054
  return {
@@ -2678,9 +3057,218 @@ function executeCodexCheck(args, sessionManager) {
2678
3057
  };
2679
3058
  }
2680
3059
  }
3060
+ function enrichCheckResult(sessionManager, result) {
3061
+ const actionTypes = result.status === "waiting_approval" ? sessionManager.getPendingActionTypes(result.sessionId) : [];
3062
+ return {
3063
+ ...result,
3064
+ interactionState: interactionStateForStatus(result.status),
3065
+ recommendedNextAction: recommendedNextActionForStatus(result.status, actionTypes)
3066
+ };
3067
+ }
2681
3068
 
2682
- // src/resources/register-resources.ts
3069
+ // src/tools/codex-setup.ts
2683
3070
  import { spawnSync } from "child_process";
3071
+ import { existsSync as existsSync5 } from "fs";
3072
+ import { homedir } from "os";
3073
+ import path5 from "path";
3074
+
3075
+ // src/app-server/detect.ts
3076
+ import { spawn as spawn2 } from "child_process";
3077
+ var DETECTION_TIMEOUT_MS = 5e3;
3078
+ async function detectClientMode(codexCommand, codexIsPath = false, env = process.env) {
3079
+ const override = env.CODEX_MCP_MODE;
3080
+ if (override === "app-server" || override === "exec") {
3081
+ return override;
3082
+ }
3083
+ try {
3084
+ const supported = await probeAppServer(codexCommand, codexIsPath, env);
3085
+ return supported ? "app-server" : "exec";
3086
+ } catch {
3087
+ return "exec";
3088
+ }
3089
+ }
3090
+ function probeAppServer(codexCommand, codexIsPath, env) {
3091
+ return new Promise((resolve) => {
3092
+ const invocation = resolveCodexInvocation(["app-server", "--help"], {
3093
+ env,
3094
+ codexCommand,
3095
+ codexIsPath
3096
+ });
3097
+ let settled = false;
3098
+ const settle = (result) => {
3099
+ if (settled) return;
3100
+ settled = true;
3101
+ clearTimeout(timer);
3102
+ resolve(result);
3103
+ };
3104
+ let stdout = "";
3105
+ let stderr = "";
3106
+ const proc = spawn2(invocation.cmd, invocation.args, {
3107
+ stdio: ["ignore", "pipe", "pipe"],
3108
+ env,
3109
+ windowsHide: true
3110
+ });
3111
+ proc.stdout?.on("data", (chunk) => {
3112
+ stdout += chunk.toString();
3113
+ });
3114
+ proc.stderr?.on("data", (chunk) => {
3115
+ stderr += chunk.toString();
3116
+ });
3117
+ proc.on("error", () => {
3118
+ settle(false);
3119
+ });
3120
+ proc.on("exit", (code) => {
3121
+ if (code === 0) {
3122
+ settle(true);
3123
+ } else {
3124
+ const combined = (stdout + stderr).toLowerCase();
3125
+ const isUnknown = combined.includes("unknown") || combined.includes("unrecognized") || combined.includes("not found") || combined.includes("no such subcommand");
3126
+ settle(!isUnknown && combined.includes("app-server"));
3127
+ }
3128
+ });
3129
+ const timer = setTimeout(() => {
3130
+ try {
3131
+ proc.kill("SIGTERM");
3132
+ } catch {
3133
+ }
3134
+ const forceKill = setTimeout(() => {
3135
+ try {
3136
+ if (!proc.killed && proc.exitCode === null) {
3137
+ proc.kill("SIGKILL");
3138
+ }
3139
+ } catch {
3140
+ }
3141
+ }, 2e3);
3142
+ if (forceKill.unref) forceKill.unref();
3143
+ settle(false);
3144
+ }, DETECTION_TIMEOUT_MS);
3145
+ if (timer.unref) timer.unref();
3146
+ });
3147
+ }
3148
+
3149
+ // src/tools/codex-setup.ts
3150
+ function classifyAuthResult(status, combined) {
3151
+ if (status === 0) return "authenticated";
3152
+ if (/(not (logged|authenticated)|login required|run\s+codex\s+login)/i.test(combined)) {
3153
+ return "unauthenticated";
3154
+ }
3155
+ return "unknown";
3156
+ }
3157
+ function resolveCodexStateDir() {
3158
+ const configured = process.env.CODEX_MCP_STATE_DIR?.trim();
3159
+ return configured && configured !== "" ? configured : path5.join(homedir(), ".codex-mcp", "state");
3160
+ }
3161
+ function probeCodexAuth(info) {
3162
+ const invocation = resolveCodexInvocation(["login", "status"], {
3163
+ codexCommand: info.command,
3164
+ codexIsPath: info.isPath
3165
+ });
3166
+ const run = spawnSync(invocation.cmd, invocation.args, {
3167
+ encoding: "utf8",
3168
+ timeout: 5e3,
3169
+ windowsHide: true
3170
+ });
3171
+ const combined = `${run.stdout ?? ""}
3172
+ ${run.stderr ?? ""}`.trim();
3173
+ if (run.error) {
3174
+ return {
3175
+ ok: false,
3176
+ state: "unknown",
3177
+ detail: `Failed to probe auth status: ${run.error.message}`
3178
+ };
3179
+ }
3180
+ const state = classifyAuthResult(run.status, combined);
3181
+ return {
3182
+ ok: state !== "unauthenticated",
3183
+ state,
3184
+ detail: combined || (state === "authenticated" ? "Authenticated." : "Auth status unknown.")
3185
+ };
3186
+ }
3187
+ async function executeCodexSetup(input, serverCwd) {
3188
+ const cwd = input?.cwd && input.cwd.trim() !== "" ? input.cwd : serverCwd;
3189
+ const warnings = [];
3190
+ const nextSteps = [];
3191
+ let executable;
3192
+ let auth;
3193
+ let clientMode;
3194
+ try {
3195
+ const info = resolveDefaultCodexExecutable();
3196
+ const available = info.source !== "default";
3197
+ executable = {
3198
+ ok: available,
3199
+ source: info.source,
3200
+ command: info.command,
3201
+ isPath: info.isPath,
3202
+ detail: info.source === "default" ? "No codex executable was auto-detected; the server would fall back to `codex` and let process spawn fail later." : `Codex resolves via ${info.source}.`
3203
+ };
3204
+ if (available) {
3205
+ auth = probeCodexAuth(info);
3206
+ clientMode = await detectClientMode(info.command, info.isPath);
3207
+ } else {
3208
+ auth = {
3209
+ ok: false,
3210
+ state: "unknown",
3211
+ detail: "Auth status not checked because no codex executable was detected."
3212
+ };
3213
+ }
3214
+ } catch (err) {
3215
+ const message = err instanceof Error ? err.message : String(err);
3216
+ executable = {
3217
+ ok: false,
3218
+ source: "error",
3219
+ detail: message
3220
+ };
3221
+ auth = {
3222
+ ok: false,
3223
+ state: "unknown",
3224
+ detail: "Auth status not checked because executable resolution failed."
3225
+ };
3226
+ }
3227
+ const projectContext = {
3228
+ hasUserConfig: existsSync5(path5.join(homedir(), ".codex", "config.toml")),
3229
+ hasProjectConfig: existsSync5(path5.join(cwd, ".codex", "config.toml"))
3230
+ };
3231
+ if (!executable.ok) {
3232
+ warnings.push(executable.detail);
3233
+ nextSteps.push(
3234
+ "Install Codex or fix CODEX_MCP_COMMAND / CODEX_MCP_PATH so the executable can be resolved."
3235
+ );
3236
+ }
3237
+ if (auth.state === "unauthenticated") {
3238
+ warnings.push(auth.detail);
3239
+ nextSteps.push("Run `codex login` and rerun `codex_setup`.");
3240
+ } else if (auth.state === "unknown") {
3241
+ warnings.push(auth.detail);
3242
+ nextSteps.push(
3243
+ "Verify Codex authentication explicitly (for example with `codex login status`) before relying on this environment."
3244
+ );
3245
+ }
3246
+ if (!projectContext.hasUserConfig && !projectContext.hasProjectConfig) {
3247
+ warnings.push("No Codex config.toml was found in ~/.codex or this project.");
3248
+ }
3249
+ if (clientMode === "exec") {
3250
+ warnings.push(
3251
+ "Codex app-server support was not detected; codex-mcp would run in exec fallback mode with fewer capabilities."
3252
+ );
3253
+ }
3254
+ return {
3255
+ ready: executable.ok && auth.ok,
3256
+ cwd,
3257
+ executable,
3258
+ auth,
3259
+ runtime: {
3260
+ sameMachineRequired: true,
3261
+ clientMode,
3262
+ stateDir: resolveCodexStateDir()
3263
+ },
3264
+ projectContext,
3265
+ warnings,
3266
+ nextSteps
3267
+ };
3268
+ }
3269
+
3270
+ // src/resources/register-resources.ts
3271
+ import { spawnSync as spawnSync2 } from "child_process";
2684
3272
 
2685
3273
  // src/utils/stdio-guard.ts
2686
3274
  var STDIO_MODES = ["auto", "strict", "off"];
@@ -2779,7 +3367,8 @@ var RESOURCE_URIS = {
2779
3367
  config: `${RESOURCE_SCHEME}:///config`,
2780
3368
  gotchas: `${RESOURCE_SCHEME}:///gotchas`,
2781
3369
  quickstart: `${RESOURCE_SCHEME}:///quickstart`,
2782
- errors: `${RESOURCE_SCHEME}:///errors`
3370
+ errors: `${RESOURCE_SCHEME}:///errors`,
3371
+ delegationGuide: `${RESOURCE_SCHEME}:///delegation-guide`
2783
3372
  };
2784
3373
  var RESOURCE_CATALOG = [
2785
3374
  {
@@ -2823,6 +3412,13 @@ var RESOURCE_CATALOG = [
2823
3412
  title: "Errors",
2824
3413
  description: "Error code reference and recovery hints",
2825
3414
  mimeType: "text/markdown"
3415
+ },
3416
+ {
3417
+ key: "delegationGuide",
3418
+ name: "delegation_guide",
3419
+ title: "Delegation Guide",
3420
+ description: "Best practices for delegating tasks to Codex",
3421
+ mimeType: "text/markdown"
2826
3422
  }
2827
3423
  ];
2828
3424
  var ERROR_CODE_HINTS = {
@@ -2854,7 +3450,7 @@ function asTextResource(uri, text, mimeType) {
2854
3450
  function detectCodexCliVersion(timeoutMs = 1500) {
2855
3451
  try {
2856
3452
  const executable = getDefaultCodexExecutable();
2857
- const run = spawnSync(executable.command, ["--version"], {
3453
+ const run = spawnSync2(executable.command, ["--version"], {
2858
3454
  encoding: "utf8",
2859
3455
  timeout: timeoutMs,
2860
3456
  windowsHide: true
@@ -2996,6 +3592,8 @@ function buildQuickstartText() {
2996
3592
  return [
2997
3593
  "## Minimal flow",
2998
3594
  "",
3595
+ "0. Optional but recommended: run `codex_setup` first to verify the local Codex CLI, login state, and backend mode.",
3596
+ "",
2999
3597
  "1. Start session (`codex`)",
3000
3598
  "",
3001
3599
  "```json",
@@ -3094,6 +3692,64 @@ function buildErrorsText() {
3094
3692
  lines.push("");
3095
3693
  return lines.join("\n");
3096
3694
  }
3695
+ function buildDelegationGuideText() {
3696
+ return [
3697
+ "# Codex Delegation Guide",
3698
+ "",
3699
+ "## When to delegate",
3700
+ "- Bug investigation or fix that benefits from a second opinion",
3701
+ "- Code review (use read-only sandbox)",
3702
+ "- Refactoring or migration tasks with clear scope",
3703
+ "- Tasks where the calling agent is stuck or wants parallel work",
3704
+ "",
3705
+ "## Permission combinations by task type",
3706
+ "",
3707
+ "| Task | approvalPolicy | sandbox | Notes |",
3708
+ "|------|---------------|---------|-------|",
3709
+ "| Code review / analysis | `never` | `read-only` | Safe: sandbox blocks writes, no approval needed |",
3710
+ "| Quick bug fix | `on-failure` | `workspace-write` | Auto-approves unless error; good with `waitForResult` |",
3711
+ "| Feature implementation | `on-failure` | `workspace-write` | Async mode recommended for longer tasks |",
3712
+ "| Sensitive refactor | `on-request` | `workspace-write` | Codex asks before each action; requires active polling |",
3713
+ "| Full autonomy | `never` | `workspace-write` | No guardrails \u2014 only for well-scoped, trusted tasks |",
3714
+ "| Network access needed | `on-failure` | `danger-full-access` | Rare; avoid unless genuinely required |",
3715
+ "",
3716
+ "**Key rule:** `read-only` sandbox already prevents writes, so `approvalPolicy: 'never'` is safe with it. Avoid `untrusted` + `read-only` \u2014 every read command triggers approval for no safety gain.",
3717
+ "",
3718
+ "## Quick mode: `waitForResult`",
3719
+ "For short tasks (< 2 min), set `advanced.waitForResult` to get the final result in a single tool call:",
3720
+ "```json",
3721
+ '{ "prompt": "Fix the null check in auth.ts", "approvalPolicy": "on-failure", "sandbox": "workspace-write", "effort": "medium", "advanced": { "waitForResult": 120000 } }',
3722
+ "```",
3723
+ "If the task finishes within the timeout, the result is returned directly. Otherwise the response falls back to polling metadata (including `sessionId` and `threadId`).",
3724
+ "",
3725
+ "**Constraint:** `waitForResult` blocks your tool call. If the task hits `waiting_approval`, you cannot respond \u2014 the timeout will always expire. Only use with `approvalPolicy: 'on-failure'` or `'never'`.",
3726
+ "",
3727
+ "## Async mode: poll loop",
3728
+ 'For long tasks, omit `waitForResult`. Use `codex_check(action="poll", pollOptions={ waitMs: 30000 })` with long-polling to avoid empty polls.',
3729
+ "",
3730
+ "## Effort selection",
3731
+ "- `low` (default): quick questions, lookups, simple edits",
3732
+ "- `medium`: multi-file changes, moderate reasoning",
3733
+ "- `high`/`xhigh`: complex architecture decisions, large refactors",
3734
+ "",
3735
+ "## Troubleshooting",
3736
+ "",
3737
+ "**Tool call hangs:** Likely `waitForResult` + approval conflict; cancel and retry with `on-failure`/`never`. See `codex-mcp:///gotchas`.",
3738
+ "",
3739
+ "**Empty polls:** Use `pollOptions.waitMs` for long-polling; stop when status is terminal. See `codex-mcp:///gotchas`.",
3740
+ "",
3741
+ "**Session not found after restart:** Previously-running sessions surface as `status: 'error'` with restart reason. See `codex-mcp:///gotchas`.",
3742
+ "",
3743
+ "**Approval timeout:** Default is 60s; infrequent polling causes silent auto-decline. See `codex-mcp:///gotchas`.",
3744
+ "",
3745
+ "## Security notes",
3746
+ "- `sandbox: 'read-only'` is the strongest isolation \u2014 blocks all writes regardless of approval policy",
3747
+ "- `approvalPolicy: 'never'` + `sandbox: 'workspace-write'` gives the agent full write access with no human oversight \u2014 use only for well-defined, low-risk tasks",
3748
+ "- `danger-full-access` allows network and system access \u2014 treat as root-equivalent",
3749
+ "- Persisted session data (events, results) may contain code snippets and file paths \u2014 stored in `~/.codex-mcp/state/`",
3750
+ ""
3751
+ ].join("\n");
3752
+ }
3097
3753
  function buildCompatReport(deps, codexCliVersion) {
3098
3754
  const runtimeWarnings = [];
3099
3755
  if (!codexCliVersion) {
@@ -3128,7 +3784,7 @@ function buildCompatReport(deps, codexCliVersion) {
3128
3784
  }
3129
3785
  },
3130
3786
  toolCounts: {
3131
- core: 4
3787
+ core: 5
3132
3788
  },
3133
3789
  runtimeWarnings,
3134
3790
  detectedMismatches: [],
@@ -3258,10 +3914,22 @@ function registerResources(server, deps) {
3258
3914
  },
3259
3915
  () => asTextResource(errorsUri, buildErrorsText(), "text/markdown")
3260
3916
  );
3917
+ const delegationGuideMeta = byKey.get("delegationGuide");
3918
+ const delegationGuideUri = new URL(RESOURCE_URIS.delegationGuide);
3919
+ server.registerResource(
3920
+ delegationGuideMeta.name,
3921
+ delegationGuideUri.toString(),
3922
+ {
3923
+ title: delegationGuideMeta.title,
3924
+ description: delegationGuideMeta.description,
3925
+ mimeType: delegationGuideMeta.mimeType
3926
+ },
3927
+ () => asTextResource(delegationGuideUri, buildDelegationGuideText(), "text/markdown")
3928
+ );
3261
3929
  }
3262
3930
 
3263
3931
  // src/server.ts
3264
- var SERVER_VERSION = true ? "2.1.4" : "0.0.0-dev";
3932
+ var SERVER_VERSION = true ? "2.1.5" : "0.0.0-dev";
3265
3933
  function formatErrorMessage(err) {
3266
3934
  const message = err instanceof Error ? err.message : String(err);
3267
3935
  const m = /^Error \[([A-Z_]+)\]:\s*(.*)$/.exec(message);
@@ -3307,20 +3975,63 @@ function createServer(serverCwd, options) {
3307
3975
  error: z.string().optional(),
3308
3976
  isError: z.boolean().optional()
3309
3977
  };
3310
- const sessionStartOutputShape = {
3978
+ const executionInfoSchema = z.object({
3979
+ requested: z.enum(["background", "foreground"]),
3980
+ effective: z.enum(["background", "foreground"]),
3981
+ waitForResultMs: z.number().int().positive().optional(),
3982
+ fallbackReason: z.enum(["wait_for_result_timeout", "interactive_poll_required"]).optional()
3983
+ });
3984
+ const interactionStateSchema = z.enum(["working", "waiting_input", "finished"]);
3985
+ const nextActionSchema = z.enum(["poll", "respond_permission", "respond_user_input", "none"]);
3986
+ const setupResultShape = {
3987
+ ready: z.boolean(),
3988
+ cwd: z.string(),
3989
+ executable: z.object({
3990
+ ok: z.boolean(),
3991
+ source: z.string(),
3992
+ command: z.string().optional(),
3993
+ isPath: z.boolean().optional(),
3994
+ detail: z.string()
3995
+ }),
3996
+ auth: z.object({
3997
+ ok: z.boolean(),
3998
+ state: z.enum(["authenticated", "unauthenticated", "unknown"]),
3999
+ detail: z.string()
4000
+ }),
4001
+ runtime: z.object({
4002
+ sameMachineRequired: z.boolean(),
4003
+ clientMode: z.enum(["app-server", "exec"]).optional(),
4004
+ stateDir: z.string()
4005
+ }),
4006
+ projectContext: z.object({
4007
+ hasUserConfig: z.boolean(),
4008
+ hasProjectConfig: z.boolean()
4009
+ }),
4010
+ warnings: z.array(z.string()),
4011
+ nextSteps: z.array(z.string())
4012
+ };
4013
+ const sessionStartOutputShape = {
3311
4014
  sessionId: z.string().optional(),
3312
4015
  threadId: z.string().optional(),
3313
- status: z.enum(["running", "idle"]).optional(),
4016
+ status: z.enum(["running", "waiting_approval", "idle", "error", "cancelled"]).optional(),
3314
4017
  pollInterval: z.number().int().optional().describe(
3315
4018
  "Recommended minimum delay before next poll (ms): running >=120000, waiting_approval ~=1000."
3316
4019
  ),
4020
+ result: z.unknown().optional().describe("Final result when waitForResult is set and session completed."),
4021
+ completedAt: z.string().optional().describe("ISO timestamp when the session completed (only when waitForResult succeeded)."),
4022
+ execution: executionInfoSchema.optional(),
4023
+ interactionState: interactionStateSchema.optional(),
4024
+ recommendedNextAction: nextActionSchema.optional(),
3317
4025
  ...errorOutputShape
3318
4026
  };
3319
4027
  const codexCheckPollOptionsSchema = z.object({
3320
4028
  includeEvents: z.boolean().optional().describe("Default: true. Include events[] in response."),
3321
4029
  includeActions: z.boolean().optional().describe("Default: true. Include actions[] in response."),
3322
4030
  includeResult: z.boolean().optional().describe("Default: true. Include result in response."),
3323
- maxBytes: z.number().int().positive().optional().describe("Default: unlimited. Best-effort response payload cap in bytes.")
4031
+ maxBytes: z.number().int().positive().optional().describe("Default: unlimited. Best-effort response payload cap in bytes."),
4032
+ waitMs: z.number().int().nonnegative().optional().describe(
4033
+ "Long-poll: block up to this many ms for new events (max 120000). Omit or 0 for immediate return."
4034
+ )
3324
4035
  }).optional().describe("Optional poll shaping controls.");
3325
4036
  const codexCheckInputSchema = z.object({
3326
4037
  action: z.enum(CHECK_ACTIONS),
@@ -3350,10 +4061,10 @@ function createServer(serverCwd, options) {
3350
4061
  })
3351
4062
  ).optional().describe("question-id -> answers map (id from actions[] user_input request).")
3352
4063
  }).superRefine((value, ctx) => {
3353
- const addIssue = (path5, message) => {
4064
+ const addIssue = (path6, message) => {
3354
4065
  ctx.addIssue({
3355
4066
  code: z.ZodIssueCode.custom,
3356
- path: [path5],
4067
+ path: [path6],
3357
4068
  message
3358
4069
  });
3359
4070
  };
@@ -3480,7 +4191,10 @@ function createServer(serverCwd, options) {
3480
4191
  ephemeral: z.boolean().optional().describe("Do not persist thread (default: false)."),
3481
4192
  outputSchema: z.record(z.string(), z.unknown()).optional().describe("Structured output schema."),
3482
4193
  images: z.array(z.string()).optional().describe("Local image paths."),
3483
- approvalTimeoutMs: z.number().int().positive().default(DEFAULT_APPROVAL_TIMEOUT_MS).optional().describe(`Auto-decline timeout in ms (default: ${DEFAULT_APPROVAL_TIMEOUT_MS})`)
4194
+ approvalTimeoutMs: z.number().int().positive().default(DEFAULT_APPROVAL_TIMEOUT_MS).optional().describe(`Auto-decline timeout in ms (default: ${DEFAULT_APPROVAL_TIMEOUT_MS})`),
4195
+ waitForResult: z.number().int().positive().max(3e5).optional().describe(
4196
+ "Block up to this many ms for session completion (max 300000). Falls back to sessionId for polling if not done in time. Only use with approvalPolicy on-failure/never."
4197
+ )
3484
4198
  }).optional().describe("Advanced settings.")
3485
4199
  },
3486
4200
  outputSchema: sessionStartOutputShape,
@@ -3492,9 +4206,9 @@ function createServer(serverCwd, options) {
3492
4206
  openWorldHint: true
3493
4207
  }
3494
4208
  },
3495
- async (args) => {
4209
+ async (args, extra) => {
3496
4210
  try {
3497
- const result = await executeCodex(args, sessionManager, serverCwd);
4211
+ const result = await executeCodex(args, sessionManager, serverCwd, extra.signal);
3498
4212
  return {
3499
4213
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3500
4214
  structuredContent: toStructuredContent(result),
@@ -3525,7 +4239,10 @@ function createServer(serverCwd, options) {
3525
4239
  personality: z.enum(PERSONALITIES).optional().describe("Override personality."),
3526
4240
  sandbox: z.enum(SANDBOX_MODES).optional().describe("Override sandbox."),
3527
4241
  cwd: z.string().optional().describe("Override cwd."),
3528
- outputSchema: z.record(z.string(), z.unknown()).optional().describe("Structured output schema override (top-level in codex_reply).")
4242
+ outputSchema: z.record(z.string(), z.unknown()).optional().describe("Structured output schema override (top-level in codex_reply)."),
4243
+ waitForResult: z.number().int().positive().max(3e5).optional().describe(
4244
+ "Wait up to this many ms for the reply turn to complete and return the result directly. Max 300000 (5 min). If the turn does not finish in time or enters interactive approval/user-input flow, returns session metadata for polling."
4245
+ )
3529
4246
  },
3530
4247
  outputSchema: sessionStartOutputShape,
3531
4248
  annotations: {
@@ -3536,9 +4253,9 @@ function createServer(serverCwd, options) {
3536
4253
  openWorldHint: true
3537
4254
  }
3538
4255
  },
3539
- async (args) => {
4256
+ async (args, extra) => {
3540
4257
  try {
3541
- const result = await executeCodexReply(args, sessionManager);
4258
+ const result = await executeCodexReply(args, sessionManager, extra.signal);
3542
4259
  return {
3543
4260
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3544
4261
  structuredContent: toStructuredContent(result),
@@ -3554,6 +4271,32 @@ function createServer(serverCwd, options) {
3554
4271
  }
3555
4272
  }
3556
4273
  );
4274
+ server.registerTool(
4275
+ "codex_setup",
4276
+ {
4277
+ title: "Codex Setup",
4278
+ description: "Run local readiness checks for codex-mcp: executable resolution, login status, detected backend mode, and project config. Use this before starting a session when setup is uncertain.",
4279
+ inputSchema: {
4280
+ cwd: z.string().optional().describe("Optional cwd to inspect for project-local Codex config. Default: server cwd.")
4281
+ },
4282
+ outputSchema: setupResultShape,
4283
+ annotations: {
4284
+ title: "Codex Setup",
4285
+ readOnlyHint: true,
4286
+ destructiveHint: false,
4287
+ idempotentHint: true,
4288
+ openWorldHint: false
4289
+ }
4290
+ },
4291
+ async (args) => {
4292
+ const result = await executeCodexSetup(args, serverCwd);
4293
+ return {
4294
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
4295
+ structuredContent: toStructuredContent(result),
4296
+ isError: false
4297
+ };
4298
+ }
4299
+ );
3557
4300
  server.registerTool(
3558
4301
  "codex_session",
3559
4302
  {
@@ -3625,23 +4368,11 @@ function createServer(serverCwd, options) {
3625
4368
  "codex_check",
3626
4369
  {
3627
4370
  title: "Poll & Respond",
3628
- description: `Poll session for events or respond to approval/input requests.
3629
-
3630
- POLLING FREQUENCY: Do NOT poll every turn. Codex tasks take minutes, not seconds.
3631
- - Treat pollInterval as a minimum hint, not a fixed schedule.
3632
- - "running": sleep at least 2 minutes between polls; increase for complex tasks. Do NOT high-frequency poll \u2014 it wastes tokens and provides no benefit.
3633
- - "waiting_approval": poll about every 1000ms and respond quickly to actions[].
3634
- - When status is "idle"/"error"/"cancelled": stop polling, the session is done.
3635
- - Adapt interval based on task complexity and whether the previous poll returned new events.
4371
+ description: `Poll session for events or respond to approval/input requests. Use pollInterval as a minimum hint; stop polling on terminal status (idle/error/cancelled). See codex-mcp:///gotchas for poll frequency guidance.
3636
4372
 
3637
4373
  poll: events since cursor. Default maxEvents=${POLL_DEFAULT_MAX_EVENTS}.
3638
-
3639
4374
  respond_permission: approval decision. Default maxEvents=${RESPOND_DEFAULT_MAX_EVENTS} (compact ACK).
3640
-
3641
- respond_user_input: user-input answers. Default maxEvents=${RESPOND_DEFAULT_MAX_EVENTS} (compact ACK).
3642
-
3643
- events[].type is coarse-grained; details are in events[].data.method.
3644
- cursor omitted => use session last cursor. cursorResetTo => reset and continue.`,
4375
+ respond_user_input: user-input answers. Default maxEvents=${RESPOND_DEFAULT_MAX_EVENTS} (compact ACK).`,
3645
4376
  inputSchema: codexCheckInputSchema,
3646
4377
  outputSchema: {
3647
4378
  sessionId: z.string().optional(),
@@ -3649,6 +4380,8 @@ cursor omitted => use session last cursor. cursorResetTo => reset and continue.`
3649
4380
  pollInterval: z.number().int().optional().describe(
3650
4381
  "Recommended minimum delay before next poll (ms): running >=120000, waiting_approval ~=1000."
3651
4382
  ),
4383
+ interactionState: interactionStateSchema.optional(),
4384
+ recommendedNextAction: nextActionSchema.optional(),
3652
4385
  events: z.array(
3653
4386
  z.object({
3654
4387
  id: z.number().int(),
@@ -3703,9 +4436,9 @@ cursor omitted => use session last cursor. cursorResetTo => reset and continue.`
3703
4436
  openWorldHint: false
3704
4437
  }
3705
4438
  },
3706
- async (args) => {
4439
+ async (args, extra) => {
3707
4440
  try {
3708
- const result = executeCodexCheck(args, sessionManager);
4441
+ const result = await executeCodexCheck(args, sessionManager, extra.signal);
3709
4442
  const isError = typeof result.isError === "boolean" ? result.isError : false;
3710
4443
  return {
3711
4444
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
@@ -3727,81 +4460,7 @@ cursor omitted => use session last cursor. cursorResetTo => reset and continue.`
3727
4460
  sessionManager.destroy();
3728
4461
  await originalClose();
3729
4462
  };
3730
- return server;
3731
- }
3732
-
3733
- // src/app-server/detect.ts
3734
- import { spawn as spawn2 } from "child_process";
3735
- var DETECTION_TIMEOUT_MS = 5e3;
3736
- async function detectClientMode(codexCommand, codexIsPath = false, env = process.env) {
3737
- const override = env.CODEX_MCP_MODE;
3738
- if (override === "app-server" || override === "exec") {
3739
- return override;
3740
- }
3741
- try {
3742
- const supported = await probeAppServer(codexCommand, codexIsPath, env);
3743
- return supported ? "app-server" : "exec";
3744
- } catch {
3745
- return "exec";
3746
- }
3747
- }
3748
- function probeAppServer(codexCommand, codexIsPath, env) {
3749
- return new Promise((resolve) => {
3750
- const invocation = resolveCodexInvocation(["app-server", "--help"], {
3751
- env,
3752
- codexCommand,
3753
- codexIsPath
3754
- });
3755
- let settled = false;
3756
- const settle = (result) => {
3757
- if (settled) return;
3758
- settled = true;
3759
- clearTimeout(timer);
3760
- resolve(result);
3761
- };
3762
- let stdout = "";
3763
- let stderr = "";
3764
- const proc = spawn2(invocation.cmd, invocation.args, {
3765
- stdio: ["ignore", "pipe", "pipe"],
3766
- env,
3767
- windowsHide: true
3768
- });
3769
- proc.stdout?.on("data", (chunk) => {
3770
- stdout += chunk.toString();
3771
- });
3772
- proc.stderr?.on("data", (chunk) => {
3773
- stderr += chunk.toString();
3774
- });
3775
- proc.on("error", () => {
3776
- settle(false);
3777
- });
3778
- proc.on("exit", (code) => {
3779
- if (code === 0) {
3780
- settle(true);
3781
- } else {
3782
- const combined = (stdout + stderr).toLowerCase();
3783
- const isUnknown = combined.includes("unknown") || combined.includes("unrecognized") || combined.includes("not found") || combined.includes("no such subcommand");
3784
- settle(!isUnknown && combined.includes("app-server"));
3785
- }
3786
- });
3787
- const timer = setTimeout(() => {
3788
- try {
3789
- proc.kill("SIGTERM");
3790
- } catch {
3791
- }
3792
- const forceKill = setTimeout(() => {
3793
- try {
3794
- if (!proc.killed && proc.exitCode === null) {
3795
- proc.kill("SIGKILL");
3796
- }
3797
- } catch {
3798
- }
3799
- }, 2e3);
3800
- if (forceKill.unref) forceKill.unref();
3801
- settle(false);
3802
- }, DETECTION_TIMEOUT_MS);
3803
- if (timer.unref) timer.unref();
3804
- });
4463
+ return { server, sessionManager };
3805
4464
  }
3806
4465
 
3807
4466
  // src/app-server/exec-client.ts
@@ -3916,6 +4575,9 @@ var ExecClient = class extends EventEmitter2 {
3916
4575
  get supportsTurnOverrides() {
3917
4576
  return this.turnCount <= 1 || this.realThreadId == null;
3918
4577
  }
4578
+ get childPid() {
4579
+ return this.process?.pid ?? void 0;
4580
+ }
3919
4581
  async start(opts) {
3920
4582
  if (this._destroyed) throw new Error("Client destroyed");
3921
4583
  this.spawnOpts = opts;
@@ -4349,7 +5011,625 @@ var ExecClient = class extends EventEmitter2 {
4349
5011
  }
4350
5012
  };
4351
5013
 
5014
+ // src/session/persistence.ts
5015
+ import { join as join5 } from "path";
5016
+ import { mkdirSync as mkdirSync4, existsSync as existsSync7 } from "fs";
5017
+ import { homedir as homedir2 } from "os";
5018
+
5019
+ // src/persistence/atomic-writer.ts
5020
+ import { writeFileSync as writeFileSync2, renameSync, mkdirSync, unlinkSync } from "fs";
5021
+ import { dirname, join as join2 } from "path";
5022
+ import { randomBytes } from "crypto";
5023
+ function atomicWriteJson(filePath, data) {
5024
+ const dir = dirname(filePath);
5025
+ mkdirSync(dir, { recursive: true });
5026
+ const tmpPath = join2(dir, `.tmp-${randomBytes(6).toString("hex")}`);
5027
+ try {
5028
+ writeFileSync2(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
5029
+ renameSync(tmpPath, filePath);
5030
+ } catch (err) {
5031
+ try {
5032
+ unlinkSync(tmpPath);
5033
+ } catch {
5034
+ }
5035
+ throw err;
5036
+ }
5037
+ }
5038
+
5039
+ // src/persistence/lockfile.ts
5040
+ import {
5041
+ openSync,
5042
+ closeSync,
5043
+ readFileSync as readFileSync2,
5044
+ writeFileSync as writeFileSync3,
5045
+ unlinkSync as unlinkSync2,
5046
+ mkdirSync as mkdirSync2,
5047
+ constants as constants2
5048
+ } from "fs";
5049
+ import { dirname as dirname2 } from "path";
5050
+ function isPidAlive(pid) {
5051
+ try {
5052
+ process.kill(pid, 0);
5053
+ return true;
5054
+ } catch {
5055
+ return false;
5056
+ }
5057
+ }
5058
+ function acquireLock(lockPath) {
5059
+ mkdirSync2(dirname2(lockPath), { recursive: true });
5060
+ try {
5061
+ const raw = readFileSync2(lockPath, "utf-8");
5062
+ const existing = JSON.parse(raw);
5063
+ if (isPidAlive(existing.pid) && existing.pid !== process.pid) {
5064
+ throw new Error(
5065
+ `STATE_DIR is locked by another process (pid=${existing.pid}, started=${existing.startedAt}). If this is stale, delete ${lockPath}`
5066
+ );
5067
+ }
5068
+ unlinkSync2(lockPath);
5069
+ } catch (err) {
5070
+ if (err.code !== "ENOENT") {
5071
+ if (err.message?.includes("STATE_DIR is locked")) throw err;
5072
+ console.error(
5073
+ `[lockfile] Existing lock at ${lockPath} is unreadable \u2014 will attempt O_EXCL create`
5074
+ );
5075
+ }
5076
+ }
5077
+ const content = {
5078
+ pid: process.pid,
5079
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
5080
+ };
5081
+ let fd;
5082
+ try {
5083
+ fd = openSync(lockPath, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL);
5084
+ writeFileSync3(fd, JSON.stringify(content) + "\n", "utf-8");
5085
+ closeSync(fd);
5086
+ fd = void 0;
5087
+ } catch (err) {
5088
+ if (fd !== void 0) {
5089
+ try {
5090
+ closeSync(fd);
5091
+ } catch {
5092
+ }
5093
+ }
5094
+ if (err.code === "EEXIST") {
5095
+ throw new Error(
5096
+ `Failed to acquire STATE_DIR lock at ${lockPath} \u2014 race with another process`
5097
+ );
5098
+ }
5099
+ try {
5100
+ unlinkSync2(lockPath);
5101
+ } catch {
5102
+ }
5103
+ throw err;
5104
+ }
5105
+ let released = false;
5106
+ return () => {
5107
+ if (released) return;
5108
+ released = true;
5109
+ try {
5110
+ unlinkSync2(lockPath);
5111
+ } catch {
5112
+ }
5113
+ };
5114
+ }
5115
+
5116
+ // src/persistence/event-log.ts
5117
+ import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
5118
+ import { dirname as dirname3 } from "path";
5119
+ var EventLog = class {
5120
+ filePath;
5121
+ batchIntervalMs;
5122
+ buffer = [];
5123
+ flushTimer = null;
5124
+ nextSeq = 0;
5125
+ destroyed = false;
5126
+ constructor(opts) {
5127
+ this.filePath = opts.filePath;
5128
+ this.batchIntervalMs = opts.batchIntervalMs ?? 100;
5129
+ mkdirSync3(dirname3(this.filePath), { recursive: true });
5130
+ }
5131
+ /**
5132
+ * Append an event to the log.
5133
+ * @param event - Arbitrary JSON-serializable event data
5134
+ * @param criticality - "critical" forces immediate flush; "normal" batches
5135
+ */
5136
+ append(event, criticality = "normal") {
5137
+ if (this.destroyed) return -1;
5138
+ const seq = this.nextSeq++;
5139
+ const line = JSON.stringify({ seq, ...event }) + "\n";
5140
+ this.buffer.push({ line });
5141
+ if (criticality === "critical") {
5142
+ this.flushSync();
5143
+ } else if (!this.flushTimer) {
5144
+ this.flushTimer = setTimeout(() => {
5145
+ this.flushTimer = null;
5146
+ this.flushSync();
5147
+ }, this.batchIntervalMs);
5148
+ if (this.flushTimer.unref) this.flushTimer.unref();
5149
+ }
5150
+ return seq;
5151
+ }
5152
+ /** Set the next sequence number (used during recovery to continue from last known seq). */
5153
+ setNextSeq(seq) {
5154
+ this.nextSeq = seq;
5155
+ }
5156
+ /** Synchronously flush all buffered writes to disk. */
5157
+ flushSync() {
5158
+ if (this.buffer.length === 0) return;
5159
+ const chunk = this.buffer.map((w) => w.line).join("");
5160
+ this.buffer = [];
5161
+ if (this.flushTimer) {
5162
+ clearTimeout(this.flushTimer);
5163
+ this.flushTimer = null;
5164
+ }
5165
+ try {
5166
+ appendFileSync(this.filePath, chunk, "utf-8");
5167
+ } catch (err) {
5168
+ console.error(`[event-log] Failed to flush to ${this.filePath}:`, err);
5169
+ }
5170
+ }
5171
+ /** Destroy the log, flushing any remaining events. */
5172
+ destroy() {
5173
+ if (this.destroyed) return;
5174
+ this.destroyed = true;
5175
+ this.flushSync();
5176
+ if (this.flushTimer) {
5177
+ clearTimeout(this.flushTimer);
5178
+ this.flushTimer = null;
5179
+ }
5180
+ }
5181
+ };
5182
+
5183
+ // src/persistence/recovery-scanner.ts
5184
+ import { readdirSync, readFileSync as readFileSync3, existsSync as existsSync6, statSync as statSync4 } from "fs";
5185
+ import { join as join3 } from "path";
5186
+ var SCHEMA_VERSION = 1;
5187
+ function parseEventsJsonl(filePath) {
5188
+ if (!existsSync6(filePath)) return [];
5189
+ const raw = readFileSync3(filePath, "utf-8");
5190
+ const lines = raw.split("\n");
5191
+ const events = [];
5192
+ for (const line of lines) {
5193
+ const trimmed = line.trim();
5194
+ if (!trimmed) continue;
5195
+ try {
5196
+ const parsed = JSON.parse(trimmed);
5197
+ if (typeof parsed.seq === "number") {
5198
+ events.push(parsed);
5199
+ }
5200
+ } catch {
5201
+ break;
5202
+ }
5203
+ }
5204
+ return events.sort((a, b) => a.seq - b.seq);
5205
+ }
5206
+ function readJsonSafe(filePath) {
5207
+ if (!existsSync6(filePath)) return null;
5208
+ try {
5209
+ return JSON.parse(readFileSync3(filePath, "utf-8"));
5210
+ } catch {
5211
+ return null;
5212
+ }
5213
+ }
5214
+ function scanRecoverableSessions(sessionsDir, maxEvents = 500) {
5215
+ if (!existsSync6(sessionsDir)) return [];
5216
+ const results = [];
5217
+ let entries;
5218
+ try {
5219
+ entries = readdirSync(sessionsDir);
5220
+ } catch {
5221
+ return [];
5222
+ }
5223
+ for (const entry of entries) {
5224
+ const sessionDir = join3(sessionsDir, entry);
5225
+ try {
5226
+ if (!statSync4(sessionDir).isDirectory()) continue;
5227
+ } catch {
5228
+ continue;
5229
+ }
5230
+ const meta = readJsonSafe(join3(sessionDir, "meta.json"));
5231
+ if (!meta || !meta.sessionId) continue;
5232
+ if (meta.schemaVersion !== void 0 && meta.schemaVersion > SCHEMA_VERSION) {
5233
+ console.error(
5234
+ `[recovery] Skipping session ${meta.sessionId}: schema version ${meta.schemaVersion} > ${SCHEMA_VERSION}`
5235
+ );
5236
+ continue;
5237
+ }
5238
+ let events = parseEventsJsonl(join3(sessionDir, "events.jsonl"));
5239
+ if (events.length > maxEvents) {
5240
+ events = events.slice(-maxEvents);
5241
+ }
5242
+ const lastSeq = events.length > 0 ? events[events.length - 1].seq : -1;
5243
+ const result = readJsonSafe(join3(sessionDir, "result.json"));
5244
+ const pidInfo = readJsonSafe(join3(sessionDir, "pid.json"));
5245
+ results.push({
5246
+ sessionId: meta.sessionId,
5247
+ meta,
5248
+ events,
5249
+ lastSeq,
5250
+ result,
5251
+ pidInfo,
5252
+ sessionDir
5253
+ });
5254
+ }
5255
+ return results;
5256
+ }
5257
+
5258
+ // src/persistence/retention.ts
5259
+ import { readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync5, readFileSync as readFileSync4 } from "fs";
5260
+ import { join as join4 } from "path";
5261
+ var DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
5262
+ var DEFAULT_MAX_COUNT = 200;
5263
+ var DEFAULT_MAX_DISK_BYTES = 500 * 1024 * 1024;
5264
+ function getDirSize(dirPath) {
5265
+ let total = 0;
5266
+ try {
5267
+ for (const entry of readdirSync2(dirPath)) {
5268
+ try {
5269
+ const st = statSync5(join4(dirPath, entry));
5270
+ total += st.isFile() ? st.size : 0;
5271
+ } catch {
5272
+ }
5273
+ }
5274
+ } catch {
5275
+ }
5276
+ return total;
5277
+ }
5278
+ function pruneSessionDirs(sessionsDir, policy) {
5279
+ const maxAgeMs = policy?.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
5280
+ const maxCount = policy?.maxCount ?? DEFAULT_MAX_COUNT;
5281
+ const maxDiskBytes = policy?.maxDiskBytes ?? DEFAULT_MAX_DISK_BYTES;
5282
+ let entries;
5283
+ try {
5284
+ entries = readdirSync2(sessionsDir);
5285
+ } catch {
5286
+ return 0;
5287
+ }
5288
+ const now = Date.now();
5289
+ const dirs = [];
5290
+ for (const entry of entries) {
5291
+ const dirPath = join4(sessionsDir, entry);
5292
+ try {
5293
+ if (!statSync5(dirPath).isDirectory()) continue;
5294
+ } catch {
5295
+ continue;
5296
+ }
5297
+ let lastActiveAt = 0;
5298
+ try {
5299
+ const meta = JSON.parse(readFileSync4(join4(dirPath, "meta.json"), "utf-8"));
5300
+ lastActiveAt = new Date(meta.lastActiveAt || meta.createdAt || 0).getTime();
5301
+ } catch {
5302
+ try {
5303
+ lastActiveAt = statSync5(dirPath).mtimeMs;
5304
+ } catch {
5305
+ lastActiveAt = 0;
5306
+ }
5307
+ }
5308
+ dirs.push({
5309
+ path: dirPath,
5310
+ sessionId: entry,
5311
+ lastActiveAt,
5312
+ diskBytes: getDirSize(dirPath)
5313
+ });
5314
+ }
5315
+ dirs.sort((a, b) => a.lastActiveAt - b.lastActiveAt);
5316
+ const toRemove = /* @__PURE__ */ new Set();
5317
+ for (const dir of dirs) {
5318
+ if (now - dir.lastActiveAt > maxAgeMs) {
5319
+ toRemove.add(dir.path);
5320
+ }
5321
+ }
5322
+ const remaining = dirs.filter((d) => !toRemove.has(d.path));
5323
+ if (remaining.length > maxCount) {
5324
+ const excess = remaining.length - maxCount;
5325
+ for (let i = 0; i < excess; i++) {
5326
+ toRemove.add(remaining[i].path);
5327
+ }
5328
+ }
5329
+ const afterCountPrune = dirs.filter((d) => !toRemove.has(d.path));
5330
+ let totalSize = afterCountPrune.reduce((sum, d) => sum + d.diskBytes, 0);
5331
+ for (const dir of afterCountPrune) {
5332
+ if (totalSize <= maxDiskBytes) break;
5333
+ toRemove.add(dir.path);
5334
+ totalSize -= dir.diskBytes;
5335
+ }
5336
+ let pruned = 0;
5337
+ for (const dirPath of toRemove) {
5338
+ try {
5339
+ rmSync2(dirPath, { recursive: true, force: true });
5340
+ pruned++;
5341
+ } catch (err) {
5342
+ console.error(`[retention] Failed to remove ${dirPath}:`, err);
5343
+ }
5344
+ }
5345
+ return pruned;
5346
+ }
5347
+
5348
+ // src/session/persistence.ts
5349
+ var CRITICAL_EVENT_TYPES = /* @__PURE__ */ new Set([
5350
+ "approval_request",
5351
+ "approval_result",
5352
+ "result",
5353
+ "error"
5354
+ ]);
5355
+ function eventCriticality(type) {
5356
+ return CRITICAL_EVENT_TYPES.has(type) ? "critical" : "normal";
5357
+ }
5358
+ var SessionPersistence = class {
5359
+ stateDir;
5360
+ sessionsDir;
5361
+ releaseLock = null;
5362
+ eventLogs = /* @__PURE__ */ new Map();
5363
+ constructor(stateDir) {
5364
+ this.stateDir = stateDir ?? process.env.CODEX_MCP_STATE_DIR ?? join5(homedir2(), ".codex-mcp", "state");
5365
+ this.sessionsDir = join5(this.stateDir, "sessions");
5366
+ mkdirSync4(this.sessionsDir, { recursive: true });
5367
+ }
5368
+ /** Acquire the STATE_DIR lock. Call once at startup. */
5369
+ acquireLock() {
5370
+ this.releaseLock = acquireLock(join5(this.stateDir, ".lock"));
5371
+ }
5372
+ /** Release the lock. Call on shutdown. */
5373
+ releaseLockIfHeld() {
5374
+ if (this.releaseLock) {
5375
+ this.releaseLock();
5376
+ this.releaseLock = null;
5377
+ }
5378
+ }
5379
+ // ── Write operations ────────────────────────────────────────────
5380
+ /** Persist session metadata (called on create and status changes). */
5381
+ writeSessionMeta(session) {
5382
+ const meta = {
5383
+ schemaVersion: SCHEMA_VERSION,
5384
+ sessionId: session.sessionId,
5385
+ status: session.status,
5386
+ createdAt: session.createdAt,
5387
+ lastActiveAt: session.lastActiveAt,
5388
+ cancelledAt: session.cancelledAt,
5389
+ cancelledReason: session.cancelledReason,
5390
+ threadId: session.threadId,
5391
+ model: session.model,
5392
+ cwd: session.cwd,
5393
+ approvalPolicy: session.approvalPolicy,
5394
+ sandbox: session.sandbox,
5395
+ profile: session.profile
5396
+ };
5397
+ const dir = join5(this.sessionsDir, session.sessionId);
5398
+ atomicWriteJson(join5(dir, "meta.json"), meta);
5399
+ }
5400
+ /** Persist PID info for orphan detection. */
5401
+ writePidInfo(sessionId, pid, command) {
5402
+ const info = {
5403
+ pid,
5404
+ spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
5405
+ command
5406
+ };
5407
+ const dir = join5(this.sessionsDir, sessionId);
5408
+ mkdirSync4(dir, { recursive: true });
5409
+ atomicWriteJson(join5(dir, "pid.json"), info);
5410
+ }
5411
+ /** Append an event to the session's event log. */
5412
+ appendEvent(sessionId, type, data) {
5413
+ let log = this.eventLogs.get(sessionId);
5414
+ if (!log) {
5415
+ const dir = join5(this.sessionsDir, sessionId);
5416
+ mkdirSync4(dir, { recursive: true });
5417
+ log = new EventLog({ filePath: join5(dir, "events.jsonl") });
5418
+ this.eventLogs.set(sessionId, log);
5419
+ }
5420
+ log.append({ type, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, eventCriticality(type));
5421
+ }
5422
+ /** Set the next sequence number for a recovered session's event log. */
5423
+ setEventLogNextSeq(sessionId, seq) {
5424
+ let log = this.eventLogs.get(sessionId);
5425
+ if (!log) {
5426
+ const dir = join5(this.sessionsDir, sessionId);
5427
+ mkdirSync4(dir, { recursive: true });
5428
+ log = new EventLog({ filePath: join5(dir, "events.jsonl") });
5429
+ this.eventLogs.set(sessionId, log);
5430
+ }
5431
+ log.setNextSeq(seq);
5432
+ }
5433
+ /** Persist the final result. */
5434
+ writeResult(sessionId, result) {
5435
+ const dir = join5(this.sessionsDir, sessionId);
5436
+ mkdirSync4(dir, { recursive: true });
5437
+ atomicWriteJson(join5(dir, "result.json"), result);
5438
+ }
5439
+ // ── Read / Recovery ─────────────────────────────────────────────
5440
+ /** Scan and recover sessions from disk. */
5441
+ recoverSessions(maxEvents) {
5442
+ return scanRecoverableSessions(this.sessionsDir, maxEvents);
5443
+ }
5444
+ /** Apply retention policy. Returns number of sessions pruned. */
5445
+ prune(policy) {
5446
+ return pruneSessionDirs(this.sessionsDir, policy);
5447
+ }
5448
+ // ── Lifecycle ───────────────────────────────────────────────────
5449
+ /** Flush all event logs synchronously (call on shutdown). */
5450
+ flushAll() {
5451
+ for (const log of this.eventLogs.values()) {
5452
+ log.flushSync();
5453
+ }
5454
+ }
5455
+ /** Destroy a single session's event log. */
5456
+ destroySessionLog(sessionId) {
5457
+ const log = this.eventLogs.get(sessionId);
5458
+ if (log) {
5459
+ log.destroy();
5460
+ this.eventLogs.delete(sessionId);
5461
+ }
5462
+ }
5463
+ /** Clean up: flush all logs, release lock. */
5464
+ destroy() {
5465
+ for (const log of this.eventLogs.values()) {
5466
+ log.destroy();
5467
+ }
5468
+ this.eventLogs.clear();
5469
+ this.releaseLockIfHeld();
5470
+ }
5471
+ /** Check if a session directory exists on disk. */
5472
+ hasSessionOnDisk(sessionId) {
5473
+ return existsSync7(join5(this.sessionsDir, sessionId, "meta.json"));
5474
+ }
5475
+ };
5476
+
5477
+ // src/session/orphan-reaper.ts
5478
+ import { execSync, spawn as spawn4 } from "child_process";
5479
+ import { readFileSync as readFileSync5 } from "fs";
5480
+ function isAlive(pid) {
5481
+ try {
5482
+ process.kill(pid, 0);
5483
+ return true;
5484
+ } catch {
5485
+ return false;
5486
+ }
5487
+ }
5488
+ function getWindowsCreationTimeMs(pid) {
5489
+ try {
5490
+ const raw = execSync(`wmic process where "ProcessId=${pid}" get CreationDate /value`, {
5491
+ stdio: ["ignore", "pipe", "ignore"],
5492
+ timeout: 5e3
5493
+ }).toString();
5494
+ const match = raw.match(/CreationDate=(\d{14})/);
5495
+ if (!match || !match[1]) return null;
5496
+ const s = match[1];
5497
+ const iso = `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}T${s.slice(8, 10)}:${s.slice(10, 12)}:${s.slice(12, 14)}.000Z`;
5498
+ const ms = new Date(iso).getTime();
5499
+ return isNaN(ms) ? null : ms;
5500
+ } catch {
5501
+ return null;
5502
+ }
5503
+ }
5504
+ function getProcStatStartTick(pid) {
5505
+ try {
5506
+ const stat = readFileSync5(`/proc/${pid}/stat`, "utf-8");
5507
+ const afterComm = stat.slice(stat.lastIndexOf(")") + 1).trim();
5508
+ const fields = afterComm.split(" ");
5509
+ const starttime = fields[19];
5510
+ return starttime ?? null;
5511
+ } catch {
5512
+ return null;
5513
+ }
5514
+ }
5515
+ function getPsLstart(pid) {
5516
+ try {
5517
+ const output = execSync(`ps -p ${pid} -o lstart=`, {
5518
+ stdio: ["ignore", "pipe", "ignore"],
5519
+ timeout: 5e3
5520
+ }).toString().trim();
5521
+ if (!output) return null;
5522
+ const ms = new Date(output).getTime();
5523
+ return isNaN(ms) ? null : ms;
5524
+ } catch {
5525
+ return null;
5526
+ }
5527
+ }
5528
+ function isOrphan(pid, spawnedAt) {
5529
+ const storedMs = new Date(spawnedAt).getTime();
5530
+ if (isNaN(storedMs)) return false;
5531
+ if (process.platform === "win32") {
5532
+ const procMs = getWindowsCreationTimeMs(pid);
5533
+ if (procMs === null) return false;
5534
+ return Math.abs(procMs - storedMs) < 5e3;
5535
+ }
5536
+ const lstartMs = getPsLstart(pid);
5537
+ if (lstartMs !== null) {
5538
+ return Math.abs(lstartMs - storedMs) < 5e3;
5539
+ }
5540
+ const tick = getProcStatStartTick(pid);
5541
+ if (tick !== null) {
5542
+ const ageMs = Date.now() - storedMs;
5543
+ return ageMs > 0 && ageMs < 24 * 60 * 60 * 1e3;
5544
+ }
5545
+ return false;
5546
+ }
5547
+ function sendGraceful(pid) {
5548
+ try {
5549
+ if (process.platform === "win32") {
5550
+ spawn4("taskkill", ["/PID", String(pid)], {
5551
+ stdio: "ignore",
5552
+ windowsHide: true
5553
+ });
5554
+ } else {
5555
+ process.kill(pid, "SIGTERM");
5556
+ }
5557
+ } catch {
5558
+ }
5559
+ }
5560
+ function sendForce(pid) {
5561
+ try {
5562
+ if (process.platform === "win32") {
5563
+ spawn4("taskkill", ["/PID", String(pid), "/F"], {
5564
+ stdio: "ignore",
5565
+ windowsHide: true
5566
+ });
5567
+ } else {
5568
+ process.kill(pid, "SIGKILL");
5569
+ }
5570
+ } catch {
5571
+ }
5572
+ }
5573
+ function sleep(ms) {
5574
+ return new Promise((resolve) => {
5575
+ const t = setTimeout(resolve, ms);
5576
+ if (t.unref) t.unref();
5577
+ });
5578
+ }
5579
+ async function reapOrphanProcesses(recovered) {
5580
+ const summary = { reaped: 0, alreadyDead: 0, skipped: 0 };
5581
+ const candidates = recovered.filter((s) => s.pidInfo !== null);
5582
+ if (candidates.length === 0) return summary;
5583
+ const reapPromises = candidates.map(async (session) => {
5584
+ const { pid, spawnedAt } = session.pidInfo;
5585
+ if (!isAlive(pid)) {
5586
+ summary.alreadyDead++;
5587
+ return;
5588
+ }
5589
+ if (!isOrphan(pid, spawnedAt)) {
5590
+ console.error(
5591
+ `[orphan-reaper] PID ${pid} (session ${session.sessionId}) is alive but does not match stored spawn time \u2014 likely a reused PID. Skipping.`
5592
+ );
5593
+ summary.skipped++;
5594
+ return;
5595
+ }
5596
+ if (!isAlive(pid) || !isOrphan(pid, spawnedAt)) {
5597
+ summary.skipped++;
5598
+ return;
5599
+ }
5600
+ console.error(
5601
+ `[orphan-reaper] Sending graceful terminate to orphan PID ${pid} (session ${session.sessionId}).`
5602
+ );
5603
+ sendGraceful(pid);
5604
+ const deadline = Date.now() + 5e3;
5605
+ while (isAlive(pid) && Date.now() < deadline) {
5606
+ await sleep(250);
5607
+ }
5608
+ if (isAlive(pid)) {
5609
+ console.error(
5610
+ `[orphan-reaper] PID ${pid} did not exit after graceful terminate \u2014 force killing.`
5611
+ );
5612
+ sendForce(pid);
5613
+ await sleep(500);
5614
+ }
5615
+ summary.reaped++;
5616
+ });
5617
+ await Promise.all(reapPromises);
5618
+ return summary;
5619
+ }
5620
+
5621
+ // src/utils/stdin-shutdown.ts
5622
+ function decideStdinShutdown(params) {
5623
+ if (!params.stdinUnavailable) return "clear";
5624
+ if (params.isConnected) return "reschedule";
5625
+ if (!params.hasActiveSessions) return "shutdown_now";
5626
+ if (params.elapsedMs >= params.maxWaitMs) return "shutdown_timeout";
5627
+ return "reschedule";
5628
+ }
5629
+
4352
5630
  // src/index.ts
5631
+ var STDIN_SHUTDOWN_CHECK_MS = 750;
5632
+ var STDIN_SHUTDOWN_MAX_WAIT_MS = process.platform === "win32" ? 15e3 : 1e4;
4353
5633
  async function main() {
4354
5634
  const preflight = runStdioPreflight();
4355
5635
  for (const note of preflight.notes) {
@@ -4374,27 +5654,177 @@ async function main() {
4374
5654
  const clientMode = await detectClientMode(executable.command, executable.isPath);
4375
5655
  console.error(`[codex-mcp] client mode: ${clientMode} (binary: ${executable.command})`);
4376
5656
  const createClient = () => clientMode === "exec" ? new ExecClient() : new AppServerClient();
5657
+ const persistence = new SessionPersistence();
5658
+ try {
5659
+ persistence.acquireLock();
5660
+ console.error("[codex-mcp] STATE_DIR lock acquired");
5661
+ } catch (err) {
5662
+ console.error("[codex-mcp] WARNING: Failed to acquire STATE_DIR lock:", err);
5663
+ }
5664
+ const recovered = persistence.recoverSessions();
5665
+ if (recovered.length > 0) {
5666
+ console.error(`[codex-mcp] Recovered ${recovered.length} session(s) from disk`);
5667
+ }
5668
+ const pruned = persistence.prune();
5669
+ if (pruned > 0) {
5670
+ console.error(`[codex-mcp] Pruned ${pruned} old session(s)`);
5671
+ }
5672
+ const reaped = await reapOrphanProcesses(recovered);
5673
+ if (reaped.reaped > 0) console.error(`[codex-mcp] Reaped ${reaped.reaped} orphan process(es)`);
4377
5674
  const serverCwd = process.cwd();
4378
- const server = createServer(serverCwd, {
5675
+ const ctx = createServer(serverCwd, {
4379
5676
  createClient,
4380
- clientMode
5677
+ clientMode,
5678
+ persistence
4381
5679
  });
5680
+ const server = ctx.server;
5681
+ const sessionManager = ctx.sessionManager;
5682
+ if (recovered.length > 0) {
5683
+ sessionManager.ingestRecovered(recovered);
5684
+ }
4382
5685
  const transport = new StdioServerTransport();
4383
5686
  let closing = false;
4384
- const shutdown = async () => {
5687
+ let lastExitCode = 0;
5688
+ let stdinClosedAt;
5689
+ let stdinClosedReason;
5690
+ let stdinShutdownTimer;
5691
+ const onStdinEnd = () => handleStdinTerminated("end");
5692
+ const onStdinClose = () => handleStdinTerminated("close");
5693
+ const clearStdinShutdownTimer = () => {
5694
+ if (stdinShutdownTimer) {
5695
+ clearTimeout(stdinShutdownTimer);
5696
+ stdinShutdownTimer = void 0;
5697
+ }
5698
+ };
5699
+ function hasActiveSessions() {
5700
+ return sessionManager.listSessions().some((s) => s.status === "running" || s.status === "waiting_approval");
5701
+ }
5702
+ const shutdown = async (reason = "unknown") => {
4385
5703
  if (closing) return;
4386
5704
  closing = true;
5705
+ clearStdinShutdownTimer();
5706
+ if (typeof process.stdin.off === "function") {
5707
+ process.stdin.off("error", handleStdinError);
5708
+ process.stdin.off("end", onStdinEnd);
5709
+ process.stdin.off("close", onStdinClose);
5710
+ }
5711
+ const forceExitMs = process.platform === "win32" ? 1e4 : 5e3;
5712
+ const forceExitTimer = setTimeout(() => process.exit(lastExitCode), forceExitMs);
5713
+ if (forceExitTimer.unref) forceExitTimer.unref();
5714
+ const activeSessions = sessionManager.listSessions();
5715
+ const runningCount = activeSessions.filter(
5716
+ (s) => s.status === "running" || s.status === "waiting_approval"
5717
+ ).length;
5718
+ console.error(
5719
+ `[codex-mcp] shutdown triggered (reason=${reason}, activeSessions=${runningCount}, total=${activeSessions.length})`
5720
+ );
5721
+ try {
5722
+ if (server.isConnected()) {
5723
+ await server.sendLoggingMessage({
5724
+ level: "info",
5725
+ data: {
5726
+ event: "server_stopping",
5727
+ reason,
5728
+ activeSessions: runningCount,
5729
+ totalSessions: activeSessions.length
5730
+ }
5731
+ });
5732
+ }
5733
+ } catch {
5734
+ }
5735
+ try {
5736
+ persistence.flushAll();
5737
+ persistence.releaseLockIfHeld();
5738
+ } catch {
5739
+ }
4387
5740
  try {
4388
5741
  await server.close();
4389
5742
  } catch {
4390
5743
  }
4391
- process.exitCode = 0;
4392
- const exitTimer = setTimeout(() => process.exit(0), 100);
4393
- exitTimer.unref();
5744
+ persistence.destroy();
5745
+ process.exitCode = lastExitCode;
5746
+ try {
5747
+ await new Promise((resolve) => process.stderr.write("", () => resolve()));
5748
+ } catch {
5749
+ } finally {
5750
+ clearTimeout(forceExitTimer);
5751
+ }
4394
5752
  };
4395
- process.on("SIGINT", shutdown);
4396
- process.on("SIGTERM", shutdown);
4397
- process.on("SIGBREAK", shutdown);
5753
+ function handleStdinError(error) {
5754
+ console.error("[codex-mcp] stdin error:", error);
5755
+ lastExitCode = 1;
5756
+ void shutdown("stdin_error");
5757
+ }
5758
+ const evaluateStdinTermination = () => {
5759
+ if (closing || stdinClosedAt === void 0) return;
5760
+ const stdinUnavailable = process.stdin.destroyed || process.stdin.readableEnded || !process.stdin.readable;
5761
+ const elapsedMs = Date.now() - stdinClosedAt;
5762
+ const active = hasActiveSessions();
5763
+ const connected = server.isConnected();
5764
+ const decision = decideStdinShutdown({
5765
+ stdinUnavailable,
5766
+ elapsedMs,
5767
+ maxWaitMs: STDIN_SHUTDOWN_MAX_WAIT_MS,
5768
+ hasActiveSessions: active,
5769
+ isConnected: connected
5770
+ });
5771
+ if (decision === "clear") {
5772
+ stdinClosedAt = void 0;
5773
+ stdinClosedReason = void 0;
5774
+ return;
5775
+ }
5776
+ if (decision === "shutdown_now") {
5777
+ console.error("[codex-mcp] stdin closed with no active sessions \u2014 shutting down");
5778
+ void shutdown(`stdin_${stdinClosedReason ?? "closed"}`);
5779
+ return;
5780
+ }
5781
+ if (decision === "shutdown_timeout") {
5782
+ console.error(
5783
+ `[codex-mcp] stdin closed and drain period (${STDIN_SHUTDOWN_MAX_WAIT_MS}ms) elapsed \u2014 forcing shutdown`
5784
+ );
5785
+ void shutdown(`stdin_${stdinClosedReason ?? "closed"}_timeout`);
5786
+ return;
5787
+ }
5788
+ if (active) {
5789
+ console.error(
5790
+ `[codex-mcp] stdin closed; ${sessionManager.getActiveSessionCount()} active session(s) \u2014 waiting up to ${STDIN_SHUTDOWN_MAX_WAIT_MS}ms (elapsed: ${elapsedMs}ms)`
5791
+ );
5792
+ }
5793
+ stdinShutdownTimer = setTimeout(evaluateStdinTermination, STDIN_SHUTDOWN_CHECK_MS);
5794
+ if (stdinShutdownTimer.unref) stdinShutdownTimer.unref();
5795
+ };
5796
+ function handleStdinTerminated(event) {
5797
+ if (closing) return;
5798
+ if (stdinClosedAt === void 0) {
5799
+ stdinClosedAt = Date.now();
5800
+ stdinClosedReason = event;
5801
+ console.error(`[codex-mcp] stdin ${event} observed \u2014 entering guarded shutdown checks`);
5802
+ }
5803
+ clearStdinShutdownTimer();
5804
+ stdinShutdownTimer = setTimeout(evaluateStdinTermination, STDIN_SHUTDOWN_CHECK_MS);
5805
+ if (stdinShutdownTimer.unref) stdinShutdownTimer.unref();
5806
+ }
5807
+ const handleUnexpectedError = (error) => {
5808
+ console.error("[codex-mcp] Unhandled runtime error:", error);
5809
+ lastExitCode = 1;
5810
+ void shutdown("runtime_error");
5811
+ };
5812
+ process.on("SIGINT", () => void shutdown("SIGINT"));
5813
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
5814
+ process.on("SIGBREAK", () => void shutdown("SIGBREAK"));
5815
+ process.on("beforeExit", () => {
5816
+ if (!server.isConnected()) {
5817
+ void shutdown("beforeExit");
5818
+ }
5819
+ });
5820
+ process.on("uncaughtException", handleUnexpectedError);
5821
+ process.on("unhandledRejection", handleUnexpectedError);
5822
+ if (typeof process.stdin.resume === "function") {
5823
+ process.stdin.resume();
5824
+ }
5825
+ process.stdin.on("error", handleStdinError);
5826
+ process.stdin.on("end", onStdinEnd);
5827
+ process.stdin.on("close", onStdinClose);
4398
5828
  await server.connect(transport);
4399
5829
  console.error(`codex-mcp server started (cwd: ${serverCwd})`);
4400
5830
  }