# Notifications and events

> Server notification method names, `ThreadItem` union variants, per-item delta events, turn-level events, realtime thread notifications, and `CodexErrorInfo` values on failures.

- Repository: openai/codex
- GitHub: https://github.com/openai/codex
- Human docs: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1
- Complete Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/llms-full.txt

## Source Files

- `README.md`
- `src/bespoke_event_handling.rs`
- `src/filters.rs`
- `src/thread_status.rs`
- `src/fs_watch.rs`
- `src/fuzzy_file_search.rs`
- `src/request_processors/feedback_processor.rs`

---

---
title: "Notifications and events"
description: "Server notification method names, `ThreadItem` union variants, per-item delta events, turn-level events, realtime thread notifications, and `CodexErrorInfo` values on failures."
---

`codex app-server` streams work to clients as JSON-RPC 2.0 notifications: each message has a `method` string and a `params` object. Turn and item progress for a subscribed thread is delivered through `ThreadScopedOutgoingMessageSender`; connection-wide notifications (config warnings, filesystem watches, account updates) go to every initialized connection. The authoritative method list is generated from `ServerNotification` in `app-server-protocol`; regenerate TypeScript or JSON Schema with `codex app-server generate-ts` / `generate-json-schema` when you pin to a binary version.

## Wire shape

Notifications use the same JSON-RPC envelope as requests, without an `id`:

```json
{ "method": "item/agentMessage/delta", "params": { "threadId": "…", "turnId": "…", "itemId": "…", "delta": "…" } }
```

`ServerNotification` is defined with `#[serde(tag = "method", content = "params")]`, so the Rust variant name maps to the wire `method` (for example `TurnCompleted` → `turn/completed`). Payload fields use `camelCase`.

## Delivery and subscription

| Scope | How it reaches the client |
| --- | --- |
| Thread turn/item stream | `thread/start`, `thread/resume`, and `thread/fork` auto-subscribe the calling connection. Further subscribers are tracked in `ThreadState`; `ThreadScopedOutgoingMessageSender` fans out only to `connection_ids` for that thread. |
| Connection-wide | `OutgoingMessageSender::send_server_notification` broadcasts to all initialized connections (for example `configWarning`, `fs/changed`). |
| Per-connection watch | `fs/watch` registers a `watchId` on one connection; `fs/changed` includes that `watchId`. |

`thread/unsubscribe` removes a connection from a thread’s subscriber set. When the last subscriber leaves and the thread is idle for 30 minutes, the server unloads it and emits `thread/closed`.

<Note>
`turn/started` and `turn/completed` currently carry a `turn` object whose `items` array is empty even when `item/*` notifications were streamed. Build UI state from `item/started`, deltas, and `item/completed` until that behavior changes.
</Note>

```mermaid
sequenceDiagram
    participant Client
    participant AppServer as app-server
    participant Core as codex-core

    Client->>AppServer: turn/start
    AppServer->>Client: turn/started (turn.status inProgress)
    Core-->>AppServer: EventMsg stream
    AppServer->>Client: item/started
    AppServer->>Client: item/agentMessage/delta
    AppServer->>Client: item/completed
    AppServer->>Client: turn/completed (turn.status terminal)
```

Core `EventMsg` values are projected to v2 notifications in `item_event_to_server_notification` and `apply_bespoke_event_handling` (turn completion, errors, realtime, hooks, plan/diff updates).

## Notification opt-out and experimental gating

Per connection, pass exact method names in `initialize.params.capabilities.optOutNotificationMethods`:

- Matching is exact (no wildcards or prefixes).
- Unknown names are accepted and ignored.
- Applies to typed server notifications (`thread/*`, `turn/*`, `item/*`, `rawResponseItem/*`, etc.), not RPC requests, responses, or JSON-RPC errors.
- Filtering happens in `should_skip_notification_for_connection` before write.

Experimental notifications are dropped entirely unless `capabilities.experimentalApi` is true at initialize time (same transport path as opt-out).

Examples:

