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_reset | epoch 不同,或 N>=nextId;先发信号,再尝试回放当前 ring |
| seeded_replay_not_in_ring | 序列被恢复历史推进但 live ring 为空,N 仍落后;需 load 取 seeded 历史 |
| ring_evicted | ring 最早 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 秒无限重试。