Feat/ 待发送消息从前端队列移入rust #514

Merged
k88936 merged 32 commits from feat/msg-queue-to-rust into master 2026-09-30 17:26:08 +08:00
Member

closes #499
沿用thread manager, 状态存在日志里

closes #499 沿用thread manager, 状态存在日志里
k88936 added 3 commits 2026-09-24 21:25:18 +08:00
- 新增 ADR:命令=入队、放行归 Thread Manager、待发消息队列作为运行态事件归宿主
- 新增实施计划:五步落地顺序、标识符映射表、每步不变式与验收证据
- CONTEXT.md:接单/拒单词条替换为入队/入队失败/放行,新增待发消息队列与待发消息词条
- docs/README.md 补两条索引;decision-log.md 追加同日决策记录
- 新增 `DirectThreadEvent::QueueEnqueued`(`queue.enqueued`,带 canonical 用户条目与可选 `creationType`,不带 prompt)与 `QueueRemoved`(`queue.removed`,`reason` 为 typed 枚举 `cancelled | dispatched`)
- 新增 typed 枚举 `DirectQueueRemovalReason` 与 `DirectQueueRemovalOutcome`,并重跑 ts-rs 绑定
- 新增 `agent/direct_thread_queue.rs`:`PendingDirectTurn`(入队时冻结 canonical 形状与 prompt)、上限 `MAX_PENDING_DIRECT_TURNS = 5`、`EnqueueOutcome` / `EnqueueRejection`
- Thread Manager 的 `observe_event` 认队列事件:在队期间的 `queue.enqueued` 不可回收,`queue.removed` 把它转成可回收,新订阅者的 bootstrap 因此天然看得见当前队列
- `DirectCodexUserItem` 及其子类型补 `PartialEq`,`DirectThreadEvent` 不再需要 `Eq`
Thread Manager:待发消息队列的入队 / 取消 / 放行认领
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m31s
Project CI / Backend tests (pull_request) Successful in 4m1s
Project CI / Frontend tests (pull_request) Successful in 2m5s
Project CI / Native shell tests (pull_request) Successful in 6m7s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m0s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m3s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m52s
Project CI / Repository checks (pull_request) Successful in 2m17s
053e0bd932
- `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 可见、幂等、容量、放行原子性与三种取消结果
Author
Member
  • 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),这次释放会清掉新那一轮的占用,并给已经结束的老回合补一条假终态——正是这条路径本来要防的"多轮同时在跑 / 终态张冠李戴"。
    • 修法(已做,按你选的"新增一个原子方法"):Thread Manager 新增 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)

    • 现状:注释写"用户条目由放行半在放行之后、起 codex 之前落盘"。
    • 问题:这套词表里的两个角色是"入队 / 放行"(ADR 口径),"放行半"只在注释里出现,读起来像错字。
    • 修法(已做):连同另外 4 处注释与实施计划文档里的同一措辞一起改成"入队侧 / 放行侧",只改措辞。
    • 判断:有效(纯可读性,无行为变化)。
  • 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。
    • review 的说法:命令返回与事件到达之间,三个判据可能同时为 false,发送钮短暂回到手上,快速第二次提交会再入队一条。
    • 结论:不改。这一小段空窗不丢消息、也不会并发跑两轮(放行仍由 Thread Manager 串行),最坏结果是用户多插一条自己并不想发的排队消息。要消掉它得再引入一层"本地在飞待确认"状态,而 ADR 明确"命令只等到入队"——收益不抵复杂度。
  • 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 的句柄。
    • 测试:新增一条 appSurface 用例,撤掉修复后确实会红(回放一条没有身份的失败终态时,兜底结算必须落到还能收口的那一条,而不是被取消的那条)。
    • 判断:有效。
  • 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 之后。
    • 问题:工程准备是分钟级的活(新 Web 工程要 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 状态决定要不要恢复。
    • 问题:正在跑的那一轮整包重生成会整轮持有这把锁,于是整包重生成期间入队先同步阻塞约 1 秒,再以 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)时仍然取锁并如实报错。
    • 实施计划补了一行说明,与 ADR §6"入队不取整轮持有的锁"对齐。
  • 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 撑起忙态时,会出现一个点了只会报「当前没有正在运行的回合,无法终止。」的终止钮。
    • 分层关系(回答"怎么这么多状态"):
      • 宿主(Rust Thread Manager)= 唯一事实:active_turn(这个 thread 上有没有未收口的回合)+ 事件列表本身就是队列(pending 挂在 queue.enqueued 上)。它只写四种事件:turn.started / turn.completed / queue.enqueued / queue.removed{reason}。
      • 桥(Tauri notify + 前端 consume())= 纯传输:只有"有增量"这一条通知,不存状态。
      • reducer(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 补"只有待发消息、没有回合在跑时不显示终止钮",撤掉修复会红。
    • 没做的另一个选项:给纯 pending 一个真动作(取消全部待发 / 逐条取消)——那是新的产品语义,要单独定;这次只把"按钮在、点不了"消掉。
    • 现状判断:这个状态真的可能出现,但要"队列非空 + 什么都没在跑"同时成立;正常路径上认领队首与 turn.started 在同一个临界区里发,前端通常看不到这段空窗,所以更像短窗口的显示不一致,而不是常驻的错误按钮。
- [x] 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`。 - 判断:有效。 - [x] 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`),这次释放会清掉**新那一轮**的占用,并给已经结束的老回合补一条假终态——正是这条路径本来要防的"多轮同时在跑 / 终态张冠李戴"。 - 修法(已做,按你选的"新增一个原子方法"):Thread Manager 新增 `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 立刻腾出来"。 - [x] 3 · `apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs:3303` · documentation/low · 已修(`20126ead0`) - 现状:注释写"用户条目由**放行半**在放行之后、起 codex 之前落盘"。 - 问题:这套词表里的两个角色是"入队 / 放行"(ADR 口径),"放行半"只在注释里出现,读起来像错字。 - 修法(已做):连同另外 4 处注释与实施计划文档里的同一措辞一起改成"入队侧 / 放行侧",只改措辞。 - 判断:有效(纯可读性,无行为变化)。 - [x] 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`。 - review 的说法:命令返回与事件到达之间,三个判据可能同时为 false,发送钮短暂回到手上,快速第二次提交会再入队一条。 - 结论:**不改**。这一小段空窗不丢消息、也不会并发跑两轮(放行仍由 Thread Manager 串行),最坏结果是用户多插一条自己并不想发的排队消息。要消掉它得再引入一层"本地在飞待确认"状态,而 ADR 明确"命令只等到入队"——收益不抵复杂度。 - [x] 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` 的句柄。 - 测试:新增一条 appSurface 用例,撤掉修复后**确实会红**(回放一条没有身份的失败终态时,兜底结算必须落到还能收口的那一条,而不是被取消的那条)。 - 判断:有效。 - [x] 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`,文案与行为一字未改。 - 判断:有效。 - [x] 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` **之后**。 - 问题:工程准备是分钟级的活(新 Web 工程要 `npm ci` + Vite 构建),会在磁盘上留产物;一条注定被 `QueueFull` 拒绝的消息先付了这份代价。 - 修法(已做):前置条件之后、工程准备之前先做一次容量预判(新增只读的 `direct_pending_turn_count`),满队直接返回 `QueueFull`;权威判据仍是入队临界区里的那一次,这里只求早失败。 - 判断:有效。注意它只是快速失败:真到入队那一刻被别人的消息挤满,照样会在同一位置被拒(那条路径没变)。 - [x] 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 状态决定要不要恢复。 - 问题:正在跑的那一轮整包重生成会**整轮持有**这把锁,于是整包重生成期间入队先同步阻塞约 1 秒,再以 `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`)时仍然取锁并如实报错。 - 实施计划补了一行说明,与 ADR §6"入队不取整轮持有的锁"对齐。 - [x] 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` 之后那一遍,删掉后面那遍。 - 判断:有效。 - [x] 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` 撑起忙态时,会出现一个点了只会报「当前没有正在运行的回合,无法终止。」的终止钮。 - **分层关系(回答"怎么这么多状态")**: - **宿主(Rust Thread Manager)= 唯一事实**:`active_turn`(这个 thread 上有没有未收口的回合)+ 事件列表本身就是队列(`pending` 挂在 `queue.enqueued` 上)。它只写四种事件:`turn.started` / `turn.completed` / `queue.enqueued` / `queue.removed{reason}`。 - **桥(Tauri notify + 前端 `consume()`)= 纯传输**:只有"有增量"这一条通知,不存状态。 - **reducer(`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 补"只有待发消息、没有回合在跑时不显示终止钮",撤掉修复会红。 - 没做的另一个选项:给纯 pending 一个真动作(取消全部待发 / 逐条取消)——那是新的产品语义,要单独定;这次只把"按钮在、点不了"消掉。 - 现状判断:这个状态真的可能出现,但要"队列非空 + 什么都没在跑"同时成立;正常路径上认领队首与 `turn.started` 在同一个临界区里发,前端通常看不到这段空窗,所以更像短窗口的显示不一致,而不是常驻的错误按钮。
k88936 added 18 commits 2026-09-30 13:14:18 +08:00
- 命令改名 enqueue_direct_codex_turn:跑完入队检查后只入队,返回结构化的入队失败载荷
- direct_turn_accept.rs 改为 direct_turn_dispatch.rs:放行占用对象改为接管 Thread Manager 认领好的那一轮
- 新增 kick_direct_queue_dispatch:入队、占用释放与中止路径共用同一个幂等放行点
- 放行之后取调用身份、落盘用户条目、下发条目、跑整轮,失败一律收口成回合失败
- 新增 remove_direct_project_pending_turn 命令,取消一条还没放行的待发消息
- 队列条目补 analytics_attempt_id:埋点身份跟着入队走,放行不重算
- DirectTurnRejection 改名 DirectTurnEnqueueFailure,新增 QueueFull 变体
- 退役 accept_turn / accept_direct_thread_turn,测试改用 accept_for_test 建占用
- 前端与生成绑定同步改名,DirectTurnRejection.ts 换成 DirectTurnEnqueueFailure.ts
- 新增 conversation/directPendingTurns.ts:入队/移除两个纯函数,不判上限、不排期、不排序
- reducer 新增 queue.enqueued / queue.removed 分支,折成 pendingTurns 投影
- chip 文案派生搬到 pendingTurnChipLabel.ts,删除 chatComposerQueue.ts 整套本地队列实现
- controller 退役 queuedTurns / queueSequenceRef / completionPendingRef / busyBaselineTurnCountRef
  与看门 counts effect;取消 chip 改调 remove_direct_project_pending_turn 按 typed 结果说话
