保留唯一的thread manager作为direct project的状态来源 #384
Reference in New Issue
Block a user
Delete Branch "fix/chat-status-lost"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
说明: 在把工作交给段哥前还没有实现direct project聊天页面的迁移, 导致在 #375 里用很复杂的实现又做了一套事件流, 实测还有会话丢失的bug, 我在这里把数据获取的部分迁移到 #367 上
- 新增 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 绑定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_slicealways returns the full in-memory history withhasMore: falseandfirstItemId: null, ignoringlimitandbeforeItemId.现状:测试骨架的桩直接返回整份
directThreadHistoryItems,hasMore恒false、firstItemId恒null。问题:生产的 Rust 读取器会钳制 limit、排除锚点条目并返回
hasMore/firstItemId,App.tsx 靠它驱动「显示更早的对话」。桩不分页 → 首屏游标、翻页、hasMore三条路径在测试里永远不会被跑到,回归无人兜住。修法:桩按生产口径改成有界窗口——按
limit截取、按beforeItemId定位锚点(锚点自身不进窗口)、hasMore = start > 0、firstItemId取窗口最老一条的itemId;并补一条「26 条历史 → 首屏 20 条 → 点『显示更早的对话』翻出前 6 条」的用例。提交:
64bd006982.
apps/ai-game-creator-shell/tests/appSurface/harness.ts:1306· [test · low] 测试骨架重复下发turn.started原文:
completeDirectThreadTurnunconditionally emitsturn.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。提交:
1b3a3b0033.
apps/ai-game-creator-shell/tests/appSurface/harness.ts:951-961· [test · low]subscribebootstrap 口径与生产不符原文:
subscribe_direct_project_threadreturns every pending event as the bootstrap, but production'ssubscribeplaces the cursor at the queue tail and only replays unfinished-item events plus the latest lifecycle anchor.现状:桩把队列里所有未消费事件都当作 bootstrap 回放。
问题:生产(
is_bootstrap_event)游标落队尾,只回放「未完成条目的快照 + 最新一条生命周期锚点」,已完成的条目与瞬时增量不补发。桩全量回放会让「先发事件、后订阅」的用例拿到生产拿不到的内容,订阅/重订阅语义被写成错的契约。修法:桩按同一口径过滤(未完成条目 + 生命周期锚点)。
提交:
6a4cb2bc14.
apps/ai-game-creator-shell/src/App.tsx:3504-3508· [bug · high] 首屏历史切片在 staleness 守卫之前就写状态原文:The initial history slice is merged into
directThreadChatinside theinvoke(...).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。(本次未保留跨项目污染的回归用例,因为当前不可复现。)提交:
e17af30085.
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 可见性规则塞回后端),改为前端把前导条目先缓存,并入后面第一个用户条目开的回合;只有整份历史都没有用户条目时才保留兜底回合。已补单测。
提交:
0986936366.
apps/ai-game-creator-shell/src/features/project-workspace/directThreadChat.ts:34-37· [maintainability · low] reducer 里的死状态原文:
subscriptionIdandlastCompletedItemIdare written here (and inresolveDirectThreadBootstrap) but never read anywhere.现状:确认属实——App.tsx 用自己的 effect 局部
subscriptionId,分页靠directHistoryOldestItemIdRef/loadedDirectHistoryFirstItemId。问题:只写不读的字段会让人误以为订阅身份与分页锚点在 reducer 里,掩盖真实契约。
修法:从
DirectThreadChatState与resolveDirectThreadBootstrap中删除(resolveDirectThreadBootstrap收窄为只 reduce 事件),同步修改directThreadChat.test.ts。提交:
1b40f030e7.
apps/ai-game-creator-shell/src/App.tsx:6556-6557· [maintainability · low]void reply;死语句原文:
void reply;discards the final response returned bychat_with_game_creator_direct_codex... Avoid reply;statement also reads as dead code.现状:
const reply = await ...; void reply;。问题:正文按单一事实源只从线程事件/历史切片进聊天,这个绑定+丢弃既无作用又读起来像「兜底被删了」。
修法:不绑定返回值,直接
await,并保留说明「正文只从线程事件/历史切片进聊天」的注释(不再有void)。评审提到的「最后一条 notify 丢失时没有本地兜底」属于设计取舍,未改行为。提交:
ff8e517c88.
apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs:1078-1081· [bug · high] 思考正文在生产路径没有下发 → 已按ReasoningDelta修复原文:This
ReasoningDeltabranch only runs insidedirect_codex_notification_event, which is invoked solely from unit tests. The production stdout reader still maps the two reasoning delta methods toCodexTurnEvent::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 仍只降级成活动类别);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_identityreturnscall_idfor both a tool call and itsfunction_call_output(they share the samecall_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,已做变异验证)。提交:
da2ad83c310.
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 aturn.startedthread event ... Use the combined busy signal (e.g.supervisorChatBusy).现状:
handleCancelDirectCodexTurn的 guard 之前只认directTurnRunning。问题:
turn.started是异步事件,刚提交(invoke 已在跑、事件还没到)或订阅静默失败时,按钮可见但点击只提示「当前没有正在运行的回合,无法终止。」,拒绝取消。修法:guard 改用
supervisorChatBusy(本地 invoke 忙 或 订阅说还有回合没结束),与按钮可见条件一致;补用例「生命周期事件尚未到达时也能终止」(去掉修复即红,已做变异验证)。提交:
975a2e57711.
apps/ai-game-creator-shell/src-tauri/src/agent/direct_project_history.rs:153-160· [bug · medium]terminated由分支位置推断原文:
terminatedis 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两条用例(均做变异验证)。提交:
ad19c947512.
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:5320subscribe_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,界面表现就是「回合在跑、运行态一条事件都没有」。什么时候会真的不同(两个条件必须同时满足):
..、尾随分隔符、macOS/tmp→/private/tmp、Windows 短名 / UNC /\?\前缀等;projectId还是空串、manifest 正被应用的重写/回收流程替换(recoverRejectedManifestSnapshot那条链路)、读到半截 JSON。如果 manifest 一直读不到,反而不会命中:回合入口
direct_game_creator_codex_chat_at_with_optional_observer(mod.rs:4349)是?直接抛错,用户根本发不出消息,看到的是报错而不是空订阅。所以坏窗口是「瞬时不一致 × 非 canonical 路径」,两个条件都不满足时(例如用户从文件选择器拿到的就是真实路径,P == C)即使 manifest 抖动也不会绑错。建议(留人工,二选一):
resolve_direct_codex_project_authority(root)(只做「绝对路径 / 存在 / 是目录 / canonicalize」)取 canonical 路径,只有 canonicalize 本身失败才退回原始字符串。不改任何对外错误语义,直接消掉条件 2。direct_thread_id_for_project改成返回Result,订阅边界与回合入口一样直接报错(前端会显示订阅失败,而不是静默空订阅);兜底释放那条路径需要一条不失败的降级分支。- 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 用例继续覆盖读取器close #335
审查结论:要求修改
整体方向认可:把 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:265submitting=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:56hasMore 初始为 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(重新提交绑定)两条提交信息自相矛盾,建议说明。后续跟进(不阻塞合并,建议挂号)
direct_thread_manager.rs:388-396),永远不会触发SUBSCRIPTION_EXPIRED→ 事件流静默停摆、turnRunning可能永久卡 true。建议visibilitychange/ 聚焦时主动 consume 一次,notify 到达且无 subscriptionId 时节流重试 bootstrap(App.tsx:1876-1900当前只记账不重试)。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_direct_project_thread在 effect cleanup 调用。mod.rs:3664-3672的aborted与3401-3419的failed兜底可能重复下发且与守卫释放非原子;前端虽幂等,建议终态带上 client_turn_id 或在同一临界区判断。本地验证
vitest run tests/directThreadChat.test.ts tests/directHistoryPaging.test.ts tests/directTurnPresentation.test.ts:27/27 通过cargo test direct_thread25 通过、direct_project_history23 通过npm run check:encoding通过、git diff --check干净rehype-highlight跑不起来(与 PR 无关的环境问题),以 CI 结果为准as for 2 #403
复审结论:仍要求修改(阻塞项 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)。direct_project_history26 条(新增 3 条 Through 用例)、前端四个文件 37 条,本地实跑全绿;里程碑/决策记录证据同步到位。仍阻塞(与上轮一致,代码未动)
1. 重进项目时进行中的回合:终止按钮不可见、发送不走队列(前端·高)
现状与上轮完全相同:
App.tsx:12524仍controlBusy={chatAgentBusy};ProjectSupervisorView.tsx:265submitting仍只由 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')→ 3651mergeDirectHistoryItems前插)自上轮以来零改动。翻到过更早页后输入/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 通过rehype-highlight跑不起来(与 PR 无关),以 CI 为准阻塞项 1、2 都是小改动,修完即可通过。
review outdated
kimi智商有问题
第三轮评审:阻塞项只剩两条小修(3、4),2 不再阻断
先确认进展:
controlBusy改传综合信号、入队判定对齐supervisorChatBusy、队列推进改由turn.completed经 pending 标记驱动(de7211e8f),254f3fcc5 再收紧成"订阅恢复的运行态只由turn.completed结束",方向正确,回归用例也补上了。direct_thread_delta_text复用历史正文的脱敏与字符预算,Message / Reasoning 两条 delta 路径统一走它,密钥与绝对路径的 Rust 回归测试到位(5a0a3362f)。/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(重新提交)两条提交信息自相矛盾,也建议顺手澄清。本地验证(本轮)
rehype-highlight跑不起来(与 PR 无关),新增的两条 composer 回归用例以 CI 结果为准1
第四轮评审:通过 ✅
467b712da把剩余两条小修干净地收掉了,五轮意见全部闭环:setDirectHistoryHasMore收入if (!pages.error)守卫,失败保留旧值,「显示更早」按钮不再被一次 IO 抖动永久收掉;directHistoryPaging.test.ts补了首页失败用例,锚点保留语义也钉住了。DirectCodexUserMessageEnvelope从 Rust 模型里删除(漂移的源头没了),envelope 与两个无人引用的 ui-editor 绑定文件已删,DirectCodexUser*生成绑定恢复仓库契约形状——PR 相对 master 已不再触碰这批绑定;pitfall 同步改写为新口径,还特意收窄成"只恢复DirectCodexUser*.ts、不要恢复整个generated/",保护了本 PR 现役的 DirectThread 绑定,考虑得比我的建议更细。五轮意见终态
de7211e8f+254f3fcc5/history翻页后重读乱序467b712da5a0a3362f本轮验证
directHistoryPaging/directThreadChat/directHistoryAnchorGate28/28 通过(实跑,含新增首页失败用例)check:encoding4796 文件通过、git diff --check干净@types/three、@types/react-window,与本 PR 无关,以 CI 为准合并后跟进(此前已挂号,不重复展开)
订阅被 evict 无自愈、bootstrap 重放 delta 的非幂等、缺 unsubscribe 命令、Stale cancel 双终态、Through 锚点缺失整屏报错、里程碑两项真实运行时验收——建议照此前清单逐项挂 issue 跟踪。
可以合并了。