A guided walk through the database, the architecture, and the places it shows its history.
Loomworks is a knowledge-work engine. It hosts long-lived engagements — persistent spaces where people and AI agents accumulate, refine, and produce structured knowledge together. Every contribution is preserved; nothing is erased.
An engagement starts with a seed — a founding document declaring what the work is, who consumes it, what voice it takes, and what success looks like. Once the seed is committed, people and agents add assertions (statements with provenance) into the engagement's Memory. The Memory grows; it never shrinks. Corrections supersede prior assertions but don't replace them.
From an engagement's Memory the engine produces three downstream artifacts in a pipeline:
People interact with the engine through a per-Operator Companion — an AI conversational partner that can converse, classify intent, propose actions, surface notifications, and write to a private personal memory. The Companion runs on credits; consumption is metered per token through a small accounting substrate.
memory_events) and is projected into typed materialized views (current state, shape events, render events, manifestations, compositions). The event log is canonical; everything else is rebuildable from it.
Loomworks tracks persons (global identity, one row per natural human) joined to engagements through memberships, with stackable designations declaring what each person does in that engagement.
Global identity. Started small in Phase 14; has grown to ~18 columns covering the Companion (name, personal-engagement pointer), the credit substrate (tier, account status, expiration), and Phase-49 thresholds (three near-exhaustion timestamps).
Person ↔ engagement join. Soft-deleted via an active flag (the substrate forbids hard delete). Each membership carries a per-Operator sequence number — slot 0 reserved for the person's Personal engagement, 1+ for everything else.
Stackable role flags. Three values today: operator, contributor, domain_expert. A person can hold multiple designations on the same engagement. The set lives in Python as a frozenset; no database constraint.
The older bearer-token actor model. Still around, still used for token auth. Never merged with persons. A Phase-3 holdover that the substrate has carried through identity evolution.
WebAuthn credentials, recovery codes, and TOTP secrets all live on the person; the substrate supports the full passkey-and-recovery flow.
The engagements table is the root row for every space in the system. It started with four columns in Phase 1; it now carries ten, accumulated through ten migrations.
| Column | What it tracks | Added in |
|---|---|---|
id | The UUID primary key | 0001 |
current_engagement_version | Monotonic version counter, FOR UPDATE locked at every event write | 0001 |
state | candidate / active / suspended / archived (no DB constraint) | 0001 |
candidate_seed_id | Pointer to a draft seed before instantiation (transient) | 0007 |
visibility | private / automatic / personal | 0030 |
next_display_number | Per-engagement counter for assertion display numbers | 0041 |
created_by_person_id | Who drafted it (Phase 25; nullable for old rows) | 0047 |
title | UI label, kept separate from the seed | 0049 |
display_identifier | E0001-style human handle, sequence-backed | 0065 |
E0001…E9999). The Operator plans to migrate to unbounded integer form with E1–E999 reserved for foundational engagements and regular allocation starting at E1000. The underlying sequence object is already unbounded — only the format expression needs adjusting.
Everything an engagement "remembers" is a row in memory_events — the canonical append-only log. Each row is one version of one object (an assertion, a relationship, a shape event, a render event, a manifestation, a composition, a consideration, a cadence firing, a key-provided record, …).
Sixteen-plus object types are registered in the type registry. They all write to the same log. They are projected into:
current_memory_objects — generic current-state view of every object.shape_events_view, render_events_view, manifestation_view, external_production_records_view, render_compositions_view. Each unpacks JSONB payload into scalar columns to make queries fast.The projections are all rebuildable from memory_events. The event log is the source of truth; views are derived.
FORAY is the substrate's record-of-action concept. It currently has three concrete surfaces. They share a grammar but not a shape — and the Operator has flagged this as architecturally worth examining.
credit.foray_action_flows value flowsEvery credit movement is one row. Trigger-maintained materialized balances. Five flow shapes (issuance, consumption-of-5-per-turn, suspension, reactivation, deletion, balance-zeroing, referral). Parties are polymorphic: UUIDs for persons, literal strings ('dunin7', 'anthropic') for institutional parties.
audit.foray_events narrative eventsv0.1 ships one event type: setting_change, written when an Operator tunes a preference. Actor is always a person (UUID-typed, NOT NULL). A signature column is reserved for future Kaspa-anchoring; never written today.
memory_events readiness columns mostly dormantThree columns plus an in-payload _foray sub-block. content_hash is written every event but never read. foray_tx_ref is never written. The _foray sub-block is injected for 22 anchorable event kinds but never read. Only attestation (WebAuthn at engagement-commit) is fully wired.
transaction_id, tx_id, or foray_tx_ref — three names, two types).metadata or payload).credit / audit / public).The credit substrate landed across three migrations covering Phases 47, 48, and 49.
The flow log. Append-only. Every credit movement is one row; a trigger maintains the materialized balance.
Materialized per-(party, asset) balance. Never written directly by application code.
One row per claim token. Tracks pending → claimed lifecycle; recipient email dropped after claim, leaving only the hash.
Per-email-hash ledger for eligibility decisions. Carries a parallel hash for Gmail-aliased forms.
Conversion rates: credits debited per 1,000,000 provider tokens. Ten seeded pairs at launch.
Persistent per-evaluator state (suspension/deletion + reconciliation).
Plus eleven lifecycle columns added to persons across two phases (tier, account status, expiration, referral, exhaustion preferences, three near-exhaustion timestamps, previous responder model).
The Companion is a per-person AI conversational layer. Every turn is logged in conversation_turns:
operator or companion.add_knowledge, remember_about_me, create_engagement_active, save_filter, tune_setting, general_conversation.text or voice. The substrate distinguishes voice-originated turns at the database level.Companion notifications (companion_notifications) are proactive messages: informational alerts and approval cards live in the same table, distinguished by whether approval_status is non-null. Lifecycle: pending → delivered → seen → dismissed.
memory_events as operator_turn / companion_turn events on the candidate engagement. The split is deliberate — seed conversation is auditable engagement memory; general conversation is operational state.
The Operator can organise engagements with three primitives:
{"all_of": [<predicate>, …]}. Three system-defined filters seed for every new person: Everywhere, Recent, Active.Eight tables across Phases 4-10 track agent dispatch and background work. They share a broad shape but no shared base — each was introduced when its agent landed.
| Table | Phase | Statuses |
|---|---|---|
| deferral_queue | 4 | (unresolved when resolved_by_record_id is null) |
| retrieval_jobs | 4 | pending / running / complete / failed |
| summarization_jobs | 5 | pending / running / complete / failed |
| drift_detection_jobs | 5 | pending / running / complete / failed |
| cadence_firings | 7 | opening / opened / failed |
| seed_cadence_synthesis_jobs | 7 | pending / running / complete / failed |
| shaping_jobs | 9 | queued / dispatched / completed / failed |
| render_jobs | 10 | queued / dispatched / awaiting_external / completed / failed |
Four different status vocabularies for what is effectively the same lifecycle.
The substrate has four key-value stores, each introduced at a different phase for a different actor scope:
| Store | Scope | Phase |
|---|---|---|
| credentials | system / operator / engagement (scope_id discriminator) | 2 |
| engagement_api_keys | per engagement, per service | 16 addendum |
| system_config | system-wide (Fernet-encrypted) | 31 |
| person_settings | per person, dotted-namespace key | 47 voice listening |
They are not unified. Each uses its own scope encoding.
Other side-tables: shape_event_title (Operator label per shape), render_specialist_binding (durable agent-registry entry, the only persisted agent registration), uploaded_files (binary file metadata; bytes on disk), email_send_attempts (per-send observability), migration_events (Phase 14 identity-migration audit).
Three engagements have deterministic, fixed UUIDs and ship with the substrate:
00000000-…-000000000001 — administrative engagement. Seeds live here by convention.00000000-…-000000000002 — Loomworks commons. The universal-commons engagement every signed-up person joins. Visibility automatic.00000000-…-0000000000e2 — E2E test sandbox. Hosts Playwright mutation traffic so live engagements stay clean.Plus a per-person Personal engagement created at signup — invisible, Memory-only, visibility personal, hosts the Companion's per-person knowledge.
70 migrations across roughly six weeks of development time (April 13 – May 20, 2026). The migration history itself has shape:
append_event inline so they stay self-contained.These are places where the structure shows that it grew rather than was designed up-front. They are observations, not judgements.
At least six different layers each declare their own minimal _EngagementRow mapping. Each declares a subset of the engagements row's ten columns. They are silently drifting from the actual table shape; nothing enforces consistency.
The Pydantic Engagement.state Literal admits ("active", "suspended", "archived"); the database has no CHECK constraint; raw SQL inserts 'candidate'. Neither enumeration fully describes the in-use value set.
Three of four Phase-25 columns (content_hash, foray_tx_ref, the _foray payload sub-block) have no consumers. Only attestation is fully wired.
credentials (scope-keyed), engagement_api_keys (per-engagement), system_config (system), person_settings (per-person). Not unified.
The substrate encodes "who did this" three different ways across its three audit-shaped surfaces: polymorphic VARCHAR (credit), UUID-only NOT NULL (audit), UUID + kind discriminator (memory_events).
At least seven state columns across MemoryObject types and projection views use overlapping verbs (active / retired / confirmed / superseded / current / produced / held / committed / …). "Current" on Manifestation means the same thing as "confirmed" on ShapeEvent, under different names.
Informational notifications (Phase 44) and approval cards (Phase 45) share one table with six nullable-by-shape columns, discriminated on whether approval_status is non-null.
The substrate has five append-only event-log-shaped tables (memory_events, migration_events, credit.foray_action_flows, audit.foray_events, plus per-event tables like email_send_attempts). Five tables, five concrete shapes, one recurring grammar.
Current format (E#### zero-padded) was introduced recently; the Operator wants to move to unbounded form. The underlying sequence is already unbounded — only the format expression and assignment policy need to change.
Loomworks is a knowledge-work engine built on a single append-only event log with typed projections — an architecture that honours its core promise that knowledge accumulates and never disappears. After 70 migrations across roughly six weeks of phased development, the substrate covers identity, engagement lifecycle, memory and projections, credit and accounting, conversation and the Companion, settings, audit, grouping, and operational bookkeeping. The picture is coherent at the level of grammar; at the level of shapes it shows the seams of incremental growth — the FORAY substrate splits into three siblings, four key-value stores exist side by side, status enumerations repeat without sharing, and several columns are wired ahead of integrations that haven't landed.