Skip to content

Design consistent EventV2 payloads for aggregate replay and live events #31130

Closed

Description

@opencode-agent

Goal

Expose replayable aggregate event streams without changing the event envelope based on where it was subscribed from.

opencode.sessions.events({ sessionID, after? })

Decision

Use one minimal, orthogonal payload shape everywhere:

type Payload = {
  id: Event.ID
  type: string
  location?: Location.Ref
  subject?: string
  durable?: {
    version: number
    seq: number
  }
  data: unknown
}

Each field answers one question:

  • id: Which event? Independently generated for every event; no DB required for ephemeral fast paths.
  • type + data: What happened?
  • location: Where did it happen?
  • subject: Which resource does it concern? For Session events, this is the Session ID. Subject membership does not imply durability.
  • durable: Can this exact event be replayed?
    • version: persisted schema version.
    • seq: ordered position within the subject stream.

Subscription changes selection, never payload shape. EventV2.subscribe, EventV2.all, and sessions.events return the same envelope.

Stream semantics

  • sessions.events({ sessionID, after }) is a typed subject-specific stream.
  • after is an exclusive durable sequence: replay events with durable.seq > after, then tail live events.
  • During catch-up, emit durable history only. Do not buffer ephemeral deltas.
  • After catch-up, emit both durable and ephemeral events live.
  • Full-value terminal events (text.ended, reasoning/tool-input equivalents) are durable, occur after all corresponding deltas, and are never followed by another delta for that identity.
  • A reconnecting client may miss partial deltas and wait for the durable terminal value. This is intentional.
sequenceDiagram
    participant C as Client
    participant E as Event stream
    participant DB as Durable log

    C->>E: events({ subject, after: 12 })
    E->>DB: read seq > 12
    DB-->>E: text.ended (seq 13, full value)
    E-->>C: durable event (seq 13)
    Note over E,C: catch-up complete; ephemeral events now enabled
    E-->>C: text.started (durable seq 14)
    E-->>C: text.delta (ephemeral)
    E-->>C: text.delta (ephemeral)
    E-->>C: text.ended (durable seq 15, full value)
Loading

Consumer strategies

  • GUI: on reconnect, discard local state, refetch canonical state, then restart live observation.
  • Durable integration: persist the latest durable.seq, resume with after, and make external side effects idempotent. OpenCord uses this for restart-safe Discord publication.
  • Control-plane replication: may share the underlying log and sequence semantics, but should use a stricter protocol for ownership, fencing, and replay validation.

Participant perspectives

  • Dax (@thdxr): prioritize a consistent envelope; keep version and seq together; keep random event IDs; model subject independently of durability; retain location even when redundant in a Session subscription.
  • James (@jlongster): prioritize one ergonomic stream; terminal full values should make simple reducers converge; skip ephemeral buffering during catch-up.
  • Kit (@kitlangton): preserve durable replication for integrations such as OpenCord while also exposing useful live deltas; keep the public model minimal and orthogonal.

Implementation work

  • Restructure EventV2.Payload to the agreed envelope.
  • Separate subject routing from durable persistence in event definitions.
  • Generalize aggregate replay/tail into a subject-scoped primitive; retain sessions.events as the typed facade.
  • Add ordering and reconnect tests, especially proving no delta appears after its terminal full-value event.
  • Update OpenCord to read durable.seq rather than the current cursor wrapper.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions