Skip to content
FunCoding

Search

Search docs, Skills and MCP

SSE 重连、Epoch 与重同步

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

This page has not been translated into English yet. The original Chinese version is shown below.

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 秒无限重试。