User Memory
User memory is the application of Causet’s event-sourced state model to user-level history. Every action a user takes generates an event. Core rules accumulate those events into a structured, queryable memory on the user entity.
The Concert App Example
In the Jamlet concert discovery application, a user’s memory builds from every concert they attend, every show they like, and every artist they follow:
state:
user:
entity_key: user_id
fields:
- name: concert_count
type: int
default: 0
- name: concerts_attended
type: array
item_type: object
item_fields:
show_id: { type: string, required: true }
venue: { type: string, required: true }
artist_id: { type: string, required: true }
attended_at: { type: datetime, required: true }
default: []
- name: total_distance_traveled
type: number
default: 0
- name: liked_show_ids
type: array
item_type: string
default: []
- name: followed_artist_ids
type: array
item_type: string
default: []
- name: favorite_venue
type: string
default: ""
- name: preferred_genres
type: array
item_type: string
default: []Building User Memory via Core Rules
Recording Concert Attendance
core:
rules:
- name: record_concert
when: {}
then:
- op: add
path: /concert_count
value: 1
- op: push
path: /concerts_attended
value:
show_id: intent.show_id
venue: intent.venue
artist_id: intent.artist_id
attended_at: intent.attended_at
- op: add
path: /total_distance_traveled
value: intent.distance_milesThis rule fires on every CONCERT_ATTENDED intent. After 48 concerts, the user has concert_count: 48, a full history in concerts_attended, and their cumulative travel distance.
Recording Liked Shows
core:
rules:
- name: record_show_like
when:
- path: /liked_show_ids
op: not_contains
value: intent.show_id
then:
- op: push
path: /liked_show_ids
value: intent.show_idThe not_contains guard prevents duplicate entries if a SHOW_LIKED intent is submitted more than once for the same show.
Recording Artist Follows
core:
rules:
- name: record_artist_follow
when:
- path: /followed_artist_ids
op: not_contains
value: intent.artist_id
then:
- op: push
path: /followed_artist_ids
value: intent.artist_idQuerying User Memory
Entity Snapshot (per-user)
Access a specific user’s full memory via the entity snapshot:
const user = await causet.getEntity("user", "user_abc123");
console.log(`Concerts attended: ${user.concert_count}`);
console.log(`Total distance: ${user.total_distance_traveled} miles`);
console.log(`Favorite venue: ${user.favorite_venue}`);
console.log(`Artists followed: ${user.followed_artist_ids.length}`);CLI Entity Inspection
Useful for debugging and support tooling:
causet inspect entity user_abc123 --fork main --stream userHTTP Entity Endpoint
The causet-query-service exposes an HTTP endpoint for entity queries:
GET /v1/entities/user/user_abc123Projecting User Memory for Aggregate Queries
Per-user memory is efficient for single-user reads. For aggregate queries across users, materialize the relevant fields into projection tables.
Artist Affinity Projection
projections:
user_artist_affinity:
source_events: [CONCERT_ATTENDED]
target:
table: user_artist_affinity
primary_key: [user_id, artist_id]
fields:
user_id: TEXT
artist_id: TEXT
concert_count: BIGINT
derive:
user_id: event.entity_id
artist_id: event.payload.artist_id
aggregates:
CONCERT_ATTENDED:
concert_count: { op: add, by: 1, floor: 0 }Query: all users who have seen Pearl Jam more than 3 times:
SELECT user_id, concert_count
FROM user_artist_affinity
WHERE artist_id = 'artist_pearl_jam'
AND concert_count > 3
ORDER BY concert_count DESC;Venue Affinity Projection
projections:
user_venue_affinity:
source_events: [CONCERT_ATTENDED]
target:
table: user_venue_affinity
primary_key: [user_id, venue]
fields:
user_id: TEXT
venue: TEXT
visit_count: BIGINT
derive:
user_id: event.entity_id
venue: event.payload.venue
aggregates:
CONCERT_ATTENDED:
visit_count: { op: add, by: 1, floor: 0 }User Memory for AI Features
User memory provides structured, auditable context for AI-driven features. Unlike raw event logs, entity state is already aggregated and typed — it can be passed directly to an LLM or recommendation model.
Example: generating a personalized concert recommendation prompt using user memory:
const user = await causet.getEntity("user", userId);
const context = {
concerts_attended: user.concert_count,
top_artists: user.concerts_attended
.reduce((acc, c) => {
acc[c.artist_id] = (acc[c.artist_id] || 0) + 1;
return acc;
}, {} as Record<string, number>),
total_distance_miles: user.total_distance_traveled,
favorite_venue: user.favorite_venue,
};
const prompt = `
User has attended ${context.concerts_attended} concerts.
Most-seen artists: ${Object.entries(context.top_artists)
.sort(([, a], [, b]) => b - a)
.slice(0, 3)
.map(([id, count]) => `${id} (${count}x)`)
.join(", ")}.
Favorite venue: ${context.favorite_venue}.
Recommend shows that match this profile.
`;The key advantage: this context is grounded in the ledger. It is not synthesized or hallucinated — every data point corresponds to a committed event with an audit trail.
Privacy and PII
User memory is personal data. Each field in the user state definition that tracks individual behavior is potentially PII under GDPR, CCPA, or equivalent regulations.
Considerations:
concerts_attendedcontains location data (venue) and behavioral historytotal_distance_traveledreveals mobility patternsfollowed_artist_idsreveals cultural preferences
The append-only ledger makes hard deletion complex. If you need to support right-to-erasure:
- Define a
USER_MEMORY_CLEAREDevent that resets all memory fields to their defaults - Implement a projection that suppresses deleted users from query results
- Consult your legal team on whether ledger retention requires a separate redaction mechanism
See Security: PII and Multitenancy for Causet’s tenant isolation and data handling model.