MemoryEntry

On this page

Purpose

MemoryEntry is the single universal record type used by every memory tier in this package — ShortTermMemory, WorkingMemory, LongTermMemory, and SemanticMemory all store and return MemoryEntry instances. It carries content, timing, importance/decay bookkeeping, tags, and metadata in one dataclass.

Constructor

Python · 13 lines
@dataclass
class MemoryEntry:
    content: Any
    timestamp: float
    memory_type: MemoryType
    importance: float = 0.5
    access_count: int = 0
    tags: List[str] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)
    embedding: Optional[List[float]] = None
    last_access: float = field(default_factory=time.time)
    decay_factor: float = 1.0
    original_importance: Optional[float] = None
ParameterTypeRequiredDefaultNotes
contentAnyYes—Typically a dict with input/output keys, but can be any JSON-serializable value or a {"summary": ...} / {"merged": ...} shape (see MemoryCompressor).
timestampfloatYes—Unix epoch seconds. Not auto-populated — callers must supply time.time() themselves.
memory_typeMemoryTypeYes—See MemoryType.
importancefloatNo0.5Must be in [0, 1] — validated in __post_init__, raises ValueError otherwise.
access_countintNo0Incremented by .access().
tagsList[str]No[]Use .add_tag()/.add_tags() rather than mutating directly, so lowercasing/stripping is applied.
metadataDict[str, Any]No{}Free-form. An "id" key is auto-populated if absent (see below).
embeddingOptional[List[float]]NoNonePopulated by SemanticMemory.add(); not serialized by to_dict() unless include_embedding=True is passed.
last_accessfloatNotime.time() at constructionUpdated by .access().
decay_factorfloatNo1.0Multiplicative factor applied on top of importance to get effective_importance.
original_importanceOptional[float]NoNone → copied from importancePreserved as a reference point; not otherwise read by any class in this source tree.

__post_init__

  1. If original_importance is None, sets it to the current importance.
  2. Validates 0 <= importance <= 1, raising ValueError(f"importance must be between 0 and 1, got {self.importance}") otherwise.
  3. If "id" is not already a key in metadata, generates one via _generate_id() and stores it in metadata["id"].

ID generation

Python · 3 lines
def _generate_id(self) -> str:
    data = f"{self.content}_{self.timestamp}_{self.memory_type.value}"
    return hashlib.md5(data.encode('utf-8')).hexdigest()[:16]

The ID is an MD5 hash (truncated to 16 hex chars) of str(content), timestamp, and memory_type.value. Because timestamp is normally a high-resolution time.time() float, collisions between two distinct entries are unlikely in practice, but this is not a cryptographic or collision-resistant ID scheme — if you construct two entries with the identical content and an identical (e.g. manually-supplied, rounded) timestamp, they will get the same ID and be treated as equal by __eq__/__hash__.

Properties

PropertyReturnsNotes
idstrmetadata.get('id', 'unknown')
age_secondsfloattime.time() - timestamp, recomputed on every access
age_daysfloatage_seconds / 86400
time_since_access_secondsfloattime.time() - last_access
effective_importancefloatimportance * decay_factor — the value actually used for ranking/eviction throughout the package
priorityMemoryPriorityMemoryPriority.from_importance(effective_importance)
has_embeddingboolembedding is not None and len(embedding) > 0

Methods

access() -> None

Records an access: increments access_count, resets last_access to now, and applies a small importance boost:

Python · 2 lines
boost = 0.05 * (1 - self.importance)  # smaller boost the more important it already is
self.importance = min(1.0, self.importance + boost)

Called automatically by LongTermMemory.get() and SemanticMemory.search() whenever an entry is retrieved.

apply_decay(decay_rate: float = 0.1) -> None

Python · 4 lines
days_since_access = self.time_since_access_seconds / 86400
decay = decay_rate * days_since_access
self.decay_factor = max(0.1, 1.0 - decay)
self.importance = max(0.1, self.importance * self.decay_factor)

Linear (not exponential) decay based on days since last access, floored at 0.1 for both decay_factor and importance. Mutates the entry in place. Note this is a different decay formula than the exponential model used by ForgettingMechanism and LongTermMemory.apply_global_decay — three different decay implementations coexist in this package (see Known Issues).

add_tag(tag: str) / add_tags(tags: List[str]) / has_tag(tag: str) -> bool