- 埋点句柄按 clientTurnId 存表,回合终态按本轮开口条目身份结算
- turnBusy 改名 commandInFlight 并收窄成 IPC 在飞;忙态 = 原生在跑 ∨ IPC 在飞 ∨ 待发消息非空
- 提交改成等命令的入队结果:用户自己能改的入队失败保留草稿,其余按已交出处理
- 测试:队列投影单测、入队只发一次 IPC、IPC 在飞挡住第二次提交、满队保留草稿、取消已放行条目
- 文档:实施计划标注第 0–3 步落地并补第 3 步细则,ADR 的埋点与退役面口径改成落地后的写法
- cli.rs 删 CliCommand::DirectCodexChat 变体、project_path_mut 分支、--direct-codex-chat 解析与派发
- direct_runtime/mod.rs 删 run_direct_game_creator_turn_at 与 run_direct_game_creator_turn_at_with_creation_type
- 删 scripts/direct-execution-production-fixture.mjs(CLI 唯一消费者)
- user_input.rs 模块注释改成"唯一的命令入口",不再承诺 CLI 分工
- 顺带删掉只剩测试在用的 direct_turn_error_boundary_text,判据只剩 direct_turn_enqueue_failure 一处
- 三条命令边界测试改打 direct_turn_enqueue_failure(...).message,覆盖不变
- 文档:09-22 里程碑把 --direct-codex-chat 从"范围外(保留)"改成退役项,两份 Direct 技术方案的 CLI 承诺删掉,
  09-23 ADR 的"CLI 保持 await"标为口径反转,decision-log 追加口径修正,实施计划标注第 4 步落地
