diff --git a/docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md b/docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md index 76e36bb03..e7b474cd4 100644 --- a/docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md +++ b/docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md @@ -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 会话核对仍列为运行时验收项。 diff --git a/docs/project-memory/plans/【实施计划】DirectProject聊天真相源收敛-2026-09-16.md b/docs/project-memory/plans/【实施计划】DirectProject聊天真相源收敛-2026-09-16.md index 9815096d9..f9040e238 100644 --- a/docs/project-memory/plans/【实施计划】DirectProject聊天真相源收敛-2026-09-16.md +++ b/docs/project-memory/plans/【实施计划】DirectProject聊天真相源收敛-2026-09-16.md @@ -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. 前端收敛为单一 reducer:bootstrap 与 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. 前端收敛为单一 reducer:bootstrap 与 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` + 原子替换处理,需要单测覆盖;不引入定时轮询。 +- 合并规则退化为"先到定形"后,若某类条目只有输出没有调用条目,该输出不显示;这是有意取舍,先观察再决定是否补规则。 - 回滚点:每一步都保持"新源可用即不依赖旧源"的中间态可回退;不允许出现新源未启用而旧源已删除的提交。 diff --git a/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md b/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md index f0b124e5a..8009c1e3f 100644 --- a/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md +++ b/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md @@ -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` 的残回合、工具输出超长截断与脱敏。 diff --git a/docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md b/docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md index 1e66add9c..8ae5d836e 100644 --- a/docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md +++ b/docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md @@ -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 和回收