# How the OpenMAIC Agent Runtime Handles Event Replay for Session History

> Learn how the OpenMAIC agent runtime replays session history by reading immutable PostgreSQL events, compacting no-ops, and folding them into session state using monotonic seq ordering.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: internals
- Published: 2026-09-06

---

**The OpenMAIC agent runtime replays session history by reading immutable events from PostgreSQL, compacting them to remove no-ops, and folding them into session state using a monotonic `seq` ordering key.**

Every interaction in an OpenMAIC session—user messages, tool calls, thinking events, assistant replies—is durably logged as an immutable row. When a client reconnects or a session resumes, the runtime reconstructs the current view by replaying these events through a deterministic fold operation. This article explains the complete replay pipeline, from storage to state reconstruction.

## Event Storage and Ordering Guarantees

OpenMAIC persists session events to a PostgreSQL table managed by `PgAgentSessionStore`. Each row carries a **monotonically increasing `seq`** field that serves as the sole ordering key for replay, as defined in `packages/@openmaic/storage/src/agent-session/types.ts`.

This design guarantees:

- **Deterministic ordering** – Events are always processed in the sequence they were written
- **Idempotent folding** – Re-applying an already-processed event is a no-op because `seq` ensures unique positioning
- **Gap detection** – Clients can resume from any point using a cursor value

## The Replay Pipeline: Fetch, Compact, Fold

The runtime reconstructs session state through three distinct phases.

### 1. Fetching Events from Durable Storage

The `PgAgentSessionStore.readEventsAfterForReplay(sessionId, cursor)` method pulls all rows with `seq` greater than the provided cursor. This interface, located in the storage package, enables both cold replays (full session reconstruction) and mid-gap replays (resuming from a disconnection point).

### 2. Compacting the Event Stream

Before folding, events pass through `compactReplayEvents` to remove no-op duplicates. For example, a re-sent `thinking_end` event that carries no new information is eliminated while preserving the original order of meaningful events.

### 3. Folding into Session State

The `foldEvents` function, implemented in `packages/@openmaic/dsl/src/runtime.ts`, walks the compacted stream and applies each record to an in-memory session state object. This state includes:

- Chat transcript accumulation
- Generating order tracking
- Page and scene context

The fold operation is **pure and deterministic**—given the same event sequence, it always produces identical state.

## Replay Consistency Across Connection Scenarios

The system handles two primary reconnection patterns, validated in [`tests/workbench/session-fold.test.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/tests/workbench/session-fold.test.ts).

### Cold Replay

After a full disconnect, the client reconstructs state from scratch:

```typescript
// Rebuild from cursor 0 (complete history)
const { events } = await store.readEventsAfterForReplay(sessionId, 0);
const replayable = compactReplayEvents(events);
const sessionState = foldEvents(undefined, replayable);

```

As verified in [`tests/workbench/session-fold.test.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/tests/workbench/session-fold.test.ts) lines 227-238, this produces a state matching the live session exactly.

### Mid-Gap Replay

When reconnecting with a `Last-Event-ID` header, the runtime resumes from the known cursor:

```typescript
// Resume from last known sequence number
const { events, scanned } = await store.readEventsAfterForReplay(
  sessionId,
  lastSeq  // cursor from client
);

```

The overlapping prefix is folded once; duplicates are ignored. Test lines 166-165 validate that the cursor stays synchronized without state corruption.

## Event Notification and Real-Time Delivery

Every durable append triggers a PostgreSQL `NOTIFY` within the same transaction that writes the row. In [`lib/server/agent-runtime/store.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/server/agent-runtime/store.ts) lines 71-80, the `notifyDurableAgentEvent` call wakes the per-session Server-Sent-Events (SSE) tail.

The notification flow:

1. Transaction commits event to `agent-session` table
2. `NOTIFY` payload emitted with session ID and new `seq`
3. [`event-notify-bus.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/event-notify-bus.ts) routes to active SSE connections
4. Clients trigger replay for any missed events

