保留唯一的thread manager作为direct project的状态来源 #384

Merged
lhk229 merged 53 commits from fix/chat-status-lost into master 2026-09-18 02:13:51 +08:00
Member

说明: 在把工作交给段哥前还没有实现direct project聊天页面的迁移, 导致在 #375 里用很复杂的实现又做了一套事件流, 实测还有会话丢失的bug, 我在这里把数据获取的部分迁移到 #367

说明: 在把工作交给段哥前还没有实现direct project聊天页面的迁移, 导致在 #375 里用很复杂的实现又做了一套事件流, 实测还有会话丢失的bug, 我在这里把数据获取的部分迁移到 #367 上
k88936 self-assigned this 2026-09-16 17:01:22 +08:00
k88936 added this to the 陶泥儿gameAgent project 2026-09-16 17:01:22 +08:00
k88936 added 8 commits 2026-09-16 19:11:57 +08:00
- Thread Manager 事件新增 callId,条目身份统一为 callId 优先、其次 itemId
- 工具条目按 callId 归并 started/completed,不再留下永远收不到完成事件的幽灵活跃条目
- 历史锚点仍取 response item id,保证分页锚点能在 project.jsonl 中命中
- 补充跨 id 空间归并的单元测试
- 新增 direct_chat_entry 深模块,把原始 response item 与 app-server item 投影成同一聊天条目
- item.started/item.completed 载荷改为完整条目,前端不再需要按 itemId 取快照
- 原始 function_call 与 function_call_output 按 callId 并成一张卡片,文本与明细统一脱敏截断
- 历史切片新增 entries 与 first_item_id,分页锚点不再依赖是否出现可显示条目
- directThreadChat 把运行态事件与历史切片归并成同一份聊天条目,活动回合只由 turn.started/turn.completed 判定
- item.delta 载荷补 kind,reducer 不再靠 itemId 猜条目类型
- bootstrap 原子替换运行态并保留历史窗口,历史与运行态按 callId ?? itemId 去重合并
- 新增 5 条 reducer 单测
- 主规范新增首屏锚点、生命周期锚点必进 bootstrap、事件自足与不提供单点快照四条契约
- 新增 ADR 记录单一事实源取舍、删除清单与待验证的 id 空间风险
- 新增里程碑与实施计划,CONTEXT.md 补项目对话历史/运行态事件/聊天投影词条
- 目标文档合同与文档索引同步
- 唤醒只由 notify 驱动,bootstrap 后立刻 consume 一次补齐竞态窗口
- 定时 tick 无法自举:判断活动回合本身依赖事件,唤醒丢失时定时器不会启动
- 主规范、ADR 与实施计划同步删除该条款
新增 agent/direct_thread_raw_item.rs:把 Codex 原始 response item 与 app-server item 收敛成同一形状的脱敏原始条目,只做挑字段、脱敏、截断,不再生成卡片的 kind / 标题 / 折叠摘要
删除 agent/direct_chat_entry.rs:工具卡片形状不再由 Rust 生产
历史切片改为返回脱敏原始条目列表,不再在 Rust 侧合并 function_call 与 function_call_output
新增前端 directThreadItemProjection.ts:工具卡片 kind / 标题 / 摘要 / 状态与可见性判定全部在前端完成
directThreadChat reducer 改用同一投影函数处理运行态事件,新增 mergeDirectHistoryItems 走同一份投影与合并规则
DirectThreadHistorySlice 去掉 entries 与 itemTimestamps,改为 items + firstItemId
里程碑与实施计划改定边界:Thread Manager 只搬运脱敏原始条目,不生成卡片形状与可见性
条目身份归一为 call_id ?? id,Thread Manager 与前端不再暴露第二个 id 概念,分页锚点仍取文件里的原始 item id
合并规则收敛为"先到定形、后到补空白",删除逐字段优先级表
主规范补充条目形状三条职责边界,并说明思考正文流式下发不放宽可见范围
ADR 同步:DirectRuntime 自己的进度事件与投影文件保留,前端删除的是 DirectProject 的读路径
失败与中止说明只在运行期显示;历史切片改为从文件尾反向扫描;未知 item 类型透传并加 TODO
修正 DirectProject 订阅语义:bootstrap 事件就是当前要处理的事件
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
09ad0073fe
subscribe 已在同一边界把游标钉在队尾,因此前端直接 reduce bootstrap 事件,不再补一次 consume
主规范、ADR 与实施计划同步删除"竞态窗口"说法,notify 仍是唯一唤醒来源
k88936 added 1 commit 2026-09-16 19:44:45 +08:00
DirectProject 聊天事件改用 ts-rs 导出的 tagged enum 并删掉 turn id
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 5m11s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m51s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m57s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m50s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m2s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m14s
Project CI / Native shell tests (pull_request) Failing after 2m13s
Project CI / AI game creator shell web tests (pull_request) Failing after 46s
Project CI / Frontend tests (pull_request) Failing after 3m32s
2748468d12
- 新增 direct_thread_wire.rs:DirectThreadItem / DirectThreadEvent / 订阅与历史切片全部改成 ts-rs 导出的 tagged enum,取代原大而全的可空结构体
- 删除 direct_thread_raw_item.rs,模块注册与直通引用改到 direct_thread_wire
- 条目身份只看一个 itemId:工具条目的第二个 id 在 Rust 边界归一,不再对外暴露
- 删除 DirectProject 聊天事件里的 turn id:生命周期用无载荷的 turn.started / turn.completed{status} 表示
- append 直接接收 DirectThreadEvent 并返回同一事件,队列内部自算 seq
- 请求事件改为携带 DirectThreadRequestKind,去掉字符串中转
- 思考增量走 ReasoningDelta 通道,与正文增量共用 item.delta
- at 用 #[ts(as = "f64")] 对齐 Tauri JSON 通道的 number
- 用 cargo test export_bindings 重新生成 project-workspace/generated 绑定
k88936 added 2 commits 2026-09-16 20:09:43 +08:00
- directThreadItemProjection.ts 直接消费生成的 DirectThreadItem,工具卡片 kind / 标题 / 摘要 / 状态与可见性判定都在这里
- directThreadChat.ts 聊天条目只留一个 itemId:去掉 callId 概念,去掉 deltaText 缓冲,增量直接追加到运行态条目正文
- 合并规则收成先到者赢、后到只补空:正文只增不减、工具状态允许从 running 升级到终态
- 回合结束把运行态条目并入历史并清空 live,条目既不消失也不重复
- directThreadEvents.ts 改为转发 ts-rs 绑定,只保留历史条目转消息的过渡函数
- ToolCallGroup 与 toolCallGroupPresentation 改用去掉回合身份的 DirectChatToolCard
- generated/index.ts 补上 DirectThread* 绑定导出
- 回退本仓库其它模块仍在改的 DirectCodexUser* 绑定重新生成(避免误改他人进行中的契约)
- 两个 vitest 用例文件按新 API 重写,11 条用例通过
文档先行:把 DirectProject 聊天真相源收敛的文档改到当前实现状态
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 5m13s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m30s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m39s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m43s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m8s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m10s
Project CI / Frontend tests (pull_request) Failing after 3m24s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m7s
Project CI / Native shell tests (pull_request) Successful in 6m8s
ed3369a494
- 主规范把事件模型写成 ts-rs 导出的 tagged enum,说明 seq 不下发、事件不带回合身份,并补上 ts 绑定生成与 f64 时间戳约定
- ADR 补上 tagged enum 绑定、turn id 删除、正文只增不减与回合回收、前端卡片去掉 turnId、未知类型前端丢弃
- 里程碑与实施计划同步:文件改名到 direct_thread_wire.rs,新增删回合身份与生成绑定两步,验证命令加上 export_bindings
- 工具卡片技术方案在文首加修订段:数据来源层作废,表现层契约仍然有效,DirectRuntime 的 tool-calls.jsonl 写入保留
- 对话回合唯一投影里程碑改为 superseded,列出仍然有效与已作废的分界
k88936 added 15 commits 2026-09-17 13:02:06 +08:00
- 新增 DirectProjectHistoryReverseLines:按块从文件尾逐行回读 project.jsonl
- 幂等回扫与"读一屏"共用这一套尾部回读,不再各自从文件头读到尾
- read_direct_project_history_items_slice_at 语义不变仍返回元组,新增第四项为本屏最老的 itemId 作分页锚点
- 分页锚点只按归一身份 itemId 匹配,不再接受第二个 id
- read_direct_project_last_item_id_at 改为尾部回扫读一行,不再读整份历史
- 删除只服务聊天框回读的 read_direct_tool_calls / read_direct_turn_stream 命令与注册
- 保留 list_game_creator_direct_active_turns:它服务首页跨页面"运行中的项目",不是聊天框读路径
- 补历史切片尾部回扫与分页锚点两条用例
- 新增 direct_thread_id_for_project:订阅、回合事件写入与兜底释放共用一个线程身份
- 订阅命令改用同一个身份函数,删掉命令层自己那套路径归一
- 用户消息落盘后立刻作为 item.completed 下发,前端用同一个 itemId 合并乐观气泡与历史条目
- 采集终态事件是否已经下发,缺失时按成功 / 失败补一条 turn.completed,避免前端永远停在运行中
- 取消回合的 Stale 分支补一条 turn.completed(aborted)
- App.tsx 删除 Direct 回合事件订阅、turn-stream/tool-calls 读取、活动回合快照接管与瞬时应答文本
- 新增 game-creator-direct-thread-notify 唤醒的 subscribe/consume 单飞循环,过期时重订一次
- 通知先于订阅回执到达时记一笔欠账,回执到达后补一次 consume,避免回合尾部事件卡在队列里
- 历史切片并入同一个 reducer,聊天条目只由 selectDirectChatEntries 投影
- ProjectSupervisorView 改为渲染用户气泡 / 执行过程折叠区 / 最终回复,运行态只保留一个 turnRunning
- directTurnPresentation 重写为条目分区:连续工具合块、本地运行期说明只作最终提示
- 测试改按订阅事件与历史条目断言,补回执竞态、持久化工具卡片、空对话首轮三条用例
- 主规范:运行态事件改为下发与历史切片同形的脱敏原始条目,不再只给类型与身份
- 主规范与 ADR 写清回执竞态:回执前到达的通知只能记欠账,回执到达后必须补一次 consume
- ADR 与实施计划说明 list_game_creator_direct_active_turns 有意保留,服务首页运行中项目列表
- 里程碑验收标准按自动化证据勾选,运行时验收与已知环境缺口单独标注
- 实施计划补 export_bindings 后需 prettier 生成目录、测试文件更名与竞态风险项
- pitfalls 记录:Node 26 的实验性 globalThis.localStorage 会顶掉 jsdom 的 Storage,导致只声明 jsdom 的用例读到 undefined
- 给出跑法:NODE_OPTIONS=--localstorage-file=… 后 clientApi 与 chatPromptPolish 共 26 项转绿
- 里程碑证据更新为全量结果:125 files / 1676 passed / 17 skipped,tsc exit 0
- App.tsx 不再绑定 chat_with_game_creator_direct_codex 的返回值,删掉 void reply
- 保留说明:回复正文按条目身份从线程事件 / 历史切片进聊天,本地不再补第二条
- harness.ts 记录运行态"最新回合是否在跑",emitDirectThreadEvents 只在生命周期事件上更新
- completeDirectThreadTurn 仅在回合没在跑时补 turn.started,避免与用例先发的事件重叠
- harness.ts 记录未完成条目集合、事件序号与最新生命周期锚点
- subscribe 只回放未完成条目与最新生命周期锚点,已完成条目与增量正文不再补发
- 去掉已下发事件的序号记录,避免序号表无限增长
- harness.ts 的 read_direct_project_history_slice 按生产口径从队尾取一屏、锚点条目不进窗口
- 按本屏最老一条算 firstItemId,收满一屏后再有条目才算 hasMore
- project-development.suite.ts 补一条用例:26 条历史只显示尾部 20 条,点「显示更早的对话」按锚点取回前 6 条后按钮消失
- loadProjectConversation 不再在 read_direct_project_history_slice 的 then 回调里直接写 directThreadChat
- 切片先暂存,等 loadVersion / localProjectPathRef 守卫通过后再与 hasMore、锚点一起落到状态
- 现有 launcher 按项目路径 key 重挂载 App,跨项目污染暂不可复现;本次只是让这条写入与其它状态写入的时序一致
- buildDirectChatTurns 先攒住第一个用户条目之前的过程条目,交给后面那个回合
- 不再渲染没有用户气泡的孤儿回合;整份历史都没有用户条目时仍保留一个回合
- directTurnPresentation 单测补一条分页切片从半截回合开头的情形
- DirectThreadChatState 去掉 subscriptionId 与 lastCompletedItemId 两个只写不读的字段
- resolveDirectThreadBootstrap 只把 bootstrap 事件 reduce 进状态,不再顺带写入这两个字段
- directThreadChat 单测改成断言聊天状态里不再出现这两个字段(bootstrap 载荷本身不变)
- handleCancelDirectCodexTurn 的守卫改用 supervisorChatBusy,与「终止」按钮的可见条件对齐
- 刚提交、turn.started 还没到达的窗口里点击终止不再回"当前没有正在运行的回合"
- chat-composer 用例补一条:生命周期事件缺席时也能发出 cancel_direct_codex_turn
- DirectProjectHistoryReverseLines 增记 saw_delimiter,第一段按"文件是否以换行结尾"判定,之后的行按已切掉的分隔符判定
- 尾部残行(上一行完整、这一行没有换行)解析失败时改为跳过继续回扫,不再报错、也不再就地结束丢掉更早的完整历史
- 换行结尾的坏行(含文件第一段)与顺序读取保持一致地失败关闭
- 补两条回扫用例:尾部残行不丢更早条目、坏掉的第一段必须失败关闭(都按旧实现变异验证过)
分页锚点改用文件里的原始 item id
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 6m22s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 6m34s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m49s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m49s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m19s
Project CI / Frontend tests (pull_request) Failing after 6m15s
Project CI / AI game creator shell web tests (pull_request) Failing after 5m47s
Project CI / Native shell tests (pull_request) Successful in 12m22s
da2ad83c3b
- 新增 direct_project_history_anchor_id:优先取 project.jsonl 原始 id,缺 id 时退回归一身份兜底
- read_direct_project_history_items_slice_at 的锚点匹配与 first_item_id 都改用原始 id
- 工具条目的调用与输出共用 call_id,按归一身份当锚点会先跳过 output 再把上一屏的 function_call 带回来
- 原分页用例更名 history_window_paginates_upwards_by_item_id_anchor
- 新增 pagination_anchor_uses_raw_item_id_for_tool_call_pairs,旧实现下会返回 call-1 而非 fc-1(已做变异验证)
Author
Member

