@@ -1,5 +1,138 @@
# 决策记录
## 2026-09-22 UI 编辑器预览画布补上右键拖拽平移,节点菜单改为右键抬起弹出
- 背景:预览画布此前只有中键与空格+左键平移,右键整段留给节点操作菜单(`UiTreeRenderer.onContextMenu` 直接弹 `UiNodeContextMenu` )。这次要补右键拖拽平移,并要求"拖拽过就不许再触发右键菜单"。实测(Linux Chromium 151 / Firefox 151,真实 X11 输入)确认 `contextmenu` 在**按下**瞬间触发,且原生菜单一旦弹出,页面之后收不到任何 `pointermove` / `pointerup` / `mouseup` / `auxclick` ,所以"先让菜单弹、拖拽时再关"在浏览器层面不可行;headless 没有原生菜单,Playwright 复现不出该行为。macOS 的 `contextmenu` 在 mouseup 触发(本容器无法实测),但同一条实现路径对两种时序都成立。同一次实测:键盘菜单键触发的是 `button: -1` ,所以"只认按钮 2"的拦截天然把键盘菜单留给原有节点菜单路径。
- 决策:预览视口在捕获阶段拦截按钮 2 的 `contextmenu` ( `preventDefault` + `stopPropagation` ),右键手势改由预览自己裁决:按下时记录起点、`setPointerCapture` 并交焦点;移动越过与左键拖拽共用的 `DRAG_THRESHOLD_SCREEN_PX` (2px)后本次手势定死为平移,按"按下点全量 delta"更新视口(光标复用共享 `CanvasViewport` 的 `isPanning` → `cursor: grabbing` ,三个平移绑定一起生效);未越阈值且在预览内抬起时,用 `[data-node-id]` 加树容器 `data-tree-id` 命中节点并打开 `UiNodeContextMenu` ,保留"右键即选中该节点"的既有语义;空白处干净右键不做事(原生菜单已被抑制)。中键、空格+左键平移不变,macOS ctrl+左键与键盘菜单键继续走原有即时菜单路径;平移是纯视图操作,不写 State、不进历史、不受 `isLocked` 与空格按住态限制。
- 原因:右键同时承载菜单与视图平移,只能等到手势结束再裁决;把判定放进预览,是因为树、节点命中、预览边界与拖动阈值都在预览手里,而 `useNodeTransformInteraction` 应保持左键变换的单一职责;`[data-node-id]` 命中也已是 `resolveHitNodeId` 的既有模式。
- 代价与取舍:右键从"按下即弹菜单"变成"抬起才弹"(与 Windows 自身右键菜单一致);预览内空白处的浏览器原生菜单被永久抑制;右键平移与节点菜单互斥(越过阈值后抬起不再弹菜单);`grabbing` 光标在悬停到节点上时仍会被节点自身的 `cursor-move` 覆盖,与共享画布现状一致。本次只改 UI 编辑器预览:AGC 美术画布仍只有空格/中键平移,资源画布保留自己的右键平移实现,都不动,也不抽 `packages/shared` 。
- 验证方式:新增 `previewRightPanGesture` 纯状态机单测(阈值跨越、起点全量 delta、拖拽吞菜单 / 干净抬起开菜单、取消与失焦清理),并在 `previewRightPanDrag.test.tsx` 、`previewRightPanGesture.test.ts` 补右键回归(含键盘菜单键 `button: -1` 不被拦截的用例);运行 `npx vitest run` (定向文件)、`apps/ai-game-creator-shell` `npm run typecheck` 、`npm run lint:eslint` 、`npm run check:encoding` 、`git diff --check` 。
## 2026-09-24 接单化 review 收口(第二轮):失败载荷分类、拒单身份与提示口径
- 决策(失败载荷的 `kind` 收成 typed 枚举):新增 `DirectTurnFailureKind` ( `Serialize + Deserialize + TS` ,
`kebab-case` ,7 个变体,含先前两份名单都漏登记的 `turn-interrupted` ),`DirectTurnError::wire_kind`
返回 `Option<DirectTurnFailureKind>` 。线上仍是 `{kind, message}` 、取值不变,只有 TS 侧从裸 `string`
变成可穷尽收窄的联合类型;全仓没有按 `failure.kind` 分流的代码,它只给界面选语气。
- 决策(并发拒单的两个身份是回合身份):`DirectThreadManager::accept_turn` 冲突时返回占用对象的
`turn_id` , `DirectTurnReservation::accept` 把这一轮请求的 `clientTurnId` 传成
`incoming_invocation_id` 。改动前这两项是进程内 UUID, `TurnAlreadyRunning` 的"同一轮仍在处理中"
分支永远命中不了,也与"回合身份由 `clientTurnId` 推导"的口径冲突。占用对象自己的 `token` 仍是
UUID( `complete_direct_thread_turn_if_reserved` 靠它配对),只换错误载荷里的两项。
- 决策(目录锚不定的拒单不再写诊断):`DirectTurnError::ProjectRootUnanchored` 从 `is_reportable()`
拿掉,与 `ProjectRootUnusable` 同类——符号链接 / 权限 / 目录被删都是用户自己就能修的文件系统事实。
改动前它被命令边界覆写成 `direct-codex-failure:v2` 收口文案,界面上那句"无法锚定 Direct 调用项目
目录:{cause}"被内部诊断串顶掉;现在界面按 `Display` 显示,也不再进 `.agent/runtime/errors` 。
可留痕的拒单只剩 `environmentNotReady` / `hostStateUnavailable` 。
- 决策(认不出的拒单也要在聊天里有同级提示):`environmentNotReady` / `hostStateUnavailable` 除上报 +
横幅外,再补一条与用户消息同级的提示——拒单没有接单、不产生 `turn.completed` ,否则那条乐观用户
气泡后面永远没有解释(改动前的注释"宿主已经把它放进了 `turn.completed.failure` "对拒单不成立)。
文案走 `projectRuntimeVisibleRejectionError` :取宿主收口文案里已脱敏的摘要与建议,**不套阶段标签**
(拒单这一轮没有开始,阶段只会是默认值);非结构化错误仍只走横幅(它可能发生在接单之后)。
- 决策(失败说明的文案口径):`projectRuntimeVisibleError` 补上宿主 `Display` 事实句的模式
( `执行通道已断开` / `等待模型回合结束达到硬上限` / `宿主任务提前结束` / `收尾历史失败` 一族),
并给落盘那档补上不带"失败"二字的事实句;不回落宿主原文(`TransportClosed` 的原文带 `exitStatus=` /
`stderrClass=` )。同时修掉收口文案的版本口径:解析只认 `v1` 、宿主发的是多一段 `code=` 的 `v2` ,
脱敏摘要一直命中不了。口径定为"不加模式就只会看到通用文案",写在 `directTurnFailure.ts` 的注释里。
- 明确不做:不改线上载荷形状与 `kind` 取值;不加新的失败阶段取值(拒单仍落默认阶段);不动
`ProjectRootUnanchored` 之外的拒单分类。
- 决策(连接死亡的失败事实先于看门狗可见):`CodexAppServerInner::closed` 的语义定为"这一段已经收束 /
失败事实已经记下",看门狗就盯着它,所以它不能再兼作死亡收口的去重标志——去重改用私有的
`connection_end_claimed` , `fail_game_creator_codex_app_server_connection` 不再置 `closed` ,
`closed` 只在 `shutdown_game_creator_codex_app_server_inner` 里、`record_execution_turn_failure` **之后**
置位。改动前收口路径先置 `closed` 再做"两次加锁 + 一次日志写",200ms 看门狗可能在这一段里抢跑,把
这一轮收束成 `Interrupted` , typed `TransportClosed` 记不进去,终态退化成"本轮已结束、没有原因"
(失败事实是在模型终态那一刻被快照的,晚补记无用,所以只能保证"事实先于可见性")。代价是其它读
`closed` 的地方会晚几十微秒看到"连接已死",两个并发的死亡观察者仍会各自走到幂等的收束函数。回归用例
`connection_death_records_the_failure_fact_before_the_watchdog_seals_the_turn` 卡住 stderr 摘要锁把窗口
拉成确定性,把看门狗真正跑起来钉这条(顺序反了就红)。
- 决策(接单之后的失败不回命令返回值):`chat_with_game_creator_direct_codex_typed` 在接单后的历史追加
写失败时仍然写 `turn.completed` 失败终态,但 `return Ok(())` ——命令的 `Err` 只表示**拒单**。改动前同一
个失败从"事件里的说明"和"命令 `Err` 的横幅"两条通道下发(且 `EnvironmentNotReady` 会写诊断 + 上报),
前端又把 `Err` 当"这一轮没开始",于是忙态与出队同时被事件和返回值两条路推。**不继续起整轮**:
`project.jsonl` 是这条对话的单一事实源,用户消息没落盘时继续跑只会得到一条没有开口用户消息的助手回复,
失败还会被静默。用例:Rust `a_history_write_failure_after_accept_closes_the_turn_instead_of_rejecting`
(恰好一条失败终态、不带拒单收口文案、占用释放)、前端 appSurface 的落盘失败用例(说明只来自事件且
恰好一条、忙态放掉、下一条能发)。
- 影响范围:Rust `apps/ai-game-creator-shell/src-tauri/src/agent/{codex_app_server/mod.rs,direct_turn_error.rs,direct_turn_failure.rs,direct_turn_accept.rs,direct_thread_manager.rs,direct_runtime/user_input.rs}` ;
前端 `src/features/agent-runtime/model.ts` 、`src/view/project-development/chat/{conversation/directCodexConversation.ts,conversation/directTurnFailure.ts,controller/useDirectProjectChatController.ts}` 、
`src/view/project-development/chat/generated/DirectTurnFailureKind.ts` 与
`tests/{agentRuntimeModel.test.ts,directThreadChat.test.ts,appSurface/chat-composer.suite.ts,appSurface/project-conversation.suite.ts}` ;
文档 `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md` 、`docs/technical/【实施计划】DirectProject命令接单化-2026-09-23.md` 。
- 验证:Rust `cargo test --bins "agent::"` ( 902 passed / 5 ignored)、定向
`cargo test --bins "agent::direct_turn_error"` ( 15 passed)、`cargo fmt` ;前端
`npx vitest run tests/{appSurface.test.ts,directRunAnalytics.test.ts,directProjectTurn.test.tsx,agentRuntimeModel.test.ts,directThreadChat.test.ts}`
( 277 passed / 9 skipped)、`npm --prefix apps/ai-game-creator-shell run typecheck` 、`npm run check:encoding` 、
`git diff --check` 。真实客户端观感未复核。
## 2026-09-24 接单化 review 收口:终态写点、返修控制流、终止判据与失败投影
- 决策(终态的写点在整轮真正结束之后):Direct 回合先固定终态判定的上下文,`turn.completed` 的写出
挪到执行结果收集、历史落盘、structured output 解析都定型之后,成功与失败共用一个写点。解析失败也是
这一轮的失败,落进同一份失败载荷;改动前终态先写、再解析,解析失败时终态已是 `completed` ,占用对象
的兜底变成空操作,用户看到"本轮结束、没有回复、没有任何解释"。收尾结果因此拆成
`DirectTurnReport` (报告正文 + 解析结果),占用解除与终态事件一起走 `DirectTurnTerminalContext::write` 。
- 决策(封口返修要求是控制流,不是失败):`HostOutcome::RepairRequired` 不再伪装成
`LlmError::InvalidRequest("validation-source-changed: …")` ,改为 typed 的
`DirectTurnRunFailure::RepairRequired` → `DirectTurnError::RepairRequired` :不写终态、不进载荷、不上报,
由 `direct_runtime` 的返修循环写回提示词继续跑(与 `ReviewRequired` 同一族,次数上限仍留在产生侧)。
改动前它被判成 `failed` 终态、界面收到一条假失败,还会让同一个逻辑回合写出第二条终态。
- 决策(用户按下的终止不算通道失败,判据收进 `fail_turn` ):失败事实的判据是
`!is_closed() && !host_stop_requested()` ,不再由各调用点各写一遍 `!is_host_ending()` 。用户点「终止」时
标志先置位、阶段后变,原来的窗口里到达的 `TransportClosed` 会把用户自己的终止记成 `transport-failed` 。
- 决策(登录态失效的两条分类路径统一可重试):认证失败不再按"重跑整轮"处理,刷新失败与重试失败都按
可重试的回合失败呈现(用户可见文案可能多一句"可直接重试",真实客户端观感未复核)。
- 决策(失败载荷的健壮性):前端 reducer 对 `failure.message` 做运行时判据(缺字段 / `null` 不再抛错,
与 `directTurnFailureNoticeText` 同口径);交付报告兜底只读一次 `terminal_report` (两次读取之间状态可能
变化,`None` 不再被 `unwrap_or_default()` 变成空回复);失败说明条目在无身份无时间时会撞成同一条
(已知边界,仅补注释)。
- 明确不做:不改线上载荷形状(仍是 `{kind, message}` );不给 DirectProject 回合补端到端集成用例(缺轻型
假 app-server 夹具),判据落在策略函数与适配器单测;不持久化"可见但不喂模型"的失败条目(TODO)。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{codex_app_server/{mod.rs,execution.rs},direct_runtime/{mod.rs,user_input.rs},direct_turn_error.rs}` 、前端
`chat/{conversation/directThreadChat.ts,generated/DirectTurnError.ts}` 与 `tests/directThreadChat.test.ts` ;
文档 `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md` 、`docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md` 。
- 验证:`cargo test --bins "agent::"` ( 952 passed)、`cargo test --bins "direct_"` ( 475 passed)、定向
`codex_app_server` ( 102 passed)、前端 `directThreadChat.test.ts` ( 36 passed)与
`npm run ai-game-creator-shell:typecheck` 、`npm run check:encoding` 、`git diff --check` 通过。真实客户端观感未复核
(终态写点与终止竞态落在真实宿主收尾上,单测盖不住)。
## 2026-09-23 Direct 回合错误改 typed:调用级拒绝与回合级失败分开
- 回合失败在宿主内部改成 typed 的 `DirectTurnError` ( `apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_error.rs` ):每个变体自带字段,调用级拒绝(并发复用同一 `clientTurnId` 、另一条回合在跑、权限策略拒绝、目录锚不定、输入校验、环境/凭据未就绪)与回合级失败(模型调用失败、通道断开、等待超时、app-server 单方面中断、阶段失败)不共用判据,分流只认 `is_turn_failure()` 。
- 根因:改造前两层错误混在同一份字符串里,靠对原因文本做子串匹配决定"算不算失败""要不要反馈给模型""怎么给建议",任何文案改动都可能静默改变分流;并发拒绝还只靠一个前缀字面量给前端识别。
- 分类不再做文本匹配:原生失败分类只解析 app-server 写下的 `codex-app-server-error:<kind>` 结构化前缀,转成 `DirectCodexNativeKind` 后再 `match` 。
- 明确不做:不改线上载荷(仍是 `{kind, message}` )、不改命令边界签名(仍是 `Result<String, String>` )、不改前端可见文案映射与 `wire_kind` 取值;Rust 侧不再解析那份字符串,字符串只在 `Display` 一处生成。不给深层尚未 typed 的事实补 typed 出口,只留一个显式的桥变体并在注释里写明新分类必须先加 typed 变体。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{direct_turn_error.rs,direct_turn_failure.rs,direct_delivery.rs,direct_runtime/mod.rs,direct_runtime/user_input.rs,codex_app_server/mod.rs,codex_app_server/execution.rs}` 与 `apps/ai-game-creator-shell/src-tauri/src/cli.rs` ;文档 `docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md` 。
- 验证:`cargo test --bins -- direct_` ( 460 passed)、`cargo test --bins -- codex_app_server` ( 100 passed)、`cargo fmt --check` 、`npm run check:encoding` 、定向 `git diff --check` 。整包 `cargo test --bins` 在本机被既有 `tests::provider` / `tests::project` 重型用例挂住(并发跑测试时另有一条锁竞争用例会假失败),非本次改动引入。真实客户端观感未复核。
## 2026-09-23 ACL 提权修复按目标做 single-flight
- 背景:`windows_acl_repair_target` 对 Managed 作用域返回的是「第一个读取被拒的祖先」,同一祖先下的多个项目会解析到**同一个** repair target;而唯一的去重只是单次调用内的局部 `attempted_targets` 。于是启动页一次挂载(≤8 个最近项目并发检查)会启动同样多次 `powershell -Verb RunAs` ,用户看到叠在一起的 UAC 弹窗(issue #498 )。
- 决策:新增进程级闸门 `acl_repair_gate` , key = `(规范化 repair target, scope)` 。并发调用只允许一次真实提权,其余等待并复用**同一结果**;结果在冷却窗口内直接复用(成功 30s / 失败 15s / 用户取消 120s),等待窗口 60s 超时按失败关闭。leader 异常退出由 RAII 兜底记为失败并唤醒全部等待者,避免等待者被永久挂住。
- 决策补充(key 归一化):key 的路径半边经 `windows_acl_repair_gate_key` 归一化——去掉 `\\?\` / `\\?\UNC\` 前缀并统一小写。最近项目列表里同一项目实测同时存在 `\\?\C:\...` 与 `C:\...` 两种写法(客户端 localStorage 实测),不归一化就是两个 key,同一个目录仍会弹两次 UAC。这里刻意只做前缀与大小写归一而不 `canonicalize` :待修复目标恰恰是「读不动的目录」,解析不可靠。
- 决策补充(冷却基准):冷却从**结果落库**时刻算起,不是 leader 起跑时刻。UAC 弹窗会被挂着几十秒到两分钟,用起跑时刻会让 120s 拒绝冷却在用户应答前就过期,前端 15s/45s/120s 的整表重查紧跟着再弹一次。
- 决策补充(leader 失效接管):`leader_deadline` (默认 5 分钟)之后,新调用可以接管仍是 `running` 的 key;每个 leader 带令牌,被接管后旧 leader 迟到的结果直接丢弃,不会覆盖接管者的结果。真机上无人应答的 UAC 约 2 分钟自然超时,所以这个上限只兜「提权子进程真挂死」——否则该目标会永久按失败关闭(`clear_denials` 不清理 running,只能重启客户端)。
- 错误类型化:用户取消 UAC 的错误统一带稳定标记 `AGC_ACL_ELEVATION_DENIED` ,前端据此判定「不可自动重试」,不再依赖中文文案匹配。
- 用户主动操作(打开/新建项目、文件选择器选择目录、重命名刷新)会调用 `clear_game_creator_acl_elevation_denials` 清除拒绝记忆,保证显式重试仍能再次请求提权。前端唯一入口是 `features/app-shell/aclElevation.ts` 的 `clearAclElevationDenials()` :最近项目 hook( `rememberRecentWorkspace` / `refreshRecentWorkspace` )与打开/新建链路(`useHomeProjectCreation.openProject` ,覆盖行内打开与 picker)共用它;漏挂入口会让用户「点了打开立即失败、也不问授权」。
- 未做:给提权子进程加有界等待(`Start-Process -Wait` 目前无超时)。理由:中断挂起的 UAC 流程比等待更糟,single-flight 已把并发弹窗收成一个,follower 的等待由 60s 窗口兜底。
## 2026-09-24 DirectProject 状态条口径翻转、几何约束与对话 Markdown 容错
- 背景:AGC DirectProject 对话区底部的「陶泥儿正在处理 / 已耗时 12.4秒」状态条同时退化三处:① 读秒 1 秒一跳(耗时文案不足一分钟显示一位小数,小数位却一秒才动一格);② 窗口压矮时被挤扁(300px 高压到 33px、240px 时 24px,文字被 `overflow: hidden` 裁掉);③ `turn.started` 之前(模型首 token 前,实测约十秒)整条卡片不出现,界面没有任何「正在处理」的交代。同批还修了对话 Markdown 的两处代码块问题(不换行把消息拉宽、粘在正文行里的围栏导致代码块解析错位)。
- 决策(卡片口径翻转,**更正** 2026-09-22「卡片口径取保守」):卡片与已耗时起点改读 `displayBusy` (本地命令在飞 ∪ 原生已确认在跑)与「最新一个**未结束**回合的用户发送时间」。理由:`turn.started` 要等宿主应答返回才发出,只认原生真相会让首 token 之前那段没有交代;窗口期这一轮确实已经交给宿主(本地命令在飞),文案不虚报「宿主已在跑」之外的东西。(**再更正** 同日:本地乐观气泡已删,已耗时起点改读该轮的 `turn.started.at` ——运行中读实时值、收口后读盖在条目上的值;接单窗口里还没有这一轮的条目,卡片只报「正在处理」、这一段不读秒。卡片口径本身不变:仍读 `displayBusy` 。)
- 决策(那条预言的处置):2026-09-22 那条写「若将来改成窗口期也显示卡片,`running` 在渲染层就没有消费者了,应把投影压成 `unfinished: boolean` 」。本次改完后投影三态**仍有**消费者(**再更正** 同日:投影已压成两态 `running` / `finished` , `awaiting-start` 随本地乐观气泡一起删除;下面这两条消费者读的判据不变)——`DirectProjectTurn` 用 `state !== 'finished'` 做否定式判断、`state === 'running'` 挑流式正文,状态条也用 `state !== 'finished'` 定起点——所以不动 `DirectChatTurnState` ,也不压缩成布尔。
- 决策(状态条几何):卡片在 `.project-chat-conversation` 这条定高 flex 列里必须 `flex: 0 0 auto` 。它带 `overflow: hidden` ,按 flex 规范该项的自动最小尺寸归零,是这条链上唯一还能被压缩的项;压缩只能由消息列表吸收。同一选择器只保留一条规则(几何 + 不可压缩),不留两份。
- 决策(对话 Markdown 对模型输出的容错):解析前先 `normalizeMarkdownFences` 再压缩空行;代码块 `pre` 与块内 `code` 各自都给 `whitespace-pre-wrap` + `break-words` 。细则与判据见 `pitfalls.md` 同日两条。
- 影响面:`apps/ai-game-creator-shell/src/{styles.css,components/ChatMarkdownMessage/index.tsx,view/project-development/chat/{DirectProjectChatView.tsx,components/DirectProjectConversation/DirectProjectConversation.tsx,controller/useDirectProjectTurnStatus.ts}}` ;用例 `tests/{ChatMarkdownMessage.test.tsx,directProjectProcessStatus.test.tsx,appSurface/{chat-composer.suite.ts,project-development.suite.ts}}` 。
- 验证:`npx vitest run apps/ai-game-creator-shell/tests` 186 passed / 1 skipped( 1874 条里 1860 passed / 14 skipped);真实 Chromium 夹具复核读秒 100ms、窗口 900→220px 高度下卡片恒为 36px 不被裁、代码块换行与两类粘住围栏;变异验证(读秒改回 1000ms、删 `flex: 0 0 auto` 、卡片退回 `nativeRunning` 、停掉围栏归一化、换行类名退回)逐条变红。
## 策划 V1/V2 退役的现行边界
- 旧策划 V1 和 Runtime V2 均已删除,当前策划入口统一使用独立 Design Agent。V1 被 V2 接替只描述历史过程,不表示 V2 仍在使用。
- 下文旧策划版本的阶段审批、`plan.submit_gdd` 、planning session binding、exact planning lifecycle v3、专属身份白名单、IPC 和测试约束均为历史记录,不能作为恢复代码或保留孤立实现的理由。不新增旧版本兼容别名、双跑或回退链路。
- 通用项目锁、权限、持久化和当前 Design Agent 能力按实际调用保留;清理未用参数不扩大为删除调用方的持锁范围或锁归属校验。
- 当前事实源:[策划 Agent 生产迁移与工作区浏览 ](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md )。
## 2026-09-23 运行视窗:右下角全屏预览 + 没有内容就自动收起的信息栏
- 背景:运行页右下角缺一个把游戏画面放大到整屏的入口;运行视窗下方常驻「信息展示 / 数值微调」两张卡片,没有选中资源时就是两块空白,验收现场提出「没有功能就暂时隐藏」。
@@ -924,7 +1057,7 @@ Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和
- 决策:Direct 过程卡顶部标题只由 `GameCreatorDirectTurnUpdateStatus` 决定(accepted=需求已接收 / running=任务执行中 / streaming=回复生成中 / finalizing=结果整理中 / completed=回复已生成 / failed=处理失败),小字只展示当前正在执行的具体内容并统一加“正在”前缀;真实回复增量(AccumulatedText)才标记 streaming,计划、推理、工具输出与 Activity 一律 running。生成中的累计回复直接作为 assistant 消息气泡在会话列表中原位更新,不再拼进过程卡;进入 finalizing / completed 时保留完整累计回复直到正式消息接管,失败时清除未完成正文。移除合成打字机回放;工具说明/中间文本不再触发 streaming。计划/推理通知收敛为 `preparing` 活动并在界面显示“正在思考中”,原始推理/计划正文不进入 UI,思考期的心跳按 1.2s 限流。命令/文件/工具执行细节与回复流解耦,`stream=false` 时仍展示在过程卡;MCP 工具按用户语义显示(例如 `agc_write_file` 为“正在写入文件:<项目相对路径>”、图片/素材/搜索/试玩分别显示生成、导入、搜索、试玩等动作),未知工具只显示“正在调用工具”不暴露内部工具名;命令显示“正在执行命令:<命令>”,验证类命令显示“正在验证游戏:<命令>”。同一活动后续无正文的心跳不得用通用文案覆盖已展示的具体工作。展开/收起是同一 `project + clientTurnId` 内的持久状态,内容更新不重置,切换新回合才收起;展开详情的滚动条轨道和角落保持透明。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs` 的 DirectProject observer、`apps/ai-game-creator-shell/src/App.tsx` 的事件投影、`ProjectSupervisorView` 过程卡渲染与对应 AppSurface 回归。
- 验证方式:Rust 单测证明只有开启流式时的 AccumulatedText 是 streaming、preparing 通知只产生 thinking 活动词且不携带原始推理文本、执行细节在 `stream=false` 时仍保留,并覆盖全部 AGC MCP 工具语义、未知工具不泄漏、绝对路径 / 上跳路径不展示;AppSurface 覆盖接受态、preparing 显示“正在思考中”、running 长文本展开、command-exec 与写文件心跳不覆盖具体工作、streaming 正文进入 assistant 气泡且过程卡只显示阶段、同一回合后续 running 不覆盖正文也不收起、失败后清除未完成正文、正式消息接管不重复;样式核对确认展开详情的滚动条轨道与角落透明;AGC typecheck、全量 appSurface、rustfmt、`npm run check:encoding` 、`git diff --check` 通过。
- 关联文档 : `docs/technical/【技术方案】Direct回合行为审计账本-2026-08-31.md` 、分支 `feat/agc-llm-router-official-chain` 。
- 当前行为依据 : `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` ;旧 `docs/technical/【技术方案】Direct回合行为审计账本-2026-08-31.md` 仅用于追溯,不承诺继续生成平行日志 。
---
@@ -959,20 +1092,19 @@ Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和
---
## 2026-08-31 Direct 回合把 Codex item 落成有界行为账本
## DirectProject 平行审计与请求分段计时退役边界(2026-09-23 核准)
- 背景:sidecar 已让模型看见本轮附件路径,但 native 读 / MCP / 写文件只存在于隔离 `CODEX_HOME` 的瞬时 stdout,回合结束即删。无法判断「没读附件」还是「读了仍走默认收集类」 。
- 决策:GUI DirectProject 每个 `clientTurnId` 追加 `.agent/runtime/direct-codex/turns/<id>.jsonl` ,并在 `agent.db` 写一条 `direct.codex.turn` 摘要。记 sidecar 提供的路径与文件 hash、 `item/completed` 的 Read/List/Search/MCP/写文件(不含 stdout、patch、MCP result),以及 `offeredRead` / `firstDesign` 。审计 fail-open,不阻断做游戏。Home、CLI、Supervisor 收据模型不接。不灌附件正文,不强制读取,不为 GDD 开特例 。
- 影响范围: `direct_codex_audit.rs` 、Direct GUI command 边界、Codex collect 循环;前端 / jsonl 气泡 / sidecar 文案不变 。
- 验证方式:Rust fixture 覆盖 turn_start hash、绝对路径相对化、stdout/diff 不落盘、art brief 保留、list/search 不算已读、firstDesign 顺序、256 条截断、写盘失败不 panic; sidecar 渲染与 Direct 活动词测试保持通过 。
- 关联文档:`docs/technical/【技术方案】Direct回合行为审计账本-2026-08-31.md` 、issue #212 。
- 当前合同:完整用户与 Codex 完成 item 保存在 `.agent/conversations/project.jsonl` ; GUI 回合不再创建 `runtime/direct-codex/turns/<id>.jsonl` 或对应 `agent.db` 的 `direct.codex.turn` 摘要。旧 `offeredRead` / `firstDesign` 和请求分段计时不再属于生产保证,旧审计专题仅作历史追溯 。
- 实现边界:旧 `DirectCodexTurnAudit` 、 `DirectTurnMetrics` 、可选审计 / 计时参数及专用批量 JSONL 追加包装已删除。Provider proxy 本体及独立 model-usage observer 仍有现役用途,不能随旧计时链退役;字节流透传和上游错误传递继续由现有测试验证 。
- 保留边界:运行中的界面对话 / 工具耗时、 `.agent/model-usage.jsonl` 、产品埋点及 Runtime Agent 审计保持各自合同。 `project.jsonl` 的完成 item 写入时间不等于 turn 起止或请求阶段计时;不据此补造旧历史耗时。本次不清理或迁移用户项目内的旧审计文件 。
- 维护依据:AGC 实施计划“Direct 历史、审计与耗时的现行边界”和“DirectProject Codex 原始历史与异常恢复”。不能以原审计方案或已退役测试为由恢复旧 writer 。
## 2026-08-31 Direct 本轮附件只映射路径,不灌正文、不区别 GDD
- 背景:issue #212 。首页附件已经复制到 `assets/uploads/` 并登记,但 Direct 首轮只把用户原文发给 Codex,原文件名不是磁盘路径,模型会另起一套玩法。
- 决策:Home 与 Project 共用 `DirectCodexTurnAttachment` 。有项目路径或导入状态时,只在发给 Codex 的 user prompt 末尾附有界 sidecar(原名 → 项目相对路径、类型、大小、状态);无路径且无状态时保持首页元数据文案 。不灌正 文、不强制读取、不按 GDD 开特例。做成游戏固定 prompt 不改,同一条 Direct 首轮附件链自动吃到 sidecar。jsonl 与工作台气泡仍只写用户原文 。
- 影响范围:`direct_codex_attachments.rs` 、Direct command 边界、首页建项 latch、工作台首轮 invoke; Supervisor / 做方案首轮忽略附件 sidecar 。
- 验证方式:Rust 渲染测试(Home 逐字兼容、Project 映射、非法路径);home.suite 附件 Direct invoke 含 `localPath` ;无附件不出现 `attachments` 键;做方案首轮仍走 Supervisor 且无 sidecar;后续手打消息不带 attachments。
- 当前决策(2026-09-23 更新):原 sidecar 已由 canonical `userItem.content` 中的 `agc_attachment_reference` 替代;每项保留名称、媒体类型、大小、项目相对路径和状态,经 validation/wire 校验投影 。不灌全 文、不强制读取、不按 GDD 开特例。未注册的 DirectHome 命令及其专属附件 DTO、渲染、prompt key 已清理,首页先创建项目再进入 DirectProject 。
- 影响范围:`direct_codex_attachments.rs` 只保留现役附件清洗与数量边界,canonical user-item 深模块、首页建项与项目工作台继续使用现行结构化输入;历史按主实施计划的完整 canonical 条目合同记录 。
- 验证边界:保留 canonical validation/wire、附件路径与状态投影及首页创建项目测试;退役 Home/sidecar 专属测试一并清理,不要求恢复旧独立 attachments 参数 。
- 关联文档:`docs/technical/【技术方案】DirectProject本轮附件路径映射-2026-08-31.md` 、issue #212 。
## 2026-08-26 运行中自主扩图提案留在编排层
@@ -2752,7 +2884,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
## 2026-06-22 编辑器生成扣费与新用户赠送收口
- 背景:画板多个生成按钮已经展示泥点消耗,但部分图片、图标、UI 提取、视频、角色动作或音频链路只校验 / 展示价格,没有统一进入钱包预扣;新用户注册送泥点也需要与当前生成价格匹配。
- 决策:编辑器所有外部生成入口不再从前端请求接收 `priceMudPoints` ,后端按运行时模型定价配置计算价格后统一进入 `execute_billable_asset_operation_with_cost` 或等价音频发布扣费链路;角色动作和视频使用真实登录用户作为扣费 owner。新用户注册赠送固定为 `100` 泥点。
- 决策:编辑器所有外部生成入口不再从前端请求接收 `priceMudPoints` ,后端按运行时模型定价配置计算价格后统一进入 `execute_billable_asset_operation_with_cost` 或等价音频发布扣费链路;角色动作和视频使用真实登录用户作为扣费 owner。~~ 新用户注册赠送固定为 `100` 泥点。~~( 2026-09-24 更正:注册赠送金额不是固定值,由线上 `profile_wallet_config.initial_mud_points` 配置决定,后台通过 `/admin/api/profile/wallet-config` 与钱包配置页随时调整;代码内常量仅为未写入配置时的兜底默认,本地编译行为不代表线上实际赠送金额,线上数值以配置表当前值为准。契约说明见 [ `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` ]( ../../【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md )。)
- 影响范围:编辑器图片 / 图片修改 / 图标 spritesheet / UI 提取 / 视频 / 角色动作 / 音频生成 BFF,前端画板生成提交模型,外部 OpenAPI,`module-runtime` 钱包注册奖励。
- 验证方式:运行编辑器图片、图标、UI 提取、视频、角色动作、音频扣费结构性测试,前端生成提交和 API client 测试,`module-runtime` 注册奖励测试。
- 关联文档:`docs/【编辑器】模型定价配置管理方案-2026-06-22.md` 、`docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md` 、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` 。
@@ -8445,7 +8577,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 恢复入口:通用恢复扫描与 Direct 回合启动前置恢复都必须发现 `resetting` 、`compensating` 和带替换锚点的 `in-progress` ,并在专用执行锁内清阶段、补偿和中性化。补偿恢复旧文件并清除本地 replacement CAS 锚点,但保留已 `prepared / accepted` 的阶段账本、原 `Idempotency-Key / operationId` ;同冻结意图续跑必须复用原请求身份,未知账本在文件 mutation 前失败关闭。冻结意图一致时,新进程 invocation 可接管未完成阶段;`completed` 以原始外层 `clientTurnId` 等值回放,不受模型 brief 重采样影响。App 在 Direct 调用前幂等持久化原始 User 消息与稳定回合 ID,Tauri 在成功返回及 `completed` 事件前以同一回合 ID 幂等持久化 assistant 终态,项目重开只续跑最近一条真正未回答的合法原始回合。
- 资源投影:工具返回主包路径、已登记切片路径、安全 `resources` 身份,并分开保留普通 warning 与 slice warning。标准核心图集首次创建和重生成都必须严格提交恰好四张 canonical 切片;alpha、可见像素、规范像素唯一、Canvas resource/asset identity 唯一任一不满足即失败。旧项目补登记与已有完整登记都必须由客户端私有回执交叉验证,不能把可编辑公开清单或顶层 manifest 中的自述身份单独升级为权威源;部分登记要么按私有回执事务补全,要么明确 warning。规范图只作 reference,不再计为运行态平台素材。
- 隐私投影:成功结果中的普通 warning 与 slice warning 也必须逐条经过宿主路径、凭据、URL 脱敏及长度限制,不能只保护错误分支。
- 权限边界:开放的是 `regenerate / registered resources / playtest` 等产品语义,不是原始最高权限。`regenerate` 只由当前请求最新一条原始 User 消息授权并绑定客户端稳定 `clientTurnId` ;模型参数、MCP 自动批准和缺失 clientTurnId 都失败关闭。授权输入先对完整原文做 Unicode NFKC 与撇号规范化,随后整串必须完整匹配审核过的独立立即执行指令,只允许句号/感叹号收尾;不得剥离引号、方括号或代码片段,动作前后也不得携带 brief、条件、否定、选择、确认、费用、延迟或其它文本。复杂风格需求先单独描述,再由下一条独立确认消息授权,不能用开放式 deny 词表推断付费同意 。同一进程重复水合相同 stable turn 时,“回合仍在运行”只作为非终态占用提示,不得以该 turn 的稳定 assistant messageId 持久化并覆盖原执行结果。DirectProject 的 cwd、sandbox writable root 与文件批准根只允许 canonical 且非 symlink/reparse point 的真实 `game/` , canonical 项目根的原生 OS 路径字节和 权威 manifest `projectId` 经域标签及独立长度前缀编码后共同绑定连接池与 thread 身份;项目根、 `assets/` 、 `.agent/` 不可写,网络关闭,命令、MCP 扩权和额外权限批准全部拒绝。受控 `agc_tools` 只在客户端内部从同一真实 `game/` cwd 反查已校验的 canonical 项目根,不把项目根加入 Codex writable roots 。Codex 不获得任意 Tauri invoke、Token/Key/Cookie; `resources` 也只投影稳定身份与相对路径,不返回 prompt、provider route、URL 或绝对路径。
- 权限边界:开放的是 `regenerate / registered resources / playtest` 等产品语义,不是原始最高权限。`regenerate` 的旧文本授权规则已由 2026-09-03 MCP 决策替代:Codex 根据当前用户请求显式选择工具模式,客户端不再用关键词、否定词表或独立确认句式判断业务意图;保留活动客户端回合、稳定 `clientTurnId` 、首次 brief 摘要、项目权限、计费、幂等、锁及未知结果恢复边界。2026-09-23 清理了旧文本判断残留;此处其余旧沙箱描述按主实施计划后续 DirectProject 完整访问合同覆盖 。同一进程重复水合相同 stable turn 时,“回合仍在运行”只作为非终态占用提示,不得以该 turn 的稳定 assistant messageId 持久化并覆盖原执行结果。DirectProject 的 cwd 与 AGC 项目身份根使用 canonical 项目根及 权威 manifest `projectId` ,进程 sandbox 和批准规则按主实施计划“DirectProject Codex 完整访问覆盖”;不再沿用此旧决策中的 game/ 唯一可写根、关闭网络或拒绝全部命令的描述 。Codex 不获得任意 Tauri invoke、Token/Key/Cookie; `resources` 也只投影稳定身份与相对路径,不返回 prompt、provider route、URL 或绝对路径。
- Direct 恢复 claim:同一 App 实例重复水合相同 stable turn 并收到“仍在运行”时,必须释放该 `projectPath + clientTurnId` 的恢复 claim,且不得写稳定 assistant 终态。后续显式刷新对话可按原身份重新读取或续跑;不新增无界自动重试。
- 严格图集崩溃收口:workflow 在严格图集调用前先持久化 `strictSpritesheetPending` 并冻结底层严格事务覆盖的九项旧合同身份;旧路径可精确冻结为缺失。Provider 完成结果先绑定原 retained stage ledger。恢复在同一项目锁内对账严格事务;只有新九项合同、规范图/背景图替换锚点与 retained spritesheet result 三者一致才补写 `completed` ,旧九项合同才允许补偿。旧合同判定、写 `compensating` 、恢复两项素材与登记、回读和清锚点必须在同一项目锁内,重启已有 `compensating` 也重新判定;第三种混合、漂移或 foreign result 状态进入 reconciliation。不能在主图集与四切片已整体提交后仍按两文件 rollback 制造混合包;若中断前阶段告警尚未进入 durable completed result,恢复结果追加“原阶段告警无法完整重放”的明确 warning,不静默清空。
- Direct 对话恢复从新到旧扫描全部合法 User 回合,遇到较新已回答回合继续向前,不得丢失更早未回答回合。成功返回时 Rust 已先持久化 assistant,前端冗余 append 失败也不得重跑 Provider;普通错误终态的显式 append 失败后,恢复 claim 必须保持到 React fallback writer 对同一稳定 assistant messageId 的写入明确成功或失败,不能在 writer 尚在途时按旧会话快照重跑。fallback 成功后释放 claim; fallback 失败时跳过该 writer 的无界迟到重试并释放 claim,后续显式重新加载对话才可复用原稳定 `clientTurnId` 。终态收敛后删除 claim,避免长会话无界增长。
@@ -9244,6 +9376,51 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 影响面:`apps/ai-game-creator-shell/src/features/{project-workspace/resourceReferences.ts,project-workspace/ResourceReferenceInput.tsx,resource-canvas/ResourceCanvasAssetGenerationPanelView.tsx,resource-canvas/resourceCanvasAssetGenerationTaskModel.ts,resource-canvas/resourceCanvasAssetGenerationReferenceModel.ts}` 、`apps/ai-game-creator-shell/src/view/project-development/index.tsx` 与对应 6 个定向测试文件。
- 验证:定向 `resourceCanvasAssetGenerationReferences` / `resourceCanvasAssetGenerationBackgroundClose` / `resourceCanvasBottomToolbar` / `resourceCanvasGenerationFloatingPanel(Chrome)` / `resourceReferenceInput` / `resourceReferences` / `resourceCanvasAssetGenerationTasksPanel` 全绿;全量 `npm run test -- apps/ai-game-creator-shell/tests` 只剩 `clientHttp` / `clientApi` / `clientAuthStorage` / `projectCreationDirectory` / `recentProjectsHook` 五个 jsdom `localStorage` 环境用例红(与本次改动无调用关系);TS typecheck、`check:encoding` 、`git diff --check` 通过。未复核真实客户端观感。
## 2026-09-18 UI 编辑器深模块 seam 收敛
- 决策:UI 编辑器的语义状态写入通过 React-free `stateTransition` seam;页面 hook 继续负责 React/history/lock adapter。保存前增加 `stateInvariants` projection, Rust 持久化规则仍是最终权威。
- 决策:节点几何新增 State 级 `findStateNodePageContext` ,页面选择、预览和状态迁移共享同一坐标递归入口;四类异步操作的 running/status 由 `operationLifecycle` adapter 承接。
- 决策:结构化 LLM action 共用 `commands::utils::required_tool_arguments` ,仅统一必需 tool-call 定位与有界 JSON 解析,不合并 prompt、schema 或 materializer。
- 验证:AGC typecheck、UI State 定向 Vitest、编码检查、doc-index、diff 检查和 Tauri Rust fmt 通过;Tauri 全量 cargo check 仍受现有 platform-llm API 漂移错误阻断,与本次 UI editor 改动无关。
## 2026-09-23 UI 编辑器退役界面图参考语义建议
- 背景:UI 编辑器的“分析参考图”步骤只用一次 LLM 调用给界面图补 `name` / `description` / `role` / `slave_to` ,四个字段又反过来决定结构识别的上下文分组、合并的树优先级和 Inspector 的可选项;这条链路的价值不足以支撑它引入的跨层耦合。
- 决策:`commands/ui_design_suggestion.rs` 、`suggest_ui_design_semantic` 命令与 `UIDesignImage.metadata` (含 `UIDesignImageRole` 、`UIDesignImageMetadata` )整体退役,`UIDesignImage` 只剩 `path` / `pixel_size` / `pixels_per_unit` ;前端从三步工作流收敛为“识别界面结构 / 自动切分素材”两步,`model.ts` 、`WorkflowActionCard` 、`WorkflowChecks` 、完成通知、`InputSidebar` 、`InspectorSidebar` 与 `ImportOverview` 同步删减。不保留兼容字段、回退路径、旧文档迁移或写回。
- 决策:界面图之间不再有持久化关系,结构识别按“每张界面图各自一棵树、各自一个上下文”执行(删掉 `recognition_root_image_ids` / `slave_image_ids` );界面图显示名统一取 `path` 的 basename(复用 `view/project-development/resourceAssetDisplayName.ts` )。
- 决策:多树合并暂时没有优先级来源(原优先级由 `slave_to` 祖先链计数得出),`merge` 现在把所有输入树优先级恒置 0 并留 `TODO` ,合并冲突取 `merged_from` 首位成员;`ui-workflow.*` 阶段与页面级工作流不在本次范围,后续整体重写。
- 代价与取舍:删掉 `name` / `description` 后界面图在 UI 上只能用文件名标识;合并冲突的代表节点选择不再有“优先级”依据;`role` / `slave_to` 曾承担的“主页面 + 子界面”语义彻底消失。旧 `ui_design.json` 里的 `metadata` 由 serde 默认忽略、下次保存后自然消失(`State` / `UIDesignImage` 都没有 `deny_unknown_fields` ),已生成的 `ui_trees` 不受影响。
- 影响面:`apps/ai-game-creator-shell/src-tauri/src/{main.rs,ui_editor/**}` 、`src/features/ui-editor/**` 、`src/view/ui-editor/**` 、`src/view/project-development/index.tsx` 、`tests/{uiEditorPage,uiEditorState,previewWorkspaceZoom}.test.*` 、`docs/technical/【技术方案】UI编辑器代码地图与模块职责-2026-09-23.md` 、`docs/【技术方案】UI工作流资源桥接与Runtime执行-2026-08-24.md` 、`docs/technical/【设计】UI编辑器工作流完成通知弹窗-2026-09-04.md` 、`docs/technical/【前端架构】UI编辑会话模块边界-2026-08-19.md` 。
- 验证:`cargo check` 与 `cargo test --bin genarrative-ai-game-creator-shell ui_editor` ( 160 passed)通过,ts-rs 重新导出 `types/UIDesignImage.ts` 并删除三个已退役类型;`npx vitest run` 定向 `uiEditorPage` / `uiEditorState` / `uiDesignStateStore` / `previewWorkspaceZoom` / `appSurface` 全绿;AGC `tsc --noEmit` 、改动文件 eslint、`cargo fmt --check` 、`check:encoding` 、`git diff --check` 通过。整套 Rust 测试在本容器仍有 60 条环境性失败(`/sbin -> usr/bin` 让 `command.exec` 沙箱 merged-usr 预检失败),与本次改动无关。
## 2026-09-24 UI 设计文档三个工具的持久回执明细与入参摘要改为可读白名单
- 背景:动作回执的"安全明细"是一份白名单——只有认识的工具才把明细整理成安全字段。`ui.workflow.run` 退役时删掉了它那段分支,接管的 `ui-design-doc.*` 三个工具没补,于是统一落到兜底:明细整块变成 `detailUnavailable` ,模型与审计都看不到;入参那一栏也只剩哈希。原始明细既不落盘也不给模型,所以**没有宿主路径泄露**,丢的是可用性(例如切分没登记上的素材清单)。
- 决策:按既有白名单口径给三个工具各补一条分支。`from-images` 放 `assetId` / `relativePath` / `imageIds` / `revisionAdvanceCount` ; `run-workflow` 再放文档 `revision` 、恢复标记、识别与绑定计数、`backfillErrors` 与总数;`into-js` 只放相对路径与计数(导出名清单只用来核对数量,不逐项外传)。
- 决策(校验口径):身份字段走 `agent_runtime_action_receipt_identity_text` (禁控制字符、限长、含 file URI 或绝对路径即失败关闭);相对路径必须归一化后落在 `ui/` 下;回填说明是自由文本,按既有口径把绝对路径脱敏成占位符,含控制字符或超长则整条明细失败关闭。
- 决策(体积):回执明细总长仍受 `AGENT_RUNTIME_ACTION_RECEIPT_SAFE_DETAIL_MAX_CHARS` (500)约束。设计图身份与回填说明按长度上限能放多少放多少,放不下的部分用 `backfillErrorCount` 表达总数;必需字段本身就超限时整条明细不可用。
- 决策(入参摘要):`agent_runtime_tool_action_input_summary` 为三个工具产出可读摘要(设计图逐张身份或目标文档 id),并把工具名加进 `agent_runtime_public_action_input_summary` 的可读名单,不再退化成只报哈希。
- 验证方式:`tests::runtime_actions::action_execution::ui_design_doc_receipts_*` 两条用例(三个工具都能留下可用明细且不超长;宿主路径被脱敏;`ui/` 之外与宿主路径身份失败关闭)与 `action_audit::ui_design_doc_public_input_summary_tests` 两条用例;`cargo test --bin genarrative-ai-game-creator-shell receipt` 55 条全绿。
## 2026-09-24 UI 设计文档三个工具纳入项目变更门禁(成功返回即算改过项目)
- 背景:`ui.workflow.run` 退役时,项目变更门禁的两处工具名单(`agent/runtime_actions/project_gates.rs` 的 `is_agent_runtime_project_mutation_observation` 与 `agent_runtime_observation_advances_project_revision` )只删未补,新接管的 `ui-design-doc.from-images` / `ui-design-doc.run-workflow` / `ui-design-doc.into-js` 都没登记。后果:改完项目可能被判定"没改过",于是不要求验证就判完成、自动模式的 liveness 判据看不到进展;`agent_runtime_pending_expected_project_revision` 少算推进量又会误报 `pending_project_revision_drift` ("并行项目变更使旧动作过期")。
- 口径(产品确认):三个工具都在**成功返回时**改项目——`from-images` 登记设计图与文档、`run-workflow` 登记切图并保存文档、`into-js` 重写 `ui/generated-*.js` ;调用中途不产生需要门禁额外追踪的中间态。
- 决策:门禁按"工具名 + 成功返回"判定三者都算项目变更;其中 `into-js` 只重写派生产物、**不推进 revision**,所以不进 revision 推进名单——进去会虚报推进量,正好把要修的误报再造出来。
- 决策(真实推进量):`from-images` / `run-workflow` 的观察明细带上 `revisionAdvanceCount` (沿用 `canvas.asset_import` 的既有字段),失败路径也带——切图素材先登记、后面步骤才失败时按约定不回滚,仍要如实计数,避免门禁把真实推进当成"别处改动"。
- 代价与取舍:失败路径要多读一次项目 revision;`into-js` 属于"改了东西但项目 revision 没动"的少数派,与写文件类工具口径一致。
- 验证方式:新增 `project_gates::ui_design_doc_project_mutation_gate_tests` 六条用例(成功即算变更、`into-js` 不推进 revision、登记类兜底推进量为 1、明细里的真实推进量优先、失败但已推进才算、无关工具不受影响)。
## 2026-09-23 UI 编辑器 Agent 工具化重写
- 背景:UI 编辑器的 Agent 链路原本只有一个 `ui.workflow.run` ,把发现页面、桥接设计图、结构识别、多树合并、组件绑定、finalize 全塞进一个工具,工具参数本身就是工作流状态;识别与切分的产物由前端 `useUiEditorPage.ts` 落 State 再保存,Agent 侧没有任何恢复点,任一步失败只能整轮重来。
- 决策:拆成三个各自只做一件事的工具——`ui-design-doc.from-images` (一至四张设计图新建并登记文档,返回 `assetId` 与 `relativePath` )、`ui-design-doc.run-workflow` ( `recognize → separate → write-back` )、`ui-design-doc.into-js` (渲染 `ui/generated-<stem>-<digest>.js` ,不推进 revision)。只有 `run-workflow` 带崩溃恢复,粒度到子步骤。
- 决策(检查点):恢复判据只有「这一步有没有对应、且带着 State 快照的检查点行」。检查点是文档旁追加式 JSONL `ui/.<文档名>-workflow.jsonl` (不进 manifest、不推进 revision),行类型 `run` / `recognize` / `separate` / `write-back` / `outdated` ; `run` 行带这一轮开始时的 State 快照,`recognize` / `separate` 行带该步应用完之后的 State 快照。恢复只读快照:逐级取回已完成步骤留下的 State,只把新完成那一步的改动应用到文档,已完成步骤整步跳过、绝不照 DTO 重跑(重跑会重复登记切图、把同一条回填出错原因重复报一遍)。只有 DTO、没有 State 快照的旧行按未完成处理,由主流程重跑该步。一轮以 `run` 开头、以首个 `write-back` 或 `outdated` 结束,只有最后一轮没有结束行时才恢复。追加前截断崩溃留下的半行;文档中途漂移(当前 State 既不是 `run` 行快照、也不是切分后那份快照)时追加 `outdated` 并返回错误,由下一次调用显式开新一轮,不在同一次调用里自动重启;写回按「切分后那份 State 快照与文档当前 State 相等」判幂等并补 `write-back` 行。
- 决策(边界):文档内设计图身份直接采用该图在 manifest 里的 `assetId` ,输入给相对路径时先登记再用它的 `assetId` ;每次调用都新建文档,不做「原型 → 已存在文档」的幂等查找。切图资源失败不回滚,重放靠 by-path 复用接上;切分 op 内部更细粒度的恢复仍由 `SeparationState` sidecar 承担,日志不复制它的进度。前端 `useUiEditorPage.ts` 的人工链路保留同语义,Rust 只是第二份实现,不把编排搬进 Rust。
- 退役:`ui.workflow.run` 、`ui_editor/commands/{merge.rs,binding.rs}` 、`ui_editor/workflow.rs` 、`ensure_ui_design_resource_for_prototype` 、`ui/ui-workflow-<sha256前24>.json` 命名与 `ui-workflow.*` manifest 阶段全部删除,不保留迁移、兼容与 fallback。
- 代价与取舍:不接受跨语言 fixture 比对(四个 seam 都是简单变换,靠同语义实现与各自单测覆盖);`run-workflow` 每次调用都推进一轮,调用方重复调用会重新识别而不是被幂等短路(除「保存成功但缺 `write-back` 行」这一种重放)。工具名 `ui-design-doc.*` 含连字符,Function Calling 的函数名归一同时处理 `.` 与 `-` 。
- 验证方式:`cargo test --bin genarrative-ai-game-creator-shell agent_tools` 覆盖检查点、识别/切分镜像与切图登记;`ui_design_doc` 与 `native_ui_design_doc_tools` 用例覆盖工具入参和函数名;`npm run check:encoding` 、`npm run check:doc-index` 、`git diff --check` 通过。整套 Rust 用例在本容器仍有 9 条既有环境性失败(本地 HTTP 资源编辑器与 LLM 超时,改动前同样失败)。
## 2026-09-22 运行页收口:过程提示退出对话区,顶栏统一承载运行入口
- 背景:点播放(以及历史上 `/preview` 、`/open-preview` 、生成后自动启动预览)都会往对话区写一条 assistant 提示(`运行通过,已载入客户端运行视图:http://127.0.0.1:63155/` 这类)。它常驻对话底部遮挡运行画面,也让对话区混进非对话内容;运行页本身还有三处遮挡与两套皮:预览地址是一行常驻小字(不可点)、版本入口是绝对定位压在画面右上角的浮层、右上角还叠着「生成任务」开关。
@@ -9273,14 +9450,52 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 边界(A 仍未修):`DirectProjectTurnUsage` 的 `Math.max(turn.endedAt, turn.startedAt)` 兜底没动,所以两类 `finished` 回合仍显示「耗时 0.0秒」——① 页面重进后读回来的历史回合(`turnEndedAt` 只是会话内展示缓存);② 发送后没有产生任何原生事件 / 发送失败的本地回合。为什么会有这两类、修法与要产品确认的口径都写在代码里(`DirectProjectTurn.tsx` 的 `DirectProjectTurnUsage` 注释与 `directTurnPresentation.ts` 的 `DirectChatTurnState` 注释),改完删掉那段注释。
- 验证:`tests/directProjectTurn.test.tsx` (新增 3 条渲染契约:`awaiting-start` 与 `running` 不显示终态文案且不折叠、`finished` 有终态时显示结束时间与耗时);`tests/appSurface/chat-composer.suite.ts` 新增 `does not report a finished turn while the host has not acknowledged the send yet` (invoke 挂起、无任何原生事件时断言不出现「本轮结束于」);变异验证:把 `state !== 'finished'` 退回 `state === 'running'` 后渲染契约用例变红,恢复即绿。定向 vitest、`appSurface.test.ts` ( 203 passed / 13 skipped)、`tsc` 、ESLint、Prettier、`check:encoding` 、`check:doc-index` 、`git diff --check` 通过。真实客户端观感未复核。
## 2026-09-22 宿主崩掉不再留下永远开着的回合:本地命令失败时按身份兜底收口
## 2026-09-22 失败回合的终态: `turn.completed` 带 `failure` 载荷 + 宿主 Drop 守卫兜底
- 背景:`turn.started` / `turn.completed` 是原生回合唯一的开闭配对,界面上的「正在处理」卡片与输入盒忙态都读 reducer 的 `turnRunning` 。但 app-server 崩了、回合任务被中止或 panic 时没人补终态事件,事件流里就留一条永远开着的 `turn.started` :界面一直显示「陶泥儿正在处理」、输入盒一直排队(用户现场反馈) 。
- 决策(本地命令返回即这一轮在宿主那边收场): `chat_with_game_creator_direct_codex` 以真失败返回时,controller 按本轮身份调用 `stopDirectThreadTurn` ,只放掉「是否在跑」,**不写终态时间**——命令返回不等于知道这一轮真正的结束时刻,编一个只会让耗时变成假数。用户主动终止与「正在跑的是另一轮」两条不适用:前者宿主必然补终态,后者不是这一轮(不能顺手抹掉别人的回合) 。
- 决策(身份作用域 + 不复活): `stopDirectThreadTurn` 只在 reducer 里的运行身份相同或为空时生效;收口记进 `commandClosedTurnUserItemId` ,同身份迟到的 `turn.started` 不再把这一轮拉回运行态(迟到的 `turn.completed` 例外放行,仍要拿它补上真正的结束时间)。身份按 clientTurnId 唯一,所以这条记忆只挡它自己那一轮。
- 影响面: `apps/ai-game-creator-shell/src/view/project-development/chat/{conversation/directThreadChat.ts,controller/useDirectThreadChatSubscription.ts,controller/useDirectProjectChatController.ts}` 与 `apps/ai-game-creator-shell/tests/{directThreadChat.test.ts,appSurface/chat-composer.suite.ts}` 。
- 验证:reducer 新增 2 条用例(兜底收口后同名 `turn.started` 不复活且真终态仍能补上结束时间;身份不同的回合不动),appSurface 新增 `stops claiming the turn is running when a failed send left turn.started open` ;变异验证:拿掉 controller 里的兜底收口调用后该用例变红(界面仍显示「陶泥儿正在处理」),恢复即绿。
- 边界(未做):根因仍在宿主侧——要在进程内保证开闭配对,应由 Rust 在回合函数退出(含 panic / 任务中止)时补一条终态事件(drop 守卫);本次只做到前端不再跟着说谎。另:兜底收口的回合没有终态时间,仍会落进「 `finished` 但拿不到终态时间」那个已知缺口(终态文案要不要藏,见 `DirectProjectTurn.tsx` 与 `DirectChatTurnState` 注释里的 A 项 )。
- 背景:宿主崩在 `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` ,只给界面选语气;**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` )补前端判据。(**已由 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` )。
## 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` 。真实客户端观感未复核。
## 2026-09-23 终态由事实判定:模型自报失败的原生错误投影进既有错误通道
- 背景:app-server 老实报了 `turn/completed{status:"failed", error:{message, additionalDetails, codexErrorInfo}}` ,界面却没有任何原因。两条通道同时哑:① 事件侧 `lifecycle_status` 拿收尾阶段当终态口径(执行适配器 `drain()` → `interrupt()` 把 ledger 推成 `Interrupted` ),把模型报的 `failed` 改写成 `interrupted` ,失败载荷因此永远产不出来;② 命令侧有执行许可时 `finish_model_attempt` 先返回交付报告,命令变成 Ok,运行错误横幅的前提也消失。原生 `turn.error` (带 `codexErrorInfo` 分类,前端 `projectRuntimeVisibleError` 有现成中文映射表)只在"没有执行许可"的 `Err` 分支里被读一次。
- 决策(终态由事实判定,有载荷必 `failed` ):`direct_turn_terminal` 去掉 `model_status` 入参,判定按「宿主当场记下的失败(通道断开 / 等待超时 / app-server 单方面中断)→ 本回合的错误结果是 Err → 只有收尾阶段账本读不出来时才用交付报告」取原因;**有载荷一定写 `status="failed"` **,没载荷才用收尾阶段推出来的 `status` 。收尾阶段的中断不再有终态否决权——这是本次修复的根因。
- 决策(原生失败用投影,不扩载荷、不加入参):`turn.error` 由既有的 `game_creator_codex_app_server_failed_turn_error` 投影成 `LlmError` ,在 `"failed"` 分支里作为本回合的错误结果返回,于是走已有的错误槽位产出 `{kind, message}` 载荷;`RepairRequired` (返修请求)保持原语义优先。前端零改动——`projectRuntimeVisibleError` 现有映射直接命中 `codex-app-server-error:<kind>` ;命令返回同时回到 Err,运行错误横幅恢复。
- 明确不做:不新增事件类型、不给失败载荷加字段、不改前端可见文案映射;不给回合路径补轻型假 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` 通过。真实客户端观感未复核。
## 2026-09-23 后台 Dashboard「消耗泥点」改为对冲退还后的净消耗
@@ -9337,3 +9552,66 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 背景:`docs/project-memory/todos/【待办】引用输入区后续收口-2026-09-24.md` 第 2 条曾把「`resource-reference-*` 类名重命名」列为待做的机械提交,理由是「与 `RESOURCE_REFERENCE_OVERLAY_SELECTOR` 同批改名,避免选择器与类名短暂不一致」。
- 审计:选择器常量正好是 `.resource-reference-picker, .resource-reference-menu` ,指向的就是这两个组件渲染的类;其余类名也逐条与归属组件同名(`ResourceReferenceInput` / `ResourceReferencePicker` / `ResourceReferenceChip` )。规模为 80 条规则(`styles.css` 78 + `resourceCanvasChrome.css` 2)、17 个文件、32 处测试断言。
- 决策:不改名。没有现役调用方、公开契约或样式隔离问题需要靠改名解决,纯改名只增加回归面;等出现新的命名口径时,再与选择器常量、样式表与断言一次性同批改。该条以「接受现状」关闭,其余两条(归一化撞名的候选菜单提示、真机手感验收)仍开放。
## 2026-09-23 游戏发行入口改为服务端按模板派生:管理员不再手填地址
- 背景:游戏审核通过要求管理员手填绝对 HTTPS `entryUrl` ,现场出现「不知道该填什么、随手填一个外部站点也能过校验」的风险;而每游戏独立来源本身完全能由 gameId 推出,人工输入没有增加任何判断。
- 决策(唯一口径):部署侧用 `GENARRATIVE_GAME_DISTRIBUTION_RELEASE_ENTRY_TEMPLATE` 配置带 `{gameId}` 占位符的模板(生产形如 `https://{gameId}.games.<发行域名>/` );审核通过时 `api-server` 读版本取 gameId、替换模板、再走原有绝对 HTTPS / 无凭据 / 无 query / 无 fragment 校验后写入公开投影。后台审核请求 DTO 删除 `entryUrl` ,页面不再渲染输入框,也不要求二次确认。
- 决策(失败关闭):模板缺 `{gameId}` 、生产未配置模板、gameId 含非主机安全字符或派生结果非法时,审核通过直接失败,不回落主站、内网或任意外部地址;非生产未配置模板时回落 `http://127.0.0.1:<bind_port>/api/game-distribution/releases/{gameId}/` ,保持免 TLS 的本地内嵌游玩验证。
- 边界:`entryUrl` 仍是公开投影字段,只是改由服务端写入;审核请求摘要不再包含它,表结构与版本回读不变;模板变更只影响之后新通过审核的版本,历史版本已冻结的 `entry_url` 不改写。
- 影响面:`server-rs/crates/api-server/src/{config.rs,modules/game_distribution.rs}` 、`apps/admin-web/src/{api/adminApiTypes.ts,api/adminApiClient.test.ts,pages/AdminGameDistributionReviewPage.tsx,pages/AdminGameDistributionReviewPage.test.tsx}` 、`scripts/check-game-distribution-media-e2e.mjs` 、`deploy/{nginx,env,container}` 、平台与运维主规范、发行里程碑实施计划。
- 验证:`cargo check -p api-server` 、`cargo test -p api-server game_distribution` ( 31 passed)、admin-web 定向 Vitest( 19 passed)与 `apps/admin-web` typecheck、`npm run check:release-origin-config` 、`npm run check:doc-index` 、`npm run check:encoding` 、`git diff --check` 全部通过;真实栈端到端(真实 OSS + SpacetimeDB + 审核通过)未在本轮复跑。
## 2026-09-24 游戏发行入口改为平台同源路径:取消发行域名与部署模板变量
- 背景:每游戏独立来源要求 `*.games.<域名>` 通配 DNS 与通配 TLS,一直未在任何环境落地,dev / release 审核通过直接报「发行来源未配置」;同时线上 SPA 白名单缺少 `games` 系列路由,`/games` 、`/games/detail` 、`/games/play` 在真实域名上全部 404。运行隔离实际由 iframe `sandbox="allow-scripts"` 的不透明来源承担,不需要独立 origin 兜底。
- 决策(唯一口径):发行入口固定为平台同源路径 `/games/{gameId}/` 。审核通过时 `api-server` 按 gameId 派生该相对路径写入公开投影,不再读取 `GENARRATIVE_GAME_DISTRIBUTION_RELEASE_ENTRY_TEMPLATE` ; `AppConfig` 字段与两份部署 env 示例一并删除。dev / release / 预览环境口径一致,不再需要发行域名、通配 DNS 或通配 TLS。
- 决策(边缘):`deploy/nginx/genarrative.conf` 、`deploy/nginx/genarrative-dev-http.conf` 、`deploy/container/nginx.conf` 三份模板内联同一条同源发行入口 location,把 `/games/<gameId>/` 与 `/games/<gameId>/<asset>` 转发到发行网关,转发前清空 `Cookie` ;正则整体必须加双引号,否则 `{32}` 会被 nginx 当块定界符。SPA allowlist 补齐 `components` 、`design-system` 、`games` 、`games/detail` 、`games/mine` 、`games/play` 、`games/publish` 。
- 决策(客户端):`normalizeGameEntryUrl` 接受相对路径与同源发行路径,按当前 origin 解析成绝对地址后交给 iframe;无尾斜杠会归一化补齐。同源非发行路径继续拒绝,非当前源的绝对 https 继续兼容历史数据。
- 决策(退役):删除 `deploy/nginx/genarrative-release-origin.conf` 、`scripts/check-release-origin-config.mjs` 与 `npm run check:release-origin-config` ;独立来源不再作为上线门禁。
- 影响面:`server-rs/crates/api-server/src/{config.rs,modules/game_distribution.rs}` 、`server-rs/crates/shared-contracts/src/game_distribution.rs` 、`packages/shared/src/contracts/gameDistribution.ts` 、`src/components/game-distribution/gameDistributionGuards.ts` (含新增测试)、`deploy/{nginx,container,env}` 、`scripts/check-game-distribution-media-e2e.mjs` 、`package.json` 、平台与运维主规范。
- 边界:SpacetimeDB 表结构与公开契约字段不变(`entryUrl` 仍是 string),只是取值从绝对 URL 变为相对路径;历史版本已冻结的绝对值不改写,admin 页与详情页展示口径不变。线上 dev / release 的 nginx 已按同源路径改动并 reload,`/etc/genarrative/api-server.env` 已删除模板变量;api-server 未重启,新写入要等下次重启。
- 验证:`cargo check -p api-server --tests` 、`cargo test -p api-server game_distribution` ( 27 passed)、`cargo fmt --all --check` 、`npx vitest run src/components/game-distribution` ( 57 passed)、`npm run check:nginx-spa-routes` 、`npm run check:encoding` ( 5060 文件)、`npm run check:doc-index` 、`git diff --check` 全部通过;三份 nginx 模板渲染后 `nginx -t` 语法通过;dev 线上实测 `/games/game_2dcd…4955/` 与 `./assets/index-2Ws3zHlS.js` 均 200。
## 2026-09-24 DirectProject 失败说明按回合身份归位:开口用户条目发点提前到接单之后
- 背景:用户在同一个项目里连发消息,每条都在**连接获取阶段**就失败(执行器版本未通过逐次审批协议验收),界面上"错误显示在用户消息上面",上一轮还顶替本轮显示耗时(现场 15.6 秒),本轮气泡自成一轮显示 0.0 秒;后面再发一条,说明落进更早的分区里,用户以为"这条没报错"。区分两个 `turn/start` :逻辑回合的 `turn.started` 由接单动作发出(成对、一定有);app-server 协议的 `turn/start` 请求在连接拿到之后才发。失败发生在后者之前。
- 根因:本轮的**开口用户条目**( `item_completed` ,身份 `direct-codex:{clientTurnId}:user` )原来在 app-server `turn/start` 应答之后才下发,于是"接单到 `turn/start` 之间"的失败没有用户条目可挂;前端 `buildDirectChatTurns` 按**条目顺序**分回合,失败说明只能落在上一轮末尾,而本轮的乐观气泡被排在所有正式条目之后 → 渲染成"错误在用户消息之上"。
- 决策(宿主):开口用户条目的**发点**提前到"接单成立、用户条目落盘成功、起 codex 之前"( `emit_direct_thread_user_item` ,调用点 `direct_runtime/user_input.rs` 的命令主体),删掉 `turn/start` 之后那一处;线上仍然只有一处下发,不变式变成 `接单 → 开口用户条目 → 整轮里其余一切` 。
- 决策(前端):回合归属只认**身份**——失败说明条目带 `turnUserItemId` ( reducer 写),`buildDirectChatTurns` 按开口条目身份分组(同一身份的条目永远同一轮),本地乐观气泡按身份挂回自己的回合而不是另开一轮;reducer 的收口早退只挡重复终态,不再吞掉"订阅重建只回放生命周期锚点"时那条还没写进界面的失败说明(`direct_thread_manager.rs` 的 `lifecycle_anchor` )。
- 影响面:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs` 、`.../agent/direct_runtime/user_input.rs` 、`.../chat/conversation/{directThreadChat.ts,directTurnPresentation.ts}` 。
- 边界:`project.jsonl` 里的用户条目依旧只在首屏 / 翻页时读进前端,本轮不改读取时机——开口条目的运行态下发 + 身份归位已经让"说明挂错回合"不成立。
- 验证:宿主 `cargo test --bins "agent::"` 、`the_opening_user_item_is_emitted_before_anything_that_can_fail_in_the_turn` 、`direct_project_turn_does_not_forward_codex_user_echo_as_chat_items` (补上同一发点);前端 `directTurnPresentation.test.ts` 的"本轮用户条目没到时,失败说明按身份挂回自己那一轮,本地气泡不再自成假回合"、`directThreadChat.test.ts` 的两条(身份字段、收口早退不吞说明)。
## 2026-09-24 DirectProject 删掉本地乐观用户气泡:用户气泡只来自宿主条目
- 背景:接单化之后,"接单窗口期"只服务本地乐观气泡(`awaiting-start` 展示态 + `pendingUserItemId` 身份)。上一轮把开口用户条目的发点提前到接单之后,"说明挂错回合"已不再需要气泡兜底;用户确认按"这条消息就像从来没存在过"处理,直接删干净。
- 决策(不造用户消息):删 `pendingUserItemId` / `beginTurnCommand` / `endTurnCommand` (忙态改由 `beginTurnBusy` / `endTurnBusy` 持有,宿主认领判据 = `turnRunning` 或收口计数变过)、权限确认重跑的 `messageAppended` 参数、`DirectProjectTurnInput.messageText` (含首轮 `directInitialTurnText` )、投影里的 `awaiting-start` 与本地用户气泡路径(展示态只剩 `running` / `finished` );controller 不再需要 `assets` 。
- 决策(时间口径):回合起点只认 `turn.started.at` 、终点只认 `turn.completed.at` ;用户气泡的时钟是宿主落盘 / 观测时间,不再有"本地更早的真实发送时刻"(`sameIdentitySentAt` 删除)。两边都拿不到(重进项目读回来的历史回合)时整条「本轮结束于 … 」隐藏,不再兜出 0.0 秒。
- 决策(本地说明):拒单提示带自己的身份(`…:rejected` ),投影据此在会话末尾自成一组,不挂进上一轮;壳层 `announce` (无身份)照旧挂当前回合末尾。带身份的本地说明不开运行态标记,避免把真正在跑的那一轮读成已结束。
- 代价(已知并接受):接单窗口与订阅重建窗口里聊天区没有这一轮的显示,反馈只有 composer 忙态、状态行与「陶泥儿正在处理」卡片(卡片这一段还读不出「已耗时」——起点是宿主的 `turn.started.at` ,开始事件到了才开始读秒);`project.jsonl` 用户条目依旧只在首屏 / 翻页读进前端。
- 影响面:`apps/ai-game-creator-shell/src/view/project-development/chat/conversation/{directTurnPresentation.ts,directCodexConversation.ts}` 、`.../chat/controller/{useDirectProjectChatController.ts,useDirectProjectTurnStatus.ts}` 、`.../chat/DirectProjectChatView.tsx` 、`.../chat/components/DirectProjectConversation/DirectProjectTurn.tsx` 、`src-tauri/src/agent/codex_app_server/mod.rs` (用户条目时间的注释口径)、对应 ADR 与实施计划。
- 验证:`npx vitest run tests/directTurnPresentation.test.ts tests/directProjectTurn.test.tsx tests/directProjectTurnStatus.test.ts tests/directThreadChat.test.ts tests/chatComposerAttachmentCap.test.tsx` 、`tests/appSurface.test.ts` ( 213 passed / 9 skipped)、`npx tsc -p tsconfig.json --noEmit` 、`eslint` 、`prettier --check` 、`npm run check:encoding` 、`git diff --check` 全绿。真实客户端观感未复核。
## 2026-09-24 AGC 模型目录初始值改为上游同步:不再回退写死的 gpt-6-astra/gpt-5.6-luna
- 背景:`agc_model_catalog` 缺行时 procedure 兜底返回内置目录(`quality → gpt-6-astra` 、`fast → gpt-5.6-luna` ),两个模型都已从上游移除;从未配置过目录的环境(新库、清库、本地调试)会把不存在的模型下发给客户端,选中后上游 `model_not_found` 。
- 决策(范围):本次只改目录初始值的来源,保持既有格式与契约不变 —— 目录字段仍是 `id` /`alias` /`modelId` /`defaultModelId` ,后台页面与 admin DTO、`GET /api/llm/models` 形状、客户端 `select_game_creator_model` 的标识校验都不动,因此没有不兼容变更。
- 决策(初始化):api-server(API/All 角色)启动时目录缺失、结构与当前定义不符或校验不通过即视为未初始化;此时请求上游 Router 控制面的分组定价列表 `GET {控制面}/api/pricing?group=taonier` (公开只读、不带凭据),按 `data[].model_name` 排序生成目录:`modelId` 与 `alias` 用上游原名(不再填“高质量/快速”),`id` 用模型名 slug(小写字母/数字/`-` /`_` ,同名冲突追加 `-2` ,因此客户端标识校验无需放宽),全部 enabled,默认项取排序后第一项,并按存量 revision 写回自增。
- 决策(来源选择,2026-09-24 实测后确定):不用管理面模型注册表 `/api/models/` (会带出已下线、没有路由绑定的 `gpt-6-astra` /`gpt-6-luna` ),也不用 `/v1/models` (要求 Router 用户 Key,用管理 token 实测 401)。当日 `group=taonier` 在售 6 个:`deepseek-flash` 、`deepseek-v4-pro` 、`glm-5.3` 、`glm-5.3-flash` 、`qwen-image-3.0` 、`qwen3.8-flash` 。
- 决策(失败关闭与重试):拉取失败、空列表、响应超 1 MiB、缺可解析 revision 或写回失败都只记录 error,不写替代目录;未初始化期间 `GET /api/llm/models` 、`/api/llm/responses` 、`/api/llm/chat/completions` 与后台 `GET/PUT /admin/api/agc-models` 返回 `503` “模型目录未初始化”;只在启动期尝试一次,下一次启动重试,直到目录里有数据。启动本身不因同步失败而失败,避免 Router 短时不可用放大成 api-server 起不来。
- 决策(幂等与并发):目录只取决于模型集合(排序后生成),重复同步结果一致;多实例并发启动只有一个写入成功,冲突方重读并校验既有目录可用性。目录只在未初始化时重建,上游变化不自动跟随。
- 决策(存量目录):结构合法的目录不会被自动重建,包括旧版写死的 `quality/fast` —— 需要 owner 在后台改掉,或清空该行后重启重新同步。
- 影响范围:`module-runtime` ( `from_upstream_models` + slug 生成,删除写死的 `Default` )、`spacetime-module` (缺行返回 `AGC_MODEL_CATALOG_NOT_INITIALIZED` )、`api-server` (启动期同步、上游请求硬化、只校验地址的目标校验、后台 PUT 未初始化门禁)、AGC 客户端(默认模型占位改为 `platform-default` )、AGC 模型弹层 CSS、主规范/后端契约/运维文档。
- 验证:`cargo test -p module-runtime --lib agc_models::` ( 4 passed)、`cargo test -p api-server --bin api-server agc` 与 `llm::` 、AGC 客户端 `configuration::` 、admin-web 页面定向 Vitest 与 typecheck、两套 workspace 的 `cargo fmt -- --check` 、`check:encoding` /`check:doc-index` /`check:spacetime-schema` /`git diff --check` 。
- 验证(真实上游 smoke,本地 dev DB):清空 `agc_model_catalog` 后启动 api-server → 日志 `已按上游模型列表初始化 AGC 模型目录 revision=1 model_count=6` ;登录后 `GET /api/llm/models` 返回同一批模型、`displayName` 即上游原名、默认项为排序后第一项;上游不可达/非 2xx 时启动只记录 error、AGC 接口 `503` 且目录保持未初始化;目录已存在时重启不重写。
- 边界(未验证/残留):上游在售模型超过 32 条时同步会失败(目录项上限未改);`qwen-image-3.0` 这类图像模型会一起进入目录,是否对 AGC 隐藏由 owner 在后台停用;混合版本期间未升级的 api-server 会把自己的 AGC 接口打到 `503` , module 与 api-server 必须同批发布/回滚。
## 2026-09-28 渲染层下沉分支与最新 master 对齐:活动回合事实源、维护态出口与诊断详情
- 背景:`codex/agc-renderer-io-downshift` 把 AGC 渲染层的网络与状态下沉 Rust 之后,要重新落到 master 已经演进出的新形状上。29 处冲突里 25 处是机械取舍,真正的分歧只有三处需要定口径。
- 决策(活动回合事实源唯一):master 把首页「运行中的项目」快照改成 Thread Manager 的 `active_turn_snapshots` ,本分支的活动回合事件挂在 `DIRECT_TAONIER_ACTIVE_INVOCATIONS` 上。对齐后只保留 Thread Manager 一份快照:`accept_direct_thread_turn` 、`update_direct_thread_active_turn` (仅内容变化时)、`complete_direct_thread_turn(_if_reserved)` 各广播一次 `game-creator-direct-active-turns-changed` , `direct_runtime` 不再导出第二份快照。理由:通知必须与快照同源,否则首页会把「不刷新」或「永不消失」表现成 bug。
- 决策(维护态判定归 Rust,渲染层只订阅):master 的维护弹窗建在渲染层 `clientApi` / `fetchClientHttp` 上,而本分支已删除该传输层。对齐后由 Rust 在平台请求的错误分支用 `platform_maintenance::watch_platform_response` (只有 503 且命中 `MAINTENANCE` / 「维护」)广播 `genarrative-client-maintenance-detected` ,渲染层 `subscribeClientMaintenanceEvent` 只负责打开唯一弹窗;同一批并发失败只弹一次。分类复用调用方本来就要读的响应体,不发额外请求,也不改写任何业务错误文案。
- 决策(跟随 master 退役诊断详情入口):master `5398a53e6` 明确把诊断引用移出用户可见文案并删除 `read_agent_runtime_error_detail` 与渲染层读取逻辑,本分支《AGC统一错误诊断与验收反馈》里程碑第 6 条因此被上游取代,实现与专属用例一并退役;失败说明继续由 `turn.completed.failure` 事件投影。
- 影响范围:AGC 客户端(`direct_thread_manager` 、`direct_runtime` 、`platform_maintenance` 、`llm_catalog` 、`account_api` 、`auth_session` 、`platform_asset_upload` 、`assets` 、`game_distribution_publish` 、`agent/generation/canvas_generation` 、渲染层 chat 控制器与视图、`tests/` )、nginx SPA 白名单、`.gitignore` 、mobile 检查脚本、capabilities 描述、文档与共享记忆。
- 验证:`cargo check` ( 0 error)、`cargo fmt -- --check` 、Rust 定向 `direct_thread_manager` 17 passed 与 `platform_maintenance` 1 passed、`npx vitest run tests/appSurface.test.ts` ( 201 passed / 9 skipped)与受影响套件、`npm run ai-game-creator-shell:check:web` 、根 `npm test` 、`npm run lint` 、`npm run check:encoding` 、`npm run check:doc-index` 、`git diff --check` 。
- 边界(未验证):真实维护窗口下的弹窗与并发失败表现、真实 Provider 下长回合的活动回合事件时序,都需要真机运行时证据;本轮只到源码级与定向用例级。