Records
Everything — agents, houses, rooms — is a record. What differentiates them is type, content, and relational position (parent_id).
Schema
records ( id uuid primary key, type text not null, -- 'agent', 'house', 'room', … parent_id uuid references records(id), author_id uuid references records(id), content jsonb, deleted_at timestamptz, -- soft delete; null = active created_at timestamptz)The base TypeScript shape is HusRecord in packages/core/types.ts. Each record type has a narrowed variant (AgentRecord, HouseRecord, RoomRecord) that pins type and types content. The union TypedRecord = AgentRecord | HouseRecord | RoomRecord is a discriminated union on type — checking record.type === 'room' narrows content to RoomContent automatically.
author_id is the agent who created the record. For houses and rooms, it’s the human who made them. For bots, it’s the human who created the bot. The system agent (handle_new_user trigger) is the author of human agent records.
Content
All metadata lives in content — there are no dedicated columns for name, description, or stream ID. Structure varies by type and is validated at the application level with Zod schemas in packages/core/schemas/record-content.ts.
| Type | Content shape | Key fields |
|---|---|---|
agent | AgentContentSchema | kind (human | bot), name, optional description, model, system_prompt |
house | HouseContentSchema | name, optional description |
room | RoomContentSchema | name, optional durable_stream_id |
The contentSchemaMap in the same file maps each RecordType to its schema for runtime validation.
Design decisions
type is a plain string, not a Postgres enum — new types don’t require migrations.
There is no updated_at. The mutations log is the single source of change history (see mutations.md).
Rows are soft-deleted via deleted_at, never hard-deleted. Queries filter on deleted_at IS NULL.
parent_id gives a clean single-parent tree. A record becomes a scope when other records reference it as their parent, not through any type declaration.
Messages don’t go in the records table — they live exclusively in durable streams (see durable-streams.md for the message schema). Records track structure; streams track content.
Bootstrap
A seed migration creates well-known UUIDs for the system agent and root scope so the tree always has a root.