DirectProject 聊天迁移评审项处理结果

分支:fix/chat-status-lost(仅本地提交,未 push)
设计依据:docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md
标记:- [x] = 已自动修复并单独提交;- [ ] = 留给人工决定(第 12 项:错误语义/前端可见行为决策)
编号:按本文件顺序;每条给出「现状 / 问题 / 修法」

  • 1. apps/ai-game-creator-shell/tests/appSurface/harness.ts:970-976 · [test · medium] 历史切片桩无视分页参数
    原文:read_direct_project_history_slice always returns the full in-memory history with hasMore: false and firstItemId: null, ignoring limit and beforeItemId.
    现状:测试骨架的桩直接返回整份 directThreadHistoryItemshasMorefalsefirstItemIdnull
    问题:生产的 Rust 读取器会钳制 limit、排除锚点条目并返回 hasMore/firstItemId,App.tsx 靠它驱动「显示更早的对话」。桩不分页 → 首屏游标、翻页、hasMore 三条路径在测试里永远不会被跑到,回归无人兜住。
    修法:桩按生产口径改成有界窗口——按 limit 截取、按 beforeItemId 定位锚点(锚点自身不进窗口)、hasMore = start > 0firstItemId 取窗口最老一条的 itemId;并补一条「26 条历史 → 首屏 20 条 → 点『显示更早的对话』翻出前 6 条」的用例。
    提交:64bd00698

  • 2. apps/ai-game-creator-shell/tests/appSurface/harness.ts:1306 · [test · low] 测试骨架重复下发 turn.started
    原文:completeDirectThreadTurn unconditionally emits turn.started, even when the test already emitted one earlier in the same turn.
    现状:completeDirectThreadTurn 无条件在事件序列开头补一条 turn.started;多个工具卡片用例会先自己 emitDirectThreadEvents({ type: 'turn.started' }),于是同一回合出现两条。
    问题:生产只有一个生命周期锚点,重复事件虽然被 reducer 幂等吃掉,但与真实语义偏离,会掩盖生命周期处理上的回归。
    修法:只有 directThreadTurnRunningfalse 时才补 turn.started
    提交:1b3a3b003

  • 3. apps/ai-game-creator-shell/tests/appSurface/harness.ts:951-961 · [test · low] subscribe bootstrap 口径与生产不符
    原文:subscribe_direct_project_thread returns every pending event as the bootstrap, but production's subscribe places the cursor at the queue tail and only replays unfinished-item events plus the latest lifecycle anchor.
    现状:桩把队列里所有未消费事件都当作 bootstrap 回放。
    问题:生产(is_bootstrap_event)游标落队尾,只回放「未完成条目的快照 + 最新一条生命周期锚点」,已完成的条目与瞬时增量不补发。桩全量回放会让「先发事件、后订阅」的用例拿到生产拿不到的内容,订阅/重订阅语义被写成错的契约。
    修法:桩按同一口径过滤(未完成条目 + 生命周期锚点)。
    提交:6a4cb2bc1

  • 4. apps/ai-game-creator-shell/src/App.tsx:3504-3508 · [bug · high] 首屏历史切片在 staleness 守卫之前就写状态
    原文:The initial history slice is merged into directThreadChat inside the invoke(...).then(...) callback before the staleness guards run.
    现状:read_direct_project_history_slice.then() 里直接 setDirectThreadChat(mergeDirectThreadHistorySlice(state, slice)),而 projectSupervisorHistoryLoadVersionRef / localProjectPathRef 守卫在该回调之后很久才执行;DirectThreadChatState.history 本身不带项目键。
    问题:守卫返回时状态已经被改过;若 A 项目的切片迟到、此时已切到 B,就会把 A 的历史写进 B 的聊天。补充核对:当前聊天状态由 App 持有,而 WorkspaceLauncherShell`${projectPath}:${agentRuntimeMode}` 为 key 渲染 ProjectSupervisor={App},换项目会整体重挂载,同时订阅 effect([directCodexProjectRuntime, localProject?.projectPath])会先把状态清空——所以这条污染路径在当前 launcher 结构下不可复现,评审描述的现象目前不成立。即便如此,「先写状态、后判 staleness」与同一函数里其它状态写入的时序不一致,一旦去掉 key 或在 App 内直接切项目就会立刻变成真 bug。
    修法:切片先暂存到局部变量,守卫通过后再与 hasMore、锚点一起并入聊天 reducer。(本次未保留跨项目污染的回归用例,因为当前不可复现。)
    提交:e17af3008

  • 5. apps/ai-game-creator-shell/src/features/project-workspace/directTurnPresentation.ts:157-161 · [bug · medium] 分页切片开头渲染出孤儿回合
    原文:a paginated older slice can start with assistant/tool items whose user message lives in an even older slice ... rendering an orphaned turn with process/final blocks but no user bubble.
    现状:历史切片是「按可显示条目数从文件尾切出来的裸窗口」,不按回合边界对齐;旧实现把切片开头那些没有归属的条目塞进 history:${turns.length} 兜底回合。
    问题:翻页后会渲染出一个没有用户气泡、只有过程/正文的回合。
    修法:不改成 Rust 按回合切(会把 UI 可见性规则塞回后端),改为前端把前导条目先缓存,并入后面第一个用户条目开的回合;只有整份历史都没有用户条目时才保留兜底回合。已补单测。
    提交:098693636

  • 6. apps/ai-game-creator-shell/src/features/project-workspace/directThreadChat.ts:34-37 · [maintainability · low] reducer 里的死状态
    原文:subscriptionId and lastCompletedItemId are written here (and in resolveDirectThreadBootstrap) but never read anywhere.
    现状:确认属实——App.tsx 用自己的 effect 局部 subscriptionId,分页靠 directHistoryOldestItemIdRef / loadedDirectHistoryFirstItemId
    问题:只写不读的字段会让人误以为订阅身份与分页锚点在 reducer 里,掩盖真实契约。
    修法:从 DirectThreadChatStateresolveDirectThreadBootstrap 中删除(resolveDirectThreadBootstrap 收窄为只 reduce 事件),同步修改 directThreadChat.test.ts
    提交:1b40f030e

  • 7. apps/ai-game-creator-shell/src/App.tsx:6556-6557 · [maintainability · low] void reply; 死语句
    原文:void reply; discards the final response returned by chat_with_game_creator_direct_codex ... A void reply; statement also reads as dead code.
    现状:const reply = await ...; void reply;
    问题:正文按单一事实源只从线程事件/历史切片进聊天,这个绑定+丢弃既无作用又读起来像「兜底被删了」。
    修法:不绑定返回值,直接 await,并保留说明「正文只从线程事件/历史切片进聊天」的注释(不再有 void)。评审提到的「最后一条 notify 丢失时没有本地兜底」属于设计取舍,未改行为。
    提交:ff8e517c8

  • 8. apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs:1078-1081 · [bug · high] 思考正文在生产路径没有下发 → 已按 ReasoningDelta 修复
    原文:This ReasoningDelta branch only runs inside direct_codex_notification_event, which is invoked solely from unit tests. The production stdout reader still maps the two reasoning delta methods to CodexTurnEvent::Activity("preparing").
    现状(修复前):direct_codex_notification_event 的调用方只有它自己和 3 处单测(mod.rs:4790/4801/4812),生产 stdout 读取器内联了自己的一份分类逻辑,其中 safe_activity == "preparing" 一律降级成 CodexTurnEvent::Activity("preparing"),正文被丢;且活动节流(should_emit_direct_codex_activity,preparing 间隔 1200ms)会把逐段思考正文整段吃掉——这才是"生产从不产生 ReasoningDelta"的直接原因。
    问题:ADR 定的「思考正文以 item.delta{kind:"reasoning"} 流式下发」在生产不生效;单测 direct_preparing_notifications_emit_thinking_activity_without_raw_text 断言的是旧行为,在本分支第一个提交之前就已经是红的(与本次修复无关)。
    修法(已做,提交 e64021924):

    • 抽出 direct_codex_reasoning_delta_event,两条 reasoning 增量通知只在这一处分类;
    • 生产读取器 read_game_creator_codex_app_server_stdout 改用它,并跳过 preparing 活动的降级与活动节流;
    • 旧分类函数 direct_codex_notification_event 同步改用它,避免两份实现再次分叉;
    • 单测换成 direct_reasoning_deltas_stream_text_while_plan_and_command_output_stay_activity(reasoning → ReasoningDelta;plan / commandExecution 仍只降级成活动类别);
    • 新增 E2E codex_app_server_streams_reasoning_deltas_without_activity_fallback:fixture 不发 turn/started,不存在节流退路,关掉读取器路由即失败(已做变异验证)。
    • 遗留(非必须):direct_codex_notification_event 仍只被单测调用,生产读取器里活动/正文/请求三类分支还是内联的;把读取器整体收敛到这一个分类函数是后续可选项。
  • 9. apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs:622-627 · [bug · medium] 分页锚点不唯一
    原文:direct_thread_item_identity returns call_id for both a tool call and its function_call_output (they share the same call_id), so this pagination anchor is not unique.
    现状:锚点用归一身份(工具条目归一成 call_id),而 function_callfunction_call_output 共用同一个 call_id
    问题:页边界落在调用条目时,回扫先撞上更新的 output 并把它当作锚点跳过,下一屏再把上一屏已展示的 function_call 带回来,同一张卡片跨屏重复。
    修法:新增 direct_project_history_anchor_id,锚点匹配与 first_item_id 都取 project.jsonl 里的原始 id(缺 id 才退回归一身份兜底);补用例 pagination_anchor_uses_raw_item_id_for_tool_call_pairs(旧实现会返回 call-1 而非 fc-1,已做变异验证)。
    提交:da2ad83c3

  • 10. apps/ai-game-creator-shell/src/App.tsx:12047 · [bug · medium] 终止按钮判据过早
    原文:The cancel guard now relies solely on directTurnRunning, which only becomes true after the reducer consumes a turn.started thread event ... Use the combined busy signal (e.g. supervisorChatBusy).
    现状:handleCancelDirectCodexTurn 的 guard 之前只认 directTurnRunning
    问题:turn.started 是异步事件,刚提交(invoke 已在跑、事件还没到)或订阅静默失败时,按钮可见但点击只提示「当前没有正在运行的回合,无法终止。」,拒绝取消。
    修法:guard 改用 supervisorChatBusy(本地 invoke 忙 或 订阅说还有回合没结束),与按钮可见条件一致;补用例「生命周期事件尚未到达时也能终止」(去掉修复即红,已做变异验证)。
    提交:975a2e577

  • 11. apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs:153-160 · [bug · medium] terminated 由分支位置推断
    原文:terminated is derived from which branch emitted the line, not from whether the line actually has a trailing newline ... Track whether the line was actually delimited by a trailing newline.
    现状:旧实现按「哪条分支产出这一行」给 terminated:文件尾不含 \nrposition 分支仍返回 trueposition == 0 分支恒为 false
    问题:① 崩溃截断的尾行被当成硬解析错误,调用方 Err(_) if !line.terminated => break 的容忍形同虚设;② 首行即使后面有换行也被标成 false,损坏首行被静默跳过,has_more/分页随之出错。
    修法:结构体加 saw_delimiterterminated 按「是否真的被换行分隔」判定;截断尾行改为 continue 跳过并继续回扫;补 truncated_history_tail_is_skipped_without_losing_earlier_itemscorrupt_first_line_fails_closed_like_any_newline_terminated_line 两条用例(均做变异验证)。
    提交:ad19c9475

  • 12. apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_identity.rs:28-31 · [bug · low] 身份归一失败时静默降级(留人工)
    现状:direct_thread_id_for_project(root)direct_codex_canonical_project_identity(root) 返回的 canonical 路径当成线程 id;该函数一旦返回 Err,就退回「调用方原样传进来的字符串」(root.to_string_lossy())。

    为什么它要读 manifest:线程 id 本身只用得到 canonical 路径,但这条路径顺路复用了「项目权威身份」函数,而后者在 canonicalize 之后还要 ① 读 .agent/manifest.json(文件不存在、不是普通文件、JSON 解析失败都算失败)② 校验 manifest.projectId 非空且不超过 256 字符 ③ 用 path+projectId 算摘要(摘要给连接池 / session 身份用)。所以「manifest 读不到」= 该函数 Err = 线程 id 退回原始字符串。换句话说 manifest 只是被顺路要求可读,它并不是线程 id 的组成部分。

    「订阅绑定的线程 id 与回合事件写入的线程 id 不同」是什么:Thread Manager 的线程表就是 HashMap<String, ThreadState>subscribe(thread_id)entry().or_default() 凭空建一条(可能永远没人写事件的)线程并把订阅者挂上去,append(thread_id, event) 只投递给同一个 key 的订阅者。这个 key 就是线程 id。三处调用点:

    • 订阅侧:commands.rs:5320 subscribe_direct_project_threaddirect_thread_id_for_project(projectPath)
    • 回合事件写入侧:mod.rs:2965(turn/start 之后)→ direct_thread_id_for_project(history_root)
    • 兜底释放侧:mod.rs:3597(Stale 分支补 turn.completed)→ 同一个函数。
      两侧算出来的字符串不同,事件就写进 C 那条线程,而订阅者挂在 P 那条线程:订阅永远为空,subscribe 也不会报错(or_default 凭空建线程),前端 effect 里又是静默 catch,界面表现就是「回合在跑、运行态一条事件都没有」。

    什么时候会真的不同(两个条件必须同时满足):

    1. 调用方传进来的字符串 P 与它的 canonical 形式 C 不同:符号链接目录、路径里有 ..、尾随分隔符、macOS /tmp/private/tmp、Windows 短名 / UNC / \?\ 前缀等;
    2. manifest 在订阅那一刻读失败、在跑回合时读成功:项目刚创建还没落盘 manifest、projectId 还是空串、manifest 正被应用的重写/回收流程替换(recoverRejectedManifestSnapshot 那条链路)、读到半截 JSON。
      如果 manifest 一直读不到,反而不会命中:回合入口 direct_game_creator_codex_chat_at_with_optional_observermod.rs:4349)是 ? 直接抛错,用户根本发不出消息,看到的是报错而不是空订阅。所以坏窗口是「瞬时不一致 × 非 canonical 路径」,两个条件都不满足时(例如用户从文件选择器拿到的就是真实路径,P == C)即使 manifest 抖动也不会绑错。

    建议(留人工,二选一):

    1. 最小改动:让线程 id 只依赖 canonicalize,不再依赖 manifest——改用 resolve_direct_codex_project_authority(root)(只做「绝对路径 / 存在 / 是目录 / canonicalize」)取 canonical 路径,只有 canonicalize 本身失败才退回原始字符串。不改任何对外错误语义,直接消掉条件 2。
    2. 统一口径:把 direct_thread_id_for_project 改成返回 Result,订阅边界与回合入口一样直接报错(前端会显示订阅失败,而不是静默空订阅);兜底释放那条路径需要一条不失败的降级分支。
