固化 DirectProject 聊天真相源契约

- 主规范新增首屏锚点、生命周期锚点必进 bootstrap、事件自足与不提供单点快照四条契约
- 新增 ADR 记录单一事实源取舍、删除清单与待验证的 id 空间风险
- 新增里程碑与实施计划,CONTEXT.md 补项目对话历史/运行态事件/聊天投影词条
- 目标文档合同与文档索引同步
This commit is contained in:
2026-09-16 18:05:30 +08:00
parent ae0f9376c9
commit 2938a49cac
6 changed files with 154 additions and 0 deletions
+16
View File
@@ -172,6 +172,20 @@ _Avoid_: 多步骤向导、完整规则编辑器、拖拽编辑器
Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。
_Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
## 项目开发对话(DirectProject
**项目对话历史**:
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
**运行态事件**:
Thread Manager 向订阅者推送的当前回合原始事件流,只服务运行期间与短期断线恢复,不替代项目对话历史。
_Avoid_: 进度通知、快照轮询、第二套历史
**聊天投影**:
把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。
_Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
## Relationships
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**
@@ -206,3 +220,5 @@ _Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
- “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。
- “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。
- “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。
- “回合进度事件”曾同时指 Direct turn update 与 Thread Manager 运行态事件;已解析为 AGC 项目开发对话只保留 **运行态事件**
- “哪些消息可显示”曾可能由后端历史分页判断;已解析为可见性判断属于 **聊天投影**,后端只按原始条目分页,前端负责跳过不可显示条目并推进分页锚点。
+1
View File
@@ -34,6 +34,7 @@
- [AGC 客户端稳定版生命周期大切换](./【技术方案】AGC客户端稳定版生命周期大切换-2026-09-14.md):统一 operation、认证/Runner、项目入口、本地恢复和 dev-stack 身份边界。
- [策划会话 Runtime V2 接入与旧链路退役方案](./technical/【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md):新单 Agent 策划会话、GDD 策略、未来 MCP/Skill 兼容插槽、阶段任务与退役验收合同。
- [DirectProject Codex 原始历史与异常恢复](<./technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md>):原始 Responses item 持久化、线程注入与异常回合收尾。
- [DirectProject 对话历史单一事实源](./adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md):AGC 项目开发对话只以项目对话历史与运行态事件为真相源,聊天投影不落盘。
- [GameAgent 对话工具调用卡片](./technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md):把右侧对话里的执行命令 / 写文件投影成 Codex 风格可折叠卡片,含采集、独立历史文件、事件字段与回读契约。
- [DirectProject 客户端 Skill 与 MCP 扩展导入方案](./technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md):客户端扩展导入、按独立 Skill/MCP 拆分、命名、启用和启动时注入边界。
- [AGC 通用插件宿主与编辑器适配](./technical/【技术方案】AGC通用插件宿主与编辑器适配-2026-09-09.md):通用插件宿主、SDK、权限审计、UI 挂载和 Cocos 编辑器适配边界。
@@ -0,0 +1,42 @@
# 【ADR】DirectProject对话历史单一事实源-2026-09-16
状态:已接受
## 背景
AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(实时)、`turn-stream.jsonl`(文本段与工具交替顺序)、`tool-calls.jsonl`(已脱敏工具卡片),重进页面时还要额外接管活动回合快照。同一段文本和同一张工具卡片因此存在多个来源,实时与回读会互相覆盖,恢复路径也只能靠"哪个源先到"决定。
`.agent/conversations/project.jsonl` 里的 Codex 原始条目本身已经带着顺序(`function_call``function_call_output` 按写入顺序落行),顺序信息并不是协议缺陷,而是在投影层被丢弃。
## 决策
- AGC 项目开发对话的持久事实源只有 **项目对话历史**`.agent/conversations/project.jsonl` 的原始条目);消息文本、工具卡片和它们的先后顺序都从它派生。
- 运行期间的回合状态只来自 **运行态事件**Thread Manager 的 subscribe / consume / notify);`notify` 只做唤醒,不携带状态。
- **聊天投影** 在读取与渲染时生成,不落盘、不成为第二事实源;`turn-stream.jsonl``tool-calls.jsonl` 停止写入、停止读取,也不再提供读取命令。
- 页面重进的运行态只由 `subscribe` 的 bootstrap 事件重建,删除活动回合快照接管路径。
- 可见性判断留在前端聊天投影:后端历史分页只按原始条目切片,前端自己跳过不可显示条目并推进锚点。
- 运行态事件与历史切片使用同形条目信封(`turnId` / `itemId` / `seq` / `payload`),Rust 在两侧套同一套安全过滤(脱敏、截断、路径归一),前端只有一个「条目 → 视图」投影函数;Rust 做的是机械投影,不是可见性判断。
- 活动回合的唯一判据是「出现过 `turn.started` 且未出现对应 `turn.completed`」;进程重启后队列消失,历史里的半截回合一律按已结束渲染。
- 分页锚点取原始条目 id;一次翻页操作在前端自动连拉,直到出现可显示条目或 `hasMore=false`,上限 5 页。
- `notify` 仍是唯一唤醒意图,但前端在已判定存在活动回合期间叠加低频 `consume` tick(1–2 秒)兜底,因为唤醒本身允许丢失。
- 迁移按一次干净切换落地:不做灰度、不做运行时开关、不双跑;允许提交序列里存在「新源已启用、旧代码尚未删除」的中间窗口,禁止反向的「新源未启用、旧源已删」。
- 思考过程与工具活动同样从运行态事件与历史条目推断,界面展示保持不变。
- 运行态事件必须自足:`item.started` / `item.completed` 携带与历史切片同形的完整条目(同一套脱敏、截断、路径归一),前端按 `itemId` 合并快照得到运行中与完成态;不提供按 `itemId` 单点取快照的接口。
- 首屏历史由 `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` 事件与两个投影文件模块;保留分页用的历史切片读取。
- 审批与提问事件本次只作为同一条事件流 pass-through,不并入聊天 reducer 驱动的状态机,迁移面收敛在历史与运行态一致性上。
## 备选方案与取舍
1. **保留 `tool-calls.jsonl` 作为"读侧已脱敏"缓存**:省一次脱敏与截断,但它成为与项目对话历史并行的第二事实源,卡片状态与顺序会和实时事件分叉。选择按读取期投影,必要时在进程内缓存。
2. **保留 Direct 回合事件作为实时传输**:迁移量小,但同一段文本仍有两条实时链路,reducer 必须处理互相覆盖,正是本次要消除的问题。
3. **让后端分页按"可显示消息数"切片**:界面能少写循环,代价是 Rust 需要理解 UI 可见性,界面规则一变就要同步改后端。
## 影响
- 旧项目磁盘上遗留的 `turn-stream.jsonl` / `tool-calls.jsonl` 保留不动,不迁移、不清理、不再读。
- 工具卡片的脱敏与截断必须在读取期执行一次,不能因为"原始条目已在磁盘"就把未脱敏内容直接渲染到界面。
- 回合结束语义务必由 `turn.completed` 判定;缺少该事件的残留回合不得被渲染成运行中。
- 验收证据是端到端行为,不是单元测试:回合进行中杀掉应用进程后重开项目,应看到部分文本与工具卡片按原顺序出现且不显示忙碌;正常结束后重进应与实时渲染一致;文件系统不得再新增 `turn-stream.jsonl` / `tool-calls.jsonl`
- 待实现前验证的风险:`item.started` 的条目 id 来自 app-server item`item.completed` 的来自 raw response item,两者是否同一 id 空间必须先拿真实 app-server 会话核对;若不是同一 id 空间,活跃条目将永远收不到完成事件。
@@ -0,0 +1,38 @@
# 【实施计划】DirectProject 聊天真相源收敛
| 字段 | 值 |
| --- | --- |
| Milestone | `docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md` |
| Status | ready |
| Owner | Codex |
## 修改边界
- 允许修改:`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 行为、审批弹层现有状态来源。
- 保持 `.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 走同一条事件流,notify 唤醒为主、活动回合期间低频 `consume` tick 兜底,过期后重新订阅并原子替换。
5. 首屏与分页:以 `lastCompletedItemId` 为锚点取最近切片,锚点按原始条目 id 推进,切片无可见条目时自动连拉(上限 5 页)。
6. 删除前端 `directTurnStream` / `directToolCalls` 状态、活动回合快照接管与流排序回退分支,把聊天投影收敛成"条目 → 视图"一条路径。
7. 测试与文档收口:补 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`
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 步的显式前置验证,不通过不进入实现。
- 事件 payload 变大(命令输出、文件变更明细):继续沿用既有截断上限,并观察 Thread Manager 单 thread 字节上限是否被提前触发。
- 订阅过期与 notify 丢失:以重新 `subscribe` + 原子替换处理过期,以低频 tick 兜底丢失,两者都需要单测覆盖。
- 回滚点:每一步都保持"新源可用即不依赖旧源"的中间态可回退;不允许出现新源未启用而旧源已删除的提交。
@@ -0,0 +1,47 @@
# 【里程碑】DirectProject 聊天真相源收敛
| 字段 | 值 |
| --- | --- |
| Version | 1.0 |
| Status | proposed |
| Date | 2026-09-16 |
| Parent Spec | `docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md` |
## 目标
AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话历史**(`.agent/conversations/project.jsonl`)与 **运行态事件**Thread Manager `subscribe` / `consume` / `notify`)。删除 Direct 回合事件、`turn-stream.jsonl``tool-calls.jsonl` 与活动回合快照接管这些并行真相源。
## 范围
- 运行态事件自足化:`item.started` / `item.completed` 携带与历史切片同形的完整脱敏条目,前端按条目身份合并运行中与完成态。
- 历史读取以 `subscribe` 返回的 `lastCompletedItemId` 为首屏锚点,分页锚点取原始条目 id;不可显示条目由前端跳过并继续取页。
- 前端收敛为单一事件 reducer 与单一聊天投影;活动回合只由 `turn.started``turn.completed` 判定。
- 删除不再被使用的运行时事件、持久投影文件读写、命令与前端状态。
## 不在范围内
- 审批、提问与用户输入请求的状态机迁移;本次事件只作同一条流的 pass-through。
- 跨进程回合账本、按回合统计与持久 turn ledger。
- 旧项目磁盘上既有投影文件的清理、迁移或回填。
- 非 DirectProject 运行时、Codex durable thread 语义、SpacetimeDB 与 HTTP 契约。
## 依赖与前置条件
- Thread Manager 深模块、app-server 事件适配与 Tauri 桥接已存在。
- 主规范中的生命周期锚点、事件自足与首屏锚点条款已生效。
- 实现前需用真实 app-server 会话核对 `item.started``item.completed` 是否同一 id 空间。
## 验收标准
- [ ] 一次回合内,实时渲染的文本段与工具卡片顺序,与回合结束后重进项目看到的顺序一致。
- [ ] 回合进行中终止并重启进程后重进项目:已落盘的部分文本与工具卡片按原顺序出现,且界面不显示忙碌态。
- [ ] 进程存活期间的页面重进(含切走再切回)能恢复运行中回合,并允许终止。
- [ ] 历史分页在切片内全部是不可显示条目时仍能继续向前,不出现锚点停滞。
- [ ] 文件系统不再新增 `turn-stream.jsonl` / `tool-calls.jsonl`;聊天内容只由项目对话历史与运行态事件重建。
- [ ] 订阅过期后重新 `subscribe` 并原子替换状态,不重复渲染已完成的条目。
## 证据要求
- 自动化:Thread Manager 事件契约测试、聊天 reducer 单测(bootstrap / consume / 过期重订阅 / 残回合)、历史分页锚点测试、前端渲染测试(含此前跳过的工具卡片用例)。
- 运行时:真实 app-server 会话下的新回合、杀进程重开、页面重进、分页与终止。
- 边界:订阅过期、事件重复与乱序、不可显示切片、无 `turn.completed` 的残回合、工具输出超长截断与脱敏。
@@ -98,6 +98,10 @@ readHistory(threadId, { beforeItemId?, limit }) -> {
`notify` 只负责唤醒,不携带事件、cursor 或持久化状态。前端收到通知后调用 `consume`;通知可合并、重复或丢失,事件完整性由 `consume` 保证。
因为唤醒本身允许丢失,前端在「已判定存在活动回合」期间以低频 `consume` tick12 秒)兜底;`consume` 自带 cursor,重复 tick 不会重复返回事件。
首屏历史不通过"读取整份对话"的命令获取:`subscribe` 返回的 `lastCompletedItemId` 就是首屏锚点,前端据此调用 `readHistory` 取最近的切片,再按滚动或按钮继续向前分页。系统不提供返回整份对话历史的命令。
### 事件和顺序
Thread 内所有公开事件共用一个单调递增 seq;seq 允许跳号,前端不要求连续。事件 envelope 至少包含:
@@ -116,6 +120,12 @@ Thread 内所有公开事件共用一个单调递增 seq;seq 允许跳号,
一个 thread 同时最多有一个 active turn;一个 turn 内允许多个并发 item。`turn.completed` 必须在该 turn 的完成 item 均成功持久化后进入队列,前端据此结束运行态;不能用“不存在 unfinished item”猜测 turn 是否完成。
前端 reducer 的活动回合判定只有一条:state 中存在「出现过 `turn.started` 且未出现对应 `turn.completed`」的 `turnId` 时,该 turn 才是活动回合,界面才允许显示忙碌态。`subscribe` bootstrap 中没有这样的 `turnId`,就表示当前没有活动回合;Thread Manager 队列随进程消失,因此进程重启后历史里留下的半截回合一律按已结束渲染,前端不发明中断态,也不从历史条目反推忙碌态。
生命周期锚点独立于 replay 队列保存:`turn.started` / `turn.completed` 事件即使已被队列前缀回收,`subscribe` 仍必须把最新的一条作为 bootstrap 事件返回。因此进程内任意时刻新建订阅,都能判定最新回合是运行中还是已结束,不依赖"未完成 item 恰好还在队列里"。
`item.started``item.completed` 必须携带与历史切片同形的完整条目(经同一套脱敏、截断、路径归一),不得只给 item 类型或空 payload。前端不得依赖"按 `itemId` 单点取快照"补齐正文:Rust 不提供 `getItemSnapshot(itemId)`,未完成条目的正文随事件下发,已完成条目一律通过历史读取。
### 队列、subscriber 和回收
每个 thread 一个 Vec-based append-only replay queue,使用逻辑 head 偏移清理前缀,不做中间删除。完成 item 的事件在持久化成功后才可进入普通 replay 回收流程;unfinished item 的事件必须保留到 item 完成,不能被普通上限截断。