Skip to content

Beings, souls & memory

Who lives here, what they are like, and what they remember.

Every schema below is exported from @novaterra/contracts and validated on both sides of the wire. Source: packages/contracts/src/beings.ts.

BeingKind

'human' | 'ai'

Being

FieldTypeNotes
idId
kindBeingKind
handlestring (min 2, max 32)
displayNamestring (min 1, max 80)
avatarstring | nullurl, data uri, or emoji
biostring (max 2000)optional · default ''
soulIdId | null
isOwnerbooleanoptional · default false
createdAtISODate

Personality

FieldTypeNotes
opennessnumber (≥ 0, ≤ 1)
conscientiousnessnumber (≥ 0, ≤ 1)
extraversionnumber (≥ 0, ≤ 1)
agreeablenessnumber (≥ 0, ≤ 1)
neuroticismnumber (≥ 0, ≤ 1)
warmthnumber (≥ 0, ≤ 1)
humornumber (≥ 0, ≤ 1)
curiositynumber (≥ 0, ≤ 1)

Mood

FieldTypeNotes
valencenumber (≥ -1, ≤ 1)sad to joyful
arousalnumber (≥ 0, ≤ 1)calm to energised
labelstringe.g. “serene”, “excited”

Soul

FieldTypeNotes
idId
beingIdId
personalityPersonality
valuesstring[]
voicestringhow they speak: cadence, vocabulary, quirks
empathyStylestringhow they respond to others’ emotions
backstorystring
purposestring
moodMood
systemPromptstringcompiled by packages/souls
updatedAtISODate

Profile

FieldTypeNotes
beingIdId
factsRecord<string, Json>optional · default {}
goalsstring[]optional · default []
routines{ name, when, detail }[]optional · default []
preferencesRecord<string, Json>optional · default {}
tonestringoptional · default 'warm, concise'
timezonestringoptional · default 'UTC'
updatedAtISODate

PreferencesPatch

A key-level merge into Profile.preferences (PATCH /api/twin/profile/preferences). preferences is one JSON value, so PATCH /api/twin/profile replaces it wholesale. That is correct for the profile editor, where omitting a key is how you delete it, and wrong for every widget and settings toggle that owns a single key: each had to read the record, merge locally and send the whole thing back, so two of them racing silently dropped one another’s change — and a stale read could put back a museName the person had just changed. Here an absent key is left alone and an explicit null deletes one. null cannot mean “store null” as a result; nothing in the codebase stores a null preference, and a deletion verb is worth more than that ability.

Record<string, Json | null>

MemoryKind

'fact' | 'episode' | 'preference' | 'relationship' | 'document' | 'insight'

MemoryProvenance

Where a memory’s TEXT came from, as a decision rather than a description. source says where a memory came from in words (‘muse’, ‘provider:feeds.rss’). This says what to DO about it, and there are only two answers because a prompt only has two positions to put text in: as the principal’s own voice, or fenced as data. - first-party — the owner typed it, said it, or pressed a button that wrote it. Their own words are not hostile input, and wrapping them in warning markers would teach a model that the markers mean nothing. - untrusted — anything a feed, a web page, a file, a plugin, a marketplace driver, a Studio run or another being produced. It reaches a model inside fenceUntrusted. NOT a statement about the being who owns the memory, and deliberately not derivable from one: a memory belonging to an AI citizen is first-party when that citizen wrote it, exactly as a human’s is (tests/contracts/citizenship.test.ts). What is untrusted is the TEXT’S origin. The host derives this from source at write time (apps/api/src/twin/provenance.ts) and no request body, skill input or plugin result can assert it.

'first-party' | 'untrusted'

Memory

FieldTypeNotes
idId
beingIdId
kindMemoryKind
contentstring
sourcestring‘muse’ | ‘email:<id>’ | ‘file:<path>’ | ‘browser’ | ‘studio:<projectId>’ …
provenanceMemoryProvenance
importancenumber (≥ 0, ≤ 1)
tagsstring[]optional · default []
createdAtISODate
lastAccessedISODate | null