Skip to main content

Observations

An observation is the atom of memory in iLAB Memory. Every fact, preference, decision, bug fix, or pattern you persist is one observation. Sessions group observations; topic_keys evolve them; scoring ranks them. But the observation is the indivisible building block.

Anatomy

Every observation has these fields (the Observation Pydantic model):

FieldTypeNotes
idintAssigned by the store on insert (None for drafts).
session_idstrThe session of the last material update (Braess #4).
user_idstrOwner scope. All reads/writes are user-scoped.
typestrOne of the allowed types — see below.
titlestrShort, human-readable.
contentstrThe full body. <private> regions stripped before persist.
topic_keystr | NoneOptional stable key for upserts (see Topic keys).
normalized_hashstr | NoneSHA-256 of the post-strip content. Internal.
revision_countintBumped on every UPDATE (default 1).
created_at, updated_atstrISO 8601, UTC, timezone-aware.
note

created_at is set on first INSERT and never changes. updated_at is refreshed on every UPDATE — that is what feeds the recency component of scoring.

The 5 user-facing types

The library ships with a frozen set of observation types. v0.1.0 does not support adding new types at runtime in the default configuration; you opt-in by passing Config(observation_types=(...)).

profile

Stable user facts: name, role, attribute-style preferences. Highest priority in ContextScore (1.0).

preference

Explicit preferences: "prefers TypeScript over JavaScript", "responds in Spanish". Priority 0.9.

decision

Architectural or design decisions made during the conversation. Priority 0.7.

discovery

Bug fixes, gotchas, edge cases learned the hard way. Priority 0.5.

pattern

Naming, structure, or style conventions established. Priority 0.6.

warning

summary is a reserved type used internally by mem_session_summary and the auto-close path. Calling mem_save(type="summary", ...) raises ValueError. Use mem_session_summary(...) instead.

Compact vs full vs public

iLAB Memory exposes three flavours of the same record. This is intentional — different surfaces have different cost/leak constraints.

VariantUsed byIncludesExcludes
ObservationLibrary callers (full read)Everything
ObservationCompactmem_search, memories[] in mem_session_startid, type, title, topic_key, score, snippet, updated_atcontent, normalized_hash
ObservationPublicHTTP API responsesEverything visiblenormalized_hash (internal)
tip

Progressive disclosure: lists return ObservationCompact (~100 tokens). When you need the body, hydrate one observation at a time with mem_get_observation(observation_id).

Save, search, hydrate

from ilab_memory import ILabMemory

mem = ILabMemory.from_path("./mem.db")

result = mem.mem_save(
user_id="alice",
type="preference",
title="Preferred greeting",
content="Alice prefers being greeted as 'Ali'.",
topic_key="user/alice/greeting",
)

print(result.id, result.outcome) # e.g. 1 'created'

Outcomes

mem_save always returns a SaveResult with one of three outcomes:

created

A brand new row was inserted. id is freshly assigned. revision_count is 1.

updated

An existing row matched (same topic_key, different content). The row was updated in place: revision_count bumped, updated_at refreshed, created_at preserved, session_id overwritten with the current session (Braess #4).

deduped

An existing row matched and the post-strip content hash is identical. No write happened. The existing id is returned. The session is still touched (D8: mem_save always extends the active session's life).

Cross-user safety

Every read enforces user_id scoping at the orchestrator. mem_get_observation(user_id="bob", observation_id=alice_obs_id) returns None — never raises, never leaks the existence of another user's data.

Next

  • Sessions — the container that groups observations.
  • Topic keys — how to make observations evolve instead of duplicate.
  • Scoring — why compact.score looks the way it does.