@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/CHANGELOG.md +11 -0
- package/README.md +74 -45
- package/dist/index.js +1571 -141
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
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
|
|
2645
|
-
|
|
2646
|
-
|
|
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
|
|
2670
|
-
|
|
2671
|
-
|
|
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/
|
|
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 =
|
|
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:
|
|
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.
|
|
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
|
|
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 = (
|
|
4064
|
+
const addIssue = (path6, message) => {
|
|
3354
4065
|
ctx.addIssue({
|
|
3355
4066
|
code: z.ZodIssueCode.custom,
|
|
3356
|
-
path: [
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
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
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
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
|
}
|