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

# Real-time events

> Consume the durable, redacted PostgreSQL event feed with resumable Server-Sent Events.

OAO exposes one ordered Server-Sent Events feed per project:

```http theme={null}
GET /v1/projects/{projectId}/events
Accept: text/event-stream
Authorization: Bearer oao_your_secret
Last-Event-ID: djE6MTQy
```

Each frame's SSE `event` is the product-event kind, such as
`run.state_changed` or `tool_call.requested`. Its SSE `id` is an opaque cursor
for the committed project position; send it back unchanged rather than
constructing one. Its JSON body has this shape:

```ts theme={null}
interface ProductEvent {
  id: string;
  organizationId: string;
  projectId: string;
  aggregateType: string;
  aggregateId: string;
  aggregateSequence: number;
  projectPosition: string;
  kind: string;
  publicPayload: Record<string, unknown>;
  occurredAt: string;
}
```

To receive the same events without holding a connection open, configure an
[event webhook](/integrations/event-webhooks). OAO then pushes signed,
ordered batches to your endpoint and keeps its own durable cursor.

## Connection lifecycle

* **Opening:** the server writes a `: connected` comment line as soon as the
  stream opens, so proxies commit the response before the first event exists.
* **Keepalive:** after 10 seconds without other output, the server writes a
  `: keepalive` comment line. Standards-compliant SSE parsers ignore lines that
  start with `:`. A custom parser must skip them and never treat them as events.
* **Normal close:** a healthy stream closes after about 25 seconds. Reconnect
  with `Last-Event-ID`; the close is not data loss.
* **Backlog:** after a reconnect, the server sends missed events in pages of
  200 without pausing between full pages.
* **One page:** `?once=true` returns a single page of up to 200 events with no
  comment lines and closes immediately.

Event streams are sent with `Cache-Control: no-store, no-transform` and
`X-Accel-Buffering: no`. If OAO runs behind your own reverse proxy or load
balancer, disable response buffering and compression for `text/event-stream`,
and allow at least 15 seconds between reads.

## Resume correctly

1. Persist the last event ID only after your application has applied that
   event successfully.
2. Reconnect with `Last-Event-ID` after a network error or process restart.
3. Treat duplicate events as normal and deduplicate by event `id` or by
   `(aggregateType, aggregateId, aggregateSequence)`.
4. Refresh the affected resource with a normal API read when your application
   does not understand a new event kind.

The cursor is backed by committed PostgreSQL state. `LISTEN/NOTIFY` is only a
latency optimization inside OAO, so missed notifications do not create gaps.
Each API process shares one dedicated `LISTEN` connection across all of its open
streams and wakes only the streams of the project that committed the event. If
that connection drops, streams keep polling committed events every second while
the listener reconnects.

## Model usage events

