@leo000001/codex-mcp 2.1.5 → 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
@@ -243,7 +243,7 @@ function getPathExtensions(env) {
243
243
  }
244
244
  return Array.from(new Set(merged));
245
245
  }
246
- function commandExistsOnPath(command, env) {
246
+ function resolveCommandFromPath(command, env) {
247
247
  const dirs = getPathEntries(env);
248
248
  const ext = process.platform === "win32" ? path2.extname(command) : "";
249
249
  const names = process.platform === "win32" ? Array.from(
@@ -254,10 +254,10 @@ function commandExistsOnPath(command, env) {
254
254
  for (const dir of dirs) {
255
255
  for (const name of names) {
256
256
  const candidate = path2.join(dir, name);
257
- if (isExecutableFile(candidate)) return true;
257
+ if (isExecutableFile(candidate)) return path2.normalize(candidate);
258
258
  }
259
259
  }
260
- return false;
260
+ return void 0;
261
261
  }
262
262
  function looksLikePath(value) {
263
263
  return value.includes("/") || value.includes("\\");
@@ -288,14 +288,16 @@ function resolveDefaultCodexExecutable(env = process.env) {
288
288
  `${CODEX_MCP_COMMAND}="${envCommand}" looks like a path. Use ${CODEX_MCP_PATH} for filesystem paths.`
289
289
  );
290
290
  }
291
- if (!commandExistsOnPath(envCommand, env)) {
291
+ const resolved = resolveCommandFromPath(envCommand, env);
292
+ if (!resolved) {
292
293
  throw new Error(`${CODEX_MCP_COMMAND}="${envCommand}" was not found in PATH.`);
293
294
  }
294
- return { command: envCommand, isPath: false, source: "env_command" };
295
+ return { command: resolved, isPath: true, source: "env_command" };
295
296
  }
296
297
  for (const candidate of AUTO_CODEX_COMMANDS) {
297
- if (commandExistsOnPath(candidate, env)) {
298
- return { command: candidate, isPath: false, source: "auto_detect" };
298
+ const resolved = resolveCommandFromPath(candidate, env);
299
+ if (resolved) {
300
+ return { command: resolved, isPath: true, source: "auto_detect" };
299
301
  }
300
302
  }
301
303
  return { command: "codex", isPath: false, source: "default" };
@@ -308,16 +310,15 @@ function getDefaultCodexExecutable() {
308
310
  }
309
311
  function checkDefaultCodexExecutableAvailability() {
310
312
  const info = getDefaultCodexExecutable();
311
- const label = info.isPath ? "path" : "command";
312
313
  switch (info.source) {
313
314
  case "env_path":
314
315
  console.error(`[codex-executable] Using ${CODEX_MCP_PATH}: ${info.command}`);
315
316
  break;
316
317
  case "env_command":
317
- console.error(`[codex-executable] Using ${CODEX_MCP_COMMAND}: ${info.command}`);
318
+ console.error(`[codex-executable] Using ${CODEX_MCP_COMMAND}: resolved to ${info.command}`);
318
319
  break;
319
320
  case "auto_detect":
320
- console.error(`[codex-executable] Auto-detected ${label}: ${info.command}`);
321
+ console.error(`[codex-executable] Auto-detected executable: ${info.command}`);
321
322
  break;
322
323
  case "default":
323
324
  console.error(
@@ -339,6 +340,7 @@ var SESSION_ACTIONS = [
339
340
  "cancel",
340
341
  "interrupt",
341
342
  "fork",
343
+ "clean",
342
344
  "clean_background_terminals"
343
345
  ];
344
346
  var CHECK_ACTIONS = ["poll", "respond_permission", "respond_user_input"];
@@ -392,7 +394,7 @@ var DEFAULT_TERMINAL_CLEANUP_MS = 5 * 60 * 1e3;
392
394
  var CLEANUP_INTERVAL_MS = 6e4;
393
395
 
394
396
  // src/app-server/client.ts
395
- var CLIENT_VERSION = true ? "2.1.5" : "0.0.0-dev";
397
+ var CLIENT_VERSION = true ? "2.1.7" : "0.0.0-dev";
396
398
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
397
399
  var STARTUP_REQUEST_TIMEOUT = 9e4;
398
400
  var MAX_WRITE_QUEUE_BYTES = 5 * 1024 * 1024;
@@ -843,6 +845,31 @@ function resolveAndValidateFilePath(inputPath, baseDir, label = "path") {
843
845
  return resolved;
844
846
  }
845
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
+
846
873
  // src/session/manager.ts
847
874
  var COALESCED_PROGRESS_DELTA_METHODS = /* @__PURE__ */ new Set([
848
875
  Methods.COMMAND_OUTPUT_DELTA,
@@ -850,6 +877,14 @@ var COALESCED_PROGRESS_DELTA_METHODS = /* @__PURE__ */ new Set([
850
877
  Methods.REASONING_TEXT_DELTA,
851
878
  Methods.REASONING_SUMMARY_DELTA
852
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
+ ]);
853
888
  var MAX_COALESCED_DELTA_CHARS = 16384;
854
889
  var AUTH_REFRESH_UNSUPPORTED_CODE = -32e3;
855
890
  var AUTH_REFRESH_UNSUPPORTED_MESSAGE = "account/chatgptAuthTokens/refresh unsupported: codex-mcp does not manage external ChatGPT auth tokens";
@@ -882,6 +917,22 @@ function stripShellNoise(delta) {
882
917
  }
883
918
  var MAX_WAITERS_PER_SESSION = 4;
884
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
+ ]);
885
936
  var SessionManager = class {
886
937
  sessions = /* @__PURE__ */ new Map();
887
938
  clients = /* @__PURE__ */ new Map();
@@ -936,7 +987,12 @@ var SessionManager = class {
936
987
  sandbox: rec.meta.sandbox,
937
988
  eventBuffer: createEventBuffer(),
938
989
  pendingRequests: /* @__PURE__ */ new Map(),
939
- 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
+ }
940
996
  };
941
997
  this.sessions.set(rec.sessionId, session);
942
998
  if (rec.lastSeq >= 0) {
@@ -971,6 +1027,26 @@ var SessionManager = class {
971
1027
  } catch {
972
1028
  }
973
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
+ }
974
1050
  // ── Session Creation ─────────────────────────────────────────────
975
1051
  async createSession(prompt, cwd, spawnOpts, effort, advanced) {
976
1052
  const sessionId = `sess_${randomUUID().slice(0, 12)}`;
@@ -992,7 +1068,9 @@ var SessionManager = class {
992
1068
  sandbox: spawnOpts.sandbox,
993
1069
  config: spawnOpts.config,
994
1070
  eventBuffer: createEventBuffer(),
995
- pendingRequests: /* @__PURE__ */ new Map()
1071
+ pendingRequests: /* @__PURE__ */ new Map(),
1072
+ lastAgentMessageText: void 0,
1073
+ progressState: { lastEventAt: now }
996
1074
  };
997
1075
  this.sessions.set(sessionId, session);
998
1076
  this.clients.set(sessionId, client);
@@ -1028,20 +1106,23 @@ var SessionManager = class {
1028
1106
  input.push({ type: "localImage", path: imagePath });
1029
1107
  }
1030
1108
  }
1031
- const turnStartResult = await client.turnStart({
1109
+ const turnStart = await this.startTurnWithCompatibilityFallback(client, {
1032
1110
  threadId,
1033
1111
  input,
1034
1112
  effort,
1035
1113
  summary: advanced?.summary,
1036
1114
  outputSchema: advanced?.outputSchema
1037
1115
  });
1116
+ const turnStartResult = turnStart.turnStartResult;
1038
1117
  const startedTurnId = extractTurnId(turnStartResult);
1039
1118
  if (startedTurnId) session.activeTurnId = startedTurnId;
1040
1119
  return {
1041
1120
  sessionId,
1042
1121
  threadId,
1043
1122
  status: "running",
1044
- pollInterval: DEFAULT_POLL_INTERVAL
1123
+ pollInterval: DEFAULT_POLL_INTERVAL,
1124
+ compatWarnings: turnStart.compatWarnings,
1125
+ progress: this.getProgress(sessionId)
1045
1126
  };
1046
1127
  } catch (err) {
1047
1128
  session.status = "error";
@@ -1074,6 +1155,7 @@ var SessionManager = class {
1074
1155
  );
1075
1156
  }
1076
1157
  clearTerminalEvents(session.eventBuffer);
1158
+ session.lastAgentMessageText = void 0;
1077
1159
  session.status = "running";
1078
1160
  session.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
1079
1161
  this.persistSessionIfChanged(session);
@@ -1093,8 +1175,11 @@ var SessionManager = class {
1093
1175
  if (overrides?.sandbox) {
1094
1176
  turnParams.sandboxPolicy = toSandboxPolicy(overrides.sandbox);
1095
1177
  }
1178
+ let compatWarnings;
1096
1179
  try {
1097
- const turnStartResult = await client.turnStart(turnParams);
1180
+ const turnStart = await this.startTurnWithCompatibilityFallback(client, turnParams);
1181
+ compatWarnings = turnStart.compatWarnings;
1182
+ const turnStartResult = turnStart.turnStartResult;
1098
1183
  const startedTurnId = extractTurnId(turnStartResult);
1099
1184
  if (startedTurnId) session.activeTurnId = startedTurnId;
1100
1185
  const canOverride = client.supportsTurnOverrides;
@@ -1119,7 +1204,9 @@ var SessionManager = class {
1119
1204
  sessionId,
1120
1205
  threadId: session.threadId,
1121
1206
  status: "running",
1122
- pollInterval: DEFAULT_POLL_INTERVAL
1207
+ pollInterval: DEFAULT_POLL_INTERVAL,
1208
+ compatWarnings,
1209
+ progress: this.getProgress(sessionId)
1123
1210
  };
1124
1211
  }
1125
1212
  // ── Session Management ───────────────────────────────────────────
@@ -1165,6 +1252,9 @@ var SessionManager = class {
1165
1252
  getLastResult(sessionId) {
1166
1253
  return this.getSessionOrThrow(sessionId).lastResult;
1167
1254
  }
1255
+ getProgress(sessionId) {
1256
+ return buildProgressInfo(this.getSessionOrThrow(sessionId));
1257
+ }
1168
1258
  getPendingActionTypes(sessionId) {
1169
1259
  const session = this.getSessionOrThrow(sessionId);
1170
1260
  const actionTypes = /* @__PURE__ */ new Set();
@@ -1284,6 +1374,50 @@ var SessionManager = class {
1284
1374
  true
1285
1375
  );
1286
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
+ }
1287
1421
  async forkSession(sessionId) {
1288
1422
  const session = this.getSessionOrThrow(sessionId);
1289
1423
  const originalClient = this.getClientOrThrow(sessionId);
@@ -1407,26 +1541,48 @@ var SessionManager = class {
1407
1541
  const buf = session.eventBuffer;
1408
1542
  const responseMode = options.responseMode ?? "full";
1409
1543
  const pollOptions = options.pollOptions;
1410
- 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;
1411
1547
  const includeActions = pollOptions?.includeActions ?? true;
1412
- const includeResult = pollOptions?.includeResult ?? true;
1548
+ const includeResult = finalOnly ? true : pollOptions?.includeResult ?? true;
1413
1549
  const maxBytes = pollOptions?.maxBytes;
1414
1550
  const effectiveCursor = cursor ?? session.lastEventCursor;
1415
- 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 : [];
1416
1553
  let cursorResetTo;
1417
- if (includeEvents && buf.events.length > 0) {
1554
+ if (buf.events.length > 0) {
1418
1555
  const earliest = buf.events[0].id;
1419
1556
  if (earliest > effectiveCursor) {
1420
1557
  cursorResetTo = earliest;
1421
- events = buf.events;
1558
+ if (includeEvents) {
1559
+ events = buf.events;
1560
+ }
1422
1561
  }
1423
1562
  }
1424
1563
  const cursorFloor = cursorResetTo ?? effectiveCursor;
1425
- if (events.length > maxEvents) {
1426
- 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;
1427
1583
  }
1428
1584
  let nextCursor = clampCursorToLatest(
1429
- events.length > 0 ? events[events.length - 1].id + 1 : cursorFloor,
1585
+ typeof highestConsumedEventId === "number" ? highestConsumedEventId + 1 : cursorFloor,
1430
1586
  buf.nextId
1431
1587
  );
1432
1588
  const actions = [];
@@ -1456,6 +1612,7 @@ var SessionManager = class {
1456
1612
  sessionId,
1457
1613
  status: session.status,
1458
1614
  pollInterval: pollIntervalForStatus(session.status),
1615
+ progress: buildProgressInfo(session),
1459
1616
  events: events.map((event) => serializeEventForMode(event, responseMode)),
1460
1617
  nextCursor,
1461
1618
  cursorResetTo,
@@ -1482,6 +1639,10 @@ var SessionManager = class {
1482
1639
  result.result = void 0;
1483
1640
  truncatedFields.push("result");
1484
1641
  }
1642
+ if (typeof result.progress !== "undefined" && payloadByteSize(result) > normalizedMaxBytes) {
1643
+ result.progress = void 0;
1644
+ truncatedFields.push("progress");
1645
+ }
1485
1646
  if (typeof result.actions !== "undefined" && payloadByteSize(result) > normalizedMaxBytes) {
1486
1647
  if (session.status === "waiting_approval") {
1487
1648
  result.actions = compactActionsForBudget(result.actions);
@@ -1509,7 +1670,7 @@ var SessionManager = class {
1509
1670
  }
1510
1671
  }
1511
1672
  }
1512
- if (includeEvents) {
1673
+ if (includeEvents || finalOnly) {
1513
1674
  session.lastEventCursor = persistMonotonicCursor(
1514
1675
  session.lastEventCursor,
1515
1676
  result.nextCursor,
@@ -1738,6 +1899,7 @@ var SessionManager = class {
1738
1899
  client.onNotification((method, params) => {
1739
1900
  session.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
1740
1901
  const p = params;
1902
+ recordProgressObservation(session, method, p);
1741
1903
  switch (method) {
1742
1904
  case Methods.THREAD_STARTED: {
1743
1905
  const thread = isRecord(p.thread) ? p.thread : void 0;
@@ -1781,17 +1943,21 @@ var SessionManager = class {
1781
1943
  if (session.status === "cancelled") break;
1782
1944
  const turnObj = p.turn;
1783
1945
  const completedTurnId = turnObj?.id ?? session.activeTurnId ?? "";
1946
+ const rawTurnOutput = normalizeOptionalString(turnObj?.output);
1947
+ const finalText = normalizeOptionalString(turnObj?.output) ?? session.lastAgentMessageText;
1784
1948
  session.status = "idle";
1785
1949
  session.activeTurnId = void 0;
1786
1950
  session.lastResult = {
1787
1951
  turnId: completedTurnId,
1788
- output: turnObj?.output,
1952
+ text: finalText,
1953
+ output: rawTurnOutput,
1789
1954
  structuredOutput: turnObj?.structuredOutput,
1790
1955
  turn: p.turn,
1791
1956
  status: turnObj?.status,
1792
1957
  turnError: turnObj?.error,
1793
1958
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
1794
1959
  };
1960
+ mergeProgressTokens(session, extractTokens(turnObj?.usage));
1795
1961
  pushEvent(
1796
1962
  session.eventBuffer,
1797
1963
  "result",
@@ -1846,6 +2012,10 @@ var SessionManager = class {
1846
2012
  const item = p.item;
1847
2013
  const itemType = item && typeof item.type === "string" ? item.type : void 0;
1848
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
+ }
1849
2019
  const eventType = itemType === "agentMessage" || itemType === "userMessage" ? "output" : "progress";
1850
2020
  pushEvent(session.eventBuffer, eventType, {
1851
2021
  method,
@@ -1889,6 +2059,7 @@ var SessionManager = class {
1889
2059
  }
1890
2060
  session.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
1891
2061
  const p = params;
2062
+ recordProgressObservation(session, method, p);
1892
2063
  switch (method) {
1893
2064
  case Methods.COMMAND_APPROVAL: {
1894
2065
  const requestId = `req_${randomUUID().slice(0, 8)}`;
@@ -2175,16 +2346,7 @@ var SessionManager = class {
2175
2346
  this.ttlWarningEmitted.delete(id);
2176
2347
  this.requestCancellation(id, "Running timeout");
2177
2348
  } else if ((session.status === "cancelled" || session.status === "error") && age > DEFAULT_TERMINAL_CLEANUP_MS) {
2178
- this.ttlWarningEmitted.delete(id);
2179
- this.clients.get(id)?.destroy().catch((err) => {
2180
- console.error(
2181
- `[codex-mcp] Failed to destroy app-server client during cleanup: session=${id} error=${err instanceof Error ? err.message : String(err)}`
2182
- );
2183
- });
2184
- this.clients.delete(id);
2185
- this.sessions.delete(id);
2186
- this.lastPersistedStatus.delete(id);
2187
- this.ttlWarningEmitted.delete(id);
2349
+ this.evictSession(id, true);
2188
2350
  } else {
2189
2351
  let ttlMs;
2190
2352
  if (session.status === "idle") {
@@ -2215,6 +2377,34 @@ var SessionManager = class {
2215
2377
  );
2216
2378
  });
2217
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
+ }
2218
2408
  };
2219
2409
  function pollIntervalForStatus(status) {
2220
2410
  if (status === "waiting_approval") return WAITING_APPROVAL_POLL_INTERVAL;
@@ -2229,6 +2419,114 @@ function createEventBuffer() {
2229
2419
  nextId: 0
2230
2420
  };
2231
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
+ }
2232
2530
  function clearTerminalEvents(buf) {
2233
2531
  buf.events = buf.events.filter((e) => e.type !== "result" && e.type !== "error");
2234
2532
  }
@@ -2718,6 +3016,20 @@ function buildExecutionInfo(waitForResultMs, status, fallbackReason) {
2718
3016
  fallbackReason: effective === "background" ? fallbackReason : void 0
2719
3017
  };
2720
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
+ }
2721
3033
  async function waitForCodexSessionForegroundResult(sessionManager, sessionId, waitForResultMs, signal) {
2722
3034
  const deadline = Date.now() + Math.min(waitForResultMs, 3e5);
2723
3035
  while (Date.now() < deadline) {
@@ -2760,6 +3072,9 @@ async function waitForCodexSessionForegroundResult(sessionManager, sessionId, wa
2760
3072
  }
2761
3073
 
2762
3074
  // src/tools/codex.ts
3075
+ function safeGetProgress(sessionManager, sessionId) {
3076
+ return typeof sessionManager.getProgress === "function" ? sessionManager.getProgress(sessionId) : void 0;
3077
+ }
2763
3078
  async function executeCodex(args, sessionManager, serverCwd, requestSignal) {
2764
3079
  const cwd = resolveAndValidateCwd(args.cwd, serverCwd);
2765
3080
  const spawnOpts = extractSpawnOptions(args);
@@ -2774,6 +3089,10 @@ async function executeCodex(args, sessionManager, serverCwd, requestSignal) {
2774
3089
  const waitMs = args.advanced?.waitForResult;
2775
3090
  const baseResult = {
2776
3091
  ...startResult,
3092
+ progress: coerceProgressForStatus(
3093
+ "running",
3094
+ safeGetProgress(sessionManager, startResult.sessionId) ?? startResult.progress
3095
+ ),
2777
3096
  execution: buildExecutionInfo(waitMs, "running"),
2778
3097
  interactionState: interactionStateForStatus("running"),
2779
3098
  recommendedNextAction: recommendedNextActionForStatus("running")
@@ -2791,6 +3110,15 @@ async function executeCodex(args, sessionManager, serverCwd, requestSignal) {
2791
3110
  result: foreground.result,
2792
3111
  status: foreground.status,
2793
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
+ ),
2794
3122
  pollInterval: foreground.status === "waiting_approval" ? WAITING_APPROVAL_POLL_INTERVAL : foreground.status === "running" ? startResult.pollInterval : void 0,
2795
3123
  execution: buildExecutionInfo(waitMs, foreground.status, foreground.fallbackReason),
2796
3124
  interactionState: interactionStateForStatus(foreground.status),
@@ -2802,6 +3130,9 @@ async function executeCodex(args, sessionManager, serverCwd, requestSignal) {
2802
3130
  }
2803
3131
 
2804
3132
  // src/tools/codex-reply.ts
3133
+ function safeGetProgress2(sessionManager, sessionId) {
3134
+ return typeof sessionManager.getProgress === "function" ? sessionManager.getProgress(sessionId) : void 0;
3135
+ }
2805
3136
  async function executeCodexReply(args, sessionManager, requestSignal) {
2806
3137
  const startResult = await sessionManager.replyToSession(args.sessionId, args.prompt, {
2807
3138
  model: args.model,
@@ -2816,6 +3147,10 @@ async function executeCodexReply(args, sessionManager, requestSignal) {
2816
3147
  const waitMs = args.waitForResult;
2817
3148
  const baseResult = {
2818
3149
  ...startResult,
3150
+ progress: coerceProgressForStatus(
3151
+ "running",
3152
+ safeGetProgress2(sessionManager, startResult.sessionId) ?? startResult.progress
3153
+ ),
2819
3154
  execution: buildExecutionInfo(waitMs, "running"),
2820
3155
  interactionState: interactionStateForStatus("running"),
2821
3156
  recommendedNextAction: recommendedNextActionForStatus("running")
@@ -2833,6 +3168,15 @@ async function executeCodexReply(args, sessionManager, requestSignal) {
2833
3168
  sessionId: startResult.sessionId,
2834
3169
  threadId: startResult.threadId,
2835
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
+ ),
2836
3180
  pollInterval: foreground.status === "waiting_approval" ? WAITING_APPROVAL_POLL_INTERVAL : foreground.status === "running" ? startResult.pollInterval : void 0,
2837
3181
  result: foreground.result,
2838
3182
  completedAt: foreground.completedAt,
@@ -2884,6 +3228,13 @@ async function executeCodexSession(args, sessionManager) {
2884
3228
  };
2885
3229
  }
2886
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
+ });
2887
3238
  case "clean_background_terminals":
2888
3239
  if (!args.sessionId) {
2889
3240
  return {
@@ -2919,24 +3270,15 @@ function executeCodexCheck(args, sessionManager, requestSignal) {
2919
3270
  const maxEvents = typeof args.maxEvents === "number" ? Math.max(POLL_MIN_MAX_EVENTS, Math.floor(args.maxEvents)) : POLL_DEFAULT_MAX_EVENTS;
2920
3271
  const waitMs = pollOptions?.waitMs;
2921
3272
  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);
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));
2940
3282
  }
2941
3283
  return enrichCheckResult(
2942
3284
  sessionManager,
@@ -3057,6 +3399,31 @@ function executeCodexCheck(args, sessionManager, requestSignal) {
3057
3399
  };
3058
3400
  }
3059
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
+ }
3060
3427
  function enrichCheckResult(sessionManager, result) {
3061
3428
  const actionTypes = result.status === "waiting_approval" ? sessionManager.getPendingActionTypes(result.sessionId) : [];
3062
3429
  return {
@@ -3147,6 +3514,10 @@ function probeAppServer(codexCommand, codexIsPath, env) {
3147
3514
  }
3148
3515
 
3149
3516
  // src/tools/codex-setup.ts
3517
+ function isCodexInternalExecutable(info) {
3518
+ const ext = path5.extname(info.command);
3519
+ return path5.basename(info.command, ext).toLowerCase() === "codex-internal";
3520
+ }
3150
3521
  function classifyAuthResult(status, combined) {
3151
3522
  if (status === 0) return "authenticated";
3152
3523
  if (/(not (logged|authenticated)|login required|run\s+codex\s+login)/i.test(combined)) {
@@ -3159,6 +3530,13 @@ function resolveCodexStateDir() {
3159
3530
  return configured && configured !== "" ? configured : path5.join(homedir(), ".codex-mcp", "state");
3160
3531
  }
3161
3532
  function probeCodexAuth(info) {
3533
+ if (isCodexInternalExecutable(info)) {
3534
+ return {
3535
+ ok: true,
3536
+ state: "unknown",
3537
+ detail: "Using a codex-internal executable; auth/login readiness is not probed and does not block setup readiness."
3538
+ };
3539
+ }
3162
3540
  const invocation = resolveCodexInvocation(["login", "status"], {
3163
3541
  codexCommand: info.command,
3164
3542
  codexIsPath: info.isPath
@@ -3191,9 +3569,11 @@ async function executeCodexSetup(input, serverCwd) {
3191
3569
  let executable;
3192
3570
  let auth;
3193
3571
  let clientMode;
3572
+ let internalExecutable = false;
3194
3573
  try {
3195
3574
  const info = resolveDefaultCodexExecutable();
3196
3575
  const available = info.source !== "default";
3576
+ internalExecutable = isCodexInternalExecutable(info);
3197
3577
  executable = {
3198
3578
  ok: available,
3199
3579
  source: info.source,
@@ -3237,7 +3617,7 @@ async function executeCodexSetup(input, serverCwd) {
3237
3617
  if (auth.state === "unauthenticated") {
3238
3618
  warnings.push(auth.detail);
3239
3619
  nextSteps.push("Run `codex login` and rerun `codex_setup`.");
3240
- } else if (auth.state === "unknown") {
3620
+ } else if (auth.state === "unknown" && !internalExecutable) {
3241
3621
  warnings.push(auth.detail);
3242
3622
  nextSteps.push(
3243
3623
  "Verify Codex authentication explicitly (for example with `codex login status`) before relying on this environment."
@@ -3527,8 +3907,11 @@ function buildConfigGuideText() {
3527
3907
  "- `codex_check.pollOptions.includeEvents`: default `true`.",
3528
3908
  "- `codex_check.pollOptions.includeActions`: default `true`.",
3529
3909
  "- `codex_check.pollOptions.includeResult`: default `true`.",
3910
+ "- `codex_check.pollOptions.skipDeltas`: default `false`.",
3911
+ "- `codex_check.pollOptions.finalOnly`: default `false`.",
3530
3912
  "- `codex_check.pollOptions.maxBytes`: default unlimited.",
3531
3913
  "- `codex_check.cursor`: default is session last consumed cursor when omitted.",
3914
+ "- `progress` is included on `codex`, `codex_reply`, and `codex_check` responses.",
3532
3915
  ""
3533
3916
  ].join("\n");
3534
3917
  }
@@ -3542,6 +3925,8 @@ function buildGotchasText() {
3542
3925
  `- Poll enforces minimum \`maxEvents=${POLL_MIN_MAX_EVENTS}\`; sending \`0\` is normalized to \`${POLL_MIN_MAX_EVENTS}\`.`,
3543
3926
  `- \`respond_permission\` and \`respond_user_input\` default to compact ACK with \`maxEvents=${RESPOND_DEFAULT_MAX_EVENTS}\`.`,
3544
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.",
3545
3930
  "- respond_* uses monotonic cursor handling: `max(cursor, sessionLastCursor)`.",
3546
3931
  "- If `cursorResetTo` is present, your cursor is stale (old events were evicted); restart from that value.",
3547
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.",
@@ -3556,6 +3941,8 @@ function buildGotchasText() {
3556
3941
  "## Event model",
3557
3942
  "",
3558
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.",
3559
3946
  "- Fine-grained stream semantics are in `events[].data.method` (for example command output delta, reasoning delta, turn updates).",
3560
3947
  '- Retryable interruptions surface as `progress` with `method="codex-mcp/reconnect"` and include retry fields.',
3561
3948
  "- During reconnect/retry, continue polling normally; if retries stop (`willRetry=false`), session transitions to error path.",
@@ -3572,7 +3959,8 @@ function buildGotchasText() {
3572
3959
  `- Idle sessions are auto-cleaned after ${msToMinutes(DEFAULT_IDLE_CLEANUP_MS)} minutes.`,
3573
3960
  `- Running/waiting sessions are auto-cleaned after ${msToMinutes(DEFAULT_RUNNING_CLEANUP_MS)} minutes.`,
3574
3961
  `- Error/cancelled sessions are retained for about ${msToMinutes(DEFAULT_TERMINAL_CLEANUP_MS)} minutes, then removed.`,
3575
- "- 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.",
3576
3964
  "",
3577
3965
  "## Capacity",
3578
3966
  "",
@@ -3659,6 +4047,7 @@ function buildQuickstartText() {
3659
4047
  "```",
3660
4048
  "",
3661
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.",
3662
4051
  "",
3663
4052
  "## Cursor notes",
3664
4053
  "",
@@ -3667,6 +4056,13 @@ function buildQuickstartText() {
3667
4056
  "- Omit `responseMode`: default is `minimal`.",
3668
4057
  "- Use returned `nextCursor` for the next call.",
3669
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.",
3670
4066
  ""
3671
4067
  ].join("\n");
3672
4068
  }
@@ -3715,6 +4111,13 @@ function buildDelegationGuideText() {
3715
4111
  "",
3716
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.",
3717
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
+ "",
3718
4121
  "## Quick mode: `waitForResult`",
3719
4122
  "For short tasks (< 2 min), set `advanced.waitForResult` to get the final result in a single tool call:",
3720
4123
  "```json",
@@ -3742,6 +4145,11 @@ function buildDelegationGuideText() {
3742
4145
  "",
3743
4146
  "**Approval timeout:** Default is 60s; infrequent polling causes silent auto-decline. See `codex-mcp:///gotchas`.",
3744
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
+ "",
3745
4153
  "## Security notes",
3746
4154
  "- `sandbox: 'read-only'` is the strongest isolation \u2014 blocks all writes regardless of approval policy",
3747
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",
@@ -3929,9 +4337,13 @@ function registerResources(server, deps) {
3929
4337
  }
3930
4338
 
3931
4339
  // src/server.ts
3932
- var SERVER_VERSION = true ? "2.1.5" : "0.0.0-dev";
4340
+ var SERVER_VERSION = true ? "2.1.7" : "0.0.0-dev";
3933
4341
  function formatErrorMessage(err) {
3934
4342
  const message = err instanceof Error ? err.message : String(err);
4343
+ const compatibilityKind = classifyTurnCompatibilityError(err);
4344
+ if (compatibilityKind) {
4345
+ return compatibilityErrorMessage(compatibilityKind);
4346
+ }
3935
4347
  const m = /^Error \[([A-Z_]+)\]:\s*(.*)$/.exec(message);
3936
4348
  if (m) {
3937
4349
  const [, code, rest] = m;
@@ -3983,6 +4395,28 @@ function createServer(serverCwd, options) {
3983
4395
  });
3984
4396
  const interactionStateSchema = z.enum(["working", "waiting_input", "finished"]);
3985
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
+ });
3986
4420
  const setupResultShape = {
3987
4421
  ready: z.boolean(),
3988
4422
  cwd: z.string(),
@@ -4019,6 +4453,8 @@ function createServer(serverCwd, options) {
4019
4453
  ),
4020
4454
  result: z.unknown().optional().describe("Final result when waitForResult is set and session completed."),
4021
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(),
4022
4458
  execution: executionInfoSchema.optional(),
4023
4459
  interactionState: interactionStateSchema.optional(),
4024
4460
  recommendedNextAction: nextActionSchema.optional(),
@@ -4028,6 +4464,10 @@ function createServer(serverCwd, options) {
4028
4464
  includeEvents: z.boolean().optional().describe("Default: true. Include events[] in response."),
4029
4465
  includeActions: z.boolean().optional().describe("Default: true. Include actions[] in response."),
4030
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."),
4031
4471
  maxBytes: z.number().int().positive().optional().describe("Default: unlimited. Best-effort response payload cap in bytes."),
4032
4472
  waitMs: z.number().int().nonnegative().optional().describe(
4033
4473
  "Long-poll: block up to this many ms for new events (max 120000). Omit or 0 for immediate return."
@@ -4173,7 +4613,7 @@ function createServer(serverCwd, options) {
4173
4613
  "codex",
4174
4614
  {
4175
4615
  title: "Start Codex Session",
4176
- 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.",
4177
4617
  inputSchema: {
4178
4618
  prompt: z.string().describe("Task or question"),
4179
4619
  approvalPolicy: z.enum(APPROVAL_POLICIES).describe("Required enum: untrusted/on-failure/on-request/never."),
@@ -4301,18 +4741,23 @@ function createServer(serverCwd, options) {
4301
4741
  "codex_session",
4302
4742
  {
4303
4743
  title: "Manage Sessions",
4304
- description: `Session actions: list, get, cancel, interrupt, fork, clean_background_terminals.
4744
+ description: `Session actions: list, get, cancel, interrupt, fork, clean, clean_background_terminals.
4305
4745
 
4306
4746
  - list: sessions in memory.
4307
4747
  - get: details. includeSensitive defaults to false; true adds threadId/cwd/profile/config.
4308
4748
  - cancel: terminal.
4309
4749
  - interrupt: stop current turn.
4310
4750
  - fork: clone current thread into a new session; source remains unchanged.
4751
+ - clean: batch-remove idle/error/cancelled sessions, optionally from disk too.
4311
4752
  - clean_background_terminals: ask app-server to clean stale background terminals for this thread.`,
4312
4753
  inputSchema: {
4313
4754
  action: z.enum(SESSION_ACTIONS),
4314
4755
  sessionId: z.string().optional().describe("Required for get/cancel/interrupt/fork/clean_background_terminals"),
4315
- 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.")
4316
4761
  },
4317
4762
  outputSchema: {
4318
4763
  sessions: z.array(publicSessionInfoSchema).optional(),
@@ -4333,6 +4778,11 @@ function createServer(serverCwd, options) {
4333
4778
  pollInterval: z.number().int().optional().describe(
4334
4779
  "Recommended minimum delay before next poll (ms): running >=120000, waiting_approval ~=1000."
4335
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(),
4336
4786
  success: z.boolean().optional(),
4337
4787
  message: z.string().optional(),
4338
4788
  ...errorOutputShape
@@ -4368,7 +4818,7 @@ function createServer(serverCwd, options) {
4368
4818
  "codex_check",
4369
4819
  {
4370
4820
  title: "Poll & Respond",
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.
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.
4372
4822
 
4373
4823
  poll: events since cursor. Default maxEvents=${POLL_DEFAULT_MAX_EVENTS}.
4374
4824
  respond_permission: approval decision. Default maxEvents=${RESPOND_DEFAULT_MAX_EVENTS} (compact ACK).
@@ -4380,6 +4830,7 @@ respond_user_input: user-input answers. Default maxEvents=${RESPOND_DEFAULT_MAX_
4380
4830
  pollInterval: z.number().int().optional().describe(
4381
4831
  "Recommended minimum delay before next poll (ms): running >=120000, waiting_approval ~=1000."
4382
4832
  ),
4833
+ progress: progressSchema.optional(),
4383
4834
  interactionState: interactionStateSchema.optional(),
4384
4835
  recommendedNextAction: nextActionSchema.optional(),
4385
4836
  events: z.array(
@@ -4415,6 +4866,7 @@ respond_user_input: user-input answers. Default maxEvents=${RESPOND_DEFAULT_MAX_
4415
4866
  ).optional(),
4416
4867
  result: z.object({
4417
4868
  turnId: z.string(),
4869
+ text: z.string().optional(),
4418
4870
  output: z.string().optional(),
4419
4871
  structuredOutput: z.unknown().optional(),
4420
4872
  turn: z.unknown().optional(),
@@ -5013,7 +5465,7 @@ var ExecClient = class extends EventEmitter2 {
5013
5465
 
5014
5466
  // src/session/persistence.ts
5015
5467
  import { join as join5 } from "path";
5016
- import { mkdirSync as mkdirSync4, existsSync as existsSync7 } from "fs";
5468
+ import { mkdirSync as mkdirSync4, existsSync as existsSync7, rmSync as rmSync3 } from "fs";
5017
5469
  import { homedir as homedir2 } from "os";
5018
5470
 
5019
5471
  // src/persistence/atomic-writer.ts
@@ -5460,6 +5912,11 @@ var SessionPersistence = class {
5460
5912
  this.eventLogs.delete(sessionId);
5461
5913
  }
5462
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
+ }
5463
5920
  /** Clean up: flush all logs, release lock. */
5464
5921
  destroy() {
5465
5922
  for (const log of this.eventLogs.values()) {