StateApplication Memory

Application Memory

Application memory is entity state that builds up incrementally over time. It is not a separate system — it is entity state where the fields accumulate history rather than representing current workflow status.

What Application Memory Looks Like

Consider a user in a concert attendance application. After using the app for two years:

  • Has attended 48 concerts
  • Has seen Pearl Jam 7 times (most-attended artist)
  • Has traveled 1,200 miles total to shows
  • Favorite venue is The Stone Pony (most-visited)
  • Most common genre is indie rock
  • Typically attends on Friday and Saturday evenings

None of these values were set manually. Each was derived from events as they were committed to the ledger.

Defining Memory Fields

Memory fields are ordinary entity state fields. The difference is in how core rules update them — via accumulation operations (add, push, merge) rather than point-in-time set operations.

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: artist_play_counts
        type: array
        item_type: object
        item_fields:
          artist_id: { type: string, required: true }
          play_count: { type: int, required: true }
        default: []
      - name: favorite_venue
        type: string
        default: ""
      - name: venue_visit_counts
        type: array
        item_type: object
        item_fields:
          venue_id: { type: string, required: true }
          visit_count: { type: int, required: true }
        default: []

Building Memory via Core Rules

Each time a CONCERT_ATTENDED event is processed, core rules fire and update the accumulating fields:

core:
  rules:
    - name: increment_concert_count
      when: {}
      then:
        - op: add
          path: /concert_count
          value: 1
 
    - name: record_concert
      when: {}
      then:
        - op: push
          path: /concerts_attended
          value:
            show_id: intent.show_id
            venue: intent.venue
            artist_id: intent.artist_id
            attended_at: intent.attended_at
 
    - name: accumulate_distance
      when:
        - path: intent.distance_miles
          op: gt
          value: 0
      then:
        - op: add
          path: /total_distance_traveled
          value: intent.distance_miles
 
    - name: update_venue_visits
      when: {}
      then:
        - op: find
          path: /venue_visit_counts
          where: "item.venue_id == intent.venue_id"
          as: existing_venue
        - op: if
          expr: "existing_venue != null"
          then:
            - op: map
              path: /venue_visit_counts
              as: v
              value: "v.venue_id == intent.venue_id ? { venue_id: v.venue_id, visit_count: v.visit_count + 1 } : v"
          else:
            - op: push
              path: /venue_visit_counts
              value:
                venue_id: intent.venue_id
                visit_count: 1
⚠️

There is no single op: upsert for arrays. “Find-or-append” on an array field is composed from find (locate an existing item), if (branch on whether it was found), and map or push (update or append). Verify the exact expression syntax for your compiler version — find’s where, if’s expr, and map’s value all take expression strings, but ternary support and the item/as-bound variable names should be confirmed against DSL: Operations before relying on this pattern.

These rules are deterministic: the same sequence of events always produces the same memory state.

Memory Is Just Accumulated Entity State

There is no separate “memory” API or storage system. Memory is entity state where:

  • Fields have accumulator defaults (0, [])
  • Core rules use add, push, merge, find/if/map rather than a single upsert
  • The fields are designed to grow richer over time

The entity snapshot is the memory. Reading user memory is reading the entity snapshot:

const user = await causet.getEntity("user", userId);
console.log(user.concert_count);         // 48
console.log(user.total_distance_traveled); // 1200
console.log(user.concerts_attended.length); // 48

Projecting Memory for Efficient Queries

The full concerts_attended array on the user entity is useful for per-user queries, but it is not efficient for aggregate queries across all users (e.g., “which shows have the highest attendance?”).

Materialize specific memory fields into projection tables for efficient cross-entity queries:

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
      play_count: BIGINT
    derive:
      user_id: event.entity_id
      artist_id: event.payload.artist_id
    aggregates:
      CONCERT_ATTENDED:
        play_count: { op: add, by: 1, floor: 0 }

Now you can query which users have the highest affinity for a given artist:

SELECT user_id, play_count
FROM user_artist_affinity
WHERE artist_id = 'artist_pearl_jam'
ORDER BY play_count DESC
LIMIT 100;

Memory Is Replayable

Because memory is built from deterministic core rules applied to ledger events, it can be rebuilt at any time:

  • Add a new memory field → rebuild entity snapshots from ledger → new field backfilled
  • Change accumulation logic → rebuild → all entities reflect new logic
  • Auditor needs historical state → replay ledger up to timestamp T → state at T reconstructed

This is not true of manually maintained state, which has no replay mechanism.

Memory Powers Intelligent Features

Structured, event-sourced memory is a strong foundation for personalization and AI features:

  • Recommendations: “You’ve seen Pearl Jam 7 times — there’s a show next Friday”
  • Personalization: surface venues the user has visited before
  • Context for AI: pass user memory as structured context to an LLM for grounded, non-hallucinated responses
  • Behavioral scoring: calculate engagement scores from attendance frequency, distance traveled, and genre diversity

Each feature reads from the entity snapshot directly or from a materialized projection table. For unstructured text in LLM prompts, add vector memory on top of this structured state.

Privacy Considerations

User memory contains a detailed personal history. Consider:

  • Which fields constitute PII under GDPR, CCPA, or your jurisdiction’s regulations
  • Right-to-erasure: how to handle deletion of a user’s memory (cannot remove from append-only ledger without a separate redaction mechanism)
  • Data minimization: only accumulate memory fields that serve a defined purpose

See the control plane documentation for data handling and compliance settings for your platform.