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):
| Field | Type | Notes |
|---|---|---|
id | int | Assigned by the store on insert (None for drafts). |
session_id | str | The session of the last material update (Braess #4). |
user_id | str | Owner scope. All reads/writes are user-scoped. |
type | str | One of the allowed types — see below. |
title | str | Short, human-readable. |
content | str | The full body. <private> regions stripped before persist. |
topic_key | str | None | Optional stable key for upserts (see Topic keys). |
normalized_hash | str | None | SHA-256 of the post-strip content. Internal. |
revision_count | int | Bumped on every UPDATE (default 1). |
created_at, updated_at | str | ISO 8601, UTC, timezone-aware. |
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.
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.
| Variant | Used by | Includes | Excludes |
|---|---|---|---|
Observation | Library callers (full read) | Everything | — |
ObservationCompact | mem_search, memories[] in mem_session_start | id, type, title, topic_key, score, snippet, updated_at | content, normalized_hash |
ObservationPublic | HTTP API responses | Everything visible | normalized_hash (internal) |
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
- 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'
hits = mem.mem_search(user_id="alice", query="greeting", limit=5)
for compact in hits:
print(compact.id, round(compact.score, 3), compact.title)
# ObservationCompact has snippet but not full content
obs = mem.mem_get_observation(user_id="alice", observation_id=result.id)
print(obs.title)
print(obs.content)
print(obs.revision_count)
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.scorelooks the way it does.