- ADR §9 记录核实结论:取消兜底本来就同时释放占用、补写 turn.completed 并踢队列,删守卫不需要新增无条件终态;连接死亡的失败事实由 app-server 连接层落地
- 实施计划第 5 步改写:60 秒启动窗口闸门改挂 Thread Manager 的占用登记年龄,读者改读 direct_active_turn_id_at,TurnAlreadyRunning 一并删
- decision-log 追加 2026-09-30 条目,记录核实过程、决策与影响范围
- 新增 Thread Manager 的只读身份入口:DirectTurnIdentity + read_direct_turn_identity,占用仍只由终态出口解除
- 五个身份读者(执行会话、工具桥、校验预约、上下文预取、付费美术重生成)改读 direct_active_turn_id_at
- 删 DirectTaonierActiveInvocationGuard / DIRECT_TAONIER_ACTIVE_INVOCATIONS / DirectActiveTurnView 与只读探测
- 删 release_stale_direct_taonier_active_invocation,改为 direct_stale_turn_for_release 只做前置校验(身份一致 + 启动窗口),释放仍由 complete_direct_thread_turn + kick_direct_queue_dispatch 完成
- DirectStaleTurnReleaseReason 取代 DirectTaonierStaleGuardReason;终止兜底的注释与文案改成占用口径
- 放行不再另取调用身份:认领时已在 Thread Manager 登记,删掉认领后的守卫进入与失败分支
- 新增前置判据用例 stale_cancel_releases_the_occupancy_and_dispatches_the_next_pending_turn,并补背景拨时测试钩子
- 集成用例改用 DirectTurnReservation::accept_for_test 建占用,不再进守卫
- 删 DirectTurnError::TurnAlreadyRunning 变体,以及 is_reportable / Display 的对应分支与专属用例
- 前端 directTurnEnqueueFailureNotice 删掉 turnAlreadyRunning 分支;ts-rs 绑定同步删除该变体
- 结构化载荷用例的样本换成 queueFull,本地说明用例的文案样本换成队列已满提示
- ADR 状态改为 2026-09-30 落地完成,§9 守卫退役条目改成已落地口径
- 实施计划状态改为第 0–5 步全部完成,进度表第 5 步写成落地说明;第 4 步的 TurnAlreadyRunning 交还给第 5 步
- 09-23 接单化计划加后续注记:接单/拒单词表与 TurnAlreadyRunning 已退役,正文不再回改
- decision-log 的 09-30 条目补落地与验证结果,09-24 条目加落地指向
- hostQueue 记录被放行的 clientTurnId,cancel 只在身份一致时回 alreadyDispatched,其余回 notFound
- 与宿主 remove_pending_turn(按 active_turn.turn_id 比对)保持一致,避免复用该替身时掩盖 notFound 分支
- 控制器在 `remove_direct_project_pending_turn` 返回 `removed` 时删除该 `clientTurnId` 的埋点句柄
- 被取消的待发消息永远不会放行、也就不会有自己的 `turn.completed`,句柄留着只会泄漏
- 留着还会挤进"身份缺失"的兜底结算候选(挑认领表里最早那一条),把下一次结算算到没跑过的回合头上
- 补一条 appSurface 用例钉住这条不变式:回放没有身份的失败终态时,兜底结算只落到还能收口的那一条
- `codex_app_server/mod.rs` 两处、`direct_runtime/mod.rs`、`direct_thread_queue.rs`、`direct_runtime/user_input.rs` 各处的简体表述统一
- 同步实施计划文档里的同一处措辞,保持词汇一致
- 只改措辞,不改任何行为
- `cancelPendingTurn` 里三路结果(不在队 / 已放行 / 已取消)改用 if / else 表达
- 已放行那一路的说明注释挪到对应的分支上
- 不改任何文案与行为
- `runTurn` 的 catch 里同一判据写了两遍,两处之间只有同步代码、没有 await,`projectPathRef.current` 不可能变
- 保留 await 之后那一遍,删掉其后永远不会成立的第二遍
- 不改行为:守卫效果由前一处完整覆盖
- `direct_thread_manager` 新增只读的 `direct_pending_turn_count`(管理器 `pending_turn_count` + 模块级封装)
- `enqueue_direct_codex_turn_typed` 在前置条件之后、工程准备之前先做一次容量预判,满队直接返回 `QueueFull`
- 工程准备是分钟级、会在磁盘留产物的活,为注定收不下的消息先做它没有意义;权威判据仍是入队临界区里的检查
- 补一条管理器用例钉住这个只读口径:只数在队条目,认领 / 取消后立刻跟着变
- 新增 `late_subscriber_sees_the_running_turn_and_the_pending_queue_together`
- 钉住:正在跑那一轮的 `turn.started` 锚点(带开口条目身份)与还排着的 `queue.enqueued` 同时进 bootstrap,顺序按事件序
- 原有用例各覆盖一半(一条只测队列、一条只测锚点),这条覆盖两者同时存在的中途加入场景
- `directThreadChat.test.ts` 队列投影分组里新增一条:bootstrap 同时带 `turn.started` 锚点与两条 `queue.enqueued`
- 断言 `turnRunning` / `turnUserItemId` / `pendingTurns` 三者同时正确,而不是各测一半
- 只加用例,不改实现
- `DirectThreadManager::release_stale_turn` 在同一个临界区里做完"校验 → 解除占用 → 写 aborted 终态"
- 判据不满足(没有活动回合 / 身份对不上 / 没过启动窗口)一个字都不写,占用与事件都保持原样
- 拆成"先校验后释放"两次取锁会让并发认领插进中间,清掉新那一轮的占用并给老回合补假终态
- `direct_stale_turn_for_release` 降级成"调用原子出口 + 把结果翻成人话",文案与对外语义不变
- 兜底终态改由 Thread Manager 写,身份取被释放那一轮自己的 clientTurnId,删掉 app-server 侧的建事件助手
- 测试:管理器侧新增"校验与释放是一个动作"用例;运行时侧用例补上"拒绝时零写入 / 通过时占用与终态一起落地"
- `recover_direct_taonier_regeneration_workflow_at` 先做只读判据(复用 `direct_taonier_regeneration_workflow_requires_recovery_at`),不需要恢复就直接返回
- 真要动手才取 `direct-codex-art`,并在锁下重读一次判据(以锁下那一份为准)
- 原来无条件取锁:正在跑的整包重生成整轮持有它,入队会先同步阻塞约 1 秒再被 `HostStateUnavailable` 拒掉
- 实施计划补一行说明,与 ADR §6"入队不取整轮持有的锁"对齐
- 测试:新增用例"整包重生成在跑(锁被占)时不需要恢复的入队路径不取锁",撤掉修复会红
终止钮按 canStopTurn 显示:队列非空不再顶出终止按钮
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m24s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m8s
Project CI / Frontend tests (pull_request) Successful in 1m57s
Project CI / Repository checks (pull_request) Failing after 25s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m27s
Project CI / Native shell tests (pull_request) Successful in 5m47s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m38s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m57s
e221543947
- `useDirectProjectTurnStatus` 新增派生判据 `canStopTurn`(`nativeRunning || commandInFlight`)与共享的
  `canStopDirectProjectTurn`,写清它与 `displayBusy` 的分工:前者回答"终止这一脚踢给谁",后者回答"界面能不能声称空闲"
