文档: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` 判定仍是渲染侧职责(结算时必须由渲染侧给出)。
|
||||
|
||||
## 明确不做
|
||||
|
||||
Reference in New Issue
Block a user