import { turnsInSession } from "../../../store/turnCache"; import { frameIsActive, frameSessionId, frameTurnId, type FrameEvent, type FramePayload, type StoreState, } from "./shared"; /** 流式正文帧处理组:stream_chunk / stream_end / message.start / message.delta / * message.end。命中(含组内主动丢弃)返回 true,未命中返回 false 交给下一组。 * * 裁决 16.4.5:帧按自己的 `sessionId` 落进对应会话的消息桶,**不再**因为不属于 * 激活会话而被丢掉 —— 后台会话的正文继续累积,切过去就能看到完整的这一轮。 * * 忙时并行(design/15 §2.5):帧壳 `turnId` 决定它属于**哪一路**,增量按 * `(sessionId, turnId)` 二级分桶写进那一路的助手气泡;两条回复交错到达时不会糊成 * 一条(此前只按「列表尾气泡」写,正是把两条回复糊在一起的成因)。 */ export function handleStreamEvent(ev: FrameEvent, r: FramePayload, s: StoreState): boolean { switch (ev.type) { case "stream_chunk": { if (r && typeof r.delta === "string") { s.appendChunk(r.delta, frameSessionId(ev, r), frameTurnId(ev, r)); } return true; } case "stream_end": { const sid = frameSessionId(ev, r); const turnId = frameTurnId(ev, r); s.endStream(sid, turnId); // 忙态 / 上下文是**单槽全局**状态(design/15 §5),所以只有屏幕上这个会话的 // 回合结束才收口;别的会话结束只能收它自己气泡的流,不能清掉这里的徽标。 if (frameIsActive(sid, s.activeSessionId) && !otherLegsRunning(s, sid, turnId)) { s.setSessionBusy(false, 0, turnId ? 0 : undefined); if (r && typeof r.stopReason === "string") { s.setContext({ lastAgentMode: s.context.lastAgentMode }); } } return true; } // Structured stream events (design/04 §1.1, aligned with OpenClaw). These // arrive in place of the legacy stream_chunk when the loop streams events. case "message.start": { // The assistant bubble is already opened by useChat; nothing to do here. return true; } case "message.delta": { if (r && typeof r.delta === "string") { s.appendChunk(r.delta, frameSessionId(ev, r), frameTurnId(ev, r)); } return true; } case "message.end": { // Body-text segment closed; the turn may continue with tool calls, so we // do NOT end the stream here (stream_end owns that). return true; } default: return false; } } /** 除了刚收尾的这一路,该会话还有别的路在跑吗?有就**不能**把忙态置闲 —— 并行时 * 先答完的那一路会把「她正在忙」徽标误关掉(v0.65 §4.6 的 `session_idle` 时点收紧 * 就是为这个)。没有 `turnId` 的旧帧(单路老后端)逐字沿用旧行为:照样清忙。 */ function otherLegsRunning(s: StoreState, sessionId: string | undefined, turnId: string | undefined): boolean { if (!turnId) return false; const sid = sessionId ?? s.activeSessionId; return turnsInSession(s.turnBindings, sid).some((b) => b.turnId !== turnId); }