合并:把 origin/master 并入回合错误重构分支并保留双方行为

- 解决 direct_runtime/mod.rs 冲突:保留本分支 typed 回合错误重构(TurnError/EnqueueError 与 record_direct_codex_failure_facts 拆分),接入 master 的诊断 v3 口径(persist_agent_runtime_error 新签名 + typed error 字段)、prompt 反馈改截断不脱敏、项目写锁改名
- 解决 thread_manager/dispatch.rs 冲突:保留本分支 TurnError/TurnCompletion 换代与 finish_turn_failure 分类层,接住 master 的 tt= 日志改名与 cc 执行器整轮成功后补写 completed 终态
- 解决 direct_tools_mcp.rs 冲突:采用 master 版(本分支唯一改动是 external MCP 容量测试读 body,master 已有等价实现)
- 处理 direct_turn_failure.rs 删除/修改冲突:维持本分支删除,把 completed() 语义迁进 TurnCompletion::Completed 并补回对应测试
- 处理 runtime_driver/entrypoints.rs 修改/删除冲突:接受 master 退役自建 Agent Runtime 的删除(本分支只在该文件做 direct_now_ms 改名)
- 合并 decision-log.md:双方条目都保留
- 修复 auto-merge 漏掉的语义冲突:claude_code_cli.rs 的 direct_now_ms() 改名 now_ms();诊断测试改为断言 AGENT_RUNTIME_ERROR_SCHEMA_VERSION、detail 字段改 message;record_direct_codex_failure_facts 新增 typed_error 入参(TurnError 不序列化,落盘用 classify 投影出的 TurnFailure)
This commit is contained in:
2026-10-03 13:12:10 +08:00
372 changed files with 13835 additions and 241829 deletions
+2
View File
@@ -51,12 +51,14 @@
- [DirectProject 独立聊天容器与工作台钱包布局](./adr/【ADR】DirectProject独立聊天容器与工作台钱包布局-2026-09-18.md):DirectProject 与 Supervisor 等路径分容器,钱包入口由项目工作台布局独立承载。
- [UI 工作流检查点用追加式 JSONL 日志](./adr/【ADR】UI工作流检查点用追加式JSONL日志-2026-09-23.md):UI 设计文档的 Agent 工作流用文档旁追加式 JSONL 记录步骤完成,替代每步一个 sidecar 状态机。
- [退役 AGC 项目对话斜杠命令](./adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md):AGC 项目对话与终端 swarm chat 均不再解析斜杠命令,终端聊天入口一并退役;实现、测试、门禁与文档承诺全部删除,命令 id 与权限位作为项目策略词汇表保留。
- [退役 AGC 独立 Agent Runtime 与 CLI 执行面](./adr/【ADR】退役AGC独立Agent%20Runtime与CLI执行面-2026-10-02.md):自建 Runtime 内核、Tauri 命令、`--agent-*` CLI、真实 e2e harness 与 CI job 一并退役,Runner 只保留编辑器桥与 GUI owner;历史由 Git 保存。
- [引用候选由宿主注入](./adr/【ADR】引用候选由宿主注入-2026-09-22.md):引用输入区只接受宿主注入的引用 provider,素材选择面板独立成组件,附件芯片成为本轮附件唯一事实源。
- [DirectProject 命令接单化](./adr/【ADR】DirectProject命令接单化-2026-09-23.md):命令只负责接单、事件流回答整轮结果;拒单前置、失败后置。
- [DirectProject 命令接单化实施计划](./technical/【实施计划】DirectProject命令接单化-2026-09-23.md):四步落地顺序、每步不变式与验收;四步均已落地。
- [DirectProject 命令入队化与待发消息队列归宿主](./adr/【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md):命令只负责入队,放行归 Thread Manager;待发消息队列作为运行态事件归宿主、前端只投影;CLI 直连入口与调用身份守卫一并退役。
- [AGC 命令错误结构化与错误报告口径](./adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md):AGC 命令失败按具体变体建模并用 ts-rs 导出,前端按变体分流、不匹配文案;报告池只收没人处理的错误。
- [AGC 认证失败的 JS 侧载体与抛出时机](./adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md):认证命令统一经 `invokeClientAuth` 把拒绝装进 `ClientAuthErrorWrapper`(`error` 字段就是 ts-rs 生成的 `ClientAuthError` 判别联合),不新增手写错误类;判定只写在 catch 子句里,无字段变体用固定文案、带载荷分支先 `as` 取自己的具名载荷类型(可枚举细分再 `switch (payload.reason)` 在类型化枚举上分流),系统变体原样抛出经 `unhandledrejection` 入池,`default: expectNever` 编译期挡住漏接变体。
- [DirectProject 对话滚动与历史自动加载](./adr/【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md):删掉「显示更早的对话」按钮改为自动加载 + 内联加载/错误行,回合 key 冻结锚点前插不跳,底部居中「回到底部 / 有新回复」胶囊,折叠展开按跟随状态贴底或保锚点。
- [DirectProject 命令入队化与待发消息队列归宿主实施计划](./technical/【实施计划】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md):五步落地顺序、每步不变式与验收;待实施。
- [GameAgent 对话工具调用卡片](./technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md):把右侧对话里的执行命令 / 写文件投影成 Codex 风格可折叠卡片,含采集、独立历史文件、事件字段与回读契约。
- [DirectProject 客户端 Skill 与 MCP 扩展导入方案](./technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md):客户端扩展导入、按独立 Skill/MCP 拆分、命名、启用和启动时注入边界。
@@ -35,7 +35,7 @@ AGC 项目开发聊天框当前同时从多处取数据:Direct 回合事件(
- 执行通道断开同样是失败终态,也必须带 `failure`:连接级故障(app-server 进程退出 / stdout 流断 / JSON 行越界)与回合事件通道关闭都算,`kind="transport-failed"`、`message` 用宿主当场写下的那份诊断(含 `exitStatus` 与 stderr 摘要,已脱敏截断)。宿主在检测到连接终止的第一时间把这条事实记到本回合的执行适配器上,终态判定再从适配器读:执行适配器的看门狗盯着同一个 `closed` 标志,用调用点局部变量会输给这场调度竞争,失败原因就只剩日志、界面只会看到"本轮已结束"。判据是"适配器是否已由宿主主动关闭"——宿主自己收束(正常终态 / 用户主动停止 / 预算与交付收尾)走的是同一个 `TransportClosed` 事件,但这些不算失败。
- 终态由**事实**判定,不由收尾阶段反推:判定按优先级取「宿主当场记下的失败(通道断开 / 等待超时 / app-server 单方面中断)→ 本回合的错误结果是 Err → 只有收尾阶段的账本读不出来时才用交付报告」,**有载荷一定写 `status="failed"`**,没载荷才用收尾阶段推出来的 `status`。收尾会把 ledger 阶段推成 `Interrupted`,让阶段决定终态就会把已经失败的一轮讲成"已结束"。模型自报失败(原生 `turn/completed.status="failed"` 的 `error`,带 `codexErrorInfo` 分类)不为载荷新增输入字段:宿主把原生 `error` 的 `codexErrorInfo` 解析成 typed 分类后当作本回合的错误结果,走同一条通道进载荷;交付报告只说明"收束到哪一步",不得顶掉原因。
- 回合失败在宿主内部是 **typed** 的:`agent/direct_turn_error.rs` 的 `DirectTurnError` 每个变体自带字段(并发拒绝带两个 invocation id、模型失败带分类、超时带撞的是哪条上限、通道断开带宿主诊断),**调用级拒绝**(这一轮没有开始)与**回合级失败**(这一轮已开始并被判失败)不共用判据,分流只认 `is_turn_failure()`。判据不再对原因文本做子串匹配,`LlmError` 只在平台层入口出现一次(`DirectTurnError::from_model_call`)。线上载荷 `{kind, message}`、命令边界字符串与 CLI 返回值都由这一个出口投影出来,Rust 侧任何地方都不再解析它们。
- 分页锚点取原始条目 id;一次翻页操作在前端自动连拉,直到出现可显示条目或 `hasMore=false`,上限 5 页。
- 分页锚点取原始条目 id;一次翻页操作在前端自动连拉,直到出现可显示条目或 `hasMore=false`,上限 5 页。(**按 2026-10-02 ADR 补充**:更早历史的触发时机、加载指示、失败重试与视口锚定改由 [`【ADR】DirectProject对话滚动与历史自动加载-2026-10-02`](./【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md) 规定;本条的分页锚点口径与连拉上限仍然有效。)
- `notify` 是唯一唤醒来源:`subscribe` 的 bootstrap 事件本身就是该 subscriber 此刻要处理的事件(游标已在队尾),前端直接 reduce 它们,不需要为了取这批事件再补一次 `consume`,之后完全由 `notify` 驱动,不设低频 tick 或任何轮询兜底。唯一例外是回执竞态:Rust 侧一注册完 subscriber 就开始 `notify`,前端却要等回执才知道自己的 `subscriptionId`,这段窗口内的通知只能记成欠账,回执到达后立刻补一次 `consume` 取回,否则该回合的尾部事件会卡在队列里等一个可能永不出现的下一次通知。
- 迁移按一次干净切换落地:不做灰度、不做运行时开关、不双跑;允许提交序列里存在「新源已启用、旧代码尚未删除」的中间窗口,禁止反向的「新源未启用、旧源已删」。
- 思考过程与工具活动同样从运行态事件与历史条目推断,界面展示保持不变。
@@ -0,0 +1,85 @@
# 【ADR】DirectProject对话滚动与历史自动加载-2026-10-02
状态:已接受
## 背景
DirectProject 聊天区(`apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/`)当前的滚动与历史体验有三个问题:
1. 更早历史要靠常驻按钮「显示更早的对话」(`.message-history-more`)手动拉,但 `handleScroll` 里已经有一条「滚到顶部 24px 以内就自动加载」的路径——按钮是半冗余的第二入口,还没有任何加载中的视觉反馈(`historyLoadingRef` 是 ref,渲染不出来)。
2. 用户滚上去之后没有任何「回到最新」的入口,只能自己一直滚到底。
3. 展开一折叠块(`<details>` 或 `[aria-expanded]`)时浏览器保持 `scrollTop` 不变,新展开的正文长在视口下方,用户必须再滚一次才能看到;反过来在底部展开时内容直接顶出可视区。
同一目录下的 `PlanningChatView` 复用 `.project-chat-message-list` / `.project-chat-conversation` 两个类名,所以任何滚动容器结构改动都必须留在 DirectProject 本地,不能改公共类。
## 决策
### 1. 更早历史:删掉按钮,自动加载 + 内联加载行
- 删除「显示更早的对话」按钮与 `DirectProjectConversation` 里的条件渲染;`.message-history-more` 样式**保留**——策划对话(`PlanningChatView`)仍在用同一套类名与按钮,等它一起改造时再删(见「影响」的后续项)。
- 触发一(滚动):`scrollTop <= 24` 且 `historyHasMore` 且不在加载中且没有失败记录时自动加载。
- 触发二(填充视口):首帧之后内容填不满视口(`scrollHeight <= clientHeight`)时继续加载,直到填满或 `hasMore=false`;不允许出现「历史比视口短、又没有按钮」的死局。
- 两个触发都不越过既有的首屏订阅锚点 `lastCompletedItemId`;一次加载仍最多连拉 5 页(口径见 [`【ADR】DirectProject对话历史单一事实源-2026-09-16`](./【ADR】DirectProject对话历史单一事实源-2026-09-16.md))。
- 加载中在列表最上方(比最旧一条回合更靠上)挂载一行 `role="status"`、`aria-live="polite"` 的「正在加载更早的对话」,带旋转圈;延迟 150ms 才显示,加载结束即卸载。它按需挂载,靠位置补偿(见第 2 条)保证下面的消息不跳。
- 失败:挂起自动加载,列表顶部保留一行内联错误行——`role="alert"` 只包住「加载更早对话失败」文案本身,重试是可聚焦按钮、留在 live region 之外(assertive + atomic 的 live region 里不放交互控件);**不自动重试**,只有点重试(或切换项目)才重新开始;重试成功后错误行消失。
- 一次加载与它所属的**世代**绑定:切换项目或新起一次读取都推进世代号(`historyLoadTokenRef`),旧世代落地时整段失效——不合并条目、不写游标、不关加载态。只比项目路径不够:A→B→A 之后在飞的旧读取又落回同一个路径,原守卫放行,会把新一代的加载行与 `historyLoadingRef` 这道并发闸门一起改掉。换项目的推进放在**渲染期**(与 `projectPathRef` 同一处),不放在复位 effect 里:passive effect 走宏任务、promise 续体走微任务,旧读取可能在「切换提交完成、复位 effect 还没跑」的窗口里落地,那时世代号还是旧的,守卫会放行。
### 2. 前插锚定:回合 key + 块身份 + 偏移,关掉浏览器原生锚定
- 锚点 = 顶部可见块的 `data-turn-key` + `data-block-key` + 相对列表顶边的像素偏移。
- 合并更早历史前记录锚点,合并后按同一 key/块身份把偏移还原,较早内容出现在上方而用户正在读的位置不动。
- 块身份来自 `DirectChatBlock.key`(条目块是 `${回合 key}:${条目 itemId}`,本地说明块是 `messageId`),只要求**在同一回合内唯一**。
- 不按块序号定位:`DirectProjectTurn` 的 `renderTurnProcess` 对运行中的回合把过程块**平铺**、对已结束的回合把它们折进一个 `<details>`,收口时整个回合的块序号都会后移(`[用户, 工具组1, 工具组2]` → `[用户, 执行过程, 工具组1, 工具组2, 终态正文, 终态文案]`),序号锚点会解析到隔壁块并按错误基准写 `scrollTop`,表现为一帧跳动。
- 锚点块没有布局盒时(被折进**收起**的 `<details>`、或已从列表移除)`restoreTurnAnchor` 返回 false:调用方据此放弃这次补偿,拿当前可见位置重新起锚,不写 `scrollTop`(不回跳,也不假装对齐)。同理,读锚点时跳过没有布局盒的块——它既不是「用户正在读的位置」,也还原不出来。
- 列表上加 `overflow-anchor: none`,关掉浏览器原生 scroll anchoring,保证只有一套补偿在跑(不做「两套补偿交替生效」)。
- 一次加载期间(`historyLoading` 为真)所有补偿——ResizeObserver 回调、加载行挂载、历史合并——都还原同一个冻结锚点;加载结束再刷新锚点。
- 块集合(列表里所有同时带 `data-turn-key` 与 `data-block-key` 的节点,即所有可锚定的块)在列表子元素没变时复用同一份缓存,缓存由子元素增删(与 `ResizeObserver` 重订阅共用同一个 `MutationObserver`)失效:否则一次滚动帧里读锚点、还原锚点会各查一遍整份列表,长会话下就是 O(块数) 的滚动抖动。
### 3. 回到底部胶囊
- 列表底部居中的悬浮胶囊(`sticky`),跟着滚动容器走、不随内容滚走。
- 距底部超过 48px 时出现,文案「回到底部」;用户不跟随时来了新的终态内容就改成「有新回复 · 回到底部」。
- 点击:平滑滚到底部 + 恢复跟随最新 + 清除「有新回复」,随后按钮自行消失。
- 平滑滚动期间滚动位置归这次程序化滚动所有:滚动事件不再翻转「跟随最新」,布局补偿也不写 `scrollTop`(写一次就会取消动画并把画面拉回原处,表现为「点了只下去一屏、到不了底」)。滚到贴底阈值即交还控制权;用户中途用滚轮 / 触摸 / 键盘打断则立刻交还,不会卡住后续跟随。
- 动画期间内容变高(流式正文、图片撑开)时,点击瞬间记下的 `scrollHeight` 已经不是底部:补偿不写 `scrollTop`,而是把动画目标重新对准新的底部。否则动画停在旧目标上、等不到「贴底」那次滚动事件,`programmaticScrollRef` 不会交还——跟随与布局补偿整段挂起,而 `scrollToBottom` 已把胶囊按「已贴底」隐掉,用户停在底部之上却没有任何指示与自动跟随。
### 4. 跟随最新与折叠展开
- 距底部 ≤ 48px 视为「在底部」,即跟随最新;用户手动滚回去(进入阈值)就自动恢复跟随,不需要额外开关。
- 用 `ResizeObserver` 观察列表的直接子元素(`MutationObserver` 负责在子元素变化时重订阅)——任何高度变化:
- 跟随时 → 贴底;
- 不跟随时 → 还原顶部可见回合锚点(用户正在读的那条折叠头留在原位);若刚刚展开的那个块头部仍然可见,则按展开前记录的屏幕位置把它冻结,并把新展开的正文露出来:正文底边超出视口就只滚到刚好露出底边,正文比视口还高则对齐正文顶部;
- 收起(正文高度归零)不额外滚动,只冻结头部。
- 新一回合开始(`turnInFlight` 由假转真)时强制恢复跟随并贴底,替代原先由视图直接写 `shouldFollowLatestRef` 的做法。
### 5. 结构与样式
- 滚动所有权(列表 ref、跟随最新、ResizeObserver、锚点还原、胶囊可见性)搬进 `DirectProjectConversation` 这一层;`DirectProjectChatView` 只传加载状态与回调,不再持有 `messagesRef` / `shouldFollowLatestRef` / `handleScroll`。
- 控制器对渲染层暴露可渲染的 `historyLoading` 与 `historyError`(外加 `retryEarlierHistory`),取代只存在于 ref 里的加载标志。
- 新增元素全部用内联 Tailwind 工具类,不改 `styles.css`;列表本身仍保持 `message-list project-chat-message-list` 类名不变,只追加 `overflow-anchor` 工具类。
### 6. 换会话时复位滚动所有权
- 列表容器在切项目时**不会重挂载**:`DirectProjectChatView` 在 `App.tsx` 只有一处渲染、没有 `key`,内部的 `DirectProjectConversation` 也没有 `key`,而 `useConversationScroll` 的跟随最新 / `atBottom` / `hasNewReply` / 前插锚点 / 折叠头 / 程序化滚动标记只在挂载时初始化一次。控制器自己按项目重置了历史状态,滚动所有权却一直漏着。
- 症状:在项目 A 往上滚过再切到 B——① B 的首屏不贴底,用户得自己往下滚;② B 的第一批回合会在「不跟随」分支被算成新内容,胶囊在新项目上直接显示「有新回复 · 回到底部」,可用户根本没在 B 里离开过底部。
- 决策:把会话身份(`conversationKey`,DirectProject 传项目路径)作为**显式信号**传给 `useConversationScroll`;身份变化时复位上述滚动所有权状态、把终态指纹同步成新会话内容、再贴底。不改变组件生命周期,同一个项目重开(身份不变)也不会被当成新会话;将来策划对话复用同一个 hook 时用的是同一套信号。
## 备选方案与取舍
1. **保留按钮 + 只加自动加载**:加载中仍靠按钮做唯一反馈,且删掉按钮后失败路径没有补救入口;按钮本身与滚动自动加载重复。
2. **在列表外面套一层 viewport 做浮层定位**:`PlanningChatView` 共用同一套容器规则,且工作台里 `.project-chat-conversation` 是 `display: block` + `height: 100%` 几何,套一层就会让 `height: 100%` 的列表塌成内容高度;改公共类会连带策划对话。改用列表内的 `sticky` 胶囊,零结构改动。
3. **只依赖原生 CSS scroll anchoring**:前插能免费对齐,但做不到「跟随时展开要贴底」,也无法在加载期间冻结同一套锚点;因此显式补偿 + 关闭原生锚定。
4. **展开后总是把正文滚进视口**:对正文比视口矮的折叠块会把画面大幅上移,打断正在读历史的用户;采用「跟随时贴底 / 否则冻结折叠头 + 只滚到刚好露出新展开正文的最小位移」。
5. **平滑滚动期间照常处理滚动事件与布局补偿**:程序化滚动会被应用自己的补偿打断(第一次写 `scrollTop` 即取消动画),用户点了「回到底部」也停在半路;因此改为滚动期间冻结这两条路径。
6. **自动重试失败的历史加载**:弱网下会反复打接口,且用户看不出到底在重试还是在挂起;改为挂起 + 内联重试行。
7. **用 `key={projectPath}` 让 React 重建会话列表**:改动最小,但整份消息列表连同加载行的 150ms 延迟计时一起重建,而且「同一个项目重开」也会被当成新会话;改用显式身份信号复位,重建与否与「换没换会话」解耦。
8. **锚点存块序号(`index`)而不是块身份**:序号在「回合 running→finished」时会因过程被折进 `<details>` 而整体后移,解析到隔壁块;改成稳定块身份只多一层 `data-block-key`,且块不可见仍要单独判失败,所以「失效锚点」省下的那点改动不值得留下错位。
9. **锚点块被折进收起的 `<details>` 时把还原改成找同回合内其它可见块**:会按另一条消息的基准写 `scrollTop`,比放弃这次补偿更糟;放弃只表现为不自动对齐,不回跳。
## 影响
- 历史加载失败不再只写顶部状态行,而是落到列表里的内联错误行;顶部状态行仍保留首屏读取失败等其它用途。
- 滚动是表现层行为,正式状态仍在后端投影与运行态事件;本 ADR 不新增领域概念。
- 阈值(触顶 24px、贴底 48px、spinner 150ms)是可按手感调整的常量,集中放在 `components/DirectProjectConversation/conversationScrollPolicy.ts`。
- 验收:纯函数与 jsdom 组件测试覆盖阈值、锚点还原(含收口后按块身份仍指向同一块、锚点块被折叠隐藏时放弃还原)、加载/错误行、胶囊文案与显隐;滚动观感(顶部加载圈、胶囊出现与消失、底部展开回贴、历史前插不跳、长回合收口时视口不跳、切项目后首屏贴底且不误报「有新回复」)必须真机手动验收——jsdom 没有布局。
- 明确的后续项(不在本次范围):`PlanningChatView` 与 `App.tsx` 遗留 `message-history-more` 路径的同款改造、未读条数徽标、Playwright 端到端。
@@ -0,0 +1,43 @@
# 【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02
状态:已接受
## 背景
AGC(`apps/ai-game-creator-shell`)长期同时存在两套「Agent 执行」:正式对话面已经收敛到 DirectProject 单容器(Rust `agent/direct_*` + codex app-server),而 `src-tauri/src/agent/runtime_*`、`agent/prompt.rs`、`agent_native_tools.rs`、`collaboration.rs`、`delegation.rs`、`goal.rs`、`context_compaction.rs`、`isolated_agent.rs`、`tool_plan_handoff/` 这一整套自建 Runtime(runtime driver / protocol / tools / actions / state)仍在编译、测试、注册 Tauri 命令、并以 `project-supervisor` 身份承担自主构建与专业 Agent 编排。
这套 Runtime 的入口只剩历史沉淀:前端没有任何渲染或调用入口(`ProjectSupervisorView`、Supervisor 运行态面板与专业 Agent 对话已在 2026-09-22 前后退役),`resume/confirm/read_game_creator_agent_runtime*`、`chat_with_game_creator_agent`、`*_agent_goal`、`schedule_game_creator_agent_ready_tasks`、`start_game_creator_supervisor_runtime_task` 等命令没有现役调用方;真实链路只由 `scripts/agent-runtime-real-e2e.mjs`、`agent-runtime-steer-real-e2e.mjs`、`smoke-agent-run-local-provider.mjs`、`llm-transient-fault-proxy.mjs` 等专用 harness 与它自己的 Rust 测试覆盖。外部 Runner(`--agent-runner`)里也跟着背了一份项目 execution-owner、known-roots 和 `runner.status` / `runner.shutdown_if_idle` 的 CLI 面,但其中真正仍在服役的只有编辑器桥与 manifest relay。
保留它的代价持续存在:一个没有用户入口的执行栈要求每次改动都同步维护四类东西——Rust Runtime 内核与专属测试、Tauri 命令注册与 `scripts/check-config.mjs` 白名单、真实 e2e harness 与 CI 预热 job、以及多份权威文档里按「现役」描述的协议与预算。
## 决策
- 自建 Agent Runtime 执行面整体退役,按「从未存在」处理:实现、专属测试、Tauri 命令注册、构建期门禁条目、真实 e2e harness、CI job 与文档承诺一并删除,历史由 Git 保存,不保留 feature flag、双跑路径或墓碑注释。
- Rust 删除范围:`agent/runtime_driver/`、`agent/runtime_protocol/`、`agent/runtime_tools/`、`agent/runtime_actions/`、`agent/runtime_state.rs`、`agent/runtime_adapter.rs`、`agent/prompt.rs`、`agent_native_tools.rs`、`collaboration.rs`、`delegation.rs`、`goal.rs`、`context_compaction.rs`、`isolated_agent.rs`、`provider_handoff.rs`、`provider_retry.rs`、`tool_plan_handoff/`、`user_input.rs` 及其 `src/tests/` 下的专属用例与 fixture;`agent/generation/` 只保留现役生成路径(画布 / 资源生成、prompt 上下文装载、pass artifact 落盘与 trace),`generation/run_lifecycle.rs`、`generation/role_briefs.rs`、`generation/tests.rs` 一并删除。
- Tauri 命令退役:`control_agent_run`、`generate_local_game_draft`、`chat_with_game_creator_agent`、`chat_with_game_creator_role_agent(_stream)`、`start/read/edit/pause/resume/clear_game_creator_agent_goal`、`start_game_creator_supervisor_runtime_task`、`compact_game_creator_agent_runtime_context`、`cancel/retry/confirm_retry/confirm/reject_game_creator_agent_runtime_task`、`answer_game_creator_agent_runtime_user_input`、`read_game_creator_agent_runtime(s)`、`resume/confirm_resume_game_creator_agent_runtime_tasks`、`schedule_game_creator_agent_ready_tasks` 全部移出 `desktop.rs` 的 `generate_handler!` 与 `check-config.mjs` 白名单;`agent` 会话命令(`list/create/fork/set_active/archive_game_creator_agent_session`)继续保留。
- CLI 退役:`--agent-run` / `--agent-enqueue` / `--agent-steer` / `--agent-resume` / `--agent-context-compact` / `--agent-goal-*` / `--agent-task` / `--agent-runner-status` / `--runner-shutdown-if-idle` 与其 CLI 解析、配置目录要求、测试一并删除;`CliCommand` 收敛为 `LlmStatus | EnvironmentCheck | PreviewServe`。`--agent-runner` 模式与 `--gui-owner-required` 保留。
- 外部 Runner 收缩:`runner/project_owner.rs`、项目 execution-owner / known-roots 注册表、`runner.status`、read-only configure、跨启动 owner 认领与 `shutdown_if_idle` 的项目级语义全部删除;Runner 现在只剩编辑器桥 RPC(`*.editor.rpc` / `*.editor.ack` / `*.editor.mark_uncertain`)与 `runner.attach_gui_owner` + GUI owner 参与锁 / watchdog。`runner.rs` 顶部保留 `TODO(retire-runner)` 记录「等编辑器执行收进 GUI 进程后可整体退役」的规划。
- 前端退役:删除 `agentRuntimeById` 状态与 `read_game_creator_agent_runtimes` / `resume_game_creator_agent_runtime_tasks` / `confirm_resume_game_creator_agent_runtime_tasks` 调用链、`features/agent-runtime/model.ts` 中只服务 Runtime 投影的归一化/合并/格式化函数、`app/types.ts` 的 `AgentRuntime*` / `AgentGoal*` / `GameCreatorAgentRuntimeUpdateEvent` 类型、`AgentStatusCard` 的 `runtime*` 字段与 `features/project-summary/agentPresentation.ts` 的 `projectAgentRuntimeSummaries` / `formatAgentCardRuntimeStatus`,以及 `onAgentRuntimeSummariesChange` / `activeProjectAgentRuntimeSummaries` 在 app-shell、WorkspaceLauncher、`view/project-development` 与 `App.tsx` 的透传。
- harness 与 CI 退役:删除 `apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/`、`agent-runtime-real-e2e.mjs`、`agent-runtime-steer-real-e2e.mjs`、`smoke-agent-run-local-provider.mjs`、`llm-transient-fault-proxy.mjs` 与 `tests/llmTransientFaultProxy.test.ts`;`scripts/check-native-shells.mjs` 去掉 `agc-rust-smoke` 分组,`.gitea/workflows/project-ci.yml` 去掉 `ai-game-creator-shell-rust-smoke` job,root / App `package.json` 去掉全部 `agent-*` / `agent-runtime:*` / `agent-run:smoke` 脚本,缓存维护脚本同步去掉对应 job 名。
## 备选方案与取舍
1. **只删前端入口,保留 Rust Runtime 与命令**:命令与 Runtime 内核继续编译、测试、进白名单,等于把「没有用户入口的执行栈」永久固化,正是本次要消除的成本。
2. **保留 Runtime 作为「本地 CLI 能力」**:`--agent-*` 与 `runner.status` 只服务真实 e2e harness,没有产品路径;保留它就要继续维护 harness、CI job 和缓存预热,收益为零。
3. **保留兼容别名或 feature flag**:没有现役调用方、公开契约或持久化数据需要兼容,兼容层只会把死词汇表留在解析与注册层。
4. **连带退役编辑器桥与 `--agent-runner`**:编辑器 RPC、回执确认、不确定执行 fence 与 Windows 作业对象隔离目前仍依赖独立进程托管,一次性搬进 GUI 是独立的较大重构(见 `runner.rs` 的 `TODO(retire-runner)`),不在本次范围。
## 影响
- 产品可见行为不变:正式对话、资源工作台、生成与编辑器链路本来就不经过自建 Runtime;删除后项目运行态由 DirectProject 自己的订阅与缓存(`projectResourceLiveUpdateModel` 等)持有,专业 Agent 状态卡片只从 manifest + run trace 推导。
- AGC Rust crate 的编译面与测试面显著缩小(删除 140 个源文件),`cargo check` / 分片测试不再需要 Runtime fixture、Goal sidecar、Supervisor 协作与 tool-plan handoff 用例。
- 术语收敛:「Runtime」在 AGC 里此后指 DirectProject / codex app-server 执行面,「Supervisor」不再是正式运行身份;后续文档与注释不得再按现役描述自建 Runtime。
- 新增门禁边界:`scripts/check-config.mjs` 的 native-only 白名单不得再收留已删命令;退役概念不新增守卫测试或字符串钉桩,防止回归依靠「没有解析层 / 没有注册」的架构边界。
- 保留项:AGC 会话命令、项目权限策略词汇表、DirectProject 的 `enqueue_direct_codex_turn` / `cancel_direct_codex_turn` 链路、编辑器桥(`runner.attach_gui_owner`、participant lock、watchdog)与 `--agent-runner` 模式均不受影响。
## 验证
- `cd apps/ai-game-creator-shell/src-tauri && cargo check --tests --bin genarrative-ai-game-creator-shell`、`cargo test --bin genarrative-ai-game-creator-shell runner::`。
- `cd apps/ai-game-creator-shell && node scripts/check-config.mjs`、`npx tsc -p tsconfig.json --noEmit`。
- `npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts` 与 `npx vitest run scripts/project-ci-workflow.test.ts`。
- `git diff --check`、`npm run check:encoding`。
@@ -5,6 +5,8 @@ Status: implemented-awaiting-runtime-acceptance
Date: 2026-09-14
Milestone: `【里程碑】项目客户端占用锁收敛-2026-09-14.md`
> 2026-10-02 更新:本计划里 Runner 侧的 `runner/project_owner.rs`、`.agent/runtime/execution-owner.lock` 与项目 execution-owner / known-roots 语义已随自建 Agent Runtime 退役删除(见 `docs/adr/【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02.md`);统一客户端占用锁(`project/write_lock.rs`)保留,下述 Runner owner 部分仅作历史追溯。
## 代码边界
- `apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs`
@@ -67,4 +69,4 @@ Milestone: `【里程碑】项目客户端占用锁收敛-2026-09-14.md`
- 统一锁与 Runner owner 两条路径都在工作树里:`apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs`、`runner/project_owner.rs`。
- 定向用例:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --offline -- write_lock --test-threads=1` → **21 passed**;`-- gui_ --test-threads=1` → **31 passed**(覆盖 `gui_owner_*`、`gui_owned_runner_*`、`gui_participant_lock_*` 等同进程重入、跨线程争用、owner 交接与崩溃恢复分支)。
- 仍待补:真机多窗口/跨进程占用的运行时表现(本机只到定向用例级)。
- 仍待补:真机多窗口/跨进程占用的运行时表现(本机只到定向用例级)。
@@ -5,6 +5,8 @@ Status: implemented-awaiting-runtime-acceptance
Date: 2026-09-14
Parent Spec: `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
> 2026-10-02 更新:Runner 侧 `execution-owner.lock` / `runner/project_owner.rs` 已随自建 Agent Runtime 退役删除(见 `docs/adr/【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02.md`);统一客户端占用锁保留,下述 Runner owner 迁移项不再执行。
## 目标
项目只保留一个面向客户端占用的项目级跨进程锁,防止多个客户端同时打开同一项目;同一客户端进程内**同一条写调用链(同一线程)的嵌套调用**复用既有项目锁,不因自身持锁进入等待。
@@ -60,4 +62,4 @@ Parent Spec: `docs/technical/【技术方案】AI游戏创作智能体App实施
- 统一锁与 Runner owner 两条路径都在工作树里:`apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs`、`runner/project_owner.rs`。
- 定向用例:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --offline -- write_lock --test-threads=1` → **21 passed**;`-- gui_ --test-threads=1` → **31 passed**(覆盖 `gui_owner_*`、`gui_owned_runner_*`、`gui_participant_lock_*` 等同进程重入、跨线程争用、owner 交接与崩溃恢复分支)。
- 仍待补:真机多窗口/跨进程占用的运行时表现(本机只到定向用例级)。
- 仍待补:真机多窗口/跨进程占用的运行时表现(本机只到定向用例级)。
@@ -28,6 +28,46 @@
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{codex_app_server/{mod.rs,execution.rs,turn_error.rs},thread_manager/{mod.rs,dispatch.rs,wire/},direct_runtime/{mod.rs,user_input.rs},direct_codex_user_item/*,runtime_driver/entrypoints.rs,...}`、前端 `chat/{controller,conversation}/**` 与 `chat/generated/**`(旧绑定文件删除、新绑定文件随 `cargo test export_bindings` 生成)、`scripts/check-generated-bindings.mjs`、`tests/**`。
- 验证:`cargo check`(bin)、`cargo test -- agent::thread_manager`(69 passed)与 `cargo test -- terminal`(73 passed)、`npm run ai-game-creator-shell:typecheck`、`npx vitest run apps/ai-game-creator-shell/tests`(DirectProject 相关用例全绿;`gameDistributionPublish*` / `recentProjectsHook` 的 localStorage 失败为既有问题,与本次无关)、`npm run check:generated-bindings`(104 个文件)、`npm run check:encoding`、`git diff --check`。
## 2026-10-02 资源编辑远端失败的原始原因穿出到工具错误,资源编辑错误通道补一层 typed
- 背景:轮询到 `status=failed` 时客户端只读 `status`,丢掉平台在同一个响应里给的 `error`(契约 `ExternalEditorGenerationJobResponse.error`),统一写 `terminal_failure_code = remote-generation-failed` 并返回「remote-terminal-failed: 资源编辑生成失败」。平台的可行动原因就此消失:模型与用户卡片只看到一句「失败了」,重试路径(`ensure_resource_edit_phase_resumable`)也只有分类码。这违反 `pitfalls.md`「远端资源编辑终态必须指出唯一出口」里已写下的口径——「首次失败的原始拒绝说明继续由当次错误文案承担」;提交期 HTTP 400 分支(`editor_api_rejection_reason`)兑现了,轮询分支没有。另外 `remote-terminal-failed:` 只是文案前缀(全仓没有 `starts_with` 解析它),在第一句失败文案里与「失败」重复。
- 决策(typed 承载):新增 `ResourceEditError`(`project/resource_editor/error.rs`),只两个变体:`RemoteGenerationFailed { serverMessage }` 承载平台 `error` 原文,`Other(String)` 收尚未分类的失败(`// TODO refactor string-typed`)。两个入口 `derive_local_project_resource`、`resume_local_project_resource_edit` 返回 typed;旧名 `derive_local_project_resource_at` / `resume_local_project_resource_edit_at` 保留为 `Result<_, String>` 外观(映射 `to_user_msg()`),因此 33 个既有测试调用点与两个 Tauri 命令零改动。
- 决策(不用 blanket From):不提供 `impl From<String>`;每处 String 错误显式 `.map_err(ResourceEditError::Other)`,让「还没 typed 化」的边界处处可见,而不是被一次隐式转换吞掉。
- 决策(不按 code 分支):不按 `terminal_failure_code` 分支。它两个写入点最终落到同一个 `phase`、唯一读者只做插值不比较,值域撑不起 policy;字段上加 `// TODO clean unnecessary`。清理前置条件已核实:该字段无 `skip_serializing_if`,`.agent/resource-edits/operations/*.json` 每个文件都带这个 key,而 `ResourceEditLedger` 是 `deny_unknown_fields`、扫描循环里一个文件解析失败会让整个「待恢复资源编辑」列表报错返回。
- 决策(前缀去留):删掉第一句失败文案里的 `remote-terminal-failed:`(轮询与提交期 400 两处)。`ensure_resource_edit_phase_resumable` 里那三个 token 保留:它们与三个 phase 一一对应,是那句重试文案里区分「确定失败 / 已归档 / 待对账」的唯一手段。
- 决策(原文边界):平台原文只进当次错误文案,仍不进账本(`terminal_failure_code` 的写入边界与既有断言不变)。平台 `user_visible_external_generation_error` 已对四种 kind 做 sanitize,图片/视频两种原样透出——与同 wire 的 `canvas_generation.rs` 口径一致,要收边界应改服务端。
- 决策(命名与落点):尚未 typed 化的变体叫 `Other`,不叫 `Message`(后者分不清是「已渲染文案」还是「原始消息」);新错误单独放 `project/resource_editor/error.rs`,不再往主文件里塞类型定义。
- 决策(工具层承载):`RemoteGenerationFailed` 不能到工具层又被压回一句字符串。共用载体放 `agent/tool/error.rs` 的 `RemoteResourceEditFailure { serverMessage }`(两个工具共用的文案只写一份),`CreateOrDeriveResourceError` / `RemoveBackgroundError` 各加 `RemoteGenerationFailed(RemoteResourceEditFailure)` 变体;翻译用显式 `from_resource_edit_error`,不用 `impl From<ResourceEditError>`,远端终态进 `RemoteGenerationFailed`、其余 `Other` 仍落回各工具原有的「失败:<文案>」变体。这样诊断 sidecar 的 `error` 字段(typed enum 整体序列化)天然带上平台原文,LLM 侧拿到的 `message` 也带上。
- 决策(前缀归属,2026-10-02 追加):叶子错误只给事实,不给「谁失败了」的总结前缀。`RemoteResourceEditFailure::to_user_msg` 与 `ResourceEditError::to_user_msg` 都只返回平台 `error` 原文;平台没给就回「服务器未返回错误信息」,不再说「资源编辑生成失败」这种没有信息量的总结。typed 错误新增 `phaseDetail` 字段,但它只作为结构化字段进诊断 sidecar(开发者/LLM 侧看原始值),**不参与用户文案**。前缀由使用者自己加:`agc_create_or_derive_resource` 用「生成或派生资源失败:」、`agc_remove_background` 用「抠图失败:」、桌面命令面用「资源编辑生成失败:」。同一份 typed 错误因此可以同时服务工具面(前缀各随其工具)与桌面面(保留原有文案)。
- 决策(HTTP 兜底与叶子前缀,2026-10-02 追加):叶子只给「服务端 message / code / 原始传输事实」这类事实,不把操作名写进叶子。`game_package_upload/runtime.rs` 三处 `let (_, message)` 把服务端 `code` 丢掉、再拼「读取上传状态失败(HTTP 503)」这类前缀,改成 `message` → `code` → `HTTP {status}`(操作名交给调用方的话术)。`game_distribution_publish.rs` 的 `response_data`(2xx + `ok:false`)同样用上被丢掉的 `error.code`,`account_api.rs` 的 envelope 分支补 `error.code`;服务端没给任何原因时统一回「服务器未返回错误信息」。错误类型自身的单测不再断言 `to_user_msg()` 的字面量(文案是给用户的话术,不是契约),只保留「typed 字段原样序列化进诊断」的结构断言。
- 决策(凭据作用域的 error 类型):`with_direct_editor_api_credentials` 原本把操作限定成 `Result<_, String>`,会把 typed 错误提前压掉。新增 `with_direct_editor_api_credentials_as(operation, credentials_error)` 保留调用方 error 类型,凭据解析失败由调用方显式翻译(这里传 `ResourceEditError::Other`),旧名保持 `String` 语义、零改动。
- 决策(future 装箱):`handle_direct_tool_bridge` 的状态机在调试测试线程的默认栈上已经贴着上限,资源编辑 arm 直接内联会顶穿(`bridge_write_file_waits_on_the_blocking_pool_instead_of_a_runtime_worker` 栈溢出)。桥里四个资源编辑 await 点用 `Box::pin` 只留指针进外层状态机;这是体积问题,不是错误用 `Box`。
- 决策(同类兜底,2026-10-02 追加):`agent/generation/canvas_generation.rs` 的远端 `failed` 分支原来在平台没给 `error` 时兜底成「生成任务失败」,与句首的「平台图片生成任务失败:」重复,改成「服务器未返回错误信息」;`phaseDetail` 不再参与用户文案(只作结构化字段)。
- 改动范围:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`、`project/resource_editor/error.rs`(新)、`agent/tool/error.rs`、`agent/tool/create_or_derive_resource/error.rs`、`agent/tool/remove_background/error.rs`、`agent/direct_tool_bridge.rs`、`assets.rs`、`docs/project-memory/shared-memory/pitfalls.md`。
- 验证:`cargo check --bin genarrative-ai-game-creator-shell --tests` 通过;`cargo test --bin genarrative-ai-game-creator-shell -- project::resource_editor --test-threads=1` 66 passed(并行跑会有一批 TCP fixture 用例因争用超时,串行全绿,与本次改动无关);`-- agent::tool:: agent::direct_tool_bridge` 47 passed;`npm run check:encoding` 5111 files;`git diff --check` 干净;`cargo fmt` 已跑。
- 关联:`pitfalls.md`「远端资源编辑终态必须指出唯一出口」。
## 2026-10-02 DirectProject 对话:更早历史自动加载、前插锚定与回到底部胶囊
- 背景:右侧对话更早历史只靠常驻按钮「显示更早的对话」拉,且没有任何加载反馈(`historyLoadingRef` 是 ref,渲染不出来);用户滚上去之后没有「回到最新」的入口;展开「执行过程」/工具组/思考块时浏览器保持 `scrollTop`,新展开的正文长在视口下方,在底部展开更是直接顶出可视区。
- 决策:删按钮改自动加载——触顶 24px 与「首帧后内容填不满视口」共用一道门(有更早历史 ∧ 不在加载中 ∧ 无失败记录),失败挂起、只留内联「加载更早对话失败 · 重试」且不自动重试;加载行挂载在列表最上方、延迟 150ms 才显示。前插按「回合 key(`data-turn-key`)+ 块内序号 + 相对列表顶边偏移」冻结锚点,列表显式 `overflow-anchor: none`;加载期间所有补偿还原同一个冻结锚点,加载结束后的下一次补偿再刷新。底部居中 `sticky` 胶囊「回到底部」(不跟随时来了新终态内容改「有新回复 · 回到底部」):距底 48px 阈值、点击平滑滚动并恢复跟随。`ResizeObserver` 观察列表直接子元素(`MutationObserver` 负责子元素变化时重订阅):跟随时任何高度变化贴底,否则冻结刚展开的折叠头,头部锚不住再对齐展开正文顶边;新一回合开始(`turnInFlight` 假转真)强制恢复跟随。滚动所有权(列表 ref、跟随最新、补偿、胶囊显隐)从 `DirectProjectChatView` 搬进 `DirectProjectConversation` 及其同目录 hook,控制器新增可渲染的 `historyLoading` / `historyError` 与 `retryEarlierHistory`。
- 边界:只做 DirectProject;`PlanningChatView` 与 `App.tsx` 里的 `message-history-more` 遗留路径不动,因此该样式保留(后续项已写进 ADR)。新增元素全部用内联 Tailwind,`styles.css` 未改;阈值、文案与判据集中在同目录 `conversationScrollPolicy.ts`,契约见 [`【ADR】DirectProject对话滚动与历史自动加载-2026-10-02`](../../adr/【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md),旧「单一事实源」ADR 的分页条目已改为引用它。
- 验证:新增 37 条与实现同目录的用例(阈值与加载门、锚点读取/还原、折叠头冻结与正文对齐、hook 的触顶/填充/胶囊/未读/延迟加载行、组件层的按钮移除与内联重试行)由根 `vitest.config.ts` 的 include 收进门禁;`npm run ai-game-creator-shell:typecheck`、`eslint`(含新文件)、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 通过;滚动观感(顶部加载圈、胶囊显隐、底部展开回贴、历史前插不跳)留真机手动验收。
- 追加(2026-10-02,两个真机缺陷的根因与修正):①「点回到底部只下去一屏、到不了底」——平滑动画期间滚动事件把「跟随最新」翻成假,布局补偿与锚点还原接着写 `scrollTop`,而真实浏览器里任何一次写都会取消正在跑的平滑动画;现在滚动位置由这次程序化滚动独占(滚动事件只在贴底时才结算并交还,补偿整段跳过),滚轮 / 触摸 / 键盘接手立刻交还,避免标记永远挂着。②「展开折叠块没有自动滚动到位」——旧规则只在展开正文比视口还高时才动,正文矮的折叠块(工具组、思考块)展开后正文仍在视口外;现在冻结折叠头之后统一补「刚好露出新展开正文」的最小位移(底边超出就补超出量,比视口还高则对齐正文顶边),两段位移合成一次写。落在 `conversationToggleReveal.ts`(新增纯函数 `toggleRevealDelta`)与 `useConversationScroll.ts`(新增 `programmaticScrollRef`)。
- 验证(2026-10-02 追加):同目录用例补齐到 48 条(新增 `toggleRevealDelta` 六例、hook 的四例程序化滚动用例,其中两例在修正前确实红)全部通过;Chromium 真机脚本复验三处——点回到底部收敛到 `scrollHeight - clientHeight`、底部展开贴到新底、视口外展开补 269px 后正文底边正好贴视口下缘。
- 追加(2026-10-02,换会话复位滚动所有权):`useConversationScroll` 的跟随最新 / `atBottom` / `hasNewReply` / 前插锚点 / 折叠头 / 程序化滚动标记只在挂载时初始化一次,而 `DirectProjectChatView` 切项目时不重挂载——在项目 A 往上滚过再切 B,B 首屏不贴底且胶囊直接显示「有新回复 · 回到底部」。现在把会话身份(`conversationKey`,DirectProject 传项目路径)作为显式信号传进 hook,身份变化即复位这批状态、同步终态指纹并重新贴底;不改成 `key` 重建列表,避免消息列表重建与加载行 150ms 延迟计时重来,同一个项目重开也不算换会话。
- 验证(2026-10-02 追加,换会话复位):`useConversationScroll` 新增两例换会话用例(新会话首屏贴底且不出现胶囊、切会话后在 B 里往上滚只显示「回到底部」而不是「有新回复」),修正前确实红、修正后绿;chat 范围用例全绿。
- 追加(2026-10-02,review 两处内部缺陷的修正):①更早历史读取的守卫从「项目路径相等」改成「世代号相等」(`historyLoadTokenRef`,切项目与每次读取各推进一格)——A→B→A 之后在飞的旧读取又落回同一路径,原守卫会放行,把新一代的加载态、并发闸门与游标一起改掉;现在非最新一次读取落地时整段丢弃。②锚点的块集合按列表缓存(`WeakMap`),由已有的子元素 `MutationObserver` 在同一处 `invalidateTurnBlocks` 失效——原来一个滚动帧里读锚点、还原锚点各查一遍整份列表,长会话下是 O(块数) 的 DOM 查询。
- 验证(2026-10-02 追加,review 修正):新增一例控制器用例(驱动 A→B→A 且同路径新读取在飞,修正前在「旧读取落地」处红)与两例锚点缓存用例(连续收集只查一次 DOM、子元素变化并失效后重新收集);chat 范围用例全绿,`npm run ai-game-creator-shell:typecheck`、`eslint --max-warnings 0`、`npm run check:encoding`、`git diff --check` 通过。
- 追加(2026-10-02,锚点改用稳定块身份):review 第 3 条(`findTurnBlock` 按块序号定位)成立,采纳「换一套块身份」。锚点从 `{回合 key, 块序号, 偏移}` 改为 `{回合 key, 块身份, 偏移}`,块身份即 `DirectChatBlock.key`(`${回合 key}:${条目 itemId}` / 本地说明 `messageId`),只要求同一回合内唯一;展示层给每个可锚定块加 `data-block-key`(`DirectProjectTurn` 的正文块与终态文案、折进 `<details>` 的过程包装块、`ToolCallGroup`、`AgentReasoning` 各自透传),`conversationScrollAnchor.ts` 的收集选择器因此收敛为 `[data-turn-key][data-block-key]`。原因:`renderTurnProcess` 对运行中的回合平铺过程块、对已结束的回合折进一个 `<details>`,收口时整个回合的块序号后移一格,序号锚点会解析到隔壁块并按错误基准写 `scrollTop`。同时补上 review 指出、原方案漏掉的一半:锚点块没有布局盒(被折进收起的 `<details>`)时读锚点跳过它、`restoreTurnAnchor` 判为失败,调用方放弃这次补偿并重新起锚——不回跳,也不按别的块硬对齐。
- 验证(2026-10-02 追加,稳定块身份):锚点纯函数用例改为按块身份构造(`buildList` 传 `[回合 key, 块身份]`),新增「回合收口后块序号整体后移,块身份仍指向同一块且还原成功」「收起的 `<details>` 里的块没有布局盒:读锚点跳过、还原返回 false 且不写 `scrollTop`」;组件层新增用例断言每个带 `data-turn-key` 的块都有 `data-block-key`、同回合内块身份唯一、回合 running→finished 后同一块身份仍在。修正前锚点用例在「收口后按身份定位」处红。
- 追加(2026-10-02,review:动画期间内容变高会把程序化滚动标记卡住):`scrollToBottom` 把点击那一刻的 `scrollHeight` 当动画目标,而流式正文 / 图片撑开会让它在动画期间继续变高;`programmaticScrollRef` 只在「贴底」那次滚动事件里交还,于是动画停在旧目标后标记永远为真——布局补偿整段被跳过(列表不再跟随新内容),胶囊又已按「已贴底」隐掉,用户停在底部之上却没有任何指示和自动跟随。修正:程序化滚动期间布局补偿不写 `scrollTop`,但内容变高时把动画目标重新对准新的底部(`scrollListToBottom(list, 'smooth')`),动画继续跑到真正的底,标记照常在贴底时交还。
- 验证(2026-10-02 追加,动画目标重对准):`useConversationScroll.test.tsx` 新增一例——点击回到底部后内容变高(`scrollHeight` 1200→1500)触发一次布局变化,断言动画目标从 `[1200]` 变为 `[1200, 1500]`、落到新底部后仍能交出控制权;修正前在目标数组处红。
- 追加(2026-10-02,review:错误行的重试按钮在断言式 live region 内):`DirectProjectHistoryErrorRow` 原来把重试按钮渲染在 `<p role="alert">` 里面,而 `role="alert"` 隐含 `aria-live="assertive"` + `aria-atomic="true"`,交互控件会被卷进整段断言性播报、读屏也不一定把它当可聚焦按钮。修正:容器改为普通 `<div>`,`role="alert"` 只包住「加载更早对话失败」文案本身,重试按钮是 live region 之外的兄弟。
- 验证(2026-10-02 追加,错误行结构):`DirectProjectConversation.test.tsx` 新增一例断言 `role="alert"` 节点不包含重试按钮、且文案仍在 alert 内并仍可点击;修正前红。
- 追加(2026-10-02,review:世代号推进时机):`historyLoadTokenRef` 的换项目推进原来只在复位 effect 里,而 `projectPathRef.current` 是渲染期赋值的——交接窗口里守卫不再即时生效:React 的 passive effect 走宏任务、promise 续体走微任务,切换提交之后、复位 effect 之前落地的旧读取拿到的仍是旧世代号,会照常合并条目、写游标、关加载态(复位 effect 随后清掉状态,故终态没坏,但白跑一次跨项目读取且留下瞬时脏状态)。修正:推进移到渲染期,与 `projectPathRef` 同一处、只在路径真的变化时推进;复位 effect 不再推进。安全性依据:同一提交里子组件「填充视口」effect 的判据(`turns` / `historyHasMore` / `historyLoading` / `historyError` / 稳定的 `loadEarlier`)都不变,订阅状态复位本身也是 effect,因此这个窗口里不可能新起一次带旧游标的读取,去掉 effect 里的那一次推进不会放过它。
- 验证(2026-10-02 追加,世代号推进时机):该窗口依赖 React 调度(act 会把 effect 与断言放在同一个作用域里冲掉),jsdom 下无法构造出「旧读取先于复位 effect 落地」的确定性用例,因此没有新增红灯用例;既有的控制器世代用例(切项目、A→B→A)保持全绿,行为等价性由「本窗口内不会有新读取启动」的依赖分析支撑。真机若要硬证据,需在慢读取期间切项目并观察是否多打一次跨项目读取。
## 2026-10-01 Web、后台与 AGC 一键联调
- 背景:Web、管理后台和 AGC 同时开发时,分别启动入口容易产生两套 API/worker/SpacetimeDB,以及重复后台 Vite。
@@ -109,6 +149,16 @@
- 未修(另开):`redact_absolute_path_tokens` 的无语境绝对路径扫描仍会把 HTML 结束标签、嵌套 JSON 转义里的 `/` 误判成路径,是 issue #553 的根因;本次只删死路径,未改扫描器。
- 验证:`cargo test --bin genarrative-ai-game-creator-shell agent:: -- --test-threads=1`(929 passed / 0 failed / 5 ignored)、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。
## 2026-10-02 退役AGC独立Agent Runtime与CLI执行面
- 决策:AGC 自建 Agent Runtime 执行面整体退役,按「从未存在」处理——`src-tauri/src` 的 `agent/runtime_driver`、`runtime_protocol`、`runtime_tools`、`runtime_actions`、`runtime_state`、`runtime_adapter`、`agent/prompt.rs`、`agent_native_tools.rs`、`collaboration.rs`、`delegation.rs`、`goal.rs`、`context_compaction.rs`、`isolated_agent.rs`、`provider_handoff.rs`、`provider_retry.rs`、`tool_plan_handoff/`、`user_input.rs` 及专属测试/fixture 删除;`generate_local_game_draft`、`control_agent_run`、`chat_with_game_creator_agent`、`*_game_creator_agent_goal`、`*_game_creator_agent_runtime_*`、`schedule_game_creator_agent_ready_tasks`、`start_game_creator_supervisor_runtime_task` 移出 Tauri `generate_handler!` 与 `check-config.mjs` 白名单;`--agent-run` / `--agent-task` / `--agent-goal-*` / `--agent-resume` / `--agent-context-compact` / runner 状态类 CLI 一并删除,`CliCommand` 收敛为 `LlmStatus | EnvironmentCheck | PreviewServe`。
- 决策(Runner 收缩):外部 Runner 的项目 execution-owner / known-roots / `runner.status` / read-only configure 删除,只保留编辑器桥 RPC(`*.editor.rpc` / `*.editor.ack` / `*.editor.mark_uncertain`)与 `runner.attach_gui_owner` + GUI owner 参与锁 / watchdog;`--agent-runner` 模式保留,`runner.rs` 用 `TODO(retire-runner)` 记录后续整体退役条件。
- 决策(前端与 harness):删除前端 `read/resume/confirm_resume_game_creator_agent_runtimes*` 调用链、`agentRuntimeById` 状态与 `onAgentRuntimeSummariesChange` 透传、`AgentRuntime*` / `AgentGoal*` 类型、`AgentStatusCard` 的 `runtime*` 字段与 `projectAgentRuntimeSummaries` / `formatAgentCardRuntimeStatus`;删除 `agent-runtime-real-e2e*`、`agent-runtime-steer-real-e2e`、`smoke-agent-run-local-provider.mjs`、`llm-transient-fault-proxy.mjs` 及其 CI job 与缓存预热条目。
- 边界:AGC 会话命令、项目权限策略、DirectProject 的 `enqueue/cancel_direct_codex_turn` 链路与编辑器桥不受影响;保留项与备选方案见 ADR。
- 影响范围:`apps/ai-game-creator-shell/src/**`、`src-tauri/src/**`、`scripts/**`、`tests/**`、root / App `package.json`、`.gitea/workflows/project-ci.yml`、`deploy/container/README.md`、AGC 实施计划与 Runtime V1.1 文档。
- 验证:`cargo check --tests --bin genarrative-ai-game-creator-shell`、`cargo test --bin genarrative-ai-game-creator-shell runner::`、`node apps/ai-game-creator-shell/scripts/check-config.mjs`(App 目录内执行)、`npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit`、`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`、`npx vitest run scripts/project-ci-workflow.test.ts`、`git diff --check`。
- 关联:[【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02](../../adr/【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02.md)。
## 2026-09-30 release 渠道移除产品名与包名后缀
- 决策:`release` 渠道的正式产品名统一为 `陶泥儿`,Windows NSIS、macOS DMG / updater 归档等由 Tauri `productName` 派生的包名不再包含 `Release` 文本;`identifier=world.genarrative.ai-game-creator.release` 与 `release-win` 更新分区保持不变。
@@ -470,7 +520,7 @@
- 背景:AGC 项目对话曾把大量能力挂在「聊天输入 `/<cmd>`」上(`/history`、`/read`、`/help`、`/status`、`/trace`、`/export`、`/preview`、`/remember`、`/brief` 等),无 GUI 的终端 swarm chat 入口 `--swarm-chat` 又自带一套控制命令(`/help`、`/agents`、`/status`、`/history`、`/compact`、`/resume`、`/goal`、`/quit`)。两套入口都没有现役调用方,撤回成本却持续存在:命令字面量散落在前端命令分支、润色绕过、摘要模块、`swarm_cli` 终端输入解析、构建期门禁条目和文档承诺里,任何新对话形态都要额外维护这套死词汇表。
- 决策:斜杠命令语义与终端 swarm chat 入口整体退役,按「从未存在」处理。应用侧删除 Direct 聊天的 `/history` 精确匹配分支与 `reloadHistory`、`chatPromptPolish` 的 `/` 前缀绕过、`chatCommandMetadata` / `chatCommandHelp` / `memoryCommands` 的命令清单与参数解析、只服务退役 Supervisor 摘要面板的 `project-summary/*Summaries.ts` 与 `agentTrace.ts`、草稿回填死链(前端 `agentPresentation.ts` + Rust `suggested_canvas_tool_call`)、无人调用的 Tauri 命令 `get_game_creation_agent_capabilities` / `get_limited_local_commands`,以及钉住这些字符串的构建期门禁条目与专属测试。终端侧连同入口一并删除:`--swarm-chat`、`src-tauri/src/swarm_cli.rs` 与整个 `swarm_cli/` 目录(命令解析与帮助输出、turn 派发、观察器、报告、专属测试)、`SwarmChatFlow`、`SwarmTurnObservation`、`SwarmTurnOutcome::Quit`、`SwarmConfirmationResolution::Quit`、`SWARM_TURN_*_ERROR`、只服务这些命令的 `agent.compact` / `agent.resume` / `agent.run_status` 权限门禁与 `print_runtime_response_stream_status` 打印器,以及只服务终端交互内核的 `agent/interaction.rs` 整层(`AgentInteractionAction`、tool registry、`game_creator_agent_uses_interaction_kernel`、`decide_game_creator_agent_interaction_turn_for_session_at`、`AgentInteractionProviderStreamSink`);该文件只保留自然语言 steer 决策路径 `decide_game_creator_agent_runtime_steer_at`。真实 E2E 的交互式 CLI 管道、`scripts/agent-swarm-test-chat.mjs`、`agentSwarmTestEntry.test.ts` 与 `agc:test:chat` / `agc:test:chat:manual` / `agc:chat` / `agc:swarm` 等 npm 脚本同步删除。
- 保留项:命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(项目权限策略词汇表;App 前端只用 `GameCreationAppCommandDescriptor` 类型表达权限判定与审计粒度,数组本体由 Rust 策略路径消费)、`needsInitializedChatProject`,以及 `--agent-run` / `--agent-enqueue` / `--agent-steer` / `--agent-resume` / `--agent-context-compact` 等非聊天 CLI 控制命令与 Tauri IPC 注册。
- 保留项:命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(项目权限策略词汇表;App 前端只用 `GameCreationAppCommandDescriptor` 类型表达权限判定与审计粒度,数组本体由 Rust 策略路径消费)、`needsInitializedChatProject`,以及 `--agent-run` / `--agent-enqueue` / `--agent-steer` / `--agent-resume` / `--agent-context-compact` 等非聊天 CLI 控制命令与 Tauri IPC 注册。(2026-10-02:这些 CLI 控制命令与对应 Tauri IPC 已随自建 Agent Runtime 整体退役,见同日前条与 ADR。)
- 影响范围:`apps/ai-game-creator-shell/src/**`(Direct 聊天控制器、润色、`project-summary`、`project-workspace`)、`src-tauri/src/**`(`cli.rs`、`main.rs`、`swarm_cli` 整目录删除、`agent/interaction.rs` 收敛、命令注册、canvas 生成、provider / project 测试)、`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/**`、`scripts/check-config.mjs`、root 与 App 的 `package.json` 脚本、`tests/**`,以及 AGC 主实施计划文档、Runtime V1.1 文档与 `CONTEXT.md` 术语。
- 验证方式:`npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit`、`npm run --workspace apps/ai-game-creator-shell typecheck`(含 `check-config.mjs` 的脚本与门禁一致性)、`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`、`cargo check --tests`(告警消息集与基线一致)、`npm run check:encoding`、`git diff --check`;保留的 e2e 套件为 `supervisor-swarm`、`-transient-retry`、`-final-reply-transient-retry`、`-tool-plan-handoff-runner-kill`、`goal-runtime`、`response-stream`、`web-search`、`context-compaction`、`scoped-agents`、`project-skill`、`parallel-read`、`steer-runner-kill`、`process-session`。
- 关联文档:[【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22](../../adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md)、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。(2026-09-24:对应里程碑已完成并删除,结论以 ADR 为准。)
@@ -6573,7 +6623,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
## 2026-07-15 AI 游戏创作 Agent Runtime V1.21 token-aware 持久上下文压缩
- 顺序:MCP 动态工具目录与输出会进一步放大上下文,因此先补 Codex 风格 token-aware compaction,再进入 MCP。当前固定 12 条 conversation/observation 截断不再作为“已具备压缩”的完成证据。
- 配置:`llm` 增加 `contextWindowTokens=128000 / autoCompactTokenLimit=64000 / toolOutputTokenLimit=12000`,`agentLlm` 可逐 Agent 覆盖。预算估算必须包含 function schema;Provider usage 单独标记为真实值,不能与估算混用。
- 配置:`llm` 增加 `contextWindowTokens=128000 / autoCompactTokenLimit=64000`,`agentLlm` 可逐 Agent 覆盖。预算估算必须包含 function schema;Provider usage 单独标记为真实值,不能与估算混用。
- 边界:只压缩旧 Agent/legacy conversation 和当前 run 的旧 observation,保留最近精确 tail;Goal、任务、结构化计划、steer、pending action、project/repository revision、verification、process/join/delegate、receipt 和 finalization 身份保持规范事实,不进入摘要改写。
- 持久化:私有 `game-creator-runtime-context-compaction.v1` sidecar 绑定 Agent/Session、source prefix 指纹、可选 run、summary 指纹、预算与 usage;同源幂等,追加后 revision 单调,前缀漂移失败关闭。context bundle 只绑定压缩元数据,不复制 summary 正文。
- 请求安全:compaction 使用独立 Provider lifecycle、稳定 request slot、零工具和零 web search。未知 started 或 completed 后 sidecar 未提交均按 orphan barrier 进入 reconciliation,禁止自动重发;sidecar 已提交后恢复直接复用。
@@ -9436,6 +9486,38 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 客户端把后台 `codex` 映射到现有 Codex app-server,把 `cc` 映射到独立 Claude Code CLI adapter;不通过替换 Codex JSON-RPC 可执行文件实现。
- Claude Code 只使用隔离环境和 AGC loopback MCP,禁用原生工具;取消通过独立 Direct 回合进程树回收处理。Codex、provider 和自定义 Responses 链路保持原路径。
## 2026-09-30 内置工具错误改成每工具一个 typed enum,统一错误事件去掉 retryable
- 背景:`agc-tools` 的失败诊断把预算相关三类之外的所有工具失败都写成 `retryable=true`,`code` 由错误文案 `contains("validation-budget-exhausted")` 之类反推;余额不足、非法参数这类条件不变就不会恢复的失败也被呈现为「可重试」,并发排障时只能靠 `metadata.tool` 认是哪个工具出的错。
- 决策(工具侧):每个内置工具在自己的 `agent/tool/<tool>/error.rs` 里定义错误 enum,一个 case 一个变体,用户(以及转述给用户的模型)可见文案写在变体的 `to_user_msg()` 上;捕获处只调 `to_user_msg()`,不解析文案、不分类、不算重试标志。跨工具重复的 case(入参不是对象 / 不认识的字段、项目权限门禁、分页、清单读取、资源登记完成投影、客户端 Direct 回合门禁、宿主执行门禁、未登记工具名)只在 `agent/tool/error.rs` 定义一次,各工具用包装变体 + `From` 复用,不复制文案。
- 决策(宿主侧):工具失败诊断的 `code` 改成稳定的工具名(不再从错误文案反推),开发者信息进 `metadata`:`tool`、脱敏后的 `arguments`、`directTurn`、`dispatchDenied`;`clientTurnId` 仍按回合归属记录,桥未被回合授权时如实记 `null`。
- 决策(schema):统一错误事件(`.agent/runtime/errors/<eventId>.json` 与应用日志身份行)去掉 `retryable` 字段,`AGENT_RUNTIME_ERROR_SCHEMA_VERSION` 升到 `agent-runtime-error.v2`。Direct Codex 用户可见文案里的 `direct-codex-failure:v2 … retryable=…` 由 typed `DirectTurnError::is_retryable()` 判定,不依赖这个字段,前端解析不受影响。
- 决策(MCP 预检收口,同日续做):独立客户端 MCP 的 `validate_*` 不再自带一套文案,改成调用工具桥同一份入参规则,再用该工具错误 enum 的 `to_user_msg()` 渲染 MCP 结果:`write_file_input`、`list_registered_assets_input`、`list_project_files_input`、`list_account_assets_input`、`account_asset_import_inputs`、`resource_generation_input`、`remove_background_input`、`generate_image_input`、`edit_image_arguments`、`prepare_game_art_input`、`web_search_input`、`editor_execute_code_input`、`cocos_execute_code_input` 都是「工具桥校验 + MCP 预检」共用入口;`ToolArgumentsRejection`(入参不是对象 / 不认识的字段)同时供 MCP 与 Runtime 侧虚拟工具观测(`agent/runtime_tools/context.rs`)复用。
- 收口时显式对齐的两处语义差异:① `agc_remove_background` 的 `backgroundMode` / `screenColor` 传显式 `null` 与省略等价(与工具桥其他可选字段同一口径),MCP 不再单独拒绝 `null`;② `.hermes` 并入 `bridge_project_file_is_hidden_control_path` 的保护目录集合,工具桥与 MCP、`preview.rs` 的受控目录口径一致。
- 改动范围:`agent/tool/**`(`error.rs` + 每个工具的错误模块;工具实现仍留在 `direct_tool_bridge.rs`,不搬家)、`agent/direct_tool_bridge.rs`、`agent/direct_tools_mcp.rs`、`agent/runtime_tools/context.rs`、`agent/runtime_error.rs`、`agent/runtime_state.rs`、`agent/direct_runtime/mod.rs`;同步去掉 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 与 `docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md` 身份行字段表里的 retryable。
- 验证:`cargo test --bin genarrative-ai-game-creator-shell agent::` 951 passed / 0 failed(5 ignored);`cargo check --tests` 通过;`npm run check:encoding`、`git diff --check` 通过;前端 `resourceCanvasAssetGenerationQueue`、`resourceCanvasGenerationHostLifecycle`、`appSurface` 套件通过(「项目权限策略拒绝执行:{id}」对外文案保持原字面)。
- 边界(未完成):Windows 专属的 Cocos / Unity / Godot 执行工具(含 MCP 预检)已按同一口径实现,但只做了交叉配置编译校验(Linux 上临时放开 `windows` cfg 后 `cargo check --features cocos-editor-execute,unity-editor-execute,godot-editor-execute --tests`),没有 Windows 真机构建;`agent/direct_validation.rs` 的 Runtime 动作(`run_command` / `run_browser`)与 `direct_tools_mcp.rs` 里 MCP 专有记录协议(Codex 返回记录、skill 资源读取)仍用各自的字符串错误,它们是 Runtime 动作 / MCP 协议而非内置工具。
## 2026-10-01 统一错误事件收成一个 message(去掉 public_text / recovery_hint / detail)
- 背景:`AgentRuntimeErrorEvent` 的三个文本字段在工具桥这条路径上完全退化——`publicText` 与 `detail` 逐字相同,`recoveryHint` 是常量「查看项目错误诊断后处理」,而且三个字段同时进 sidecar 与日志,读者分不清哪个才是「要给人看的话」。核对确认这个事件没有任何读取方:三处调用都是 `let _ = persist_agent_runtime_error(...)`;前端读的 `publicText` 属于 `AgentRuntimeEventRecord`(`runtime_state.rs` 的 runtime event,`app/types.ts` + `features/agent-runtime/model.ts` 的 `formatAgentRuntimeEvent`),是另一个结构;`.agent/runtime/errors/*.json` 只有测试在读,`read_agent_runtime_error_detail` 命令早已退役。这条链路的用途就是写日志与诊断包。
- 决策(字段):事件只保留一个 `message`——由产生失败的 typed 错误在失败现场写好的人类可读文案(工具侧就是 `ToolFailure::to_user_msg()` 的那一句);删掉 `recovery_hint` 与 `detail` 两个字段及其入参。开发者信息不另开字段,全部进 `metadata`:`tool` + 脱敏 `arguments` + `directTurn` / `dispatchDenied`,即「工具 + 参数(脱敏)+ 上下文」。`persist_agent_runtime_error` 由 10 个入参减到 8 个,`agent_runtime_error_app_log_lines` 同步收口。
- 决策(schema 与日志):`AGENT_RUNTIME_ERROR_SCHEMA_VERSION` 是 `agent-runtime-error.v2`,sidecar 由 `publicText / recoveryHint / detail` 收成 `message`(无读取方,不需要兼容层)。应用日志详情行由 `hint=… summary=… detail=… metadata=…` 收成 `message=… metadata=…`,身份行不变(本来就只放程序生成与调用方常量字段)。`message` 在应用日志里取原 `detail` 的 1200 字符预算、在 sidecar 里按 8 KiB 上限:用户只上传 AppData 应用日志、拿不到 sidecar,日志这一份必须是信息量最大的那份。
- 决策(保留项):`direct-codex-failure:v2 … retryable=… summary=…;建议:…` 是前端(`features/agent-runtime/model.ts` 的 v2 正则)要解析的用户可见文案,`retryable` / `recovery_hint` 由 typed `DirectTurnError::is_retryable()` / `recovery_hint()` 判定,原样保留,只是不再进统一事件;`direct-codex` 路径把信息量最大的 `failure.to_string()`(typed Display)作为 `message`。`runtime_state.rs` 那条改为只记原始 `error`(投影后的用户文案已写进 `project.jsonl`,不必再存一遍)。
- 改动范围:`agent/runtime_error.rs`、`agent/runtime_state.rs`、`agent/direct_tool_bridge.rs`、`agent/direct_runtime/mod.rs`;同步修正 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 里 2026-09-15 的「统一事件至少包含 …」字段表与 2026-09-21 的日志两行口径。
## 2026-10-01 工具失败改成 typed 错误穿出 dispatch 边界,统一事件携带原始 error
- 背景:工具桥的派发边界把失败吞成 `Value`——`bridge_outcome(root, result)` 内部就把 typed 错误压成一句 `bridge_tool_failure` 文案,`isError` 由工具与桥手传;诊断只能从结果值里回读 `/content/0/text` 与(上一阶段临时挂在结果上的)`error` 键。结果是「哪一轮、哪个工具、什么结构化事实」在派发期间被降级成字符串,composer 只能靠 `isError=true` 反推分支。
- 决策(dispatch 边界):`handle_direct_tool_bridge` 里的 `dispatch` 现在返回 `Result<Value, ToolCallError>`,失败原样穿出,不再在派发期压成 `Value`。`ToolCallError { message, redact_limit, error }` 是唯一载体:泛型 `impl<T: ToolFailure + ?Sized> From<&T>` 把具体错误 enum 的 `to_user_msg()`、`redact_limit()` 和原样序列化一起带出来——不做跨工具大 enum,不 Box(`?Sized` 让 trait 对象也能转)。`bridge_outcome` 随之删除。
- 决策(composer):工具桥只有一个出口 `compose_direct_tool_outcome(state, tool, arguments, dispatchDenied, outcome)`:`Ok` 补 `isError=false`,`Err` 补 `isError=true` 并落一次诊断。`isError` 由分支决定,工具与桥都不再手传;MCP 完成包仍必须带布尔 `isError`(`direct_tools_mcp.rs::call_client_tool_bridge` 会校验),所以两条分支都写。五条早退(付费 / 写入 / 执行许可取不到、许可任务丢失、结算未落盘 `ReceiptNotPersisted`)也走同一出口:它们以前直接 `return Json(bridge_tool_failure(…))`,不写诊断。
- 决策(诊断字段):`AgentRuntimeErrorEvent` 新增 `error`——产生失败的 typed 错误 enum 的原样序列化,`Value::Null` 表示该调用方(`agent/runtime_state.rs` 的终态公开消息)没有 typed 错误;`message` 仍是给人(以及转述给用户的模型)的那句话。应用日志详情行由 `message=… metadata=…` 扩成 `message=… error=… metadata=…`,`error` 取 400 字符预算(`AGENT_RUNTIME_ERROR_APP_LOG_ERROR_CHARS`)。`AGENT_RUNTIME_ERROR_SCHEMA_VERSION` 升到 `agent-runtime-error.v3`(sidecar 无读取方,不需要兼容层)。sidecar 里的 `error` 与既有 `metadata` 一样按原文落盘、只在应用日志里脱敏:sidecar 留在项目内不上传,上传的日志那份已经过 `redact_agent_runtime_error`。
- 决策(周边收口):`bridge_tool_result(text, images)` 去掉 `is_error` 入参,模板只剩 `{ content }`;`record_completed_regeneration` 不再用 `isError` 判「能否缓存这次美术重生成」——走到那里的只有 `Ok`(失败会 `?` 上抛),`ArtRegenerationAuthorizationRejection::CompletedResultNotSuccessful` 因此退役。
- 决策(软失败清零,同日续做):不接受「工具调用本身成功、载荷里写着失败」的软失败,十处全部改成各工具自己的 typed 变体,载荷原样进变体、由 `to_user_msg()` 拼成给模型的文案:`ImportAccountAssetsError::ImportFailed / ImportPartial`、`RunValidationError::ValidationNotPassed`、`BrowserPlaytestError::PlaytestNotPassed`、`EnvironmentCheckError::EnvironmentNotReady`、`ApplyPatchError::PatchNotApplied`、`EditorExecuteError::ExecutionNotCompleted / ExecutionUnconfirmed`、`CocosExecuteError::ExecutionBusy / ReconciliationPending / ExecutionNotCompleted`。于是 `isError` 与执行租约的 `passed`(`result.is_ok()`)重新对齐:execute 类失败仍落 `ExecutionPhase::Draining`。
- 决策(证据图):`ToolFailure` 增加 `attached_images() -> Vec<String>`(默认空),`ToolCallError` 增加 `images`;composer 的 `Err` 分支在 `bridge_tool_failure` 的成文结果上追加 MCP image block。带图的两个变体(`ValidationNotPassed` / `PlaytestNotPassed` / `ExecutionNotCompleted`)把截图正文放在 `#[serde(skip)]` 字段里:图要回到结果里,但 base64 不能进 sidecar 的 `error` 正文。`bridge_validation_result` 由泛型 `?` 改成接受一个 `fn(Value, Vec<String>) -> E` 构造器,验证与试玩共用同一份 `passed` / 截图投影。
- 改动范围:`agent/direct_tool_bridge.rs`、`agent/runtime_error.rs`、`agent/tool/error.rs` 与 23 个 `agent/tool/<tool>/error.rs`(补 `serde::Serialize`,`EditorKind` 同样补)、`agent/tool/run_validation/error.rs`、`agent/tool/browser_playtest/error.rs`、`agent/tool/environment_check/error.rs`、`agent/tool/apply_patch/error.rs`、`agent/tool/import_account_assets/error.rs`、`agent/tool/editor_execute/error.rs`、`agent/tool/cocos_execute/error.rs`、`agent/tool/prepare_game_art/error.rs`、`agent/generation/canvas_generation.rs`(并发测试改用 `Result`)、`agent/runtime_state.rs`、`agent/direct_runtime/mod.rs`;同步修正 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 的统一事件字段表与应用日志两行口径。
- 验证:`cargo check --bin genarrative-ai-game-creator-shell --tests` 通过(无新增警告);Windows 专属的 Cocos / Unity / Godot 执行路径在 Linux 上临时去掉 `#[cfg(all(windows, …))]` 的 `windows` 条件后,用 `cargo check --features cocos-editor-execute,unity-editor-execute,godot-editor-execute --tests` 交叉编译校验通过,随后原样还原(没有 Windows 真机构建);`cargo test --bin genarrative-ai-game-creator-shell -- agent::` 951 passed / 0 failed(5 ignored);`-- agent::direct_tool_bridge:: agent::direct_tools_mcp:: agent::runtime_error::` 73 passed;`-- agent:: tests::project::` 1076 passed / 2 failed,两条都是并行负载下的已知 flake(`agent::runtime_actions::provider_request_builders::tests::art_director_request_exposes_canvas_only_for_the_keyed_owner_route` 与 `tests::project::background_agent_runtime_can_generate_platform_art_asset`),单跑各自通过;`npm run check:encoding`、`git diff --check` 通过。
## 2026-09-30 合入 master 时把 Claude Agent SDK sidecar 归位到随包资源准备步骤
- 背景:master `fb130d184` 新增 `cc` 执行模式与 Claude Agent SDK sidecar,sidecar 的 staging 写在 `build.rs`(构建期 `remove_dir_all` + 从 `node_modules/@anthropic-ai/**` 复制 `resources/claude-agent`),同时把 `resources/claude-agent` 映射进**基线** `tauri.conf.json`。本分支的 M1–M3(issue #519)已把「构建期写随包资源」定性为结构问题,合入时必须按同一套架构落地,不能把写入分支带回来。
+27 -69
View File
@@ -341,8 +341,8 @@ Copy Artifact 插件在**非 SYSTEM 认证**下按「认证用户」判权:只
- 现象:「生成背景音乐」再次提交 0.1 秒就失败,卡片只有 `remote-terminal-failed: 远端资源编辑已明确失败,不允许再次请求`,既没有原因也没有下一步。
- 原因:上一次同 `operationId` 的请求被平台确定性拒绝(HTTP 400 或任务 `failed`)后,账本落到 `remote-failed`,之后所有重试都在 `ensure_resource_edit_phase_resumable` 失败关闭;唯一出口是「待恢复资源编辑」里的移出恢复队列,但终态文案没有指向它。
- 处理:终态文案带出稳定失败码,并明确「先在待恢复资源编辑中把它移出恢复队列」;上游失败原文仍不写入账本(只存分类码),首次失败的原始拒绝说明继续由当次错误文案承担。
- 验证:`remote_failed_status_is_terminal_and_can_only_be_archived`、`submission_bad_request_is_terminal_while_gateway_failure_requires_reconciliation` 等资源编辑用例继续通过,账本序列化不含上游失败原文。
- 处理:终态文案带出稳定失败码,并明确「先在待恢复资源编辑中把它移出恢复队列」;上游失败原文仍不写入账本(只存分类码),首次失败的原始拒绝说明继续由当次错误文案承担——轮询终态这条路由此改成把平台 `error` 原文装进 `ResourceEditError::RemoteGenerationFailed` 原样带出(2026-10-02),工具层经 `RemoteResourceEditFailure` 转发时保留 `error` 与 `phaseDetail` 两个原始字段(`phaseDetail` 只进诊断,不当用户文案);`to_user_msg` 只给 `error` 原文,平台没给就说「服务器未返回错误信息」。前缀由使用它的工具/命令自己加(不再统一压成「资源编辑生成失败」一句,也不再多一层无信息前缀);第一句失败文案不再带 `remote-terminal-failed:` 前缀。
- 验证:`remote_failed_status_is_terminal_and_can_only_be_archived` 断言失败文案带出平台 `error` 原文、同时账本序列化不含原文;`background_removal_remote_failure_keeps_manifest_without_result` 覆盖平台没给 `error` 时的兜底文案;`submission_bad_request_is_terminal_while_gateway_failure_requires_reconciliation` 等资源编辑用例继续通过。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/resource_editor.rs`。
## Tauri `--no-sign` 会连带跳过 updater 签名
@@ -524,7 +524,7 @@ Direct 工具桥会 canonicalize 项目根,事件中的路径可能带 `\\?\`
- **现象**:把 `Native shell tests` 拆成客户端三个 job 后,如果只跑 `npm run check:native-shells:release`,静态契约和壳运行时门禁都不会执行;如果只跑 `--groups=contract`,`desktop-release-binary-artifact` 又会因为缺少 `build/native/desktop/` 产物而失败。
- **原因**:分组是执行范围,不是"额外检查"。`desktop-release-binary-artifact` 断言依赖同 job 内的 `desktop-shell-stage-release-binary` 步骤,所以它归 `release` 组,不能放进 `contract`;反过来,任何"只跑一组"的命令都不能被当成完整门禁。
- **处理**:分组与 job 的对应关系固定为 `contract`+`shells`+`release` → `Native shell tests`,`agc-web` → `AI game creator shell web tests`,`agc-rust-shard-1..2` → `AI game creator shell Rust lane 1/2`,`agc-rust-shard-3..4` → `AI game creator shell Rust lane 2/2`,`agc-rust-smoke` → `AI game creator shell Rust smoke`,`agc-rust-crates` → `AI game creator shell Rust crates`;`scripts/project-ci-workflow.test.ts` 校验"每个分组恰好被一个 lane/job 调用一次"和"CI 不再调用全量 `npm run check:native-shells`",新增分组必须同步门禁脚本、根脚本与 workflow 三处。
- **易错点**:① 拆 job / 改 job 名后要确认分支保护里没有残留已不再上报的旧 job 名(本仓库现在不配 required context,只需人工确认 CI 结果,见置顶条目的「分支保护口径」);② 每个 job 只预热自己会构建的 Cargo 依赖,`agent-run:smoke` 因为会 spawn `cargo` 必须与 AGC 壳的依赖预热同 job;③ 本地全量 `npm run check:native-shells` 仍会串行跑完所有分组,用它作为本地完整门禁,不要用单组脚本冒充。
- **易错点**:① 拆 job / 改 job 名后要确认分支保护里没有残留已不再上报的旧 job 名(本仓库现在不配 required context,只需人工确认 CI 结果,见置顶条目的「分支保护口径」);② 每个 job 只预热自己会构建的 Cargo 依赖,需要 spawn `cargo` 的 smoke 必须与 AGC 壳的依赖预热同 job(原 `agent-run:smoke` 已随自建 Agent Runtime 退役,2026-10-02);③ 本地全量 `npm run check:native-shells` 仍会串行跑完所有分组,用它作为本地完整门禁,不要用单组脚本冒充。
- **关联**:`.gitea/workflows/project-ci.yml`、`scripts/check-native-shells.mjs`、`scripts/project-ci-workflow.test.ts`、`.gitea` 分支保护设置。
## 2026-09-24 重复 `#[test]` 属性会让 Rust 分片门禁报「同一用例被分到两片」
@@ -1009,16 +1009,6 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 验证:AppSurface 先用全新但内容相同的 Agent 结果数组 rerender,断言图读取仍只有一次且原卡片 DOM 保持连接;再增加真实资源并延迟第二次图响应,断言旧卡片在刷新窗口持续挂载,新图返回后新增卡片正常出现。
- 关联:`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`。
## Supervisor steer 不能只有内部排队事件(2026-08-10)
- 现象:自主制作期间继续向项目总控发消息,用户消息已进入同一 Run,当前 Provider 也被中断并重新规划,但普通工作台短暂的提交状态消失后一直没有回复,直到整轮制作最终收束。
- 原因:steer 只持久化用户消息与内部 `steer.queued` 事件;公开事件投影又明确排除 `steer.*`。普通工作台提交后会用后端 conversation 覆盖本地消息,因此仅追加临时前端气泡也无法稳定跨刷新显示。
- 处理:根 Project Supervisor 的 steer 进入 durable `queued` 后,先写“正在判断、当前任务继续”的公开确认,再由独立 `steer-decision` LLM turn 返回自然语言回复和 `interruptCurrentProvider`。状态询问、解释和不冲突补充默认不中断;明确停止、改向或会使在途方案过期时才允许请求中断。判定和回复按 `run + steer` 持久幂等,刷新后仍可见;判定失败时继续当前任务,并在下一安全边界应用 steer。
- 并发边界:steer 入队、Runner `runtime.steer` 通知都不得直接触发 Provider interrupt。Codex app-server 的判定使用独立节点,不能等待主节点 turn 锁;判定为 true 后也只能中断 `appliedSteerCursor < steer.sequence` 的旧 Provider 请求,已经消费该 steer 后启动的新请求不可被误杀。已经开始的工具和外部动作不强杀,完成 observation 后再消费 steer。
- 验证:真实 mock LLM 回归必须覆盖状态询问回复且 `interruptCurrentProvider=false`;持久重放只保留一条语义回复;steer 入队后旧 Provider 继续运行,判定为 true 后才中断;新规划 Provider 的 cursor 已包含该 steer 时即使旧判定为 true 也不能中断。前端同秒多条消息保持“用户补充 → 判断提示/语义回复”的关联顺序。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.rs`、`apps/ai-game-creator-shell/src-tauri/src/runner/dispatch.rs`、`apps/ai-game-creator-shell/src/features/agent-runtime/model.ts`。
- 2026-09-23 更新:`agent/interaction.rs` 与 `steer-decision` LLM 判定链已整体删除,本条中「判定 LLM / `interruptCurrentProvider` / 只中断旧 cursor」的实现细节仅作历史记录;现役语义是 steer durable 入队后由 `runtime.steer` 唤醒,并在下一安全边界应用。
## Jenkins 异步备份不能用 nohup 脱离作业
- 现象:Stdb Publish 成功,上传日志只留下“已获取进程锁 / 上传已有备份 / 目标对象”,没有成功或可捕获错误;本地 tar.gz 和 `uploadStatus=deferred` manifest 每次发布后继续增长。
@@ -1051,30 +1041,6 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 验证:锁定 `generate-ui-design.image_size = ["1K", "2K"]`,三个可切换图片模型的工具都接入共享 `gpt-image-2 -> image_size = ["1K", "2K"]` 条件,以及视频 fast 条件没有内层 `required`、其 `then.resolution = ["480p", "720p"]`;同时保留运行时拒绝 `gpt-image-2 + 0.5K` 与 `seedance2.0-fast + 1080p` 的测试。
- 关联:`server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs`、`server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs`、`server-rs/crates/platform-editor-agent/src/agent/tools/generate_video.rs`、`docs/【编辑器】画布Agent对话面板-2026-07-03.md`。
## 重复成功的 agent.message 不能被当成新的 Runtime 进展
- 现象:专业 Agent 已把一条定向消息写入目标 Session,却在后续 planning 中反复发送相同正文;目标会话看起来没有重复消息,但 Provider 请求持续增长,run 可能长期不返回自身终态回执。
- 原因:conversation 层的 messageId 幂等只能阻止重复落盘。若每个新 Runtime action 的 `status=ok` 都进入上下文进展指纹,相同 durable no-op 会不断刷新 6 轮停滞窗口;只检查目标会话条数无法证明 action loop 已有界收束。
- 处理:消息语义键必须包含来源 Agent/run、目标 Agent/已解析 Session 和清洗截断后正文 SHA-256;conversation message、`conversation.message` 和 `agent.runtime.agent.message` 各自 exactly-once。重复调用继续完整记录自己的 action/observation/receipt,但私有 observation 固定返回 `messageAppended=false`,ContextWindowTracker 只忽略这一精确 no-op,不能忽略不同正文的新消息。专业 Agent prompt 同时明确中途消息不能替代自身 final response。
- 验证:`background_agent_runtime_bounds_duplicate_agent_message_livelock` 必须真实驱动 6 个相同指纹、不同 actionId 的消息动作,证明 action/observation/receipt 各 6 条,目标消息和两类消息审计各 1 条,后 5 次不算进展,第 6 轮保留 `in_progress` 计划并进入 `budget-exhausted`,没有第 7 次 Provider 请求、context compaction 或 completed。另保留 `agent_runtime_context_window_counts_distinct_agent_message_bodies`,防止把真正不同的新消息误压成 no-op。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`apps/ai-game-creator-shell/src-tauri/src/tests.rs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## Swarm E2E 的隔离 AppData 不能建在正式 AppData 里面
- 现象:真实 suite 自称使用隔离配置,但一次性 AppData 出现在正式 AppData 子目录;源目录 watcher、配置副本计数和清理归属变得含糊,Runner 还可能把临时 endpoint 或运行态写进正式目录树。
- 原因:把 `mkdtemp` 前缀拼在 source config dir 内,只隔离了文件名,没有隔离目录所有权;source-dir guard 无法区分 suite 自己的合法子目录写入与污染,失败清理也可能触碰正式目录边界。
- 处理:需要保护正式配置的 suite 一律在 `dirname(realConfigDir)` 下创建 sentinel 管理的 sibling AppData,并要求 realpath 后与源目录同父、互不包含。配置只使用私有副本或受控 hardlink/overlay,启动 CLI/Runner 全部指向 sibling;清理前核对 sentinel、源配置 inode/hash/link count、source-dir 前缀事件、正式 endpoint 身份和正式 CLI 调用计数,随后只删除拥有明确 token 的临时目录。
- 验证:真实报告必须同时满足 `isolatedAppDataUsed=true`、`sourceAppDataDirectoryUntouched=true`、`sourceRunnerEndpointUnchanged=true`、`formalConfigCliCallCount=0`、配置副本校验和 `AppDataCleanupPerformed=true`;项目选择 `--keep-project` 时也不能改变 AppData 自动清理。
- 关联:`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## 异步 Runtime 测试不能把 child idle 当成终态结果已发布
- 现象:isolated child 已显示 idle,单次 all-join reconcile 却偶发返回空列表;或者 Runtime 已显示 completed / failed,Goal、conversation、Agent DB 审计和 per-Agent lock 仍未完成,完整 Rust suite 里出现低概率失败,单独重跑通常通过。
- 原因:Runtime state、Goal sidecar、终态 result、conversation、审计记录、handoff 清理和执行 lane 释放不是同一个原子观测点;测试只等待 idle / failed 会在同一后台 drain 的 durable 收尾前抢先断言。
- 处理:产品协议仍以 durable terminal result 和 join readiness 为准。测试在有界时限内等待业务目标终态;需要断言同一 drain 的后续副作用时,同时以 per-Agent runtime task lock 释放为 fence,命中后重新读取投影。join 场景继续重复调用幂等 reconcile,直到取得唯一 join 或超时;不得靠固定长 sleep,也不能因为第一次为空就把协议改成吞掉未完成 child。
- 验证:`isolated_agents_with_same_template_run_independently_and_join_once` 最多执行 100 次、每次间隔 20ms 的 reconcile,并继续断言只有一个 all-join 和一次父唤醒;Goal、loop-budget、finalization 与 Supervisor reconciliation 测试必须在目标 status / phase 与 Agent lane 同时收束后再读取最终副作用。`background_agent_runtime_marks_response_plan_step_failed_when_final_reply_fails` 和 `background_agent_runtime_tasks_can_run_in_parallel_and_persist_replies` 同样必须经过该 fence 后再断言 `turn.failed` 或 `agent.runtime.completed` 审计。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/tests.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`。
## CI root 环境不能用文件只读权限注入写失败
- 现象:本地测试把 conversation 文件设为 readonly 后能稳定得到写入失败,Gitea Actions 中同一断言却发现写入成功并继续执行任务。
@@ -1083,14 +1049,6 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 验证:在普通本地用户和 root 容器中分别运行用户消息、assistant 最终回复持久化失败测试,均应进入相同 durable phase 并通过恢复断言。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`apps/ai-game-creator-shell/src-tauri/src/tests.rs`。
## Ubuntu 容器不能把 chromium-browser 的 Snap 占位包当成 CI 浏览器
- 现象:AI 游戏创作壳的 1132 条 Rust 测试全部通过,尾部 `agent-run:smoke` 却以 `spawn google-chrome ENOENT` 失败;直接给 Ubuntu 24.04 job 安装 `chromium-browser` 仍拿不到可执行浏览器。
- 原因:Ubuntu 24.04 仓库里的 `chromium-browser` 是 Snap 过渡包,普通 Docker job 没有 snapd 宿主能力;固定 job image 也不预装 Google Chrome。脚本回退到命令名 `google-chrome` 后只能在本机通过,在干净 Runner 中必然 ENOENT。
- 处理:Native job 通过 Google 官方签名 APT 源安装 `google-chrome-stable`,安装后先执行 `google-chrome --version`;smoke 继续真实启动 headless 浏览器验证 DOM / canvas,不允许因 CI 缺浏览器而跳过或降级为静态 HTTP 检查。
- 验证:固定 Ubuntu 24.04 job image 内先确认 `apt-cache policy chromium-browser` 仅为 Snap 占位,再安装官方签名包并运行 `google-chrome --version`;Gitea Native job 最终必须在 1132 passed / 5 ignored 后继续通过 `agent-run:smoke`。
- 关联:`.gitea/workflows/project-ci.yml`、`apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs`。
## PTY 测试不能假设输入回显与后续输出必然分行
- 现象:PTY 环境隔离用例偶发得到 `你好BRIDGE_ENV:`,而不是独立的 `你好` 与 `BRIDGE_ENV:` 两行;真实私有环境变量并未泄漏,但整行相等断言失败。
@@ -1099,30 +1057,6 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 验证:`process_session_pty_uses_private_environment_and_redacts_public_records` 对 `BRIDGE_ENV:` 使用行尾匹配,并保留真实私有环境变量、stdin 正文与公共记录泄漏扫描。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/process_session.rs`、`apps/ai-game-creator-shell/src-tauri/src/command_output.rs`。
## 自主 Swarm 验收不能把父 project.verify 当成意外确认动作
- 现象:两个专业 Agent 已完成初始交付,Supervisor 在语义 repair 前合法执行项目宿主验证,但 E2E harness 把所有父 run pending action 一律拒绝,导致真实协作链在业务逻辑正常时提前失败。
- 原因:验收器把“repair 前不允许父 Agent 绕过专业工作”错误实现成“父 run 不能出现任何确认动作”,混淆了 Supervisor 自己的 `project.verify` 与会改变专业交付/文件的意外动作。
- 处理:确认过滤器必须按 owning run 和 tool 精确判断。repair 前允许当前父 run 的 `project.verify`,仍拒绝其它未列入场景合同的父 pending action;专业 Agent 的修改和验证继续按各自 run、policy 和预期确认集合处理。允许确认不等于通过验收,最终仍由 host oracle、最新 revision verification、delivery/claim/repair 和唯一回复共同裁决。
- 验证:自主 suite 必须出现有效 `hostVerificationPassed=true`,同时保持恰好 2 个初始 + 1 个 repair delivery、父计划完成、意外 pending 为 0、Runner 强杀恢复和唯一 Supervisor assistant;若放宽后出现额外父写动作,场景必须失败而不是吞掉。
- 关联:`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## Provider 全成功的真实报告不能证明显式重试可用
- 现象:真实 Swarm 报告显示全部 Provider lifecycle completed,E2E 的 retry validator 也没有报错,于是文档把“支持瞬态重试”一并写成已真实验收。
- 原因:validator 只在实际出现 failed lifecycle 时校验 retry audit;`failed=0 / retry=0` 会自然通过。随机等待外部网络故障既不可重复,也无法在故障和重试之间证明副作用仍为 0。
- 处理:为重试单独建立 fail-first loopback proxy。在正式 AppData 同级创建 sentinel 管理的一次性目录,只覆盖其中一个目标 Agent 的 base URL 和重试配置;首个 POST 在正文进入 upstream 前断线,第二个请求由 forwarding gate 暂停。gate 内交叉检查 failed lifecycle、retry audit、request slot/identity、action、pending、receipt、delivery、claim、assistant、project revision 和目标产物,再显式放行真实 Provider。代理不能记录 URL、headers 或正文,不能跟随 redirect,必须可幂等清理;启动 CLI/Runner 时同时设置合并后的 `NO_PROXY / no_proxy` 并显式加入 loopback,不能假设开发机已正确配置代理绕过;source-dir guard 禁止本 suite 前缀进入源目录,配置和 endpoint 身份保持只读并逐字复核。不要用源目录 mtime/ctime 归因,正式 Runner heartbeat 会并发改变它。完整链在 checkpoint 后失败时,partial report 也要保留已取得的 identity、slot 和零副作用证据,不能退回模板默认值。
- 验证:`npm run test -- apps/ai-game-creator-shell/tests/llmTransientFaultProxy.test.ts` 覆盖故障、暂停、base path、流式转发、fallback、隐私和清理;`npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-transient-retry-real-e2e -- --config-dir <AppData>` 必须得到恰好 1 failed/1 retry、重试前副作用全 0,并继续通过完整 Swarm/Runner 恢复和零泄漏门禁。
- 关联:`apps/ai-game-creator-shell/scripts/llm-transient-fault-proxy.mjs`、`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## Agent 真实验收的阶段等待必须同步观察 Runtime 终态
- 现象:真实 Provider 已因 transport、格式修复或其它不可恢复错误把 task/Runtime 写成 failed,专项验收仍在等待某个 pending action、observation 或 receipt,直到 30 分钟总超时才返回。
- 原因:阶段等待只轮询“想看到的成功证据”,没有同时读取 owning Agent/run 的最新 task 与 Runtime phase;外部错误发生在该证据之前时,目标条件永远不会出现。
- 处理:所有分钟级阶段等待都要在每轮先检查 owning task 的 failed/cancelled/budget-exhausted,以及 Runtime 的 needs-reconciliation;命中后立即抛出带阶段前缀的结构化错误。正常 pause 必须保留为可恢复状态,不能被 fail-fast 当失败;Runner 强杀后的 paused 稳定窗口继续按签名零推进单独验证。
- 验证:用正式 `goal-runtime` 观察 Provider repair transport failure,确认部分报告立即保留成功/repair 协议计数、生命周期闭合和零泄漏证据;随后完整复跑仍能通过 Goal edit/pause/Runner kill/resume/finalization,证明 fail-fast 未破坏正常恢复路径。
- 关联:`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
## 图片生成的 K 档不能靠回图后缩放实现
- 现象:用户选择 2K 时占位框看起来是 2K,最终资源元数据也显示为 2K,但模型请求实际仍是固定 1K 或竖版回落尺寸;画面只是后端放大后的低分辨率结果。
@@ -6325,3 +6259,27 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **处理**:夹具补 `None` 协议参数与 `selected_model_protocol: None`,不改任何断言口径。
- **验证**:`cargo test --features=… --bin … tests::configuration::` 45 passed。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs`、`apps/ai-game-creator-shell/src-tauri/src/commands.rs`、`apps/ai-game-creator-shell/src-tauri/src/main.rs`。
## 2026-10-02 AGC 的 cc 路由被本机 `ANTHROPIC_*` 环境顶掉,官方模型全发到用户个人中转
- **现象**:用户终端里给 Claude Code 配了自己的中转(`ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`)。AGC 选 `claude-opus-5-5` 发消息时好时坏:偶尔两分半回一句话,多数回合一路静默到 `requestTimeoutMs`(180s)超时,日志只有 `claude-sidecar-start` 加 `claude_sidecar_idle idleSeconds=30/60/…`。同一时间平台网关、模型目录、账号 Router 分组怎么改都没反应。
- **根因**:`claude_code_cli.rs` 先把 AGC 进程继承来的 `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` 原样复制给 sidecar,再让它们优先于平台会话(`claude_base_url` 里 `std::env::var("ANTHROPIC_BASE_URL")` 排在 `.or_else` 最前,凭据有 `if … is_none()` 守卫因此永远不覆盖)。实测:直接跑 sidecar 探针,`claude.exe`(PID 43256)的两条 443 连接落在 `198.18.2.60` = 用户个人的 `yunyi.rdzhvip.com`,完全没碰 `dev.genarrative.world` / Router;同一个 token 对 `POST https://yunyi.rdzhvip.com/claude/v1/messages` 45 秒不返回(Bearer 与 `x-api-key` 都一样)。用户 `~/.claude/settings.json` 里的 env 反而无关——sidecar 用 `settingSources: []` 关掉了设置文件来源,事故来源是**进程环境**。
- **现行口径**:cc 的路由与凭据只由 AGC 决定,本机 `ANTHROPIC_*` **完全不参与**(`claude_code_route` 的入参里没有进程环境)。官方模型 + 平台会话 → `{apiBaseUrl}/api/llm/anthropic` + `Bearer <平台 access token>`;`customEnabled` 或配了 `llm.apiKey` → AGC 配置的 baseUrl(自定义端点用 `x-api-key`,其余保持 Bearer);没登录也没配 key 就报「cc 模式需要先登录陶泥儿账号」,不再退回本机环境。`ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` 也不再进 `copy_env` 白名单,只由解析出的路由下发。个人中转要走 AGC 的 `customEnabled` + baseUrl + apiKey,不做隐式继承。
- **同时(配置隔离)**:sidecar 子进程把 `HOME`、`USERPROFILE`、`CLAUDE_CONFIG_DIR` 全部指向项目内隔离目录(`.agent/runtime/claude-code/home`)。只改 `HOME` 不够——Windows 上 Node 的 `os.homedir()` 只看 `USERPROFILE`,Claude Code 会照样读用户自己的 `~/.claude.json`(个人 `mcpServers`、插件市场、凭据)并把 `projects/`、`sessions/` 写回真实 profile。实测隔离前 CLI 先花 ~90 秒在个人配置上、真实 `~/.claude/sessions` 每次回合都被写;隔离后 `time_to_request_ms=41`,且只写隔离目录。
- **同时(超时语义)**:`requestTimeoutMs`(默认 180000)在 cc 执行器里只作**静默预算**:sidecar 每有一条事件就重置,连续静默超过预算才按 `sidecar-turn-timeout` 收口;另有 `CLAUDE_CODE_TURN_MAX_DURATION`(45 分钟)兜底事件流假活,正常情况更早到的是 DirectProject 的 `maxTurnSeconds`。原来「整回合 180 秒墙钟」会把「网关慢但仍在下发事件」的正常回合掐死——实测 19:46 那轮 sidecar 一直在出事件,第 180 秒被墙钟杀掉。
- **排查手段**:sidecar 每次启动都写一行 `agent.direct_codex.claude_route source=<Platform|Custom|Config> host=<…> auth=<key|session|none>`(只写主机名,不含路径与凭据)。这一行是判断"回合到底发到哪个网关"的唯一低成本证据。
- **注意**:日志脱敏标记包含 `credential`、`x-api-key`、`bearer `、`token=`、`api_key`,命中即整行替换成 `<sensitive diagnostic details redacted>`。诊断行只能写 `source=`/`host=`/`auth=` 这类自查过的字段(第一版写成 `credential=bearer`,整行被吃掉过一次)。
- **验证**:`cargo test --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute --bin genarrative-ai-game-creator-shell claude_code_cli::tests::` 11 passed,覆盖平台优先、自定义端点保留配置、未登录时不回落本机环境且报「先登录」、baseUrl `/v1` 归一化、主机名诊断、sidecar 环境隔离(`USERPROFILE`/`CLAUDE_CONFIG_DIR` 指向隔离目录且不带本机 `ANTHROPIC_*`);另用本地假 Anthropic 端点跑通真 sidecar:`REQ HEAD /api/hello` → `REQ POST /v1/messages?beta=true auth=bearer` → `result=PROBE_OK`。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs`、`apps/ai-game-creator-shell/agent-sidecar/src/index.mjs`、[`【技术方案】AGC后台模型别名与对话选择-2026-09-05.md`](../../technical/【技术方案】AGC后台模型别名与对话选择-2026-09-05.md)。
## 2026-10-03 cc 回合"模型已回复却报宿主任务提前结束":缺终态 + 缺落盘 + 工具被拒
- **现象**:Router 渠道恢复后,cc 回合能拿到真实回复(日志 `stage=claude-parse-done chars=176/208/289`),但紧接着就是 `runtime-unclassified` +「DirectProject 宿主任务提前结束(panic、future 被丢弃或被取消)」,用户看到的仍是失败;项目对话历史里只有用户消息、没有助手消息;模型还会回一句「读项目文件的工具没有权限」。
- **根因 1(缺终态)**:终态 `turn.completed` 的"深层出口"只在 codex app-server 那条路径里(`DirectTurnTerminalContext::write` → `complete_turn`)。cc 执行器(`direct_game_creator_claude_code_chat_at`)整轮成功返回后没有任何人写终态,于是 `thread_manager::dispatch` 里占用对象的兜底把一轮已经拿到回复的回合收成 `HostDropped`。诊断行 `agent.direct_turn.host_dropped … panicking=false` 且**没有** `agent.direct_turn.panic` 行,就是这条(不是 panic,是"没写终态就结束")。
- **根因 2(缺落盘)**:assistant 回复只存在于 SDK 事件流里。codex 路径由 `finish_direct_project_collect_history` 写进 `.agent/conversations/project.jsonl`,cc 没有对应步骤,所以即使回合收口成功,UI 也读不到回复。
- **根因 3(工具被拒)**:sidecar 用 `permissionMode: 'dontAsk'` 且没有 `allowedTools`,宿主 MCP 工具(`mcp__agc__*`)一律被直接拒绝,模型只能回"没有权限"。
- **根因 4(界面看不到回复)**:聊天区是按 `item.completed` 事件流投影的(codex 路径在 `rawResponseItem/completed` 时下发 `ThreadItem::Message`),只把回复落进 `project.jsonl` 不会让本轮出现在界面上——用户看到"用户气泡 + 本轮结束于 … · 耗时",回复只在重进项目时从历史读出来。
- **现行口径**:cc 成功出口由放行侧补写 `DirectTurnTerminal::completed()`(`finish_if_unfinished` 幂等,codex 已写过终态时是空操作);cc 解析成功后必须①把回复按 `{"type":"message","role":"assistant","id":"direct-codex:<clientTurnId>:assistant","content":[{"type":"output_text","text":…}]}` 落进项目历史,②用**同一个 id** 下发 `ThreadEvent::item_completed(ThreadItem::Message{role:"assistant"})`(落盘失败按回合失败收口);sidecar 按 `mcp__<server>` 前缀整体放行请求里声明的 MCP 服务器(权限策略在宿主侧执行)。
- **诊断口径**:`agent.direct_turn.host_dropped` / `agent.direct_turn.panic` 里的令牌字段必须写 `tt=`,写 `turnToken=` 会命中脱敏标记,整行变成 `<sensitive diagnostic details redacted>`,离线只剩"说不出原因"的 HostDropped。
- **验证**:dev 栈里用 CDP 注入真实回合(`node %TEMP%\agc-cdp.mjs <expr>`):①读文件轮 `claude-parse-done chars=108`,`.agent/conversations/project.jsonl` 出现 `direct-codex:cdp-…:assistant` 条目,回复内容与 `game/index.html` 前两行(`<!doctype html>` / `<html lang="zh-CN">`)逐字一致(证明宿主工具真的执行了);②聊天视图打开时注入 `只回三个字:收到了`,DOM 断言(`document.body.innerText`)同时出现用户气泡 `11:40:05`、助手回复 `收到了` 与 `本轮结束于 11:40:16 · 耗时 10.7秒`(证明 `item.completed` 实时投影生效,不必重进项目);同一日志不再出现新的 `host_dropped`。`cargo test … -- claude_code_cli::tests direct_turn_failure::tests` 19 passed。
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_failure.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs`、`apps/ai-game-creator-shell/agent-sidecar/src/index.mjs`。
@@ -70,7 +70,7 @@
- (2026-09-24 快照,已被上面 2026-09-29 重新盘点取代)74 份计划里 63 份为 `implemented-awaiting-runtime-acceptance`;当轮未勾选复选框从 85 条降到 43 条。
- **路由类门禁已于 2026-09-29 转绿并接进 CI**:`npm run check:nginx-spa-routes` OK(12 路由 / 3 模板)、`npm run check:pingora-route-parity` OK(24 路由)。这条曾经红了一个月(`/components`、`/design-system`、5 条 `/games*` 的 SPA allowlist),收口记录见上面「已关闭」两条;同日还给 parity 门禁补了**反向覆盖**(两份 Nginx 模板里的每条 `location` 都必须被矩阵声明),并将 `check:pingora-gateway-smoke` 扩到覆盖发行网关重写与 Cookie 清空。**根因修复**:两条门禁现在串进 `npm run lint`(⇒ `check:repository-ci` ⇒ CI 与 pre-push 都会跑;`npm run lint` 已实跑全绿),并且 `check:production-ops` 新增两条 guardrail 锁住这个接线——把任一条从 lint 链里拿掉都会立刻变红(变异验证通过)。判据与踩坑见 `pitfalls.md` 同日条目。
- 其余门禁(2026-09-24 本机实跑):`ai-game-creator-shell:check:rust:crates` exit 0、`agc:plugins:test` 35 passed、`agc:plugins:native-test` 30 passed、`check:repository-ci origin/master` exit 0(含 admin-web typecheck 与生产构建)。
- CI 全部 job 的命令(2026-09-24 起逐条对齐,本机实跑):AGC agent-run smoke `ai-game-creator-shell:agent-run:smoke` passed(本地 provider 桩,约 2.5 分钟);`cargo check --locked -p api-server --all-targets` 与 `-p spacetime-module` exit 0;两把 Cargo.lock 的「构建不改锁」判据以 `--locked` 全绿 + desktop 锁未被构建改动佐证。至此除「AGC Rust 四个分片跑全量(Windows 不实用)」与已记录的 Linux-only 门禁外,CI 每个 job 的命令都有本机结果。
- CI 全部 job 的命令(2026-09-24 起逐条对齐,本机实跑):(2026-10-02:AGC agent-run smoke 脚本与 `agc-rust-smoke` CI job 已随自建 Agent Runtime 退役删除,见 `docs/adr/【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02.md`;本行当时的实跑结果仅作历史记录);`cargo check --locked -p api-server --all-targets` 与 `-p spacetime-module` exit 0;两把 Cargo.lock 的「构建不改锁」判据以 `--locked` 全绿 + desktop 锁未被构建改动佐证。至此除「AGC Rust 四个分片跑全量(Windows 不实用)」与已记录的 Linux-only 门禁外,CI 每个 job 的命令都有本机结果。
- CI 对齐的其余命令(2026-09-24 本机实跑):`npm run test:ci:frontend` 230 files / 2583 passed / 12 skipped、`npm run bgfilter-worker:smoke-test` 4 passed、`npm run build`(web + admin-web)exit 0;release 分组三步在 Windows 上全部可跑——`ai-game-creator-shell:build --no-bundle`(5m47s,产出 exe)、`desktop-shell:build --no-bundle`(1m29s)、`desktop-shell:stage-release-binary`(修掉 `new URL(...).pathname` 拼出 `F:\F:\…` 的 Windows 路径缺陷后通过)。
- 运维类门禁(2026-09-24 本机实跑):`check:pingora-direct-preflight`、`check:pingora-canary-live-guard`、`check:pingora-direct-live-guard`、`check:nginx-pingora-canary` 直接通过;`check:production-api-release`、`check:production-api-deploy`、`check:production-health-patrol`、`check:pingora-release-readiness`、`check:pingora-tls-cert-sync` 是 Linux 生产机/CI 专属(`bash`+`chmod`、`/usr/bin/stat`、`systemctl`、写死的 `npm`、Linux 拷贝语义),本机不跑,已在 pitfalls 记录。
- 唯一需要环境变量的是 `check:pingora-gateway-smoke`:本机要先设 `OPENSSL_CONF=C:\Program Files\Git\usr\ssl\openssl.cnf`(Strawberry OpenSSL 自带 config 路径不存在),同时脚本已修掉 Windows 二进制名与并发保护用例的竞态;设好变量后本机 `通过`。
@@ -14,7 +14,7 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事
- 命令失败按具体变体建模(Rust 枚举 + ts-rs 导出的 `type` 判别联合;顶层只放调用方要分流的类别,可枚举细分收进类型化枚举 `reason` 字段,无字段变体生成 `{ type }`,带载荷变体生成 `{ type } & 载荷类型`),前端按变体分流,**任何地方都不对错误文案做判断**:认得的业务变体(用户输入 / 前置条件 / 预期 4xx)由调用方消化并给用户反馈,永不进池;系统变体、未识别变体和结构化之外的拒绝原样抛出,走上面的兜底入口。认证命令的封装形态:`invokeClientAuth` 只是薄包装,把结构化拒绝原样装进**已有**的 `ClientAuthErrorWrapper`(载体只有一个 `ClientAuthError` 类型的 `error` 字段,值就是 ts-rs 生成的判别联合;不读变体字段、不塞 `context`,构造时把整份载荷 `JSON.stringify` 进 `Error.message`,上报事件因此拿到机器事实),不新增手写错误类;判定只写在 catch 子句里,无字段变体用本 catch 的固定文案,带载荷变体先 `as` 取自己的具名载荷类型(`reason` 是枚举时再 `switch (payload.reason)`)、再用它自己的字段拼上本次操作的上下文前缀,`default` 用 `expectNever` 在编译期挡住漏接变体。
- 客户端 API 自动采集只覆盖网络错误、408 和 5xx(`clientApi` 作为 `fetch` 的调用方在抛出前判定);预期的 4xx 登录/鉴权失败不进入错误报告池。
- Rust 侧通过 `app_log!` 将普通文本日志同时输出到 stderr 和 AppData `diagnostics/application.log`,超出 256 KiB 滚动到 `application.previous.log`;WebView 的 console 输出通过 `append_application_log` 镜像到同一 raw log,并在客户端桥接处再次脱敏;`read_diagnostic_logs` 只读取应用级日志。
- 这里有两套互不相干的东西,不要互相代入:**错误报告事件池**是进程内 `error_report` 的结构化事件(本次变更不动它,仍然只在内存里、提交时才生成 `events.jsonl`);**统一 Agent Runtime 错误事件**是项目内 sidecar `.agent/runtime/errors/<eventId>.json`,既不进事件池也不进报告包。因为报告包里的日志附件只有 AppData 应用日志,所以 sidecar 的同一份已脱敏诊断再作为**日志行**(不是报告事件)投影成两行:`agent.runtime.error`(身份行:eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef,全部是程序生成或调用方常量)与 `agent.runtime.error.detail`(详情行:hint / summary / detail / metadata,自由文本只出现在这里)。两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,不新增字段来源;落到日志前 summary 按 320 字符、detail / metadata 按(1200 / 200 字符)预算脱敏截断(summary 由调用方给,`direct_tool_bridge` 会传工具错误原文,而 `app_log!` 同时把整行写 stderr,那里没有 `sanitize_diagnostic_message` 兜底)。`sanitize_diagnostic_message` 命中凭据标记时替换的是**整行**,自由文本因此只放详情行:详情行被吃掉也不影响身份行定位事件。
- 这里有两套互不相干的东西,不要互相代入:**错误报告事件池**是进程内 `error_report` 的结构化事件(本次变更不动它,仍然只在内存里、提交时才生成 `events.jsonl`);**统一 Agent Runtime 错误事件**是项目内 sidecar `.agent/runtime/errors/<eventId>.json`,既不进事件池也不进报告包。因为报告包里的日志附件只有 AppData 应用日志,所以 sidecar 的同一份已脱敏诊断再作为**日志行**(不是报告事件)投影成两行:`agent.runtime.error`(身份行:eventId / source / stage / code / clientTurnId / elapsedMs / detailRef,全部是程序生成或调用方常量)与 `agent.runtime.error.detail`(详情行:hint / summary / detail / metadata,自由文本只出现在这里)。两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,不新增字段来源;落到日志前 summary 按 320 字符、detail / metadata 按(1200 / 200 字符)预算脱敏截断(summary 由调用方给,`direct_tool_bridge` 会传工具错误原文,而 `app_log!` 同时把整行写 stderr,那里没有 `sanitize_diagnostic_message` 兜底)。`sanitize_diagnostic_message` 命中凭据标记时替换的是**整行**,自由文本因此只放详情行:详情行被吃掉也不影响身份行定位事件。
- 报告面板只由自动诊断通知中的“查看并报告”打开,不提供聊天命令、崩溃页按钮或其他手动入口;默认选中当前快照中的全部事件,用户可取消不想提交的事件。允许填写最多 2,000 字中文描述并取消日志附件;本版本不支持截图或任意文件附件。
- 报告面板读取当前错误快照失败时,必须明确显示“错误事件暂不可用,请关闭后重试”,不能把失败误显示为“当前没有待报告的错误”。
- 通知中的“查看并报告”打开面板时必须保留该次通知快照;最新快照读取瞬时失败时使用这份 fallback 继续展示和提交,不能因先清空通知而丢失用户刚看到的事件。
@@ -5,7 +5,7 @@
原始版本:`2026-07-14`;口径复核:`2026-08-25`
> 当前口径(2026-08-25):本文件只保留 Runtime V1.1 的持久化、恢复、安全和验证约束。AGC 当前产品入口、DirectProject、语义工具和 UI workflow 以 [`AI游戏创作智能体App实施计划-2026-06-24.md`](./【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)、当前 `apps/ai-game-creator-shell` 代码及 `docs/README.md` 为准;本文件与其冲突时以后者为准。旧 ToolHost / DirectHome 兼容条款不得重新成为产品入口或第二套 Runtime 真相。
> 当前口径(2026-10-02):本文件描述的自建 Agent Runtime 执行面(runtime driver / protocol / tools / actions)、`--agent-*` CLI、目标 / 协作协议与真实 e2e harness 已整体退役,实现、测试、门禁与 CI job 均已删除,见 [`【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02.md`](../../adr/【ADR】退役AGC独立Agent%20Runtime与CLI执行面-2026-10-02.md)。外部 Runner 仅保留编辑器桥 RPC 与 GUI owner 参与锁 / watchdog。AGC 当前产品入口、DirectProject、语义工具和 UI workflow 以 [`AI游戏创作智能体App实施计划-2026-06-24.md`](./【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)、当前 `apps/ai-game-creator-shell` 代码及 `docs/README.md` 为准;本文件与其冲突时以后者为准。旧 ToolHost / DirectHome 兼容条款不得重新成为产品入口或第二套 Runtime 真相。
## 目标
@@ -861,13 +861,13 @@ V1.20 对标 Codex CLI 的可选 Web Search,但只声明当前 `platform-llm`
## V1.21 单 Agent token-aware 持久上下文压缩
V1.21 对标 Codex CLI 的 `model_context_window`、`model_auto_compact_token_limit`、`tool_output_token_limit` 和显式手动压缩。它替换“固定保留最近 12 条就算压缩”的能力口径,但不删除原始 conversation、task、event 或工具事实,也不把模型摘要提升为 Goal、计划、权限、验证或副作用事实源。
V1.21 对标 Codex CLI 的 `model_context_window`、`model_auto_compact_token_limit` 和显式手动压缩。它替换“固定保留最近 12 条就算压缩”的能力口径,但不删除原始 conversation、task、event 或工具事实,也不把模型摘要提升为 Goal、计划、权限、验证或副作用事实源。
### 配置与预算
- `llm` 新增 `contextWindowTokens / autoCompactTokenLimit / toolOutputTokenLimit`,发布默认分别为 `128000 / 64000 / 12000`;`agentLlm.<agentId>` 复用现有 patch 继承,显式 Agent 值覆盖全局。三项都必须大于 0,自动阈值必须小于 context window,并为当前请求的生成 token 预算与固定安全余量留下空间。既有配置和持久协议键 `maxOutputTokens` 保持冻结以兼容恢复;它表示包含可见输出与隐藏 reasoning token 的生成侧预算,不表示输入加输出总量,也不保证可见正文长度。
- `llm` 新增 `contextWindowTokens / autoCompactTokenLimit`,发布默认分别为 `128000 / 64000`;`agentLlm.<agentId>` 复用现有 patch 继承,显式 Agent 值覆盖全局。两项都必须大于 0,自动阈值必须小于 context window,并为当前请求的生成 token 预算与固定安全余量留下空间。既有配置和持久协议键 `maxOutputTokens` 保持冻结以兼容恢复;它表示包含可见输出与隐藏 reasoning token 的生成侧预算,不表示输入加输出总量,也不保证可见正文长度。
- Runtime 在发送 tool-plan、context-compaction 或 final-reply 前,按消息、multimodal 文本和 function schema 的规范序列化字符数做保守 token 估算;Provider 返回 usage 时再记录真实 `prompt/completion/total`。估算只用于提前门禁,不能伪装成 Provider 计费事实。
- 单条 observation 进入模型上下文前按 `toolOutputTokenLimit` 收紧;完整命令输出仍留在 owning Agent 的私有 sidecar,通过既有分页工具读取。公共状态只显示估算 token、最近真实 usage、阈值、压缩次数和时间,不显示被压缩正文。
- 完整命令输出仍留在 owning Agent 的私有 sidecar,通过既有分页工具读取。公共状态只显示估算 token、最近真实 usage、阈值、压缩次数和时间,不显示被压缩正文。
### 可压缩内容与不可压缩事实
@@ -921,7 +921,7 @@ V1.22 对标 Codex CLI 的 MCP tool 能力,在现有单 Agent Runtime 内增
- 共享命令契约新增 `mcp.call`,项目/Agent policy 仍可统一 deny 或 confirm。server/tool 配置只会进一步收紧:`deny` 直接返回 blocked observation,`confirm` 复用现有确认卡,`auto/writes` 也必须先可靠写入 durable pending action。隔离 child 默认禁止 MCP;后续若开放必须有模板级显式 allowlist,不继承父 Agent 的宽权限。
- MCP 调用复用当前 action fingerprint、steer cursor、Goal、project/repository revision、verification gate、Provider request identity 与 `approved -> executing -> observed-*` 账本。调用前再次读取 AppData、刷新目标 tool 并核对两个 fingerprint;Runner 在 `executing` 后退出、超时后无法确定服务端是否完成、SDK transport 断开或审计落盘失败均进入 reconciliation。只有服务端明确返回 result/error,才形成可继续 planning 的确定终态。
- 完整 `CallToolResult` 原子写入 `.agent/runtime/mcp-results/<agentHash>/<runHash>/<actionHash>.json` 私有 sidecar,绑定项目/Agent/Task/Session/Run/action/server/tool/catalog/tool/result fingerprint、执行时 `toolOutputTokenLimit`、字符/内容块计数、isError 和时间。恢复时必须从完整 result 按原预算重算计数、摘要、二进制元数据和 observation 并全量比对;任一派生字段不一致都进入 reconciliation,不能接受局部损坏摘要。模型 observation 只读取受 `toolOutputTokenLimit` 约束的 text/structured content;image/audio/embedded resource 只给类型、MIME、大小和 SHA-256 元数据,本切片不把任意 MCP 二进制转发给 Provider。
- 完整 `CallToolResult` 原子写入 `.agent/runtime/mcp-results/<agentHash>/<runHash>/<actionHash>.json` 私有 sidecar,绑定项目/Agent/Task/Session/Run/action/server/tool/catalog/tool/result fingerprint、字符/内容块计数、isError 和时间。恢复时必须从完整 result 按原预算重算计数、摘要、二进制元数据和 observation 并全量比对;任一派生字段不一致都进入 reconciliation,不能接受局部损坏摘要。模型 observation 只读取 text/structured content;image/audio/embedded resource 只给类型、MIME、大小和 SHA-256 元数据,本切片不把任意 MCP 二进制转发给 Provider。
- 公共 Runtime state/event/task/Agent DB/receipt/activity/output/report 只保存 server/tool、审批、状态、参数字符数与 SHA-256、结果内容块计数/字符数与 SHA-256、sidecar 相对路径和安全错误分类,不保存 arguments、返回正文、Bearer/header/env、server instructions、绝对路径或 SDK 原始错误。`agent.action_history` 同样只返回该安全摘要。
### 开发入口与验收
@@ -1,5 +1,9 @@
# AI 游戏创作智能体 App 实施计划
## 2026-10-02 退役自建 Agent Runtime 与 CLI 执行面
自建 Agent Runtime(`agent/runtime_driver`、`runtime_protocol`、`runtime_tools`、`runtime_actions`、`runtime_state`、`runtime_adapter`、`agent/prompt.rs`、`agent_native_tools.rs`、`collaboration.rs`、`delegation.rs`、`goal.rs`、`context_compaction.rs`、`isolated_agent.rs`、`tool_plan_handoff/`、`user_input.rs`)与它的 Tauri 命令、`--agent-*` CLI、真实 e2e harness、CI job 一并退役,历史由 Git 保存,见 `docs/adr/【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02.md`。本文正文里所有关于 `--agent-run` / `--agent-task` / `--agent-runner-status` / `agent-run:smoke` / `agent-runtime-real-e2e.mjs`、后台单 Agent 队列、`read_game_creator_agent_runtime(s)`、Supervisor 自主协作与 Goal 生命周期的描述均已失效,不再作为实现依据。外部 Runner(`--agent-runner`)仅保留编辑器桥 RPC 与 `runner.attach_gui_owner` + GUI owner 参与锁 / watchdog。AGC 现役链路以 DirectProject(`agent/direct_*` + codex app-server)、资源工作台与本文其后的 DirectProject 相关条目为准。
## 2026-10-02 固定试玩控件的指针命中边界
固定试玩只按目标控件自身的计算样式判断 `pointer-events:none`,不累计祖先的 `none`。覆盖层为 `none`、按钮显式恢复 `auto` 是合法布局;按钮未覆盖时仍会继承 `none` 并被拒绝。保留祖先可见性、中心点 `elementFromPoint` 命中、原生 disabled、aria-disabled 和 inert 检查;目标自身为 `none` 时,即使内部子元素恢复 `auto`,也不放行目标控件。
@@ -1689,9 +1693,9 @@ DirectProject 在收到完整游戏策划或游戏制作请求后,必须把视
## 2026-09-15 AGC 统一错误事件、诊断落库与验收反馈
DirectProject、Agent Runtime、Provider、app-server、内置 MCP、命令执行、构建和浏览器试玩的失败必须先转换为统一的 `AgentRuntimeErrorEvent`,再分别投影到用户消息、运行面板和项目诊断文件;业务模块不得自行拼接只有一句“执行失败”的终态文案。统一事件至少包含 `schemaVersion / eventId / clientTurnId / source / stage / code / retryable / occurredAt / elapsedMs / publicText / recoveryHint / detailRef`,其中 `publicText` 是脱敏后的可行动摘要,`detailRef` 指向项目内有界诊断记录;Token、Cookie、URL/query、私钥、宿主绝对路径、原始请求正文和未脱敏 stderr 不得进入对话或用户可见文本。
DirectProject、Agent Runtime、Provider、app-server、内置 MCP、命令执行、构建和浏览器试玩的失败必须先转换为统一的 `AgentRuntimeErrorEvent`,再分别投影到用户消息、运行面板和项目诊断文件;业务模块不得自行拼接只有一句“执行失败”的终态文案。统一事件至少包含 `schemaVersion / eventId / clientTurnId / source / stage / code / occurredAt / elapsedMs / message / error / detailRef`,其中 `message` 是产生失败的 typed 错误在失败现场写好、脱敏后的人类可读文案,`error` 是同一个 typed 错误 enum 的原样序列化(一个 case 一个变体,供开发者按变体与字段定位;没有 typed 错误的调用方写 `null`),`detailRef` 指向项目内有界诊断记录;Token、Cookie、URL/query、私钥、宿主绝对路径、原始请求正文和未脱敏 stderr 不得进入对话或用户可见文本。
项目内统一落库目录为 `.agent/runtime/errors/`,事件记录采用幂等 JSONL 或 JSON sidecar;写入失败不能覆盖原始业务错误,但必须在事件中标记 `persistenceFailed`。DirectProject 对话历史必须持久化本轮用户消息、终态错误的安全 assistant 投影和诊断引用,使下一轮能够读取上一轮失败证据。前端只展示 `publicText`,点击详情后按 `detailRef` 读取有界、脱敏的诊断,不直接展示私有 `detail`。
项目内统一落库目录为 `.agent/runtime/errors/`,事件记录采用幂等 JSONL 或 JSON sidecar;写入失败不能覆盖原始业务错误,但必须在事件中标记 `persistenceFailed`。DirectProject 对话历史必须持久化本轮用户消息、终态错误的安全 assistant 投影和诊断引用,使下一轮能够读取上一轮失败证据。前端只展示宿主给的安全文案;诊断正文只留在 `detailRef` 指向的有界、脱敏记录里,不进入用户可见文本。
前端取回 `detailRef` 的口径是失败文案末尾的固定后缀「;详情:<detailRef>」:Rust 侧 `direct_codex_failure_text_keeps_the_detail_ref_marker_for_the_renderer` 与前端 `tests/agentRuntimeErrorDetail.test.ts` 各自钉住同一份文案形状与它的解析,任一侧改文案或改解析都会变红。失败提示只展示映射后的安全 `publicText`(v1 历史形状与 v2 现行形状都映射成「阶段 + 摘要 + 建议 + 是否可直接重试」),诊断正文不在提示里预读、也不写入历史投影,而是由聊天状态栏下方的「查看详情」入口按需调用只读命令读取并二次脱敏;这条交互由 `tests/appSurface/chat-composer.suite.ts` 的「失败提示保留可执行原因,诊断正文只在「查看详情」时读取」与 `tests/agentRuntimeModel.test.ts` 的 v2 映射用例钉住,渲染层仍不参与错误分类。
@@ -1842,9 +1846,9 @@ Direct 回合的所有权属于进程内项目身份锁,不属于当前页面
## 2026-09-21 统一错误事件同时落到 AppData 应用日志
`AgentRuntimeErrorEvent` 把失败投影到用户消息、运行面板和项目内 `.agent/runtime/errors/<eventId>.json` 时,同一份已脱敏诊断还要投影成 AppData `diagnostics/application.log` 的两行:`agent.runtime.error`(身份行:`eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef`)与 `agent.runtime.error.detail`(详情行:`hint / summary / detail / metadata`)。原因是项目内 sidecar 只在项目目录可见,而“报告问题”只上传应用级日志:没有这两行时,用户提交的失败消息里只剩一个 `详情:.agent/runtime/errors/...json` 路径,团队拿不到诊断正文。
`AgentRuntimeErrorEvent` 把失败投影到用户消息、运行面板和项目内 `.agent/runtime/errors/<eventId>.json` 时,同一份已脱敏诊断还要投影成 AppData `diagnostics/application.log` 的两行:`agent.runtime.error`(身份行:`eventId / source / stage / code / clientTurnId / elapsedMs / detailRef`)与 `agent.runtime.error.detail`(详情行:`message / error / metadata`)。原因是项目内 sidecar 只在项目目录可见,而“报告问题”只上传应用级日志:没有这两行时,用户提交的失败消息里只剩一个 `详情:.agent/runtime/errors/...json` 路径,团队拿不到诊断正文。
口径:两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,字段不退化成第二份来源;`summary` 按 320 字符、`detail` 与 `metadata` 按(1200 / 200 字符)预算先脱敏再截断,落盘前还会被 `sanitize_diagnostic_message` 二次脱敏并按行截断,因此自由文本字段在行内先压平换行。拆两行是因为整行一旦出现凭据标记会被整体替换成脱敏占位:所以**自由文本(summary / hint / detail)只放详情行**,身份行只留程序生成与调用方常量字段,详情行被整体脱敏时事件仍能按 eventId / detailRef 定位。写日志先于写 sidecar:sidecar 失败不能连日志一起丢。
口径:两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,字段不退化成第二份来源;sidecar 里的 `message` 按 8 KiB 上限、应用日志的 `message` / `error` / `metadata` 按(1200 / 400 / 200 字符)预算先脱敏再截断,落盘前还会被 `sanitize_diagnostic_message` 二次脱敏并按行截断,因此自由文本字段在行内先压平换行。拆两行是因为整行一旦出现凭据标记会被整体替换成脱敏占位:所以**自由文本(message)只放详情行**,身份行只留程序生成与调用方常量字段,详情行被整体脱敏时事件仍能按 eventId / detailRef 定位。写日志先于写 sidecar:sidecar 失败不能连日志一起丢。
## 2026-09-23 AGC UI 设计文档 Agent 工具化重写
@@ -98,7 +98,7 @@ readHistory(threadId, { beforeItemId?, limit }) -> {
}
```
`subscribe` 不返回完整历史。`lastCompletedItemId` 只是历史读取锚点,前端自行按 item ID 懒加载需要的历史切片。`events` 是当前运行态重建所需的未完成 item 原始事件,以及当前 turn 的生命周期锚点;前端用同一个 reducer 重放 bootstrap 和后续事件。Rust 不保存或理解前端 reducer state。只要 DirectProject 历史切片返回 `hasMore`,聊天视图必须显示“显示更早的对话”入口,并允许按钮或滚动触发下一页,即使当前可见消息窗口没有隐藏消息。
`subscribe` 不返回完整历史。`lastCompletedItemId` 只是历史读取锚点,前端自行按 item ID 懒加载需要的历史切片。`events` 是当前运行态重建所需的未完成 item 原始事件,以及当前 turn 的生命周期锚点;前端用同一个 reducer 重放 bootstrap 和后续事件。Rust 不保存或理解前端 reducer state。只要 DirectProject 历史切片返回 `hasMore`,聊天视图就必须能继续往前取页,即使当前可见消息窗口没有隐藏消息;入口形态(触顶自动加载 + 填充视口补载、加载行与失败重试、前插不跳)由 [`【ADR】DirectProject对话滚动与历史自动加载-2026-10-02`](../adr/【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md) 规定,已不再有常驻按钮。
`consume` 不接收或返回 cursor。每个 subscriber 在 Rust 内部持有自己的 cursor,并在加锁的临界区内完成过期判断、读取和 cursor 前进。前端只持有 `subscriptionId` 与 reducer state。并发 `consume` 不重复返回同一批事件。
@@ -106,7 +106,7 @@ readHistory(threadId, { beforeItemId?, limit }) -> {
`subscribe` 在同一个边界内先把新 subscriber 的游标钉在当时的队尾,再收集 bootstrap 的运行态事件,因此 bootstrap 返回的那批事件**就是**该 subscriber 此刻应处理的事件:前端直接 reduce 它们即可,不存在"先补一次 `consume` 才能拿到已暂存事件"的步骤。第一个例外只有回执竞态:Rust 侧一注册完 subscriber 就开始 `notify`,而前端要等回执到达才知道自己的 `subscriptionId`,这段窗口内的通知拿不到订阅身份。前端因此必须记一笔欠账,回执到达后立刻补一次 `consume` 取回那批事件;否则事件会卡在队列里等下一次通知,而一次回合的最后一个事件之后可能再也没有通知。除此之外不轮询,也不设任何定时 `consume`——唤醒只由 `notify` 负责。用定时器兜底既自举不了(判断"有活动回合"本身依赖事件),也把唤醒机制变成两套。
首屏历史不通过"读取整份对话"的命令获取:`subscribe` 返回的 `lastCompletedItemId` 就是首屏锚点,前端据此调用 `readHistory` 取最近的切片,再按滚动或按钮继续向前分页。系统不提供返回整份对话历史的命令。
首屏历史不通过"读取整份对话"的命令获取:`subscribe` 返回的 `lastCompletedItemId` 就是首屏锚点,前端据此调用 `readHistory` 取最近的切片,再按滚动继续向前分页(入口形态见上条 ADR)。系统不提供返回整份对话历史的命令。
### 事件和顺序
@@ -372,7 +372,7 @@ AGC 分片运行器编译测试二进制时使用 `--message-format=json-render-
Linux process-session 的 owner SIGKILL 用例必须在启动 owner 后立即建立测试清理 guard:正常退出或断言 panic 时终止、回收 owner,并在有界时间内清理其独立临时项目目录中的残留进程。原有「owner 退出后子进程自行消失」断言在兜底清理之前执行,不能由 guard 代替生产生命周期验证。清理覆盖 panic 路径及临时项目间隔离,且不得因清理失败再次 panic。
仓库级 Gitea Actions 工作流固定为 `.gitea/workflows/project-ci.yml`,在向 `master` 推送、创建或更新 PR,以及手工触发时运行。工作流拆成九个必须通过的 job。job 声明顺序就是 runner 领取顺序,因此把两条 AGC 壳 Rust lane 排在最前:并发槽位不足时它们必须最先开始,AGC 侧的关键路径才由自己而不是由排队决定。
仓库级 Gitea Actions 工作流固定为 `.gitea/workflows/project-ci.yml`,在向 `master` 推送、创建或更新 PR,以及手工触发时运行。工作流拆成八个必须通过的 job。job 声明顺序就是 runner 领取顺序,因此把两条 AGC 壳 Rust lane 排在最前:并发槽位不足时它们必须最先开始,AGC 侧的关键路径才由自己而不是由排队决定。
所有 CI job 和 Jenkins Web Build 在根 workspace 安装前都必须确认 `npm --version` 为 `10.9.7`。Gitea job 使用预构建镜像内的固定版本;Jenkins Web Build 在每个独立 `bash -lc` 中 source `scripts/jenkins-prepare-npm-env.sh`,首次为 Jenkins 运行用户的版本隔离目录引导同版 npm,后续复用并把该 `bin` 放到 `PATH` 首位。旧固定镜像缺少版本元数据时只能报告 `npm_version=partial` 并由当前 job 的根 `npm ci` 继续校验 lock,不能把过渡状态当作工具链已闭合。
@@ -382,10 +382,9 @@ Linux process-session 的 owner SIGKILL 用例必须在启动 owner 后立即建
- `Native shell tests`:按唯一根 workspace lockfile 安装全部 App 依赖后,用 `npm run check:native-shells:contract`、`npm run check:native-shells:shells` 和 `npm run check:native-shells:release` 分别执行静态契约、H5 / 微信 / Expo / Tauri 桌面壳运行时门禁,以及依赖发布产物的构建 smoke,最后确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写。
- `AI game creator shell web tests`:执行 `npm run check:native-shells:agc-web`(即 `npm run ai-game-creator-shell:check:web`:AGC 壳 typecheck 与壳内测试)。该分组不触碰 Cargo,因此不预热 Rust 依赖。
- `AI game creator shell Rust lane 1/2`、`lane 2/2`:两条 lane 各自只预热一次 AGC 壳自己的锁定依赖(`apps/ai-game-creator-shell/src-tauri/Cargo.lock` 的 path 依赖已含 `platform-llm`、`platform-agent`、`agent-runtime-core` 与 `shared-contracts`),然后顺序执行两次 `npm run check:native-shells:agc-rust-shard-<i>`(每次分片运行器使用对应的 `--shard-index=<i>`):AGC 壳 bin target 的 2466 条 Rust 单测按 `--list` 名单排序后切 4 片,片内保持 `--test-threads=1`、各片独立 `TMPDIR`,两条 lane 之间靠 job 级并发摊开;每次分片调用都会自校验「片并集等于全集且互斥」。**不要**改回「一个 job 里多进程并行这几片」:同一容器内它们共享 `HOME`、target 与固定临时路径,实测(run 2102)比整套串行还慢。这些 lane 在编译前通过 `scripts/ci-npm-ci-with-retry.sh` 执行根 `npm ci`,为 `build.rs` 准备 Claude Agent SDK 与 Linux 原生运行时。
- `AI game creator shell Rust smoke`:同样只预热 AGC 壳那份锁定依赖,执行 `npm run check:native-shells:agc-rust-smoke`(即 `npm run ai-game-creator-shell:agent-run:smoke`)。smoke 会用 `src-tauri/Cargo.toml` spawn `cargo run`,单独一个 job 以免把已经压到分钟级的片 job 拖长;smoke 脚本自身只 import `node:*`,但壳的 `build.rs` 需要 Claude Agent SDK,因此同样在编译前执行根 `npm ci`。
- `AI game creator shell Rust crates`:预热 `server-rs/Cargo.toml` 与两个无锁独立 crate(`agent-runtime-core`、`agent-runtime-orchestration`)后执行 `npm run check:native-shells:agc-rust-crates`(即 `npm run ai-game-creator-shell:check:rust:crates`),覆盖 `agent-runtime-core`、`agent-runtime-orchestration`、`platform-llm` 与 `shared-contracts`。这四条命令用的是 server-rs workspace 与独立 crate 的 manifest,属另一套依赖图,因此单独一个 job,也只跑 cargo、不装 npm 依赖。
九个 job 合起来覆盖根 `npm run check`,并补齐根检查没有包含的 BgFilter worker smoke harness、无密钥生产巡检 / 发布 / 部署行为 fixture、server-rs DDD、正式 workspace Rust 测试与现役后端编译门禁。客户端门禁的拆分口径是 `scripts/check-native-shells.mjs` 的 `--groups=`:十个分组(`contract`、`shells`、`agc-web`、`agc-rust-crates`、`agc-rust-shard-1` ~ `agc-rust-shard-4`、`agc-rust-smoke`、`release`)各自对应一个 `check:native-shells:<group>` 根脚本,并在 workflow 的某个 lane/job 里被恰好调用一次;每个 Rust lane 顺序调用两组,不带 `--groups=` 时脚本仍然串行跑全部分组,本地语义不变。`scripts/project-ci-workflow.test.ts` 会同时校验分组清单、根脚本内容、lane/job 覆盖与分片运行器,新增分组必须三处同步。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 `check:*` 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。
八个 job 合起来覆盖根 `npm run check`,并补齐根检查没有包含的 BgFilter worker smoke harness、无密钥生产巡检 / 发布 / 部署行为 fixture、server-rs DDD、正式 workspace Rust 测试与现役后端编译门禁。客户端门禁的拆分口径是 `scripts/check-native-shells.mjs` 的 `--groups=`:九个分组(`contract`、`shells`、`agc-web`、`agc-rust-crates`、`agc-rust-shard-1` ~ `agc-rust-shard-4`、`release`)各自对应一个 `check:native-shells:<group>` 根脚本,并在 workflow 的某个 lane/job 里被恰好调用一次;每个 Rust lane 顺序调用两组,不带 `--groups=` 时脚本仍然串行跑全部分组,本地语义不变。`scripts/project-ci-workflow.test.ts` 会同时校验分组清单、根脚本内容、lane/job 覆盖与分片运行器,新增分组必须三处同步。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 `check:*` 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。
PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME_SCHEMA_BASE_REF`。`check:spacetime-schema` 依赖该基线识别已有表字段删除、改名、重排和改类型;事件给出的基线缺失或本地不可解析时必须直接失败,不能退化为空差异检查。Gitea 的 PR checkout 是 PR head,不是与目标分支的预合并 commit,因此 workflow 还会验证 PR head 包含事件中的最新 base commit;分支保护必须继续开启“PR 过期禁止合并”,过期分支先更新再重跑。向 `master` 直接推送时使用 push before SHA;手工触发先尝试 `origin/master`,若它与 `HEAD` 相同则改用 `HEAD^`,仍无法得到不同提交时失败关闭。
@@ -404,13 +403,13 @@ bash scripts/gitea-ci-job-image.sh export /仓库外受控路径/genarrative-git
bash scripts/gitea-ci-job-image.sh load-runner
```
执行账号只要有权访问宿主 Docker API 并管理 runner 容器即可,不强制使用 root;无该权限时由 runner 运维人员执行。更新顺序必须是 `build/verify -> export 仓库外镜像归档与 SHA-256 sidecar -> load-runner -> 确认无活跃 job -> 备份当前 config -> 增加或替换 label -> docker restart --timeout 660 gitea-runner`。`--timeout 660` 只是停止宽限,不是 drain API;rootless DinD supervisor 可能同时停止内层 dockerd,因此重启前必须确认 Gitea 没有 `in_progress` run 且内层 `docker ps` 为空。config 和镜像归档只保存到仓库外受控位置,不在文档、仓库或日志中记录注册信息。重启后先重跑真实 PR 的九个 job,复核隔离边界并确认全部通过,再清理旧镜像。回滚时先把 workflow 的 `runs-on` 改回 `ubuntu-latest`,再恢复 config 备份并重启 runner。
执行账号只要有权访问宿主 Docker API 并管理 runner 容器即可,不强制使用 root;无该权限时由 runner 运维人员执行。更新顺序必须是 `build/verify -> export 仓库外镜像归档与 SHA-256 sidecar -> load-runner -> 确认无活跃 job -> 备份当前 config -> 增加或替换 label -> docker restart --timeout 660 gitea-runner`。`--timeout 660` 只是停止宽限,不是 drain API;rootless DinD supervisor 可能同时停止内层 dockerd,因此重启前必须确认 Gitea 没有 `in_progress` run 且内层 `docker ps` 为空。config 和镜像归档只保存到仓库外受控位置,不在文档、仓库或日志中记录注册信息。重启后先重跑真实 PR 的八个 job,复核隔离边界并确认全部通过,再清理旧镜像。回滚时先把 workflow 的 `runs-on` 改回 `ubuntu-latest`,再恢复 config 备份并重启 runner。
九个 job 先运行镜像内 `genarrative-gitea-checkout`,再以 `GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1` 执行 `scripts/check-gitea-ci-job-image.sh`,校验 Node 与 npm 固定版本、仓库 Rust toolchain、受信任 PATH、四份缓存锁命中状态、原生命令、pkg-config 依赖、完整 bwrap sandbox 和 Chrome headless。运行时发现锁不匹配时必须输出对应 `*_cache_lock=partial` 和 Actions warning,提示可信分支落地后刷新镜像,不能把陈旧缓存误报为闭合。`RUSTUP_AUTO_INSTALL=0`,因此仓库 `rust-toolchain.toml` 变更必须先更新镜像,不能让 job 现场下载。需要 `node_modules` 的 job 仍各自独立运行一次根 `npm ci`,以唯一 workspace lock 验证 PR 的全部 App 依赖;两条 AGC 壳 Rust lane 与 `AI game creator shell Rust smoke` 也必须在编译前安装 npm 依赖,因为 `build.rs` 会准备随包 Claude Agent SDK 与目标平台原生运行时;只有不构建壳的 `AI game creator shell Rust crates` 省略 npm 安装。安装覆盖与编译前顺序由 `scripts/project-ci-workflow.test.ts` 验证。`npm ci` 统一通过 `scripts/ci-npm-ci-with-retry.sh` 做最多 3 次整命令级有界重试,同时保留 `NPM_CONFIG_PREFER_OFFLINE=true` 和 npm 自身 10 次 fetch retry。命中镜像 cache 时只做干净解包,lock 变化时允许补齐差量。不在镜像内烘入 `node_modules`,也不挂载跨 PR 可写缓存。任何 job 的 sandbox canary 失败都必须停止,不允许跳过。Cargo 通过受控 proxy 下载 lock 差量时继续关闭 HTTP multiplexing,并设置 `CARGO_NET_RETRY=10`。
八个 job 先运行镜像内 `genarrative-gitea-checkout`,再以 `GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1` 执行 `scripts/check-gitea-ci-job-image.sh`,校验 Node 与 npm 固定版本、仓库 Rust toolchain、受信任 PATH、四份缓存锁命中状态、原生命令、pkg-config 依赖、完整 bwrap sandbox 和 Chrome headless。运行时发现锁不匹配时必须输出对应 `*_cache_lock=partial` 和 Actions warning,提示可信分支落地后刷新镜像,不能把陈旧缓存误报为闭合。`RUSTUP_AUTO_INSTALL=0`,因此仓库 `rust-toolchain.toml` 变更必须先更新镜像,不能让 job 现场下载。需要 `node_modules` 的 job 仍各自独立运行一次根 `npm ci`,以唯一 workspace lock 验证 PR 的全部 App 依赖;两条 AGC 壳 Rust lane 也必须在编译前安装 npm 依赖,因为 `build.rs` 会准备随包 Claude Agent SDK 与目标平台原生运行时;只有不构建壳的 `AI game creator shell Rust crates` 省略 npm 安装。安装覆盖与编译前顺序由 `scripts/project-ci-workflow.test.ts` 验证。`npm ci` 统一通过 `scripts/ci-npm-ci-with-retry.sh` 做最多 3 次整命令级有界重试,同时保留 `NPM_CONFIG_PREFER_OFFLINE=true` 和 npm 自身 10 次 fetch retry。命中镜像 cache 时只做干净解包,lock 变化时允许补齐差量。不在镜像内烘入 `node_modules`,也不挂载跨 PR 可写缓存。任何 job 的 sandbox canary 失败都必须停止,不允许跳过。Cargo 通过受控 proxy 下载 lock 差量时继续关闭 HTTP multiplexing,并设置 `CARGO_NET_RETRY=10`。
站点 stack 仍由宿主受控目录管理,`.env`、runner 注册文件和数据库凭据不进入仓库。Compose 必须在 helper/container 内把该目录挂到与宿主相同的绝对路径再执行;挂载到不同路径会让相对 bind source 被 Docker daemon 解析到错误的宿主目录并启动空数据。升级或 runner 迁移前先停止 Gitea 写入,并把 Gitea 冷快照、数据库导出、compose/env 与 runner config/.runner 保存到仓库外受控备份位置。备份文件、绝对宿主配置和注册 token 不得提交 Git,也不在共享文档中记录具体路径或注册内容。
workflow 首次成功运行后,**不**把 Project CI 的 job 配成 Gitea `master` 分支保护的合并必需检查(2026-09-14 复核口径):合并前由人工确认最近一次 Project CI 结果,因此 job 拆分或改名都不需要同步分支保护设置,代价是门禁 job 红了不会自动阻止合并。若将来改成"当前 head 全绿才能合并",清单是 9 个完整 context:`Project CI / Repository checks (pull_request)`、`Project CI / Frontend tests (pull_request)`、`Project CI / Backend tests (pull_request)`、`Project CI / Native shell tests (pull_request)`、`Project CI / AI game creator shell web tests (pull_request)`、`Project CI / AI game creator shell Rust lane 1/2 (pull_request)`、`Project CI / AI game creator shell Rust lane 2/2 (pull_request)`、`Project CI / AI game creator shell Rust smoke (pull_request)`、`Project CI / AI game creator shell Rust crates (pull_request)`;必须用 Gitea 实际上报的 `<workflow> / <job> (<event>)`,不能只填裸 job 名,也不能让清单里残留已不再上报的旧 job 名(例如拆分前的 `AI game creator shell Rust tests`),否则 PR 会永远停在"等待该检查"。只提交 workflow 文件不会自动创建 runner;如果 Actions 长时间停留在等待状态,先到仓库或组织的 Actions runner 页面确认存在在线、带 `genarrative-ci` 标签的 runner,再检查精确 Image ID 是否已装入内层 Docker。
workflow 首次成功运行后,**不**把 Project CI 的 job 配成 Gitea `master` 分支保护的合并必需检查(2026-09-14 复核口径):合并前由人工确认最近一次 Project CI 结果,因此 job 拆分或改名都不需要同步分支保护设置,代价是门禁 job 红了不会自动阻止合并。若将来改成"当前 head 全绿才能合并",清单是 8 个完整 context:`Project CI / Repository checks (pull_request)`、`Project CI / Frontend tests (pull_request)`、`Project CI / Backend tests (pull_request)`、`Project CI / Native shell tests (pull_request)`、`Project CI / AI game creator shell web tests (pull_request)`、`Project CI / AI game creator shell Rust lane 1/2 (pull_request)`、`Project CI / AI game creator shell Rust lane 2/2 (pull_request)`、`Project CI / AI game creator shell Rust crates (pull_request)`;必须用 Gitea 实际上报的 `<workflow> / <job> (<event>)`,不能只填裸 job 名,也不能让清单里残留已不再上报的旧 job 名(例如拆分前的 `AI game creator shell Rust tests`),否则 PR 会永远停在"等待该检查"。只提交 workflow 文件不会自动创建 runner;如果 Actions 长时间停留在等待状态,先到仓库或组织的 Actions runner 页面确认存在在线、带 `genarrative-ci` 标签的 runner,再检查精确 Image ID 是否已装入内层 Docker。
master 日常交付必须禁止直接 push,只允许经 PR 在最近一次 Project CI 全绿后合并;本地 `pre-commit` 的 staged ESLint/Prettier 和 master `pre-push` 的 Repository checks parity 只用于提前发现问题,可被 `--no-verify` 绕过,不能充当服务端权威门禁。紧急直推白名单如需保留,应按人员和时限最小化,并要求执行同一 `npm run check:repository-ci <base> <head>` 后回读 push CI。
@@ -418,7 +417,7 @@ master 日常交付必须禁止直接 push,只允许经 PR 在最近一次 Pro
每次生成快照必须使用不含对象缓存的原始 CI 基础镜像,避免叠加不可释放的旧层。宿主自动维护器 `scripts/maintain-gitea-rust-cache.py` 收集完整 master push CI 的六组 V4 增量产物,复用其真实来源镜像对象并合并去重,总容量 4 GiB,不重复执行 Cargo 预热编译。测试失败可收集,取消、缺组、未完成上传、旧 attempt 或混用来源镜像不可发布。候选校验、导出和装载后删除已收集 artifact,遗留项保留 7 天;切换后通过真实 master CI 才定向清理受管旧镜像/归档,保护当前、一个回滚版、基础镜像及容器引用。
AGC Rust 两条 lane、crates、agent-run smoke、Backend 和 Native shell 的桌面壳测试均启用 sccache。Native shell 的 release build smoke 显式清空两个 wrapper,保持发布构建原有 profile/features 与资源 staging;前端和 repository checks 不启用对象缓存。继续设置 `CARGO_INCREMENTAL=0`,不共享 target、不恢复 Actions 可写缓存。可信快照通过已有固定 Image ID 分发:镜像只增加固定版本的 sccache、编译对象和来源元数据,不包含源码、target 或凭据;容器写时复制层承接本 job 的新增对象,job 删除后丢弃,PR 没有 Docker API 或快照发布权限。该权限边界由 runner 基础设施保证,不能仅用 workflow 的分支条件替代。
AGC Rust 两条 lane、crates、Backend 和 Native shell 的桌面壳测试均启用 sccache。Native shell 的 release build smoke 显式清空两个 wrapper,保持发布构建原有 profile/features 与资源 staging;前端和 repository checks 不启用对象缓存。继续设置 `CARGO_INCREMENTAL=0`,不共享 target、不恢复 Actions 可写缓存。可信快照通过已有固定 Image ID 分发:镜像只增加固定版本的 sccache、编译对象和来源元数据,不包含源码、target 或凭据;容器写时复制层承接本 job 的新增对象,job 删除后丢弃,PR 没有 Docker API 或快照发布权限。该权限边界由 runner 基础设施保证,不能仅用 workflow 的分支条件替代。
人工 bootstrap 在没有可消费快照时使用 `bash scripts/build-gitea-rust-cache.sh <已验证基础镜像> <候选镜像tag> [完整master-SHA]`;自动维护不调用它。bootstrap 固定 master 归档,在无宿主挂载、无凭据、有资源限额的容器中先通过 `scripts/ci-npm-ci-with-retry.sh` 安装根 npm 依赖,再编译预热 AGC 壳,以满足 Claude Agent SDK 随包资源的构建要求;不执行测试/应用,Cargo features 和工作目录保持实际 CI 口径。日常更新由 master CI 导出新 key,命中继承对象只传使用时间;PR 不导出、不扫描。宿主验证来源、大小和哈希,不从上传产物执行程序,只从可信来源镜像复制固定 sccache。工具链或基础镜像输入变化时重建无对象缓存基础镜像,缓存工具链必须匹配。公共快照不接受 PR,不复用 Jenkins 发布缓存。
@@ -428,7 +427,7 @@ AGC Rust 两条 lane、crates、agent-run smoke、Backend 和 Native shell 的
自动维护首次部署及故障恢复统一见 `deploy/container/README.md`。timer 约每 5 分钟检查,文件锁串行执行,使用专属 clone/状态/归档目录;只管理 Gitea CI 测试镜像,Native shell release smoke 继续清空双 wrapper。凭据只需普通账号 `write:repository`,读取 run/job/日志/产物并定向删除缓存 artifact,不需要管理员权限。切换依赖 FetchTask 暂停与在途屏障、任务持久账本和内层 Docker 空闲;Gitea 显示 cancelled、disabled 或客户端超时都不能证明执行已经结束。网关依据最终日志与执行收尾后的最终任务上报确认释放任务。首次接入/旧网关升级须等待 CI 自然结束的空闲窗口,不在运行中的 CI 上试切;脚本合并不等于服务已部署。
Rust 对象 key 包含编译工作目录。预热必须使用已核实的 Gitea checkout 路径 `/workspace/GenarrativeAI/Genarrative`:后端、独立 crate、插件、桌面壳及提示词契约从仓库根目录启动 Cargo,AGC 分片从 `apps/ai-game-creator-shell/src-tauri` 启动,agent-run smoke 从 `apps/ai-game-creator-shell` 启动。切换 AGC 编译入口前清理该临时容器的 AGC target,防止 Cargo 的 fresh 判断跳过新 cwd 所需对象;CI 各 job 的 target 本来就独立。快照保存 `workspace.txt`,job 根路径不符时直接编译。不要仅设置 `SCCACHE_BASEDIRS` 就假定 Rust 可以跨 cwd 命中,也不为缓存改写 `RUSTFLAGS` 或源码路径语义。固定 sccache `0.18.0` 的基础设施错误码 `2` 会由 wrapper 回退执行本次 rustc;其它退出码原样返回,升级 sccache/Rust 时须复核此约定。
Rust 对象 key 包含编译工作目录。预热必须使用已核实的 Gitea checkout 路径 `/workspace/GenarrativeAI/Genarrative`:后端、独立 crate、插件、桌面壳及提示词契约从仓库根目录启动 Cargo,AGC 分片从 `apps/ai-game-creator-shell/src-tauri` 启动。切换 AGC 编译入口前清理该临时容器的 AGC target,防止 Cargo 的 fresh 判断跳过新 cwd 所需对象;CI 各 job 的 target 本来就独立。快照保存 `workspace.txt`,job 根路径不符时直接编译。不要仅设置 `SCCACHE_BASEDIRS` 就假定 Rust 可以跨 cwd 命中,也不为缓存改写 `RUSTFLAGS` 或源码路径语义。固定 sccache `0.18.0` 的基础设施错误码 `2` 会由 wrapper 回退执行本次 rustc;其它退出码原样返回,升级 sccache/Rust 时须复核此约定。
sccache `0.18.0` 还会 hash 大部分 `CARGO_*` 环境变量。因此 wrapper 固定在每个容器自己的 `/opt/genarrative-ci/rust-cache/rustc-wrapper`,prepare 重写 launcher 后才启用;daemon 状态与 socket 仍各自随机隔离。预热同步 workflow 的 `CARGO_INCREMENTAL`、`CARGO_HTTP_MULTIPLEXING`、`CARGO_NET_RETRY` 和 `CARGO_TERM_COLOR`,由现有 CI 契约测试验证一致;新增 Cargo 环境配置时须同步评估 cache key,不能只看快照文件是否存在。