# DirectProject 聊天迁移评审项处理结果 分支:`fix/chat-status-lost`(仅本地提交,未 push) 设计依据:`docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md` 标记:`- [x]` = 已自动修复并单独提交;`- [ ]` = 留给人工决定(第 12 项:错误语义/前端可见行为决策) 编号:按本文件顺序;每条给出「现状 / 问题 / 修法」 - [x] 1. `apps/ai-game-creator-shell/tests/appSurface/harness.ts:970-976` · [test · medium] 历史切片桩无视分页参数 原文:`read_direct_project_history_slice` always returns the full in-memory history with `hasMore: false` and `firstItemId: null`, ignoring `limit` and `beforeItemId`. 现状:测试骨架的桩直接返回整份 `directThreadHistoryItems`,`hasMore` 恒 `false`、`firstItemId` 恒 `null`。 问题:生产的 Rust 读取器会钳制 limit、排除锚点条目并返回 `hasMore`/`firstItemId`,App.tsx 靠它驱动「显示更早的对话」。桩不分页 → 首屏游标、翻页、`hasMore` 三条路径在测试里永远不会被跑到,回归无人兜住。 修法:桩按生产口径改成有界窗口——按 `limit` 截取、按 `beforeItemId` 定位锚点(锚点自身不进窗口)、`hasMore = start > 0`、`firstItemId` 取窗口最老一条的 `itemId`;并补一条「26 条历史 → 首屏 20 条 → 点『显示更早的对话』翻出前 6 条」的用例。 提交:`64bd00698` - [x] 2. `apps/ai-game-creator-shell/tests/appSurface/harness.ts:1306` · [test · low] 测试骨架重复下发 `turn.started` 原文:`completeDirectThreadTurn` unconditionally emits `turn.started`, even when the test already emitted one earlier in the same turn. 现状:`completeDirectThreadTurn` 无条件在事件序列开头补一条 `turn.started`;多个工具卡片用例会先自己 `emitDirectThreadEvents({ type: 'turn.started' })`,于是同一回合出现两条。 问题:生产只有一个生命周期锚点,重复事件虽然被 reducer 幂等吃掉,但与真实语义偏离,会掩盖生命周期处理上的回归。 修法:只有 `directThreadTurnRunning` 为 `false` 时才补 `turn.started`。 提交:`1b3a3b003` - [x] 3. `apps/ai-game-creator-shell/tests/appSurface/harness.ts:951-961` · [test · low] `subscribe` bootstrap 口径与生产不符 原文:`subscribe_direct_project_thread` returns every pending event as the bootstrap, but production's `subscribe` places the cursor at the queue tail and only replays unfinished-item events plus the latest lifecycle anchor. 现状:桩把队列里所有未消费事件都当作 bootstrap 回放。 问题:生产(`is_bootstrap_event`)游标落队尾,只回放「未完成条目的快照 + 最新一条生命周期锚点」,已完成的条目与瞬时增量不补发。桩全量回放会让「先发事件、后订阅」的用例拿到生产拿不到的内容,订阅/重订阅语义被写成错的契约。 修法:桩按同一口径过滤(未完成条目 + 生命周期锚点)。 提交:`6a4cb2bc1` - [x] 4. `apps/ai-game-creator-shell/src/App.tsx:3504-3508` · [bug · high] 首屏历史切片在 staleness 守卫之前就写状态 原文:The initial history slice is merged into `directThreadChat` inside the `invoke(...).then(...)` callback before the staleness guards run. 现状:`read_direct_project_history_slice` 的 `.then()` 里直接 `setDirectThreadChat(mergeDirectThreadHistorySlice(state, slice))`,而 `projectSupervisorHistoryLoadVersionRef` / `localProjectPathRef` 守卫在该回调之后很久才执行;`DirectThreadChatState.history` 本身不带项目键。 问题:守卫返回时状态已经被改过;若 A 项目的切片迟到、此时已切到 B,就会把 A 的历史写进 B 的聊天。补充核对:当前聊天状态由 `App` 持有,而 `WorkspaceLauncherShell` 以 `` `${projectPath}:${agentRuntimeMode}` `` 为 key 渲染 `ProjectSupervisor={App}`,换项目会整体重挂载,同时订阅 effect(`[directCodexProjectRuntime, localProject?.projectPath]`)会先把状态清空——所以这条污染路径在当前 launcher 结构下不可复现,评审描述的现象目前不成立。即便如此,「先写状态、后判 staleness」与同一函数里其它状态写入的时序不一致,一旦去掉 key 或在 App 内直接切项目就会立刻变成真 bug。 修法:切片先暂存到局部变量,守卫通过后再与 `hasMore`、锚点一起并入聊天 reducer。(本次未保留跨项目污染的回归用例,因为当前不可复现。) 提交:`e17af3008` - [x] 5. `apps/ai-game-creator-shell/src/features/project-workspace/directTurnPresentation.ts:157-161` · [bug · medium] 分页切片开头渲染出孤儿回合 原文:a paginated older slice can start with assistant/tool items whose user message lives in an even older slice ... rendering an orphaned turn with process/final blocks but no user bubble. 现状:历史切片是「按可显示条目数从文件尾切出来的裸窗口」,不按回合边界对齐;旧实现把切片开头那些没有归属的条目塞进 `history:${turns.length}` 兜底回合。 问题:翻页后会渲染出一个没有用户气泡、只有过程/正文的回合。 修法:不改成 Rust 按回合切(会把 UI 可见性规则塞回后端),改为前端把前导条目先缓存,并入后面第一个用户条目开的回合;只有整份历史都没有用户条目时才保留兜底回合。已补单测。 提交:`098693636` - [x] 6. `apps/ai-game-creator-shell/src/features/project-workspace/directThreadChat.ts:34-37` · [maintainability · low] reducer 里的死状态 原文:`subscriptionId` and `lastCompletedItemId` are written here (and in `resolveDirectThreadBootstrap`) but never read anywhere. 现状:确认属实——App.tsx 用自己的 effect 局部 `subscriptionId`,分页靠 `directHistoryOldestItemIdRef` / `loadedDirectHistoryFirstItemId`。 问题:只写不读的字段会让人误以为订阅身份与分页锚点在 reducer 里,掩盖真实契约。 修法:从 `DirectThreadChatState` 与 `resolveDirectThreadBootstrap` 中删除(`resolveDirectThreadBootstrap` 收窄为只 reduce 事件),同步修改 `directThreadChat.test.ts`。 提交:`1b40f030e` - [x] 7. `apps/ai-game-creator-shell/src/App.tsx:6556-6557` · [maintainability · low] `void reply;` 死语句 原文:`void reply;` discards the final response returned by `chat_with_game_creator_direct_codex` ... A `void reply;` statement also reads as dead code. 现状:`const reply = await ...; void reply;`。 问题:正文按单一事实源只从线程事件/历史切片进聊天,这个绑定+丢弃既无作用又读起来像「兜底被删了」。 修法:不绑定返回值,直接 `await`,并保留说明「正文只从线程事件/历史切片进聊天」的注释(不再有 `void`)。评审提到的「最后一条 notify 丢失时没有本地兜底」属于设计取舍,未改行为。 提交:`ff8e517c8` - [x] 8. `apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs:1078-1081` · [bug · high] 思考正文在生产路径没有下发 → 已按 `ReasoningDelta` 修复 原文:This `ReasoningDelta` branch only runs inside `direct_codex_notification_event`, which is invoked solely from unit tests. The production stdout reader still maps the two reasoning delta methods to `CodexTurnEvent::Activity("preparing")`. 现状(修复前):`direct_codex_notification_event` 的调用方只有它自己和 3 处单测(`mod.rs:4790/4801/4812`),生产 stdout 读取器内联了自己的一份分类逻辑,其中 `safe_activity == "preparing"` 一律降级成 `CodexTurnEvent::Activity("preparing")`,正文被丢;且活动节流(`should_emit_direct_codex_activity`,preparing 间隔 1200ms)会把逐段思考正文整段吃掉——这才是"生产从不产生 `ReasoningDelta`"的直接原因。 问题:ADR 定的「思考正文以 `item.delta{kind:"reasoning"}` 流式下发」在生产不生效;单测 `direct_preparing_notifications_emit_thinking_activity_without_raw_text` 断言的是旧行为,在本分支第一个提交之前就已经是红的(与本次修复无关)。 修法(已做,提交 `e64021924`): - 抽出 `direct_codex_reasoning_delta_event`,两条 reasoning 增量通知只在这一处分类; - 生产读取器 `read_game_creator_codex_app_server_stdout` 改用它,并跳过 `preparing` 活动的降级与活动节流; - 旧分类函数 `direct_codex_notification_event` 同步改用它,避免两份实现再次分叉; - 单测换成 `direct_reasoning_deltas_stream_text_while_plan_and_command_output_stay_activity`(reasoning → `ReasoningDelta`;plan / commandExecution 仍只降级成活动类别); - 新增 E2E `codex_app_server_streams_reasoning_deltas_without_activity_fallback`:fixture 不发 `turn/started`,不存在节流退路,关掉读取器路由即失败(已做变异验证)。 - 遗留(非必须):`direct_codex_notification_event` 仍只被单测调用,生产读取器里活动/正文/请求三类分支还是内联的;把读取器整体收敛到这一个分类函数是后续可选项。 - [x] 9. `apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs:622-627` · [bug · medium] 分页锚点不唯一 原文:`direct_thread_item_identity` returns `call_id` for both a tool call and its `function_call_output` (they share the same `call_id`), so this pagination anchor is not unique. 现状:锚点用归一身份(工具条目归一成 `call_id`),而 `function_call` 与 `function_call_output` 共用同一个 `call_id`。 问题:页边界落在调用条目时,回扫先撞上更新的 output 并把它当作锚点跳过,下一屏再把上一屏已展示的 `function_call` 带回来,同一张卡片跨屏重复。 修法:新增 `direct_project_history_anchor_id`,锚点匹配与 `first_item_id` 都取 `project.jsonl` 里的原始 `id`(缺 `id` 才退回归一身份兜底);补用例 `pagination_anchor_uses_raw_item_id_for_tool_call_pairs`(旧实现会返回 `call-1` 而非 `fc-1`,已做变异验证)。 提交:`da2ad83c3` - [x] 10. `apps/ai-game-creator-shell/src/App.tsx:12047` · [bug · medium] 终止按钮判据过早 原文:The cancel guard now relies solely on `directTurnRunning`, which only becomes true after the reducer consumes a `turn.started` thread event ... Use the combined busy signal (e.g. `supervisorChatBusy`). 现状:`handleCancelDirectCodexTurn` 的 guard 之前只认 `directTurnRunning`。 问题:`turn.started` 是异步事件,刚提交(invoke 已在跑、事件还没到)或订阅静默失败时,按钮可见但点击只提示「当前没有正在运行的回合,无法终止。」,拒绝取消。 修法:guard 改用 `supervisorChatBusy`(本地 invoke 忙 或 订阅说还有回合没结束),与按钮可见条件一致;补用例「生命周期事件尚未到达时也能终止」(去掉修复即红,已做变异验证)。 提交:`975a2e577` - [x] 11. `apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs:153-160` · [bug · medium] `terminated` 由分支位置推断 原文:`terminated` is derived from which branch emitted the line, not from whether the line actually has a trailing newline ... Track whether the line was actually delimited by a trailing newline. 现状:旧实现按「哪条分支产出这一行」给 `terminated`:文件尾不含 `\n` 时 `rposition` 分支仍返回 `true`;`position == 0` 分支恒为 `false`。 问题:① 崩溃截断的尾行被当成硬解析错误,调用方 `Err(_) if !line.terminated => break` 的容忍形同虚设;② 首行即使后面有换行也被标成 `false`,损坏首行被静默跳过,`has_more`/分页随之出错。 修法:结构体加 `saw_delimiter`,`terminated` 按「是否真的被换行分隔」判定;截断尾行改为 `continue` 跳过并继续回扫;补 `truncated_history_tail_is_skipped_without_losing_earlier_items`、`corrupt_first_line_fails_closed_like_any_newline_terminated_line` 两条用例(均做变异验证)。 提交:`ad19c9475` - [x] 12. `apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/direct_project_identity.rs:28-31` · [bug · low] 身份归一失败时静默降级(留人工) 现状:`direct_thread_id_for_project(root)` 把 `direct_codex_canonical_project_identity(root)` 返回的 canonical 路径当成线程 id;该函数一旦返回 `Err`,就退回「调用方原样传进来的字符串」(`root.to_string_lossy()`)。 为什么它要读 manifest:线程 id 本身只用得到 canonical 路径,但这条路径顺路复用了「项目权威身份」函数,而后者在 canonicalize 之后还要 ① 读 `.agent/manifest.json`(文件不存在、不是普通文件、JSON 解析失败都算失败)② 校验 `manifest.projectId` 非空且不超过 256 字符 ③ 用 path+projectId 算摘要(摘要给连接池 / session 身份用)。所以「manifest 读不到」= 该函数 `Err` = 线程 id 退回原始字符串。换句话说 manifest 只是被顺路要求可读,它并不是线程 id 的组成部分。 「订阅绑定的线程 id 与回合事件写入的线程 id 不同」是什么:Thread Manager 的线程表就是 `HashMap<String, ThreadState>`,`subscribe(thread_id)` 用 `entry().or_default()` 凭空建一条(可能永远没人写事件的)线程并把订阅者挂上去,`append(thread_id, event)` 只投递给**同一个 key** 的订阅者。这个 key 就是线程 id。三处调用点: - 订阅侧:`commands.rs:5320` `subscribe_direct_project_thread` → `direct_thread_id_for_project(projectPath)`; - 回合事件写入侧:`mod.rs:2965`(turn/start 之后)→ `direct_thread_id_for_project(history_root)`; - 兜底释放侧:`mod.rs:3597`(Stale 分支补 `turn.completed`)→ 同一个函数。 两侧算出来的字符串不同,事件就写进 C 那条线程,而订阅者挂在 P 那条线程:订阅永远为空,`subscribe` 也不会报错(`or_default` 凭空建线程),前端 effect 里又是静默 `catch`,界面表现就是「回合在跑、运行态一条事件都没有」。 什么时候会真的不同(两个条件必须同时满足): 1. 调用方传进来的字符串 P 与它的 canonical 形式 C 不同:符号链接目录、路径里有 `..`、尾随分隔符、macOS `/tmp` → `/private/tmp`、Windows 短名 / UNC / `\?\` 前缀等; 2. manifest 在**订阅那一刻**读失败、在**跑回合时**读成功:项目刚创建还没落盘 manifest、`projectId` 还是空串、manifest 正被应用的重写/回收流程替换(`recoverRejectedManifestSnapshot` 那条链路)、读到半截 JSON。 如果 manifest 一直读不到,反而不会命中:回合入口 `direct_game_creator_codex_chat_at_with_optional_observer`(`mod.rs:4349`)是 `?` 直接抛错,用户根本发不出消息,看到的是报错而不是空订阅。所以坏窗口是「瞬时不一致 × 非 canonical 路径」,两个条件都不满足时(例如用户从文件选择器拿到的就是真实路径,P == C)即使 manifest 抖动也不会绑错。 建议(留人工,二选一): 1. 最小改动:让线程 id 只依赖 canonicalize,不再依赖 manifest——改用 `resolve_direct_codex_project_authority(root)`(只做「绝对路径 / 存在 / 是目录 / canonicalize」)取 canonical 路径,只有 canonicalize 本身失败才退回原始字符串。不改任何对外错误语义,直接消掉条件 2。 2. 统一口径:把 `direct_thread_id_for_project` 改成返回 `Result`,订阅边界与回合入口一样直接报错(前端会显示订阅失败,而不是静默空订阅);兜底释放那条路径需要一条不失败的降级分支。
k88936 added 4 commits 2026-09-17 14:04:32 +08:00
- 新增 direct_codex_reasoning_delta_event,两条 reasoning 增量通知只在这一处分类
- 生产 stdout 读取器改用该函数,并跳过「preparing 活动」的降级与活动节流(活动节流会按类别吃掉逐段正文)
- 旧分类函数 direct_codex_notification_event 同步改用它,避免两份实现再次分叉
- 单测 direct_reasoning_deltas_stream_text_while_plan_and_command_output_stay_activity 取代原先断言 Activity 的旧用例
- 新增 E2E codex_app_server_streams_reasoning_deltas_without_activity_fallback:把读取器的路由关掉即转红(已做变异验证)
- direct_thread_id_for_project 改用 resolve_direct_codex_project_authority(存在/是目录/canonicalize)
- 不再读 .agent/manifest.json:那次读取是连接池权威身份的要求,线程 id 只是路径 key
- 否则 manifest 在订阅时读失败、跑回合时读成功,会让符号链接等非 canonical 调用方路径算出两个线程 id,订阅绑到空线程
- 新增 direct_project_thread_id_ignores_manifest_readability,旧实现下会退回调用方原始字符串(已做变异验证)
- direct_codex_notification_event 收编 resolution / request / item / rawItem / Terminal 全部分支,并接收 turnId
- 读取器删掉自己那份内联分类,只保留「必须有 turnId 才处理」与活动/正文节流
- agentMessage 缺 itemId 的告警与 direct-missing-item:{turnId} 兜底身份原样保留,避免行为变化
- 新增 direct_notification_classification_covers_the_reader_branches 钉住各分支;E2E 假 app-server 用例继续覆盖读取器
重新生成 ts-rs 前端绑定并让前端按 null 语义构造运行时区段
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 6m35s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m31s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m15s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m47s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m15s
Project CI / Repository checks (pull_request) Failing after 19s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m15s
Project CI / Frontend tests (pull_request) Failing after 5m44s
Project CI / AI game creator shell web tests (pull_request) Successful in 5m49s
Project CI / Native shell tests (pull_request) Successful in 10m42s
0caf998227
- 跑 export_bindings 后 prettier 格式化:DirectCodexUser* 5 个文件内容更新,新增 DirectCodexUserMessageEnvelope.ts 与 ui-editor 的 BindingChange/BindingDTO
- DirectCodexUserRuntimeRegionPart 的可选字段由 `?:` 变成 `T | null`(Rust Option 会显式序列化成 null,不是省略键)
- chatReferenceToContentPart 改用 `?? null`,消息条目的 satisfies 目标改为 DirectCodexUserItem(type:'message' 归到信封类型)
- ResourceReferenceInput 不再复制一份区段字面量,改为复用 chatReferenceToContentPart
- 验证:tsc --noEmit 通过;resourceReferences 等 5 个用例集 81 passed;appSurface 452 passed / 17 skipped(连跑两次)
k88936 added 9 commits 2026-09-17 16:55:00 +08:00
This reverts commit 036395c6c9.
This reverts commit 1b010fb1f8.
- 运行态条目投影新增回显过滤 direct_thread_visible_item,item/started 与 rawResponseItem/completed 两条路径共用
- is_direct_project_codex_user_item 提升为 pub(crate),落盘侧与运行态侧共用同一判据
- 新增 E2E direct_project_turn_does_not_forward_codex_user_echo_as_chat_items:修复前事件流会多出两条同文本用户条目(已做变异验证)
- 同步 ADR 与 shared-memory decision-log:回显过滤必须同时覆盖落盘与运行态两侧
把「可显示」判据明确为合并后聊天投影出现新回合,并写清每操作 5 页上限与锚点不前进即停
里程碑把原第 56 条按后端锚点回扫与前端连拉拆成两条独立验收项,前端那条先留待补证据
实施计划第 6 步同步口径,并把新分页单测加入验证命令
决策记录新增 2026-09-17 条目,记下落点、上限粒度、失败与竞态边界
directHistoryPaging 按「合并后聊天投影回合数增加」判断可见反馈,出现新回合或 hasMore=false 即停
连拉上限 DIRECT_HISTORY_MAX_PAGES_PER_ACTION 放进 app/constants,每个操作最多 5 次请求
锚点未推进(items 为空 / firstItemId 为 null 或原地打转)时立即停手并收口 hasMore
某页读取失败不丢已取到的页,返回 error 交给调用方报错
directThreadChat 抽出 projectDirectHistoryItems,让分页判据与视图共用同一份投影
首屏与「显示更早」共用 readDirectHistoryPages,不再各自单发一页
首屏基线取空并在守卫后一次性并入聊天 reducer,失败仍按原有语义抛出
「显示更早」按当前视图条目判断新回合,取到多页一次性并入并推进锚点
连拉中途失败保留已取到的页,报错统一走同一个出口
用 51 条历史复现用户报的现象:20 条工具条目整屏落进同一个折叠的执行过程
断言一次点击连拉两页拿到新用户气泡,并按 process-31 / process-11 两次锚点推进
断言连拉插入旧内容后视口仍停在原处,不被拉回列表底部
里程碑前端连拉那条转成已验收,证据指向新单测与跨页同回合集成用例
证据要求补上本轮实跑结果与变异验证现象,避免再出现打勾但只覆盖一半
决策记录把验证口径写成具体条数与变异验证结论
Merge branch 'master' into fix/chat-status-lost
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
cb840c6e8c
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	docs/project-memory/shared-memory/pitfalls.md
Author
Member

