# Streaming and recovery

Treat sessions and runs separately, resume with event cursors, and distinguish disconnection from cancellation and confirmed completion.

## SSE v2 protocol

Clients send X-iGraph-Stream-Version: 2 on chat, feedback, and resume requests; no user identity header is required. The browser uses a fetch stream reader because native EventSource cannot send these POST requests and custom protocol headers. The server returns text/event-stream.

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

| Chat field | Type | Description |
| --- | --- | --- |
| message | string | Required user message. |
| start_request_id | UUID \| null | Generate a UUID for a new request; retain it when retrying that same request. |
| attachments | array \| null | Attachment references contain file_token and optional filename. |
| output_format | chat \| report \| json | Defaults to chat. |

Backend uses native background streaming runs. The frontend sends stream/background as compatibility fields; the core ChatReq contract uses the fields above plus model fields such as reasoning_mode and planning_mode.

Each v2 SSE data frame contains protocol_version, session_id, run_id, event_index, and events. Multiple UI events derived from one original event are grouped in the same events array; apply the whole array before advancing the cursor. Observation-state events with event_index=null do not advance the original-event cursor. Heartbeats are SSE comments, not business messages. An incomplete frame without its terminating blank line must not be applied at 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}}]}


```

Chat requests carry the UI language at send time through Accept-Language, recorded as response_language for that turn. Switching Chinese or English affects the next submitted response; it does not rewrite existing messages.

## Resume after a disconnect

- Same-page disconnect: keep existing messages and resume from the last fully applied event_index. Commit messages, tools, plans, and the cursor in one state transaction.
- After a refresh or loss of message state: rebuild the current run into temporary state from last_event_index=-1, publish after catching up, and retain other historical messages.
- On HTTP 409 REPLAY_UNAVAILABLE or stream_recovery_required: preserve content and poll snapshot, replacing phase messages atomically only once snapshot_complete=true.
- Snapshots currently return resume_cursor=null. Do not combine snapshot messages with raw deltas after an old cursor. A run restored from a paused-phase snapshot stays in snapshot mode.

```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"
```

> **Runtime boundary for recovery**
> Observation recovery reads an existing run without resubmitting a question or approval. Original-event replay depends on the in-process buffer; when it is unavailable, use readable persisted results and complete snapshots. This does not promise automatic computation recovery after a process restart.

Original-event buffers depend on the runtime process. After a restart or buffer eviction, saved messages and complete snapshots can restore visible results; this does not restart business computation from a checkpoint. While a snapshot remains incomplete, retain current content and show recovery state instead of treating partial results as final.

## Cancellation and confirmation

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

Call GET /api/capabilities first. The current .env.example enables IGRAPH_RUN_CANCEL_ENABLED; an unset variable falls back to false, and Backend also checks runtime and graph-service capabilities. run_cancel_v1 enables stopping a run, job_cancel_v1 enables stopping a job, and edit_latest_turn_v1 controls latest-turn editing. Cancellation specifies either target.run_id or target.start_request_id and a cancel_request_id, all valid UUIDs. Keep the same cancel_request_id when retrying one cancellation.

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

After cancelling, query GET /api/sessions/{sid}/runs/control?run_id={rid}. execution_stopped reports stopped execution; terminal_persisted reports a saved terminal state. Resume editing only when both hold and the server permits sending. AbortController disconnects the observer; it does not cancel server execution.

HTTP 200 from cancellation indicates execution_stopped is true; otherwise the endpoint returns 202. Still check terminal_persisted and can_send before allowing the client to continue.

To cancel a job, POST to /api/sessions/{sid}/jobs/{jid}/cancel with cancel_request_id, then read that job’s /control endpoint. Cancelling a run and cancelling an individual job target different scopes.

## Human approval

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

The current UI offers approve and reject. The API decision also supports modify with patched_args. Requests contain run_id, request_id, and decision_id, with optional tool_call_id, tool_name, and comment. Take identities and tool metadata from the current paused request rather than an old card. A normal approve uses persisted arguments; only modify applies replacements.

Feedback continues the original run_id instead of creating a new chat request. The frontend blocks new messages while approval is pending or being submitted and resumes observation after acceptance. Stale, mismatched, or already handled paused requests may conflict; if a response is lost, read authoritative state before resubmitting a graph mutation. Tool execution approval and candidate skill approval use different endpoints.
