@leo000001/codex-mcp 2.1.6 → 2.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -340,6 +340,7 @@ var SESSION_ACTIONS = [
340
340
  "cancel",
341
341
  "interrupt",
342
342
  "fork",
343
+ "clean",
343
344
  "clean_background_terminals"
344
345
  ];
345
346
  var CHECK_ACTIONS = ["poll", "respond_permission", "respond_user_input"];
@@ -393,7 +394,7 @@ var DEFAULT_TERMINAL_CLEANUP_MS = 5 * 60 * 1e3;
393
394
  var CLEANUP_INTERVAL_MS = 6e4;
394
395
 
395
396
  // src/app-server/client.ts
396
- var CLIENT_VERSION = true ? "2.1.6" : "0.0.0-dev";
397
+ var CLIENT_VERSION = true ? "2.1.7" : "0.0.0-dev";
397
398
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
398
399
  var STARTUP_REQUEST_TIMEOUT = 9e4;
399
400
  var MAX_WRITE_QUEUE_BYTES = 5 * 1024 * 1024;
@@ -844,6 +845,31 @@ function resolveAndValidateFilePath(inputPath, baseDir, label = "path") {
844
845
  return resolved;
845
846
  }
846
847
 
848
+ // src/utils/turn-compat.ts
849
+ function classifyTurnCompatibilityError(err) {
850
+ const message = (err instanceof Error ? err.message : String(err)).toLowerCase();
851
+ const mentionsMinimal = message.includes("minimal");
852
+ const mentionsWebSearch = message.includes("web_search") || message.includes("web search");
853
+ const mentionsEffort = message.includes("effort") || message.includes("reasoning_effort") || message.includes("reasoning effort");
854
+ return mentionsMinimal && mentionsWebSearch && mentionsEffort ? "minimal_web_search" : void 0;
855
+ }
856
+ function compatibilityErrorMessage(kind) {
857
+ switch (kind) {
858
+ case "minimal_web_search":
859
+ return `Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: effort=minimal is incompatible with the Codex web_search tool in this CLI build. Use effort=low or higher, or let codex-mcp auto-upgrade it.`;
860
+ }
861
+ }
862
+ function toFriendlyTurnCompatibilityError(err) {
863
+ const kind = classifyTurnCompatibilityError(err);
864
+ if (kind) {
865
+ return new Error(compatibilityErrorMessage(kind));
866
+ }
867
+ return err instanceof Error ? err : new Error(String(err));
868
+ }
869
+ function buildEffortFallbackWarning(from, to) {
870
+ return `effort=${from} is incompatible with the Codex web_search tool in this CLI build; automatically retried with effort=${to}.`;
871
+ }
872
+
847
873
  // src/session/manager.ts