close #335

close #335
k88936 added 1 commit 2026-09-17 16:58:38 +08:00
Merge remote-tracking branch 'origin/master' into fix/chat-status-lost
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
b7622f4a24
# Conflicts:
#	docs/project-memory/shared-memory/pitfalls.md
k88936 marked the pull request as ready for review 2026-09-17 16:58:56 +08:00
lhk229 added 1 commit 2026-09-17 17:17:58 +08:00
Merge branch 'master' into fix/chat-status-lost
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
61e1021a4d
lhk229 requested changes 2026-09-17 18:14:36 +08:00
Dismissed
lhk229 left a comment
Owner

审查结论:要求修改

整体方向认可:把 DirectProject 聊天真相源收敛到唯一 thread manager、拆掉 #375 那套并行事件流,架构收敛正确。纯函数层(directThreadChat / directHistoryPaging / directThreadItemProjection)质量高、测试扎实,回执竞态与跨页同回合用例做了变异验证,文档体系(ADR / 里程碑 / 实施计划 / 主规范 / decision-log)互链一致,无墓碑注释。但有三处需要在合并前修。

阻塞项

1. 重进项目时进行中的回合:终止按钮不可见、发送不走队列,形成死路(前端·高)

本 PR 删除 restoreRunningDirectCodexTurn 后,运行中回合靠 bootstrap 重放 turn.started 恢复 directTurnRunning,但:

  • App.tsx:12481 传给视图的仍是 controlBusy={chatAgentBusy}。恢复场景下 chatAgentBusy=falsedirectTurnRunning=trueProjectSupervisorView.tsx:265 submitting=false → 渲染发送箭头而非 ComposerStopButton;
  • App.tsx:12155 入队判定也只看 chatAgentBusy,用户点发送直接 invoke,被后端拒绝后提示「可在输入盒点终止」——而终止按钮根本不存在,只能等回合自然结束。

