跳到正文
FunCoding

搜索

搜索文档、Skill 和 MCP

SSE 重连、Epoch 与重同步

保留事件游标和 bus epoch,处理四类缺口并区分历史裁剪。

SSE replay 是有界恢复,不是完整持久历史。保存最后收到的 id 和响应中的 X-Qwen-Event-Epoch,重连时一起发送,才能识别数字序号重用后的旧状态。

基础行为

Last-Event-ID结果
不提供仅 live,不回放
0请求当前 ring 中可用历史,仍受窗口与字节预算限制
合法 N 且有连续后续回放 id>N,然后进入 live
非纯十进制、负数、小数或超安全整数忽略无效游标并从 live 开始,写诊断

默认 ring 深度 8000。重放阶段结束会发无 id 的 replay_complete,即使 replayedCount 为 0;它只表示该次重放已结束,不表示没有丢历史,也不自动解除 awaitingResync。

四类重同步原因

reason触发与帧顺序
epoch_resetepoch 不同,或 N>=nextId;先发信号,再尝试回放当前 ring
seeded_replay_not_in_ring序列被恢复历史推进但 live ring 为空,N 仍落后;需 load 取 seeded 历史
ring_evictedring 最早 id>N+1;信号后只能回放仍保留的后缀
replay_budget_exceeded回放前缀后到达字节预算;再发信号,随后 replay_complete

epoch token 不同会带 detail:epoch_mismatch。仅比较 N 与 nextId 不够:新 bus 数字已经追上旧 N 时,仍需 token 识别跨代游标。

当前源码的 replayBudgetBytes 默认 8 MiB,至少允许一个符合条件的事件,即使单帧本身超过预算。这是内部有界策略,不是可由 HTTP 客户端无限提高的历史下载配额。

同一订阅可先收到 epoch/ring 类信号,之后再收到 replay_budget_exceeded。所有这些信号都不关闭流,后续帧仍会到达。

状态恢复顺序

会话 reducer 收到 state_resync_required 后设置 awaitingResync,跳过普通 delta;终止、再次 resync、session_snapshot 和 recording_degraded 等安全状态仍可处理,游标也可能继续推进。

宿主应读取 load 的有界 replay snapshot,按自身状态策略 reset/rebuild。共享 transcript store 的 clearAwaitingResync 必须早于新 SSE/replay 开始传递增量;不能等重放结束后才解除,否则重放已被跳过。

session view reducer 与 UI transcript store 是两层状态,不要只清一层就假定全部恢复。参见共享 UI。

History truncation 是提示,不是重同步命令

compactedReplay 开头的 history_truncated 表示较早的 replay 窗口已裁剪。按 fullTranscriptAvailable 决定是否可以调用持久 transcript 分页,同时正常应用保留下来的 replay,不再触发恢复循环。

可选 recordId 是向前分页的 beforeRecordId 锚点。首次裁剪时确定后应保持固定,不能随新增事件向已展示内容移动,否则前插历史会重复。

浏览器连接方式

原生 EventSource 能跟踪 id 并重连,但不能设置自定义 Authorization header。需要 bearer 时使用 fetch 加 SSE parser,SDK 的 REST SSE transport采用这一方式。

流中的 retry:3000 为兼容 EventSource 提供 3 秒重连建议,不等同于应用层所有错误都应每 3 秒无限重试。