feat(loop,gateway): 13.4.5 目标态 —— 回合内 steer(不打断、新消息进同一上下文流)+ steer_ack 帧
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

规格=design/15 末尾「13.4.5 目标态规格」(不变量 I1/I2/I3)。调研依据:豆包无公开技术文档(不编造);
同类成熟 Agent 产品无一做同会话并行回合,共同范式= queue + 回合内 steer(Claude Code / Cline PR #13958 /
Codex Queue·Steer / open-webui / LangGraph)。

- 注入点=**回合内边界**(上一模型步 + 它那批工具调用都已结束、下一次模型调用还没开始):
  driveTurn 每 step 组装前调用一次 SteerSource(run.go)——完成 step 只能靠工具批次推进,
  故唯一可注入处就是「工具批次结束之后」,循环里没有任何「起第二个回合」的分支(**I1 结构性成立**)
- **I2 恰好一次**:注入只进**本轮 view、绝不写回 msgs**(checkpoint 与记忆提取面保持干净);
  被注入的消息从 pending 移除,不会由自己的回合再答一次
- **I3 转向不取消工具**:注入调用点在 runToolCalls **之后**(此刻无工具在飞),循环从不因注入 cancel 任何 ctx;
  边界序列有测试钉死(恰为 [1 2 …],无批次中途值)
- 判定口径= **pending ∩ 转录**:候选是本会话 pending 条目,每个边界重读 history.Recent 验证已落库,
  按**行 id** 去重、按 pending FIFO 注入。**不用时间戳**(Windows time.Now() 步长 ~0.44ms,连发常同毫秒并列,
  任何 ms 基线都会漏注入或重复注入);跨重启不成立也不需要(在飞回合与 pending 都在内存)
- 新下行帧 `steer_ack`:`{type, sessionId, data:{messageId, atStep}}`,语义=「已被本回合吃进、尚未被正面回答」
  (驱动前端灰态);**不进 frameReachesAll** ⇒ fail-closed 只发 owner+admin(测试双钉);经 hub 广播、不经 bus
- 组合根**零改动**:per-turn 状态用 ctx 承载(loop.WithSteerSource),编排器在回合启动/续跑时挂
- 守卫:session_orchestrator.go 加完接线后 505 行 ⇒ 按职责纯搬出 session_events.go(常量取值未改、行为零变化)
- 复核:build/vet 绿 + **全树 0 FAIL** + linux/darwin 交叉编译绿;loop 6 例(含 3 子测)+ gateway 2 例 E2E;
  **4 条变异反证**(删 consumePending → 模型步数 4≠3;注入写回 msgs → 快照被污染;批次中途注入 → 边界 [1 1 2];
  把帧加进白名单 → viewer 收到)均已还原
- 未做(登记):IM 自有 Chatter 落库的消息不参与 steer(有意,避免重复作答);pending 条目无自落库 id
  ⇒ 配对用 (内容,图片) 逐行认领;同回合超 20 条用户消息时超出者留 pending 由各自回合作答(不丢)
This commit is contained in:
AskaEth committed 2026-10-04 14:53:40 +08:00
1 parent 8c6c796a29
commit 3d8822c922
9 files changed
+1306 -22

No files matched your search

@@ -0,0 +1,28 @@
package gateway
// 本文件归属编排器推给前端的每会话状态事件类型(design/15 §5/§2.1)。
// 它们从 session_orchestrator.go 拆出,使编排器本体只留消息状态机,并保持在
// 「单文件 ≤500 行」的硬约束内。帧名即契约:客户端按 type 分发,因此这些取值
// 绝不随重构改名(要改就是契约变更,须先登记)。
//
// 这里只放**状态机信号**(不含消息内容)。带内容的帧(message.*、steer_ack、
// chat_duplicate 等)由 design/11 §7.1 的帧门按 fail-closed 默认档只投给
// owner + admin,见 ws_frame_scope.go。
// 经 WS 通道推给前端的状态事件类型。每个的 data map 都
// 携带 sessionId,使客户端能按会话关联状态。
const (
EventSessionBusy = "session_busy"
EventPendingAck = "pending_ack"
EventPendingDrain = "pending_drain"
EventSessionIdle = "session_idle"
EventSessionInterrupt = "session_interrupted"
EventPendingFull = "pending_full"
// EventChatDuplicate 是一次重复到达的明确应答(G4 幂等):本条 clientMsgId
// 已有落库的消息,本次没有新回合、也不会有新帧。它的 data 携带
// {sessionId, clientMsgId, messageId},让客户端把本地乐观气泡对齐到既有
// 消息;没有发过这个 clientMsgId 的其他客户端自行忽略即可。它不是状态机
// 迁移,因此不在 design/11 §7.1 的「状态信号」白名单里 —— 帧门默认只把它
// 投给 owner/admin(fail-closed)。
EventChatDuplicate = "chat_duplicate"
)