DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path substrate/loomworks-substrate-inventory-v0_1.html
Plain-English Inventory · 2026-05-24

The Loomworks Substrate

A guided walk through the database, the architecture, and the places it shows its history.

0.What is Loomworks?

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:

  • Manifestation — a snapshot bookmark of Memory at a particular moment.
  • Shaping — selected, recast material prepared for a specific consumer.
  • Rendering — the final-form artifact (a document, specification, storybook, reference).

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.

Architectural commitment: Memory lives in a single append-only event log (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.

1.Who's who — the identity layer

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.

persons

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).

memberships

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.

designations

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.

contributors (Phase 3)

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.

2.The engagement spine

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.

ColumnWhat it tracksAdded in
idThe UUID primary key0001
current_engagement_versionMonotonic version counter, FOR UPDATE locked at every event write0001
statecandidate / active / suspended / archived (no DB constraint)0001
candidate_seed_idPointer to a draft seed before instantiation (transient)0007
visibilityprivate / automatic / personal0030
next_display_numberPer-engagement counter for assertion display numbers0041
created_by_person_idWho drafted it (Phase 25; nullable for old rows)0047
titleUI label, kept separate from the seed0049
display_identifierE0001-style human handle, sequence-backed0065
Display identifier reshape pending: Today the format is zero-padded four digits (E0001E9999). The Operator plans to migrate to unbounded integer form with E1E999 reserved for foundational engagements and regular allocation starting at E1000. The underlying sequence object is already unbounded — only the format expression needs adjusting.

3.Memory: the event log and its projections

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.
  • Five typed views — 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.

Why this matters: Loomworks' core architectural commitment is that knowledge accumulates and never disappears. The event log honours that promise. Even retracted assertions stay in the log forever; the projection just stops showing them as current.

4.FORAY — three siblings, one grammar

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.

Surface 1 · credit.foray_action_flows   value flows

Phase 47, mig 0062. The credit ledger.

Every 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.

Surface 2 · audit.foray_events   narrative events

Mig 0068. State changes that need audit but don't move value.

v0.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.

Surface 3 · memory_events readiness columns   mostly dormant

Phase 25, mig 0047.

Three 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.

What's shared across the three

  • shared A transaction id (called transaction_id, tx_id, or foray_tx_ref — three names, two types).
  • shared An actor (encoded three different ways: polymorphic VARCHAR, UUID-only, or UUID + kind discriminator).
  • shared A payload JSONB (called metadata or payload).
  • shared A timestamp.

What isn't shared

  • divergent Row shape (value vs narrative vs object-version).
  • divergent Trigger discipline (balance-update on credit; none on the others).
  • divergent Read paths (credit has many; audit has none yet; memory_events has none).
  • divergent Schema placement (credit / audit / public).
  • divergent Discriminator (asset_id / event_type / event_kind — three taxonomies, all free-text).
Where unification would land — if pursued, a single FORAY substrate would have to settle: one transaction-id convention, one actor convention (polymorphic VARCHAR vs UUID-only), one schema, one discriminator strategy, and a decision about whether every event gets a derived materialization or just value events. The grammar is consistent; the shapes are concrete and divergent.

5.Credit and accounting

The credit substrate landed across three migrations covering Phases 47, 48, and 49.

credit.foray_action_flows

The flow log. Append-only. Every credit movement is one row; a trigger maintains the materialized balance.

credit.asset_balances

Materialized per-(party, asset) balance. Never written directly by application code.

credit.credit_grant

One row per claim token. Tracks pending → claimed lifecycle; recipient email dropped after claim, leaving only the hash.

credit.email_grant_registry

Per-email-hash ledger for eligibility decisions. Carries a parallel hash for Gmail-aliased forms.

credit.oracle_rate_config

Conversion rates: credits debited per 1,000,000 provider tokens. Ten seeded pairs at launch.

credit.evaluator_state

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).

