文档:接单化落地收尾,ADR 转已接受并同步下游口径

docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md:状态改成已接受并指向实施计划;"落地时要同步的文档与注释"改成已同步清单。
docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md:顶部取代注扩到"事件不带回合身份"与"宿主侧 Drop 守卫兜底"两条决策形状,影响一节的两条已知边界逐条写明新口径。
docs/technical/【实施计划】DirectProject命令接单化-2026-09-23.md:四步标记落地并补每步落地结果、验收证据(rust agent:: 949 / 前端 4473)与已知坑。
docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md:正常回合改成接单后先落盘再注入;异常回合收尾写明唯一终态出口是占用对象。
docs/project-memory/shared-memory/decision-log.md:host-dropped 两条口径加取代注(含 kind 追加 environment-not-ready),并追加 2026-09-23 接单化决策一条。
docs/README.md:索引行去掉"未实施",改成四步均已落地。
This commit is contained in:
2026-09-23 22:05:11 +08:00
parent 2772081791
commit ce668bbff3
6 changed files with 114 additions and 59 deletions
@@ -9274,10 +9274,11 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
## 2026-09-22 失败回合的终态:`turn.completed` 带 `failure` 载荷 + 宿主 Drop 守卫兜底
- 背景:宿主崩在 `turn.started` 之后时没有任何终态事件,前端 `turnRunning` 永远为真,界面停在「陶泥儿正在处理」;同时失败在事件流里与正常结束同形(`turn.completed(status="failed")`,前端根本不读 `status`),失败文案只能从命令返回那条通道另造,同一次失败因此有两条通道、两份文案,而"这一轮结束了没有"只有事件说了算。
- 决策(协议形状:复用,不新增事件类型):终态事件仍只有 `turn.completed`。`status !== "failed"` 表示正常结束 / 中断 / 终止,不带载荷;`status === "failed"` 是失败终态,**必须**带 `failure { kind, message }` —— `kind` 为稳定分类(`timeout` / `model-failed` / `transport-failed` / `request-rejected` / `host-dropped`,只给界面选语气),`message` 为宿主脱敏 + 截断后的可展示原因。"是不是失败"只看两件事:`collect_result` 是 Err 就用错误本身当原因;`collect_result` 是交付报告但状态已判成 `failed` 就用那份报告当原因。
- 决策(兜底覆盖全部收场路径):`turn.started` 进入队列之后武装 Drop 守卫,正常写完终态即解除;panic、回合 future 被丢弃、终态之前的早退由守卫补一条 `host-dropped` 失败终态。Thread Manager 不改一行:`turn.completed` 本来就是 `lifecycle_anchor` 成员,失败终态天然顶替更早的 `turn.started`,重放不会把已收口的回合看成"还在跑"。
- 决策(协议形状:复用,不新增事件类型):终态事件仍只有 `turn.completed`。`status !== "failed"` 表示正常结束 / 中断 / 终止,不带载荷;`status === "failed"` 是失败终态,**必须**带 `failure { kind, message }` —— `kind` 为稳定分类(`timeout` / `model-failed` / `transport-failed` / `request-rejected` / `host-dropped`,只给界面选语气;**2026-09-23 追加 `environment-not-ready`**),`message` 为宿主脱敏 + 截断后的可展示原因。"是不是失败"只看两件事:`collect_result` 是 Err 就用错误本身当原因;`collect_result` 是交付报告但状态已判成 `failed` 就用那份报告当原因。
- 决策(兜底覆盖全部收场路径):`turn.started` 进入队列之后武装 Drop 守卫,正常写完终态即解除;panic、回合 future 被丢弃、终态之前的早退由守卫补一条 `host-dropped` 失败终态。Thread Manager 不改一行:`turn.completed` 本来就是 `lifecycle_anchor` 成员,失败终态天然顶替更早的 `turn.started`,重放不会把已收口的回合看成"还在跑"。(**已由 2026-09-23「DirectProject 命令接单化」取代**:守卫换成接单时登记的占用对象,接单前的早退改判为拒单、不再产生回合,`host-dropped` 只保留给"说不出原因"的一类。)
- 决策(失败文案只有一条通道):聊天里那条失败说明仍落在原来的展示位(本轮最后一条助手气泡、只在运行期显示、不写进 `project.jsonl`),数据来源换成事件载荷;命令返回只保留运行错误横幅(含 `read_agent_runtime_error_detail` 的长 detail)与诊断留痕,不再写聊天气泡。可见文案映射仍走既有 `projectRuntimeVisibleError` 规则,只是执行点从 controller 移到 reducer。
- 明确不做:不为 `turn.started` 之前的早退(`turn/start` 请求失败、响应缺 `turn.id`、缺稳定 `clientTurnId`、历史注入参数构建失败)补事件或兜底路径 —— 它们不产生回合、也不会留下永远开着的回合;不为进程被强杀(`kill -9`)补前端判据。
- 明确不做:不为 `turn.started` 之前的早退(`turn/start` 请求失败、响应缺 `turn.id`、缺稳定 `clientTurnId`、历史注入参数构建失败)补事件或兜底路径 —— 它们不产生回合、也不会留下永远开着的回合;不为进程被强杀(`kill -9`)补前端判据。(**已由 2026-09-23「DirectProject 命令接单化」取代**:接单前的失败改判为拒单、由命令边界返回 typed 错误;接单后的这类失败由占用对象收口成 `turn.completed`;`kill -9` 的界面表现在新 ADR §2。)
- 同日被取代的还有本条目里的另一条:命令返回只保留"运行错误横幅 + 长 detail"——`详情:` 引用与 `read_agent_runtime_error_detail` 已删除,失败说明也不再落命令边界(改由宿主投影写事件载荷与诊断池)。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{direct_thread_wire.rs,direct_turn_failure.rs,direct_thread_manager.rs,codex_app_server/mod.rs}`、`apps/ai-game-creator-shell/src/view/project-development/chat/{conversation/directThreadChat.ts,conversation/directTurnFailure.ts,controller/useDirectProjectChatController.ts}`、生成绑定与两侧用例;文档 `docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md` 与 `docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md`。
- 验证:见本条决策对应的提交记录(Rust 定向测试、reducer 与 appSurface 用例、`cargo test export_bindings` 后的生成绑定、`npm run check:encoding`、`git diff --check`)。
@@ -9299,3 +9300,20 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 明确不做:不新增事件类型、不给失败载荷加字段、不改前端可见文案映射;不给回合路径补轻型假 app-server 夹具(判据落在 `direct_turn_terminal` 与执行适配器两层单测)。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/{mod.rs,execution.rs}`、`apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_failure.rs` 与其单测;文档 `docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md` 与 `docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md`。
- 验证:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bins direct_`(442 passed)、`codex_app_server`(100 passed)、`cargo fmt --check`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。真实客户端观感未复核。
## 2026-09-23 DirectProject 命令接单化:命令只接单,逻辑回合归 Thread Manager
- 背景:`chat_with_game_creator_direct_codex` 一个命令调用覆盖整轮(校验 → 跑 → 交付验证),于是命令边界同时兼职"接单被拒"与"回合失败"两种回执:失败文案有事件载荷与命令 Err 两条来源,`withDirectCodexSessionRefresh` 的"刷新会话 + 重跑整轮"会重复落盘一条用户消息,认证重试只能挂在 Err 上;而回合边界又镜像 Codex 原生回合(`turn/start` 成功应答后才发 `turn.started`),"接单到 `turn/start` 之间"的失败(连不上 app-server、配置未就绪、历史注入失败、`turn/start` 被拒)没有任何事件可解释,命令一旦不 await 就会静默。
- 决策(命令 = 接单 / 拒单):命令只做 `clientTurnId` 校验 → 占用调用身份(只挡并发,早于工程准备)→ 工作流恢复 → 用户条目校验 → 工程准备 → 接单成立 → 用户条目落盘 → 起 codex,成功后立刻返回、不等回合。命令返回类型改成结构化的 `DirectTurnError`(ts-rs 已导出到 `chat/generated/`,与 `DirectThreadEvent` 同一套 `cargo test export_bindings` 流程)。
- 决策(分流判据从"错误种类"改成"发生位置"):**接单之前**的失败(目录、权限、输入、并发、工程准备未就绪、宿主状态取不到)是拒单——不产生回合事件、不写用户条目、不写失败诊断;**接单之后**的失败(连接、配置、历史注入、`turn/start` 被拒以及回合过程中的一切)是回合失败,只走 `turn.completed` 带 `failure` 载荷一条通道。`DirectTurnError::EnvironmentNotReady` 接单前后都可能出现,因此新增自己的失败分类 `environment-not-ready`(否则投影会写成 `model-failed`、界面语气就错了)。这条位置判据取代原先"调用级拒绝直通"的分支。
- 决策(逻辑回合由 Thread Manager 拥有):新模块 `agent/direct_turn_accept.rs` 按 thread 维护占用登记,`accept(thread, user_item_id, client_turn_id)` 在同一个临界区里完成"拒绝并发 + 登记占用 + 追加逻辑回合开始事件";`DirectTurnReservation::finish(terminal)` 幂等写出 `turn.completed` 并释放占用,`Drop` 兜底补 `host-dropped` 终态。发点在接单时、不再镜像 Codex 原生回合(原生事件留在适配器内部,不再进事件队列),线上仍只有一对 `turn.started` / `turn.completed`。因此"接单成功 ⇔ 事件流里有开始且有结束"是结构性成立,不依赖实现者给每条早退路径补事件。并发锁与首页快照的口径:`DirectTaonierActiveInvocation` 退回纯单飞锁,首页"运行中的项目"改由 `list_direct_active_turns` 从 TM 的逻辑回合导出(`ActiveDirectTurn` 带快照字段,`DirectActiveTurnSnapshot` 移入 `direct_thread_manager.rs`),不再留两处事实。
- 决策(回合身份由 `clientTurnId` 推导):`turn.started` / `turn.completed` 的 `userItemId` 按 `direct-codex:{clientTurnId}:user` 算出(与前端 `directCodexConversationMessageId` 同规则),**不读盘回填**——开始事件发生在用户条目落盘之前,落盘本身也可能失败。
- 决策(界面:同级提示、删除 `详情:`):失败说明与接单被拒提示都与用户消息**同级**、按事件顺序排在它后面,不嵌在这条用户消息里;删掉 `详情:` 引用、它的正则解析与只服务详情展开的第二次 IPC `read_agent_runtime_error_detail`;顶部状态行只显示回合状态,不承载错误文本。前端按 typed 变体分流:认得的"前置条件不满足 / 用户参数无效"(`clientTurnIdMissing` / `clientTurnIdMalformed` / `turnAlreadyRunning` / `projectRootUnanchored` / `projectRootUnusable` / `permissionRejected` / `inputRejected` / `contentEmpty`)→ 出同级提示、不走 `captureAgentRuntimeError`;认不出的变体(`environmentNotReady` / `hostStateUnavailable`)以及非结构化错误 → 抛出,走既有捕获上报链路。
- 决策(队列与埋点听回合终态,不听命令返回):前端发送队列的放行改由"回合完成(终态事件)或接单被拒"驱动,reducer 新增 `completedTurnCount` 作为唯一判据——不能用 `turnRunning` 的下降沿,一轮可能同批开始 + 结束。埋点结算同样挂到回合终态:不能在接单返回时结算,成绩是回合末才入 `pending_runs`,提前结算会变成空操作;`runTurn` 返回"是否接单",接单失败的路径只清句柄、不结算。**加 TODO:这条队列以后挪到 Rust 端,落点就是 Thread Manager 的接单动作。** 首页"运行中的项目"快照改由 TM 的逻辑回合导出,任务侧不再单独维护一张表。
- 决策(认证失败不再重跑整轮):删掉 `withDirectCodexSessionRefresh` 的"刷新 + 重跑整轮"(重跑会重复落盘用户消息),登录态失效按普通回合失败呈现;用同一个包装的 `cancel_direct_codex_turn` 一并去掉。
- 决策(失败原因本轮不落历史):失败原因只走事件载荷与宿主诊断(`.agent/runtime/errors` + 应用日志 + 错误上报池由宿主投影写出,进池责任从前端 catch 移到宿主),不写进 `project.jsonl`——重进项目只会看到那条没有回复的用户消息。**加 TODO(暂定做法见 ADR 备选方案第 3 条 (b)):以后要做"进历史但不喂模型"的失败条目,本轮明确不持久化。**
- 决策(CLI 保持 await):CLI 入口(`cli.rs` 的 `direct-codex.chat`)继续 await 整轮,因为它要把回复文本打到终端、没有事件订阅可用;两个入口共用同一份接单前检查、同一个命令主体和同一份 `Display` 文案,不各写一套判据。
- 明确不做:不给失败载荷加字段(不加 `detailRef`);不恢复 invoke 拒绝通道,也不为"接单后的前置失败"新增事件类型;本轮不做"失败条目进历史但不喂模型"(TODO)、不做 Rust 端发送队列(TODO)。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{direct_turn_accept.rs,direct_thread_manager.rs,direct_thread_wire.rs,direct_turn_error.rs,direct_turn_failure.rs,direct_project_context.rs,direct_runtime/{mod.rs,user_input.rs},codex_app_server/mod.rs,runtime_driver/entrypoints.rs,cli.rs}`、前端 `chat/{controller/useDirectProjectChatController.ts,controller/useDirectProjectTurnStatus.ts,controller/useDirectThreadChatSubscription.ts,conversation/directCodexConversation.ts,conversation/directThreadChat.ts,conversation/directTurnPresentation.ts}`、`chat/generated/{DirectTurnError,DirectTurnRejection,DirectThreadEvent,...}.ts` 与 `tests/{directThreadChat.test.ts,appSurface/*.suite.ts}`;文档 `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`、`docs/technical/【实施计划】DirectProject命令接单化-2026-09-23.md`、`docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md` 与 `docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md`。
- 已知坑:`cargo test export_bindings` 会重写全部 `chat/generated/`(引号风格漂移),跑完要 `git checkout --` 掉不是本次新增的文件;本机 rust 全量 `--bins` 测试会挂在 mock server 的 `inet_csk_accept` 上,用 `--bins "agent::"` 之类过滤跑。
- 验证:Rust 定向 `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bins "agent::"`(949 passed);前端 `NODE_OPTIONS=--localstorage-file=/tmp/ls-gen.json npm test`(4473 passed);`npm run ai-game-creator-shell:typecheck`、`cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。真实客户端观感未复核。