按 grill 结论修正 DirectProject 聊天真相源收敛计划

里程碑与实施计划改定边界:Thread Manager 只搬运脱敏原始条目,不生成卡片形状与可见性
条目身份归一为 call_id ?? id,Thread Manager 与前端不再暴露第二个 id 概念,分页锚点仍取文件里的原始 item id
合并规则收敛为"先到定形、后到补空白",删除逐字段优先级表
主规范补充条目形状三条职责边界,并说明思考正文流式下发不放宽可见范围
ADR 同步:DirectRuntime 自己的进度事件与投影文件保留,前端删除的是 DirectProject 的读路径
失败与中止说明只在运行期显示;历史切片改为从文件尾反向扫描;未知 item 类型透传并加 TODO
This commit is contained in:
2026-09-16 19:05:04 +08:00
parent 656c89e4b2
commit 42b702d362
4 changed files with 61 additions and 27 deletions
@@ -12,19 +12,25 @@ AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(
- AGC 项目开发对话的持久事实源只有 **项目对话历史**`.agent/conversations/project.jsonl` 的原始条目);消息文本、工具卡片和它们的先后顺序都从它派生。
- 运行期间的回合状态只来自 **运行态事件**Thread Manager 的 subscribe / consume / notify);`notify` 只做唤醒,不携带状态。
- **聊天投影** 在读取与渲染时生成,不落盘、不成为第二事实源;`turn-stream.jsonl``tool-calls.jsonl` 停止写入、停止读取,也不再提供读取命令。
- **聊天投影** 在读取与渲染时生成,不落盘、不成为第二事实源;DirectProject 聊天框停止读取 `turn-stream.jsonl``tool-calls.jsonl`,也不再提供供前端读取命令。DirectRuntime 自己那套进度事件与文件写入属于运行时账本,本轮保留不动。
- 页面重进的运行态只由 `subscribe` 的 bootstrap 事件重建,删除活动回合快照接管路径。
- 可见性判断留在前端聊天投影:后端历史分页只按原始条目切片,前端自己跳过不可显示条目并推进锚点。
- 运行态事件与历史切片使用同形条目信封(`turnId` / `itemId` / `seq` / `payload`),Rust 在两侧套同一套安全过滤(脱敏、截断、路径归一),前端只有一个「条目 → 视图」投影函数;Rust 做的是机械投影,不是可见性判断
- 运行态事件与历史切片使用同形条目信封(`turnId` / `itemId` / `seq` / `payload`),Rust 在两侧套同一套安全过滤(脱敏、截断、路径归一),前端只有一个「原始条目 → 视图」投影函数。
- 搬运层不生成展示形状:Thread Manager 只下发脱敏原始条目(`itemType` 原样透传),工具卡片的 `kind`、标题、折叠摘要都由前端生成。
- 条目身份只有一套:`itemId = call_id ?? id`。工具 item 的 app-server id 就是 `call_id`,而 `project.jsonl` 落盘的是原始 response item(两个 id 不同),所以在进队列前归一,不向 Thread Manager 与前端暴露第二个 id 概念。
- 合并只在前端,规则只保留「先到定形、后到补空白」:第一次见到的快照决定卡片形状,后续快照只补输出与状态,不做逐字段优先级表。
- 活动回合的唯一判据是「出现过 `turn.started` 且未出现对应 `turn.completed`」;进程重启后队列消失,历史里的半截回合一律按已结束渲染。
- 分页锚点取原始条目 id;一次翻页操作在前端自动连拉,直到出现可显示条目或 `hasMore=false`,上限 5 页。
- `notify` 是唯一唤醒来源:前端在 bootstrap 之后立刻 `consume` 一次补齐竞态窗口,之后完全由 `notify` 驱动,不设低频 tick 或任何轮询兜底。
- 迁移按一次干净切换落地:不做灰度、不做运行时开关、不双跑;允许提交序列里存在「新源已启用、旧代码尚未删除」的中间窗口,禁止反向的「新源未启用、旧源已删」。
- 思考过程与工具活动同样从运行态事件与历史条目推断,界面展示保持不变。
- 运行态事件必须自足:`item.started` / `item.completed` 携带与历史切片同形的完整条目(同一套脱敏、截断、路径归一),前端按 `itemId` 合并快照得到运行中与完成态;不提供按 `itemId` 单点取快照的接口。
- 运行态事件必须自足:`item.started` / `item.completed` 携带与历史切片同形的完整**脱敏原始条目**,前端按归一后的 `itemId` 合并快照得到运行中与完成态;不提供按 `itemId` 单点取快照的接口。
- 思考正文以 `item.delta{kind:"reasoning"}` 流式下发(`item/reasoning/summaryTextDelta``item/reasoning/textDelta`)。这不放宽可见范围:同一段文本本来就已落进 `project.jsonl` 并在 `item.completed` 展示;plan 文本与命令输出仍只降级为活动状态。
- 首屏历史由 `subscribe` 返回的 `lastCompletedItemId` 锚定,再取最近切片;删除返回整份对话的历史命令。
- 生命周期锚点独立于 replay 队列保存,`subscribe` 必须返回最新的一条 `turn.started` / `turn.completed`,否则新订阅无法判定回合是否仍在运行。
- 删除范围包含 `read_direct_turn_stream``read_direct_tool_calls``list_game_creator_direct_active_turns``read_direct_project_history`(整份历史)以及 `game-creator-direct-turn-update` 事件与两个投影文件模块;保留分页用的历史切片读取。
- 删除范围包含前端对 `read_direct_turn_stream``read_direct_tool_calls``list_game_creator_direct_active_turns``read_direct_project_history`(整份历史) `game-creator-direct-turn-update` 事件的调用;保留分页用的历史切片读取,且该切片从文件尾反向扫描
- 失败与中止说明只在运行期显示,不写进 `project.jsonl`;页面重进后不再出现。
- 历史切片的 `firstItemId` 是分页锚点,始终取 `project.jsonl` 里的原始 item id,与归一后的条目身份分开计算。
- 审批与提问事件本次只作为同一条事件流 pass-through,不并入聊天 reducer 驱动的状态机,迁移面收敛在历史与运行态一致性上。
## 备选方案与取舍
@@ -35,8 +41,8 @@ AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(
## 影响
- 旧项目磁盘上遗留的 `turn-stream.jsonl` / `tool-calls.jsonl` 保留不动,不迁移、不清理、不再
- 旧项目磁盘上遗留的 `turn-stream.jsonl` / `tool-calls.jsonl` 保留不动,不迁移、不清理、不再由 DirectProject 聊天框读取
- 工具卡片的脱敏与截断必须在读取期执行一次,不能因为"原始条目已在磁盘"就把未脱敏内容直接渲染到界面。
- 回合结束语义务必由 `turn.completed` 判定;缺少该事件的残留回合不得被渲染成运行中。
- 验收证据是端到端行为,不是单元测试:回合进行中杀掉应用进程后重开项目,应看到部分文本与工具卡片按原顺序出现且不显示忙碌;正常结束后重进应与实时渲染一致;文件系统不得再新增 `turn-stream.jsonl` / `tool-calls.jsonl`
- 待实现前验证的风险:`item.started` 的条目 id 来自 app-server item`item.completed` 的来自 raw response item,两者是否同一 id 空间必须先拿真实 app-server 会话核对;若不是同一 id 空间,活跃条目将永远收不到完成事件
- id 空间已用源码核对:codex-rs `app-server-protocol/src/protocol/thread_history.rs` 中所有工具 item 都是 `id: payload.call_id.clone()`,而 `project.jsonl` 落盘的是原始 response item。真实 app-server 会话核对仍列为运行时验收项
@@ -8,31 +8,33 @@
## 修改边界
- 允许修改:`agent/direct_thread_manager.rs``agent/codex_app_server/``agent/runtime_driver/entrypoints.rs``agent/direct_project_history.rs``main.rs` 命令注册、AGC 前端订阅与聊天投影、对应测试与 `docs/`
- 明确不修改:SpacetimeDB schema 与绑定、HTTP/OpenAPI、DirectProject Runtime、Codex durable thread 行为、审批弹层现有状态来源。
- 允许修改:`agent/direct_thread_manager.rs``agent/direct_thread_raw_item.rs``agent/codex_app_server/``agent/direct_project_history.rs``main.rs` 命令注册、AGC 前端订阅与聊天投影、对应测试与 `docs/`
- 明确不修改:SpacetimeDB schema 与绑定、HTTP/OpenAPI、DirectRuntime 自己的进度事件与 `turn-stream.jsonl` / `tool-calls.jsonl` 写入、Codex durable thread 行为、审批弹层现有状态来源。
- 保持 `.env` 未提交修改,不触碰个人配置。
## 实现顺序
1. 先用真实 app-server 会话核对 `item.started``item.completed` 的条目 id 是否同一 id 空间;不一致则先在适配层统一 id,再进入后续步骤
2. Thread Manager 事件自足化:`item.started` / `item.completed` 携带经同一套脱敏、截断、路径归一的完整条目,且与历史切片同形
3. 删除 Direct 回合事件发点、`direct_turn_stream.rs` / `direct_tool_calls.rs` 两个投影模块及其命令注册,保留历史切片读取与 `subscribe` / `consume` / notify
4. 前端收敛为单一 reducerbootstrap 与 consume 走同一条事件流,bootstrap 后立刻 consume 一次补齐竞态窗口,此后只由 notify 唤醒,过期后重新订阅并原子替换
5. 首屏与分页:以 `lastCompletedItemId` 为锚点取最近切片,锚点按原始条目 id 推进,切片无可见条目时自动连拉(上限 5 页)。
6. 删除前端 `directTurnStream` / `directToolCalls` 状态、活动回合快照接管与流排序回退分支,把聊天投影收敛成"条目 → 视图"一条路径
7. 测试与文档收口:补 reducer 单测、解锁跳过的工具卡片用例、更新主规范并把冲突的实施计划与工具卡片文档改写为当前状态
1. Rust 只搬运:`agent/direct_thread_raw_item.rs` 把 Codex 原始条目挑字段、脱敏、截断后下发,事件载荷与历史切片同形,不生成卡片形状
2. 条目身份归一:`itemId = call_id ?? id`,同时从事件 envelope 与前端形状里删掉第二个 id 概念;历史切片的 `firstItemId` 继续取文件里的原始 item id
3. 思考正文流式:`item/reasoning/summaryTextDelta` `item/reasoning/textDelta` 产出 `item.delta{kind:"reasoning"}`plan 文本与命令输出保持活动状态
4. 前端收敛为单一 reducerbootstrap 与 consume 走同一条事件流,bootstrap 后立刻 consume 一次补齐竞态窗口,此后只由 notify 唤醒;合并规则只保留"先到定形、后到补空白",并删掉 `deltaText` 缓冲与回合结束后的运行态残留
5. 首屏与分页:以 `lastCompletedItemId` 为锚点取最近切片,历史读取改为从文件尾反向扫描;锚点按原始 item id 推进,切片无可见条目时自动连拉(上限 5 页)。
6. App.tsx 接线:订阅 + 立即 consume + notify 唤醒,聊天视图改由 reducer 状态投影(含工具卡片),删除 Direct 回合事件订阅与 `directTurnStream` / `directToolCalls` 状态。
7. 删除只服务旧读路径的命令与前端调用(`read_direct_project_history``read_direct_turn_stream``read_direct_tool_calls``list_game_creator_direct_active_turns`),DirectRuntime 自己的写入保留
8. 测试与文档收口:补 reducer 单测、解锁跳过的工具卡片用例、更新主规范并把冲突的实施计划与工具卡片文档改写为当前状态。
## 验证命令
1. `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml direct_thread_manager -- --nocapture`
2. `npx vitest run apps/ai-game-creator-shell/tests/directThreadEvents.test.ts apps/ai-game-creator-shell/tests/directTurnPresentation.test.ts`
1. `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml direct_thread -- --nocapture`
2. `npx vitest run apps/ai-game-creator-shell/tests/directThreadChat.test.ts apps/ai-game-creator-shell/tests/directThreadEvents.test.ts`
3. `npx vitest run apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`
4. TypeScript 类型检查与 ESLint(范围同前次 DirectProject 迁移)。
5. `npm run check:encoding``npm run check:doc-index``git diff --check`
## 风险与回滚点
- 条目 id 空间不一致会让活跃条目永远收不到完成事件:这是1 步的显式前置验证,不通过不进入实现
- 条目 id 空间不一致会让活跃条目永远收不到完成事件:第 2 步的归一必须在 Rust 出口完成;前端不得再拿到两个 id
- 事件 payload 变大(命令输出、文件变更明细):继续沿用既有截断上限,并观察 Thread Manager 单 thread 字节上限是否被提前触发。
- 订阅过期:以重新 `subscribe` + bootstrap 后立即 `consume` + 原子替换处理,需要单测覆盖;不引入定时轮询。
- 合并规则退化为"先到定形"后,若某类条目只有输出没有调用条目,该输出不显示;这是有意取舍,先观察再决定是否补规则。
- 回滚点:每一步都保持"新源可用即不依赖旧源"的中间态可回退;不允许出现新源未启用而旧源已删除的提交。
@@ -9,27 +9,41 @@
## 目标
AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话历史**(`.agent/conversations/project.jsonl`)与 **运行态事件**Thread Manager `subscribe` / `consume` / `notify`)。删除 Direct 回合事件、`turn-stream.jsonl``tool-calls.jsonl` 与活动回合快照接管这些并行真相源
AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话历史**(`.agent/conversations/project.jsonl`)与 **运行态事件**Thread Manager `subscribe` / `consume` / `notify`)。Direct 回合事件、`turn-stream.jsonl``tool-calls.jsonl` 与活动回合快照都不再是聊天视图的输入
边界固定为:Thread Manager 只是**搬运层**——把 Codex 原始条目挑字段、脱敏、截断后下发;工具卡片的形状、可见性与合并全部由前端投影完成。条目身份只有一个:Rust 在进队列前归一到 `call_id ?? id`,不再暴露第二套 id 概念。
## 范围
- 运行态事件自足化:`item.started` / `item.completed` 携带与历史切片同形的完整脱敏条目,前端按条目身份合并运行中与完成态
- 历史读取以 `subscribe` 返回的 `lastCompletedItemId` 为首屏锚点,分页锚点取原始条目 id;不可显示条目由前端跳过并继续取页
- 前端收敛为单一事件 reducer 与单一聊天投影;活动回合只由 `turn.started` `turn.completed` 判定
- 删除不再被使用的运行时事件、持久投影文件读写、命令与前端状态
- 运行态事件自足化:`item.started` / `item.completed` 携带与历史切片同形的脱敏**原始条目**,前端用同一个投影函数处理实时与回读
- 条目身份归一:`itemId = call_id ?? id`(工具 item 的 app-server id 就是 `call_id`);历史切片另给 `firstItemId` 作为分页锚点,锚点始终是文件里的原始 item id
- 思考正文流式:`item/reasoning/summaryTextDelta``item/reasoning/textDelta` `item.delta{kind:"reasoning"}` 下发正文;plan 文本与命令输出仍只降级为活动状态
- 历史读取以 `subscribe` 返回的 `lastCompletedItemId` 为首屏锚点,切片从**文件尾反向扫描**;不可显示条目由前端跳过并继续取页(上限 5 页)
- 前端收敛为单一事件 reducer 与单一聊天投影;活动回合只由 `turn.started``turn.completed` 判定;回合结束后回收该回合运行态条目。
- 删除前端对 Direct 回合事件、`turn-stream.jsonl``tool-calls.jsonl`、活动回合快照的读取,以及只服务这些读取的命令与状态。
## 不在范围内
- 审批、提问与用户输入请求的状态机迁移;本次事件只作同一条流的 pass-through。
- 跨进程回合账本、按回合统计与持久 turn ledger。
- DirectRuntime 自己的进度事件与该运行时仍在使用的 `turn-stream.jsonl` / `tool-calls.jsonl` 写入:它们属于运行时的账本,本轮只切 DirectProject 聊天框的读路径。
- 旧项目磁盘上既有投影文件的清理、迁移或回填。
- 非 DirectProject 运行时、Codex durable thread 语义、SpacetimeDB 与 HTTP 契约。
## 已确认的决策
- 投影在前端:Rust 不生成 `kind` / 标题 / 折叠摘要,也不做合并。
- 合并只保留"先到定形、后到补空白":第一次见到的快照决定卡片形状,后续快照只补输出与状态;不做逐字段优先级表。
- 条目 id 只有一套:`call_id ?? id`;前端与 Thread Manager 都不再出现第二个 id。
- 思考正文流式下发不放宽可见范围:被下发的就是此前已在 `item.completed` 展示、并已落进 `project.jsonl` 的同一段文本。
- 失败与中止说明只在运行期显示(不写 `project.jsonl`),页面重进后不再出现。
- 未知 item 类型由 Rust 原样透传(只有类型与身份),当前由前端投影丢弃。
## 依赖与前置条件
- Thread Manager 深模块、app-server 事件适配与 Tauri 桥接已存在。
- 主规范中的生命周期锚点、事件自足与首屏锚点条款已生效。
- 实现前需用真实 app-server 会话核对 `item.started``item.completed` 是否同一 id 空间
- id 空间已用源码核对:codex-rs `app-server-protocol/src/protocol/thread_history.rs` 中所有工具 item 都是 `id: payload.call_id.clone()`,而 `project.jsonl` 落盘的是原始 response item`id``call_id` 不同)。真实 app-server 会话核对仍作为运行时验收项
## 验收标准
@@ -37,11 +51,13 @@ AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话
- [ ] 回合进行中终止并重启进程后重进项目:已落盘的部分文本与工具卡片按原顺序出现,且界面不显示忙碌态。
- [ ] 进程存活期间的页面重进(含切走再切回)能恢复运行中回合,并允许终止。
- [ ] 历史分页在切片内全部是不可显示条目时仍能继续向前,不出现锚点停滞。
- [ ] 文件系统不再新增 `turn-stream.jsonl` / `tool-calls.jsonl`;聊天内容只由项目对话历史与运行态事件重建
- [ ] 聊天视图不再读取 `turn-stream.jsonl` / `tool-calls.jsonl` / Direct 回合事件 / 活动回合快照;`read_direct_turn_stream``read_direct_tool_calls``list_game_creator_direct_active_turns` 不再被前端调用
- [ ] 同一工具调用在实时与回读各只出现一张卡片(跨 id 空间按归一身份对齐)。
- [ ] 思考正文在回合进行中即可见,且不进入活动状态文本。
- [ ] 订阅过期后重新 `subscribe` 并原子替换状态,不重复渲染已完成的条目。
## 证据要求
- 自动化:Thread Manager 事件契约测试、聊天 reducer 单测(bootstrap / consume / 过期重订阅 / 残回合)、历史分页锚点测试、前端渲染测试(含此前跳过的工具卡片用例)。
- 自动化:Thread Manager 事件契约测试、原始条目搬运与脱敏测试、聊天 reducer 单测(bootstrap / consume / 过期重订阅 / 残回合 / 先到优先合并)、历史分页锚点测试、前端渲染测试(含此前跳过的工具卡片用例)。
- 运行时:真实 app-server 会话下的新回合、杀进程重开、页面重进、分页与终止。
- 边界:订阅过期、事件重复与乱序、不可显示切片、无 `turn.completed` 的残回合、工具输出超长截断与脱敏。
@@ -124,7 +124,17 @@ Thread 内所有公开事件共用一个单调递增 seq;seq 允许跳号,
生命周期锚点独立于 replay 队列保存:`turn.started` / `turn.completed` 事件即使已被队列前缀回收,`subscribe` 仍必须把最新的一条作为 bootstrap 事件返回。因此进程内任意时刻新建订阅,都能判定最新回合是运行中还是已结束,不依赖"未完成 item 恰好还在队列里"。
`item.started``item.completed` 必须携带与历史切片同形的完整条目(经同一套脱敏、截断、路径归一),不得只给 item 类型或空 payload。前端不得依赖"按 `itemId` 单点取快照"补齐正文:Rust 不提供 `getItemSnapshot(itemId)`,未完成条目的正文随事件下发,已完成条目一律通过历史读取。
`item.started``item.completed` 必须携带与历史切片同形的**脱敏原始条目**(经同一套挑字段、脱敏、截断、路径归一),不得只给 item 类型或空 payload。前端不得依赖"按 `itemId` 单点取快照"补齐正文:Rust 不提供 `getItemSnapshot(itemId)`,未完成条目的正文随事件下发,已完成条目一律通过历史读取。
条目形状的职责边界固定为三条:
1. **搬运层不生成展示形状**。Thread Manager 只下发 Codex 原始条目(`itemType` 原样透传,正文与工具明细脱敏后带上限截断),不生成工具卡片的 `kind`、标题、折叠摘要,也不判断哪些条目要显示。
2. **只有一个条目身份**`itemId = call_id ?? id`:工具 item 的 app-server id 就是 `call_id`codex-rs `thread_history.rs` 中所有工具 item 都是 `id: payload.call_id.clone()`),而 `project.jsonl` 落盘的是原始 response item`id``call_id` 不同),所以在进队列前归一。Thread Manager 与前端都不得再出现第二个 id 概念。
3. **合并只在前端,且只保留"先到定形、后到补空白"**。第一次见到的快照决定卡片形状,后续快照只补输出与状态;同一调用只出现一张卡片。
历史切片的 `firstItemId` 不是上述归一身份:分页锚点必须是 `project.jsonl` 里的原始 item id,由 Rust 从文件扫描单独算出。
思考正文以 `item.delta{kind:"reasoning"}` 流式下发(来源是 app-server 的 `item/reasoning/summaryTextDelta``item/reasoning/textDelta`)。这不放宽可见文本范围:被下发的就是此前已在 `item.completed` 展示、并已落进 `project.jsonl` 的同一段文本;plan 文本与命令输出仍然只降级为活动状态,不下发正文。
### 队列、subscriber 和回收