- composer 的发送钮 / 终止钮按 `canStopTurn` 换;`busy` 继续管模型 / 附件 / 语音这些与当前这轮冲突的控件
- `cancelTurn` 的受理判据改用同一个函数:显示与受理同源,不再出现"按钮在、点了说没有回合"
- `displayBusy` 保持三者并集不变(卡片仍要早于首个 token 显示"正在处理")
- 测试:`directProjectTurnStatus` 补 `canStopTurn` 用例;appSurface 补"只有待发消息、没有回合在跑时不显示终止钮"(撤掉修复会红)
k88936 added 2 commits 2026-09-30 13:45:39 +08:00
- direct_runtime/mod.rs:保留本分支「Thread Manager 是活动回合唯一事实源」(direct_active_turn_id_at),
  丢掉 master 的 DirectTaonierActiveInvocationGuard 第二份进程内表;master 的美术背景路由改动全部保留
- direct_thread_manager.rs:双方都保留——本分支的待发队列 / 容量上限 / 残留回合兜底释放,
  与 master 的 emit_direct_active_turns_changed 广播、unsubscribe、update_active_turn 返回「是否真的变了」
- useDirectProjectChatController.ts:保留本分支入队化逻辑,采纳 master 的 rawMessage 小重构与保活下沉
- direct_turn_dispatch.rs:会话保活改挂放行占用(DirectTurnReservation._session_keepalive),
  随占用释放即停,等价承接 master 放在调用身份守卫里的保活意图
