Run SSE 重连与事件续播
wesgine 的 Run 事件通过 SSE(Server-Sent Events)推送到客户端。当 SSE 连接断开时,客户端可以通过 from_seq 参数从断点续播事件。
核心设计
Run 与 SSE 解耦(INV-RUN-DETACH)
Run 生命周期始终与传输层(SSE/HTTP 连接)解耦:
- SSE 断开 ≠ 取消 Run
- Run 继续在服务端执行
- 客户端可重连续播
事件序列号
每个 Run 事件携带递增序列号 seq:
{
"type": "stream_delta",
"seq": 42,
"data": { "text": "..." }
}
重连流程
1. 初始连接
POST /cells/{id}/run
→ SSE 流开始
← event: stream_delta (seq=1)
← event: tool_start (seq=2)
← event: tool_end (seq=3)
← ...
2. 连接断开
网络中断 / 客户端刷新
SSE 连接关闭
Run 继续执行(服务端)
3. 重连续播
GET /cells/{id}/runs/{runID}/events?from_seq=3
→ SSE 流从 seq=4 开始
← event: stream_delta (seq=4)
← event: done (seq=5)
RunEventBuffer
服务端为每个活跃 Run 维护事件缓冲区:
| 属性 | 说明 |
|---|---|
| 容量 | 按 Run 配置 |
| 生命周期 | Run 存活期间 |
| 所有者 | Run 的 Actor |
| 访问控制 | observe.RunVisible 判定 |
Actor 边界(INV-OBS-10)
事件缓冲区记录 Run 的 owner:
- 重连时校验请求者的 Actor
- 非 owner 的重连请求与 Run 不存在返回相同响应
- 不给 Run ID 探针
Run 终止的唯一途径
SSE 断开不是终止途径。Run 终止只能通过:
| 途径 | 触发方式 |
|---|---|
| 用户主动中断 | POST /sessions/{sid}/interrupt |
| CycleDetector HITL 超时 | 自愈失败后 HITL 超时 |
| ErrorStreak HITL 超时 | 伪进展检测 HITL 超时 |
| BudgetCheckFn | 外部预算检查返回 Exhausted |
| Cell Stop | Cell 生命周期管理 |
事件类型
SSE 流中可能出现的事件类型:
| 事件 | 说明 |
|---|---|
stream_delta | LLM 文本增量 |
tool_start | 工具调用开始 |
tool_end | 工具调用结束 |
thinking_delta | 推理过程增量 |
budget_warning | 预算警告 |
hitl_request | 人工审批请求 |
plan_created | 计划创建 |
plan_updated | 计划更新 |
plan_completed | 计划完成 |
plan_interrupted | 计划中断 |
done | Run 正常完成 |
error | Run 错误终止 |
interrupted | Run 被中断 |
消费方行为
桌面产品(wesclaw / wescode / wescraft)
- 通过 JSON-RPC 接收流事件
- 流事件与 Go 进程绑定,不走 HTTP SSE
- 但底层仍遵循
INV-RUN-DETACH
SaaS / teleclaw
- 直接消费 HTTP SSE
- 需实现
from_seq重连逻辑 - nginx 超时需对齐(90000s > 86400s)
孤儿 Run 处理
SSE 断开后的 Run 可能成为"孤儿":
- Cell Boot 时自动执行
MarkInterruptedSessions()+ListOrphanedRuns() - 可通过
GET /observe/runs?status=resumable查询 - 可标记为
status=resumable等待恢复
相关不变量
- INV-RUN-DETACH:Run 生命周期与传输层解耦
- INV-TERM-01:引擎不因计数器阈值主动终止
- INV-OBS-10:事件缓冲区遵循 Actor 边界
相关文档
- Run 生命周期 →
run-lifecycle.md - Run 事件重播 →
run-event-replay.md - 观测边界 →
observe-boundary.md