> ## Documentation Index
> Fetch the complete documentation index at: https://docs.praxa.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> Consume reconnectable Praxa SSE events, persist sequence cursors, handle heartbeats, and distinguish terminal from interrupted durable runs.

> **Status:** Durable lifecycle SSE is available in the production partner
> preview. Exact browser approval events remain pending production activation.
> Cancellation-reconciliation code is deployed, but final-status behavior
> remains pending a separate live canary.

```mermaid theme={null}
sequenceDiagram
  participant C as Your consumer
  participant P as Praxa event stream
  C->>P: GET events
  P-->>C: id 41 plus progress event
  P-->>C: id 42 plus progress event
  C--xP: connection closes
  C->>P: GET events with Last-Event-ID 42
  P-->>C: id 43 plus next event
  P-->>C: terminal event
```

```http theme={null}
GET /v1/runs/:id/events
Authorization: Bearer praxa_sk_<64 lowercase hex characters>
Accept: text/event-stream
Last-Event-ID: 42
```

The key must carry `runs:read`, and the run must belong to its tenant.
`Last-Event-ID` is optional. When supplied, it must be a non-negative safe
integer; the stream resumes strictly after that `task_run_events.sequence`.

## Framing

Every projected event uses standard SSE `id`, `event`, and `data` fields:

```text theme={null}
id: 43
event: run.progress
data: {"apiVersion":"v1","run_id":"2f47a86e-72ce-4f27-8ff0-7dc0c959fd35","sequence":43,"at":"2026-07-31T12:00:04.000Z","type":"run.progress","message":"Run phase: executing"}

```

* `id` equals the event's numeric `sequence`.
* The stream sends comment heartbeats after ten seconds without a projected event.
* Disconnecting cancels the polling loop.
* A `completed`, `failed`, or `cancelled` run closes the stream.
* Once the reconciliation final-status canary clears, reconciliation is
  non-terminal: the run remains `running`, and the stream stays open through
  reconciliation and recovery.
* The TypeScript client rejects a missing/mismatched SSE id, non-increasing
  sequences, events after a terminal event, and EOF before a terminal event.

## Durable event vocabulary

| Event               | Source condition                                                                                                              |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `run.accepted`      | Billing reservation recorded.                                                                                                 |
| `run.started`       | Durable workflow claimed the task.                                                                                            |
| `run.progress`      | A non-terminal phase update; reconciliation/recovery messages join this existing type after their final-status canary clears. |
| `approval.required` | Source-ready pending activation: an exact recorded browser action is waiting for a digest-bound decision.                     |
| `approval.resolved` | Source-ready pending activation: a digest-bound approval decision was recorded.                                               |
| `run.completed`     | `task.finalized` with completed status.                                                                                       |
| `run.failed`        | An ordinary terminal failure; after the reconciliation final-status canary clears, also an outcome Praxa could not verify.    |
| `run.cancelled`     | Cancellation completed.                                                                                                       |

## Reconciliation without a new event type

The migration and compatible Workers are deployed, but this final-status path
remains pending a separate live canary. Once that gate clears, internal
`reconcile_required` states project through the existing v1 shapes. The run
stays `running`, and the stream emits this exact
`run.progress` message:

```text theme={null}
The run is reconciling an uncertain execution outcome. Do not retry or resubmit it.
```

Do not retry or resubmit the run while you see that message. Reconciliation is
not a terminal event, so the stream remains open. If Praxa resolves the
uncertainty, it emits another `run.progress` event with this exact message:

```text theme={null}
Run reconciliation completed; execution is resuming.
```

If Praxa cannot verify whether a committing action happened, it emits a terminal
`run.failed` event:

```json theme={null}
{
  "apiVersion": "v1",
  "run_id": "2f47a86e-72ce-4f27-8ff0-7dc0c959fd35",
  "sequence": 44,
  "at": "2026-07-31T12:00:05.000Z",
  "type": "run.failed",
  "failure": {
    "code": "run_outcome_unknown",
    "message": "A committing action may have happened, but the final outcome could not be verified. Do not retry automatically.",
    "retryable": false
  }
}
```

Ordinary failed, partial, and expired outcomes continue to use
`failure.code: "run_failed"`. An accepted cancellation request can follow any
of these paths. It does not prove rollback or guarantee an immediate
`run.cancelled` event.

## Approval events fail closed (pending activation)

Praxa emits `approval.required` only when it can derive a customer-safe exact
presentation from immutable `browse_session_act` executor input. The event
contains an `approvalId`, the exact `action_digest`, and a `summary` made from
the recorded instruction or steps followed by the target host. The same fields
can appear as `RunResource.pendingApproval`.

Render that summary without truncation or re-derivation. After the person
chooses, send `approve` or `deny` with the event's exact digest to
`POST /v1/runs/:id/approve`. Praxa re-derives the presentation and compares the
digest before recording the decision. If the presentation or digest is absent,
stale, unsupported, or mismatched, Praxa emits no unsafe approval projection
and accepts no decision.

The versioned `AiPlatformRunEventV1` schema also includes output, tool, and usage
events. The current durable-task projector does not emit those event types.

No event exposes prompts, reasoning content, raw tool arguments, provider
cost, margin, lease state, or executor credentials.

## TypeScript

```ts theme={null}
for await (const event of client.streamRunEvents(runId, {
  lastEventId: 42,
})) {
  switch (event.type) {
    case "run.completed":
    case "run.failed":
    case "run.cancelled":
      console.log("terminal", event.type);
      break;
    default:
      console.log(event.sequence, event.type);
  }
}
```