```json
"optOutNotificationMethods": ["thread/started", "item/agentMessage/delta"]
```

```json
"optOutNotificationMethods": ["thread/realtime/outputAudio/delta"]
```

## Turn-level notifications

| Method | Params (summary) |
| --- | --- |
| `turn/started` | `{ threadId, turn }` — `turn.status` is `inProgress`, `items` empty |
| `turn/completed` | `{ threadId, turn }` — terminal `turn.status`: `completed`, `interrupted`, or `failed` |
| `turn/diff/updated` | `{ threadId, turnId, diff }` — aggregated unified diff after file changes |
| `turn/plan/updated` | `{ threadId, turnId, explanation?, plan }` — `plan[]` entries `{ step, status }` with `status` in `pending`, `inProgress`, `completed` |
| `model/rerouted` | `{ threadId, turnId, fromModel, toModel, reason }` |
| `model/verification` | `{ threadId, turnId, verifications }` |

`TurnStatus` values: `inProgress`, `completed`, `interrupted`, `failed`.

Token usage streams separately on `thread/tokenUsage/updated` (not embedded in `turn/completed`).

### Hooks

| Method | When |
| --- | --- |
| `hook/started` | A configured hook run begins |
| `hook/completed` | A hook run finishes |

Hook transcript content appears as `hookPrompt` `ThreadItem`s, not as separate hook payload streams.

## Item lifecycle and `ThreadItem`

Every unit of turn work follows: **`item/started` → item-specific deltas (optional) → `item/completed`**.

| Method | Role |
| --- | --- |
| `item/started` | Full `item` snapshot when work begins; `item.id` matches delta `itemId`s |
| `item/completed` | Authoritative final `item` state |
| `item/autoApprovalReview/started` | **[UNSTABLE]** Guardian auto-review began |
| `item/autoApprovalReview/completed` | **[UNSTABLE]** Guardian auto-review finished |

`ThreadItem` is a tagged union (`type` field, `camelCase` variants). Variants defined in the v2 protocol:

| `type` | Purpose |
| --- | --- |
| `userMessage` | User input (`content`: `text` / `image` / `localImage`); optional `clientId` from `turn/start` / `turn/steer` |
| `hookPrompt` | Hook-injected prompt fragments |
| `agentMessage` | Assistant reply text; optional `phase`, `memoryCitation` |
| `plan` | Plan-mode content (experimental streaming via `item/plan/delta`) |
| `reasoning` | `summary` and `content` string arrays |
| `commandExecution` | Sandboxed command: `command`, `cwd`, `status`, `commandActions`, `aggregatedOutput`, `exitCode`, `durationMs` |
| `fileChange` | Patch proposal: `changes[]` with `path`, `kind`, `diff`; `status` |
| `mcpToolCall` | MCP invocation with `server`, `tool`, `arguments`, `result` / `error` |
| `dynamicToolCall` | Client-executed dynamic tool |
| `collabAgentToolCall` | Collab tools (`spawn_agent`, etc.) with `senderThreadId`, `receiverThreadIds` |
| `webSearch` | Search tool with optional `action` |
| `imageView` | Image viewer tool |
| `imageGeneration` | Image generation tool result |
| `enteredReviewMode` / `exitedReviewMode` | Automated review boundaries |
| `contextCompaction` | History compaction marker |

Statuses for executable items typically include `inProgress`, `completed`, `failed`, and `declined` where applicable.

<Warning>
`thread/compacted` (`ContextCompacted` notification) is deprecated; prefer the `contextCompaction` item type.
</Warning>

## Per-item delta notifications

