diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs index 39b0d7fc1..1d5e3ff81 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs @@ -633,7 +633,15 @@ fn game_creator_codex_app_server_failed_turn_error( turn: &serde_json::Value, ) -> platform_llm::LlmError { let Some(error) = turn.get("error").filter(|error| !error.is_null()) else { - return game_creator_codex_app_server_error_kind("other"); + let status = turn + .get("status") + .and_then(serde_json::Value::as_str) + .unwrap_or("unknown"); + // app-server 有时只给 failed 状态、不带 error 对象。仍然保留协议事实,不能 + // 把“失败终态缺少 error 载荷”伪装成没有任何原因的 other。 + return platform_llm::LlmError::InvalidRequest(format!( + "{GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX}other detail=turn/completed status={status} error=missing" + )); }; if game_creator_codex_app_server_error_detail_indicates_insufficient_mud_points(error) { return platform_llm::LlmError::Upstream { @@ -7259,12 +7267,12 @@ mod tests { let actual = game_creator_codex_app_server_failed_turn_error(&turn).to_string(); assert!(actual.contains(expected_prefix), "{actual}"); } - assert_eq!( - game_creator_codex_app_server_failed_turn_error( - &serde_json::json!({"status":"failed","error":null}) - ), - platform_llm::LlmError::InvalidRequest("codex-app-server-error:other".to_string()) + let missing_error = game_creator_codex_app_server_failed_turn_error( + &serde_json::json!({"status":"failed","error":null}), ); + assert!(missing_error + .to_string() + .contains("status=failed error=missing")); } #[test] diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 0d65fe239..8f2a543b2 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -5,7 +5,7 @@ ## 2026-10-06 AGC 失败终态与 CC/MCP 边界不能吞掉协议、清理和 I/O 错误 - **现象**:DirectProject 的 `turn.completed(status=failed)` 若跨 IPC 丢失 `failure` 载荷,前端会只收口回合而不显示任何说明;Claude Code sidecar 的非 UTF-8 stderr、进程树清理失败、会话表锁损坏,以及 MCP loopback 服务退出、插件能力桥超时、损坏 manifest,会分别被压成空 detail、继续成功取消、伪造 hash projectId 或静默不广告插件。 -- **现行口径**:失败载荷缺失显示明确的协议错误;旧历史里的已知通用失败句明确标成“历史错误事件未记录具体错误正文”,不伪装成当前仍在吞错;CC 清理、stderr 解码、会话锁都保留原始正文;MCP server / capability bridge 失败写入统一应用日志,MCP manifest / journal / 时间 / metadata 读取失败返回具体错误;完成判定在把素材缺失归因于“未引用平台图”前先验证源码、manifest 和已登记图片的 I/O/PNG 错误;资源生成队列提交异常直接收口失败并恢复可重试草稿。切片文件是可选投影,完整 spritesheet 不因无切片而误失败。 +- **现行口径**:失败载荷缺失显示明确的协议错误;app-server 只给 `failed` 状态却不带 `error` 对象时,正文保留 `status=failed error=missing` 协议事实;旧历史里的已知通用失败句明确标成“历史错误事件未记录具体错误正文”,不伪装成当前仍在吞错;CC 清理、stderr 解码、会话锁都保留原始正文;MCP server / capability bridge 失败写入统一应用日志,MCP manifest / journal / 时间 / metadata 读取失败返回具体错误;完成判定在把素材缺失归因于“未引用平台图”前先验证源码、manifest 和已登记图片的 I/O/PNG 错误;资源生成队列提交异常直接收口失败并恢复可重试草稿。切片文件是可选投影,完整 spritesheet 不因无切片而误失败。 - **验证**:`cargo test agent::claude_code_cli::tests::` 14 passed;`cargo test agent::direct_tools_mcp::tests::` 34 passed;`cargo test agent::direct_runtime::tests::` 92 passed;`npx vitest run tests/directThreadChat.test.ts tests/directTurnFailure.test.ts` 64 passed;另有损坏 manifest 回归用例 1 passed。新失败正文仍经既有脱敏边界处理。 - **关联**:`agent/claude_code_cli.rs`、`agent/direct_tools_mcp.rs`、`agent/direct_runtime/mod.rs`、`agent/thread_manager/dispatch.rs`、`chat/conversation/directThreadChat.ts`。