848
874
  var COALESCED_PROGRESS_DELTA_METHODS = /* @__PURE__ */ new Set([
849
875
  Methods.COMMAND_OUTPUT_DELTA,
@@ -851,6 +877,14 @@ var COALESCED_PROGRESS_DELTA_METHODS = /* @__PURE__ */ new Set([
851
877
  Methods.REASONING_TEXT_DELTA,
852
878
  Methods.REASONING_SUMMARY_DELTA
853
879
  ]);
880
+ var SKIPPABLE_DELTA_METHODS = /* @__PURE__ */ new Set([
881
+ Methods.AGENT_MESSAGE_DELTA,
882
+ Methods.COMMAND_OUTPUT_DELTA,
883
+ Methods.FILE_CHANGE_OUTPUT_DELTA,
884
+ Methods.REASONING_TEXT_DELTA,
885
+ Methods.REASONING_SUMMARY_DELTA,
886
+ Methods.PLAN_DELTA
887
+ ]);
854
888
  var MAX_COALESCED_DELTA_CHARS = 16384;
855
889
  var AUTH_REFRESH_UNSUPPORTED_CODE = -32e3;
856
890
  var AUTH_REFRESH_UNSUPPORTED_MESSAGE = "account/chatgptAuthTokens/refresh unsupported: codex-mcp does not manage external ChatGPT auth tokens";
@@ -883,6 +917,22 @@ function stripShellNoise(delta) {
883
917
  }
884
918
  var MAX_WAITERS_PER_SESSION = 4;
885
919
  var MAX_WAIT_MS = 12e4;
920
+ var EFFORT_FALLBACK_LEVEL = "low";
921
+ var CLEANABLE_SESSION_STATUSES = ["idle", "error", "cancelled"];
922
+ var REASONING_PROGRESS_METHODS = /* @__PURE__ */ new Set([
923
+ Methods.REASONING_TEXT_DELTA,
924
+ Methods.REASONING_SUMMARY_DELTA,
925
+ Methods.REASONING_SUMMARY_PART_ADDED,
926
+ Methods.PLAN_DELTA
927
+ ]);
928
+ var ACTING_PROGRESS_METHODS = /* @__PURE__ */ new Set([
929
+ Methods.COMMAND_OUTPUT_DELTA,
930
+ Methods.COMMAND_TERMINAL_INTERACTION,
931
+ Methods.FILE_CHANGE_OUTPUT_DELTA,
932
+ Methods.MCP_TOOL_PROGRESS,
933
+ Methods.TURN_DIFF_UPDATED,
934
+ Methods.TURN_PLAN_UPDATED
935
+ ]);
886
936
  var SessionManager = class {
887
937
  sessions = /* @__PURE__ */ new Map();
888
938
  clients = /* @__PURE__ */ new Map();
@@ -937,7 +987,12 @@ var SessionManager = class {
937
987
  sandbox: rec.meta.sandbox,
938
988
  eventBuffer: createEventBuffer(),
939
989
  pendingRequests: /* @__PURE__ */ new Map(),
940
- lastResult: rec.result
990
+ lastResult: rec.result,
991
+ lastAgentMessageText: typeof rec.result?.text === "string" ? rec.result.text : typeof rec.result?.output === "string" ? rec.result.output : void 0,
992
+ progressState: {
993
+ lastEventAt: rec.meta.lastActiveAt ?? now,
994
+ tokens: extractTokens(rec.result?.turn)
995
+ }
941
996
  };
942
997
  this.sessions.set(rec.sessionId, session);
943
998
  if (rec.lastSeq >= 0) {
@@ -972,6 +1027,26 @@ var SessionManager = class {
972
1027
  } catch {
973
1028
  }
974
1029
  }
1030
+ async startTurnWithCompatibilityFallback(client, turnParams) {
1031
+ try {
1032
+ return { turnStartResult: await client.turnStart(turnParams) };
1033
+ } catch (err) {
1034
+ if (turnParams.effort === "minimal" && classifyTurnCompatibilityError(err) === "minimal_web_search") {
1035
+ try {
1036
+ return {
1037
+ turnStartResult: await client.turnStart({
1038
+ ...turnParams,
1039
+ effort: EFFORT_FALLBACK_LEVEL
1040
+ }),
1041
+ compatWarnings: [buildEffortFallbackWarning("minimal", EFFORT_FALLBACK_LEVEL)]
1042
+ };
1043
+ } catch (retryErr) {
1044
+ throw toFriendlyTurnCompatibilityError(retryErr);
1045
+ }
1046
+ }
1047
+ throw toFriendlyTurnCompatibilityError(err);
1048
+ }
1049
+ }
975
1050
  // ── Session Creation ─────────────────────────────────────────────
976
1051
  async createSession(prompt, cwd, spawnOpts, effort, advanced) {
977
1052
  const sessionId = `sess_${randomUUID().slice(0, 12)}`;
@@ -993,7 +1068,9 @@ var SessionManager = class {
993
1068
  sandbox: spawnOpts.sandbox,
994
1069
  config: spawnOpts.config,
995
1070
  eventBuffer: createEventBuffer(),
996
- pendingRequests: /* @__PURE__ */ new Map()
1071
+ pendingRequests: /* @__PURE__ */ new Map(),
1072
+ lastAgentMessageText: void 0,
1073
+ progressState: { lastEventAt: now }
997
1074
  };
998
1075
  this.sessions.set(sessionId, session);
999
1076
  this.clients.set(sessionId, client);
@@ -1029,20 +1106,23 @@ var SessionManager = class {
1029
1106
  input.push({ type: "localImage", path: imagePath });
1030
1107
  }
1031
1108
  }
1032
- const turnStartResult = await client.turnStart({
1109
+ const turnStart = await this.startTurnWithCompatibilityFallback(client, {
1033
1110
  threadId,
1034
1111
  input,
1035
1112
  effort,
1036
1113
  summary: advanced?.summary,
1037
1114
  outputSchema: advanced?.outputSchema
1038
1115
  });
1116
+ const turnStartResult = turnStart.turnStartResult;
1039
1117
  const startedTurnId = extractTurnId(turnStartResult);
1040
1118
  if (startedTurnId) session.activeTurnId = startedTurnId;
1041
1119
  return {
1042
1120
  sessionId,
1043
1121
  threadId,
1044
1122
  status: "running",
1045
- pollInterval: DEFAULT_POLL_INTERVAL
1123
+ pollInterval: DEFAULT_POLL_INTERVAL,
1124
+ compatWarnings: turnStart.compatWarnings,
1125
+ progress: this.getProgress(sessionId)
1046
1126
  };
1047
1127
  } catch (err) {
1048
1128
  session.status = "error";
@@ -1075,6 +1155,7 @@ var SessionManager = class {
1075
1155
  );
1076
1156
  }
1077
1157
  clearTerminalEvents(session.eventBuffer);
1158
+ session.lastAgentMessageText = void 0;
1078
1159
  session.status = "running";
1079
1160
  session.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
1080
1161
  this.persistSessionIfChanged(session);
@@ -1094,8 +1175,11 @@ var SessionManager = class {
1094
1175
  if (overrides?.sandbox) {
1095
1176
  turnParams.sandboxPolicy = toSandboxPolicy(overrides.sandbox);
1096
1177
  }
1178
+ let compatWarnings;
1097
1179
  try {
1098
- const turnStartResult = await client.turnStart(turnParams);
1180
+ const turnStart = await this.startTurnWithCompatibilityFallback(client, turnParams);
1181
+ compatWarnings = turnStart.compatWarnings;
1182
+ const turnStartResult = turnStart.turnStartResult;
1099
1183
  const startedTurnId = extractTurnId(turnStartResult);
1100
1184
  if (startedTurnId) session.activeTurnId = startedTurnId;
1101
1185
  const canOverride = client.supportsTurnOverrides;
@@ -1120,7 +1204,9 @@ var SessionManager = class {
1120
1204
  sessionId,
1121
1205
  threadId: session.threadId,
1122
1206
  status: "running",
1123
- pollInterval: DEFAULT_POLL_INTERVAL
1207
+ pollInterval: DEFAULT_POLL_INTERVAL,
1208
+ compatWarnings,
1209
+ progress: this.getProgress(sessionId)
1124
1210
  };
1125
1211
  }
1126
1212
  // ── Session Management ───────────────────────────────────────────
@@ -1166,6 +1252,9 @@ var SessionManager = class {
1166
1252
  getLastResult(sessionId) {
1167
1253
  return this.getSessionOrThrow(sessionId).lastResult;
1168
1254
  }
1255
+ getProgress(sessionId) {
1256
+ return buildProgressInfo(this.getSessionOrThrow(sessionId));
1257
+ }
1169
1258
  getPendingActionTypes(sessionId) {
1170
1259
  const session = this.getSessionOrThrow(sessionId);
1171
1260
  const actionTypes = /* @__PURE__ */ new Set();
@@ -1285,6 +1374,50 @@ var SessionManager = class {
1285
1374
  true
1286
1375
  );
1287
1376
  }
1377
+ async cleanSessions(options) {
1378
+ const statuses = new Set(options?.statuses ?? CLEANABLE_SESSION_STATUSES);
1379
+ const olderThanMs = options?.olderThanMs;
1380
+ const dryRun = options?.dryRun ?? false;
1381
+ const includeDisk = options?.includeDisk ?? true;
1382
+ const now = Date.now();
1383
+ const matchedSessionIds = [];
1384
+ for (const [sessionId, session] of Array.from(this.sessions.entries())) {
1385
+ if (!statuses.has(session.status)) continue;
1386
+ if (typeof olderThanMs === "number" && olderThanMs > 0) {
1387
+ const lastActive = new Date(session.lastActiveAt).getTime();
1388
+ if (!Number.isFinite(lastActive)) continue;
1389
+ if (now - lastActive < olderThanMs) continue;
1390
+ }
1391
+ matchedSessionIds.push(sessionId);
1392
+ }
1393
+ if (dryRun) {
1394
+ return {
1395
+ matchedSessionIds,
1396
+ removedSessionIds: [],
1397
+ removedCount: 0,
1398
+ diskSessionsRemoved: 0,
1399
+ dryRun: true
1400
+ };
1401
+ }
1402
+ let diskSessionsRemoved = 0;
1403
+ const removedSessionIds = [];
1404
+ for (const sessionId of matchedSessionIds) {
1405
+ const evicted = this.evictSession(sessionId, includeDisk);
1406
+ if (evicted.deleted) {
1407
+ removedSessionIds.push(sessionId);
1408
+ }
1409
+ if (evicted.diskRemoved) {
1410
+ diskSessionsRemoved++;
1411
+ }
1412
+ }
1413
+ return {
1414
+ matchedSessionIds,
1415
+ removedSessionIds,
1416
+ removedCount: removedSessionIds.length,
1417
+ diskSessionsRemoved,
1418
+ dryRun: false
1419
+ };
1420
+ }
1288
1421
  async forkSession(sessionId) {
1289
1422
  const session = this.getSessionOrThrow(sessionId);
1290
1423
  const originalClient = this.getClientOrThrow(sessionId);
@@ -1408,26 +1541,48 @@ var SessionManager = class {
1408
1541
  const buf = session.eventBuffer;
1409
1542
  const responseMode = options.responseMode ?? "full";
1410
1543
  const pollOptions = options.pollOptions;
1411
- const includeEvents = pollOptions?.includeEvents ?? true;
1544
+ const finalOnly = pollOptions?.finalOnly ?? false;
1545
+ const skipDeltas = pollOptions?.skipDeltas ?? false;
1546
+ const includeEvents = finalOnly ? false : pollOptions?.includeEvents ?? true;
1412
1547
  const includeActions = pollOptions?.includeActions ?? true;
1413
- const includeResult = pollOptions?.includeResult ?? true;
1548
+ const includeResult = finalOnly ? true : pollOptions?.includeResult ?? true;
1414
1549
  const maxBytes = pollOptions?.maxBytes;
1415
1550
  const effectiveCursor = cursor ?? session.lastEventCursor;
1416
- let events = includeEvents ? buf.events.filter((e) => e.id >= effectiveCursor) : [];
1551
+ const unseenEvents = buf.events.filter((e) => e.id >= effectiveCursor);
1552
+ let events = includeEvents ? unseenEvents : [];
1417
1553
  let cursorResetTo;
1418
- if (includeEvents && buf.events.length > 0) {
1554
+ if (buf.events.length > 0) {
1419
1555
  const earliest = buf.events[0].id;
1420
1556
  if (earliest > effectiveCursor) {
1421
1557
  cursorResetTo = earliest;
1422
- events = buf.events;
1558
+ if (includeEvents) {
1559
+ events = buf.events;
1560
+ }
1423
1561
  }
1424
1562
  }
1425
1563
  const cursorFloor = cursorResetTo ?? effectiveCursor;
1426
- if (events.length > maxEvents) {
1427
- events = events.slice(0, maxEvents);
1564
+ let highestConsumedEventId;
1565
+ if (includeEvents) {
1566
+ if (skipDeltas) {
1567
+ const filtered = [];
1568
+ for (const event of events) {
1569
+ highestConsumedEventId = event.id;
1570
+ if (isSkippableDeltaEvent(event)) continue;
1571
+ filtered.push(event);
1572
+ if (filtered.length >= maxEvents) break;
1573
+ }
1574
+ events = filtered;
1575
+ } else if (events.length > maxEvents) {
1576
+ events = events.slice(0, maxEvents);
1577
+ highestConsumedEventId = events.length > 0 ? events[events.length - 1]?.id : void 0;
1578
+ } else if (events.length > 0) {
1579
+ highestConsumedEventId = events[events.length - 1]?.id;
1580
+ }
1581
+ } else if (finalOnly && unseenEvents.length > 0) {
1582
+ highestConsumedEventId = unseenEvents[unseenEvents.length - 1]?.id;
1428
1583
  }
1429
1584
  let nextCursor = clampCursorToLatest(
1430
- events.length > 0 ? events[events.length - 1].id + 1 : cursorFloor,
1585
+ typeof highestConsumedEventId === "number" ? highestConsumedEventId + 1 : cursorFloor,
1431
1586
  buf.nextId
1432
1587
  );
1433
1588
  const actions = [];
@@ -1457,6 +1612,7 @@ var SessionManager = class {
1457
1612
  sessionId,
1458
1613
  status: session.status,
1459
1614
  pollInterval: pollIntervalForStatus(session.status),
1615
+ progress: buildProgressInfo(session),
1460
1616
  events: events.map((event) => serializeEventForMode(event, responseMode)),
1461
1617
  nextCursor,
1462
1618
  cursorResetTo,
@@ -1483,6 +1639,10 @@ var SessionManager = class {
1483
1639
  result.result = void 0;
1484
1640
  truncatedFields.push("result");
1485
1641
  }
1642
+ if (typeof result.progress !== "undefined" && payloadByteSize(result) > normalizedMaxBytes) {
1643
+ result.progress = void 0;
1644
+ truncatedFields.push("progress");
1645
+ }
1486
1646
  if (typeof result.actions !== "undefined" && payloadByteSize(result) > normalizedMaxBytes) {
1487
1647
  if (session.status === "waiting_approval") {
1488
1648
  result.actions = compactActionsForBudget(result.actions);
@@ -1510,7 +1670,7 @@ var SessionManager = class {
1510
1670
  }
1511
1671
  }
1512
1672
  }
1513
- if (includeEvents) {
1673
+ if (includeEvents || finalOnly) {
1514
1674
  session.lastEventCursor = persistMonotonicCursor(
1515
1675
  session.lastEventCursor,
1516
1676
  result.nextCursor,
@@ -1739,6 +1899,7 @@ var SessionManager = class {
1739
1899
  client.onNotification((method, params) => {
1740
1900
  session.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
1741
1901
  const p = params;
1902
+ recordProgressObservation(session, method, p);
1742
1903
  switch (method) {
1743
1904
  case Methods.THREAD_STARTED: {
1744
1905
  const thread = isRecord(p.thread) ? p.thread : void 0;
@@ -1782,17 +1943,21 @@ var SessionManager = class {
1782
1943
  if (session.status === "cancelled") break;
1783
1944
  const turnObj = p.turn;
1784
1945
  const completedTurnId = turnObj?.id ?? session.activeTurnId ?? "";
1946
+ const rawTurnOutput = normalizeOptionalString(turnObj?.output);
1947
+ const finalText = normalizeOptionalString(turnObj?.output) ?? session.lastAgentMessageText;
1785
1948
  session.status = "idle";
1786
1949
  session.activeTurnId = void 0;
1787
1950
  session.lastResult = {
1788
1951
  turnId: completedTurnId,
1789
- output: turnObj?.output,
1952
+ text: finalText,
1953
+ output: rawTurnOutput,
1790
1954
  structuredOutput: turnObj?.structuredOutput,
1791
1955
  turn: p.turn,
1792
1956
  status: turnObj?.status,
1793
1957
  turnError: turnObj?.error,
1794
1958
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
1795
1959
  };
1960
+ mergeProgressTokens(session, extractTokens(turnObj?.usage));
1796
1961
  pushEvent(
1797
1962
  session.eventBuffer,
1798
1963
  "result",
@@ -1847,6 +2012,10 @@ var SessionManager = class {
1847
2012
  const item = p.item;
1848
2013
  const itemType = item && typeof item.type === "string" ? item.type : void 0;
1849
2014
  const status = normalizeOptionalString(item?.status);
2015
+ const completedItem = method === Methods.ITEM_COMPLETED || method === Methods.RAW_RESPONSE_ITEM_COMPLETED;
2016
+ if (itemType === "agentMessage" && completedItem && status === "completed" && typeof item?.text === "string") {
2017
+ session.lastAgentMessageText = item.text;
2018
+ }
1850
2019
  const eventType = itemType === "agentMessage" || itemType === "userMessage" ? "output" : "progress";
1851
2020
  pushEvent(session.eventBuffer, eventType, {
1852
2021
  method,
@@ -1890,6 +2059,7 @@ var SessionManager = class {
1890
2059
  }
1891
2060
  session.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
1892
2061
  const p = params;
2062
+ recordProgressObservation(session, method, p);
1893
2063
  switch (method) {
1894
2064
  case Methods.COMMAND_APPROVAL: {
1895
2065
  const requestId = `req_${randomUUID().slice(0, 8)}`;
@@ -2176,16 +2346,7 @@ var SessionManager = class {
2176
2346
  this.ttlWarningEmitted.delete(id);
2177
2347
  this.requestCancellation(id, "Running timeout");
2178
2348
  } else if ((session.status === "cancelled" || session.status === "error") && age > DEFAULT_TERMINAL_CLEANUP_MS) {
2179
- this.ttlWarningEmitted.delete(id);
2180
- this.clients.get(id)?.destroy().catch((err) => {
2181
- console.error(
2182
- `[codex-mcp] Failed to destroy app-server client during cleanup: session=${id} error=${err instanceof Error ? err.message : String(err)}`
2183
- );
2184
- });
2185
- this.clients.delete(id);
2186
- this.sessions.delete(id);
2187
- this.lastPersistedStatus.delete(id);
2188
- this.ttlWarningEmitted.delete(id);
2349
+ this.evictSession(id, true);
2189
2350
  } else {
2190
2351
  let ttlMs;
2191
2352
  if (session.status === "idle") {
@@ -2216,6 +2377,34 @@ var SessionManager = class {
2216
2377
  );
2217
2378
  });
2218
2379
  }
2380
+ evictSession(sessionId, removeDisk) {
2381
+ const session = this.sessions.get(sessionId);
2382
+ if (!session) return { deleted: false, diskRemoved: false };
2383
+ clearSessionPendingRequests(session);
2384
+ this.notifyWaiters(sessionId);
2385
+ this.clients.get(sessionId)?.destroy().catch((err) => {
2386
+ console.error(
2387
+ `[codex-mcp] Failed to destroy app-server client during cleanup: session=${sessionId} error=${err instanceof Error ? err.message : String(err)}`
2388
+ );
2389
+ });
2390
+ this.clients.delete(sessionId);
2391
+ const deleted = this.sessions.delete(sessionId);
2392
+ this.lastPersistedStatus.delete(sessionId);
2393
+ this.ttlWarningEmitted.delete(sessionId);
2394
+ this.sessionNotifiers.delete(sessionId);
2395
+ this.cancellationInFlight.delete(sessionId);
2396
+ let diskRemoved = false;
2397
+ if (removeDisk) {
2398
+ try {
2399
+ if (this.persistence) {
2400
+ this.persistence.removeSession(sessionId);
2401
+ diskRemoved = true;
2402
+ }
2403
+ } catch {
2404
+ }
2405
+ }
2406
+ return { deleted, diskRemoved };
2407
+ }
2219
2408
  };
2220
2409
  function pollIntervalForStatus(status) {
2221
2410
  if (status === "waiting_approval") return WAITING_APPROVAL_POLL_INTERVAL;
@@ -2230,6 +2419,114 @@ function createEventBuffer() {
2230
2419
  nextId: 0
2231
2420
  };
2232
2421
  }
2422
+ function buildProgressInfo(session) {
2423
+ const storedTokens = session.progressState?.tokens;
2424
+ const resultTokens = extractTokens(session.lastResult?.turn);
2425
+ return {
2426
+ phase: deriveProgressPhase(session),
2427
+ lastEventAt: session.progressState?.lastEventAt ?? session.lastActiveAt,
2428
+ activeTurnId: session.activeTurnId,
2429
+ pendingActionCount: countPendingRequests(session),
2430
+ lastMethod: session.progressState?.lastMethod,
2431
+ percent: session.progressState?.percent,
2432
+ tokens: mergeTokens(storedTokens, resultTokens)
2433
+ };
2434
+ }
2435
+ function countPendingRequests(session) {
2436
+ let count = 0;
2437
+ for (const req of session.pendingRequests.values()) {
2438
+ if (!req.resolved) count++;
2439
+ }
2440
+ return count;
2441
+ }
2442
+ function deriveProgressPhase(session) {
2443
+ if (session.status === "waiting_approval") return "waiting_approval";
2444
+ if (session.status === "cancelled") return "cancelled";
2445
+ if (session.status === "error") return "error";
2446
+ if (session.status === "idle") return "finished";
2447
+ if (!session.activeTurnId) return "starting";
2448
+ const lastMethod = session.progressState?.lastMethod;
2449
+ if (typeof lastMethod === "string") {
2450
+ if (REASONING_PROGRESS_METHODS.has(lastMethod)) return "reasoning";
2451
+ if (ACTING_PROGRESS_METHODS.has(lastMethod)) return "acting";
2452
+ }
2453
+ return "running";
2454
+ }
2455
+ function recordProgressObservation(session, method, params) {
2456
+ const next = session.progressState ?? { lastEventAt: (/* @__PURE__ */ new Date()).toISOString() };
2457
+ next.lastEventAt = (/* @__PURE__ */ new Date()).toISOString();
2458
+ if (method !== Methods.THREAD_TOKEN_USAGE_UPDATED) {
2459
+ next.lastMethod = method;
2460
+ }
2461
+ const percent = extractPercent(params);
2462
+ if (typeof percent === "number") next.percent = percent;
2463
+ mergeProgressTokens(session, extractTokens(params));
2464
+ session.progressState = next;
2465
+ }
2466
+ function mergeProgressTokens(session, tokens) {
2467
+ if (!tokens) return;
2468
+ const next = session.progressState ?? { lastEventAt: (/* @__PURE__ */ new Date()).toISOString() };
2469
+ next.tokens = mergeTokens(next.tokens, tokens);
2470
+ session.progressState = next;
2471
+ }
2472
+ function mergeTokens(base, extra) {
2473
+ if (!base && !extra) return void 0;
2474
+ return {
2475
+ input: extra?.input ?? base?.input,
2476
+ output: extra?.output ?? base?.output,
2477
+ total: extra?.total ?? base?.total
2478
+ };
2479
+ }
2480
+ function extractPercent(value) {
2481
+ if (!isRecord(value)) return void 0;
2482
+ const candidates = [value.percent, value.percentage, value.progress, value.fractionComplete];
2483
+ for (const candidate of candidates) {
2484
+ if (typeof candidate !== "number" || !Number.isFinite(candidate)) continue;
2485
+ if (candidate >= 0 && candidate <= 1) return Math.round(candidate * 100);
2486
+ if (candidate >= 0 && candidate <= 100) return candidate;
2487
+ }
2488
+ return void 0;
2489
+ }
2490
+ function extractTokens(value) {
2491
+ if (!isRecord(value)) return void 0;
2492
+ const usage = isRecord(value.usage) ? value.usage : void 0;
2493
+ const source = usage ?? value;
2494
+ const input = pickNumber(source, [
2495
+ "inputTokens",
2496
+ "input_tokens",
2497
+ "promptTokens",
2498
+ "prompt_tokens"
2499
+ ]);
2500
+ const output = pickNumber(source, [
2501
+ "outputTokens",
2502
+ "output_tokens",
2503
+ "completionTokens",
2504
+ "completion_tokens"
2505
+ ]);
2506
+ const total = pickNumber(source, [
2507
+ "totalTokens",
2508
+ "total_tokens",
2509
+ "tokenCount",
2510
+ "token_count",
2511
+ "total"
2512
+ ]);
2513
+ if (typeof input !== "number" && typeof output !== "number" && typeof total !== "number") {
2514
+ return void 0;
2515
+ }
2516
+ return { input, output, total };
2517
+ }
2518
+ function pickNumber(source, keys) {
2519
+ for (const key of keys) {
2520
+ const value = source[key];
2521
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2522
+ }
2523
+ return void 0;
2524
+ }
2525
+ function isSkippableDeltaEvent(event) {
2526
+ if (!isRecord(event.data)) return false;
2527
+ const method = event.data.method;
2528
+ return typeof method === "string" && SKIPPABLE_DELTA_METHODS.has(method);
2529
+ }
2233
2530
  function clearTerminalEvents(buf) {
2234
2531
  buf.events = buf.events.filter((e) => e.type !== "result" && e.type !== "error");
2235
2532
  }
@@ -2719,6 +3016,20 @@ function buildExecutionInfo(waitForResultMs, status, fallbackReason) {
2719
3016
  fallbackReason: effective === "background" ? fallbackReason : void 0
2720
3017
  };
2721
3018
  }
3019
+ function coerceProgressForStatus(status, progress, options) {
3020
+ if (!progress) return void 0;
3021
+ let phase = progress.phase;
3022
+ if (status === "idle") phase = "finished";
3023
+ else if (status === "error") phase = "error";
3024
+ else if (status === "cancelled") phase = "cancelled";
3025
+ else if (status === "waiting_approval") phase = "waiting_approval";
3026
+ return {
3027
+ ...progress,
3028
+ phase,
3029
+ lastEventAt: options?.completedAt ?? progress.lastEventAt,
3030
+ pendingActionCount: status === "waiting_approval" ? Math.max(progress.pendingActionCount, options?.pendingActionCount ?? 0) : status === "running" ? progress.pendingActionCount : 0
3031
+ };
3032
+ }
2722
3033
  async function waitForCodexSessionForegroundResult(sessionManager, sessionId, waitForResultMs, signal) {
2723
3034
  const deadline = Date.now() + Math.min(waitForResultMs, 3e5);
2724
3035
  while (Date.now() < deadline) {
@@ -2761,6 +3072,9 @@ async function waitForCodexSessionForegroundResult(sessionManager, sessionId, wa
2761
3072
  }
2762
3073
 
2763
3074
  // src/tools/codex.ts
3075
+ function safeGetProgress(sessionManager, sessionId) {
3076
+ return typeof sessionManager.getProgress === "function" ? sessionManager.getProgress(sessionId) : void 0;
3077
+ }
2764
3078
  async function executeCodex(args, sessionManager, serverCwd, requestSignal) {
2765
3079
  const cwd = resolveAndValidateCwd(args.cwd, serverCwd);
2766
3080
  const spawnOpts = extractSpawnOptions(args);
@@ -2775,6 +3089,10 @@ async function executeCodex(args, sessionManager, serverCwd, requestSignal) {
2775
3089
  const waitMs = args.advanced?.waitForResult;
2776
3090
  const baseResult = {
2777
3091
  ...startResult,
3092
+ progress: coerceProgressForStatus(
3093
+ "running",
3094
+ safeGetProgress(sessionManager, startResult.sessionId) ?? startResult.progress
3095
+ ),
2778
3096
  execution: buildExecutionInfo(waitMs, "running"),
2779
3097
  interactionState: interactionStateForStatus("running"),
2780
3098
  recommendedNextAction: recommendedNextActionForStatus("running")
@@ -2792,6 +3110,15 @@ async function executeCodex(args, sessionManager, serverCwd, requestSignal) {
2792
3110
  result: foreground.result,
2793
3111
  status: foreground.status,
2794
3112
  completedAt: foreground.completedAt,
3113
+ compatWarnings: startResult.compatWarnings,
3114
+ progress: coerceProgressForStatus(
3115
+ foreground.status,
3116
+ safeGetProgress(sessionManager, startResult.sessionId) ?? startResult.progress,
3117
+ {
3118
+ completedAt: foreground.completedAt,
3119
+ pendingActionCount: foreground.pendingActionTypes?.length ?? 0
3120
+ }
3121
+ ),
2795
3122
  pollInterval: foreground.status === "waiting_approval" ? WAITING_APPROVAL_POLL_INTERVAL : foreground.status === "running" ? startResult.pollInterval : void 0,
2796
3123
  execution: buildExecutionInfo(waitMs, foreground.status, foreground.fallbackReason),
2797
3124
  interactionState: interactionStateForStatus(foreground.status),
@@ -2803,6 +3130,9 @@ async function executeCodex(args, sessionManager, serverCwd, requestSignal) {
2803
3130
  }
2804
3131
 
2805
3132
  // src/tools/codex-reply.ts
3133
+ function safeGetProgress2(sessionManager, sessionId) {
3134
+ return typeof sessionManager.getProgress === "function" ? sessionManager.getProgress(sessionId) : void 0;
3135
+ }
2806
3136
  async function executeCodexReply(args, sessionManager, requestSignal) {
2807
3137
  const startResult = await sessionManager.replyToSession(args.sessionId, args.prompt, {
2808
3138
  model: args.model,
@@ -2817,6 +3147,10 @@ async function executeCodexReply(args, sessionManager, requestSignal) {
2817
3147
  const waitMs = args.waitForResult;
2818
3148
  const baseResult = {
2819
3149
  ...startResult,
3150
+ progress: coerceProgressForStatus(
3151
+ "running",
3152
+ safeGetProgress2(sessionManager, startResult.sessionId) ?? startResult.progress
3153
+ ),
2820
3154
  execution: buildExecutionInfo(waitMs, "running"),
2821
3155
  interactionState: interactionStateForStatus("running"),
2822
3156
  recommendedNextAction: recommendedNextActionForStatus("running")
@@ -2834,6 +3168,15 @@ async function executeCodexReply(args, sessionManager, requestSignal) {
2834
3168
  sessionId: startResult.sessionId,
2835
3169
  threadId: startResult.threadId,
2836
3170
  status: foreground.status,
3171
+ compatWarnings: startResult.compatWarnings,
3172
+ progress: coerceProgressForStatus(
3173
+ foreground.status,
3174
+ safeGetProgress2(sessionManager, startResult.sessionId) ?? startResult.progress,
3175
+ {
3176
+ completedAt: foreground.completedAt,
3177
+ pendingActionCount: foreground.pendingActionTypes?.length ?? 0
3178
+ }
3179
+ ),
2837
3180
  pollInterval: foreground.status === "waiting_approval" ? WAITING_APPROVAL_POLL_INTERVAL : foreground.status === "running" ? startResult.pollInterval : void 0,
2838
3181
  result: foreground.result,
2839
3182
  completedAt: foreground.completedAt,
@@ -2885,6 +3228,13 @@ async function executeCodexSession(args, sessionManager) {
2885
3228
  };
2886
3229
  }
2887
3230
  return await sessionManager.forkSession(args.sessionId);
3231
+ case "clean":
3232
+ return await sessionManager.cleanSessions({
3233
+ statuses: args.statuses,
3234
+ olderThanMs: args.olderThanMs,
3235
+ dryRun: args.dryRun,
3236
+ includeDisk: args.includeDisk
3237
+ });
2888
3238
  case "clean_background_terminals":
2889
3239
  if (!args.sessionId) {
2890
3240
  return {
@@ -2920,24 +3270,15 @@ function executeCodexCheck(args, sessionManager, requestSignal) {
2920
3270
  const maxEvents = typeof args.maxEvents === "number" ? Math.max(POLL_MIN_MAX_EVENTS, Math.floor(args.maxEvents)) : POLL_DEFAULT_MAX_EVENTS;
2921
3271
  const waitMs = pollOptions?.waitMs;
2922
3272
  if (typeof waitMs === "number" && waitMs > 0) {
2923
- const firstCheck = sessionManager.pollEvents(args.sessionId, args.cursor, maxEvents, {
2924
- responseMode,
2925
- pollOptions
2926
- });
2927
- const hasData = firstCheck.events.length > 0 || firstCheck.actions !== void 0 && firstCheck.actions.length > 0 || firstCheck.result !== void 0;
2928
- if (!hasData) {
2929
- const clampedWait = Math.min(waitMs, 12e4);
2930
- return sessionManager.waitForChange(args.sessionId, clampedWait, requestSignal).catch(() => void 0).then(
2931
- () => enrichCheckResult(
2932
- sessionManager,
2933
- sessionManager.pollEvents(args.sessionId, args.cursor, maxEvents, {
2934
- responseMode,
2935
- pollOptions
2936
- })
2937
- )
2938
- );
2939
- }
2940
- return enrichCheckResult(sessionManager, firstCheck);
3273
+ return pollWithWait(
3274
+ sessionManager,
3275
+ args.sessionId,
3276
+ args.cursor,
3277
+ maxEvents,
3278
+ { responseMode, pollOptions },
3279
+ Math.min(waitMs, 12e4),
3280
+ requestSignal
3281
+ ).then((result) => enrichCheckResult(sessionManager, result));
2941
3282
  }
2942
3283
  return enrichCheckResult(
2943
3284
  sessionManager,
@@ -3058,6 +3399,31 @@ function executeCodexCheck(args, sessionManager, requestSignal) {
3058
3399
  };
3059
3400
  }
3060
3401
  }
3402
+ function hasVisibleData(result) {
3403
+ return result.events.length > 0 || result.actions !== void 0 && result.actions.length > 0 || result.result !== void 0;
3404
+ }
3405
+ async function pollWithWait(sessionManager, sessionId, cursor, maxEvents, options, waitMs, signal) {
3406
+ const deadline = Date.now() + waitMs;
3407
+ let currentCursor = cursor;
3408
+ while (true) {
3409
+ const result = sessionManager.pollEvents(sessionId, currentCursor, maxEvents, options);
3410
+ if (hasVisibleData(result)) {
3411
+ return result;
3412
+ }
3413
+ if (signal?.aborted) {
3414
+ return result;
3415
+ }
3416
+ const remainingMs = deadline - Date.now();
3417
+ if (remainingMs <= 0) {
3418
+ return result;
3419
+ }
3420
+ currentCursor = result.nextCursor;
3421
+ await sessionManager.waitForChange(sessionId, remainingMs, signal).catch(() => void 0);
3422
+ if (signal?.aborted) {
3423
+ return sessionManager.pollEvents(sessionId, currentCursor, maxEvents, options);
3424
+ }
3425
+ }
3426
+ }
3061
3427
  function enrichCheckResult(sessionManager, result) {
3062
3428
  const actionTypes = result.status === "waiting_approval" ? sessionManager.getPendingActionTypes(result.sessionId) : [];
3063
3429
  return {
@@ -3541,8 +3907,11 @@ function buildConfigGuideText() {
3541
3907
  "- `codex_check.pollOptions.includeEvents`: default `true`.",
3542
3908
  "- `codex_check.pollOptions.includeActions`: default `true`.",
3543
3909
  "- `codex_check.pollOptions.includeResult`: default `true`.",
3910
+ "- `codex_check.pollOptions.skipDeltas`: default `false`.",
3911
+ "- `codex_check.pollOptions.finalOnly`: default `false`.",
3544
3912
  "- `codex_check.pollOptions.maxBytes`: default unlimited.",
3545
3913
  "- `codex_check.cursor`: default is session last consumed cursor when omitted.",
3914
+ "- `progress` is included on `codex`, `codex_reply`, and `codex_check` responses.",
3546
3915
  ""
3547
3916
  ].join("\n");
3548
3917
  }
@@ -3556,6 +3925,8 @@ function buildGotchasText() {
3556
3925
  `- Poll enforces minimum \`maxEvents=${POLL_MIN_MAX_EVENTS}\`; sending \`0\` is normalized to \`${POLL_MIN_MAX_EVENTS}\`.`,
3557
3926
  `- \`respond_permission\` and \`respond_user_input\` default to compact ACK with \`maxEvents=${RESPOND_DEFAULT_MAX_EVENTS}\`.`,
3558
3927
  "- Default response mode is `minimal`; use `full` if you need full raw event payloads.",
3928
+ "- Use `pollOptions.skipDeltas=true` to suppress delta-heavy stream chunks while still advancing the cursor.",
3929
+ "- Use `pollOptions.finalOnly=true` when you only care about actions + terminal result; it also advances the cursor past hidden events.",
3559
3930
  "- respond_* uses monotonic cursor handling: `max(cursor, sessionLastCursor)`.",
3560
3931
  "- If `cursorResetTo` is present, your cursor is stale (old events were evicted); restart from that value.",
3561
3932
  "- **Poll frequency guidance**: Adapt poll interval to task complexity and previous poll results. For `running` sessions, start at 2 minutes and increase for long tasks. Only poll frequently (~1s) when `waiting_approval`. Do NOT high-frequency poll \u2014 it wastes tokens and provides no benefit.",
@@ -3570,6 +3941,8 @@ function buildGotchasText() {
3570
3941
  "## Event model",
3571
3942
  "",
3572
3943
  "- Top-level `events[].type` is one of: `output`, `progress`, `approval_request`, `approval_result`, `result`, `error`.",
3944
+ "- Terminal `result.text` provides a stable final assistant message, even when the backend omits `turn.output`; `result.output` remains the raw backend field.",
3945
+ "- `progress` normalizes the current phase, pending action count, last observed method, and token totals when available.",
3573
3946
  "- Fine-grained stream semantics are in `events[].data.method` (for example command output delta, reasoning delta, turn updates).",
3574
3947
  '- Retryable interruptions surface as `progress` with `method="codex-mcp/reconnect"` and include retry fields.',
3575
3948
  "- During reconnect/retry, continue polling normally; if retries stop (`willRetry=false`), session transitions to error path.",
@@ -3586,7 +3959,8 @@ function buildGotchasText() {
3586
3959
  `- Idle sessions are auto-cleaned after ${msToMinutes(DEFAULT_IDLE_CLEANUP_MS)} minutes.`,
3587
3960
  `- Running/waiting sessions are auto-cleaned after ${msToMinutes(DEFAULT_RUNNING_CLEANUP_MS)} minutes.`,
3588
3961
  `- Error/cancelled sessions are retained for about ${msToMinutes(DEFAULT_TERMINAL_CLEANUP_MS)} minutes, then removed.`,
3589
- "- Session state is in-memory. Restarting codex-mcp drops all existing sessions.",
3962
+ '- Use `codex_session(action="clean")` to batch-remove idle/error/cancelled sessions on demand.',
3963
+ "- Session metadata/results are persisted for recovery; manual clean can also delete those disk artifacts.",
3590
3964
  "",
3591
3965
  "## Capacity",
3592
3966
  "",
@@ -3673,6 +4047,7 @@ function buildQuickstartText() {
3673
4047
  "```",
3674
4048
  "",
3675
4049
  "5. Continue polling until terminal status (`idle`, `error`, or `cancelled`), respecting the >=2 minute interval while `running`.",
4050
+ "6. Read `progress.phase` / `progress.tokens` for a coarse execution snapshot without parsing raw delta events.",
3676
4051
  "",
3677
4052
  "## Cursor notes",
3678
4053
  "",
@@ -3681,6 +4056,13 @@ function buildQuickstartText() {
3681
4056
  "- Omit `responseMode`: default is `minimal`.",
3682
4057
  "- Use returned `nextCursor` for the next call.",
3683
4058
  "- If `cursorResetTo` appears, reset to that value and continue.",
4059
+ "- If you need schema-constrained results, pass `advanced.outputSchema` (or top-level `outputSchema` in `codex_reply`) and read terminal `result.structuredOutput`.",
4060
+ "",
4061
+ "## Read next",
4062
+ "",
4063
+ "- `codex-mcp:///config`: parameter-by-parameter guide, including `advanced.*` mapping and reply overrides.",
4064
+ "- `codex-mcp:///delegation-guide`: task presets for approvalPolicy/sandbox selection.",
4065
+ "- `codex-mcp:///gotchas`: polling, approval timeout, cursor, and exec-mode failure modes.",
3684
4066
  ""
3685
4067
  ].join("\n");
3686
4068
  }
@@ -3729,6 +4111,13 @@ function buildDelegationGuideText() {
3729
4111
  "",
3730
4112
  "**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.",
3731
4113
  "",
4114
+ "## Approval policy quick guide",
4115
+ "- `never`: no interactive prompts. Best for read-only review or tightly scoped trusted tasks.",
4116
+ "- `on-failure`: pragmatic default for implementation work when you still want some safety rails.",
4117
+ "- `on-request`: use when a human or outer agent will actively poll and answer approvals.",
4118
+ "- `untrusted`: strictest interactive mode; expect frequent prompts and higher timeout sensitivity.",
4119
+ `- Default approval timeout is ${DEFAULT_APPROVAL_TIMEOUT_MS}ms. If interactive approvals are possible, raise \`advanced.approvalTimeoutMs\` to at least 300000 so requests do not expire between normal running-session polls.`,
4120
+ "",
3732
4121
  "## Quick mode: `waitForResult`",
3733
4122
  "For short tasks (< 2 min), set `advanced.waitForResult` to get the final result in a single tool call:",
3734
4123
  "```json",
@@ -3756,6 +4145,11 @@ function buildDelegationGuideText() {
3756
4145
  "",
3757
4146
  "**Approval timeout:** Default is 60s; infrequent polling causes silent auto-decline. See `codex-mcp:///gotchas`.",
3758
4147
  "",
4148
+ "## Read next",
4149
+ "- `codex-mcp:///quickstart` for the exact start -> poll -> respond loop",
4150
+ "- `codex-mcp:///config` for parameter mapping and override persistence",
4151
+ "- `codex-mcp:///gotchas` for timeout, cursor, and exec-mode caveats",
4152
+ "",
3759
4153
  "## Security notes",
3760
4154
  "- `sandbox: 'read-only'` is the strongest isolation \u2014 blocks all writes regardless of approval policy",
3761
4155
  "- `approvalPolicy: 'never'` + `sandbox: 'workspace-write'` gives the agent full write access with no human oversight \u2014 use only for well-defined, low-risk tasks",
@@ -3943,9 +4337,13 @@ function registerResources(server, deps) {
3943
4337
  }
3944
4338
 
3945
4339
  // src/server.ts
3946
- var SERVER_VERSION = true ? "2.1.6" : "0.0.0-dev";
4340
+ var SERVER_VERSION = true ? "2.1.7" : "0.0.0-dev";
3947
4341
  function formatErrorMessage(err) {
3948
4342
  const message = err instanceof Error ? err.message : String(err);
4343
+ const compatibilityKind = classifyTurnCompatibilityError(err);
4344
+ if (compatibilityKind) {
4345
+ return compatibilityErrorMessage(compatibilityKind);
4346
+ }
3949
4347
  const m = /^Error \[([A-Z_]+)\]:\s*(.*)$/.exec(message);
3950
4348
  if (m) {
3951
4349
  const [, code, rest] = m;
@@ -3997,6 +4395,28 @@ function createServer(serverCwd, options) {
3997
4395
  });
3998
4396
  const interactionStateSchema = z.enum(["working", "waiting_input", "finished"]);
3999
4397
  const nextActionSchema = z.enum(["poll", "respond_permission", "respond_user_input", "none"]);
4398
+ const progressSchema = z.object({
4399
+ phase: z.enum([
4400
+ "starting",
4401
+ "running",
4402
+ "reasoning",
4403
+ "acting",
4404
+ "waiting_approval",
4405
+ "finished",
4406
+ "error",
4407
+ "cancelled"
4408
+ ]),
4409
+ lastEventAt: z.string(),
4410
+ activeTurnId: z.string().optional(),
4411
+ pendingActionCount: z.number().int(),
4412
+ lastMethod: z.string().optional(),
4413
+ percent: z.number().optional(),
4414
+ tokens: z.object({
4415
+ input: z.number().optional(),
4416
+ output: z.number().optional(),
4417
+ total: z.number().optional()
4418
+ }).optional()
4419
+ });
4000
4420
  const setupResultShape = {
4001
4421
  ready: z.boolean(),
4002
4422
  cwd: z.string(),
@@ -4033,6 +4453,8 @@ function createServer(serverCwd, options) {
4033
4453
  ),
4034
4454
  result: z.unknown().optional().describe("Final result when waitForResult is set and session completed."),
4035
4455
  completedAt: z.string().optional().describe("ISO timestamp when the session completed (only when waitForResult succeeded)."),
4456
+ compatWarnings: z.array(z.string()).optional(),
4457
+ progress: progressSchema.optional(),
4036
4458
  execution: executionInfoSchema.optional(),
4037
4459
  interactionState: interactionStateSchema.optional(),
4038
4460
  recommendedNextAction: nextActionSchema.optional(),
@@ -4042,6 +4464,10 @@ function createServer(serverCwd, options) {
4042
4464
  includeEvents: z.boolean().optional().describe("Default: true. Include events[] in response."),
4043
4465
  includeActions: z.boolean().optional().describe("Default: true. Include actions[] in response."),
4044
4466
  includeResult: z.boolean().optional().describe("Default: true. Include result in response."),
4467
+ skipDeltas: z.boolean().optional().describe(
4468
+ "Default: false. Drop delta-heavy streaming events while still advancing the cursor."
4469
+ ),
4470
+ finalOnly: z.boolean().optional().describe("Default: false. Omit events and focus on actions + terminal result."),
4045
4471
  maxBytes: z.number().int().positive().optional().describe("Default: unlimited. Best-effort response payload cap in bytes."),
4046
4472
  waitMs: z.number().int().nonnegative().optional().describe(
4047
4473
  "Long-poll: block up to this many ms for new events (max 120000). Omit or 0 for immediate return."
@@ -4187,7 +4613,7 @@ function createServer(serverCwd, options) {
4187
4613
  "codex",
4188
4614
  {
4189
4615
  title: "Start Codex Session",
4190
- description: "Start session asynchronously and return `{ sessionId, threadId, status, pollInterval }`. Use `pollInterval` as a minimum hint: `running` >=120000ms (increase for long tasks), `waiting_approval` ~=1000ms.",
4616
+ description: "Start a Codex session asynchronously and return `{ sessionId, threadId, status, pollInterval }`. Poll with `codex_check(action='poll')` until terminal status, and treat `pollInterval` as a minimum hint: `running` >=120000ms, `waiting_approval` ~=1000ms. See `codex-mcp:///quickstart` for the main loop, `codex-mcp:///config` for parameter guidance, and `codex-mcp:///delegation-guide` for approval/sandbox presets.",
4191
4617
  inputSchema: {
4192
4618
  prompt: z.string().describe("Task or question"),
4193
4619
  approvalPolicy: z.enum(APPROVAL_POLICIES).describe("Required enum: untrusted/on-failure/on-request/never."),
@@ -4315,18 +4741,23 @@ function createServer(serverCwd, options) {
4315
4741
  "codex_session",
4316
4742
  {
4317
4743
  title: "Manage Sessions",
4318
- description: `Session actions: list, get, cancel, interrupt, fork, clean_background_terminals.
4744
+ description: `Session actions: list, get, cancel, interrupt, fork, clean, clean_background_terminals.
4319
4745
 
4320
4746
  - list: sessions in memory.
4321
4747
  - get: details. includeSensitive defaults to false; true adds threadId/cwd/profile/config.
4322
4748
  - cancel: terminal.
4323
4749
  - interrupt: stop current turn.
4324
4750
  - fork: clone current thread into a new session; source remains unchanged.
4751
+ - clean: batch-remove idle/error/cancelled sessions, optionally from disk too.
4325
4752
  - clean_background_terminals: ask app-server to clean stale background terminals for this thread.`,
4326
4753
  inputSchema: {
4327
4754
  action: z.enum(SESSION_ACTIONS),
4328
4755
  sessionId: z.string().optional().describe("Required for get/cancel/interrupt/fork/clean_background_terminals"),
4329
- includeSensitive: z.boolean().default(false).optional().describe("Include cwd/config/threadId/profile in get (default: false)")
4756
+ includeSensitive: z.boolean().default(false).optional().describe("Include cwd/config/threadId/profile in get (default: false)"),
4757
+ statuses: z.array(z.enum(["idle", "error", "cancelled"])).optional().describe("For clean only. Default: idle/error/cancelled."),
4758
+ olderThanMs: z.number().int().nonnegative().optional().describe("For clean only. Remove sessions idle for at least this many ms."),
4759
+ dryRun: z.boolean().optional().describe("For clean only. Preview matched sessions."),
4760
+ includeDisk: z.boolean().optional().describe("For clean only. Default: true. Also remove persisted session state.")
4330
4761
  },
4331
4762
  outputSchema: {
4332
4763
  sessions: z.array(publicSessionInfoSchema).optional(),
@@ -4347,6 +4778,11 @@ function createServer(serverCwd, options) {
4347
4778
  pollInterval: z.number().int().optional().describe(
4348
4779
  "Recommended minimum delay before next poll (ms): running >=120000, waiting_approval ~=1000."
4349
4780
  ),
4781
+ matchedSessionIds: z.array(z.string()).optional(),
4782
+ removedSessionIds: z.array(z.string()).optional(),
4783
+ removedCount: z.number().int().optional(),
4784
+ diskSessionsRemoved: z.number().int().optional(),
4785
+ dryRun: z.boolean().optional(),
4350
4786
  success: z.boolean().optional(),
4351
4787
  message: z.string().optional(),
4352
4788
  ...errorOutputShape
@@ -4382,7 +4818,7 @@ function createServer(serverCwd, options) {
4382
4818
  "codex_check",
4383
4819
  {
4384
4820
  title: "Poll & Respond",
4385
- 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.
4821
+ 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). WARNING: running sessions usually poll at >=120000ms, but approvalTimeoutMs defaults to ${DEFAULT_APPROVAL_TIMEOUT_MS}ms, so approvals can expire between polls unless you raise the timeout or use non-interactive policies. See codex-mcp:///quickstart and codex-mcp:///gotchas.
4386
4822
 
4387
4823
  poll: events since cursor. Default maxEvents=${POLL_DEFAULT_MAX_EVENTS}.
4388
4824
  respond_permission: approval decision. Default maxEvents=${RESPOND_DEFAULT_MAX_EVENTS} (compact ACK).
@@ -4394,6 +4830,7 @@ respond_user_input: user-input answers. Default maxEvents=${RESPOND_DEFAULT_MAX_
4394
4830
  pollInterval: z.number().int().optional().describe(
4395
4831
  "Recommended minimum delay before next poll (ms): running >=120000, waiting_approval ~=1000."
4396
4832
  ),
4833
+ progress: progressSchema.optional(),
4397
4834
  interactionState: interactionStateSchema.optional(),
4398
4835
  recommendedNextAction: nextActionSchema.optional(),
4399
4836
  events: z.array(
@@ -4429,6 +4866,7 @@ respond_user_input: user-input answers. Default maxEvents=${RESPOND_DEFAULT_MAX_
4429
4866
  ).optional(),
4430
4867
  result: z.object({
4431
4868
  turnId: z.string(),
4869
+ text: z.string().optional(),
4432
4870
  output: z.string().optional(),
4433
4871
  structuredOutput: z.unknown().optional(),
4434
4872
  turn: z.unknown().optional(),
@@ -5027,7 +5465,7 @@ var ExecClient = class extends EventEmitter2 {
5027
5465
 
5028
5466
  // src/session/persistence.ts
5029
5467
  import { join as join5 } from "path";
5030
- import { mkdirSync as mkdirSync4, existsSync as existsSync7 } from "fs";
5468
+ import { mkdirSync as mkdirSync4, existsSync as existsSync7, rmSync as rmSync3 } from "fs";
5031
5469
  import { homedir as homedir2 } from "os";
5032
5470
 
5033
5471
  // src/persistence/atomic-writer.ts
@@ -5474,6 +5912,11 @@ var SessionPersistence = class {
5474
5912
  this.eventLogs.delete(sessionId);
5475
5913
  }
5476
5914
  }
5915
+ /** Remove a persisted session directory from disk. */
5916
+ removeSession(sessionId) {
5917
+ this.destroySessionLog(sessionId);
5918
+ rmSync3(join5(this.sessionsDir, sessionId), { recursive: true, force: true });
5919
+ }
5477
5920
  /** Clean up: flush all logs, release lock. */
5478
5921
  destroy() {
5479
5922
  for (const log of this.eventLogs.values()) {