- 保留本分支对 direct-execution-production-fixture.mjs 的删除(--direct-codex-chat CLI 已退役,夹具无入口)
- 连带退役 scripts/run-agc-direct-execution-fixture.mjs 与 npm run check:agc-direct-execution-fixture(只服务该夹具)
- 文档同步:ADR / 09-24 实施计划 / decision-log 记录上述退役,09-16 与 09-18 里程碑、pitfalls 的夹具命令标注为已退役
Merge remote-tracking branch 'origin/master' into feat/msg-queue-to-rust
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m25s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m14s
Project CI / Backend tests (pull_request) Successful in 4m1s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m40s
Project CI / Frontend tests (pull_request) Successful in 2m19s
Project CI / Native shell tests (pull_request) Successful in 6m4s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 9m52s
Project CI / Repository checks (pull_request) Successful in 2m37s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m12s
d538c6e9c9
k88936 added 1 commit 2026-09-30 14:37:28 +08:00
Merge branch 'master' into feat/msg-queue-to-rust
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 1m16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 1m16s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 1m16s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m49s
Project CI / Frontend tests (pull_request) Successful in 4m14s
Project CI / Repository checks (pull_request) Successful in 4m34s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m13s
Project CI / Backend tests (pull_request) Successful in 7m10s
Project CI / Native shell tests (pull_request) Successful in 7m58s
888c249ffd
k88936 marked the pull request as ready for review 2026-09-30 14:37:35 +08:00
k88936 added 8 commits 2026-09-30 16:55:55 +08:00
- ADR §3/§4/§5 与备选方案 6–8、影响与代价:删掉 StoredEvent.pending 的第二份产物,队列成员由事件折出;prompt 只在引用 part 上冻结算不出的片段(to_prompt_cache,宿主内存专用),canonical 形状与 prompt 放行时重投影;analyticsAttemptId 进 queue.enqueued
- 实施计划:状态改第 0–6 步、落地进度表补第 6 步、新增「第 6 步:条目不再另存产物」的实现点与验收
- decision-log:追加 2026-09-30 修订条目(背景、三条决策、埋点身份的口径)
- 四个 direct_* 文件移入 agent/thread_manager:wire.rs(运行态事件契约)、queue.rs(待发消息队列规则)、dispatch.rs(放行与占用)、mod.rs(管理器主体与对外入口)
- 类型去掉 Direct 前缀:ThreadEvent / ThreadManager / ThreadItem / ThreadFileChange / ThreadDeltaKind / ThreadRequestKind / PendingTurn / QueueRemovalReason / QueueRemovalOutcome / DispatchedTurn / TurnReservation / TurnIdentity / StaleTurnRelease / ConsumeResult / SubscriptionBootstrap / ActiveTurn / ActiveTurnSnapshot
- 对外函数改名:subscribe_thread / consume_thread / unsubscribe_thread / append_thread_event / enqueue_pending_turn / remove_pending_turn / claim_pending_turn / update_active_turn / list_active_turns / complete_turn / complete_turn_if_reserved / active_turn_id_at / read_turn_identity / stale_turn_for_release / thread_id_for_project
- 生成绑定跟着改名(DirectThreadEvent→ThreadEvent 等十个文件为 rename),前端八处 import 同步;Tauri 命令名与前端 IPC 契约不变
- 定向验证:cargo test agent::(945 passed,另两条并发用例为既有 flaky、重跑通过)、npx vitest run appSurface/directThreadChat/directHistoryPaging(247 passed)、typecheck 通过
- ADR §3/§4/§5:在队条目 = 事件窗口里“有 queue.enqueued、没有配对 queue.removed”的折叠;队列事件不带 prompt、不带埋点身份,也没有宿主私有的第二类字段
- ADR §5/§9:引用 part 的解析文本写进自己的 resolved_text 并随条目持久化(缺省合法、旧历史照样解析),放行/历史回读/turn input 共用;放行读 identity_generation、终态比对后直写成绩,删掉候选/settle 两阶段与 attempt id
- ADR 备选方案补 6–9 条(条目另存产物、整条 prompt、clientTurnId 兼作埋点身份、保留两阶段)与影响与代价(prompt 块顺序、埋点口径随放行搬家)
- 实施计划:新增第 6 步(条目不再另存产物)、第 7 步(埋点成绩归宿主),落地进度表与状态同步;补 2026-09-30 文件路径口径
- decision-log:追加并修正 2026-09-30 条目(resolved_text 持久化、埋点链退役、不用 clientTurnId 兼作身份)
- AgcResourceReference 增加 resolved_text:入队冻结时写入素材摘要 + UI 设计文档代码上下文,此后随条目持久化;缺省合法(旧历史照样解析),序列化时不写 None
- wire.rs 新增 freeze_direct_codex_user_item(校验 + 算片段 + 写盘 + 冻结进 part + 判空),direct_codex_user_item_to_prompt 改为只读条目自身事实的纯折叠(零 IO、零校验、无失败出口)
- wire 投影与 turn input 优先读 resolved_text,缺省才退回按当前 manifest 现算摘要;历史回读依旧不产生写副作用
- user_input.rs 入队侧改用 freeze,prompt 由冻结条目折叠得出
- 生成绑定 DirectCodexUserContentPart 增加可选 resolvedText
- analytics/run.rs:删 Request::DirectCandidate / Request::Settle 与 settle();direct_finished 改收放行代次,终态再读一次 identity_generation,不同则整条不记,相同则直接落 Request::Terminal
- analytics/store.rs:删 pending_runs / pending_run_bytes 与两条候选分支(连带 16 条上限),run_request 不再需要字节预算参数
- analytics/gui.rs + main.rs:删 settle_direct_run_analytics 命令与注册
- 放行侧:kick_queue_dispatch 在读 claim 之后读一次平台会话代次并随整轮传下去;user_input 命令不再收 analyticsAttemptId,队列条目不再持有埋点句柄
- 渲染层:删 beginDirectRunAnalytics、句柄表与结算 effect、settle 调用与命令参数;删除 tests/directRunAnalytics.test.ts,appSurface 两处断言同步
- 判据口径随放行搬家:从“发送时与终态同代”改成“放行时与终态同代”,入队后放行前的账号切换不再丢弃成绩
- 验证:cargo test -- analytics::(45 passed,含新增的直写与代际变化两条)、cargo test -- agent::(949 passed)、analytics_real_file_write 用例、npx vitest run tests/appSurface.test.ts(194 passed)、相关 chat/direct 单测 139 passed、typecheck 通过
- queue.rs:PendingTurn 退化成 queue.enqueued 的投影(new / from_event / enqueued_event 往返),删掉 canonical_user_item 与 prompt 两个宿主私有产物字段
- thread_manager/mod.rs:删 StoredEvent.pending(append_inner 并回 append);pending_turns 改为一次事件折叠(有 queue.enqueued、没有配对 queue.removed),容量、判重、remove、claim 全读这一次折叠;mark_queue_events_cleanable 删掉“产物还在就不回收”的防御
- dispatch.rs:放行的 canonical = serde_json::to_value(&pending.user_item),prompt = direct_codex_user_item_to_prompt(&pending.user_item)——不写盘、不读 manifest、不重跑校验
- user_input.rs:入队只造条目本身,不再冻结 prompt / canonical 形状
- wire.rs:queue.enqueued 注释改为“这条事件就是这条待发消息的全部事实”
- 验证:cargo test -- thread_manager::(62 passed,单线程跑进程级计数器用例)、-- agent::(948 passed,另一条 design_runtime 用例为既有并发 flaky,单跑通过)、-- direct_runtime:: analytics:: direct_codex_user_item(182 passed)、npx vitest run appSurface/directThreadChat/directHistoryPaging/directProjectTurn(249 passed)
- ADR 状态改为 2026-09-30 落地完成,§2 补入队侧的“用户条目校验与冻结”,§5 埋点段去掉渲染侧 discard 与句柄表措辞
- ADR 词表收口:并发拒单/放行补拒单出口/放行期拒单三处改成入队失败与放行失败口径
- ADR §9 与实施计划同步归位后的符号名:active_turn_id_at、stale_turn_for_release、kick_queue_dispatch、complete_turn
- 实施计划状态改为第 0–7 步全部落地,进度表第 6/7 步补落地说明与提交号,新增「2026-09-30 收口记录」
- 实施计划顶部路径口径补 thread_manager 之外去掉 direct_ 前缀的函数改名对照
- decision-log 2026-09-30 条目把“设计稿,尚未实施”换成落地提交链与验证结论,并补 09-24 条目指向落地证据
Merge remote-tracking branch 'origin/master' into feat/msg-queue-to-rust
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 1m9s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 1m11s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 1m12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m45s
Project CI / Frontend tests (pull_request) Successful in 4m25s
Project CI / Repository checks (pull_request) Successful in 4m34s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m15s
Project CI / Backend tests (pull_request) Successful in 7m11s
Project CI / Native shell tests (pull_request) Successful in 7m50s
8a4e3cbcdd
k88936 merged commit 9ee34809be into master 2026-09-30 17:26:08 +08:00
k88936 deleted branch feat/msg-queue-to-rust 2026-09-30 17:26:09 +08:00
Sign in to join this conversation.