文档: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:
@@ -1,6 +1,7 @@
|
||||
# 【ADR】DirectProject命令入队化与待发消息队列归宿主
|
||||
|
||||
状态:已接受(2026-09-24 设计定稿;**2026-09-30 落地完成**,实施顺序与验收见
|
||||
状态:已接受(2026-09-24 设计定稿;**2026-09-30 落地完成**;**2026-09-30 修订**:队列条目不再另存产物,
|
||||
`prompt` 与 canonical 形状改为放行时从条目重投影,见 §3 / §4 / §5 与「备选方案与取舍」6–8 条。实施顺序与验收见
|
||||
[`【实施计划】DirectProject命令入队化与待发消息队列归宿主-2026-09-24`](../technical/【实施计划】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md))
|
||||
|
||||
## 背景
|
||||
@@ -39,14 +40,17 @@
|
||||
### 3. 队列归 Thread Manager
|
||||
|
||||
- 每个 thread(线程身份就是项目规范路径)一条 FIFO;本期只支持**按顺序追加**与**按身份移除**,不做重排、优先级、编辑。
|
||||
- 队列状态**就是事件列表本身**:`queue.enqueued` 在条目仍在队期间不可回收,离开队列(取消或放行)时转为可回收并被既有规则回收。
|
||||
不新增第二张队列表;`subscribe` 的 live-set bootstrap 因此天然把当前队列交给新订阅者——这就是"加入者看到的那几条"。
|
||||
- 队列**就是事件列表本身**:在队条目 = 事件窗口里"有 `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 是入队检查的产物,只留在宿主的队列条目里。
|
||||
- `queue.enqueued { clientTurnId, userItem, creationType?, analyticsAttemptId?, at }`:`userItem` 是 canonical 用户条目,
|
||||
前端据此派生 chip 文案,Rust 不渲染、不裁成展示形状;`analyticsAttemptId` 是发送侧在入队那一刻给这一条消息的埋点身份,
|
||||
放行时用它把这一轮的成绩写成埋点候选。事件**不带** prompt——prompt 不是下发形状,放行时由条目重投影(§5)。
|
||||
- `queue.removed { clientTurnId, reason }`:`reason` 是 ts-rs 导出的 **typed 枚举**(`cancelled | dispatched`),永不用字符串,
|
||||
形状与 `turn.completed{status, failure?}` 同构。
|
||||
- 两条事件与其它运行态事件同一条流、同一个 reducer。
|
||||
@@ -59,6 +63,11 @@ Thread Manager 在一个回合收口**之后**原子地做:取队首 → 登
|
||||
**放行不重跑任何检查,也不存在"放行失败"这种状态**:放行之后的一切失败都是**回合失败**,走既有 `turn.completed.failure` 通道;
|
||||
不新增任何失败通道,也不为放行补拒单出口。
|
||||
|
||||
prompt 同样靠**重投影**,不靠另存:`userItem` 是唯一输入,其中唯一算不出的片段——引用 UI 设计文档时要展开的代码上下文,
|
||||
渲染它会往项目里写 `ui/generated-*.js`——在入队检查时冻结到该引用 part 的 `to_prompt_cache` 上。
|
||||
这个字段**宿主内存专用**:不序列化、不上线、不进历史、不进 TS 绑定。于是放行侧的 prompt 是条目的纯投影:零 IO、零校验、
|
||||
不可失败,与"放行不重跑任何检查"同一个口径。
|
||||
|
||||
### 6. 放行的触发与监护顺序
|
||||
|
||||
- 唯一放行点:回合任务收尾之后的 `kick`(正常 / 失败 / 中止三条路径共用,外加一个 drop 守卫盖 panic),幂等,并且在临界区里原子认领队首。
|
||||
@@ -107,12 +116,21 @@ Thread Manager 在一个回合收口**之后**原子地做:取队首 → 登
|
||||
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` 时会退化成时间戳 + 序号的形状。不选,两个身份各留在自己那一层。
|
||||
|
||||
## 影响与代价
|
||||
|
||||
- **入队即写盘**:引用 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` 整体退役(含它的上限常量与"队列已满"文案——
|
||||
@@ -125,9 +143,10 @@ Thread Manager 在一个回合收口**之后**原子地做:取队首 → 登
|
||||
两份已接受的 ADR 保留正文与文件名,顶部加词表注记。
|
||||
- **失去手工验证手段**:CLI 退役同时带走"真实二进制驱动执行层生产验证"这条手工路径(夹具脚本一并退役)。
|
||||
它不在 CI,损失的是排障时的一次性手段,不是门禁。
|
||||
- **埋点**:attempt id 仍由前端在**入队那一刻**生成并随命令交给宿主(放行不重算,与其它入队检查产物一样
|
||||
存进队列条目),但前端不再为排队条目**单独持有**一个句柄槽——句柄按 `clientTurnId` 存成一张表,
|
||||
回合终态按本轮开口条目的 canonical 身份认领结算,入队失败的那一条直接删掉不结算。
|
||||
- **埋点**:attempt id 仍由前端在**入队那一刻**生成并随命令交给宿主,宿主把它写进 `queue.enqueued`——它是这一条消息的埋点身份,
|
||||
与 `clientTurnId` 一样属于"发送侧在这一刻定下的身份",所以跟着事件走而不是另存条目产物。放行时用它把这一轮的成绩写成埋点候选。
|
||||
前端不再为排队条目**单独持有**一个句柄槽——句柄按 `clientTurnId` 存成一张表,回合终态按本轮开口条目的 canonical 身份认领结算,
|
||||
入队失败的那一条直接删掉不结算。
|
||||
平台会话代际翻转的 `discard` 判定仍是渲染侧职责(结算时必须由渲染侧给出)。
|
||||
|
||||
## 明确不做
|
||||
|
||||
@@ -9143,6 +9143,30 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
`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>`),条目上带七个字段。
|
||||
复核发现其中 `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。prompt 是 `userItem` 的投影,唯一的例外是引用 UI 设计文档时要展开的代码上下文
|
||||
(渲染它会往项目里写 `ui/generated-*.js`)。把它在那次入队检查里冻结到引用 part 的 `to_prompt_cache` 上——
|
||||
该字段宿主内存专用(`#[serde(skip)]` + `#[ts(skip)]`:不序列化、不上线、不进历史、不进绑定)。
|
||||
投影拆成两半:入队侧的 `freeze`(校验 + 算片段 + 写盘)与放行侧的纯折叠(零 IO、零校验、无失败出口),
|
||||
于是"放行不重跑任何检查、没有放行失败"这条不变式对新形状仍然成立。`canonical_user_item` 同理改为放行时
|
||||
`serde_json::to_value(user_item)` 重投影。副作用:引用的代码上下文片段不再统一追加在 prompt 末尾,而是跟在它所属的引用片段里,
|
||||
单条引用时逐字节不变。
|
||||
- 决策(埋点身份):`analyticsAttemptId` 由前端在入队那一刻生成、随命令交给宿主,宿主把它写进
|
||||
`queue.enqueued{clientTurnId,userItem,creationType?,analyticsAttemptId?,at}`。理由是它与 `clientTurnId` 同类——
|
||||
发送侧在这一刻定下的身份、放行时要原样交给埋点候选——所以跟着事件走,而不是另存条目产物。
|
||||
不用 `clientTurnId` 兼作埋点身份:埋点候选的 `attempt_id` 必须是 UUID(`analytics/run.rs` 的 `validate`),
|
||||
而 `clientTurnId` 在 WebView 没有 `crypto.randomUUID` 时会退化成时间戳 + 序号的形状。
|
||||
- 验证方式(设计稿,尚未实施):ADR §3 / §4 / §5 与「备选方案与取舍」6–8 条、实施计划第 6 步。
|
||||
|
||||
## 2026-09-28 渲染层下沉分支与最新 master 对齐:活动回合事实源、维护态出口与诊断详情
|
||||
|
||||
- 背景:`codex/agc-renderer-io-downshift` 把 AGC 渲染层的网络与状态下沉 Rust 之后,要重新落到 master 已经演进出的新形状上。29 处冲突里 25 处是机械取舍,真正的分歧只有三处需要定口径。
|
||||
|
||||
@@ -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` 无新增告警。
|
||||
|
||||
Reference in New Issue
Block a user