文档:待发消息条目改为事件投影、引用解析文本随条目持久化、埋点成绩归宿主
- ADR §3/§4/§5:在队条目 = 事件窗口里“有 queue.enqueued、没有配对 queue.removed”的折叠;队列事件不带 prompt、不带埋点身份,也没有宿主私有的第二类字段 - ADR §5/§9:引用 part 的解析文本写进自己的 resolved_text 并随条目持久化(缺省合法、旧历史照样解析),放行/历史回读/turn input 共用;放行读 identity_generation、终态比对后直写成绩,删掉候选/settle 两阶段与 attempt id - ADR 备选方案补 6–9 条(条目另存产物、整条 prompt、clientTurnId 兼作埋点身份、保留两阶段)与影响与代价(prompt 块顺序、埋点口径随放行搬家) - 实施计划:新增第 6 步(条目不再另存产物)、第 7 步(埋点成绩归宿主),落地进度表与状态同步;补 2026-09-30 文件路径口径 - decision-log:追加并修正 2026-09-30 条目(resolved_text 持久化、埋点链退役、不用 clientTurnId 兼作身份)
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# 【ADR】DirectProject命令入队化与待发消息队列归宿主
|
||||
|
||||
状态:已接受(2026-09-24 设计定稿;**2026-09-30 落地完成**;**2026-09-30 修订**:队列条目不再另存产物,
|
||||
`prompt` 与 canonical 形状改为放行时从条目重投影,见 §3 / §4 / §5 与「备选方案与取舍」6–8 条。实施顺序与验收见
|
||||
引用的解析文本持久化进条目、`prompt` 与 canonical 形状放行时重投影,埋点候选 / settle 两阶段链退役
|
||||
(见 §3 / §4 / §5 与「备选方案与取舍」6–9 条)。实施顺序与验收见
|
||||
[`【实施计划】DirectProject命令入队化与待发消息队列归宿主-2026-09-24`](../technical/【实施计划】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md))
|
||||
|
||||
## 背景
|
||||
@@ -48,9 +49,9 @@
|
||||
|
||||
### 4. 线上形状
|
||||
|
||||
- `queue.enqueued { clientTurnId, userItem, creationType?, analyticsAttemptId?, at }`:`userItem` 是 canonical 用户条目,
|
||||
前端据此派生 chip 文案,Rust 不渲染、不裁成展示形状;`analyticsAttemptId` 是发送侧在入队那一刻给这一条消息的埋点身份,
|
||||
放行时用它把这一轮的成绩写成埋点候选。事件**不带** prompt——prompt 不是下发形状,放行时由条目重投影(§5)。
|
||||
- `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。
|
||||
@@ -63,11 +64,16 @@ Thread Manager 在一个回合收口**之后**原子地做:取队首 → 登
|
||||
**放行不重跑任何检查,也不存在"放行失败"这种状态**:放行之后的一切失败都是**回合失败**,走既有 `turn.completed.failure` 通道;
|
||||
不新增任何失败通道,也不为放行补拒单出口。
|
||||
|
||||
prompt 同样靠**重投影**,不靠另存:`userItem` 是唯一输入,其中唯一算不出的片段——引用 UI 设计文档时要展开的代码上下文,
|
||||
渲染它会往项目里写 `ui/generated-*.js`——在入队检查时冻结到该引用 part 的 `to_prompt_cache` 上。
|
||||
这个字段**宿主内存专用**:不序列化、不上线、不进历史、不进 TS 绑定。于是放行侧的 prompt 是条目的纯投影:零 IO、零校验、
|
||||
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),幂等,并且在临界区里原子认领队首。
|
||||
@@ -123,6 +129,9 @@ prompt 同样靠**重投影**,不靠另存:`userItem` 是唯一输入,其
|
||||
要写盘的那段代码上下文。改为只把算不出的片段冻结在引用 part 上,整条 prompt 从条目重投影。
|
||||
8. **用 `clientTurnId` 兼作埋点身份**:埋点候选的 `attempt_id` 必须是 UUID(`analytics/run.rs` 的 `validate`),
|
||||
而 `clientTurnId` 在 WebView 没有 `crypto.randomUUID` 时会退化成时间戳 + 序号的形状。不选,两个身份各留在自己那一层。
|
||||
9. **保留"宿主写候选 + 渲染侧 settle"两阶段埋点**:入队化之后一次发送只有一次尝试(命令只入队、放行由宿主自己做),
|
||||
"哪一次尝试算数"不再是渲染侧才知道的事;宿主自己就有 `identity_generation`,直接写终态即可。
|
||||
候选表、`settle` 命令与 attempt id 一起退役,成绩也不再依赖"恰好有一个窗口在消费终态事件"。
|
||||
|
||||
## 影响与代价
|
||||
|
||||
@@ -143,10 +152,10 @@ prompt 同样靠**重投影**,不靠另存:`userItem` 是唯一输入,其
|
||||
两份已接受的 ADR 保留正文与文件名,顶部加词表注记。
|
||||
- **失去手工验证手段**:CLI 退役同时带走"真实二进制驱动执行层生产验证"这条手工路径(夹具脚本一并退役)。
|
||||
它不在 CI,损失的是排障时的一次性手段,不是门禁。
|
||||
- **埋点**:attempt id 仍由前端在**入队那一刻**生成并随命令交给宿主,宿主把它写进 `queue.enqueued`——它是这一条消息的埋点身份,
|
||||
与 `clientTurnId` 一样属于"发送侧在这一刻定下的身份",所以跟着事件走而不是另存条目产物。放行时用它把这一轮的成绩写成埋点候选。
|
||||
前端不再为排队条目**单独持有**一个句柄槽——句柄按 `clientTurnId` 存成一张表,回合终态按本轮开口条目的 canonical 身份认领结算,
|
||||
入队失败的那一条直接删掉不结算。
|
||||
- **埋点**:成绩由宿主在回合终态直接写,中间不再有候选与 settle:放行那一刻读一次 `identity_generation`、终态再读一次,变了就整条不记。
|
||||
于是前端不再生成、持有或回传任何埋点身份(`analyticsAttemptId` 命令参数、`beginDirectRunAnalytics`、句柄表与
|
||||
"入队失败就删掉句柄"一并退役),队列事件也不需要埋点字段。判据口径随放行搬家:从"发送时与终态同代"改成"放行时与终态同代",
|
||||
入队后、放行前发生的账号切换因此不再丢弃这一轮的成绩——那一轮确实是在新身份下跑的。
|
||||
平台会话代际翻转的 `discard` 判定仍是渲染侧职责(结算时必须由渲染侧给出)。
|
||||
|
||||
## 明确不做
|
||||
@@ -167,5 +176,5 @@ prompt 同样靠**重投影**,不靠另存:`userItem` 是唯一输入,其
|
||||
`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 分工一段删掉;`direct_turn_accept.rs`、
|
||||
`direct_thread_manager.rs`、`useDirectProjectChatController.ts` 的旧措辞与 TODO 一并改。
|
||||
- 代码注释:`agent/direct_runtime/user_input.rs` 模块注释里的 CLI 分工一段删掉;`agent/thread_manager/` 的
|
||||
`dispatch.rs` / `mod.rs` 与 `useDirectProjectChatController.ts` 的旧措辞与 TODO 一并改。
|
||||
|
||||
@@ -9153,18 +9153,19 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 决策(队列成员):删掉 `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` 时会退化成时间戳 + 序号的形状。
|
||||
- 决策(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` 也不需要埋点字段。判据口径随放行搬家:
|
||||
从"发送时与终态同代"改成"放行时与终态同代"——入队后、放行前发生的账号切换不再丢弃这一轮的成绩。
|
||||
- 验证方式(设计稿,尚未实施):ADR §3 / §4 / §5 与「备选方案与取舍」6–8 条、实施计划第 6 步。
|
||||
|
||||
## 2026-09-28 渲染层下沉分支与最新 master 对齐:活动回合事实源、维护态出口与诊断详情
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
更新时间:`2026-09-24`
|
||||
|
||||
状态:**已落地**(第 0–6 步全部完成,2026-09-30)
|
||||
状态:**第 0–5 步已落地;第 6、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`)。
|
||||
|
||||
设计口径见 [`【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24`](../adr/【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md)。
|
||||
本文件只排实施顺序、不变式与验收,不重复设计理由。
|
||||
@@ -17,7 +21,8 @@
|
||||
| 第 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` 形状放行时重投影 |
|
||||
| 第 6 步 条目不再另存产物 | 进行中 | 删 `StoredEvent.pending` 与 `PendingTurn` 的存储形态(改事件投影);引用解析文本落 `AgcResourceReference.resolved_text`(持久化);`canonical` 与 `prompt` 放行时重投影 |
|
||||
| 第 7 步 埋点成绩归宿主 | 进行中 | 删候选 / settle 两阶段与 `analyticsAttemptId`(含命令参数、前端句柄表与结算 effect);放行读 `identity_generation`、终态比对后直写 |
|
||||
|
||||
## 第 0 步:词表切换(与代码同批,不单独提交)
|
||||
|
||||
@@ -181,26 +186,44 @@ kick 幂等(并发两次只认领一次);队首在放行后被移除、rem
|
||||
|
||||
判据:**在队条目除 `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),
|
||||
- `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/direct_thread_wire.rs`:`queue.enqueued` 增加 `analyticsAttemptId?`(发送侧在入队那一刻定的埋点身份)。
|
||||
- `agent/direct_thread_queue.rs`:`PendingDirectTurn` 改成 `queue.enqueued` 的**投影**(`from_event` / `enqueued_event` 往返),
|
||||
- `agent/thread_manager/queue.rs`:`PendingTurn` 改成 `queue.enqueued` 的**投影**(`from_event` / `enqueued_event` 往返),
|
||||
不再有 `canonical_user_item` 与 `prompt`;新增"在队 = 有 enqueued、没有配对 removed"的事件折叠。
|
||||
- `agent/direct_thread_manager.rs`:删 `StoredEvent.pending`(`append_inner` 并入 `append`);`pending_turns` / 容量 /
|
||||
- `agent/thread_manager/mod.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` 从事件读。
|
||||
- `agent/thread_manager/dispatch.rs`:放行的 canonical 形状 = `serde_json::to_value(&pending.user_item)`,prompt = 纯投影。
|
||||
|
||||
不变式:放行侧不写盘、不读 manifest、不重跑校验;入队检查仍然是这条消息唯一的失败出口。
|
||||
|
||||
验收(Rust 单测):`PendingDirectTurn` 与 `queue.enqueued` 往返一致;`to_prompt_cache` 不出现在序列化结果里;
|
||||
队列折叠与原有 `pending_turn_ids` 语义一致(顺序、按身份移除、已放行返回 `AlreadyDispatched`);
|
||||
验收(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` 无新增告警。
|
||||
|
||||
Reference in New Issue
Block a user