Run 生命周期解耦(INV-RUN-DETACH)
Run 生命周期始终与传输层(SSE / HTTP 连接)解耦。SSE 断开不取消 Run。
核心原则
SSE 连接断开 ≠ 取消 Run
Run 取消的唯一途径是显式操作
取消 Run 的五种途径
| 途径 | 触发者 | 说明 |
|---|---|---|
POST /sessions/{sid}/interrupt | 用户主动 | 点击停止按钮 |
| CycleDetector HITL 超时 | 引擎自动 | 真循环检测后用户未响应 |
| ErrorStreakDetector HITL 超时 | 引擎自动 | 伪进展检测 |
BudgetCheckFn 返回 Exhausted | 外部限制 | 商业预算耗尽 |
| Cell Stop | 生命周期 | Cell 停止或进程退出 |
引擎不因 counter 阈值(轮次/token/时间)主动终止(INV-TERM-01)。
RunEventBuffer
SSE 断线后重连可续播:
GET /cells/{id}/runs/{runID}/events?from_seq=N
- 每个 Run 有内存事件环
from_seq=N表示从第 N 个事件开始续播- 事件环保留到 Run 结束后一段时间
终端事件
runLoop 的每个 return 路径必须在返回前发出终端事件(INV-TERM-04):
| 事件 | 含义 |
|---|---|
EventDone | 正常完成 |
EventError | 错误终止 |
EventInterrupted | 用户中断 |
// callLLM/execute 返回 phaseReturn 时
if ctx.Err() != nil {
// 必须发 EventDone("context_cancelled")
// 不能依赖 for 循环顶部的 ctx.Err() 检查
// 因为 phaseReturn 直接 return,永远到不了那里
}
缺少终端事件 → terminal guard 合成虚假 EventError(missing_terminal_event) → 消费方显示误导性错误 + 无效重试按钮。
RunEnd 摘要消息
RunEnd 时引擎写入 role=system 摘要消息到 wes_messages。
孤儿 Run
可标记 status=resumable,通过查询发现:
GET /observe/runs?status=resumable
各消费方的处理
wesclaw 桌面
SSE 断开 → Go 后端继续 Run → 前端重连后通过事件重播恢复。
wescode
chatInFlight 锁绑定用户可见轮次(INV-WS-11):
- 流终端事件到达即释放
- RPC 尾巴(Settlement / CKG)不占发送权
企业版(teleclaw)
Run 不随 SSE 断开而死 → Java 返回 stopReason=detached。
hasActiveRun 门控防止新 Run 与活跃 Run 冲突。
反模式
| 禁止 | 正确做法 |
|---|---|
| SSE 断开取消 Run | Run 继续,客户端重连续播 |
| 用 counter 阈值终止 Run | 只有显式途径取消 |
phaseReturn 后不发终端事件 | 检查 ctx.Err() 发 EventDone |
| 假设 SSE 连接与 Run 生命周期相同 | 始终解耦 |