前端收口:待发消息队列改由事件投影驱动,退役本地队列机器

- 新增 conversation/directPendingTurns.ts:入队/移除两个纯函数,不判上限、不排期、不排序
- reducer 新增 queue.enqueued / queue.removed 分支,折成 pendingTurns 投影
- chip 文案派生搬到 pendingTurnChipLabel.ts,删除 chatComposerQueue.ts 整套本地队列实现
- controller 退役 queuedTurns / queueSequenceRef / completionPendingRef / busyBaselineTurnCountRef
  与看门 counts effect;取消 chip 改调 remove_direct_project_pending_turn 按 typed 结果说话
- 埋点句柄按 clientTurnId 存表,回合终态按本轮开口条目身份结算
- turnBusy 改名 commandInFlight 并收窄成 IPC 在飞;忙态 = 原生在跑 ∨ IPC 在飞 ∨ 待发消息非空
- 提交改成等命令的入队结果:用户自己能改的入队失败保留草稿,其余按已交出处理
- 测试:队列投影单测、入队只发一次 IPC、IPC 在飞挡住第二次提交、满队保留草稿、取消已放行条目
- 文档:实施计划标注第 0–3 步落地并补第 3 步细则,ADR 的埋点与退役面口径改成落地后的写法
This commit is contained in:
2026-09-30 10:20:37 +08:00
parent 132f96581a
commit 0534e9c678
14 changed files with 887 additions and 610 deletions
@@ -2,11 +2,22 @@
更新时间:`2026-09-24`
状态:**待实施**(设计已定稿,尚未开工)
状态:**实施中**(第 0–3 步已落地;第 4、5 步待做)
设计口径见 [`【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`,带上入队时产出的 `prompt` / `creation_type` / `analytics_attempt_id` |
| 第 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 与夹具退役 | 待做 | |
| 第 5 步 守卫清理 | 待做 | |
## 第 0 步:词表切换(与代码同批,不单独提交)
「接单 / 拒单」退役,改成「入队 / 入队失败 / 放行」。逐处按角色改,**不做字面替换**:
@@ -88,6 +99,26 @@ kick 幂等(并发两次只认领一次);队首在放行后被移除、rem
## 第 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` /
@@ -104,8 +135,9 @@ kick 幂等(并发两次只认领一次);队首在放行后被移除、rem
- 忙态 = 事件投影 + 「队列非空」指示;`directProjectTurnStatus` 的"命令在飞"分支收成 IPC 在飞。
- 写权限门 `ensureConversationWriteAllowed` 留在入队之前(确认框必须在用户在场时弹)。
验收:`tests/directThreadChat.test.ts` 补队列事件投影用例;`tests/appSurface/chat-composer.suite.ts` 的排队 / 取消 / 满队 /
放行三组用例改成新语义;`npm --workspace apps/ai-game-creator-shell run typecheck` 通过。
验收:`tests/directThreadChat.test.ts` 补队列事件投影用例(顺序、幂等、按身份移除、bootstrap 带出在队条目、
回合收口不清队列);`tests/appSurface/chat-composer.suite.ts` 的排队 / 取消 / 满队 / 放行四组用例改成新语义,
并新增「入队只发一次 IPC」「IPC 在飞时挡住第二次提交」两条;`npm --workspace apps/ai-game-creator-shell run typecheck` 通过。
## 第 4 步:CLI 与夹具退役