If notifications are missed due to network partitions, SSE clients fall back to periodic polling. **Correctness is never compromised** because the replay mechanism is source-of-truth based.

## Key Source Files and Responsibilities

| Component | File Path | Role in Replay |
|-----------|-----------|----------------|
| Persistent event store | [`lib/server/agent-runtime/store.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/server/agent-runtime/store.ts) | Transaction wrapper, `readEventsAfterForReplay` |
| Notification dispatch | [`lib/server/agent-runtime/event-notify-bus.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/server/agent-runtime/event-notify-bus.ts) | PostgreSQL `LISTEN`/ `NOTIFY` bridge to SSE |
| Resume orchestration | [`lib/server/agent-runtime/resume.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/server/agent-runtime/resume.ts) | Entry point when sessions (re)attach |
| Fold/compact logic | `packages/@openmaic/dsl/src/runtime.ts` | `foldEvents`, `compactReplayEvents` implementations |
| Type definitions | `packages/@openmaic/storage/src/agent-session/types.ts` | Event shapes, `seq` ordering contract |
| Determinism tests | [`tests/workbench/session-fold.test.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/tests/workbench/session-fold.test.ts) | Validation of cold and mid-gap replay |

## Complete Replay Example

The following pattern, derived from the runtime's implementation in [`resume.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/resume.ts), demonstrates full session reconstruction:

```typescript
import { getAgentSessionStore } from '@/lib/server/agent-runtime/store';
import { foldEvents, compactReplayEvents } from '@openmaic/dsl';

interface SessionState {
  messages: Array<{ role: string; content: string }>;
  generating: boolean;
  seq: number;
}

async function rebuildSession(
  sessionId: string,
  cursor: number
): Promise<SessionState> {
  // 1. Initialize storage layer
  const store = await getAgentSessionStore();
  
  // 2. Fetch events after known cursor
  const { events, scanned } = await store.readEventsAfterForReplay(
    sessionId,
    cursor
  );
  
  // 3. Remove redundant events while preserving order
  const replayable = compactReplayEvents(events);
  
  // 4. Fold into fresh state (undefined = initial state)
  const sessionState = foldEvents(undefined, replayable);
  
  // 5. Return reconstructed state with new cursor
  return {
    ...sessionState,
    seq: scanned  // Latest sequence for next resume
  };
}

```

In production, [`resume.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/resume.ts) automates this workflow—detecting whether to perform cold replay or resume from a partial state based on client-provided cursors.

## Summary

- **Immutable durable log**: All events stored in PostgreSQL with monotonic `seq` ordering
- **Three-phase replay**: Fetch via `readEventsAfterForReplay`, compact via `compactReplayEvents`, fold via `foldEvents`
- **Idempotent guarantee**: `seq` key ensures safe reprocessing of overlapping events
- **Dual consistency modes**: Cold replay from cursor 0, mid-gap replay from `Last-Event-ID`
- **Notification-driven real-time**: PostgreSQL `NOTIFY` triggers SSE updates with polling fallback

## Frequently Asked Questions

### How does OpenMAIC handle duplicate events during replay?

The `seq` field provides unique positioning for every event. When `foldEvents` processes a stream, events with `seq` values already incorporated into state are ignored. This makes the fold idempotent—replaying the same prefix multiple times produces identical results.

### What happens if the PostgreSQL notification is lost?

Correctness is maintained through polling fallback. The SSE client periodically queries `readEventsAfterForReplay` with its last known cursor, discovering any events missed due to notification failures. Notifications optimize latency; the durable log guarantees consistency.

### Can session replay work across server restarts?

Yes. All event state resides in PostgreSQL, not server memory. A new server process can reconstruct any session's state by reading the persistent log and applying the standard fold operation, as implemented in [`resume.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/resume.ts).

### Where is the event ordering contract defined?

The `seq` field and event type definitions are located in `packages/@openmaic/storage/src/agent-session/types.ts`. The runtime in `packages/@openmaic/dsl/src/runtime.ts` implements the fold logic that consumes this contract.