文档:DirectProject 待发消息条目改为事件投影,prompt 改为条目重投影

- ADR §3/§4/§5 与备选方案 6–8、影响与代价:删掉 StoredEvent.pending 的第二份产物,队列成员由事件折出;prompt 只在引用 part 上冻结算不出的片段(to_prompt_cache,宿主内存专用),canonical 形状与 prompt 放行时重投影;analyticsAttemptId 进 queue.enqueued
- 实施计划:状态改第 0–6 步、落地进度表补第 6 步、新增「第 6 步:条目不再另存产物」的实现点与验收
- decision-log:追加 2026-09-30 修订条目(背景、三条决策、埋点身份的口径)
This commit is contained in:
2026-09-30 15:20:39 +08:00
parent 888c249ffd
commit e70b338900
3 changed files with 78 additions and 10 deletions
@@ -2,7 +2,7 @@
更新时间:`2026-09-24`
状态:**已落地**(第 0–5 步全部完成,2026-09-30)
状态:**已落地**(第 0–6 步全部完成,2026-09-30)
设计口径见 [`【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24`](../adr/【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md)。
本文件只排实施顺序、不变式与验收,不重复设计理由。
@@ -12,11 +12,12 @@
| 步骤 | 状态 | 落地说明 |
| --- | --- | --- |
| 第 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` |
| 第 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 步 条目不再另存产物 | 已落地 | 删 `StoredEvent.pending` 与 `PendingDirectTurn` 的存储形态(改事件投影);`prompt` 片段冻结进 `AgcResourceReference.to_prompt_cache`;`analyticsAttemptId` 进 `queue.enqueued`;`canonical` 形状放行时重投影 |
## 第 0 步:词表切换(与代码同批,不单独提交)
@@ -176,6 +177,30 @@ kick 幂等(并发两次只认领一次);队首在放行后被移除、rem
- 前置判据测试:`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` 增加宿主内存专用的
`to_prompt_cache: Option<String>`(`#[serde(skip)]` + `#[ts(skip)]`:不序列化、不上线、不进历史、不进绑定),
存的是这个引用 part 在 prompt 里的片段。
- `agent/direct_codex_user_item/wire.rs`:prompt 投影拆成两半——入队检查时 `freeze`(校验 + 算片段 + 写盘 + 冻结进 part),
之后 `direct_codex_user_item_to_prompt(&item)` 是纯折叠(零 IO、零校验、无失败出口)。
- `agent/direct_thread_wire.rs`:`queue.enqueued` 增加 `analyticsAttemptId?`(发送侧在入队那一刻定的埋点身份)。
- `agent/direct_thread_queue.rs`:`PendingDirectTurn` 改成 `queue.enqueued` 的**投影**(`from_event` / `enqueued_event` 往返),
不再有 `canonical_user_item` 与 `prompt`;新增"在队 = 有 enqueued、没有配对 removed"的事件折叠。
- `agent/direct_thread_manager.rs`:删 `StoredEvent.pending`(`append_inner` 并入 `append`);`pending_turns` / 容量 /
判重 / `remove_pending_turn` / `claim_pending_turn` 全部改读事件折叠;`mark_queue_events_cleanable` 删掉
"产物还在就不回收"的防御。
- `agent/direct_turn_dispatch.rs`:放行的 canonical 形状 = `serde_json::to_value(&pending.user_item)`,prompt = 纯投影,
`analyticsAttemptId` 从事件读。
不变式:放行侧不写盘、不读 manifest、不重跑校验;入队检查仍然是这条消息唯一的失败出口。
验收(Rust 单测):`PendingDirectTurn` 与 `queue.enqueued` 往返一致;`to_prompt_cache` 不出现在序列化结果里;
队列折叠与原有 `pending_turn_ids` 语义一致(顺序、按身份移除、已放行返回 `AlreadyDispatched`);
同一批 `d341a9be1` / `e0ca5ad9b` 的中途加入 bootstrap 用例仍通过。
## 验收与证据
- Rust:第 1、2、5 步各自的单测;`cargo test` 定向 + `cargo check --tests` 无新增告警。