| Method | Item kind | Concatenate by |
| --- | --- | --- |
| `item/agentMessage/delta` | `agentMessage` | `itemId` → full reply text |
| `item/plan/delta` | `plan` (experimental) | `itemId` |
| `item/reasoning/summaryTextDelta` | `reasoning` | `itemId` + `summaryIndex` |
| `item/reasoning/summaryPartAdded` | `reasoning` | Marks new summary section |
| `item/reasoning/textDelta` | `reasoning` | `itemId` + `contentIndex` |
| `item/commandExecution/outputDelta` | `commandExecution` | `itemId` (stdout/stderr) |
| `item/commandExecution/terminalInteraction` | `commandExecution` | PTY interaction metadata |
| `item/fileChange/patchUpdated` | `fileChange` | Structured patch snapshots when streaming is enabled |
| `item/fileChange/outputDelta` | `fileChange` | Deprecated; server no longer emits |
| `item/mcpToolCall/progress` | `mcpToolCall` | Progress events |

`rawResponseItem/completed` is internal-oriented (Codex Cloud); most clients should ignore it.

## Thread lifecycle notifications

| Method | Params (summary) |
| --- | --- |
| `thread/started` | `{ thread }` after `thread/start`, `thread/resume`, or `thread/fork` |
| `thread/status/changed` | `{ threadId, status }` — `ThreadStatus`: `notLoaded`, `idle`, `systemError`, or `active` with `activeFlags` (`waitingOnApproval`, `waitingOnUserInput`) |
| `thread/archived` | Per archived thread |
| `thread/unarchived` | Restored thread |
| `thread/closed` | After idle unload |
| `thread/name/updated` | Name change |
| `thread/goal/updated` / `thread/goal/cleared` | Persisted goal changes |
| `thread/settings/updated` | Experimental effective next-turn settings |
| `thread/tokenUsage/updated` | Restored or live token usage |

`thread/status/changed` is driven by `ThreadWatchManager` when loaded thread activity or approval/input waits change.

## Thread realtime (experimental)

Realtime notifications are **not** `ThreadItem`s and are not returned by `thread/read`, `thread/resume`, or `thread/fork`.

| Method | Params (summary) |
| --- | --- |
| `thread/realtime/started` | `{ threadId, realtimeSessionId?, version }` |
| `thread/realtime/itemAdded` | `{ threadId, item }` — raw JSON (unstable upstream schema) |
| `thread/realtime/transcript/delta` | `{ threadId, role, delta }` |
| `thread/realtime/transcript/done` | `{ threadId, role, text }` |
| `thread/realtime/outputAudio/delta` | `{ threadId, audio }` — `audio`: `{ data, sampleRate, numChannels, samplesPerChannel }` |
| `thread/realtime/sdp` | `{ threadId, sdp }` — WebRTC answer |
| `thread/realtime/error` | `{ threadId, message }` |
| `thread/realtime/closed` | `{ threadId, reason? }` |

Requires `capabilities.experimentalApi` at initialize (notifications are dropped otherwise).

## Other server notifications

| Category | Methods |
| --- | --- |
| Errors and warnings | `error`, `warning`, `configWarning`, `guardianWarning`, `deprecationNotice` |
| Account / auth | `account/updated`, `account/rateLimits/updated`, `account/login/completed` |
| MCP | `mcpServer/startupStatus/updated`, `mcpServer/oauthLogin/completed` |
| Skills / apps | `skills/changed`, `app/list/updated` |
| Filesystem | `fs/changed` (`watchId`, `changedPaths`; 200ms debounce per watch) |
| Fuzzy search (experimental) | `fuzzyFileSearch/sessionUpdated`, `fuzzyFileSearch/sessionCompleted` |
| Standalone processes | `command/exec/outputDelta`, `process/outputDelta`, `process/exited` (experimental) |
| Server requests | `serverRequest/resolved` after client answers an approval/elicitation |
| Windows | `windows/worldWritableWarning`, `windowsSandbox/setupCompleted` |
| Remote / import | `remoteControl/status/changed`, `externalAgentConfig/import/completed` |
| Internal | `rawResponseItem/completed` |

`configWarning` may appear during initialization (`{ summary, details?, path?, range? }`). `warning` carries `{ threadId?, message }` for non-fatal core warnings.

## Failures, `error`, and `CodexErrorInfo`

Two surfaces carry the same error payload shape (`TurnError`):

| Surface | When |
| --- | --- |
| `error` notification | Mid-turn failure or retryable stream error |
| `turn/completed` with `turn.status: "failed"` | Terminal turn failure |