add_tag lowercases and strips the tag before appending (skips empty strings and duplicates). has_tag lowercases only the query argument, not the stored tags — since add_tag always stores lowercase, this is consistent as long as tags was never populated by direct list mutation bypassing add_tag.

update_metadata(key, value) / merge_metadata(dict)

Simple dict.__setitem__ / dict.update() wrappers.

to_dict(include_embedding: bool = False) -> Dict[str, Any]

Returns a dict with id, content, timestamp, memory_type (as .value string), importance, effective_importance, access_count, tags, metadata, last_access, decay_factor, age_seconds, priority (as .name string). Does not include embedding unless include_embedding=True, and never includes original_importance.

from_dict(data: Dict[str, Any]) -> MemoryEntry (classmethod)

Reconstructs an entry. Requires data['content'] and data['timestamp'] (raises KeyError if absent); every other field has a .get() fallback. memory_type is parsed via MemoryType.from_string(data.get('memory_type', 'short_term')) — meaning an unrecognized memory_type string silently becomes SHORT_TERM rather than raising (see MemoryType.from_string).

[!WARNING] to_dict → from_dict is lossy for embeddings entry.to_json() then MemoryEntry.from_json(...) (or to_dict()/from_dict()) drops the embedding field unless you explicitly pass include_embedding=True to to_dict(). If you persist entries via this path and later reload them into SemanticMemory, the embedding will need to be recomputed.

to_json(include_embedding=False) -> str / from_json(json_str) -> MemoryEntry (classmethod)

Thin json.dumps/json.loads wrappers around to_dict/from_dict.

__repr__, __eq__, __hash__, __lt__

  • __repr__ truncates content to 50 characters with "..." suffix if longer.
  • __eq__/__hash__ are based solely on .id — two entries with the same generated (or manually supplied) ID are considered equal and hash identically, regardless of differing content.
  • __lt__ compares by effective_importance, enabling sorted(entries) to rank by importance ascending. Comparing against a non-MemoryEntry returns NotImplemented (Python then falls back to the other operand's comparison, or raises TypeError if neither side implements it).

Thread safety

MemoryEntry itself performs no locking. It is safe to read from multiple threads, but concurrent .access() / .apply_decay() calls on the same instance from different threads are a plain unsynchronized read-modify-write race (e.g. access_count += 1 is not atomic).

Complexity

All operations are O(1) except _generate_id (O(len(str(content)))) and dict/JSON serialization (O(size of content + metadata)).

Examples

Python · 32 lines
import time
from zerda.memory import MemoryEntry, MemoryType

# 1. Basic construction
entry = MemoryEntry(
    content={"input": "What's the capital of Japan?", "output": "Tokyo."},
    timestamp=time.time(),
    memory_type=MemoryType.SHORT_TERM,
    importance=0.6,
)
print(entry.id)                 # 16-char hex string

# 2. Recording an access
entry.access()
print(entry.access_count, entry.importance)  # 1, slightly boosted

# 3. Applying decay after a week without access
entry.last_access -= 7 * 86400
entry.apply_decay(decay_rate=0.1)
print(entry.decay_factor, entry.effective_importance)

# 4. Error handling — invalid importance
try:
    MemoryEntry(content="x", timestamp=time.time(),
                memory_type=MemoryType.SHORT_TERM, importance=1.5)
except ValueError as e:
    print(e)  # "importance must be between 0 and 1, got 1.5"

# 5. Production-style round trip through JSON (note: embedding dropped)
json_str = entry.to_json()
restored = MemoryEntry.from_json(json_str)
assert restored.id == entry.id

Best practices

  • Always construct with a real time.time() timestamp unless you have a specific reason to backdate an entry (e.g. tests).
  • Use .access() when your application logic "reads" a memory to keep decay/priority bookkeeping accurate — don't just read .content directly if you want recency tracking to reflect real usage.
  • If you need embeddings preserved across serialization, always pass include_embedding=True explicitly.

Common mistakes

  • Constructing with importance outside [0, 1] (e.g. passing a raw LLM confidence score on a 0–100 scale) — raises ValueError immediately.
  • Assuming tags stores case-preserved values — add_tag always lowercases.
  • Comparing two entries with == and expecting content equality — it's ID equality only.

Related pages