handleCancelDirectCodexTurn 的守卫(App.tsx:12090)已改用综合信号 supervisorChatBusy,注释自称「繁忙判据与终止按钮可见条件一致」,实现却没跟上。建议 Direct 场景传 supervisorChatBusy(或加独立 prop 驱动终止按钮),入队判定同步对齐;注意改完后恢复回合结束(turn.completed 到达,而非本地 invoke 的 finally)时也要能 dispatchNextQueuedChatTurn(),否则队列会卡住。

2. /history 在翻页之后重读会乱序(前端·高)

App.tsx:3602-3609:replace 模式把最新尾屏切片经 mergeDirectHistoryItems 并入,而 directThreadChat.ts:247-258 把新切片作为 leading 前插(mergeHistoryEntries(newItems, state.history))。推演:打开项目 [m7..m26] → 显示更早 [m1..m26] → 输入 /history → [m7..m26, m1..m6],最新 20 条排在更早页之上;buildDirectChatTurns 不按时间排序,乱序直接可见,锚点同时被重置回尾屏 firstItemId。被删除的 tests/directHistoryPagination.test.tsx 里「加载旧页后重读历史回到最新页,再翻页仍按原顺序且不重复」钉的正是这个场景,新测试体系没有任何 /history 用例。建议 replace 分支合并前先 resetDirectThreadChat(),并补一条 /history 重读集成测试。

3. ReasoningDelta 明文下发绕过 sanitize_detail_text(后端·高)

新增的 direct_codex_reasoning_delta_event(codex_app_server/mod.rs)把 params.delta 原文直接入队 item_delta;而完成态 item 投影路径(direct_thread_wire.rs:326/363)是过 sanitize_detail_text 脱敏的(绝对路径归一 + sk- 密钥打码)。同一段思考文本流式期未脱敏、完成时脱敏,口径不一致;且 mergeDirectChatEntry 正文取更长一份,未脱敏的 delta 累计文本会盖过完成时的脱敏快照常驻历史。master 上 reasoning delta 被刻意降级为 Activity("preparing")(旧测试名 ...without_raw_text),本 PR 这是新增泄露面,而 secrets_and_absolute_paths_are_not_leaked 只覆盖 item 投影、不覆盖 delta。建议 delta 入队前同样过 sanitize_detail_text(可做字符预算控制成本),并补一条 reasoning / agentMessage delta 不泄露 sk- 与绝对路径的测试。

建议同 PR 处理

4. 「显示更早」第一页失败时 hasMore 被置 false,按钮永久消失(前端·中)

App.tsx:11855if (pages.error) throw 之前无条件 setDirectHistoryHasMore(pages.hasMore);directHistoryPaging.ts:56 hasMore 初始为 false,首页即失败时返回 false → 一次瞬时 IO 抖动就永久剥夺翻历史能力(中途失败的路径是对的,会保留上一页的 true)。建议 error 时保留旧值,并补首页失败用例(现有测试只覆盖第 2 页失败)。

5. 提交的 ts-rs 生成绑定与 pitfalls 共享记忆正面冲突(文档·中)

docs/project-memory/shared-memory/pitfalls.md 的 ts-rs 条目明确指示不要把生成器重写产物当改动提交,本 PR 提交了这批重写(is generatedwas generated、Option 可选字段变 T | null、新增 envelope、两个无人引用的 ui-editor 绑定)。Rust 侧模型未变,属于生成器口径切换;若是有意为之,按 AGENTS.md「共享记忆与代码冲突时同步修正」必须改写该 pitfall。BindingChange.ts / BindingDTO.ts 全仓无人引用,要么删除要么说明保留理由。另外 fdc48fe72(回退绑定避免误改他人契约)与 0caf99822(重新提交绑定)两条提交信息自相矛盾,建议说明。

后续跟进(不阻塞合并,建议挂号)

  • 订阅失效无自愈:被 evict 的 subscriber 收不到 notify(direct_thread_manager.rs:388-396),永远不会触发 SUBSCRIPTION_EXPIRED → 事件流静默停摆、turnRunning 可能永久卡 true。建议 visibilitychange / 聚焦时主动 consume 一次,notify 到达且无 subscriptionId 时节流重试 bootstrap(App.tsx:1876-1900 当前只记账不重试)。
  • 重订 bootstrap 重放原始 delta 会正文翻倍:appendLiveText(directThreadChat.ts:139-153)是合并规则里唯一非幂等路径,bootstrap 对 active item 重放全部在队 delta;一旦加上面的自愈重试就会真实触发。建议后端 bootstrap 对 active item 先发累计快照再补增量。
  • last_completed_item_id 全链路死代码:每次 subscribe 白做一次文件回扫(direct_thread_manager.rs:153 / commands.rs:5363),建议删字段并重新生成绑定。
  • 无 unsubscribe 命令:换项目后 subscriber 残留至 evict,建议加 unsubscribe_direct_project_thread 在 effect cleanup 调用。
  • Stale cancel 双终态:mod.rs:3664-3672aborted3401-3419failed 兜底可能重复下发且与守卫释放非原子;前端虽幂等,建议终态带上 client_turn_id 或在同一临界区判断。
  • 里程碑两项「杀进程重开后部分历史按序出现」「页面重进恢复运行中回合」仍待真实 app-server 运行时验收,建议在 PR 描述显式挂号跟踪;B05 文档(AGC 画布交互稳定性修复计划)的状态与本 PR 现状不一致,顺手更新。

本地验证

  • vitest run tests/directThreadChat.test.ts tests/directHistoryPaging.test.ts tests/directTurnPresentation.test.ts:27/27 通过
  • cargo test direct_thread 25 通过、direct_project_history 23 通过
  • npm run check:encoding 通过、git diff --check 干净
  • appSurface 集成套件本机因 node_modules 缺 rehype-highlight 跑不起来(与 PR 无关的环境问题),以 CI 结果为准
## 审查结论:要求修改 整体方向认可:把 DirectProject 聊天真相源收敛到唯一 thread manager、拆掉 #375 那套并行事件流,架构收敛正确。纯函数层(directThreadChat / directHistoryPaging / directThreadItemProjection)质量高、测试扎实,回执竞态与跨页同回合用例做了变异验证,文档体系(ADR / 里程碑 / 实施计划 / 主规范 / decision-log)互链一致,无墓碑注释。但有三处需要在合并前修。 ## 阻塞项 **1. 重进项目时进行中的回合:终止按钮不可见、发送不走队列,形成死路(前端·高)** 本 PR 删除 `restoreRunningDirectCodexTurn` 后,运行中回合靠 bootstrap 重放 `turn.started` 恢复 `directTurnRunning`,但: - `App.tsx:12481` 传给视图的仍是 `controlBusy={chatAgentBusy}`。恢复场景下 `chatAgentBusy=false`、`directTurnRunning=true` → `ProjectSupervisorView.tsx:265` `submitting=false` → 渲染发送箭头而非 `ComposerStopButton`; - `App.tsx:12155` 入队判定也只看 `chatAgentBusy`,用户点发送直接 invoke,被后端拒绝后提示「可在输入盒点终止」——而终止按钮根本不存在,只能等回合自然结束。 `handleCancelDirectCodexTurn` 的守卫(`App.tsx:12090`)已改用综合信号 `supervisorChatBusy`,注释自称「繁忙判据与终止按钮可见条件一致」,实现却没跟上。建议 Direct 场景传 `supervisorChatBusy`(或加独立 prop 驱动终止按钮),入队判定同步对齐;注意改完后恢复回合结束(`turn.completed` 到达,而非本地 invoke 的 finally)时也要能 `dispatchNextQueuedChatTurn()`,否则队列会卡住。 **2. `/history` 在翻页之后重读会乱序(前端·高)** `App.tsx:3602-3609`:`replace` 模式把最新尾屏切片经 `mergeDirectHistoryItems` 并入,而 `directThreadChat.ts:247-258` 把新切片作为 leading 前插(`mergeHistoryEntries(newItems, state.history)`)。推演:打开项目 [m7..m26] → 显示更早 [m1..m26] → 输入 `/history` → [m7..m26, m1..m6],最新 20 条排在更早页之上;`buildDirectChatTurns` 不按时间排序,乱序直接可见,锚点同时被重置回尾屏 firstItemId。被删除的 `tests/directHistoryPagination.test.tsx` 里「加载旧页后重读历史回到最新页,再翻页仍按原顺序且不重复」钉的正是这个场景,新测试体系没有任何 `/history` 用例。建议 replace 分支合并前先 `resetDirectThreadChat()`,并补一条 /history 重读集成测试。 **3. ReasoningDelta 明文下发绕过 `sanitize_detail_text`(后端·高)** 新增的 `direct_codex_reasoning_delta_event`(`codex_app_server/mod.rs`)把 `params.delta` 原文直接入队 `item_delta`;而完成态 item 投影路径(`direct_thread_wire.rs:326/363`)是过 `sanitize_detail_text` 脱敏的(绝对路径归一 + `sk-` 密钥打码)。同一段思考文本流式期未脱敏、完成时脱敏,口径不一致;且 `mergeDirectChatEntry` 正文取更长一份,未脱敏的 delta 累计文本会盖过完成时的脱敏快照常驻历史。master 上 reasoning delta 被刻意降级为 `Activity("preparing")`(旧测试名 `...without_raw_text`),本 PR 这是新增泄露面,而 `secrets_and_absolute_paths_are_not_leaked` 只覆盖 item 投影、不覆盖 delta。建议 delta 入队前同样过 `sanitize_detail_text`(可做字符预算控制成本),并补一条 reasoning / agentMessage delta 不泄露 `sk-` 与绝对路径的测试。 ## 建议同 PR 处理 **4. 「显示更早」第一页失败时 hasMore 被置 false,按钮永久消失(前端·中)** `App.tsx:11855` 在 `if (pages.error) throw` 之前无条件 `setDirectHistoryHasMore(pages.hasMore)`;`directHistoryPaging.ts:56` hasMore 初始为 false,首页即失败时返回 false → 一次瞬时 IO 抖动就永久剥夺翻历史能力(中途失败的路径是对的,会保留上一页的 true)。建议 error 时保留旧值,并补首页失败用例(现有测试只覆盖第 2 页失败)。 **5. 提交的 ts-rs 生成绑定与 pitfalls 共享记忆正面冲突(文档·中)** `docs/project-memory/shared-memory/pitfalls.md` 的 ts-rs 条目明确指示不要把生成器重写产物当改动提交,本 PR 提交了这批重写(`is generated`→`was generated`、Option 可选字段变 `T | null`、新增 envelope、两个无人引用的 ui-editor 绑定)。Rust 侧模型未变,属于生成器口径切换;若是有意为之,按 AGENTS.md「共享记忆与代码冲突时同步修正」必须改写该 pitfall。`BindingChange.ts` / `BindingDTO.ts` 全仓无人引用,要么删除要么说明保留理由。另外 `fdc48fe72`(回退绑定避免误改他人契约)与 `0caf99822`(重新提交绑定)两条提交信息自相矛盾,建议说明。 ## 后续跟进(不阻塞合并,建议挂号) - **订阅失效无自愈**:被 evict 的 subscriber 收不到 notify(`direct_thread_manager.rs:388-396`),永远不会触发 `SUBSCRIPTION_EXPIRED` → 事件流静默停摆、`turnRunning` 可能永久卡 true。建议 `visibilitychange` / 聚焦时主动 consume 一次,notify 到达且无 subscriptionId 时节流重试 bootstrap(`App.tsx:1876-1900` 当前只记账不重试)。 - **重订 bootstrap 重放原始 delta 会正文翻倍**:`appendLiveText`(`directThreadChat.ts:139-153`)是合并规则里唯一非幂等路径,bootstrap 对 active item 重放全部在队 delta;一旦加上面的自愈重试就会真实触发。建议后端 bootstrap 对 active item 先发累计快照再补增量。 - **`last_completed_item_id` 全链路死代码**:每次 subscribe 白做一次文件回扫(`direct_thread_manager.rs:153` / `commands.rs:5363`),建议删字段并重新生成绑定。 - **无 unsubscribe 命令**:换项目后 subscriber 残留至 evict,建议加 `unsubscribe_direct_project_thread` 在 effect cleanup 调用。 - **Stale cancel 双终态**:`mod.rs:3664-3672` 的 `aborted` 与 `3401-3419` 的 `failed` 兜底可能重复下发且与守卫释放非原子;前端虽幂等,建议终态带上 client_turn_id 或在同一临界区判断。 - 里程碑两项「杀进程重开后部分历史按序出现」「页面重进恢复运行中回合」仍待真实 app-server 运行时验收,建议在 PR 描述显式挂号跟踪;B05 文档(AGC 画布交互稳定性修复计划)的状态与本 PR 现状不一致,顺手更新。 ## 本地验证 - `vitest run tests/directThreadChat.test.ts tests/directHistoryPaging.test.ts tests/directTurnPresentation.test.ts`:27/27 通过 - `cargo test direct_thread` 25 通过、`direct_project_history` 23 通过 - `npm run check:encoding` 通过、`git diff --check` 干净 - appSurface 集成套件本机因 node_modules 缺 `rehype-highlight` 跑不起来(与 PR 无关的环境问题),以 CI 结果为准
k88936 added 6 commits 2026-09-17 18:48:46 +08:00
删除 Rust 侧 read_direct_tool_calls_at 与 read_direct_turn_stream_at:前端已改走项目对话历史,这两个回读函数不再有调用方
direct_tool_calls / direct_turn_stream 的模块注释与上限常量口径改成写侧,回读命令退役后不再提「回读」
direct_tool_calls 单测改为直接读盘断言落盘内容,删掉只服务已删回读的跳过损坏行 / 合并重复行 / 回读上限三条用例
写前读取的 UTF-8 容错用例改名为 tool_call_pre_read_skips_invalid_utf8_line 并补缺文件返回空表断言
app/types.ts 删除 GameCreatorDirectTurnUpdateEvent 与 TurnStream* 五个只服务旧事件链路的类型
工具卡片方案 §3 由「回读命令」改写为「落盘契约(写侧)」,文首修订说明同步
ADR 首屏锚点那条写清锚点语义:新端(较新一侧)边界含锚点条目,命令参数 throughItemId,更新条目只从运行态事件来,翻页仍用 firstItemId 的 beforeItemId 不含锚点
里程碑范围与已确认决策同步该口径,验收标准新增一条待验收行(首屏 throughItemId + 回执前不发请求)
实施计划第 6 步补上首屏读取等订阅回执、锚点缺失才退化取文件尾
决策记录新增 2026-09-17 条目,记下 manager 恒 None / commands 磁盘回填 / 前端自 1b40f030e 起不再读取的现状与本次决策
direct_project_history 的分页锚点收敛成 DirectProjectHistoryAnchor:Newest(取文件尾)/ Before(不含锚点,翻页用)/ Through(含锚点,首屏边界用)
read_direct_project_history_slice 命令新增 throughItemId,与 beforeItemId 互斥,两个都传直接报错
新增三条用例:Through 含锚点且挡掉更新的条目、锚点是最老一条时仍把它算进窗口、锚点不存在时失败关闭
既有用例改走显式锚点(Newest / Before),行为不变
变异验证:把 anchor_seen 初始化成 true(等于忽略锚点)后四条锚点用例变红
锚点闸门抽到 directHistoryAnchorGate:开闸、同项目复用、消费后不再等待三件事只有一份实现
App 订阅 effect 复用首屏开好的闸门,success 时 settle 成回执的 lastCompletedItemId,订阅不可用/失败/切项目/清理时 settle 成 null
首屏切片只在新端边界 throughItemId 有值时带上它,非 replace 模式才等闸门,/history 手动重读仍按文件尾
测试 harness 的切片桩支持 throughItemId,setDirectThreadHistory 同时同步回执用的 lastCompletedItemId
新增 directHistoryAnchorGate 单测覆盖复用/消费回退规则,App 集成用例断言首屏锚在回执那条、回执先于首屏读取
证据要求新增「首屏锚点(已跑)」一条:Rust 22 条、闸门单测 10 条、appSurface 全量 475/457/17 与唯一既有失败的对照结论
里程碑验收「首屏边界只认订阅回执」那条打勾,并写明两处变异验证现象
决策记录把「验证方式(待跑)」改成已跑的实跑结果与变异验证结论
Merge remote-tracking branch 'origin/master' into fix/chat-status-lost
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
cb7db4923b
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	docs/project-memory/shared-memory/pitfalls.md
lhk229 added 1 commit 2026-09-18 00:31:46 +08:00
Merge branch 'master' into fix/chat-status-lost
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
bb772c7422
k88936 added 3 commits 2026-09-18 00:38:25 +08:00
统一恢复回合的忙碌态与终止按钮显示

按 turn.completed 推进 DirectProject 消息队列并补充回归测试
对 reasoning 与 agentMessage 增量复用历史正文脱敏和截断规则

补充密钥与绝对路径不泄露的 Rust 回归测试
收紧 DirectProject 回合完成判定
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 6m53s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 6m43s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m58s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m18s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m47s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m14s
Project CI / Repository checks (pull_request) Successful in 3m38s
Project CI / Backend tests (pull_request) Successful in 9m6s
Project CI / Frontend tests (pull_request) Failing after 8m35s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m25s
Project CI / Native shell tests (pull_request) Successful in 12m22s
254f3fcc5d
仅由 turn.completed 结束订阅恢复的运行态