<ResponseField name="error" type="object">
  <ResponseField name="message" type="string">Human-readable error text</ResponseField>
  <ResponseField name="codexErrorInfo" type="CodexErrorInfo | null">Structured failure class (see table)</ResponseField>
  <ResponseField name="additionalDetails" type="string | null">Extra context (for example stream errors)</ResponseField>
</ResponseField>

`error` also includes `threadId`, `turnId`, and `willRetry`:

- `willRetry: true` — transient `StreamError`; turn may continue (automatic retry).
- `willRetry: false` — terminal for that error path; expect `turn/completed` with `failed` when the turn ends.

### `CodexErrorInfo` variants

Wire format is camelCase. Unit variants serialize as strings (for example `"cyberPolicy"`). Variants with data serialize as a single-key object (for example `{ "httpConnectionFailed": { "httpStatusCode": 503 } }`).

| Variant | Meaning |
| --- | --- |
| `contextWindowExceeded` | Context window exceeded |
| `usageLimitExceeded` | Usage / quota limit |
| `serverOverloaded` | Upstream overload |
| `cyberPolicy` | Cyber safety policy block |
| `httpConnectionFailed` | HTTP connection failure; optional `httpStatusCode` |
| `responseStreamConnectionFailed` | Could not open response SSE stream |
| `responseStreamDisconnected` | SSE stream dropped mid-turn |
| `responseTooManyFailedAttempts` | Retry limit exhausted |
| `activeTurnNotSteerable` | `turn/start` or `turn/steer` while turn is not steerable; `turnKind`: `review` or `compact` |
| `badRequest` | Invalid request |
| `unauthorized` | Auth failure |
| `sandboxError` | Sandbox execution failure |
| `threadRollbackFailed` | Rollback operation failed |
| `internalServerError` | Internal server error |
| `other` | Unclassified |

<RequestExample>
```json
{
  "method": "turn/completed",
  "params": {
    "threadId": "thr_abc",
    "turn": {
      "id": "turn_1",
      "items": [],
      "itemsView": "notLoaded",
      "status": "failed",
      "error": {
        "message": "Rate limit exceeded",
        "codexErrorInfo": "usageLimitExceeded"
      }
    }
  }
}
```
</RequestExample>

<RequestExample>
```json
{
  "method": "error",
  "params": {
    "threadId": "thr_abc",
    "turnId": "turn_1",
    "willRetry": true,
    "error": {
      "message": "Stream disconnected",
      "codexErrorInfo": {
        "responseStreamDisconnected": { "httpStatusCode": 502 }
      },
      "additionalDetails": "retrying"
    }
  }
}
```
</RequestExample>

## Client checklist

<Steps>
<Step title="Subscribe to a thread">
Call `thread/start`, `thread/resume`, or `thread/fork` on the connection that should receive events.
</Step>
<Step title="Handle the turn envelope">
On `turn/started`, record `turn.id`. Stream items from `item/*`. Finish on `turn/completed` using `turn.status` and optional `turn.error`.
</Step>
<Step title="Reduce volume if needed">
Pass `optOutNotificationMethods` at `initialize` for high-frequency methods (for example `item/agentMessage/delta`, `item/commandExecution/outputDelta`).
</Step>
<Step title="Enable experimental streams">
Set `capabilities.experimentalApi: true` before relying on realtime, `thread/settings/updated`, or other gated notifications.
</Step>
</Steps>

## Related pages

<CardGroup>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Consumer-oriented walkthrough of turn and item streaming, deltas, and opt-out patterns.
</Card>
<Card title="Threads, turns, and items" href="/threads-turns-items">
Core primitives, subscription model, and thread status semantics.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Initialize handshake, notification opt-out, and thread subscribe/unsubscribe.
</Card>
<Card title="Experimental API" href="/experimental-api">
Runtime opt-in required for gated notifications and RPC fields.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Turn failures, `codexErrorInfo` interpretation, and overload retries.
</Card>
</CardGroup>