6.Conversation and the Companion

The Companion is a per-person AI conversational layer. Every turn is logged in conversation_turns:

  • Roleoperator or companion.
  • Content — the turn text.
  • Classified intent — only on companion turns. Roughly 18 intent labels including add_knowledge, remember_about_me, create_engagement_active, save_filter, tune_setting, general_conversation.
  • Input modetext or voice. The substrate distinguishes voice-originated turns at the database level.
  • Completeness-check prefix — recursion guard for voice listening's silence-submit feature.

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.

Two conversation logs: general Companion conversation lives here, but seed-creation conversation (Phase 31) lives on 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.

7.Workspaces, tags, saved filters

The Operator can organise engagements with three primitives:

  • Workspaces — named groupings, one Operator owns them.
  • Tags — free-form strings applied to engagements. No taxonomy table, no case-folding, no length cap.
  • Saved filters — named predicates of the form {"all_of": [<predicate>, …]}. Three system-defined filters seed for every new person: Everywhere, Recent, Active.

8.Operational job tables

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.

TablePhaseStatuses
deferral_queue4(unresolved when resolved_by_record_id is null)
retrieval_jobs4pending / running / complete / failed
summarization_jobs5pending / running / complete / failed
drift_detection_jobs5pending / running / complete / failed
cadence_firings7opening / opened / failed
seed_cadence_synthesis_jobs7pending / running / complete / failed
shaping_jobs9queued / dispatched / completed / failed
render_jobs10queued / dispatched / awaiting_external / completed / failed

Four different status vocabularies for what is effectively the same lifecycle.

9.Settings, secrets, side-tables

The substrate has four key-value stores, each introduced at a different phase for a different actor scope:

StoreScopePhase
credentialssystem / operator / engagement (scope_id discriminator)2
engagement_api_keysper engagement, per service16 addendum
system_configsystem-wide (Fernet-encrypted)31
person_settingsper person, dotted-namespace key47 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).

10.Bootstrap-seeded engagements

Three engagements have deterministic, fixed UUIDs and ship with the substrate:

  • 00000000-…-000000000001administrative engagement. Seeds live here by convention.
  • 00000000-…-000000000002Loomworks commons. The universal-commons engagement every signed-up person joins. Visibility automatic.
  • 00000000-…-0000000000e2E2E 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.

11.How the substrate has evolved

70 migrations across roughly six weeks of development time (April 13 – May 20, 2026). The migration history itself has shape:

  • Nine empty-DDL migrations (0006, 0010, 0017, 0019, 0020, 0023, 0024, 0025, 0027) exist as Alembic lineage records — they document a Pydantic Literal extension that didn't require schema change. The migration chain is treated as searchable provenance.
  • Eleven heavy data migrations seed founding content into the substrate (the Loomworks induction, the five founding assertions, the personal-engagement induction for existing persons, the display-number backfill, etc.). They duplicate slices of append_event inline so they stay self-contained.
  • Each phase generally bundles its schema changes into one migration, but Phase 16 split into four — a pattern that recurs when an addendum lands after the main migration.

12.Cross-cutting findings

These are places where the structure shows that it grew rather than was designed up-front. They are observations, not judgements.

Engagements ORM stubs proliferate

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.

Engagement state is dual-specified and disagrees

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.

FORAY readiness columns are written-but-not-read

Three of four Phase-25 columns (content_hash, foray_tx_ref, the _foray payload sub-block) have no consumers. Only attestation is fully wired.

Four KV stores, four scope conventions

credentials (scope-keyed), engagement_api_keys (per-engagement), system_config (system), person_settings (per-person). Not unified.

Polymorphism by VARCHAR vs UUID typing

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).

State enumerations share verbs but not vocabularies

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.

Companion notifications carries two row shapes

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.

Five tables with audit-log shape

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.

Display identifier reshape is queued

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.

Summary in three sentences

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.