Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions scripts/mock-codex.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ function readStdin() {
process.exit(1);
}

if (input === 'trigger invalid encrypted content') {
process.stdout.write(`${JSON.stringify({
type: 'turn.failed',
error: {
message: '{"type":"error","error":{"code":"invalid_encrypted_content","message":"The encrypted content QVhO... could not be verified."},"status":400}',
},
})}\n`);
process.exit(1);
}

const responseText = input === '/compact'
? 'Codex compact finished.'
: isInitPrompt
Expand Down
17 changes: 17 additions & 0 deletions scripts/regression.js
Original file line number Diff line number Diff line change
Expand Up @@ -479,8 +479,25 @@ async function main() {
), 20000);
if (autoCompactRetry.type === 'text_delta') {
assert(/trigger codex context limit/.test(autoCompactRetry.text || ''), 'Codex auto /compact should replay the failed prompt after compact');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === autoCompactSession.sessionId);
}

const invalidEncryptedCwd = path.join(tempRoot, 'codex-invalid-encrypted');
mkdirp(invalidEncryptedCwd);
ws.send(JSON.stringify({ type: 'new_session', agent: 'codex', cwd: invalidEncryptedCwd, mode: 'yolo' }));
const invalidEncryptedSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codex' && msg.cwd === invalidEncryptedCwd);
ws.send(JSON.stringify({ type: 'message', text: 'warm up invalid encrypted', sessionId: invalidEncryptedSession.sessionId, mode: 'yolo', agent: 'codex' }));
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === invalidEncryptedSession.sessionId);
const invalidEncryptedSessionPath = path.join(sessionsDir, `${invalidEncryptedSession.sessionId}.json`);
const storedBeforeInvalidEncrypted = JSON.parse(fs.readFileSync(invalidEncryptedSessionPath, 'utf8'));
assert(storedBeforeInvalidEncrypted.codexThreadId, 'Codex invalid encrypted fixture should start with a persisted thread id');
ws.send(JSON.stringify({ type: 'message', text: 'trigger invalid encrypted content', sessionId: invalidEncryptedSession.sessionId, mode: 'yolo', agent: 'codex' }));
const invalidEncryptedError = await nextMessage(messages, ws, (msg) => msg.type === 'error' && /加密上下文/.test(msg.message || ''));
assert(/重新发送/.test(invalidEncryptedError.message || ''), 'Codex invalid encrypted error should explain retry after reset');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === invalidEncryptedSession.sessionId);
const storedAfterInvalidEncrypted = JSON.parse(fs.readFileSync(invalidEncryptedSessionPath, 'utf8'));
assert(!storedAfterInvalidEncrypted.codexThreadId, 'Codex invalid encrypted error should clear stale thread id');

const claudeAttachment = await uploadAttachment(port, token, {
filename: 'claude-test.png',
mime: 'image/png',
Expand Down
18 changes: 18 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,9 @@ function formatRuntimeError(agent, raw, context = {}) {
}

if (agent === 'codex') {
if (/invalid_encrypted_content|encrypted content.*could not be/i.test(condensed)) {
return 'Codex 官方接口无法恢复这个旧会话的加密上下文。通常是该会话曾使用过不同的 provider、账号或登录态。已重置 Codex 上下文绑定,请重新发送一次消息以建立新的官方会话。';
}
if (/stream disconnected before completion|stream closed before response\.completed|response\.completed/i.test(condensed)) {
return 'Codex 上游响应流提前中断:当前自定义 API 的 Responses 流式协议没有完整发送 response.completed。请检查该 API 端点是否完整兼容 OpenAI Responses SSE,或切回确认兼容的 API 模板。';
}
Expand Down Expand Up @@ -1480,6 +1483,10 @@ function formatRuntimeError(agent, raw, context = {}) {
return `Claude 任务失败${exitInfo}:${condensed}`;
}

function isCodexEncryptedContentError(raw) {
return /invalid_encrypted_content|encrypted content.*could not be/i.test(String(raw || ''));
}

function compactStartMessage(agent) {
return agent === 'codex'
? '正在执行 Codex /compact 压缩上下文,请稍候…'
Expand Down Expand Up @@ -1619,6 +1626,17 @@ function handleProcessComplete(sessionId, exitCode, signal) {
saveSession(session);
}

if (session && entry.agent === 'codex' && rawCompletionError && isCodexEncryptedContentError(rawCompletionError)) {
clearRuntimeSessionId(session);
session.codexRuntimeKey = entry.codexRuntimeKey || session.codexRuntimeKey || '';
session.updated = new Date().toISOString();
saveSession(session);
plog('WARN', 'codex_encrypted_context_reset', {
sessionId: sessionId.slice(0, 8),
codexRuntimeKey: session.codexRuntimeKey || null,
});
}

let shouldReturnForFollowup = false;
let shouldAutoCompact = false;

Expand Down