修复 Codex App MCP 重载与线程库存恢复
This commit is contained in:
@@ -1170,6 +1170,9 @@ function handleRequest(message) {
|
||||
}
|
||||
if (method === 'mcpServerStatus/list') {
|
||||
const thread = ensureThread(params.threadId, params);
|
||||
const projectMcpConfigured = Boolean(thread.config?.['mcp_servers.reg-app-project'])
|
||||
|| thread.staleProjectMcpInventoryWarmup === true;
|
||||
if (thread.staleProjectMcpInventoryWarmup === true) thread.staleProjectMcpInventoryWarmup = false;
|
||||
send({
|
||||
id,
|
||||
result: {
|
||||
@@ -1188,7 +1191,7 @@ function handleRequest(message) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...(projectMcpConfigured ? [{
|
||||
name: 'reg-app-project',
|
||||
authStatus: 'unsupported',
|
||||
resources: [],
|
||||
@@ -1201,7 +1204,7 @@ function handleRequest(message) {
|
||||
inputSchema: { type: 'object' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}] : []),
|
||||
{
|
||||
name: 'reg-runtime-only',
|
||||
authStatus: 'unsupported',
|
||||
@@ -1218,13 +1221,19 @@ function handleRequest(message) {
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
threadId: thread.id,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (method === 'thread/start') {
|
||||
const thread = ensureThread(null, params);
|
||||
if (process.env.MOCK_CODEX_APP_STALE_PROJECT_MCP === '1'
|
||||
&& String(params.cwd || '').includes('codexapp-stale-mcp')
|
||||
&& params.config?.['mcp_servers.reg-app-project']) {
|
||||
thread.staleProjectMcpConfig = params.config['mcp_servers.reg-app-project'];
|
||||
thread.staleProjectMcpInventoryWarmup = true;
|
||||
delete thread.config['mcp_servers.reg-app-project'];
|
||||
}
|
||||
thread.lastThreadConfigMethod = 'thread/start';
|
||||
send({ id, result: { thread: threadPayload(thread), model: params.model || 'gpt-5.5', cwd: thread.cwd, modelProvider: 'mock', approvalPolicy: params.approvalPolicy || 'never', approvalsReviewer: 'user', sandbox: params.sandbox || 'danger-full-access' } });
|
||||
return;
|
||||
|
||||
@@ -1635,6 +1635,9 @@ function assertCcwebMcpRecoveryContract() {
|
||||
const readinessSource = extractFunctionSource(serverSource, 'waitForCodexAppMcpReadyStatus');
|
||||
const preflightSource = extractFunctionSource(serverSource, 'ensureCodexAppMcpReadyForThread');
|
||||
const inventorySource = extractFunctionSource(serverSource, 'loadCodexAppMcpInventory');
|
||||
const reloadStart = serverSource.indexOf('async function handleReloadMcpApi');
|
||||
const reloadEnd = serverSource.indexOf('\nfunction setRuntimeSessionId', reloadStart);
|
||||
const reloadSource = reloadStart >= 0 && reloadEnd > reloadStart ? serverSource.slice(reloadStart, reloadEnd) : '';
|
||||
assert(
|
||||
serverSource.includes('CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC')
|
||||
&& serverSource.includes('CODEX_APP_MCP_STARTUP_TIMEOUT_SEC,')
|
||||
@@ -1645,11 +1648,12 @@ function assertCcwebMcpRecoveryContract() {
|
||||
&& startTurnSource.includes('ensureCodexAppMcpReadyForThread')
|
||||
&& readinessSource.includes("status === 'ready'")
|
||||
&& statusSource.includes('statusThreadId !== normalizedThreadId')
|
||||
&& preflightSource.includes('waitForCodexAppMcpReadyStatus')
|
||||
&& !preflightSource.includes('waitForCodexAppMcpInventory')
|
||||
&& preflightSource.includes('waitForCodexAppMcpInventory')
|
||||
&& preflightSource.includes('requireServers')
|
||||
&& inventorySource.includes('return []')
|
||||
&& reloadSource.includes('refreshCodexAppThreadMcpConfig')
|
||||
&& !serverSource.includes('allowThreadMismatch: true'),
|
||||
'ccweb MCP recovery should gate turns on current-thread startup status and degrade inventory lookup safely'
|
||||
'ccweb MCP recovery should gate turns on current-thread inventory and rebind the original thread before reload'
|
||||
);
|
||||
assert(
|
||||
serverSource.includes('codex_app_mcp_reload_timeout')
|
||||
@@ -1667,7 +1671,8 @@ function assertCcwebMcpRecoveryContract() {
|
||||
mockSource.includes('mock-reload-unrelated-thread')
|
||||
&& mockSource.includes('without thread id')
|
||||
&& mockSource.includes("name: 'ccweb'")
|
||||
&& mockSource.includes('ccweb_list_conversations'),
|
||||
&& mockSource.includes('ccweb_list_conversations')
|
||||
&& mockSource.includes('projectMcpConfigured'),
|
||||
'MCP reload regression fixture should cover unrelated notifications and current-thread tool inventory'
|
||||
);
|
||||
}
|
||||
@@ -1678,9 +1683,11 @@ function assertCodexAppMcpReadinessContract() {
|
||||
const statusSource = extractFunctionSource(source, 'codexAppMcpStatusForThread');
|
||||
const readinessSource = extractFunctionSource(source, 'waitForCodexAppMcpReadyStatus');
|
||||
const inventorySource = extractFunctionSource(source, 'loadCodexAppMcpInventory');
|
||||
assert(readinessSource.includes("status === 'ready'"), 'Codex App first turn should wait for current-thread ccweb readiness');
|
||||
const preflightSource = extractFunctionSource(source, 'ensureCodexAppMcpReadyForThread');
|
||||
assert(readinessSource.includes("status === 'ready'"), 'Codex App readiness should retain current-thread startup status handling');
|
||||
assert(readinessSource.includes("status === 'failed'") && readinessSource.includes("status === 'cancelled'"), 'Codex App readiness should surface terminal MCP startup failures');
|
||||
assert(statusSource.includes('statusThreadId !== normalizedThreadId'), 'Codex App readiness must reject stale status from another thread');
|
||||
assert(preflightSource.includes('waitForCodexAppMcpInventory') && preflightSource.includes('requireServers'), 'Codex App first turn should verify the configured server inventory before sending a turn');
|
||||
assert(inventorySource.includes('return []'), 'Composer MCP inventory failure should degrade to local suggestions');
|
||||
assert(workerClientSource.includes("sendWorker('start', {}, 120000)"), 'Codex App worker start should cover initialize and best-effort capability probes');
|
||||
}
|
||||
@@ -7374,6 +7381,7 @@ async function main() {
|
||||
CC_WEB_CODEX_TRANSIENT_RETRY_BASE_DELAY_MS: '100',
|
||||
CC_WEB_CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS: '1000',
|
||||
MOCK_CODEX_APP_MCP_RELOAD_TIMEOUT_ON_SECOND: '1',
|
||||
MOCK_CODEX_APP_STALE_PROJECT_MCP: '1',
|
||||
}, async () => {
|
||||
await assertWsUpgradeRejected(port, '/not-ws');
|
||||
|
||||
@@ -8320,6 +8328,34 @@ async function main() {
|
||||
const codexAppSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codexapp' && msg.cwd === codexAppCwd);
|
||||
assert(codexAppSession.model === 'gpt-5.5(max)', 'Codex App new_session should preserve the max default Codex model');
|
||||
|
||||
const staleMcpCwd = path.join(tempRoot, 'codexapp-stale-mcp');
|
||||
mkdirp(path.join(staleMcpCwd, '.codex'));
|
||||
fs.writeFileSync(path.join(staleMcpCwd, '.codex', 'config.toml'), [
|
||||
'[mcp_servers.reg-app-project]',
|
||||
'type = "stdio"',
|
||||
`command = ${JSON.stringify(process.execPath)}`,
|
||||
'args = ["regression-app-mcp.js"]',
|
||||
'enabled = true',
|
||||
].join('\n'));
|
||||
ws.send(JSON.stringify({ type: 'new_session', agent: 'codexapp', cwd: staleMcpCwd, mode: 'yolo' }));
|
||||
const staleMcpSession = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_info' && msg.agent === 'codexapp' && msg.cwd === staleMcpCwd
|
||||
));
|
||||
ws.send(JSON.stringify({ type: 'message', text: 'stale mcp dynamic inventory probe', sessionId: staleMcpSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||
const staleMcpTool = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'tool_end' && msg.sessionId === staleMcpSession.sessionId && msg.toolUseId === 'mcp-ccweb-list'
|
||||
));
|
||||
assert(/"hasProjectMcpConfig": false/.test(staleMcpTool.result || ''), 'Stale MCP fixture should reproduce an original thread missing its project MCP config');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === staleMcpSession.sessionId);
|
||||
const staleMcpReload = await postAuthedJson(port, token, `/api/sessions/${staleMcpSession.sessionId}/reload-mcp`);
|
||||
assert(staleMcpReload.ok === true, `MCP reload should restore the stale original thread: ${JSON.stringify(staleMcpReload)}`);
|
||||
assert(staleMcpReload.mcpStatus?.inventoryStatus === 'ready', `MCP reload should verify the restored thread inventory: ${JSON.stringify(staleMcpReload)}`);
|
||||
assert(staleMcpReload.mcpStatus?.inventoryServers?.some((server) => (
|
||||
server.server === 'reg-app-project'
|
||||
&& Array.isArray(server.tools)
|
||||
&& server.tools.some((tool) => tool.name === 'reg_app_inspect')
|
||||
)), `MCP reload inventory should contain the project MCP restored by thread resume: ${JSON.stringify(staleMcpReload.mcpStatus)}`);
|
||||
|
||||
const codexAppGoalTitleCwd = path.join(tempRoot, 'codexapp-goal-title');
|
||||
mkdirp(codexAppGoalTitleCwd);
|
||||
ws.send(JSON.stringify({ type: 'new_session', agent: 'codexapp', cwd: codexAppGoalTitleCwd, mode: 'yolo' }));
|
||||
|
||||
Reference in New Issue
Block a user