调整终止回归测试以覆盖真实生命周期事件
k88936 requested review from lhk229 2026-09-18 00:38:43 +08:00
Author
Member

as for 2 #403

as for 2 #403
lhk229 requested changes 2026-09-18 00:45:07 +08:00
Dismissed
lhk229 left a comment
Owner

复审结论:仍要求修改(阻塞项 1、2 未动)

复审范围:61e1021a4..bb772c742。首屏锚点闸门这组提交(de4dfe853 / 110e9260a / fae02694f / e7bd6a339 / 180a249a6)质量很好,上一轮的两条意见已闭环;但两条阻塞项原样未动。

上轮意见的闭环情况

  • last_completed_item_id 死代码 → 已闭环,且做法比"删掉"更好:升级为首屏切片的新端边界(Through 锚点,含该条),directHistoryAnchorGate 把"回执到达前不读首屏、同一订阅只锚一次、消费后退文件尾"收成一份实现。Rust 侧 Through 语义正确(跳过更新条目、锚点进窗口、收满一屏多看一条定 hasMore、锚点不存在显式报错)。read_direct_project_last_item_id_at 等死回读也清了(180a249a6)。
  • 首屏切片与运行态事件的重叠竞态(上轮前端审查第 5 条)→ 随锚点闸门一并闭环。
  • 测试:锚点闸门单测 10 条、direct_project_history 26 条(新增 3 条 Through 用例)、前端四个文件 37 条,本地实跑全绿;里程碑/决策记录证据同步到位。

仍阻塞(与上轮一致,代码未动)

1. 重进项目时进行中的回合:终止按钮不可见、发送不走队列(前端·高)

现状与上轮完全相同:App.tsx:12524controlBusy={chatAgentBusy};ProjectSupervisorView.tsx:265 submitting 仍只由 controlBusy 推导,768 行终止按钮仍 submitting && onCancelTurn;App.tsx:12198 入队仍只判 chatAgentBusy。恢复出来的进行中回合(directTurnRunning=truechatAgentBusy=false)下:过程卡转圈、终止按钮不渲染、点发送直接打后端被拒,提示用户去点一个不存在的终止按钮。handleCancelDirectCodexTurn 的守卫注释仍自称"繁忙判据与终止按钮可见条件一致",依然名实不符。修法同上轮:Direct 场景传 supervisorChatBusy(或独立 prop),入队判定对齐,并保证恢复回合 turn.completed 到达时驱动 dispatchNextQueuedChatTurn()

2. /history 在翻页之后重读会乱序(前端·高)

directThreadChat.ts/history 路径(12218 → loadProjectConversation(..., 'replace') → 3651 mergeDirectHistoryItems 前插)自上轮以来零改动。翻到过更早页后输入 /history,最新尾屏切片仍会被前插到更早页之前([m7..m26, m1..m6]),锚点也被重置;被删旧测试钉的正是这个场景,新体系仍无 /history 用例。注意新闸门刻意不管 replace("/history 手动重读仍按文件尾"),所以闸门不改变此结论。修法同上轮:replace 分支并入前先 resetDirectThreadChat(),补一条 /history 重读集成测试。

建议同 PR 处理(同上轮,未动)

3. 「显示更早」首页失败 hasMore 被置 false:App.tsx:11898 仍在 pages.error 抛出(11901)之前无条件写入,首页即失败时按钮永久消失。改成 error 时保留旧值,补首页失败用例。

4. ts-rs 生成绑定与 pitfalls 正面冲突:pitfalls.md:5690-5696 现行口径仍写"不要把重写结果当改动提交、先 git checkout -- generated",而本 PR 提交的正是这批重写产物。按 AGENTS.md「共享记忆与代码冲突时同步修正」,这条 pitfall 必须改写为新口径,否则下一个开发者会按它把绑定回滚。

降级为建议(考虑 ADR 后)

5. ReasoningDelta 明文下发:docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md 已明文记录"不放宽可见范围"的决策,尊重该决策,降级为建议。但 ADR 的依据("被下发的就是 item.completed 展示的同一段文本")在脱敏生效时不成立:完成态走 item_text → sanitize_detail_text(wire.rs:362-365,绝对路径归一 + sk- 打码),delta 是原文;且 mergeDirectChatEntry 正文取长,未脱敏的累计文本会盖过完成时的脱敏快照。建议在 delta 入队前过一遍 sanitize_detail_text(一次正则扫描,成本可控),让流式与完成态口径一致,ADR 的依据也才真正成立。

新代码的一处小边界

6. Through 锚点不在文件中时整屏加载失败:direct_project_history.rs:712-717 锚点缺失直接 Err,首屏加载整体报错。订阅回执到首屏读取之间若文件被修复/截断(repair 残尾、外部写入),用户看到的是"历史读取失败"而非降级。建议锚点缺失时降级为 Newest(记一条日志),或前端捕获后退文件尾重读一次。

本地验证(本轮实跑)

  • vitest run tests/directHistoryAnchorGate.test.ts tests/directThreadChat.test.ts tests/directHistoryPaging.test.ts tests/directTurnPresentation.test.ts:37/37 通过
  • cargo test --bins direct_project_history:26/26 通过
  • appSurface 集成套件本机仍因 node_modules 缺 rehype-highlight 跑不起来(与 PR 无关),以 CI 为准

阻塞项 1、2 都是小改动,修完即可通过。

## 复审结论:仍要求修改(阻塞项 1、2 未动) 复审范围:61e1021a4..bb772c742。首屏锚点闸门这组提交(de4dfe853 / 110e9260a / fae02694f / e7bd6a339 / 180a249a6)质量很好,上一轮的两条意见已闭环;但两条阻塞项原样未动。 ## 上轮意见的闭环情况 - **`last_completed_item_id` 死代码** → 已闭环,且做法比"删掉"更好:升级为首屏切片的**新端边界**(Through 锚点,含该条),`directHistoryAnchorGate` 把"回执到达前不读首屏、同一订阅只锚一次、消费后退文件尾"收成一份实现。Rust 侧 Through 语义正确(跳过更新条目、锚点进窗口、收满一屏多看一条定 hasMore、锚点不存在显式报错)。`read_direct_project_last_item_id_at` 等死回读也清了(180a249a6)。 - **首屏切片与运行态事件的重叠竞态**(上轮前端审查第 5 条)→ 随锚点闸门一并闭环。 - 测试:锚点闸门单测 10 条、`direct_project_history` 26 条(新增 3 条 Through 用例)、前端四个文件 37 条,本地实跑全绿;里程碑/决策记录证据同步到位。 ## 仍阻塞(与上轮一致,代码未动) **1. 重进项目时进行中的回合:终止按钮不可见、发送不走队列(前端·高)** 现状与上轮完全相同:`App.tsx:12524` 仍 `controlBusy={chatAgentBusy}`;`ProjectSupervisorView.tsx:265` `submitting` 仍只由 controlBusy 推导,768 行终止按钮仍 `submitting && onCancelTurn`;`App.tsx:12198` 入队仍只判 `chatAgentBusy`。恢复出来的进行中回合(`directTurnRunning=true`、`chatAgentBusy=false`)下:过程卡转圈、终止按钮不渲染、点发送直接打后端被拒,提示用户去点一个不存在的终止按钮。`handleCancelDirectCodexTurn` 的守卫注释仍自称"繁忙判据与终止按钮可见条件一致",依然名实不符。修法同上轮:Direct 场景传 `supervisorChatBusy`(或独立 prop),入队判定对齐,并保证恢复回合 `turn.completed` 到达时驱动 `dispatchNextQueuedChatTurn()`。 **2. `/history` 在翻页之后重读会乱序(前端·高)** `directThreadChat.ts` 与 `/history` 路径(12218 → `loadProjectConversation(..., 'replace')` → 3651 `mergeDirectHistoryItems` 前插)自上轮以来零改动。翻到过更早页后输入 `/history`,最新尾屏切片仍会被前插到更早页之前([m7..m26, m1..m6]),锚点也被重置;被删旧测试钉的正是这个场景,新体系仍无 `/history` 用例。注意新闸门刻意不管 replace("/history 手动重读仍按文件尾"),所以闸门不改变此结论。修法同上轮:replace 分支并入前先 `resetDirectThreadChat()`,补一条 /history 重读集成测试。 ## 建议同 PR 处理(同上轮,未动) **3. 「显示更早」首页失败 hasMore 被置 false**:`App.tsx:11898` 仍在 `pages.error` 抛出(11901)之前无条件写入,首页即失败时按钮永久消失。改成 error 时保留旧值,补首页失败用例。 **4. ts-rs 生成绑定与 pitfalls 正面冲突**:`pitfalls.md:5690-5696` 现行口径仍写"不要把重写结果当改动提交、先 `git checkout -- generated`",而本 PR 提交的正是这批重写产物。按 AGENTS.md「共享记忆与代码冲突时同步修正」,这条 pitfall 必须改写为新口径,否则下一个开发者会按它把绑定回滚。 ## 降级为建议(考虑 ADR 后) **5. ReasoningDelta 明文下发**:`docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md` 已明文记录"不放宽可见范围"的决策,尊重该决策,降级为建议。但 ADR 的依据("被下发的就是 item.completed 展示的同一段文本")在脱敏生效时不成立:完成态走 `item_text → sanitize_detail_text`(wire.rs:362-365,绝对路径归一 + `sk-` 打码),delta 是原文;且 `mergeDirectChatEntry` 正文取长,未脱敏的累计文本会盖过完成时的脱敏快照。建议在 delta 入队前过一遍 `sanitize_detail_text`(一次正则扫描,成本可控),让流式与完成态口径一致,ADR 的依据也才真正成立。 ## 新代码的一处小边界 **6. Through 锚点不在文件中时整屏加载失败**:`direct_project_history.rs:712-717` 锚点缺失直接 `Err`,首屏加载整体报错。订阅回执到首屏读取之间若文件被修复/截断(repair 残尾、外部写入),用户看到的是"历史读取失败"而非降级。建议锚点缺失时降级为 `Newest`(记一条日志),或前端捕获后退文件尾重读一次。 ## 本地验证(本轮实跑) - `vitest run tests/directHistoryAnchorGate.test.ts tests/directThreadChat.test.ts tests/directHistoryPaging.test.ts tests/directTurnPresentation.test.ts`:37/37 通过 - `cargo test --bins direct_project_history`:26/26 通过 - appSurface 集成套件本机仍因 node_modules 缺 `rehype-highlight` 跑不起来(与 PR 无关),以 CI 为准 阻塞项 1、2 都是小改动,修完即可通过。
Author
Member

review outdated

review outdated
k88936 requested review from lhk229 2026-09-18 00:53:42 +08:00
Owner

kimi智商有问题

kimi智商有问题
lhk229 requested changes 2026-09-18 00:59:54 +08:00
Dismissed
lhk229 left a comment
Owner

第三轮评审:阻塞项只剩两条小修(3、4),2 不再阻断

先确认进展:

  • 阻塞项 1(恢复回合终止按钮/消息排队)→ 已闭环 controlBusy 改传综合信号、入队判定对齐 supervisorChatBusy、队列推进改由 turn.completed 经 pending 标记驱动(de7211e8f),254f3fcc5 再收紧成"订阅恢复的运行态只由 turn.completed 结束",方向正确,回归用例也补上了。
  • 建议项 5(ReasoningDelta 脱敏)→ 已闭环 direct_thread_delta_text 复用历史正文的脱敏与字符预算,Message / Reasoning 两条 delta 路径统一走它,密钥与绝对路径的 Rust 回归测试到位(5a0a3362f)。
  • 阻塞项 2(/history 翻页后重读乱序)→ 不再阻断本 PR 。接受 #403 的口径:这个命令是"无推送通道年代的人工重灌",订阅通道就位后应由 #403 清理下线,乱序问题随命令一起消失。本 PR 不再要求处理;请把 #403 挂上 triage 标签并在合并后别让它沉没即可。