`model.invocation_completed` and `model.invocation_failed` payloads include
`inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, and
`costMicrounits`, plus OAO's normalized `finishReason`. Cache reads represent
prompt tokens reused from the provider cache; cache writes represent prompt
tokens newly stored there. A provider that does not report cache usage
contributes zero for those fields.

When a model call fails because of a provider finish condition, the failed
event also includes the provider's exact bounded `providerFinishReason` when it
is available, plus a safe `errorExplanation` describing why OAO classified the
response as failed. For example, `providerFinishReason: "content_filter"`
explains that the provider's content filter stopped the response and OAO could
not treat the partial output as a completed reply. These payloads never include
prompt or response content, raw provider error messages, or credentials.

## Harness Operation events

The runtime emits `harness.operation_started`,
`harness.operation_completed`, `harness.operation_failed`, and the applicable
cancellation or timeout event for focused scratch-loop work. It also emits one
`harness.operation_step` event for each completed inner model or tool step. Safe
payloads can
include `operationKey`, `toolCallId`, `taskCharacters`, `timeoutMs`,
`durationMs`, and `resultValidated`. Step payloads use `harnessToolCallId`,
`stepKind`, `stepId`, `stepIndex`, status, duration, bounded token counts, and a
safe summary. Model-step summaries are derived only from bounded tool names—for
example, `Requested activate_skill.` or `Returned the structured result for
validation.` Tool steps can include a safe tool label or path summary.

These events are correlated to the aggregate run and tool call. They never
contain the detailed task, scratch conversation, structured result, Skill
instructions/resources, tool payloads, authorization material, or raw shared
documents. The authorized console groups the start and terminal lifecycle
events by `toolCallId` into one Harness transcript row. Opening that row shows a
details modal with safe, sequential inner-turn metadata. New events carry the
owning Harness tool-call ID, so concurrent scratch loops are attributed exactly
instead of by timing. Lifecycle windows that overlap are shown as one parallel
group with a shared colored rail and count in the console. Legacy activity that
does not contain explicit correlation is folded only when its timing identifies
one unambiguous invocation; ambiguous legacy activity remains in the parent
timeline and the affected Harness modals are marked as partial.

## Filter in the client

The server feed is project-wide. A session UI normally stores its cursor, then
filters frames whose `aggregateId` equals a known run, thread, tool call,
approval, or sandbox ID. On relevant events, re-fetch
`GET /sessions/{sessionId}` to obtain the current transcript and debug view.

<Note>
  Event bodies contain public state and identifiers, not raw model reasoning,
  credentials, authorization headers, unredacted tool payloads, or attached file
  bytes. A user `message.created` event may include only the safe `fileCount`;
  re-fetch the session for filename, content type, size, and SHA-256 metadata.
</Note>

## Delegation events

| Kind | Meaning |
| - | - |
| `delegation.created` | A persistent child thread/session and first run exist. |
| `delegation.follow_up_created` | Another run was queued on the same child session. |
| `delegation.completed` | A child run completed and returned a result. |
| `delegation.failed` | A child run failed, timed out, or was cancelled. |
| `delegation.cancelled` | The relation was closed to future follow-ups. |

Payloads contain only delegate/version/session/run identifiers, ordinal,
state, and bounded status metadata. They never contain the delegated prompt,
child response, workspace contents, credentials, or authorization headers.

## Skill events

| Kind | Meaning |
| - | - |
| `skill.created` | A project Skill and version 1 were committed. |
| `skill.draft_created` | A mutable package authoring draft was created or cloned. |
| `skill.draft_discarded` | An unpublished package draft was discarded. |
| `skill.version_published` | A new immutable package version was committed. |
| `skill.version_deprecated` | A version was blocked from new agent bindings. |
| `skill.version_revoked` | A version was blocked from binding and runtime admission. |
| `skill.disabled` | The Skill was paused: no new agent version can pin it. |
| `skill.enabled` | A disabled Skill became attachable again. |
| `skill.deleted` | The Skill was removed (archived) and its key released. |
| `skill.activated` | Flue progressively loaded a bound Skill's instructions. |
| `skill.resource_read` | Flue read one supporting resource from a bound Skill. |

Publication events may include Skill/version identifiers, version number, and
content hash. Runtime events contain only safe identifiers, the requested
resource path, outcome, and duration. They never contain instructions,
resource contents, decoded bytes, model text, credentials, or authorization
headers. Audit records follow the same boundary.

## MCP events

MCP configuration emits `mcp.server_created`,
`mcp.server_version_published`, `mcp.discovery_completed`,
`mcp.discovery_failed`, `mcp.toolset_published`,
`mcp.credential_created`, `mcp.credential_rotated`, and
`mcp.credential_revoked`. Runtime calls emit `mcp.call_started`,
`mcp.call_completed`, `mcp.call_failed`, or `mcp.call_cancelled`.

Payloads contain only tenant resource IDs, remote tool name, safe outcome/error
code, response byte count, and timing/state metadata. They exclude credentials,
headers, tool arguments, tool results, model content, and sandbox state.

## TypeScript SDK

The workspace SDK reconnects automatically unless disabled:

```ts theme={null}
for await (const frame of client.streamProjectEvents(projectId, {
  lastEventId: savedCursor,
  reconnect: true,
  signal: abortController.signal,
})) {
  await applyEvent(frame.data);
  await saveCursor(frame.id);
}
```

Abort the supplied signal during application shutdown. For a browser using the
WorkOS cookie, configure the client with `credentials: "include"`; for a
server-side integration, provide `apiKey`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.