# 流式事件与恢复

把会话与 Run 分开处理，通过事件游标恢复连接，并区分断开连接、取消请求和执行结束。

## SSE v2 协议

客户端向聊天、反馈和 resume 请求发送 X-iGraph-Stream-Version: 2，无需用户身份头。浏览器使用 fetch 流读取器；原生 EventSource 无法直接发送这些 POST 请求及自定义协议头。服务端返回 text/event-stream。

```http
X-iGraph-Stream-Version: 2
Accept-Language: zh-CN
Content-Type: application/json
```

| 聊天字段 | 类型 | 说明 |
| --- | --- | --- |
| message | string | 必填用户消息。 |
| start_request_id | UUID \| null | 为每个新请求生成 UUID；重试同一请求时保留。 |
| attachments | array \| null | 附件引用包含 file_token 和可选 filename。 |
| output_format | chat \| report \| json | 默认 chat。 |

Backend 使用原生后台流式 Run。stream/background 是前端发送的兼容字段；ChatReq 的核心契约以上述字段和 reasoning_mode / planning_mode 等模型字段为准。

v2 的每个 SSE data 帧包含 protocol_version、session_id、run_id、event_index 和 events。一个原始事件产生的多个界面事件放在同一 events 数组中，客户端必须完整应用该数组后才推进游标。event_index 为 null 的观察状态事件不推进原始事件游标；心跳是 SSE 注释，不是业务消息。收到 EOF 时未以空行结束的残缺帧不能当成完整事件应用。

```text
data: {"protocol_version":2,"session_id":"SESSION_ID","run_id":"RUN_ID","event_index":12,"events":[{"type":"tool_progress","tool":"train","call_id":"JOB_ID","progress":{"epoch":2,"epochs":10}}]}


```

聊天请求通过 Accept-Language 传递发送时的界面语言，并记录为本轮 response_language。切换中文或英文影响下一次发送的回复语言，不会重写已有消息。

## 断线恢复

- 同页面断流：保留已有消息，从最后完整应用的 event_index 续接。消息、工具、计划与游标应在同一状态事务提交。
- 刷新或消息内存丢失：从 last_event_index=-1 暂存重建当前 Run，追平后发布，保留其他历史消息。
- HTTP 409 REPLAY_UNAVAILABLE 或 stream_recovery_required：保留内容，轮询 snapshot，直到 snapshot_complete=true 后原子替换阶段消息。
- 快照 resume_cursor 当前为 null，不能将快照消息与旧游标后的原始增量拼接。使用快照恢复暂停阶段后，该 Run 保持快照模式。

```bash
curl -N -X POST \
  "http://localhost:21108/api/sessions/SESSION_ID/runs/RUN_ID/resume?last_event_index=12" \
  -H "X-iGraph-Stream-Version: 2"
```

> **恢复的运行边界**
> 恢复观察只读取已有运行，不自动重发问题或审批。原始事件重放依赖进程内缓存；缓存不可用时使用可读的持久化结果和完整快照，不承诺在进程重启后自动续算。

原始事件缓存依赖运行进程。服务重启或缓存裁剪后，可从已保存消息与完整快照恢复可见结果，但这不代表业务计算从断点重新启动。快照未完整时保留当前内容并显示恢复状态，避免把中间结果当成最终回答。

## 取消与状态确认

**POST** `/api/sessions/{session_id}/runs/cancel`

先调用 GET /api/capabilities。最新 .env.example 启用 IGRAPH_RUN_CANCEL_ENABLED；环境未设置时回退 false，Backend 还会检查运行服务与图服务的能力。run_cancel_v1 为 true 时才启用运行停止，job_cancel_v1 为 true 时才启用作业停止，edit_latest_turn_v1 决定最新一轮编辑是否可用。取消请求携带 target.run_id 或 target.start_request_id 二选一，并提供 cancel_request_id；这些字段必须是合法 UUID。同一次取消重试保留相同 cancel_request_id。

```json
{
  "target": {
    "run_id": "4d227ea8-c8ca-4f0d-b5e4-81a44de7bd4c"
  },
  "cancel_request_id": "f186c422-5eed-4d0c-a4b9-e7d459124fd0"
}
```

取消后查询 GET /api/sessions/{sid}/runs/control?run_id={rid}。execution_stopped 表示执行已停止，terminal_persisted 表示最终状态已保存；只有两者都满足且服务端允许发送时才恢复编辑。AbortController 只断开观察连接，不等于取消服务器任务。

取消接口返回 200 只说明 execution_stopped 已为 true；未停止时返回 202。仍需同时确认 terminal_persisted 和 can_send，才能认定客户端已可以继续。

作业停止使用 POST /api/sessions/{sid}/jobs/{jid}/cancel，正文包含 cancel_request_id；随后读取同一作业的 /control。运行停止和单个作业停止的目标范围不同，不应相互替代。

## 人工审批

**POST** `/api/sessions/{session_id}/feedback/stream`

当前界面提供批准与拒绝。API 的 decision 还支持 modify，可同时提交 patched_args。请求包含 run_id、request_id、decision_id，并可附 tool_call_id、tool_name、comment；标识和工具信息应来自当前暂停请求，不应从旧卡片猜测。普通 approve 使用服务端保存的原始参数，modify 才采用替换参数。

反馈继续原 run_id，不创建新聊天请求。前端在等待或提交审批时阻止发送新消息，接收确认后再恢复观察。过期、不匹配或已处理的暂停请求可能返回冲突；响应丢失时先读取权威状态，避免重复提交图修改。工具执行审批与技能候选批准使用不同接口。
