合并 origin/master(33 个提交):DirectProject 入队化与事件投影
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m30s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 1m54s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m57s
Project CI / Frontend tests (pull_request) Successful in 3m39s
Project CI / Backend tests (pull_request) Successful in 6m13s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 7m59s
Project CI / Repository checks (pull_request) Successful in 3m29s
Project CI / Native shell tests (pull_request) Successful in 8m0s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m58s

- 合并 origin/master(425fbcf63..9ee34809b),唯一冲突是共享决策日志两侧各插条目,取「两侧并存」
- 本轮 master 未触及随包资源声明、准备步骤与三份 tauri 配置,build.rs 只读校验保持不变
- 决策日志与踩坑记录同步两侧条目;docs/README.md 与项目索引按 master 收敛
- 构建期重新生成的 ts-rs 绑定经 prettier 归一后与 master 逐字节一致(合并未丢类型)
- 校验:cargo test --no-run 全 target 通过、AGC 应用 vitest 191 files / 1889 tests passed、check-package-layout、prepare-bundled-resources 18 passed、check-config、check:encoding、prettier、cargo fmt --check 全绿
This commit is contained in:
2026-09-30 17:38:35 +08:00
90 changed files with 5473 additions and 5291 deletions
+2
View File
@@ -50,6 +50,8 @@
- [引用候选由宿主注入](./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 直连入口与调用身份守卫一并退役。
- [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 拆分、命名、启用和启动时注入边界。
- [AGC 通用插件宿主与编辑器适配](./technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md):通用插件宿主、SDK、权限审计、UI 挂载和 Cocos 编辑器适配边界。
@@ -0,0 +1,180 @@
# 【ADR】DirectProject命令入队化与待发消息队列归宿主
状态:已接受(2026-09-24 设计定稿;**2026-09-30 落地完成**,含当日修订条目:队列条目不再另存产物,
引用的解析文本持久化进条目、`prompt` 与 canonical 形状放行时重投影,埋点候选 / settle 两阶段链退役
(见 §3 / §4 / §5 与「备选方案与取舍」6–9 条)。实施顺序、落地进度与验证证据见
[`【实施计划】DirectProject命令入队化与待发消息队列归宿主-2026-09-24`](../technical/【实施计划】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md))
## 背景
待发消息队列今天是纯前端状态:回合运行中用户再发送就进本地 FIFO
(`chat/components/DirectProjectComposer/chatComposerQueue.ts`),回合终态事件到达后由**恰好开着的那个窗口**放行队首
(`useDirectProjectChatController.ts` 的完成计数 effect 与 `startTurn` 的 `finally`)。三个问题:
1. 队列是这条对话里唯一没有宿主持有者的事实:另一个窗口、另一个订阅者,或只是离开工作台再回来,都看不到已经排了什么队。
2. 放行落在"哪个窗口恰好开着"上。队列一旦共享(本 ADR 要做的),两个窗口都会去放行队首,必然双发。
3. 命令边界今天写的是"接单":校验通过就登记占用、落盘用户条目、起整轮。但用户按下发送时想要的是"这条消息会被依次处理"——
命令的成功含义与用户意图之间隔着一次长度未定义的等待。
前置口径:`【ADR】DirectProject命令接单化-2026-09-23` 已把逻辑回合收归 Thread Manager,并在 §8 留了 TODO
「以后这条队列挪到 Rust 端,落点就是 Thread Manager 的接单动作」,同时把 Rust 端发送队列列进"明确不做"。
本 ADR 就是那条 TODO 的收口,并顺带收掉两处已经没有现役价值的实现。
## 决策
### 1. 词表:入队 / 入队失败 / 放行
- 命令边界的成功与失败改叫 **入队 / 入队失败**;「接单」「拒单」两个词退役,不再出现在文档、注释、标识符与测试名里。
- 旧「接单」在语义上的角色(这一轮真正成立的那一刻)改叫 **放行**。
- 因此旧句「接单成立 ⇔ 事件流里有开始有结束」要改写成「**放行成立** ⇔ 事件流里有开始有结束」。这不是换词而是角色搬家:
逐处改写时按角色判,不做字面替换(命令边界→入队/入队失败;回合成立→放行;检查归属→入队时的检查)。
### 2. 命令 = 入队
`chat_with_game_creator_direct_codex` 改义改名(建议 `enqueue_direct_codex_turn`),主体是今天"接单前"那条链**原样**跑完:
`clientTurnId` 校验 → 工作流恢复 → 用户条目校验与冻结(引用解析文本写进条目,§5)→ 前置条件 → 工程准备;通过后**只入队**——
追加 `queue.enqueued`,不登记占用、不落盘用户条目、不发回合事件、不起 codex。
任何一步失败就是**入队失败**,走命令返回的 typed 载荷(`DirectTurnRejection` → `DirectTurnEnqueueFailure`),用户就在现场。
入队按 `clientTurnId` 幂等,判重范围是"在队 ∪ 正在跑的那一轮";重复入队返回同一次成功,不再是并发入队失败。
### 3. 队列归 Thread Manager
- 每个 thread(线程身份就是项目规范路径)一条 FIFO;本期只支持**按顺序追加**与**按身份移除**,不做重排、优先级、编辑。
- 队列**就是事件列表本身**:在队条目 = 事件窗口里"有 `queue.enqueued`、且还没有配对 `queue.removed`"的那些事件,事件顺序即队首到队尾。
宿主不为条目另存第二份产物(`StoredEvent` 上没有"挂着的队列条目"这种字段),成员与顺序都从事件折出来。
`queue.enqueued` 在队期间不可回收,离开队列(取消或放行)时与配对的 `queue.removed` 一起转可回收并被既有规则回收。
`is_bootstrap_event` 不改——`subscribe` 的 live-set bootstrap 因此天然把当前队列交给新订阅者,这就是"加入者看到的那几条"。
- 上限 5 只数**在队条目**(不数正在跑的那一轮),由 Rust 持有;满队时入队失败并给出既有提示文案。
### 4. 线上形状
- `queue.enqueued { clientTurnId, userItem, creationType?, at }`:`userItem` 是 canonical 用户条目,前端据此派生 chip 文案,
Rust 不渲染、不裁成展示形状。事件**不带** prompt(prompt 不是下发形状,放行时由条目重投影,§5),
也不带埋点身份(成绩由宿主自己结算,§5)。队列事件**没有**宿主私有的第二类字段:这一条消息的全部事实就是它自己。
- `queue.removed { clientTurnId, reason }`:`reason` 是 ts-rs 导出的 **typed 枚举**(`cancelled | dispatched`),永不用字符串,
形状与 `turn.completed{status, failure?}` 同构。
- 两条事件与其它运行态事件同一条流、同一个 reducer。
### 5. 放行 = 旧「接单」的语义角色
Thread Manager 在一个回合收口**之后**原子地做:取队首 → 登记占用 → 发 `turn.started` → 落盘用户条目 → 下发用户条目 → 起整轮,
并在同一临界区写 `queue.removed{ dispatched }`(chip 消失与气泡出现在同一批 consume 里,中间没有空窗)。
**放行不重跑任何检查,也不存在"放行失败"这种状态**:放行之后的一切失败都是**回合失败**,走既有 `turn.completed.failure` 通道;
不新增任何失败通道,也不为放行补失败出口。
prompt 同样靠**重投影**,不靠另存:`userItem` 是唯一输入。引用 part 的解析文本(素材摘要 + 引用 UI 设计文档时要展开的
代码上下文,渲染它要往项目里写 `ui/generated-*.js`)由入队检查写进该 part 自己的 `resolved_text`,
放行、历史回读、turn input 三个读点共用这一份——**历史必须回放出当初那条消息**,所以它是条目事实的一部分、随条目持久化,
不是内存备忘;字段缺省即合法(`serde(default)`),旧历史没有它照样解析、按当前 manifest 现算。于是放行侧的 prompt 是条目的纯投影:零 IO、零校验、
不可失败,与"放行不重跑任何检查"同一个口径。
埋点成绩同样不靠渲染侧结算:`identity_generation`(`platform_session`:只在登录主体 / 服务 origin / 登出状态变化时推进)
在**放行**那一刻被读一次,回合终态再读一次,变了就整条不记。所以队列事件上不需要任何埋点身份字段,
也不需要"先写候选、等渲染侧确认"的两阶段。
### 6. 放行的触发与监护顺序
- 唯一放行点:回合任务收尾之后的 `kick`(正常 / 失败 / 中止三条路径共用,外加一个 drop 守卫盖 panic),幂等,并且在临界区里原子认领队首。
- 入队时也踢一脚:「队列非空 + 线程空闲」是合法状态,对应今天的"直接发送"。
- 入队**不取** `DirectTaonierActiveInvocationGuard`:它必须整轮持有(它是这一轮的调用身份,付费美术、执行会话、MCP、校验、
上下文预取都靠它把工作归属到自己那一轮),入队若取它等于"回合运行中不能入队"。入队路径的并发由工程准备自身的项目写锁与队列兜。
### 7. 渲染侧
前端不再持有队列副本:chip 只由事件投影(入队被拒时只由命令返回值给反馈,保留既有提示文案与"不丢草稿"行为);
忙态只由事件投影加「队列非空」指示。排队消息在放行前**不写** `project.jsonl`,用户气泡仍然只来自宿主条目("落盘即放行"不变)。
### 8. 作用域与寿命
每项目一条、全进程共享:A 窗口排队 B 窗口可见可取消;切项目、离开工作台、关窗口都不影响队列继续放行;进程结束队列消失。
**不做跨进程持久化**。
### 9. 顺带退役
- `--direct-codex-chat` 整个退役(解析、派发,以及只服务它的 `run_direct_game_creator_turn_at` 一对函数)。
它的历史用途只有一个:手工生产验证夹具 `scripts/direct-execution-production-fixture.mjs`(PR #439 引入,不在 CI、
没有 npm 脚本或 harness 注册、没有任何测试钉它,唯一硬依赖是进程退出码)。没有产品入口价值,也不该在入队化之后
成为第二条直接起回合的路径;夹具脚本一并退役。
- 夹具的一键入口 `scripts/run-agc-direct-execution-fixture.mjs`(09-29 新增,只把 `--agc-exe` 默认成工作区
debug 二进制)与 `package.json` 的 `check:agc-direct-execution-fixture` 随夹具一起删:夹具没了它没有第二个消费者。
- CLI 一退,`DirectTurnError::TurnAlreadyRunning` 的两个生产点(调用身份守卫、占用登记)都没有调用方,
它连同前端"同一轮消息仍在处理中"文案、专属分支与测试一起删。
- `DirectTaonierActiveInvocationGuard` 的**身份**与 Thread Manager 的 `active_turn.turn_id` 是同一件事的两份记录
(GUI 路径下同源字符串),而 09-23 ADR 立的是"同一件事只许有一处真相"。CLI 退役后它的硬阻塞消失:五个读者
(`direct_execution` / `direct_tool_bridge` / `direct_validation` / `direct_project_context` / 付费美术重生成)
已改读 Thread Manager 的活动回合身份(`active_turn_id_at`),守卫连同它的测试一起删掉,不再有第二份
进程内记录。
- 删守卫的前置判据**已核实(2026-09-30)**,不需要新增"取消路径补无条件终态":`d833ca9d3` 的兜底路径本来就
同时做三件事——释放占用、往事件流补一条 `turn.completed{aborted}`(否则前端会永远停在运行中)、调
`complete_turn` 解除占用。删掉的只有 `DirectTaonierActiveInvocationGuard` 那张进程内表;
唯一独有的东西是 60 秒启动窗口闸门,改挂在 Thread Manager 的占用登记年龄上
(`stale_turn_for_release` 的 `NeverReachedExecutor` 分支)。连接死亡那条失败事实由 app-server
连接层自己落地(`ab970b9fd`),与守卫无关。
## 备选方案与取舍
1. **状态归宿主、放行留前端**(再用一条 `claim` 命令做 CAS 认领):前端机器原样保留,但队列的继续推进依赖至少一个窗口活着,
且"谁去认领"要靠竞态解决——正是要消掉的东西。作废。
2. **入队只做形状校验、把前置检查留到放行**:会造出"放行失败"这种状态——一个没有调用方在等的失败,得为它发明新通道;
而且"首个回合还在建工程、第二条已经排队"这类合法流程会被入队误拒。作废(检查跟着入队走)。
3. **一条 `queue.changed{items:[…]}` 快照事件**代替两条细粒度事件:reducer 更傻,但每次变更搬全量、与既有细粒度事件风格不一致。不选。
4. **`reason` 用字符串**:前端只能猜、无法穷举、无法在类型层穷尽分支。不选,用 typed 枚举。
5. **保留 CLI**:省掉夹具改写,但等于为手工验证工具长期保留第二条直接起回合的路径,与"命令只有入队一个入口"冲突。不选。
6. **宿主侧另存条目产物**(`StoredEvent.pending` 挂 `clientTurnId` / `userItem` / `canonical_user_item` / `prompt` / `creation_type` / `at`):
前四项与 `queue.enqueued` 的载荷是同一份事实的第二、第三份拷贝,取消与放行要同时改两处,回收规则里还得加"产物还在就不算可回收"
的防御来兜住不一致。作废:条目就是事件载荷的投影,claim 时现推、不存。
7. **把整条 prompt 存进条目**:prompt 是 `userItem` 的投影,存整条等于把 `userItem` 的 JSON 再存一遍;唯一的例外是引用 UI 设计文档时
要写盘的那段代码上下文。改为只把算不出的片段冻结在引用 part 上,整条 prompt 从条目重投影。
8. **用 `clientTurnId` 兼作埋点身份**:埋点候选的 `attempt_id` 必须是 UUID(`analytics/run.rs` 的 `validate`),
而 `clientTurnId` 在 WebView 没有 `crypto.randomUUID` 时会退化成时间戳 + 序号的形状。不选,两个身份各留在自己那一层。
9. **保留"宿主写候选 + 渲染侧 settle"两阶段埋点**:入队化之后一次发送只有一次尝试(命令只入队、放行由宿主自己做),
"哪一次尝试算数"不再是渲染侧才知道的事;宿主自己就有 `identity_generation`,直接写终态即可。
候选表、`settle` 命令与 attempt id 一起退役,成绩也不再依赖"恰好有一个窗口在消费终态事件"。
## 影响与代价
- **入队即写盘**:引用 UI 设计文档的条目在 prompt 投影时会生成 `ui/generated-<stem>.js`(`ui_editor/persistence.rs`);
从队列里取消不撤该文件。
- **检查是时间点事实**:放行不重跑,按入队那一刻的结论放行;manifest、权限、目录在入队之后变化也照旧放行,偏差落到回合失败。
- **prompt 形状随冻结片段走**:引用的 UI 设计文档代码上下文不再统一追加在 prompt 末尾,而是跟在它所属的引用片段里。
只引用一条时 prompt 与改前逐字节相同;多条引用时块的先后变、语义不变。
- **队列随进程消失**:待发消息只在内存与事件流里,`kill -9` / 退出后重进看不到(与 09-16 ADR 的 `kill -9` 口径一致)。
- **退役面**:前端 `completionPendingRef`、`handledCompletedTurnCountRef`、`busyBaselineTurnCountRef`、`dispatchNextQueuedTurn`、
`queueSequenceRef`、`queuedTurns*` 与 `chatComposerQueue.ts` 整体退役(含它的上限常量与"队列已满"文案——
上限只留宿主一处);`turnBusy`/`beginTurnBusy`/`endTurnBusy` 改名 `commandInFlight` 并收窄成 IPC 在飞;
`directProjectTurnStatus` 的忙态改成「原生在跑 ∨ IPC 在飞 ∨ 待发消息非空」。
队列投影落在 `chat/conversation/directPendingTurns.ts`,chip 文案落在
`chat/components/DirectProjectComposer/pendingTurnChipLabel.ts`。
- **词表切换是一次性跨文档动作**:仓库里「接单/拒单」共 384 处,并非全属同一个域
(`features/agent-runtime` 的"拒单文案"属另一个域,改成"请求被拒";`单测` 这类是假阳性)。
两份已接受的 ADR 保留正文与文件名,顶部加词表注记。
- **失去手工验证手段**:CLI 退役同时带走"真实二进制驱动执行层生产验证"这条手工路径(夹具脚本一并退役)。
它不在 CI,损失的是排障时的一次性手段,不是门禁。
- **埋点**:成绩由宿主在回合终态直接写,中间不再有候选与 settle:放行那一刻读一次 `identity_generation`、终态再读一次,变了就整条不记。
于是前端不再生成、持有或回传任何埋点身份(`analyticsAttemptId` 命令参数、`beginDirectRunAnalytics`、句柄表、
settle 调用与"入队失败就删掉句柄"一并退役),队列事件也不需要埋点字段。判据口径随放行搬家:从"发送时与终态同代"
改成"放行时与终态同代",入队后、放行前发生的账号切换因此不再丢弃这一轮的成绩——那一轮确实是在新身份下跑的。
代际比对本身也不再是渲染侧职责:放行侧读一次代次、终态侧再读一次,都在宿主内完成。
## 明确不做
- 不做跨进程持久化(不在入队时写 `project.jsonl`:那会在历史里留下一条永远不会跑的假消息,破坏"用户气泡只来自宿主条目")。
- 不做重排、优先级、编辑待发条目;也不做"队列挂起 / 继续"状态——入队化之后队列里不存在会被拒的条目。
- 不给放行新增失败通道,不为"放行不重跑检查"补兜底。
- 不保留 CLI,也不为夹具保留别名或兼容入口。
## 落地时要同步的文档与注释
- `CONTEXT.md`:词条(待发消息队列 / 待发消息 / 入队 / 入队失败 / 放行 / 逻辑回合 / 在途回合)。
- `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`:§8 的 TODO 与"明确不做:Rust 端发送队列"、§7 的"命令在飞"措辞、
CLI 保持 await 的分工,全部改为入队化口径(正文保留,顶部加词表注记)。
- `docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md`:事件协议一节补两个队列事件与放行时序。
- `docs/technical/【实施计划】DirectProject命令接单化-2026-09-23.md`:第 4 步的队列 TODO 指向本 ADR。
- `docs/technical/【技术方案】Direct回合行为审计账本-2026-08-31.md`、`【技术方案】DirectProject本轮附件路径映射-2026-08-31.md`、
`docs/project-memory/plans/【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md`:删掉对 CLI 入口的承诺
(最后那处现在写在"范围外(保留)"里,口径反转)。
- `docs/project-memory/shared-memory/decision-log.md`:追加本决定,并修正"CLI 保持 await"那条。
- 代码注释:`agent/direct_runtime/user_input.rs` 模块注释里的 CLI 分工一段删掉;`agent/thread_manager/` 的
`dispatch.rs` / `mod.rs` 与 `useDirectProjectChatController.ts` 的旧措辞与 TODO 一并改。
@@ -110,7 +110,9 @@
界面不会卡在忙碌态。
- 必须同步的注释:`chat/controller/useDirectProjectChatController.ts`(catch 的职责)、
`chat/conversation/directTurnPresentation.ts`("`invoke` 直到整轮结束才返回"这句会变成错的)。
- CLI 保持 await(它要那段回复文本),两个入口的分工在命令模块里写清楚。
- ~~CLI 保持 await(它要那段回复文本),两个入口的分工在命令模块里写清楚。~~ **口径反转**:`--direct-codex-chat`
整个退役(见 [`【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24`](./【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md)),
DirectProject 只剩「入队」一个命令入口。
## 备选方案与取舍
@@ -95,4 +95,4 @@ AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话
- 宿主确实对外发布「这一轮正在跑」:快照 `phase=working`、`active` 里恰好一条 `kind=execute` 的在途许可、`executorStopped=false` —— 这正是界面重进时判断「有运行中回合可恢复/可终止」读的事实;
- 收尾后账本落 `completed`、`active` 清空、`executorStopped=true`;宿主侧落的条目(中间文本 → 工具卡片 → 工具回执)顺序不变。
- 顺带记实:CLI / 无前端宿主的历史里**既没有用户消息、也没有最终助手回复**(两者都由前端 `append_direct_project_conversation_message` 写),CLI 只有宿主侧条目与回执文本;夹具断言据此只校验宿主侧条目。
- **可复跑入口(2026-09-29)**:新增 `npm run check:agc-direct-execution-fixture`(`scripts/run-agc-direct-execution-fixture.mjs`)——默认用工作区 `src-tauri/target/debug` 里的 AGC 二进制跑**全部 11 个用例**(`completed / passes / mcp / mcp-write / native / native-resources / patch / deadline / native-session / restart / running`),也支持 `-- --cases <子集>` 与 `-- --agc-exe <path>` 透传。本轮实跑:**11/11 passed**(修复前这些用例要么根本到不了 Provider,要么统一卡在账本阶段)。这样这条真机证路不再需要手打长命令,后续改 DirectProject/执行许可链路可以直接拿它当判据。
- **可复跑入口(2026-09-29)**:新增 `npm run check:agc-direct-execution-fixture`(`scripts/run-agc-direct-execution-fixture.mjs`)——默认用工作区 `src-tauri/target/debug` 里的 AGC 二进制跑**全部 11 个用例**(`completed / passes / mcp / mcp-write / native / native-resources / patch / deadline / native-session / restart / running`),也支持 `-- --cases <子集>` 与 `-- --agc-exe <path>` 透传。本轮实跑:**11/11 passed**(修复前这些用例要么根本到不了 Provider,要么统一卡在账本阶段)。这样这条真机证路不再需要手打长命令,后续改 DirectProject/执行许可链路可以直接拿它当判据。**(该入口与它服务的夹具已随 `--direct-codex-chat` CLI 退役一并删除,见 09-24 实施计划第 4 步;本段记的是当时的原命令与结果。)**
@@ -47,7 +47,8 @@
- **随包载荷完整**:安装目录 `coding-agent/win-x64/` 下 6 个声明组件全部在位(`bin/codex.exe`、`bin/codex-code-mode-host.exe`、`codex-path/rg.exe`、`codex-resources/codex-command-runner.exe`、`codex-resources/codex-windows-sandbox-setup.exe`、`codex-package.json`),且**逐文件 SHA-256 与 `manifest.json` 全部一致(6/6)**;`codex-package.json` 声明 `codex-cli 0.155.1` / `layoutVersion=1` / `target=x86_64-pc-windows-msvc`。
- **随包 Codex 确实能起来**:把已安装的 exe 直接交给仓库夹具当被测对象
(`npm run check:agc-direct-execution-fixture -- --agc-exe "%LOCALAPPDATA%\陶泥儿开发版\genarrative-ai-game-creator-shell.exe" --cases completed`),
(当时的命令是 `npm run check:agc-direct-execution-fixture -- --agc-exe "%LOCALAPPDATA%\陶泥儿开发版\genarrative-ai-game-creator-shell.exe" --cases completed`;
该入口与夹具已随 `--direct-codex-chat` CLI 退役删除,见 09-24 实施计划第 4 步),
结果是随包 app-server **启动并回了一个 JSON-RPC 错误**:`Codex app-server JSON-RPC 失败:items must not be empty`。也就是说缺的不是随包依赖,而是客户端请求体(`agent.codex_app_server.remote_control disabled reason=provider-proxy-auth` 是夹具本地回环模式的预期行为)。
- **这是已修缺陷的现场复现,不是新问题**:该报错对应的修复是 `7ec984d0d`(2026-09-28 22:45「修复 DirectProject 空历史注入导致新项目第一条消息失败」)。`0.1.154` 的源码是 `76cdb96c5`(20:52),`git merge-base --is-ancestor 7ec984d0d 76cdb96c5` 不成立;`dev-win` 当前 `0.1.158` 的源码 `e1dacccec5` 包含它。同一条 `completed` 用例在**当前 master 的调试构建**上 PASS,所以差异来自版本而不是构建配置。
- **对本条验收的含义**:①「随包 Codex 在 Windows 上能起来」已有现场证据;②本机这份 0.1.154 在执行链路上是坏的,**更新到 `dev-win` 当前的 0.1.158 才会恢复**(这次更新同时是「AGC 客户端更新切换到官方更新插件」那条的验收动作);③仍只能由用户在桌面会话完成的:**NSIS 安装器实跑**(安装 / 卸载 / 覆盖升级)与 **GUI 登录对话 + Cocos 编辑器回归**。
@@ -8972,7 +8972,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 决策(队列与埋点听回合终态,不听命令返回):前端发送队列的放行改由"回合完成(终态事件)或接单被拒"驱动,reducer 新增 `completedTurnCount` 作为唯一判据——不能用 `turnRunning` 的下降沿,一轮可能同批开始 + 结束。埋点结算同样挂到回合终态:不能在接单返回时结算,成绩是回合末才入 `pending_runs`,提前结算会变成空操作;`runTurn` 返回"是否接单",接单失败的路径只清句柄、不结算。**加 TODO:这条队列以后挪到 Rust 端,落点就是 Thread Manager 的接单动作。** 首页"运行中的项目"快照改由 TM 的逻辑回合导出,任务侧不再单独维护一张表。
- 决策(认证失败不再重跑整轮):删掉 `withDirectCodexSessionRefresh` 的"刷新 + 重跑整轮"(重跑会重复落盘用户消息),登录态失效按普通回合失败呈现;用同一个包装的 `cancel_direct_codex_turn` 一并去掉。
- 决策(失败原因本轮不落历史):失败原因只走事件载荷与宿主诊断(`.agent/runtime/errors` + 应用日志 + 错误上报池由宿主投影写出,进池责任从前端 catch 移到宿主),不写进 `project.jsonl`——重进项目只会看到那条没有回复的用户消息。**加 TODO(暂定做法见 ADR 备选方案第 3 条 (b)):以后要做"进历史但不喂模型"的失败条目,本轮明确不持久化。**
- 决策(CLI 保持 await):CLI 入口(`cli.rs` 的 `direct-codex.chat`)继续 await 整轮,因为它要把回复文本打到终端、没有事件订阅可用;两个入口共用同一份接单前检查、同一个命令主体和同一份 `Display` 文案,不各写一套判据。
- 决策(CLI 保持 await):CLI 入口(`cli.rs` 的 `direct-codex.chat`)继续 await 整轮,因为它要把回复文本打到终端、没有事件订阅可用;两个入口共用同一份接单前检查、同一个命令主体和同一份 `Display` 文案,不各写一套判据。**(口径修正:本条已作废——2026-09-24 的 `--direct-codex-chat` 退役把 CLI 入口整个删掉,DirectProject 只剩「入队」一个命令入口;见本文件同日「命令入队化」那条与 `【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24`。)**
- 明确不做:不给失败载荷加字段(不加 `detailRef`);不恢复 invoke 拒绝通道,也不为"接单后的前置失败"新增事件类型;本轮不做"失败条目进历史但不喂模型"(TODO)、不做 Rust 端发送队列(TODO)。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{direct_turn_accept.rs,direct_thread_manager.rs,direct_thread_wire.rs,direct_turn_error.rs,direct_turn_failure.rs,direct_project_context.rs,direct_runtime/{mod.rs,user_input.rs},codex_app_server/mod.rs,runtime_driver/entrypoints.rs,cli.rs}`、前端 `chat/{controller/useDirectProjectChatController.ts,controller/useDirectProjectTurnStatus.ts,controller/useDirectThreadChatSubscription.ts,conversation/directCodexConversation.ts,conversation/directThreadChat.ts,conversation/directTurnPresentation.ts}`、`chat/generated/{DirectTurnError,DirectTurnRejection,DirectThreadEvent,...}.ts` 与 `tests/{directThreadChat.test.ts,appSurface/*.suite.ts}`;文档 `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`、`docs/technical/【实施计划】DirectProject命令接单化-2026-09-23.md`、`docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md` 与 `docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md`。
- 已知坑:`cargo test export_bindings` 会重写全部 `chat/generated/`(引号风格漂移),跑完要 `git checkout --` 掉不是本次新增的文件;本机 rust 全量 `--bins` 测试会挂在 mock server 的 `inet_csk_accept` 上,用 `--bins "agent::"` 之类过滤跑。
@@ -9108,6 +9108,105 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 影响面:`apps/ai-game-creator-shell/src-tauri/build_support/{package-layout.json,package-layout.generated.rs,package_layout.rs,godot_bundle.rs}`、`src-tauri/build.rs`、`scripts/{prepare-bundled-resources.mjs,prepare-bundled-resources.test.mjs,check-package-layout.mjs,build-release.mjs}`、两份 `.taurignore`、技术方案 §4.9/§8、M3 里程碑、运维文档、决策日志与排障经验。
- 验证:准备步骤 13 条用例通过(含三类准备步骤调度、指纹跳过、缺产物失败关闭、幂等与失败关闭);`npm run agc:bundled-resources:check` 通过;`cargo check --no-default-features` 通过(构建脚本仅剩只读校验,且不再出现在随包资源的写入路径上)。
- 边界(未验证):Windows 真机未验证——powershell/cargo 两条命令路径、Unity/Godot/Cocos 产物归位、包内容一致性与客户端加载,需按 M3 里程碑的验收清单在 Windows 上确认。
## 2026-09-24 命令入队化与待发消息队列归宿主:放行归 Thread Manager,CLI 直连入口退役
- 决策(词表):「接单 / 拒单」退役,命令边界的成功与失败改叫「入队 / 入队失败」;旧「接单」的语义角色
(这一轮真正成立的那一刻)改叫「放行」。所以旧句"接单成立 ⇔ 事件流里有开始有结束"改成"放行成立 ⇔ …",
逐处按角色改、不做字面替换;`features/agent-runtime` 的"拒单文案"属另一个域,改"请求被拒"。
- 决策(命令 = 入队):`chat_with_game_creator_direct_codex` 改义改名(暂定 `enqueue_direct_codex_turn`),
把今天"接单前"那条检查链原样跑完(身份 → 工作流恢复 → 用户条目校验 → prompt 投影 → 前置条件 → 工程准备),
通过后只入队:不登记占用、不落盘用户条目、不发回合事件、不起 codex;失败走命令返回的 typed 载荷
(`DirectTurnRejection` → `DirectTurnEnqueueFailure`)。入队按 `clientTurnId` 幂等(判重范围 = 在队 ∪ 在跑)。
- 决策(队列归 Thread Manager):每项目一条 FIFO,只支持按顺序追加与按身份移除;队列的成员与顺序就是事件列表本身
(`queue.enqueued` 在队期间不可回收、离开队列后可回收),`subscribe` 的 live-set bootstrap 因此天然把当前队列
交给中途加入的订阅者;上限 5 只数在队条目,由 Rust 持有。
- 决策(线上形状):`queue.enqueued{clientTurnId,userItem,creationType?,at}` + `queue.removed{clientTurnId,reason}`,
`reason` 是 ts-rs 导出的 typed 枚举(`cancelled|dispatched`),不用字符串;事件不带 prompt——
prompt 是入队检查的产物,只留在宿主的队列条目里。
- 决策(放行):Thread Manager 在回合收口之后原子地「取队首 → 登记占用 → `turn.started` + `queue.removed{dispatched}`」,
再落盘用户条目、下发用户条目、起整轮。**放行不重跑检查、不存在放行失败**,放行之后的一切失败都是回合失败,
走既有 `turn.completed.failure`,不新增通道。kick 点 = 回合任务收尾(含 drop 守卫盖 panic)+ 中止路径 + 入队之后,
幂等且在临界区里认领队首;入队不取 `DirectTaonierActiveInvocationGuard`(它必须整轮持有,是这一轮的调用身份)。
- 决策(前端):不再持有队列副本,chip 只由事件投影,入队失败只由命令返回值给反馈(保留提示文案与"不丢草稿")。
**(落地修正,2026-09-24)**:埋点句柄仍由前端在入队那一刻生成并随命令交给宿主(放行不重算,与其它入队检查产物一起
存进队列条目),前端把句柄按 `clientTurnId` 存成一张表、回合终态按本轮开口条目的 canonical 身份认领结算;
"不丢草稿"由「提交等命令的入队结果、只有用户自己能改的入队失败才返回 false」实现(不再是"宿主在放行时开句柄")。
- 决策(顺带退役):`--direct-codex-chat` 整个退役——它唯一的实际消费者是手工夹具
`scripts/direct-execution-production-fixture.mjs`(PR #439 引入、不在 CI、无 npm/harness 注册、无测试钉它),
夹具一并退役。**(落地修正,2026-09-24)**:`TurnAlreadyRunning` 的最后一个生产点在调用身份守卫里
(占用登记那条早已随入队化消失),所以它连同前端"同一轮消息仍在处理中"分支一起放到**守卫清理那一步**再删。
`DirectTaonierActiveInvocationGuard` 的身份与 Thread Manager 的 `active_turn.turn_id` 是同一件事的两份记录,
CLI 退役后让那五个读者改读 Thread Manager,再在第二步删掉守卫与它的 60 秒卡死兜底
(删前必须保住"回合卡死可被取消解开"这条由 `d833ca9d3` 事故换来的保证)。
**(落地,2026-09-30)**:见下方 09-30 条,守卫已删、`TurnAlreadyRunning` 已删,60 秒闸门改挂
Thread Manager 的占用登记年龄。夹具的一键入口 `scripts/run-agc-direct-execution-fixture.mjs` 与
`npm run check:agc-direct-execution-fixture`(09-29 新增,只把 `--agc-exe` 默认成工作区 debug 二进制)
同样没有任何现役消费者,随夹具一并摘掉。
- 代价:入队即写盘(引用 UI 设计文档的条目会生成 `ui/generated-<stem>.js`,取消不撤);检查是时间点事实、
放行不重跑(manifest / 权限 / 目录变化后照旧放行,偏差落到回合失败);队列随进程消失,不做跨进程持久化;
失去"真实二进制驱动执行层生产验证"这条手工路径。
- 验证方式:设计见 `docs/adr/【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md` 与
`docs/technical/【实施计划】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md`;落地提交与验证证据
见下方 2026-09-30 三条,以及该实施计划的「落地进度」与「2026-09-30 收口记录」。
## 2026-09-30 守卫清理前置判据核实:取消兜底不需要新增无条件终态,60 秒闸门改挂占用登记年龄
- 背景:按 09-24 ADR §9 的计划,删 `DirectTaonierActiveInvocationGuard` 前必须确认"回合任务 park / 泄漏时,
取消仍能清空占用并让队列继续放行"。查 git 历史(`d833ca9d3` / `ab970b9fd`)与当前实现后确认:
`cancel_direct_codex_turn_at` 的兜底路径本来就同时做三件事——释放占用、往事件流补一条
`turn.completed{aborted}`(否则前端会永远停在运行中)、调 `complete_direct_thread_turn` 解除占用并踢队列。
所以删守卫**不需要**新增"取消路径补无条件终态",那是当时就有的东西,不是守卫带来的。
- 决策:守卫只在两件事上是独有的——(1) 它是"这一轮是谁"的第二份进程内记录(与 Thread Manager 的
`active_turn.turn_id` 同源),CLI 退役后已无第二个入口,按"同一件事只许有一处真相"删;
(2) 它的 60 秒启动窗口闸门(防"刚放行、还在本地准备的回合被终止误伤")保留,改挂在 Thread Manager
的占用登记年龄上:`direct_stale_turn_for_release(root, expected, reason)` 只在
`NeverReachedExecutor` 时校验年龄(`DIRECT_STALE_TURN_RELEASE_MIN_AGE_MS`),返回身份;释放本身仍由
既有的 `complete_direct_thread_turn` + `kick_direct_queue_dispatch` 完成(天然幂等)。
- 决策:连接死亡的失败事实由 app-server 连接层自己落地(`ab970b9fd`),与守卫无关,删除守卫不动那条链路。
- 落地(`direct_runtime/mod.rs` 删守卫表与只读探测,改 `direct_active_turn_id_at`;`direct_thread_manager.rs`
补 `DirectTurnIdentity` + `read_direct_turn_identity`;`codex_app_server/mod.rs` 兜底目标改
`DirectStaleTurnReleaseReason` 并接 `direct_stale_turn_for_release`;五个身份读者与相关测试改写;
`direct_turn_dispatch.rs` 不再另取调用身份;删 `DirectTurnError::TurnAlreadyRunning` 与前端分支 / 生成绑定)。
- 验证:`cargo test --bin genarrative-ai-game-creator-shell -- agent::direct`(370 passed);新增
`stale_cancel_releases_the_occupancy_and_dispatches_the_next_pending_turn` 与两条 `direct_stale_turn_for_release`
单测;`npx vitest run tests/appSurface.test.ts`(214 passed / 9 skipped)、`directThreadChat` /
`directTurnPresentation` / `project-conversation` 定向用例、`npm --workspace apps/ai-game-creator-shell run typecheck`、
`npm run check:encoding`、`git diff --check`。
## 2026-09-30 待发消息条目不再另存产物:队列成员由事件折出,prompt 改为条目重投影
- 背景:09-24 落地时给 `StoredEvent` 加了一个"挂着的队列条目"字段(`Option<PendingDirectTurn>`,
归位后是 `agent/thread_manager/mod.rs` 的 `StoredEvent.pending: Option<PendingTurn>`),条目上带七个字段。
复核发现其中 `client_turn_id` / `user_item` / `creation_type` / `at` 与 `queue.enqueued` 的载荷是同一份事实的第二份拷贝,
`canonical_user_item` 是 `serde_json::to_value(user_item)`(第三份),而这两项在构造事件之后再没被读过;
`Some/None` 同时兼着"宿主产物袋"与"在队标记"两职,于是取消 / 放行要写两处事实、回收规则还要加"产物还在就不许回收"的防御。
与该 ADR §3 写的"队列的成员与顺序就是事件列表本身"已经不一致。
- 决策(队列成员):删掉 `StoredEvent.pending`(`append_inner` 并入 `append`);在队条目 = 事件窗口里"有 `queue.enqueued`、
且还没有配对 `queue.removed`"的那些事件,按事件顺序即队首到队尾。判重、容量、取消、认领、bootstrap 全部改读这一次折叠,
`mark_queue_events_cleanable` 删掉产物防御。
- 决策(prompt):不另存整条 prompt。引用 part 的解析文本(素材摘要 + 引用 UI 设计文档时要展开的代码上下文,
后者渲染会往项目里写 `ui/generated-*.js`)由入队检查写进该 part 自己的 `resolved_text`,**随条目持久化**:
放行、历史回读、turn input 三个读点共用这一份,历史因此回放出当初那条消息(字段缺省合法、旧历史照样解析,
只是在缺省时才退回按当前 manifest 现算)。投影拆成两半:入队侧的 `freeze`(校验 + 算片段 + 写盘 + 冻结 + 判空)
与放行侧的纯折叠(零 IO、零校验、无失败出口),于是"放行不重跑任何检查、没有放行失败"这条不变式对新形状仍然成立。
`canonical_user_item` 同理改为放行时 `serde_json::to_value(user_item)` 重投影。
副作用:引用的代码上下文片段不再统一追加在 prompt 末尾,而是跟在它所属的引用片段里,单条引用时逐字节不变。
- 决策(埋点成绩):`analyticsAttemptId` 那条链整条退役,不换字段存。入队化之后一次发送只有一次尝试,
"哪一次算数"不再是渲染侧才知道的事;宿主在**放行**那一刻读一次 `platform_session` 的 `identity_generation`、
回合终态再读一次,变了就整条不记,成绩直接写终态(不再先写候选、等渲染侧 `settle`)。
于是 `analyticsAttemptId` 命令参数、候选表 `pending_runs`、`settle_direct_run_analytics`、前端
`beginDirectRunAnalytics` 与句柄表一起删,`queue.enqueued` 也不需要埋点字段。判据口径随放行搬家:
从"发送时与终态同代"改成"放行时与终态同代"——入队后、放行前发生的账号切换不再丢弃这一轮的成绩。
- 落地(`2b98b022c` 文档 → `15a495dc2` `resolved_text` 与 prompt 拆 freeze / 纯折叠 → `0270cd601` 埋点归宿主
→ `28b64cf25` 条目改事件投影):`PendingTurn` 只留 `client_turn_id` / `user_item` / `creation_type` / `at` 并配
`from_event` / `enqueued_event` 往返;`StoredEvent` 上没有队列字段,`pending_turns` 一次折叠出成员与顺序;
`freeze_direct_codex_user_item` 是入队侧唯一算片段 / 写盘处,`direct_codex_user_item_to_prompt(&item)` 变纯折叠;
`direct_finished` 收放行代次并直写 `Request::Terminal`。
- 验证:`cargo test -- thread_manager::`(62 passed,进程级计数器用例单线程)、`-- agent::`(948 passed)、
`-- direct_runtime:: analytics:: direct_codex_user_item`(182 passed);`analytics::store_tests` 换成"终态直写"
与"放行后代际变化不记"两条用例;`npx vitest run tests/appSurface.test.ts`(194 passed)与 chat / direct 定向套件
(249 passed);`npm --workspace apps/ai-game-creator-shell run typecheck`、`npm run check:encoding`、`git diff --check`。
## 2026-09-28 渲染层下沉分支与最新 master 对齐:活动回合事实源、维护态出口与诊断详情
- 背景:`codex/agc-renderer-io-downshift` 把 AGC 渲染层的网络与状态下沉 Rust 之后,要重新落到 master 已经演进出的新形状上。29 处冲突里 25 处是机械取舍,真正的分歧只有三处需要定口径。
@@ -6139,7 +6139,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
## 2026-09-29 用夹具直接验证「已安装渠道包里的随包 Codex 能不能跑」(不用开 GUI)
- **做法**:`npm run check:agc-direct-execution-fixture -- --agc-exe "<已安装渠道包的主程序绝对路径>" --cases completed`。夹具在临时项目和 loopback Provider 上跑 CLI 执行链路,可直接核对已安装包的随包 Codex。
- **做法**:`npm run check:agc-direct-execution-fixture -- --agc-exe "<已安装渠道包的主程序绝对路径>" --cases completed`。夹具在临时项目和 loopback Provider 上跑 CLI 执行链路,可直接核对已安装包的随包 Codex。(该入口、夹具与 `--direct-codex-chat` CLI 已随入队化退役删除,见 09-24 实施计划第 4 步;这里记的是当时的做法。)
- **判读**:若报 `Codex app-server JSON-RPC 失败`,sidecar 已启动并返回协议错误,应先检查请求体及已安装包的源码版本;若根本无法启动,再查安装目录 `coding-agent/win-x64/manifest.json` 的组件哈希、`codex-package.json` 的版本和缺失组件。夹具模式中的 `remote_control disabled reason=provider-proxy-auth` 是预期诊断行。
## 2026-09-30 居中溢出叠加内部滚动,会把顶部内容裁到滚不到的地方(游玩页启动面板)
@@ -0,0 +1,253 @@
# DirectProject 命令入队化与待发消息队列归宿主实施计划
更新时间:`2026-09-24`
状态:**第 0–7 步全部落地**(2026-09-30 修订并收口)
> 文件路径口径(2026-09-30):本文里 2026-09-24 写的 `direct_thread_manager.rs` / `direct_thread_wire.rs` /
> `direct_thread_queue.rs` / `direct_turn_dispatch.rs` 现在分别是 `agent/thread_manager/{mod,wire,queue,dispatch}.rs`,
> 类型与函数去掉 `Direct` 前缀(`ThreadEvent`、`PendingTurn`、`subscribe_thread` …,见提交 `f121257cc`)。
> 同一次归位里 thread_manager 之外的几个函数也去掉了 `direct_` 前缀:`direct_active_turn_id_at` → `active_turn_id_at`、
> `direct_stale_turn_for_release` → `stale_turn_for_release`、`kick_direct_queue_dispatch` → `kick_queue_dispatch`、
> `complete_direct_thread_turn` → `complete_turn`。
设计口径见 [`【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24`](../adr/【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md)。
本文件只排实施顺序、不变式与验收,不重复设计理由。
## 落地进度
| 步骤 | 状态 | 落地说明 |
| --- | --- | --- |
| 第 0 步 词表切换 | 已落地 | `rg "接单\|拒单"` 只剩 `direct_runtime/mod.rs` 对旧 ADR 文件名的引用(链接完整性,故意保留)与 `codex_app_server/mod.rs` 的一处假阳性 |
| 第 1 步 命令 = 入队 | 已落地 | `enqueue_direct_codex_turn`(`+_typed`);队列条目落在 `agent/direct_thread_queue.rs`(第 6 步改为事件投影,不再另存 `prompt` / canonical 形状) |
| 第 2 步 队列归 Thread Manager | 已落地 | `StoredEvent.pending` 产物字段 + `enqueue_pending_turn` / `remove_pending_turn` / `claim_pending_turn`;放行在 `agent/direct_turn_dispatch.rs`(`kick_direct_queue_dispatch` + `DirectTurnReservation`) |
| 第 3 步 前端收口 | 已落地 | 见下面「第 3 步的落地细则」 |
| 第 4 步 CLI 与夹具退役 | 已落地 | 删 `CliCommand::DirectCodexChat`(变体 / `project_path_mut` / 解析 / 派发)、`run_direct_game_creator_turn_at` 一对包装函数与夹具脚本;顺带删掉只剩测试在用的 `direct_turn_error_boundary_text`(判据只剩 `direct_turn_enqueue_failure` 一处),三条边界测试改打 `direct_turn_enqueue_failure(...).message`。**`TurnAlreadyRunning` 挪到第 5 步**(它最后一个生产点在调用身份守卫里) |
| 第 5 步 守卫清理 | 已落地 | 五个身份读者改读 `direct_active_turn_id_at`;删 `DirectTaonierActiveInvocationGuard` / `DirectActiveTurnView` / 只读探测与 `release_stale_direct_taonier_active_invocation`(改 `direct_stale_turn_for_release` 只做前置校验);删 `DirectTurnError::TurnAlreadyRunning` 与前端分支 / 生成绑定;补前置判据用例 |
| 第 6 步 条目不再另存产物 | 已落地 | `PendingTurn` 就是 `queue.enqueued` 的投影(`from_event` / `enqueued_event` 往返),删掉 `canonical_user_item` 与 `prompt` 两个宿主私有产物字段;`StoredEvent` 上没有队列条目字段(`pending_turns` 一次事件折叠);引用解析文本落 `AgcResourceReference.resolved_text` 并随条目持久化;`canonical` 与 `prompt` 放行时重投影(提交 `15a495dc2` / `28b64cf25`) |
| 第 7 步 埋点成绩归宿主 | 已落地 | 删候选 / settle 两阶段与 `analyticsAttemptId`(`Request::DirectCandidate` / `Request::Settle` / `settle` / `pending_runs` / `settle_direct_run_analytics` / 前端句柄表与结算 effect);放行读一次 `identity_generation`、终态再读一次后直写 `Request::Terminal`(提交 `0270cd601`) |
## 第 0 步:词表切换(与代码同批,不单独提交)
「接单 / 拒单」退役,改成「入队 / 入队失败 / 放行」。逐处按角色改,**不做字面替换**:
| 旧写法 | 新写法 |
| --- | --- |
| 命令的成功 / 失败(接单 / 拒单) | 入队 / 入队失败 |
| 这一轮真正成立的那一刻(接单) | 放行 |
| 接单前的检查 | 入队时的检查 |
| 接单成立 ⇔ 事件流里有开始有结束 | **放行成立** ⇔ 事件流里有开始有结束 |
| 接单后的失败都是回合失败 | **放行后**的失败都是回合失败 |
三类出现点必须分开处理(全仓 384 处):
1. DirectProject 域(文档、注释、标识符、测试名):按上表改。主要落点 `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`、
`docs/technical/【实施计划】DirectProject命令接单化-2026-09-23.md`、
`docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md`、`CONTEXT.md`、
`agent/direct_runtime/user_input.rs`、`agent/direct_turn_accept.rs`、`agent/direct_turn_error.rs`、`agent/direct_thread_manager.rs`、
`chat/controller/useDirectProjectChatController.ts`、`chat/conversation/directTurnPresentation.ts`、`tests/appSurface/chat-composer.suite.ts`。
2. `features/agent-runtime` 域的「拒单文案」(`model.ts`、`tests/agentRuntimeModel.test.ts`):那里没有队列,
改成「请求被拒 / 拒绝」,不要写成「入队失败」。
3. 假阳性:`单测` 这类词不动(如 `server-rs/crates/api-server/src/editor_project.rs`)。
标识符同批改(映射表):
| 旧 | 新 |
| --- | --- |
| `chat_with_game_creator_direct_codex` | `enqueue_direct_codex_turn` |
| `DirectTurnRejection`(ts-rs 导出) | `DirectTurnEnqueueFailure` |
| 前端 `readDirectTurnRejection` / `directTurnRejectionNotice*` | `readDirectTurnEnqueueFailure` / `directTurnEnqueueFailureNotice*` |
| Rust `direct_turn_rejection` | `direct_turn_enqueue_failure` |
| `DirectTurnReservation::accept` | `DirectTurnReservation::start` |
| `accept_direct_thread_turn` / `DirectThreadManager::accept_turn` | `start_direct_thread_turn` / `start_turn` |
| `agent/direct_turn_accept.rs` | `agent/direct_turn_dispatch.rs` |
| `DirectTurnError::TurnAlreadyRunning` | 删除(第 4 步判据确认零调用方后) |
两份已接受的 ADR 保留正文与文件名,只在顶部加一行词表注记,避免正文里的旧词变成假命题。
验收:`rg -n "接单|拒单"` 在 DirectProject 域与 `agent-runtime` 域均为 0;生成绑定重跑(`cargo test export_bindings`)后
`git diff` 只剩映射表内的改动。
## 第 1 步:命令 = 入队(Rust)
改动点:
- `agent/direct_runtime/user_input.rs`:把命令主体拆成两半。
**入队侧**:`clientTurnId` 校验 → 工作流恢复 → 用户条目校验 → prompt 投影 → 前置条件 → 容量预判 → 工程准备 → 入队;
任何一步失败返回 typed 入队失败。容量预判只是一次提前的快速失败,权威判据仍在入队临界区里(工程准备是分钟级、会
在磁盘留产物的活,满了就不该先做它)。**放行侧**(第 2 步)从占用登记起。
其中"工作流恢复"只在**真的需要恢复**时才去取整轮任务锁(`direct-codex-art`):正在跑的那一轮整包重生成整轮持有
它,入队无条件取锁会先同步阻塞约 1 秒再被拒(见 ADR §6"入队不取整轮持有的锁")。
- 队列条目(宿主侧产物,只在内存):`PendingDirectTurn { client_turn_id, user_item: Value, prompt: String, creation_type: Option<String>, at: u64 }`。
`prompt` 与 `creation_type` 是入队检查的产物,放行不再重算;事件里**不带** `prompt`。
- `agent/direct_thread_manager.rs`:
- `MAX_PENDING_DIRECT_TURNS = 5` 落在 Rust,只数在队条目;满队 → typed 入队失败。
- 队列的成员与顺序**就是事件列表本身**:宿主侧产物挂在对应的 `queue.enqueued` 事件上(`StoredEvent` 增加一个非序列化的
可选产物字段),不另建队列表。
- `observe_event`:`queue.enqueued` 在队期间**不可回收**;`queue.removed` 可回收,并把同 `clientTurnId` 的 enqueued 标记为可回收。
`is_bootstrap_event` 不改——live-set bootstrap 因此自动把当前队列交给新订阅者。
- 入队幂等:判重范围「在队 ∪ 正在跑的那一轮」,重复入队返回同一次成功。
- `remove_direct_project_pending_turn(project_path, client_turn_id)`:typed 结果枚举 `Removed | AlreadyDispatched | NotFound`,
在临界区里判「是否仍未被认领」。
- 线上形状:`DirectThreadEvent::QueueEnqueued { client_turn_id, user_item, creation_type?, at }`(`queue.enqueued`)、
`QueueRemoved { client_turn_id, reason: DirectQueueRemovalReason }`(`queue.removed`),
`DirectQueueRemovalReason` 是 ts-rs 导出的 typed 枚举 `cancelled | dispatched`。
不变式:入队不登记占用、不落盘、不发回合事件、不起 codex;入队失败不写用户条目、不产生事件。
## 第 2 步:放行与 kick
- `start_direct_thread_turn`:同一个临界区里「取队首 → 占用登记 → `turn.started` + `queue.removed{dispatched}`」;
之后落盘用户条目 → 下发用户条目 → spawn 整轮(顺序与今天的接单后半段一致,`accept` 必须早于落盘与 `turn/start`)。
- `kick_direct_queue_dispatch(thread_id)`:幂等;在临界区里判「无占用 + 队首存在 + 未被认领」,认领后 spawn 放行任务。
调用点三个:回合任务收尾(正常 / 失败共用)、中止路径、入队之后。
- panic 兜底:回合任务里的一个 drop 守卫负责踢一脚,保证任务 panic 或 future 被丢弃时队列不会永久停住。
- 入队不取 `DirectTaonierActiveInvocationGuard`;该守卫继续由整轮持有。
- 不变式:放行不重跑检查、没有放行失败;放行之后的一切失败都走 `turn.completed.failure`。
验收(Rust 单测):放行原子性(`queue.removed{dispatched}` 与 `turn.started` 同批、无中间窗口);
kick 幂等(并发两次只认领一次);队首在放行后被移除、remove 对已放行条目返回 `AlreadyDispatched`;
回合失败 / 中止后队列继续放行下一条;任务 panic 后队列仍能继续;多订阅者游标各自独立时 bootstrap 仍重建完整队列。
## 第 3 步:前端收口
### 第 3 步的落地细则
- 队列投影是新文件 `chat/conversation/directPendingTurns.ts`:只做 `enqueuePendingTurn` /
`removePendingTurn` 两个纯函数,**不判上限、不排期、不排序**;上限与认领顺序只在宿主。
reducer 的 `queue.enqueued` / `queue.removed` 两个分支是它唯一的调用方。
- chip 文案派生搬到 `chat/components/DirectProjectComposer/pendingTurnChipLabel.ts`
(`pendingTurnChipLabel`),与 `chatComposerQueue.ts` 一起把"队列在本地"的最后一份实现删掉。
- **草稿清不清由命令的入队结果回答**:`onSubmit` 改成返回 `Promise<boolean>`,composer 只在
`true` 时清草稿。返回 `false` 的口子是"用户自己就能改的入队失败"(队列已满、参数无效这类)——
宿主已经给了同级提示,草稿再没了就等于让用户重打一遍。写权限门让路给确认流程时返回 `true`
(内容已经在重跑的入参里),与入队化之前一致。
- **埋点句柄按 `clientTurnId` 存成一张表**(`pendingRunAnalyticsRef`),回合终态按本轮开口条目的
canonical 身份(`turn.completed.userItemId` ↔ `directCodexConversationMessageId(clientTurnId,'user')`)
认领结算;身份缺失时退回结算最早的那一条(放行严格按队首顺序,收口顺序就是入队顺序)。
入队失败的那一轮直接把句柄删掉,不结算。
- `displayBusy` = 原生在跑 ∨ IPC 在飞 ∨ 待发消息非空;`commandInFlight` 收窄成"IPC 在飞"。
- 取消 chip 调 `remove_direct_project_pending_turn`,按 typed 结果 `removed / alreadyDispatched /
notFound` 说清楚;chip 的撤除仍然只认 `queue.removed` 事件(界面不改本地队列)。
退役:
- `chatComposerQueue.ts` 的 `enqueueChatTurn` / `dequeueChatTurn` / `removeQueuedChatTurn` / `isChatTurnQueueFull` /
`MAX_QUEUED_CHAT_TURNS`(提示文案 `chatQueueFullNotice` 保留,改由 Rust 的 typed 入队失败驱动)。
- controller 的 `queuedTurns` / `queuedTurnsRef` / `queueSequenceRef` / `completionPendingRef` / `handledCompletedTurnCountRef` /
`busyBaselineTurnCountRef` / `dispatchNextQueuedTurn` / `beginTurnBusy` / `endTurnBusy` / `turnBusyRef` 与排序用的计数 effect。
- 排队条目那部分 `pendingRunAnalyticsRef` 与 `beginDirectRunAnalytics` 的调用时序(埋点句柄改由宿主在放行时开)。
保留与改写:
- chip 由运行态事件的投影驱动(新增 pending 列表投影与两条队列事件的 reducer 分支);
chip 文案仍用现成的派生(`directCodexContentToPromptText` + `resourceLabelResolver`),只是输入换成事件里的 `userItem`。
- 取消 chip 改调 `remove_direct_project_pending_turn`;入队失败只由命令返回值驱动提示(保留"不丢草稿"行为)。
- 忙态 = 事件投影 + 「队列非空」指示;`directProjectTurnStatus` 的"命令在飞"分支收成 IPC 在飞。
- 写权限门 `ensureConversationWriteAllowed` 留在入队之前(确认框必须在用户在场时弹)。
验收:`tests/directThreadChat.test.ts` 补队列事件投影用例(顺序、幂等、按身份移除、bootstrap 带出在队条目、
回合收口不清队列);`tests/appSurface/chat-composer.suite.ts` 的排队 / 取消 / 满队 / 放行四组用例改成新语义,
并新增「入队只发一次 IPC」「IPC 在飞时挡住第二次提交」两条;`npm --workspace apps/ai-game-creator-shell run typecheck` 通过。
## 第 4 步:CLI 与夹具退役
- `cli.rs`:删 `CliCommand::DirectCodexChat` 变体、`project_path_mut` 分支(`cli.rs:181`)与派发分支(`cli.rs:899-935`)。
- `agent/direct_runtime/mod.rs`:删 `run_direct_game_creator_turn_at` 与 `run_direct_game_creator_turn_at_with_creation_type`
(各自只有彼此与 CLI 一个调用方)。
- 删 `apps/ai-game-creator-shell/scripts/direct-execution-production-fixture.mjs`(CLI 的唯一消费者)。
- 删 `scripts/run-agc-direct-execution-fixture.mjs` 与 `package.json` 的 `check:agc-direct-execution-fixture`
(只服务该夹具,夹具没了没有第二个消费者)。
- `DirectTurnError::TurnAlreadyRunning` 与前端"同一轮消息仍在处理中"文案、专属分支、`project-conversation.suite.ts`
的对应断言**挪到第 5 步**(它最后一个生产点在调用身份守卫里)。
- 文档同步:09-22 里程碑把 `--direct-codex-chat` 从"范围外(保留)"改成退役项;两份 Direct 技术方案的 CLI 承诺删掉;
09-23 ADR 与 `decision-log.md` 里"CLI 保持 await"的口径改掉。
验收:`rg -n -- "--direct-codex-chat"` 与 `rg -n "run_direct_game_creator_turn_at"` 零命中;
`cargo check --tests` 无新增 `dead_code` 告警;`check-config.mjs` 与 `.gitea/workflows/project-ci.yml` 不受影响(已核实无引用)。
## 第 5 步:守卫清理(CLI 退役之后)
> 前置判据**已核实(2026-09-30)**:`d833ca9d3` 的取消兜底本来就同时做三件事——释放占用、往事件流补一条
> `turn.completed{aborted}`(否则前端永远停在运行中)、解除 `complete_direct_thread_turn` 占用;所以删守卫
> **不需要**新增任何"取消路径补无条件终态"。连接死亡那条失败事实由 app-server 连接层自己落地(`ab970b9fd`),
> 也与守卫无关。守卫唯一独有的东西是 60 秒启动窗口闸门,改挂在 Thread Manager 的占用登记年龄上。
- 五个身份读者(`agent/direct_execution.rs`、`agent/direct_tool_bridge.rs`、`agent/direct_validation.rs`、
`agent/direct_project_context.rs`、`agent/direct_runtime/mod.rs` 的付费美术重生成)改读
`direct_active_turn_id_at`(Thread Manager 的活动回合,只读、不改占用)。
- 删 `DirectTaonierActiveInvocationGuard`、`DirectActiveTurnView`、`read_direct_taonier_active_invocation_at` 与
`release_stale_direct_taonier_active_invocation` 及它们的测试;`codex_app_server` 的兜底目标改
`DirectStaleTurnReleaseReason`,释放本身仍由既有的 `complete_direct_thread_turn` + `kick_direct_queue_dispatch` 完成。
- 删 `DirectTurnError::TurnAlreadyRunning`(最后一个生产点在守卫里)与前端"同一轮消息仍在处理中"文案、专属分支、
生成绑定与 `tests/appSurface/project-conversation.suite.ts` 的对应断言。
- 前置判据测试:`cancel_direct_codex_turn_at` 在"回合任务泄漏、app-server 侧没有可中断句柄"时仍能解除占用,
并把队首放行出去(`codex_app_server` 的兜底用例 + `direct_stale_turn_for_release` 的单测)。
## 第 6 步:条目不再另存产物(2026-09-30 修订)
判据:**在队条目除 `queue.enqueued` 的载荷之外不得有第二个字段**,`prompt` 也不得整条另存。
- `agent/direct_codex_user_item/model.rs`:`AgcResourceReference` 增加 `resolved_text: Option<String>`,
存这个引用 part 被解析出来的文本(素材摘要 + 引用 UI 设计文档时要展开的代码上下文)。它是条目事实的一部分、
**随条目持久化**:放行、历史回读、turn input 三个读点共用这一份,历史因此回放出当初那条消息;
字段缺省合法(`#[serde(default)]`,序列化时 `None` 不写字段),旧历史没有它照样解析、只在缺省时按当前 manifest 现算。
- `agent/direct_codex_user_item/wire.rs`:prompt 投影拆成两半——入队检查时 `freeze`(校验 + 算片段 + 写盘 + 冻结进 part + 判空),
之后 `direct_codex_user_item_to_prompt(&item)` 是纯折叠(零 IO、零校验、无失败出口)。
- `agent/thread_manager/queue.rs`:`PendingTurn` 改成 `queue.enqueued` 的**投影**(`from_event` / `enqueued_event` 往返),
不再有 `canonical_user_item` 与 `prompt`;新增"在队 = 有 enqueued、没有配对 removed"的事件折叠。
- `agent/thread_manager/mod.rs`:删 `StoredEvent.pending`(`append_inner` 并入 `append`);`pending_turns` / 容量 /
判重 / `remove_pending_turn` / `claim_pending_turn` 全部改读事件折叠;`mark_queue_events_cleanable` 删掉
"产物还在就不回收"的防御。
- `agent/thread_manager/dispatch.rs`:放行的 canonical 形状 = `serde_json::to_value(&pending.user_item)`,prompt = 纯投影。
不变式:放行侧不写盘、不读 manifest、不重跑校验;入队检查仍然是这条消息唯一的失败出口。
验收(Rust 单测):`PendingTurn` 与 `queue.enqueued` 往返一致;队列折叠与原有 `pending_turn_ids` 语义一致
(顺序、按身份移除、已放行返回 `AlreadyDispatched`);`resolved_text` 缺省的历史条目仍能解析并投影;
同一批 `d341a9be1` / `e0ca5ad9b` 的中途加入 bootstrap 用例仍通过。
## 第 7 步:埋点成绩由宿主自己结算(2026-09-30 修订)
判据:`analyticsAttemptId` 整条链删除,且**不换字段存**。
- `agent/direct_runtime/user_input.rs`:入队命令不再收 `analyticsAttemptId`;`agent/thread_manager/wire.rs` 的
`queue.enqueued` 不加埋点字段(队列事件相对第 5 步零新增字段)。
- `agent/thread_manager/dispatch.rs`:放行时读一次 `platform_session` 的 `identity_generation`,随放行调用链交给回合。
- `agent/direct_runtime/mod.rs`:回合终态再读一次,与放行时不同则整条不记;相同则直接写 `Request::Terminal`。
- `analytics/run.rs`:删 `Request::DirectCandidate` / `Request::Settle` 与 `settle`,`direct_finished` 改收代际并直接落终态。
- `analytics/store.rs`:删 `pending_runs` / `pending_run_bytes` 与两条候选分支(连带 16 条上限)。
- `analytics/gui.rs` + `main.rs`:删 `settle_direct_run_analytics` 命令与注册。
- 渲染层:删 `services/clientAnalytics.ts` 的 `beginDirectRunAnalytics`、控制器的句柄表与结算 effect、
`TauriInvoke` 里的命令签名;`userItemId` 不再用于认领结算。
不变式:成绩不再依赖"恰好有一个窗口在消费终态事件";判据口径随放行搬家——入队后、放行前发生的账号切换不再丢弃成绩。
验收(Rust 单测):`analytics::store_tests` 的候选 / settle 用例改为终态直写用例(含"放行后代际变化不记");
`cargo test` 定向 + 前端 `npx vitest run tests/appSurface.test.ts`、`npm --workspace apps/ai-game-creator-shell run typecheck`。
## 验收与证据
- Rust:第 1、2、5 步各自的单测;`cargo test` 定向 + `cargo check --tests` 无新增告警。
- Node:`npx vitest run tests/appSurface.test.ts`、`npm --workspace apps/ai-game-creator-shell run typecheck`。
- 端到端(`chat-composer.suite.ts`):回合运行中入队两条 → 取消一条 → 终态后只放行剩下那条;
入队只发生一次 IPC、没有第二次发送命令;满队提示;入队失败时草稿不丢。
- 手工:两个窗口看同一项目(A 排队 B 可见可取消);离开工作台再回来队列仍在并继续放行;
`kill -9` 后重进队列消失(与 ADR 的已知边界一致)。
- 全仓:`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。
- 不涉及 SpacetimeDB schema,不需要 `npm run check:spacetime-schema`。
## 2026-09-30 收口记录
第 6、7 步按上面两节落地,提交按"文档定稿 → 引用解析文本持久化 → 埋点归宿主 → 条目改事件投影"切开:
`2b98b022c`(文档)、`15a495dc2`(`resolved_text` 与 prompt 拆 freeze / 纯折叠)、`0270cd601`(埋点成绩归宿主)、
`28b64cf25`(条目改事件投影)。
- Rust:`cargo test --bin genarrative-ai-game-creator-shell -- thread_manager::`(62 passed,进程级计数器用例单线程跑)、
`-- agent::`(948 passed;`design_runtime` 与两条历史并发用例在整包并行下偶发,单跑通过,与本次改动无关)、
`-- direct_runtime:: analytics:: direct_codex_user_item`(182 passed)、`-- analytics_real_file_write_...`(1 passed)。
- Node:`npx vitest run tests/appSurface.test.ts`(194 passed)、`tests/directThreadChat.test.ts`、
`tests/directHistoryPaging.test.ts`、`tests/directProjectTurn.test.ts` 等 chat / direct 单测(249 passed)、
`npm --workspace apps/ai-game-creator-shell run typecheck` 通过。
- 全仓:`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。
@@ -4,6 +4,9 @@
状态:**四步全部落地**。
后续:09-24 起命令边界改叫「入队 / 入队失败 / 放行」,本文里的「接单 / 拒单」与 `TurnAlreadyRunning`
都已退役(见 09-24 ADR 与 09-30 决策记录);正文保留当时口径,不再回改。
设计口径见 [`【ADR】DirectProject命令接单化-2026-09-23`](../adr/【ADR】DirectProject命令接单化-2026-09-23.md)。
本文件只排实施顺序、不变式与验收,不重复设计理由。
@@ -165,7 +165,8 @@ Supervisor / 做方案首轮忽略 `attachments`,行为不变。
- `chat_with_game_creator_direct_codex` 增加 `attachments: Option<Vec<DirectCodexTurnAttachment>>`,先渲染再调用现有 `run_direct_game_creator_turn_at_with_creation_type_and_emitter`
- 把现有 Home 渲染测试迁到新文件;本文件不再保留一份平行实现
不要把 attachments 顺着 inner turn / emitter / CLI 往下传。CLI `run_direct_game_creator_turn_at` 不变。
不要把 attachments 顺着 inner turn / emitter 往下传。(CLI 入口 `run_direct_game_creator_turn_at` 已随命令入队化退役,见
[`【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24`](../adr/【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md)。)
### 5.2 前端
@@ -38,7 +38,7 @@ Direct GUI 回合已经能看见 Codex `item/completed`,但只收成 UI 活动
- 不拷隔离 `CODEX_HOME`、不落 `auth.json`、不落 `aggregated_output` / MCP `result` / patch `diff` / `FunctionCallOutput` 正文。
- 不把原始 item JSON 送进 Tauri 前端事件(现有 `DirectCodexTurnObservation` 仍只允许安全活动词和流式正文)。
- 不扫 `kind=uploaded` 历史附件;只记本轮 sidecar 提供的集合。
- DirectHome、ToolHost、CLI `--direct-codex-chat`(无 `clientTurnId`)本期不写这份账本。
- DirectHome、ToolHost 本期不写这份账本。(`--direct-codex-chat` 已随命令入队化退役,不再是入口。)
- 本期不改 UI,不在聊天面板展示审计。
- 不把 issue #212 标成已修复;sidecar 与本账本是两段工作。
@@ -292,7 +292,9 @@ chat_with_game_creator_direct_codex
→ 写 turn_end + agent.db 摘要
```
- CLI `run_direct_game_creator_turn_at` **不** 接 audit(无 `clientTurnId`)。
- CLI 入口已退役:`run_direct_game_creator_turn_at` 与 `--direct-codex-chat` 一起删除(见
[`【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24`](../adr/【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md)),
所以「无 `clientTurnId` 的 Direct 回合」这条路径不存在了。
- Home command 不接 audit。
- 不要把 attachments / audit 顺着 CLI inner、pool、ToolHost 往下传。
- `DirectCodexTurnObservation` **不** 增加原始 `params`。审计走独立 `DirectCodexTurnAudit`,避免 stdout 正文进入 Tauri 事件。