文档:执行通道断开定为失败终态,诊断记在执行适配器上
- ADR【DirectProject对话历史单一事实源】补一条决策:连接级故障与回合事件通道关闭同样带 failure{kind:"transport-failed"},原因用宿主当场写下的诊断,判据是"适配器是否已由宿主主动关闭"
- ADR「影响」补一条:断开时用户看到的仍是既有映射结果,真实诊断在事件载荷、宿主交付报告与运行日志里,改可见文案属于映射规则变更
- 技术方案【DirectProject Codex原始历史与异常恢复】同步线上形状,并写明失败事实为什么必须记在执行适配器上(看门狗会抢时序)
- decision-log 记本次决策、判据、不做项、影响范围与验证方式
This commit is contained in:
@@ -26,6 +26,7 @@ AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(
|
||||
- 活动回合的唯一判据是「出现过 `turn.started` 且未出现 `turn.completed`」;进程重启后队列消失,历史里的半截回合一律按已结束渲染。
|
||||
- 终态事件只有 `turn.completed` 一种,它同时承载三种语义:`status !== "failed"` 是正常结束 / 中断 / 终止,`status === "failed"` 是**失败**,且必须再带 `failure { kind, message }`(`message` 已脱敏截断)。失败原因只走这一条通道:前端不再从命令返回或另一条 IPC 里另造失败文案,聊天里那条失败说明仍落在同一个展示位上(本轮最后一条助手气泡、只在运行期显示),只是数据来源换成事件载荷;命令返回只用于运行错误横幅与诊断留痕。
|
||||
- 宿主侧兜底:`turn.started` 发出之后才武装 Drop 守卫,正常写完终态即解除;panic、future 被丢弃、终态之前的早退由守卫补一条 `status="failed"` + `failure.kind="host-dropped"` 的终态,避免前端永远停在"还在跑"。已知边界见「影响」一节。
|
||||
- 执行通道断开同样是失败终态,也必须带 `failure`:连接级故障(app-server 进程退出 / stdout 流断 / JSON 行越界)与回合事件通道关闭都算,`kind="transport-failed"`、`message` 用宿主当场写下的那份诊断(含 `exitStatus` 与 stderr 摘要,已脱敏截断)。宿主在检测到连接终止的第一时间把这条事实记到本回合的执行适配器上,终态判定再从适配器读:执行适配器的看门狗盯着同一个 `closed` 标志,用调用点局部变量会输给这场调度竞争,失败原因就只剩日志、界面只会看到"本轮已结束"。判据是"适配器是否已由宿主主动关闭"——宿主自己收束(正常终态 / 用户主动停止 / 预算与交付收尾)走的是同一个 `TransportClosed` 事件,但这些不算失败。
|
||||
- 分页锚点取原始条目 id;一次翻页操作在前端自动连拉,直到出现可显示条目或 `hasMore=false`,上限 5 页。
|
||||
- `notify` 是唯一唤醒来源:`subscribe` 的 bootstrap 事件本身就是该 subscriber 此刻要处理的事件(游标已在队尾),前端直接 reduce 它们,不需要为了取这批事件再补一次 `consume`,之后完全由 `notify` 驱动,不设低频 tick 或任何轮询兜底。唯一例外是回执竞态:Rust 侧一注册完 subscriber 就开始 `notify`,前端却要等回执才知道自己的 `subscriptionId`,这段窗口内的通知只能记成欠账,回执到达后立刻补一次 `consume` 取回,否则该回合的尾部事件会卡在队列里等一个可能永不出现的下一次通知。
|
||||
- 迁移按一次干净切换落地:不做灰度、不做运行时开关、不双跑;允许提交序列里存在「新源已启用、旧代码尚未删除」的中间窗口,禁止反向的「新源未启用、旧源已删」。
|
||||
@@ -55,6 +56,7 @@ AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(
|
||||
- 回合结束语义务必由 `turn.completed` 判定(失败时同一事件带 `failure` 载荷,不新增事件类型);缺少该事件的残留回合不得被渲染成运行中。
|
||||
- 两条已知边界,都**不**在本次补路径:① 宿主进程被强杀(`kill -9`)时没有任何 `Drop` 会执行,前端仍会停在运行态,那要靠前端自己的"命令已返回却没有任何终态事件"判据;② `turn.started` 之前的早退(`turn/start` 请求失败、响应缺 `turn.id`、历史注入参数构建失败等)不产生回合、也不补终态事件——它们不会留下永远开着的回合,出问题时只有运行错误横幅解释。
|
||||
- 失败原因里的 `message` 是宿主侧脱敏 + 截断后的可展示文本,前端仍按既有口径做一次可见文案映射(`projectRuntimeVisibleError`),映射规则不因这次改动改变。
|
||||
- 执行通道断开时用户看到的仍是既有映射结果(诊断命中不了专门规则,落到通用兜底),真实诊断在事件载荷、宿主交付报告与运行日志里;把"连接断开"改成专门文案属于映射规则变更,不在本 ADR 范围内。
|
||||
- 「活动回合的唯一判据」约束的是**原生回合**:界面上的「本地已发出、原生还没认领」是投影的展示态(`DirectChatTurn.state = 'awaiting-start'`),由本地在途用户条目身份派生,不构成第二套原生生命周期,也不参与 `turnRunning` 的判定。
|
||||
- 三层数据流、变量归属与一次发送的时序写在代码里:`apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts` 的模块注释;回合三态的定义与判据真值表在 `apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directTurnPresentation.ts` 的 `DirectChatTurnState`。改判据时同步这两处与对应测试。
|
||||
- 验收证据是端到端行为,不是单元测试:回合进行中杀掉应用进程后重开项目,应看到部分文本与工具卡片按原顺序出现且不显示忙碌;正常结束后重进应与实时渲染一致;文件系统不得再新增 `turn-stream.jsonl` / `tool-calls.jsonl`。
|
||||
|
||||
@@ -9394,3 +9394,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 明确不做:不为 `turn.started` 之前的早退(`turn/start` 请求失败、响应缺 `turn.id`、缺稳定 `clientTurnId`、历史注入参数构建失败)补事件或兜底路径 —— 它们不产生回合、也不会留下永远开着的回合;不为进程被强杀(`kill -9`)补前端判据。
|
||||
- 影响范围:`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`)。
|
||||
|
||||
## 2026-09-22 执行通道断开也是失败终态:诊断记在执行适配器上,不与看门狗抢时序
|
||||
|
||||
- 背景:手工杀掉 codex app-server(`kill -9`)验证上一条修复时,回合确实收口了(界面不再停在"还在处理"),但**没有任何失败说明**:连接级故障走的是 `TransportClosed` 分支,那里用一句策略文案 `adapter.interrupt(...)` 收束成 `ExecutionPhase::Interrupted`,`lifecycle_status` 把 `Interrupted` 映射成 `status="interrupted"`,`direct_turn_failure` 因此返回 `None`,事件不带载荷、reducer 也就不落说明条目;真实诊断(`Codex app-server 已退出;exitStatus=signal: 9 (SIGKILL);stderrClass=...`)只进了 `app_log!`。
|
||||
- 决策(失败事实记在执行适配器上):新增 `ExecutionAdapter::transport_failed(diagnostic)` 与只读的 `transport_failure()`。连接级故障(`fail_game_creator_codex_app_server_connection`)与回合事件通道关闭(`TransportClosed` / 事件通道 `None`)都调它:先同步记下"本轮以传输失败收口"与原因,再把同一份原因补进宿主交付报告(`interrupt` 对已有终态不覆盖,报告只作旁证)。`lifecycle_status` 见到这条事实一律返回 `failed`,`direct_turn_failure` 因此产出 `kind="transport-failed"`、`message=诊断` 的载荷。原因不能存在调用点局部变量里:执行适配器的看门狗盯着同一个 `inner.closed` 标志,它可能先把回合收束成 `Interrupted`,而终态判定发生在收束之后。
|
||||
- 决策(区分"连接自己断了"与"宿主关的连接",判据收在适配器里):`transport_failed` 先看 `is_closed`——宿主自己收束(正常终态 / 用户主动停止 / 预算与交付收尾)时适配器先于连接置位 `closed`,那种情况下只按既有口径中断收口(原因照样写进报告),不记失败事实;调用点两条分支的判据保持原样(`!is_host_ending()`),不动它们的控制流。
|
||||
- 决策(载荷取诊断而不是交付报告):`direct_turn_failure` 增加第三来源且优先级最高——通道断开时原因用宿主诊断(含 `exitStatus` / stderr 摘要),不用 `collect_result` 里那份只说"收束到哪一步"的交付报告;报告与载荷同源的说法只对"原因写进报告"这一步成立,事件载荷才是失败原因的唯一权威。
|
||||
- 明确不做:不改前端可见文案映射(诊断命中不了专门规则,仍落到通用兜底文案);不给连接级故障补端到端集成用例(判据落在策略函数与适配器两层单测,DirectProject 回合路径缺轻型假 app-server 夹具);`kill -9` 掉宿主进程本身仍没有 `Drop`,不在本次范围。
|
||||
- 影响范围:`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 direct_`(439 passed)、定向 `transport_failure` / `direct_turn_failure::`(10 passed,含新增三条:适配器把诊断记成失败终态且只认第一份原因、宿主自己关的连接不算失败、失败载荷优先取宿主诊断)、`cargo fmt --check`、`npm run check:encoding`、`git diff --check`。真实客户端观感未复核。
|
||||
|
||||
@@ -126,6 +126,8 @@ type DirectThreadEvent =
|
||||
|
||||
**终态只有 `turn.completed` 一种,失败靠 `failure` 载荷区分。** `status !== "failed"` 表示正常结束 / 中断 / 终止,事件不带 `failure`;`status === "failed"` 是失败终态,**必须**带 `failure { kind, message }`:`kind` 是稳定分类(`timeout` / `model-failed` / `transport-failed` / `request-rejected` / `host-dropped`,只给界面选语气),`message` 是脱敏截断后的失败原因。失败原因只走这一条通道——前端不再从命令返回或另一条 IPC 里另造失败文案;`status="failed"` 却没有载荷视为协议违规。
|
||||
|
||||
执行通道断开(app-server 进程退出、stdout 流断、回合事件通道关闭)也走同一条终态:`kind="transport-failed"`,`message` 是宿主当场记下的诊断(`exitStatus` + stderr 摘要,脱敏截断)。宿主在检测到连接终止时**第一时间**把这条事实记到本回合的执行适配器上,终态判定再从适配器读——执行适配器的看门狗盯着同一个 `closed` 标志,若只在调用点用局部变量记录,会与看门狗的收束竞争,输掉时就只剩 `status="interrupted"` 加一句收尾说明,界面只显示"本轮已结束"、看不到原因。判据是"适配器是否已由宿主主动关闭":宿主自己收束(正常终态 / 用户主动停止 / 预算与交付收尾)同样会发 `TransportClosed`,但那些不算失败。
|
||||
|
||||
宿主的异常收场同样靠这条事件:`turn.started` 进入队列之后武装一个 Drop 守卫,正常写完终态即解除;panic、回合 future 被丢弃、终态之前的早退由守卫补一条 `status="failed"` + `failure.kind="host-dropped"` 的终态。两条已知边界——宿主进程被强杀(`kill -9`)时没有任何 `Drop` 执行;`turn.started` 之前的早退(`turn/start` 请求失败、响应缺 `turn.id`)本身不产生回合——都不产出终态事件,也不假装有回合可收,前端在这两种情况下仍按"命令已返回"的既有语义收尾。
|
||||
|
||||
一个 thread 同时最多有一个 active turn;一个 turn 内允许多个并发 item。`turn.completed` 必须在该 turn 的完成 item 均成功持久化后进入队列,前端据此结束运行态;不能用“不存在 unfinished item”猜测 turn 是否完成。
|
||||
|
||||
Reference in New Issue
Block a user