feat(web,ws): 会话级消息缓存 + 图桶 + 忙态按会话过滤(裁决 16.4.5/16.4.2 + 修 16.4.1 的帧缺口)
- 16.4.5:非激活会话的帧**不再丢弃**,按自己的 sessionId 落进该会话桶(新增 store/sessionCache.ts 纯函数 + useAppStore.sessionMessages/flowGraphs);切会话改为「存回/取出」而非清空;流式增量 O(1) 定位桶、 只延长已存在的助手气泡(不凭一帧 delta 凭空造消息) - 16.4.2:图按会话入桶(上限 12 个),Backthink 页取 sess-backthink 的图而不再被用户会话图顶替; 空态区分「实时通道未连接」与「尚未产生链路图」;wire_runtime 的图源白名单注释按裁决口径重写(零行为改动) - 修 16.4.1 的缺口:handlers/session.ts 补 `session_idle` / `session_interrupted`(此前落到未知帧兜底 ⇒ sessionBusy 可能卡 true ⇒ 预约永不落地);忙态**置忙与置闲两侧**统一按 frameIsActive 过滤(只过滤置闲 会让后台会话的忙帧点亮徽标而它自己的 idle 被丢 ⇒ 永久卡忙) - 清理:删除 lib/streamScope.ts(口径已被 16.4.5 推翻)与只剩测试在用的 messageEventApplies - 复核:tsc 0 错 + vite build 成功;messageSync 69 / session 19 项逻辑测试;4 条变异反证
This commit is contained in:
1 parent
f454273c5a
commit
b480b04dd0
15 files changed
+733
-300
No files matched your search
@@ -1,27 +1,26 @@
|
||||
import { streamFrameApplies } from "../../streamScope";
|
||||
import type { FrameEvent, FramePayload, StoreState } from "./shared";
|
||||
import { frameIsActive, frameSessionId, type FrameEvent, type FramePayload, type StoreState } from "./shared";
|
||||
|
||||
/** 流式正文帧处理组:stream_chunk / stream_end / message.start / message.delta /
|
||||
* message.end。命中(含组内主动丢弃)返回 true,未命中返回 false 交给下一组。 */
|
||||
* message.end。命中(含组内主动丢弃)返回 true,未命中返回 false 交给下一组。
|
||||
*
|
||||
* 裁决 16.4.5:帧按自己的 `sessionId` 落进对应会话的消息桶,**不再**因为不属于
|
||||
* 激活会话而被丢掉 —— 后台会话的正文继续累积,切过去就能看到完整的这一轮。 */
|
||||
export function handleStreamEvent(ev: FrameEvent, r: FramePayload, s: StoreState): boolean {
|
||||
switch (ev.type) {
|
||||
case "stream_chunk": {
|
||||
if (!streamFrameApplies(ev, r, s.activeSessionId)) return true;
|
||||
if (r && typeof r.delta === "string") s.appendChunk(r.delta);
|
||||
if (r && typeof r.delta === "string") s.appendChunk(r.delta, frameSessionId(ev, r));
|
||||
return true;
|
||||
}
|
||||
case "stream_end": {
|
||||
// Another session's turn ending must not close the streaming bubble on
|
||||
// screen or clear this conversation's busy badge (both are single-slot
|
||||
// state: the last message's flag, and one shell-wide `sessionBusy`).
|
||||
if (!streamFrameApplies(ev, r, s.activeSessionId)) return true;
|
||||
s.endStream();
|
||||
// The assistant reply stopped streaming, so the immediate turn is over:
|
||||
// close the busy/pending window so the badge does not stick. If the backend
|
||||
// keeps a task/subagent running it re-emits session_busy (design/15 §5).
|
||||
s.setSessionBusy(false, 0);
|
||||
if (r && typeof r.stopReason === "string") {
|
||||
s.setContext({ lastAgentMode: s.context.lastAgentMode });
|
||||
const sid = frameSessionId(ev, r);
|
||||
s.endStream(sid);
|
||||
// 忙态 / 上下文是**单槽全局**状态(design/15 §5),所以只有屏幕上这个会话的
|
||||
// 回合结束才收口;别的会话结束只能收它自己气泡的流,不能清掉这里的徽标。
|
||||
if (frameIsActive(sid, s.activeSessionId)) {
|
||||
s.setSessionBusy(false, 0);
|
||||
if (r && typeof r.stopReason === "string") {
|
||||
s.setContext({ lastAgentMode: s.context.lastAgentMode });
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -32,8 +31,7 @@ export function handleStreamEvent(ev: FrameEvent, r: FramePayload, s: StoreState
|
||||
return true;
|
||||
}
|
||||
case "message.delta": {
|
||||
if (!streamFrameApplies(ev, r, s.activeSessionId)) return true;
|
||||
if (r && typeof r.delta === "string") s.appendChunk(r.delta);
|
||||
if (r && typeof r.delta === "string") s.appendChunk(r.delta, frameSessionId(ev, r));
|
||||
return true;
|
||||
}
|
||||
case "message.end": {
|
||||
|
||||
Reference in new issue
Block a user