From f1523e12c42c8ee296b9f71470d5bd2246ed73df Mon Sep 17 00:00:00 2001 From: msgaxzzz Date: Wed, 29 Apr 2026 19:12:21 +0000 Subject: [PATCH] fix(codex): recover from stale official OAuth session context --- scripts/mock-codex.js | 10 ++++++++++ scripts/regression.js | 17 +++++++++++++++++ server.js | 18 ++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/scripts/mock-codex.js b/scripts/mock-codex.js index d136b6e..11f8d1e 100755 --- a/scripts/mock-codex.js +++ b/scripts/mock-codex.js @@ -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 diff --git a/scripts/regression.js b/scripts/regression.js index 438741c..95ab1da 100644 --- a/scripts/regression.js +++ b/scripts/regression.js @@ -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', diff --git a/server.js b/server.js index 2be8f3b..db3baef 100644 --- a/server.js +++ b/server.js @@ -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 模板。'; } @@ -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 压缩上下文,请稍候…' @@ -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;