Skip to content

Commit 4cb9a52

Browse files
xuiocodex
andcommitted
Harden Codex failure reporting and progress compatibility
Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent 9ae5c0a commit 4cb9a52

13 files changed

Lines changed: 212 additions & 39 deletions

dist/index.js

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22298,7 +22298,13 @@ function defaultModel(env = process.env) {
2229822298
return value;
2229922299
}
2230022300
function resolveRequestedModel(options, env = process.env) {
22301-
return options.model?.trim() || modelForPreset(options.modelPreset) || defaultModel(env);
22301+
return normalizeRequestedModel(options.model) || modelForPreset(options.modelPreset) || defaultModel(env);
22302+
}
22303+
function normalizeRequestedModel(model) {
22304+
const value = model?.trim();
22305+
if (!value) return void 0;
22306+
if (value === "gpt-5.5-codex") return "gpt-5.5";
22307+
return value;
2230222308
}
2230322309
var RunValidationError = class extends Error {
2230422310
constructor(message) {
@@ -23754,6 +23760,10 @@ function recoveryForWait(kind, timeoutReason) {
2375423760
}
2375523761

2375623762
// src/progress.ts
23763+
function progressNotificationsEnabled(env = process.env) {
23764+
const raw = env.CODEX_SUBAGENTS_ENABLE_PROGRESS_NOTIFICATIONS?.trim().toLowerCase();
23765+
return ["1", "true", "yes", "on"].includes(raw ?? "");
23766+
}
2375723767
function progressSendTimeoutMs(env = process.env) {
2375823768
const parsed = Number(env.CODEX_SUBAGENTS_PROGRESS_SEND_TIMEOUT_MS);
2375923769
if (!Number.isFinite(parsed) || parsed <= 0) return 1e3;
@@ -23775,6 +23785,7 @@ function progressMinIntervalMs(env = process.env) {
2377523785
}
2377623786
function createProgressReporter(extra, options = {}) {
2377723787
const progressToken = extra?._meta?.progressToken;
23788+
const enabled = options.enabled ?? progressNotificationsEnabled();
2377823789
const sendTimeoutMs = options.sendTimeoutMs ?? progressSendTimeoutMs();
2377923790
const minIntervalMs = options.minIntervalMs ?? progressMinIntervalMs();
2378023791
let progress = 0;
@@ -23785,7 +23796,7 @@ function createProgressReporter(extra, options = {}) {
2378523796
let throttleTimer;
2378623797
let pendingThrottled;
2378723798
function queueSend(message, progressOptions = {}) {
23788-
if (progressToken === void 0 || !extra) return;
23799+
if (!enabled || progressToken === void 0 || !extra) return;
2378923800
pending = pending.catch(() => {
2379023801
}).then(async () => {
2379123802
if (disabled) return;
@@ -23841,10 +23852,11 @@ function createProgressReporter(extra, options = {}) {
2384123852
async function send(message, progressOptions = {}) {
2384223853
logger.rawDebug("mcp.progress", {
2384323854
hasProgressToken: progressToken !== void 0,
23855+
enabled,
2384423856
message,
2384523857
options: progressOptions
2384623858
});
23847-
if (progressToken === void 0 || !extra || disabled) return;
23859+
if (!enabled || progressToken === void 0 || !extra || disabled) return;
2384823860
const elapsed = Date.now() - lastSentAt;
2384923861
if (progressOptions.force || minIntervalMs === 0 || lastSentAt === 0 || elapsed >= minIntervalMs) {
2385023862
if (progressOptions.force && throttleTimer) {
@@ -23861,7 +23873,7 @@ function createProgressReporter(extra, options = {}) {
2386123873
});
2386223874
}
2386323875
async function flush() {
23864-
if (progressToken === void 0 || !extra) return;
23876+
if (!enabled || progressToken === void 0 || !extra) return;
2386523877
if (throttleTimer) {
2386623878
clearTimeout(throttleTimer);
2386723879
throttleTimer = void 0;
@@ -24217,6 +24229,9 @@ function cloneSummary(summary) {
2421724229
events: summary.events ? [...summary.events] : void 0
2421824230
});
2421924231
}
24232+
function summaryErrorText(summary) {
24233+
return summary.errors.map((error2) => String(error2).trim()).filter(Boolean).join("\n");
24234+
}
2422024235
function truncate2(text, maxChars) {
2422124236
if (text.length <= maxChars) return { text, truncatedChars: 0 };
2422224237
return { text: text.slice(0, maxChars), truncatedChars: text.length - maxChars };
@@ -24532,7 +24547,7 @@ var CodexAppServerSession = class _CodexAppServerSession {
2453224547
exitCode: status === "completed" ? 0 : null,
2453324548
signal: null,
2453424549
finalMessage: final.text,
24535-
stderr: redactSensitiveText(stderr.text() || error2 || ""),
24550+
stderr: redactSensitiveText(stderr.text() || summaryErrorText(summary) || error2 || ""),
2453624551
stdoutTail: redactSensitiveText(stdout.text()),
2453724552
truncated: {
2453824553
stdoutChars: stdout.truncated(),
@@ -24951,7 +24966,7 @@ var CodexAppServerSession = class _CodexAppServerSession {
2495124966
exitCode: status === "completed" ? 0 : null,
2495224967
signal: null,
2495324968
finalMessage: final.text,
24954-
stderr: redactSensitiveText(active.stderr.text()),
24969+
stderr: redactSensitiveText(active.stderr.text() || summaryErrorText(active.summary)),
2495524970
stdoutTail: redactSensitiveText(active.stdout.text()),
2495624971
truncated: {
2495724972
stdoutChars: active.stdout.truncated(),
@@ -26463,6 +26478,7 @@ var usageGuide = [
2646326478
"- If a response mentions outputArtifacts, use the artifact paths for full retained output instead of asking Codex to resend huge stdout/stderr.",
2646426479
'- Do not use model_preset "spark" by default. Use Spark only when the user asks for Spark or when a quick focused sidecar check is clearly more appropriate than the default Codex model.',
2646526480
'- Use reasoning "medium" by default, "low" for simple checks, and "high" only for difficult normal analysis. Use advanced.reasoning "xhigh" only when the user explicitly asks for maximum reasoning.',
26481+
'- For the current strongest ChatGPT-backed Codex model, use advanced.model "gpt-5.5" or omit advanced.model. Do not invent a "-codex" suffix for GPT-5.5.',
2646626482
'- Do not combine model_preset "spark" with reasoning_summary values other than "none"; Spark does not support reasoning.summary.',
2646726483
"- Do not set service_tier by default. Let Codex use its normal account/default service tier unless the user explicitly asks for a service tier.",
2646826484
"- Pass project_dir whenever Claude knows the active project directory so Codex works in the same tree as Claude Code.",
@@ -26727,7 +26743,9 @@ var codexRoleDefaults = {
2672726743
}
2672826744
};
2672926745
var advancedInputSchema = external_exports.object({
26730-
model: external_exports.string().trim().min(1).optional().describe("Exact Codex model. Use gpt-5.3-codex-spark only when the user explicitly asks for Codex Spark."),
26746+
model: external_exports.string().trim().min(1).optional().describe(
26747+
"Exact Codex model. Use gpt-5.5 for the current strongest ChatGPT-backed model; do not add a -codex suffix to GPT-5.5. Use gpt-5.3-codex-spark only when the user explicitly asks for Codex Spark."
26748+
),
2673126749
model_preset: modelPresetSchema.optional().describe("Compatibility preset. Prefer advanced.model for new calls."),
2673226750
reasoning: reasoningEffortSchema.optional().describe("Advanced reasoning effort, including xhigh. Minimal is still rejected by this server."),
2673326751
reasoning_effort: reasoningEffortSchema.optional().describe("Compatibility alias for advanced.reasoning."),
@@ -28217,28 +28235,33 @@ registerTool(
2821728235
const compactSession2 = compactSessionSnapshotForMcp(waited.session);
2821828236
const recovery = recoveryForWait("codex_session", waited.timeoutReason);
2821928237
const waitResult = waited.result ? compactAgentResultForMcp(waited.result) : compactSession2.lastResult;
28238+
const waitAgent = waitResult && typeof waitResult === "object" && "ok" in waitResult && "status" in waitResult ? waitResult : void 0;
28239+
const waitAgentRecovery = waitAgent ? recoveryForAgentResult(waitAgent) : void 0;
2822028240
const waitValue = waitResult && typeof waitResult === "object" ? waitResult.structuredOutput ?? waitResult.finalMessage : void 0;
2822128241
const waitFallback = waitResult && typeof waitResult === "object" ? waitResult.finalMessage ?? "" : "";
28222-
const resultText = waitResult && typeof waitResult === "object" ? stringifyResultValue(waitValue, waitFallback) : "";
28242+
const resultText = waitAgent ? visibleAgentAnswer(waitAgent, waitAgentRecovery) : waitResult && typeof waitResult === "object" ? stringifyResultValue(waitValue, waitFallback) : "";
2822328243
const completed = Boolean(waited.completed);
28244+
const terminalStatus = waitAgent?.status ?? (completed ? sessionResourceStatus(waited.session) : "running");
28245+
const ok = waited.timeoutReason !== "wait_cancelled" && (!completed || !waitAgent || Boolean(waitAgent.ok)) && terminalStatus !== "failed";
2822428246
const progressPayload = sessionProgressPayload(compactSession2, waitResult);
2822528247
const payload2 = {
28226-
ok: waited.timeoutReason !== "wait_cancelled",
28248+
ok,
2822728249
completed,
28228-
status: completed ? "completed" : "running",
28229-
result: resultText || (completed ? "Codex session is idle." : "Codex session is still running."),
28250+
status: terminalStatus,
28251+
result: resultText || (completed ? `Codex session ${terminalStatus}.` : "Codex session is still running."),
2823028252
session_id: args.session_id,
2823128253
last_milestone_seq: waited.session.lastMilestoneSeq,
2823228254
elapsed_ms: progressPayload.elapsed_ms,
2823328255
...waitTimeoutFields(waitTimeout2),
28234-
summary: completed ? summarizeResultValue(waitValue, resultText, "Codex session is ready.") : waited.timeoutReason === "wait_timeout" ? "Codex session is still running." : "Codex session wait was cancelled."
28256+
summary: completed ? summarizeResultValue(waitValue, resultText, `Codex session ${terminalStatus}.`) : waited.timeoutReason === "wait_timeout" ? "Codex session is still running." : "Codex session wait was cancelled."
2823528257
};
2823628258
if (waited.timeoutReason) payload2.timeoutReason = waited.timeoutReason;
2823728259
if (!completed && waited.timeoutReason === "wait_timeout") {
2823828260
payload2.hint = waitTimeout2.capped ? "This wait returned at the server responsiveness cap. Call codex_followup mode wait again, or read codex://sessions/<session_id> for current progress." : "Call codex_followup mode wait again, or read codex://sessions/<session_id> for current progress.";
2823928261
}
2824028262
if (recovery) {
2824128263
payload2.error = {
28264+
message: waitAgent && !waitAgent.ok ? agentFallbackErrorText(waitAgent, waitAgentRecovery) ?? `Codex task ${waitAgent.status}` : void 0,
2824228265
recoverable: recovery.recoverable,
2824328266
kind: recovery.reason,
2824428267
retry_after_ms: recovery.retryAfterMs
@@ -28251,7 +28274,7 @@ registerTool(
2825128274
...progressPayload
2825228275
};
2825328276
}
28254-
return nativeTextResult(payload2, waited.timeoutReason === "wait_cancelled");
28277+
return nativeTextResult(payload2, waited.timeoutReason === "wait_cancelled" || completed && !ok);
2825528278
}
2825628279
if (mode === "cancel") {
2825728280
await progress.send(`Cancelling Codex session ${args.session_id}`);
@@ -28499,12 +28522,13 @@ registerTool(
2849928522
});
2850028523
}
2850128524
const compactResult = waited.result ? compactAgentResultForMcp(waited.result) : void 0;
28525+
const recovery = waited.result ? recoveryForAgentResult(waited.result) : void 0;
2850228526
const resultValue = compactResult?.structuredOutput ?? compactResult?.finalMessage;
28503-
const resultText = compactResult ? stringifyResultValue(resultValue, compactResult.finalMessage) : waited.session.error ?? `Codex session ${sessionResourceStatus(waited.session)}`;
28527+
const resultText = compactResult ? visibleAgentAnswer(compactResult, recovery) : waited.session.error ?? `Codex session ${sessionResourceStatus(waited.session)}`;
2850428528
const status = compactResult?.status ?? (waited.session.status === "cancelled" ? "cancelled" : waited.session.status === "failed" ? "failed" : "completed");
2850528529
await progress.send(`Codex session ${waited.session.id} finished`, { force: true });
2850628530
await progress.flush();
28507-
return nativeTextResult({
28531+
const payload = {
2850828532
ok: status === "completed",
2850928533
status,
2851028534
completed: true,
@@ -28515,7 +28539,16 @@ registerTool(
2851528539
last_milestone_seq: waited.session.lastMilestoneSeq,
2851628540
...waitTimeoutFields(waitTimeout),
2851728541
hint: "Call codex_wait_any again with remaining_session_ids to collect the next finisher."
28518-
});
28542+
};
28543+
if (compactResult && recovery) {
28544+
payload.error = {
28545+
message: agentFallbackErrorText(compactResult, recovery) ?? `Codex task ${status}`,
28546+
recoverable: recovery.recoverable,
28547+
kind: recovery.reason,
28548+
retry_after_ms: recovery.retryAfterMs
28549+
};
28550+
}
28551+
return nativeTextResult(payload, status === "failed" || status === "cancelled");
2851928552
} catch (error2) {
2852028553
await progress.flush();
2852128554
logger.error("codex_wait_any.failed", { error: errorForLog(error2) });

docs/ARCHITECTURE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ long-running work.
8383

8484
## Progress, Backpressure, And Retention
8585

86-
The server emits MCP progress notifications when the client supplies a progress
87-
token. Long waits include heartbeat progress so Claude Code can keep the request
88-
alive.
86+
MCP progress notifications are disabled by default because current Claude Code
87+
builds can close the stdio transport when they receive progress for a token they
88+
no longer track. Set `CODEX_SUBAGENTS_ENABLE_PROGRESS_NOTIFICATIONS=1` only when
89+
the client is known to accept MCP progress notifications reliably.
8990

9091
Background Codex sessions also expose `codex://sessions/{session_id}` resources.
9192
Each resource carries a small in-memory milestone ring buffer and a compact

docs/USAGE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ servers should not be loaded for the run.
360360
| `CODEX_SUBAGENTS_LOG_FILE` | Optional JSONL log file path |
361361
| `CODEX_SUBAGENTS_LOG_FILE_MAX_BYTES` | Rotate the log file after this size |
362362
| `CODEX_SUBAGENTS_LOG_MAX_STRING_CHARS` | Maximum retained string payload per log field |
363+
| `CODEX_SUBAGENTS_ENABLE_PROGRESS_NOTIFICATIONS` | Set `1` to emit MCP progress notifications; disabled by default for Claude Code compatibility |
363364
| `CODEX_SUBAGENTS_PROGRESS_HEARTBEAT_MS` | Progress heartbeat interval |
364365
| `CODEX_SUBAGENTS_PROGRESS_MIN_INTERVAL_MS` | Minimum delay between progress notifications; rapid updates are coalesced |
365366
| `CODEX_SUBAGENTS_ENABLE_DEBUG_TOOLS` | Set `1` to expose tool-callable diagnostics |

src/app-server.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ function cloneSummary(summary: CodexEventSummary): CodexEventSummary {
212212
});
213213
}
214214

215+
function summaryErrorText(summary: Pick<CodexEventSummary, "errors">): string {
216+
return summary.errors
217+
.map((error) => String(error).trim())
218+
.filter(Boolean)
219+
.join("\n");
220+
}
221+
215222
function truncate(text: string, maxChars: number): { text: string; truncatedChars: number } {
216223
if (text.length <= maxChars) return { text, truncatedChars: 0 };
217224
return { text: text.slice(0, maxChars), truncatedChars: text.length - maxChars };
@@ -547,7 +554,7 @@ export class CodexAppServerSession {
547554
exitCode: status === "completed" ? 0 : null,
548555
signal: null,
549556
finalMessage: final.text,
550-
stderr: redactSensitiveText(stderr.text() || error || ""),
557+
stderr: redactSensitiveText(stderr.text() || summaryErrorText(summary) || error || ""),
551558
stdoutTail: redactSensitiveText(stdout.text()),
552559
truncated: {
553560
stdoutChars: stdout.truncated(),
@@ -1007,7 +1014,7 @@ export class CodexAppServerSession {
10071014
exitCode: status === "completed" ? 0 : null,
10081015
signal: null,
10091016
finalMessage: final.text,
1010-
stderr: redactSensitiveText(active.stderr.text()),
1017+
stderr: redactSensitiveText(active.stderr.text() || summaryErrorText(active.summary)),
10111018
stdoutTail: redactSensitiveText(active.stdout.text()),
10121019
truncated: {
10131020
stdoutChars: active.stdout.truncated(),

0 commit comments

Comments
 (0)