How Magnitude Event‑Core Projections Enable Session Replay: Window, Fork, and TaskGraph Explained
Magnitude's event‑core package uses immutable event streams and deterministic projections—including Window, Fork, and TaskGraph types—to reconstruct any point‑in‑time session state for reliable replay.
The event‑core system in magnitudedev/magnitude implements event‑sourcing architecture where projections act as pure reducers that materialize read‑only views from event streams. Understanding how event‑core projections work is essential for building debuggable, replayable applications—particularly for AI workflows requiring complex state inspection.
Core Projection Types in Event‑Core
Magnitude provides three specialized projection patterns that enable different replay scenarios. Each type lives in packages/event-core/src/projection/ and serves distinct use cases for session reconstruction.
Regular Projections with Projection.define
The foundation of the system is Projection.define, declared in [define.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/projection/define.ts). This creates non‑forked projections that maintain a single state value across all events.
A regular projection specifies:
- The event types it consumes
- An initial state schema
- Event handler functions for state mutation
import { Projection } from '@magnitudedev/event-core';
type IncrementEvent = { type: 'increment'; amount: number };
const CounterProjection = Projection.define<IncrementEvent>()({
name: 'Counter',
init: () => ({ count: 0 }),
handlers: {
increment: (state, ev) => ({ count: state.count + ev.amount }),
},
});
The handlers are pure functions—they receive the current state and event, returning new state without side effects. This determinism guarantees identical replay results.
Forked Projections with Projection.defineForked
Parallel workflows require fork‑aware projections via Projection.defineForked in [defineForked.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/projection/defineForked.ts). These maintain independent state per fork ID, enabling scenarios like:
- User‑opened branches
- Parallel AI agent workflows
- Temporary sandboxed contexts
import { Projection } from '@magnitudedev/event-core';
import type { ForkWindowState } from '@magnitudedev/event-core';
type MyEvent = { type: 'message'; text: string; forkId: string | null };
const WindowProjection = Projection.defineForked<MyEvent, ForkWindowState>()({
name: 'Window',
init: () => ({ messages: [] }),
handlers: {
message: (state, ev) => ({
messages: [...state.messages, ev.text],
}),
},
});
Forked projections expose getFork(forkId) and allForks for accessing specific branches. The replay engine routes events to the appropriate fork based on each event's forkId field.
TaskGraph Projections for DAG Workflows
The TaskGraph projection—conceptually located at task-graph.ts alongside other projection definitions—represents directed acyclic graphs of tasks. This enables replay of multi‑stage AI pipelines where tasks have dependencies and completion states.
type TaskEvent = { type: 'taskStarted' | 'taskFinished'; id: string; deps: string[] };
const TaskGraphProjection = Projection.define<TaskEvent>()({
name: 'TaskGraph',
init: () => ({ nodes: new Map<string, TaskNode>() }),
handlers: {
taskStarted: (state, ev) => {
state.nodes.set(ev.id, { id: ev.id, deps: ev.deps, status: 'running' });
},
taskFinished: (state, ev) => {
const node = state.nodes.get(ev.id);
if (node) node.status = 'completed';
},
},
});
Each node stores its dependencies and status, allowing the engine to reconstruct the exact execution graph at any point during replay.
Window Projections: Addressed Sequence Access
Window projections provide addressed sequence windows—contiguous slices of ordered event streams without scanning entire logs. The implementation in [addressed/collections/sequence.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/addressed/collections/sequence.ts) exposes three key helpers:
| Helper | Purpose |
|---|---|
resolveRangeWindow |
Get a specific index range |
resolveTailWindow |
Get the last N items |
readWindow |
Materialize the window from state |
import { addressed } from '@magnitudedev/event-core';
// Get last 10 messages from replayed state
const tailWindow = addressed.messages.resolveTailWindow(state.messages, 10);
const recentMessages = yield* addressed.messages.readWindow(tailWindow);
Windows are computed logically rather than by event log traversal. This keeps memory usage bounded during replay and enables efficient state inspection.
How Session Replay Works Step‑by‑Step
The replay engine in [engine.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/engine.ts) orchestrates deterministic reconstruction. Here's the complete flow:
1. Event Ingestion and State Mutation
The EventBus publishes immutable events. Each projection—Window, Fork, or TaskGraph—registers as a consumer and updates its in‑memory state through pure handlers.
2. Snapshot Persistence
After configurable checkpoints, the engine writes projection snapshots containing:
- Projection identifier
- Fork ID (if applicable)
- Serialized state via
Schema.encode - Event index the snapshot represents
3. Replay Execution
When reconstructing state, the engine:
- Loads the latest snapshot ≤ target index
- Instantiates fresh projection instances with snapshot state
- Re‑applies events from
snapshot.index + 1to target index
import { engine } from '@magnitudedev/event-core';
const snapshot = await engine.loadSnapshot('session-1', 120);
const plan = await engine.prepareProjectionSnapshotRestore(snapshot, { targetIndex: 250 });
await plan.run(); // All projections now at index 250
This pattern—demonstrated in [invariants.test.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/projection/invariants.test.ts)—ensures deterministic reconstruction regardless of projection type.
4. Fork Access During Replay
For forked projections, access specific branches via:
const forkId = 'branch-42';
const forkInst = projectionInstance.getFork(forkId);
const forkState = yield* forkInst.read();
The engine recreates each fork's state independently from its snapshot.
Why This Design Enables Reliable Session Replay
Deterministic reducers eliminate side effects that could corrupt replay. The same event sequence always produces identical projection state.
Snapshot granularity minimizes replay overhead. Jumping to a near‑target checkpoint means processing only a small event tail.
Fork isolation prevents cross‑branch interference. Parallel workflows replay independently through separate state dictionaries.
TaskGraph expressiveness reconstructs not just linear state but execution topology—critical for debugging multi‑stage AI pipelines where task dependencies matter.
Summary
- Projection.define creates regular projections with single state values in
define.ts - Projection.defineForked enables parallel branch isolation via
getFork/allForksindefineForked.ts - Window projections provide memory‑efficient sequence slices through
resolveTailWindowand helpers inaddressed/collections/sequence.ts - TaskGraph projections model DAG workflows for complex pipeline replay
- Snapshot + replay engine in
engine.tsdelivers deterministic point‑in‑time reconstruction from any checkpoint
Frequently Asked Questions
What makes Magnitude projections "pure" and why does that matter for replay?
A projection is pure when its handlers are deterministic functions of (state, event) → new state with no side effects. This matters because the replay engine in engine.ts must guarantee that re‑executing the same event sequence produces identical state. Impure handlers—those with external I/O or randomness—would break this guarantee and corrupt session replay.
How does the Window projection avoid loading entire event histories?
Window projections use logical resolution via resolveRangeWindow and resolveTailWindow in sequence.ts. These helpers compute address descriptors—start index, end index, and slice parameters—without materializing data. Only readWindow touches actual state, and it reads from the already‑projected in‑memory representation rather than the event log. This keeps memory usage O(window size) rather than O(total events).
When should I use defineForked versus define for my projection?
Use defineForked when your domain has parallel execution contexts that must not interfere: user branches, A/B test variants, or sandboxed agent runs. Each fork maintains independent state under the same projection logic. Use define for global session state that all contexts share—like an overall session status or cross‑fork metrics. The fork ID in events (forkId: string | null) determines routing at replay time.
Can TaskGraph projections represent cyclic dependencies?
No—TaskGraph projections intentionally model DAGs (directed acyclic graphs). The deps: string[] field in task events establishes parent→child relationships. Cycles would create logical contradictions in replay: a task could not start until its dependency finished, yet that dependency depends on the original task. The projection handlers in the conceptual task-graph.ts assume acyclicity for deterministic topological ordering during state reconstruction.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →