修复 ccweb MCP 启动与重载并重新打包
This commit is contained in:
@@ -10,6 +10,12 @@ CLAUDE_PATH=claude
|
||||
# Codex CLI 路径(默认在 PATH 中查找 codex)
|
||||
CODEX_PATH=codex
|
||||
|
||||
# Codex App / ccweb MCP 启动与调用超时(可选)
|
||||
# CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC=30
|
||||
# CC_WEB_CODEX_MCP_TOOL_TIMEOUT_SEC=60
|
||||
# CC_WEB_CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS=35000
|
||||
# CC_WEB_CODEX_APP_MCP_RELOAD_TRACK_MS=60000
|
||||
|
||||
# PushPlus Token(可选,首次启动会自动迁移到 config/notify.json)
|
||||
PUSHPLUS_TOKEN=
|
||||
|
||||
|
||||
39
.planning/2026-09-12-ccweb-mcp-create-failure/findings.md
Normal file
39
.planning/2026-09-12-ccweb-mcp-create-failure/findings.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 调查发现
|
||||
|
||||
## 用户现象
|
||||
|
||||
- ccweb MCP 创建对话有时失败。
|
||||
- 创建失败后重载 MCP 也挂载不上,导致当前对话不可继续使用。
|
||||
- 截图中的 `$log-ccweb-title` 提示显示:当前会话没有提供 `ccweb_list_conversations`、`ccweb_set_title` 或 `wiznote_mcp_wiz_*` 工具,因此无法获取对话 ID,无法安全写入 `/coding` 日志。
|
||||
|
||||
## 代码与运行证据
|
||||
|
||||
### 1. MCP 冷启动窗口固定为 10 秒
|
||||
|
||||
- `server.js:3528-3572` 的 `buildCcwebMcpRuntimeConfig()` 在 streamable HTTP 和 stdio 两条路径都固定写入 `startup_timeout_sec: 10`,工具调用窗口为 60 秒。
|
||||
- 历史会话 `723ffd3f-71fc-42ee-87b6-768836316099`、`4c0f6be3-b46c-4ec0-b6f5-03c31188b7d8`、`5ed15712-312c-4d1b-b629-3d3a3c0d06a7` 均持久化了:`MCP client for ccweb timed out after 10 seconds`。
|
||||
- 同一批历史记录随后出现 `ccweb_list_conversations` 60 秒工具调用超时,说明客户端失败后仍可能继续尝试调用失效连接。
|
||||
|
||||
### 2. 创建对话成功不等于目标 MCP 已就绪
|
||||
|
||||
- `createMcpConversation()` 先通过 `createPersistentConversationSession()` 写入会话文件,再调用 `sendCrossConversationMessage()` 投递首条消息。
|
||||
- `sendCrossConversationMessage()` 调用 `handleMessage()`;Codex App 分支的 `handleCodexAppMessage()` 只登记 active turn 并异步执行 `startCodexAppTurn(...).catch(...)`,立即返回 `{ ok: true }`。
|
||||
- 因此创建接口返回的 `ok/status=running` 只表示“会话已落盘、首轮已开始”,不保证首轮完成,更不保证 ccweb MCP 已 ready。MCP 启动失败会在返回之后发生。
|
||||
|
||||
### 3. 重载状态关联存在 threadId 严格匹配竞态
|
||||
|
||||
- `handleReloadMcpApi()` 在 `markCodexAppMcpReloadPending()` 后调用无 thread 参数的 `config/mcpServer/reload`,并只等待 `CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS = 1200` 毫秒。
|
||||
- `codexAppMcpStatusTargetSessionIds()` 对 pending 会话要求 `statusRecord.threadId === pending.threadId`;不一致时直接跳过。
|
||||
- `logs/process.old.log:6713-6721` 中,重载请求针对 `019ff161-...`,随后上报的是 `019fef64-...` 与 `019fef0f-...`,且全部 `targetSessions=0`。对应会话 `b73e4b07-4aaa-43d5-906b-413747a09f4b` 最终仍持久化为 `ccweb.status=starting/rawStatus=pending`。
|
||||
- `cleanupExpiredCodexAppMcpReloads()` 只删除内存 pending 映射,不会把持久化状态从 `starting/pending` 改成失败或可重试,因此会话在 UI 上长期像“挂载中”。
|
||||
|
||||
### 4. 当前环境可复现“并发启动时序差异”
|
||||
|
||||
- 当前调查会话的 `ccweb` 最终为 `ready`,但同一线程的 `playwright` 在 20 秒后失败,说明多个 MCP 并发启动时不同服务的 ready/fail 到达时间并不一致;固定 10 秒窗口和 1.2 秒重载等待会放大这个时序问题。
|
||||
|
||||
## 已实施修复
|
||||
|
||||
- `server.js` 和 `lib/agent-runtime.js`:ccweb MCP 默认启动超时提高到 30 秒,工具超时和 reload 等待窗口支持环境变量覆盖;reload 默认等待 35 秒。
|
||||
- `server.js`:reload pending 记录允许同一 app-server 的全局 reload 通知跨 threadId 关联;等待窗口结束仍未收到 ready/failed/cancelled 时,将持久化状态收敛为 `failed` 并标记可重试。
|
||||
- `server.js`:Codex App 创建对话结果附带当前 `mcpStatus`;首条消息投递失败时保留会话 ID、失败阶段和可重试标记,避免“已创建的会话”被误认为完全不存在。
|
||||
- `scripts/mock-codex-app-server.js`、`scripts/regression.js`:新增无关 threadId 和无最终状态通知的回归场景。
|
||||
13
.planning/2026-09-12-ccweb-mcp-create-failure/progress.md
Normal file
13
.planning/2026-09-12-ccweb-mcp-create-failure/progress.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 调查进度
|
||||
|
||||
## 记录
|
||||
|
||||
- 2026-09-12:建立本轮 scoped 排查计划;确认 codebase-memory 项目 `home-cc-web` 索引状态为 ready。
|
||||
- 2026-09-12:截图现象先记录为“当前会话 MCP 工具未注入/不可见”,待与创建和重载链路对照。
|
||||
- 2026-09-12:确认 `buildCcwebMcpRuntimeConfig()` 两条传输路径均固定 `startup_timeout_sec=10`;历史会话持久化了 ccweb 10 秒超时。
|
||||
- 2026-09-12:确认 `createMcpConversation()` 的首条消息通过 `handleCodexAppMessage()` 异步启动,接口返回不等待目标线程/MCP ready。
|
||||
- 2026-09-12:确认 reload 仅等待 1200ms,状态通知按 threadId 严格关联;历史日志出现请求线程与上报线程不一致、`targetSessions=0`,导致会话持久化为 `starting/pending`。
|
||||
- 2026-09-12:用户确认进入修复实施;已将 ccweb MCP 启动超时、工具超时、reload 等待窗口改为可配置,默认分别为 30 秒、60 秒、35 秒。
|
||||
- 2026-09-12:已修正 reload 的跨 threadId 全局状态关联,并在等待窗口超时后持久化 `failed` 状态和可重试提示。
|
||||
- 2026-09-12:创建对话结果补充 Codex App MCP 状态;首条消息投递失败保留会话 ID、creationStatus 和 retryable 元数据。
|
||||
- 2026-09-12:mock + 完整 regression 已通过;未重启服务,待收尾清理临时清单。
|
||||
25
.planning/2026-09-12-ccweb-mcp-create-failure/task_plan.md
Normal file
25
.planning/2026-09-12-ccweb-mcp-create-failure/task_plan.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# ccweb MCP 创建对话失败与重载失败排查
|
||||
|
||||
## 目标
|
||||
|
||||
定位 ccweb MCP 创建新对话偶发失败、失败后重载 MCP 无法挂载的共同根因,判断是否需要修复代码并完成最小回归验证。
|
||||
|
||||
## 阶段
|
||||
|
||||
- [completed] 梳理现有 MCP 配置与重载回归入口
|
||||
- [completed] 提高并配置 ccweb MCP 启动与重载等待窗口
|
||||
- [completed] 修正重载状态的 threadId 关联与超时收敛
|
||||
- [completed] 补充创建对话的 MCP 状态可见性与失败信息
|
||||
- [completed] 补充回归断言并运行相关验证
|
||||
- [completed] 清理临时清单并汇总交付风险
|
||||
|
||||
## 约束
|
||||
|
||||
- 使用 codebase-memory-mcp 做代码定位,rg 只做行号与文本补充校验。
|
||||
- 不覆盖用户已有未提交改动。
|
||||
- 仅在确认根因且改动属于本问题范围时修改业务代码。
|
||||
|
||||
## 错误记录
|
||||
|
||||
| 错误 | 尝试 | 处理 |
|
||||
|---|---:|---|
|
||||
@@ -262,6 +262,10 @@ node server.js
|
||||
| `CC_WEB_LOGS_DIR` | `./logs` | 日志目录覆写,常用于测试隔离 |
|
||||
| `CC_WEB_CODEX_APP_WORKER` | 开启 | 设为 `0` / `false` / `off` 可关闭 Codex App worker |
|
||||
| `CC_WEB_PROCESS_CLEAN_PATH` | 自动探测 | 清理旧 Codex app-server 进程时使用的匹配路径覆写 |
|
||||
| `CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC` | `30` | Codex App MCP 启动等待秒数,范围 10-300 |
|
||||
| `CC_WEB_CODEX_MCP_TOOL_TIMEOUT_SEC` | `60` | ccweb MCP 工具调用超时秒数,范围 10-600 |
|
||||
| `CC_WEB_CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS` | `35000` | 重载 MCP 等待最终状态的毫秒数,范围 1000-180000 |
|
||||
| `CC_WEB_CODEX_APP_MCP_RELOAD_TRACK_MS` | `60000` 或等待窗口+10秒的较大值 | 重载状态关联保留时间,范围 5000-300000 |
|
||||
|
||||
还有若干面向大历史、长输出和 Codex App 状态落盘的高级限制参数,例如:
|
||||
|
||||
|
||||
Binary file not shown.
@@ -37,6 +37,16 @@ function createAgentRuntime(deps) {
|
||||
256 * 1024,
|
||||
{ min: 4096 },
|
||||
);
|
||||
const CCWEB_MCP_STARTUP_TIMEOUT_SEC = readRuntimePositiveIntEnv(
|
||||
'CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC',
|
||||
30,
|
||||
{ min: 10, max: 300 },
|
||||
);
|
||||
const CCWEB_MCP_TOOL_TIMEOUT_SEC = readRuntimePositiveIntEnv(
|
||||
'CC_WEB_CODEX_MCP_TOOL_TIMEOUT_SEC',
|
||||
60,
|
||||
{ min: 10, max: 600 },
|
||||
);
|
||||
const RUNTIME_TRUNCATED_HEAD = '[cc-web: 前文过长,已保留尾部以保护服务稳定性]\n';
|
||||
|
||||
function keepTail(value, maxLen) {
|
||||
@@ -99,8 +109,8 @@ function createAgentRuntime(deps) {
|
||||
'-c', `mcp_servers.ccweb.command=${tomlString(nodePath || 'node')}`,
|
||||
'-c', `mcp_servers.ccweb.args=${tomlStringArray(serverArgs)}`,
|
||||
'-c', `mcp_servers.ccweb.env_vars=${tomlStringArray(envVars)}`,
|
||||
'-c', 'mcp_servers.ccweb.startup_timeout_sec=10',
|
||||
'-c', 'mcp_servers.ccweb.tool_timeout_sec=60'
|
||||
'-c', `mcp_servers.ccweb.startup_timeout_sec=${CCWEB_MCP_STARTUP_TIMEOUT_SEC}`,
|
||||
'-c', `mcp_servers.ccweb.tool_timeout_sec=${CCWEB_MCP_TOOL_TIMEOUT_SEC}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1133,24 +1133,29 @@ function handleRequest(message) {
|
||||
}
|
||||
if (method === 'config/mcpServer/reload') {
|
||||
mcpReloadCount += 1;
|
||||
send({
|
||||
method: 'mcpServer/startupStatus/updated',
|
||||
params: {
|
||||
server: 'ccweb',
|
||||
state: 'starting',
|
||||
message: 'ccweb MCP starting',
|
||||
threadId: null,
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: 'mcpServer/startupStatus/updated',
|
||||
params: {
|
||||
name: 'ccweb',
|
||||
status: 'ready',
|
||||
message: 'ccweb MCP ready CC_WEB_MCP_TOKEN=mock-secret-token',
|
||||
threadId: null,
|
||||
},
|
||||
});
|
||||
const suppressReloadStatus = process.env.MOCK_CODEX_APP_MCP_RELOAD_TIMEOUT_ON_SECOND === '1'
|
||||
&& mcpReloadCount >= 2;
|
||||
if (!suppressReloadStatus) {
|
||||
send({
|
||||
method: 'mcpServer/startupStatus/updated',
|
||||
params: {
|
||||
server: 'ccweb',
|
||||
state: 'starting',
|
||||
message: 'ccweb MCP starting',
|
||||
// 模拟 app-server 的全局 reload 通知可能携带其他线程 ID。
|
||||
threadId: 'mock-reload-unrelated-thread',
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: 'mcpServer/startupStatus/updated',
|
||||
params: {
|
||||
name: 'ccweb',
|
||||
status: 'ready',
|
||||
message: 'ccweb MCP ready CC_WEB_MCP_TOKEN=mock-secret-token',
|
||||
threadId: 'mock-reload-unrelated-thread',
|
||||
},
|
||||
});
|
||||
}
|
||||
send({ id, result: { reloaded: true, reloadCount: mcpReloadCount } });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1613,6 +1613,32 @@ function assertFrontendMcpReloadContract() {
|
||||
assert(source.includes('MCP 启动失败'), 'Frontend should expose a failed startup toast');
|
||||
}
|
||||
|
||||
function assertCcwebMcpRecoveryContract() {
|
||||
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
|
||||
const runtimeSource = fs.readFileSync(path.join(REPO_DIR, 'lib', 'agent-runtime.js'), 'utf8');
|
||||
const mockSource = fs.readFileSync(MOCK_CODEX_APP_SERVER, 'utf8');
|
||||
assert(
|
||||
serverSource.includes('CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC')
|
||||
&& serverSource.includes('CODEX_APP_MCP_STARTUP_TIMEOUT_SEC,')
|
||||
&& serverSource.includes('CC_WEB_CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS')
|
||||
&& serverSource.includes('CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS,')
|
||||
&& serverSource.includes('allowThreadMismatch: true'),
|
||||
'ccweb MCP recovery should expose configurable startup/reload windows and tolerate global reload thread IDs'
|
||||
);
|
||||
assert(
|
||||
serverSource.includes('codex_app_mcp_reload_timeout')
|
||||
&& serverSource.includes("creationStatus: 'created_but_initial_message_failed'")
|
||||
&& serverSource.includes('mcpStatus: buildCodexAppMcpStatusSummary'),
|
||||
'ccweb MCP recovery should persist timeout failure and expose creation/MCP status'
|
||||
);
|
||||
assert(
|
||||
runtimeSource.includes('CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC')
|
||||
&& runtimeSource.includes('mcp_servers.ccweb.startup_timeout_sec=${CCWEB_MCP_STARTUP_TIMEOUT_SEC}'),
|
||||
'Legacy Codex runtime should use the same configurable ccweb MCP startup timeout'
|
||||
);
|
||||
assert(mockSource.includes('mock-reload-unrelated-thread'), 'MCP reload regression fixture should cover unrelated notification thread ids');
|
||||
}
|
||||
|
||||
function assertFrontendSubagentCardMetadataContract() {
|
||||
const source = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||
const styleSource = fs.readFileSync(PUBLIC_STYLE_PATH, 'utf8');
|
||||
@@ -6637,6 +6663,7 @@ async function main() {
|
||||
assertFrontendMarkdownLinkContract();
|
||||
assertMockCodexAppPromptUserNotTextTriggered();
|
||||
assertFrontendMcpReloadContract();
|
||||
assertCcwebMcpRecoveryContract();
|
||||
assertPlanListProgressContract();
|
||||
assertFrontendSubagentCardMetadataContract();
|
||||
assertCodexAppRuntimeSubAgentActivityContract();
|
||||
@@ -6793,6 +6820,8 @@ async function main() {
|
||||
CLAUDE_PATH: MOCK_CLAUDE,
|
||||
CODEX_PATH: MOCK_CODEX_APP_SERVER,
|
||||
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',
|
||||
}, async () => {
|
||||
await assertWsUpgradeRejected(port, '/not-ws');
|
||||
|
||||
@@ -8212,6 +8241,11 @@ async function main() {
|
||||
Number(reloadAfterTracking.result?.reloadCount || 0) === baselineMcpReloadCount + 1,
|
||||
'Task tracking changes must not trigger an implicit MCP reload; only the explicit reload request should count'
|
||||
);
|
||||
assert(reloadAfterTracking.reloadTimedOut === true, 'MCP reload without a final startup notification should report a timeout');
|
||||
assert(reloadAfterTracking.mcpStatus?.status === 'failed', 'MCP reload timeout should converge to failed instead of pending');
|
||||
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
|
||||
assert(storedCodexApp.codexAppMcpStartupStatus?.servers?.ccweb?.status === 'failed', 'MCP reload timeout should persist failed status for retry');
|
||||
assert(/可稍后再次重试/.test(storedCodexApp.codexAppMcpStartupStatus?.servers?.ccweb?.message || ''), 'MCP reload timeout should explain that retry is available');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-codexapp-prompt-user-mcp', trigger: '/', query: 'prompt_user', sessionId: codexAppSession.sessionId, agent: 'codexapp' }));
|
||||
const codexAppPromptUserMcpComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-prompt-user-mcp');
|
||||
|
||||
69
server.js
69
server.js
@@ -1444,8 +1444,28 @@ const CODEX_APP_UNROUTED_NOTIFICATION_LOG_THROTTLE_MS = 30 * 1000;
|
||||
const CODEX_APP_UNROUTED_NOTIFICATION_LOG_MAX = 1000;
|
||||
const CODEX_APP_MCP_STARTUP_STATUS_METHOD = 'mcpServer/startupStatus/updated';
|
||||
const CODEX_APP_MCP_DEFAULT_SERVER = 'ccweb';
|
||||
const CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS = 1200;
|
||||
const CODEX_APP_MCP_RELOAD_TRACK_MS = 15000;
|
||||
// Codex App 会并发启动多个 MCP;10 秒和 1.2 秒都不足以覆盖冷启动抖动。
|
||||
// 保留环境变量覆盖,便于低配机器或 CI 按需调整,而不必重新发布代码。
|
||||
const CODEX_APP_MCP_STARTUP_TIMEOUT_SEC = readPositiveIntEnv(
|
||||
'CC_WEB_CODEX_APP_MCP_STARTUP_TIMEOUT_SEC',
|
||||
30,
|
||||
{ min: 10, max: 300 },
|
||||
);
|
||||
const CODEX_APP_MCP_TOOL_TIMEOUT_SEC = readPositiveIntEnv(
|
||||
'CC_WEB_CODEX_MCP_TOOL_TIMEOUT_SEC',
|
||||
60,
|
||||
{ min: 10, max: 600 },
|
||||
);
|
||||
const CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS = readPositiveIntEnv(
|
||||
'CC_WEB_CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS',
|
||||
35000,
|
||||
{ min: 1000, max: 180000 },
|
||||
);
|
||||
const CODEX_APP_MCP_RELOAD_TRACK_MS = readPositiveIntEnv(
|
||||
'CC_WEB_CODEX_APP_MCP_RELOAD_TRACK_MS',
|
||||
Math.max(60000, CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS + 10000),
|
||||
{ min: 5000, max: 300000 },
|
||||
);
|
||||
const CODEX_APP_MCP_INVENTORY_TTL_MS = 3000;
|
||||
const CODEX_APP_MCP_INVENTORY_MAX_PAGES = 10;
|
||||
const codexAppMcpStartupStatusByServer = new Map();
|
||||
@@ -3547,8 +3567,8 @@ function buildCcwebMcpRuntimeConfig(session, options = {}) {
|
||||
type: 'streamable_http',
|
||||
url: `http://127.0.0.1:${PORT}/api/internal/mcp/stream?${params.toString()}`,
|
||||
bearer_token_env_var: CODEX_APP_CCWEB_MCP_BEARER_TOKEN_ENV,
|
||||
startup_timeout_sec: 10,
|
||||
tool_timeout_sec: 60,
|
||||
startup_timeout_sec: CODEX_APP_MCP_STARTUP_TIMEOUT_SEC,
|
||||
tool_timeout_sec: CODEX_APP_MCP_TOOL_TIMEOUT_SEC,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3565,8 +3585,8 @@ function buildCcwebMcpRuntimeConfig(session, options = {}) {
|
||||
command: commandSpec.command,
|
||||
args: commandSpec.args,
|
||||
env,
|
||||
startup_timeout_sec: 10,
|
||||
tool_timeout_sec: 60,
|
||||
startup_timeout_sec: CODEX_APP_MCP_STARTUP_TIMEOUT_SEC,
|
||||
tool_timeout_sec: CODEX_APP_MCP_TOOL_TIMEOUT_SEC,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4600,6 +4620,9 @@ function markCodexAppMcpReloadPending(session, sessionId) {
|
||||
requestedAt,
|
||||
expiresAt: Date.now() + CODEX_APP_MCP_RELOAD_TRACK_MS,
|
||||
reloadRequestId,
|
||||
// config/mcpServer/reload 作用于整个 app-server,不保证通知携带当前线程。
|
||||
// 因此 reload 窗口内允许用同一 app-server 的其他 threadId 状态完成关联。
|
||||
allowThreadMismatch: true,
|
||||
});
|
||||
saveSession(session);
|
||||
return {
|
||||
@@ -4714,7 +4737,12 @@ function codexAppMcpStatusTargetSessionIds(statusRecord, routed) {
|
||||
const targetSessionIds = new Set();
|
||||
if (routed?.sessionId) targetSessionIds.add(routed.sessionId);
|
||||
for (const [sessionId, pending] of pendingCodexAppMcpReloads.entries()) {
|
||||
if (statusRecord.threadId && pending.threadId && statusRecord.threadId !== pending.threadId) continue;
|
||||
if (
|
||||
statusRecord.threadId
|
||||
&& pending.threadId
|
||||
&& statusRecord.threadId !== pending.threadId
|
||||
&& pending.allowThreadMismatch !== true
|
||||
) continue;
|
||||
targetSessionIds.add(sessionId);
|
||||
}
|
||||
return targetSessionIds;
|
||||
@@ -4785,12 +4813,26 @@ async function handleReloadMcpApi(req, res, rawSessionId) {
|
||||
const result = typeof client.reloadMcpServers === 'function'
|
||||
? await client.reloadMcpServers()
|
||||
: await client.request('config/mcpServer/reload', {}, 30000);
|
||||
const mcpStatus = await waitForCodexAppMcpStatusAfterReload(sessionId, reloadRequestedAt);
|
||||
let mcpStatus = await waitForCodexAppMcpStatusAfterReload(sessionId, reloadRequestedAt);
|
||||
let reloadTimedOut = false;
|
||||
if (!isFinalCodexAppMcpStatus(mcpStatus?.status)) {
|
||||
reloadTimedOut = true;
|
||||
const timeoutMessage = `重载 MCP 在 ${Math.ceil(CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS / 1000)} 秒内未收到最终启动状态,可稍后再次重试。`;
|
||||
mcpStatus = markCodexAppMcpReloadFailed(sessionId, timeoutMessage)
|
||||
|| mcpStatus
|
||||
|| pendingMcp.summary;
|
||||
plog('WARN', 'codex_app_mcp_reload_timeout', {
|
||||
sessionId: sessionId.slice(0, 8),
|
||||
threadId: getRuntimeSessionId(session) || null,
|
||||
waitMs: CODEX_APP_MCP_RELOAD_STATUS_WAIT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
plog('INFO', 'codex_app_mcp_reload_requested', {
|
||||
sessionId: sessionId.slice(0, 8),
|
||||
threadId: getRuntimeSessionId(session) || null,
|
||||
status: mcpStatus?.status || pendingMcp.summary?.status || 'pending',
|
||||
reloadTimedOut,
|
||||
});
|
||||
|
||||
return jsonResponse(res, 200, {
|
||||
@@ -4798,6 +4840,7 @@ async function handleReloadMcpApi(req, res, rawSessionId) {
|
||||
sessionId,
|
||||
threadId: getRuntimeSessionId(session) || null,
|
||||
result: result || {},
|
||||
reloadTimedOut,
|
||||
mcpStatus: mcpStatus || pendingMcp.summary || buildCodexAppMcpStatusSummary(loadSession(sessionId), { reloadRequestedAt }),
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -7061,6 +7104,7 @@ function createMcpConversation(args = {}, sourceSessionId = '', sourceHopCount =
|
||||
content: initialMessage,
|
||||
}, sourceId, normalizedHopCount, { expectReply: requestReply });
|
||||
if (!delivery?.ok) {
|
||||
const persistedSession = loadSession(session.id) || session;
|
||||
return mcpToolError(delivery?.code || 'initial_message_failed', delivery?.message || '创建对话后发送首条消息失败。', {
|
||||
conversationId: session.id,
|
||||
title: session.title,
|
||||
@@ -7068,10 +7112,16 @@ function createMcpConversation(args = {}, sourceSessionId = '', sourceHopCount =
|
||||
cwd: session.cwd || '',
|
||||
mode: session.permissionMode || 'yolo',
|
||||
status: isSessionRunning(session.id) ? 'running' : 'idle',
|
||||
creationStatus: 'created_but_initial_message_failed',
|
||||
retryable: true,
|
||||
...(isCodexAppSession(persistedSession)
|
||||
? { mcpStatus: buildCodexAppMcpStatusSummary(persistedSession) }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const persistedSession = loadSession(session.id) || session;
|
||||
broadcastSessionList();
|
||||
return {
|
||||
ok: true,
|
||||
@@ -7082,6 +7132,9 @@ function createMcpConversation(args = {}, sourceSessionId = '', sourceHopCount =
|
||||
mode: session.permissionMode || 'yolo',
|
||||
status: isSessionRunning(session.id) ? 'running' : 'idle',
|
||||
sourceConversationId: sourceId || null,
|
||||
...(isCodexAppSession(persistedSession)
|
||||
? { mcpStatus: buildCodexAppMcpStatusSummary(persistedSession) }
|
||||
: {}),
|
||||
...(delivery ? {
|
||||
messageId: delivery.messageId || null,
|
||||
deliveryStatus: delivery.deliveryStatus || 'delivered',
|
||||
|
||||
Reference in New Issue
Block a user