Skip to content

Event Sourcing

Stoneforge stores events, not state. Every mutation — creating a task, changing a priority, closing a bug — generates an immutable event record. Current state is derived by replaying events. The events are the source of truth; state is a cached computation.

Why events instead of state

Traditional systems store only the current state. When you update a task’s status from open to in_progress, the old status is overwritten and lost forever. In a multi-agent system, this creates serious problems:

No audit trail

Who changed what, and when? With state-only storage, you can’t answer this without adding a separate logging system.

No time travel

What did this task look like yesterday? The previous state is gone — overwritten by the latest update.

No accountability

Which agent closed this task incorrectly? When multiple agents write to the same data, attribution disappears.

Conflict blindness

Which update wins when two agents edit simultaneously? Without a history of changes, conflict resolution is guesswork.

Event sourcing solves all of these by recording every change as a first-class, immutable record.

The Event record

Every mutation in Stoneforge generates an event with these fields:

FieldTypeDescription
idnumberAuto-incrementing, globally ordered
elementIdElementIdThe element that was changed
eventTypeEventTypeCategory of change (e.g., created, updated, closed)
actorEntityIdWho made the change (agent ID, user ID, or system:*)
oldValueobject | nullPrevious state (null for created events)
newValueobject | nullNew state (null for deleted events)
createdAtTimestampWhen the change occurred

Event types

Events are categorized by what changed. Each category captures different kinds of state transitions.

Lifecycle events

Core element state changes:

EventMeaningoldValuenewValue
createdNew element bornnullFull element state
updatedFields changedChanged fields onlyNew values
closedWork completedPrior stateState with closedReason
reopenedClosed undoneClosed stateOpen state
deletedSoft delete (tombstone)Prior statenull

Dependency events

Relationship changes between elements:

EventMeaningContext
dependency_addedNew relationship createdBoth element IDs and dependency type
dependency_removedRelationship deletedBoth element IDs and dependency type

System events

Events triggered by Stoneforge’s internal machinery:

EventMeaningActor
auto_blockedElement automatically blocked by unresolved dependencysystem:blocked-cache
auto_unblockedAll blocking dependencies resolvedsystem:blocked-cache
tag_addedTag attached to elementThe agent or user who added it
tag_removedTag removed from elementThe agent or user who removed it
member_addedElement added to a collectionThe agent or user who added it
member_removedElement removed from a collectionThe agent or user who removed it

How state is derived

State in Stoneforge is a projection of the event stream. Given a sequence of events for an element, you replay them in order to compute the current state:

Event 1 Event 2 Event 3 Event 4
(created) (updated) (auto_blocked) (closed)
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ status: │ │ status: │ │ status: │ │ status: │
│ open │───▶│ open │───▶│ blocked │───▶│ closed │
│ priority:│ │ priority:│ │ priority:│ │ priority:│
│ 3 │ │ 1 │ │ 1 │ │ 1 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
Snapshot 1 Snapshot 2 Snapshot 3 Snapshot 4

Each event type maps to a state transformation:

  • created — Initialize state from newValue
  • updated / closed / reopened — Replace state with newValue
  • deleted — Set state to null (tombstone)
  • auto_blocked / auto_unblocked — Update the status field only
  • Dependency/tag events — Stored separately, don’t modify element state

The SQLite database stores the latest computed state for fast reads. But because every event is preserved, you can reconstruct the state at any point in history.

Timeline snapshots

You can generate a complete timeline of an element’s history — useful for debugging agent behavior and understanding how a task evolved:

[2024-01-15T09:00:00.000Z] Created by director-1
Status: open
Priority: 3
[2024-01-15T09:30:00.000Z] Updated priority by director-1
Status: open
Priority: 1
[2024-01-15T10:00:00.000Z] Automatically blocked (dependency not satisfied)
Status: blocked
Priority: 1
[2024-01-15T11:00:00.000Z] Automatically unblocked (blockers resolved)
Status: open
Priority: 1
[2024-01-15T12:00:00.000Z] Closed by worker-1: Done
Status: closed
Priority: 1

This timeline is reconstructed purely from the event stream. No separate logging infrastructure needed — the events are the log.

Events vs. JSONL sync

The event system and the JSONL sync system are separate systems with different purposes. Don’t confuse them:

AspectEvents (SQLite)Sync (JSONL)
PurposeAudit trail and historyData portability and Git collaboration
GranularityEvery individual mutationFinal state snapshots
Storageevents SQL table.stoneforge/sync/*.jsonl files
Use case”What happened to this task?""Sync this project across branches”
Persists acrossServer lifetimeGit history (forever)

Storage model

Events are stored in a dedicated SQLite table with auto-incrementing IDs that provide a global ordering:

CREATE TABLE events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
element_id TEXT NOT NULL REFERENCES elements(id) ON DELETE CASCADE,
event_type TEXT NOT NULL,
actor TEXT NOT NULL,
old_value TEXT, -- JSON
new_value TEXT, -- JSON
created_at TEXT NOT NULL
);
CREATE INDEX idx_events_element ON events(element_id);
CREATE INDEX idx_events_type ON events(event_type);
CREATE INDEX idx_events_actor ON events(actor);
CREATE INDEX idx_events_created_at ON events(created_at);

The four indexes support the most common query patterns: “all events for this element,” “all events of this type,” “all events by this actor,” and “all events in this time range.”

Next steps