Refactor/整理项目对话错误相关代码 #593
Reference in New Issue
Block a user
Delete Branch "refactor/turn-error-to-explicit-struct"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
- 三条错误表去冗余前缀:DirectTurnError→TurnError、DirectTurnEnqueueError→EnqueueError、DirectTurnFailure→TurnFailure、DirectTurnDeadline→Deadline、DirectCodexFailureStage→FailureStage、DirectCodexNativeKind→NativeKind、DirectModelCallKind→ModelCallKind - 用户条目去前缀:DirectCodexUserItem→UserItem 等六个类型;agent/direct_turn_error.rs 移到 codex_app_server/turn_error.rs - thread_manager/wire.rs 拆为 wire/{mod,clock,items,turn,failure,tests}.rs;direct_turn_failure.rs 的失败载荷并入 wire/failure.rs - 宿主侧回合终态从 struct TurnTerminal 改为 enum TurnCompletion{Completed,Interrupted,Aborted,Failed},从 wire 移到 thread_manager/turn_completion.rs,status 由变体反推 - 过期回合释放兜底改走 TurnCompletion::Aborted.event - 同步生成绑定、前端调用点与测试;修正终态单测里过时的 TransportClosed 断言 - 对 src-tauri 全量 cargo fmt,满足 pre-commit 的 rustfmt 门禁- items.rs:item_text 先判空/全空白再回落 content/summary;此前 Some("") 会短路兜底,末尾非空过滤把整条 message/reasoning 丢掉 - wire/tests.rs:补空 text + 有 content 的回归用例- codex_app_server/mod.rs:Self::Failed(error) 用 write!(formatter, "{error}") 直接走 Display,不再先 error.to_string() 再 write_str1.
resourceReferences.ts残留DirectCodex函数名 —— 已修(cb13ecf87)。当前实现:
chatComposerDraftToDirectCodexUserItem/directCodexUserItemFromContent已改名为chatComposerDraftToUserItem/userItemFromContent,调用方同步。说明:文件里
DirectCodexLegacyContentDto、hasMeaningfulDirectCodexContent描述的是 legacy content 形态,不属于这次改名范围。2.
wire/turn.rs的status是自由字符串 —— 已实现(40b79bb1e)。当前实现:字段类型改为
TurnCompletedStatus(completed/interrupted/aborted/failed,serde 序列化到同样的 JSON 字符串);turn_completed的参数类型是只有三档的TurnOutcome,函数内部Into成字段类型。解决了什么:
turn_completed("failed".into(), at)这种"failed 但没有失败载荷"在构造层已经写不出来;turn_completed_failed固定写Failed+ 载荷。线上形状没变:JSON 仍是
{"type":"turn.completed","status":"completed",...};TS 侧status从string收成'completed' | 'interrupted' | 'aborted' | 'failed'(新增TurnCompletedStatus.ts),前端 reducer 只读failure,不受影响。仍留的口子(评审提过、这次没动):
status: "failed"但没有failure的 JSON 仍能反序列化(wire/tests.rs 有用例专门盯这个宽松行为,是给重放兼容留的);把failure并进Failed变体做成"内部 tagged 的扁平枚举"能彻底关掉,但会改前端判别联合的形状,需要你点头再动。3.
wire/items.rs:426-432等上游字符串绕过限长/脱敏 —— 已修(fc10b2359)。当前实现:
FunctionCall.name→detail_text(脱敏 + 4000 字);McpToolCall.tool由sanitize_detail_text改成detail_text;CommandExecution.status、McpToolCall.status、ThreadItem::Other.raw_type、ThreadFileChange.kind统一bounded(..., THREAD_DETAIL_MAX_CHARS)。说明:机器判别字段只限长、不脱敏改写,避免破坏枚举语义。
4.
wire/items.rs:244空text短路content/summary兜底 —— 已修(25213c9ff)。当前实现:先做"非空/非纯空白"过滤,再回落
content/summary。问题(原):过滤在兜底之后,
"text": ""会产出Some("")短路兜底,末尾再返回None,整条 message/reasoning 被丢掉。5.
detail_text先脱敏后截断的成本 —— 已答复,代码不动。结论:脱敏有两条链路,你问的那处只影响前端,不落盘。
· 前端这条:
wire/items.rs::detail_text/sanitize_detail_text只被thread_item_from_value/thread_items_from_history调用,产物只进下发给前端的 ThreadEvent 与历史切片,没有任何写盘操作。它的口径:项目内绝对路径换成项目相对路径(<root>/game/src/x.ts→game/src/x.ts),其余绝对路径<absolute-path>、密钥[redacted-secret]。· 落盘这条:
runtime_state.rs::redact_agent_runtime_error,负责.agent/runtime/errors/*.json、.agent/runtime/direct-codex-diagnostics/**/failure.json、AppData 的 application.log,以及turn.completed.failure载荷里的detail/cause/diagnostic。它的口径:项目根换成$PROJECT_ROOT,另有<redacted-url>。它同样在最后一步才截断(truncate_agent_runtime_text)。· 所以"先脱敏后截断"两条都存在,理由也相同(先截断会把 token 切断、漏半个密钥);前端这条只是每次事件的 CPU 成本,没有磁盘副作用。真要治只能在入口限制原始 payload 大小,建议保持现状。
6.
check-generated-bindings.mjsWindows 上的npx—— 已修(09f007d51):模块级npxCommand = win32 ? 'npx.cmd' : 'npx'。7.
check-generated-bindings.mjs:114-115逐目标process.exit—— 已修(80afa2a13):失败改为failed = true+continue,跑完全部目标再统一exit 1。8.
check-generated-bindings.mjs:52hint 缺参数 —— 已修(bfbcb1dd0):shared-contracts 补--locked --manifest-path server-rs/Cargo.toml,AGC 补--locked。9.
check-generated-bindings.mjs:76-77snapshot()不递归 —— 已修(fff4e8ff6):readdirSync(recursive)+ 文件过滤 + POSIX 键名。10.
codex_app_server/mod.rs:4265多余的一次String分配 —— 已修(e9ad1f3dd):改回write!(formatter, "{error}")。11.
directTurnEnqueueFailure.ts:50无条件包 wrapper —— 按你的结论不改:信任 Tauri,invoke的 rejection 形状就是EnqueueError。评审提的两个反例(
JSON.stringify(new Error('boom')) === '{}'、半结构化 payload 让.trim()再抛)都建立在"rejection 可能是任意 JS 值"上;在这个前提下不成立。若哪天发现不成立,再回来加形状判据。12.
directTurnFailure.ts:167缺kind时抛错 —— 已修(1ba9ac848):directModelCallText参数放宽为ModelCallKind | null | undefined,缺分类时回落通用句。13.
dispatch.rs的Option<TurnFailure>—— 已改(11e98a3ca)。当前实现:
TurnError::terminal_failure -> Option<TurnFailure>换成TurnError::classify -> TurnErrorClassified{ShouldStop(TurnFailure), ShouldContinue { detail }}。dispatch.rs两处.expect("回合失败必可投影成失败载荷")改成按分类 match:ShouldStop写终态,ShouldContinue不写终态、不伪造失败;turn_terminal里ShouldContinue显式认账(debug 构建断言,release 落到账本正常终态)。
说明:控制流不再借
None表达"要继续跑";TurnFailure上加了// TODO badnaming。14.
check-generated-bindings.mjsrun()吞掉 spawn 失败原因 —— 已修(6f2eed0dc):result.error时打印无法启动 <命令>:<原因>。15.
check-generated-bindings.mjs:73目录不存在静默通过 —— 已修(f593bd4bf):目录不存在时console.warn。16.
direct_runtime/mod.rs:7310断言direct-codex-failure:v2—— 已在更早的提交(9a75ea57f)修掉,当前 HEAD 不成立。当前实现:该用例断言结构化 sidecar 的
schemaVersion/source/stage/code/clientTurnId与脱敏结果;direct-codex-failure:v2 ...只在record_direct_codex_failure的返回值和错误上报池上断言。17.
direct_runtime/mod.rs:2017ModelCallFailed走文本分类 —— 已实现(2dbbf7245)。当前实现(原):
direct_codex_error_code对ModelCallFailed一律拿宿主原文跑子串分类。结果是ModelCallKind::TransportBroken(来自LlmError::Transport)只有在原文恰好含transport closed/连接已关闭时才会被标成transport-closed;宿主换一句 Display 就会掉进runtime-unclassified,同样的失败给出不同的诊断 code。改成:先看
payload.kind,TransportBroken直接给transport-closed;其余 kind 还没有专属 code,仍按原文兜底(保留TODO(文本判据))。新增用例
model_call_transport_broken_is_classified_from_the_typed_kind:原文不含任何分类标记,也只可能由 typed kind 得出transport-closed。18.
turn_completion.rs的and_then与SessionOutcome—— 已改(11e98a3ca)。当前实现:删掉 private
SessionOutcome,收尾 status → 终态的解析并成session_completion(status) -> Option<TurnCompletion>;turn_terminal不再用and_then压平:先按
host_failure > collect_outcome > 账本 failed 兜底选出错误,再走classify,ShouldStop直接成失败终态,
ShouldContinue不伪造失败。说明:
TurnCompletion仍是"这一轮怎么收场"的唯一定义,少了一个同义类型。19. ThreadManager 脱敏 / 限长搬前端 —— 已改(68cb41e33 Rust 侧,549915a2b 前端侧)。
· Rust 侧:删掉
wire/items.rs的bounded/detail_text/sanitize_detail_text/relativize_project_root_paths/thread_delta_text与THREAD_TEXT_MAX_CHARS/THREAD_DETAIL_MAX_CHARS/THREAD_PATH_MAX_CHARS;thread_item_from_value/thread_items_from_history/ 运行态投影 / 流式增量签名去掉 root,字段原样透传。dispatch.rs写历史失败不再预脱敏——错误文案的脱敏保留在
classify投影成失败载荷那一步(脱敏 + 截断)。· 前端侧:新增
directThreadSanitize,在 bootstrap / consume / 历史切片进入聊天状态之前统一过一遍;用 hook 已有的
projectPath把项目内绝对路径归一成相对路径,其余绝对路径 / 密钥 /敏感赋值 / Bearer / 配置文件 / 私钥块 / 敏感开关换成占位符。占位符沿用
<absolute-path>/[redacted-secret]/[redacted-sensitive-field]/[redacted-config]/[redacted sensitive context],model.ts的中文显示映射不用改;不做任何截断。配 12 条单测。· 说明:ThreadManager 每条线程的 8 MiB / 8192 事件订阅缓冲上限没动——那是订阅背压,不是
字段限长;字段限长已按"no bounded at all"全删。
20.
directTurnFailure.ts:175未知stage渲染undefined—— 已修(adcf3b973):查不到标签时回落"执行失败,请稍后重试"。(原评审的 patch 用的是旧分支名turnFailed,现名是superErrorFromStringPlusStage。)21.
direct_runtime/mod.rs入队失败硬写stage=code-generation—— 已答复:这是重构前就有的行为,不是本次引入。证据:重构前的
direct_turn_error.rs里,turn_failure_stage()的注释与实现是"已记录失败的诊断阶段;拿不到阶段的错误归到回合主体的代码生成段",即TurnFailed { stage, .. } => *stage,其余全部CodeGeneration。入队失败是EnvironmentNotReady/HostStateUnavailable,本来就落在CodeGeneration。这次重构只是把同一行为原样搬成record_direct_codex_enqueue_failure里的常量,没有改变结果。结论:评审"重构把 stage 记错了"的表述不成立。若你认为入队失败不该记成"代码生成段",那是新的语义需求(另立一个 enqueue / unknown 阶段值),可以单开一项——本轮按不改处理。
1.(转后续 issue,本分支不做)AGC 的
tests/不在任何 tsconfig、也没有 type-aware lint,satisfies TurnFailure这类 mock 类型断言是空写;需要单独立项:建
tsconfig.tests.json把 tests 挂进类型门禁。实测并入后暴露 640 个既有错误,须按文件分批清零,清零前不能挂进默认typecheck。2. apps/ai-game-creator-shell/tests/appSurface/home.suite.ts:2540(test · low)—— 入队被拒后没有任何"不重放"断言
已修(6550e9a4b):恢复
persistedMessages桩并让历史切片返回它;新增断言"被拒消息不落历史、不被乐观渲染、不写本地历史",并在
cleanup()+ 重挂载后断言被拒消息与拒绝文案都不重放、也不自动重试。appSurface 全套 195 passed / 9 skipped。名词澄清:"被拒回合" =
enqueue_direct_codex_turn命令返回结构化EnqueueError(本例permissionRejected)被拒,消息没进待发队列、这一轮从未成立——就是 msg enqueue error,不是回合失败。
更正 review 原始描述(已核对 git 历史,见 394b8de9d):原用例锁的其实不是"被拒回合不落盘",而是
"已跑起来的回合以
unauthorized失败后,用户消息 + 脱敏失败文案会落盘并在重挂载时重放"。3. apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs:205(bug · low)—— 泄漏的控制流仍会被 Drop 兜底收成 HostDropped
已修(84cbe9b1a、47da90ab8)。根因不是 dispatch,而是 direct_runtime 的返修循环:它只接住了
direct_delivery::review_reply返回的控制流,漏掉了 chat 调用自身返回的
TurnError::RepairRequired(app-server 封口复核,HostOutcome::RepairRequired)。这条控制流从 chat 的
Err漏出循环 → dispatch 的ShouldContinue => {}静默吞掉 →TurnReservation::drop兜成HostDropped。修法:① 循环的流式 / 非流式两条分支都补上
Err(TurnError::RepairRequired { detail }),与 review_reply 的返修要求同一处理,写回 prompt 继续一轮;② 删掉
finish_turn_failure,两处收口就地对classify显式分流:ShouldStop写失败终态,ShouldContinue直接unreachable!——不再有静默接受控制流的出口。③ 顺手清掉
turn_completion.rs::turn_terminal里同一口径的两处 ShouldContinue 分支(debug_assert!+ 回落 /host_dropped)→unreachable!(47da90ab8)。说明:修复前控制流是真漏,用户会看到一条说不出原因的失败;修完后它已被返修循环与终态投影挡在门外,
unreachable!只是把不变式写死在收口处。验证:
cargo test --bin genarrative-ai-game-creator-shell -- agent:: --skip export_bindings -- --test-threads=1→ 689 passed / 0 failed / 5 ignored。4. apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/turn_completion.rs:87(maintainability · low)—— 多余的 clone
已修(bc115002d):
(None, Err(error)) => Some(error)。collect_outcome按值匹配,Err(error)已经是 owned;host_failure那支仍是&TurnError,clone 保留。行为不变。5. scripts/check-generated-bindings.mjs:76-79(bug · high)—— 目录缺失时静默通过
已修(7fb4dabb5):
snapshot()遇到目录不存在改为throw new Error('生成绑定目录不存在,无法校验:<dir>')。原实现
console.warn+ 返回空 Map:若export_to路径被改名、或生成文件被整体删除,前后快照都是空 Map,diff 报不出任何问题,这个 target 会以"0 个文件"通过,门禁形同虚设。
6. scripts/check-generated-bindings.mjs:92(bug · medium)—— Windows 上无法执行 npx.cmd
已修(3ab313a11):
run()的spawnSync增加shell: process.platform === 'win32'(与 scripts/database-backup-to-oss.mjs、scripts/lint-staged-rustfmt.mjs 等既有写法一致)。不带 shell 时 Node 修完 CVE-2024-27980 直接 EINVAL,
把命令换成 npx.cmd 只是把 ENOENT 换成了 EINVAL,prettier 这步在 Windows 上仍然失败。
说明:Linux 侧已跑 check:generated-bindings 验证;Windows 路径本机实测不了,麻烦你在 Windows 上确认一次。
7. scripts/check-generated-bindings.mjs:80(maintainability · low)—— readdirSync 的 recursive 依赖 Node 版本
已修(6a4bcf4f3):改回普通
fs.readdirSync(absolute)。recursive要 Node 18.17 / 20.1 才有,仓库没有 engines 约束,旧版本会静默退化成只读顶层、反而削弱覆盖;四个目标目录实测都是平铺的(无子目录、无深度 ≥2 的文件),去掉后行为不变。
8. apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadSanitize.ts:434(security · medium)—— Bearer 引号值只遮到第一个空格
已修(d07f894bf):
\\.?["']→\\?["'],与同文件 SENSITIVE_ASSIGNMENT 的写法一致。原写法要求引号前必须有一个字面反斜杠,
Bearer "abc def"落不到引号分支,退化到裸 token 分支后实测输出Bearer [redacted-secret] def"(即def"漏出)。已在 directThreadSanitize.test.ts 补用例:修复前失败,修复后 12/12 通过。你的决定(2026-10-03):wontfix(维持宿主侧不做条目脱敏的有意设计)。下面保留现状与风险备查。
9. apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs:1184-1190(security · medium)—— 流式 delta 不再在宿主侧脱敏
现状:这是本分支的有意设计(
68cb41e33/549915a2b)——线程条目(流式 delta、direct_thread_event_item的 arguments/result)由 Rust 原样透传,统一交给前端
directThreadSanitize;只有错误文案的脱敏保留在宿主侧(TurnError::classify投影点)。问题:review 的风险成立——事件到达 webview 之前是明文,任何在 TS 脱敏之前读流的订阅方(dev 日志、持久化、错误捕获、其它窗口)
都能看到
sk-...与 tool-call 里的 token;渲染层一旦 XSS 也会暴露本来不必离开后端的数据。建议:你当时明确拍板"宿主侧不做条目脱敏",所以我没动。若要收口,最小折中是只在宿主侧保留凭据级脱敏
(
redact_secret_tokens,现在在 agent/generation/prompt_context.rs),不恢复整套路径/换行敏感的 sanitizer——既避开当初促使退役的那些 bug,又不让凭据明文跨进程边界。收哪些字段、要不要收由你定。
10. apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directTurnEnqueueFailure.ts:50-52(bug · medium)—— 真 Error 被包装后文案变成 {}
已修(0a16d5d57):catch 里先
if (error instanceof Error) throw error;再包装。JSON.stringify(new Error(...))得到{}(message/stack 不可枚举),包进EnqueueFailureWrapper后控制器读error.message只能拿到
{},横幅只剩 fallback。Tauri 的结构化拒绝是普通对象,不受影响;真 Error 交回控制器既有的instanceof分界,与类注释的口径一致。
11. apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directTurnEnqueueFailure.ts:75-78(bug · low)—— policyDetail / detail 直接 trim
已修(2fe651c84):改用
?.trim()。跨 IPC 载荷没有运行时校验,字段为 null/undefined 时原实现会在 runTurn 的 catch 里抛裸TypeError、草稿还被清掉;现在与空串同档:返回 null,按"没有提示"处理,保留草稿并走上报通道。
- TurnError::terminal_failure 的 Option<TurnFailure> 换成 TurnError::classify -> TurnErrorClassified{ShouldStop, ShouldContinue},控制流不再借 None 表达 - dispatch.rs 两处 .expect("回合失败必可投影成失败载荷") 改为按分类层 match,收到 ShouldContinue 不写终态、不伪造失败 - turn_terminal 不再用 and_then 压平;ShouldContinue 显式认账并落到账本正常终态 - 并掉 private SessionOutcome,收尾 status 解析直接产出 TurnCompletion - TurnFailure 补 // TODO badnaming,同步更新相关文档注释- directTurnEnqueueFailure.ts:invokeTurnEnqueue 的 catch 先判断 error instanceof Error,是则原样抛出 - JSON.stringify(new Error(...)) 得到 {}(message/stack 不可枚举),包进 EnqueueFailureWrapper 后控制器读 error.message 只能拿到 {},真实失败文案丢失 - 结构化拒绝是普通对象,不受影响;真 Error 交回调用方既有的 instanceof 分界,与类注释口径一致