仍需处理(都是小修,处理完即可通过)

3. 「显示更早」首页失败时 hasMore 被置 false,按钮永久消失(前端·中)

App.tsx:11905 仍在 if (pages.error) throw(11908)之前无条件 setDirectHistoryHasMore(pages.hasMore);directHistoryPaging.tshasMore 初始为 false,第一页就失败时返回 false → 一次瞬时 IO 抖动就让「显示更早」按钮永久消失,只能重进项目(中途失败的路径是对的,会保留上一页的 true)。

修法:pages.error 时跳过 setDirectHistoryHasMore(保留旧值),或让 readDirectHistoryPages 首页失败时不回传 hasMore;并补一条首页失败用例(现有 directHistoryPaging.test.ts 只覆盖了第 2 页失败)。

4. 提交的 ts-rs 生成绑定与 pitfalls 共享记忆正面冲突(文档·中)

docs/project-memory/shared-memory/pitfalls.md:5690-5696 现行口径仍写着「不要把重写结果当改动提交;跑过 cargo test 后先 git checkout -- …/generated,删掉多出来的 DirectCodexUserMessageEnvelope.ts」,而本 PR 提交的正是这批重写产物(头注释、Option<T>T | null、新增 envelope、两个无人引用的 ui-editor 绑定)。

如果"以生成器输出为准"是新的既定口径(里程碑证据也是这么写的),按 AGENTS.md「共享记忆与代码冲突时同步修正」的要求,这条 pitfall 必须在同一次变更里改写成新口径,否则下一个开发者会按它把绑定 git checkout 回滚,再与 export_bindings 的证据要求打架。顺带:BindingChange.ts / BindingDTO.ts 全仓无人引用,要么删要么在文档里说明保留理由;fdc48fe72(回退绑定)与 0caf99822(重新提交)两条提交信息自相矛盾,也建议顺手澄清。

本地验证(本轮)

  • 三个修复提交已逐行复审,前端四个纯函数测试文件 37/37 通过(实跑)
  • appSurface 集成套件本机因 node_modules 缺 rehype-highlight 跑不起来(与 PR 无关),新增的两条 composer 回归用例以 CI 结果为准
## 第三轮评审:阻塞项只剩两条小修(3、4),2 不再阻断 先确认进展: - **阻塞项 1(恢复回合终止按钮/消息排队)→ 已闭环** ✅。`controlBusy` 改传综合信号、入队判定对齐 `supervisorChatBusy`、队列推进改由 `turn.completed` 经 pending 标记驱动(de7211e8f),254f3fcc5 再收紧成"订阅恢复的运行态只由 `turn.completed` 结束",方向正确,回归用例也补上了。 - **建议项 5(ReasoningDelta 脱敏)→ 已闭环** ✅。`direct_thread_delta_text` 复用历史正文的脱敏与字符预算,Message / Reasoning 两条 delta 路径统一走它,密钥与绝对路径的 Rust 回归测试到位(5a0a3362f)。 - **阻塞项 2(`/history` 翻页后重读乱序)→ 不再阻断本 PR** ✅。接受 #403 的口径:这个命令是"无推送通道年代的人工重灌",订阅通道就位后应由 #403 清理下线,乱序问题随命令一起消失。本 PR 不再要求处理;请把 #403 挂上 triage 标签并在合并后别让它沉没即可。 ## 仍需处理(都是小修,处理完即可通过) **3. 「显示更早」首页失败时 hasMore 被置 false,按钮永久消失(前端·中)** `App.tsx:11905` 仍在 `if (pages.error) throw`(11908)**之前**无条件 `setDirectHistoryHasMore(pages.hasMore)`;`directHistoryPaging.ts` 的 `hasMore` 初始为 `false`,第一页就失败时返回 false → 一次瞬时 IO 抖动就让「显示更早」按钮永久消失,只能重进项目(中途失败的路径是对的,会保留上一页的 `true`)。 修法:`pages.error` 时跳过 `setDirectHistoryHasMore`(保留旧值),或让 `readDirectHistoryPages` 首页失败时不回传 hasMore;并补一条首页失败用例(现有 `directHistoryPaging.test.ts` 只覆盖了第 2 页失败)。 **4. 提交的 ts-rs 生成绑定与 pitfalls 共享记忆正面冲突(文档·中)** `docs/project-memory/shared-memory/pitfalls.md:5690-5696` 现行口径仍写着「不要把重写结果当改动提交;跑过 `cargo test` 后先 `git checkout -- …/generated`,删掉多出来的 `DirectCodexUserMessageEnvelope.ts`」,而本 PR 提交的正是这批重写产物(头注释、`Option<T>` → `T | null`、新增 envelope、两个无人引用的 ui-editor 绑定)。 如果"以生成器输出为准"是新的既定口径(里程碑证据也是这么写的),按 AGENTS.md「共享记忆与代码冲突时同步修正」的要求,这条 pitfall 必须在同一次变更里改写成新口径,否则下一个开发者会按它把绑定 `git checkout` 回滚,再与 export_bindings 的证据要求打架。顺带:`BindingChange.ts` / `BindingDTO.ts` 全仓无人引用,要么删要么在文档里说明保留理由;`fdc48fe72`(回退绑定)与 `0caf99822`(重新提交)两条提交信息自相矛盾,也建议顺手澄清。 ## 本地验证(本轮) - 三个修复提交已逐行复审,前端四个纯函数测试文件 37/37 通过(实跑) - appSurface 集成套件本机因 node_modules 缺 `rehype-highlight` 跑不起来(与 PR 无关),新增的两条 composer 回归用例以 CI 结果为准
k88936 added 2 commits 2026-09-18 01:45:28 +08:00
首页历史读取失败时保留显示更早按钮状态并补充回归用例

恢复 DirectCodexUser 生成绑定契约,移除多余 envelope 与无引用 UI 绑定

同步 ts-rs 生成物排障共享记忆
Merge remote-tracking branch 'origin/master' into fix/chat-status-lost
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 7m10s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 7m21s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m26s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m41s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m59s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m40s
Project CI / Repository checks (pull_request) Successful in 4m58s
Project CI / Native shell tests (pull_request) Successful in 7m41s
Project CI / Backend tests (pull_request) Successful in 9m14s
Project CI / Frontend tests (pull_request) Successful in 7m37s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m43s
e0dda975e8
k88936 requested review from lhk229 2026-09-18 01:50:26 +08:00
lhk229 approved these changes 2026-09-18 01:56:23 +08:00
lhk229 left a comment
Owner

第四轮评审:通过

467b712da 把剩余两条小修干净地收掉了,五轮意见全部闭环:

  • 3. hasMore 首页失败 → 已修 setDirectHistoryHasMore 收入 if (!pages.error) 守卫,失败保留旧值,「显示更早」按钮不再被一次 IO 抖动永久收掉;directHistoryPaging.test.ts 补了首页失败用例,锚点保留语义也钉住了。
  • 4. ts-rs 绑定冲突 → 已修,且修了根因 。未使用的 DirectCodexUserMessageEnvelope 从 Rust 模型里删除(漂移的源头没了),envelope 与两个无人引用的 ui-editor 绑定文件已删,DirectCodexUser* 生成绑定恢复仓库契约形状——PR 相对 master 已不再触碰这批绑定;pitfall 同步改写为新口径,还特意收窄成"只恢复 DirectCodexUser*.ts、不要恢复整个 generated/",保护了本 PR 现役的 DirectThread 绑定,考虑得比我的建议更细。

五轮意见终态

# 意见 终态
1 恢复回合终止按钮不可见、发送不走队列 de7211e8f + 254f3fcc5
2 /history 翻页后重读乱序 不阻断,由 #403 清理过时命令(请记得给它挂 triage 标签)
3 「显示更早」首页失败 hasMore 永久置 false 467b712da
4 ts-rs 生成绑定与 pitfalls 冲突 467b712da(含根因)
5 ReasoningDelta 明文绕过脱敏 5a0a3362f

本轮验证

  • directHistoryPaging / directThreadChat / directHistoryAnchorGate 28/28 通过(实跑,含新增首页失败用例)
  • check:encoding 4796 文件通过、git diff --check 干净
  • 本机 typecheck 的 14 条报错全部位于本 PR 未触碰的文件(template-library / resourceModel / appUpdate 等),原因是本 worktree node_modules 缺 @types/three@types/react-window,与本 PR 无关,以 CI 为准

合并后跟进(此前已挂号,不重复展开)

订阅被 evict 无自愈、bootstrap 重放 delta 的非幂等、缺 unsubscribe 命令、Stale cancel 双终态、Through 锚点缺失整屏报错、里程碑两项真实运行时验收——建议照此前清单逐项挂 issue 跟踪。

可以合并了。

## 第四轮评审:通过 ✅ 467b712da 把剩余两条小修干净地收掉了,五轮意见全部闭环: - **3. hasMore 首页失败 → 已修** ✅。`setDirectHistoryHasMore` 收入 `if (!pages.error)` 守卫,失败保留旧值,「显示更早」按钮不再被一次 IO 抖动永久收掉;`directHistoryPaging.test.ts` 补了首页失败用例,锚点保留语义也钉住了。 - **4. ts-rs 绑定冲突 → 已修,且修了根因** ✅。未使用的 `DirectCodexUserMessageEnvelope` 从 Rust 模型里删除(漂移的源头没了),envelope 与两个无人引用的 ui-editor 绑定文件已删,`DirectCodexUser*` 生成绑定恢复仓库契约形状——PR 相对 master 已不再触碰这批绑定;pitfall 同步改写为新口径,还特意收窄成"只恢复 `DirectCodexUser*.ts`、不要恢复整个 `generated/`",保护了本 PR 现役的 DirectThread 绑定,考虑得比我的建议更细。 ## 五轮意见终态 | # | 意见 | 终态 | |---|------|------| | 1 | 恢复回合终止按钮不可见、发送不走队列 | ✅ de7211e8f + 254f3fcc5 | | 2 | `/history` 翻页后重读乱序 | ✅ 不阻断,由 #403 清理过时命令(请记得给它挂 triage 标签) | | 3 | 「显示更早」首页失败 hasMore 永久置 false | ✅ 467b712da | | 4 | ts-rs 生成绑定与 pitfalls 冲突 | ✅ 467b712da(含根因) | | 5 | ReasoningDelta 明文绕过脱敏 | ✅ 5a0a3362f | ## 本轮验证 - `directHistoryPaging` / `directThreadChat` / `directHistoryAnchorGate` 28/28 通过(实跑,含新增首页失败用例) - `check:encoding` 4796 文件通过、`git diff --check` 干净 - 本机 typecheck 的 14 条报错全部位于本 PR 未触碰的文件(template-library / resourceModel / appUpdate 等),原因是本 worktree node_modules 缺 `@types/three`、`@types/react-window`,与本 PR 无关,以 CI 为准 ## 合并后跟进(此前已挂号,不重复展开) 订阅被 evict 无自愈、bootstrap 重放 delta 的非幂等、缺 unsubscribe 命令、Stale cancel 双终态、Through 锚点缺失整屏报错、里程碑两项真实运行时验收——建议照此前清单逐项挂 issue 跟踪。 可以合并了。
lhk229 merged commit 30c377d0f0 into master 2026-09-18 02:13:51 +08:00
lhk229 deleted branch fix/chat-status-lost 2026-09-18 02:13:51 +08:00
Sign in to join this conversation.