Feat/ 待发送消息从前端队列移入rust #514
Reference in New Issue
Block a user
Delete Branch "feat/msg-queue-to-rust"
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?
closes #499
沿用thread manager, 状态存在日志里
- `StoredEvent` 增加非序列化的 `pending` 产物字段:它的有无就是队列成员身份,队列的先后就是事件先后,不另建队列表 - 新增 `enqueue_pending_turn`:按 `clientTurnId` 幂等(判重范围「在队 ∪ 正在跑的那一轮」),容量用 `queue_has_room` 挡在队条目 - 新增 `remove_pending_turn`:typed 结果 `Removed | AlreadyDispatched | NotFound`,在临界区里取走产物并追加 `queue.removed{cancelled}` - 新增 `claim_pending_turn`:同一临界区取队首 → 登记占用 → 追加 `turn.started` 与 `queue.removed{dispatched}`,已有未收口回合或队列为空时返回 `None` - `mark_queue_events_cleanable` 跳过仍挂着产物的条目,保证在队条目永远不会被回收 - 新增薄包装 `enqueue_direct_pending_turn` / `remove_direct_pending_turn` / `claim_direct_pending_turn`,并补 5 条单测覆盖 bootstrap 可见、幂等、容量、放行原子性与三种取消结果1 ·
apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts:286· maintainability/low · 已修(3e8954548)hostQueue.cancel在队里找不到身份时,只要running === true就回alreadyDispatched。remove_pending_turn是按身份判的(只有正在跑那一轮的active_turn.turn_id相同才算已放行)。替身拿"线程上有任意一轮在跑"近似,会让一个从没入队过的 id 也被答成已放行,掩盖notFound这条界面路径。dispatchedClientTurnId,只有它相等才回alreadyDispatched。2 ·
apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs:4527· bug/high · 已修(51e481134)direct_stale_turn_for_release先取一次锁做前置校验(确实登记着一轮、传了身份就必须一致、NeverReachedExecutor还要过启动窗口),返回client_turn_id;再complete_direct_thread_turn取第二次锁,complete_turn不校验 token,无条件把active_turn = None并追加turn.completed{aborted}。turn.started),这次释放会清掉新那一轮的占用,并给已经结束的老回合补一条假终态——正是这条路径本来要防的"多轮同时在跑 / 终态张冠李戴"。DirectThreadManager::release_stale_turn(thread_id, expected_client_turn_id, min_age_ms, now_ms) -> DirectStaleTurnRelease,同一个临界区里完成"校验 → 解除占用 → 写 aborted 终态"。判据不过时(NoActiveTurn/AnotherTurnRunning/WithinStartupWindow)占用被放回、一个字都不写。direct_stale_turn_for_release只剩"调用原子出口 + 把 typed 结果翻成人话":三个失败分支的文案与对外语义一字不改;兜底终态改由 Thread Manager 写(身份取被释放那一轮自己的clientTurnId,与放行时turn.started同一口径),app-server 侧的建事件助手与它的专属测试一并删掉。stale_release_validates_and_releases_as_one_action(拒绝时占用与事件都不动;通过时占用解除与终态一起落地;释放后立刻可认领下一条);运行时侧原用例补上"① ② 拒绝路径事件流为空 / ③ 占用与终态同批落地、身份正确 / ④ 释放后 thread 立刻腾出来"。3 ·
apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs:3303· documentation/low · 已修(20126ead0)4 ·
apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts:525-526· bug/medium · 不修(你的判断,2026-09-30)startTurn的finally里endCommandInFlight()在enqueue_direct_codex_turn一返回就放掉忙态;pendingTurns/turnRunning要等宿主queue.enqueued被consume()折进 reducer 才有值。displayBusy = nativeRunning || commandInFlight || pendingCount > 0。5 ·
apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts:308· bug/low · 已修(f4558475a)cancelPendingTurn只把结果说给用户,从不在pendingRunAnalyticsRef里删掉被取消那一条的句柄;而取消掉的待发消息永远不会放行,也就永远不会有自己的turn.completed。settlePendingRunAnalytics在"身份缺失"时会退一步挑最早那一条,被取消的句柄会顶替掉真正该结算的那一轮,而且那一轮再也没有第二次结算机会。remove_direct_project_pending_turn返回removed时删掉该clientTurnId的句柄。6 ·
apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts:339-344· style/low · 已修(25cec7955)notFound? … :alreadyDispatched? … : '')。if / else if / else,文案与行为一字未改。7 ·
apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs:115-123· bug/medium · 已修(75f9bbacb)queue_has_room)只发生在enqueue_direct_pending_turn里,而它在prepare_new_web_project_at(...).await之后。npm ci+ Vite 构建),会在磁盘上留产物;一条注定被QueueFull拒绝的消息先付了这份代价。direct_pending_turn_count),满队直接返回QueueFull;权威判据仍是入队临界区里的那一次,这里只求早失败。8 ·
apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime/user_input.rs:67-68· bug/medium · 已修(9f04940c6)recover_direct_taonier_regeneration_workflow_at(direct_runtime/mod.rs:1072)无条件先取任务锁direct-codex-art(acquire_..._with_wait:100 次 × 10ms 的std::thread::sleep),然后才读 workflow 状态决定要不要恢复。HostStateUnavailable("Agent Runtime 正在执行该 Agent 的其他任务:direct-codex-art")拒绝——而队列本该收下这条消息。direct_taonier_regeneration_workflow_requires_recovery_at),不需要恢复就直接返回(不取锁、一个字节都不写);真要动手才取锁,并在锁下重读一次判据(以锁下那一份为准)。工作流侧车是"临时文件 + rename"落盘,无锁读最坏读到上一版,不会撕裂。recovery_skips_the_executor_task_lock_when_nothing_needs_recovery——锁被别人的整包重生成占着时,没有工作流 / 正在正常进行的InProgress都必须Ok(false)(撤掉修复就会红,报的正是 review 描述的那句话),真需要恢复(Resetting)时仍然取锁并如实报错。9 ·
apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts:603-606· maintainability/low · 已修(a83ed0131)runTurn的 catch 里,同一个判据projectPathRef.current !== nextProjectPath写了两遍。captureAgentRuntimeError与两个纯文案函数),没有await;projectPathRef.current只在渲染时赋值,所以第二遍判断不可能成立,是死代码。(我先前一度以为它可达,重读确认 review 是对的。)await之后那一遍,删掉后面那遍。10 ·
apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectTurnStatus.ts:61· bug/medium · 已修(e22154394,按你同意的canStopTurn)displayBusy = nativeRunning || commandInFlight || pendingCount > 0。composer 读busy决定显示发送钮还是终止钮;而cancelTurn的受理判据是commandInFlightRef.current || currentTurnRunning。两处口径不一致:只有pendingCount撑起忙态时,会出现一个点了只会报「当前没有正在运行的回合,无法终止。」的终止钮。active_turn(这个 thread 上有没有未收口的回合)+ 事件列表本身就是队列(pending挂在queue.enqueued上)。它只写四种事件:turn.started/turn.completed/queue.enqueued/queue.removed{reason}。consume())= 纯传输:只有"有增量"这一条通知,不存状态。conversation/directThreadChat.ts)= 事件流的投影:turnRunning(nativeRunning的来源)、turnUserItemId(本轮开口条目身份)、completedTurnCount(单调计数,用来识别"又有回合收口"——下降沿在同一次 consume 里开始并结束时会丢)、pendingTurns(队列投影,界面不存在第二份队列)、live/history(条目)。commandInFlight(只有 IPC 在飞这一段)、turnCancelling(终止请求在飞)。useDirectProjectTurnStatus)= 给组件的一个对象:displayBusy是上面三层的并集,卡片与 composer 都读它。nativeRunning= "宿主已经放行了",pendingCount= "在队、还没放行",commandInFlight= "命令还没落地"。它们都不是"这一轮占用没有"本身——那个事实只在宿主 Thread Manager 里。canStopTurn = nativeRunning || commandInFlight与共享的canStopDirectProjectTurn;composer 的发送钮 / 终止钮按canStopTurn换(busy继续管模型 / 附件 / 语音那些与当前这轮冲突的控件);cancelTurn的受理判据也改用同一个函数——显示与受理同源。displayBusy保持三者并集(卡片仍要早于首个 token 显示"正在处理")。directProjectTurnStatus补canStopTurn用例(队列非空 →displayBusy真、canStopTurn假);appSurface 补"只有待发消息、没有回合在跑时不显示终止钮",撤掉修复会红。turn.started在同一个临界区里发,前端通常看不到这段空窗,所以更像短窗口的显示不一致,而不是常驻的错误按钮。