Real-Time Events

Events provide real-time notifications for cache invalidation. Frontends subscribe to an SSE stream and receive lightweight JSON events when resources change. Events are best-effort cache invalidation hints — on receiving an event, refetch the affected resource. Never rely on events as a source of record.

Event Format

Each SSE message contains a JSON envelope on a single data: line:

data: {"event":"created","kind":"conversation","data":{"conversation":"<uuid>","conversation_group":"<uuid>"}}

The envelope has three fields:

  • event — the action that occurred (e.g. created, updated, deleted, appended)
  • kind — the resource type (e.g. conversation, entry, response, membership, stream)
  • data — kind-specific payload with identifiers for the affected resource

A keepalive comment : keepalive is sent every 30 seconds to keep the connection alive.

Event Kinds and Actions

KindEventTriggerdata Fields
conversationcreatedNew conversation createdconversation, conversation_group
conversationupdatedTitle, metadata, or other conversation-level changeconversation, conversation_group
conversationdeletedConversation soft-deletedconversation, conversation_group
entryappendedNew entry added to a conversationconversation, entry
responsestartedResponse recording session beginsconversation, recording
responsecompletedResponse recording session ends successfullyconversation, recording
responsefailedResponse recording session failsconversation, recording
membershipaddedAccess granted to a userconversation_group, user, role
membershipupdatedAccess level changedconversation_group, user, role
membershipremovedAccess revoked from a userconversation_group, user
streamevictedServer closing connection (slow consumer)reason
streaminvalidateEvents may have been missed (pub/sub disruption)reason

Subscribing with curl

The SSE endpoint:

GET /v1/events
Accept: text/event-stream
Authorization: Bearer <token>

An optional kinds query parameter (comma-separated) filters which event kinds are delivered:

curl -N -H "Authorization: Bearer $(get-token)" \
  http://localhost:8080/v1/events

With a filter to receive only conversation and entry events:

curl -N -H "Authorization: Bearer $(get-token)" \
  "http://localhost:8080/v1/events?kinds=conversation,entry"

Walkthrough

Open two terminals side by side to see events in real time.

Terminal 1 — subscribe to the event stream:

curl -N -H "Authorization: Bearer $(get-token)" \
  http://localhost:9090/v1/events

Terminal 2 — create a conversation and add an entry:

# Create a conversation
curl -sSfX POST http://localhost:9090/chat/e2c9a1b0-0001-4000-8000-000000000001 \
  -H "Content-Type: text/plain" \
  -H "Authorization: Bearer $(get-token)" \
  -d "Hello, this is a test."

Terminal 1 output shows a conversation/created event followed by entry/appended events:

data: {"event":"created","kind":"conversation","data":{"conversation":"e2c9a1b0-0001-4000-8000-000000000001","conversation_group":"..."}}
data: {"event":"appended","kind":"entry","data":{"conversation":"e2c9a1b0-0001-4000-8000-000000000001","entry":"..."}}

Update the conversation title:

curl -sSfX PATCH http://localhost:9090/v1/conversations/e2c9a1b0-0001-4000-8000-000000000001 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $(get-token)" \
  -d '{"title":"Events demo"}'

Terminal 1 receives a conversation/updated event:

data: {"event":"updated","kind":"conversation","data":{"conversation":"e2c9a1b0-0001-4000-8000-000000000001","conversation_group":"..."}}

Testing Events

function get-token() {
  curl -sSfX POST http://localhost:8081/realms/memory-service/protocol/openid-connect/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id=memory-service-client" \
    -d "client_secret=change-me" \
    -d "grant_type=password" \
    -d "username=bob" \
    -d "password=bob" \
    | jq -r '.access_token'
}

Chat to create a conversation and trigger events:

curl -NsSfX POST http://localhost:9090/chat/e2c9a1b0-0001-4000-8000-000000000001 \
  -H "Content-Type: text/plain" \
  -H "Authorization: Bearer $(get-token)" \
  -d "Hello, this is a test."

Example output:

I am a Python memory-service demo agent.

Verify the conversation was created:

curl -sSf http://localhost:9090/v1/conversations/e2c9a1b0-0001-4000-8000-000000000001 \
  -H "Authorization: Bearer $(get-token)"

Example output:

{
  "id": "e2c9a1b0-0001-4000-8000-000000000001"
}

Verify entries exist:

curl -sSf http://localhost:9090/v1/conversations/e2c9a1b0-0001-4000-8000-000000000001/entries \
  -H "Authorization: Bearer $(get-token)"

Example output:

{"data":[...]}

Access Control

Events are filtered by conversation membership. Users only receive events for conversations they have at least reader access to. Stream-level events (stream kind) bypass membership filtering because they describe the health of the SSE connection itself, not a specific resource.

Connection Lifecycle

Keepalive: A : keepalive comment is sent every 30 seconds to prevent proxies and load balancers from closing idle connections.

Slow consumer eviction: If a client falls too far behind processing events, the server sends a stream/evicted event with a reason field, then closes the connection. The client should reconnect and do a broad cache refresh.

Pub/sub recovery: In multi-node deployments, if the event bus (Redis or PostgreSQL) experiences a disruption, clients receive a stream/invalidate event. This means events may have been missed during the gap. The client should refresh all cached data.

Disconnection: On any disconnection, reconnect and do a broad cache refresh. The event stream does not support Last-Event-ID replay.

Frontend Integration Tips

  • On event receive, invalidate cache entries for the affected resource and refetch.
  • On stream/invalidate, do a broad cache invalidation across all cached resources.
  • On reconnect after a disconnection, refetch all active data.
  • Never assume the event stream replaces normal REST reads — events are hints, not data.

Next Steps