Skip to content

feat(mem0): add Ebbinghaus decay-based time-series awareness#12987

Open
quinnmacro wants to merge 1 commit into
NousResearch:mainfrom
quinnmacro:feat/mem0-ebbinghaus-decay
Open

feat(mem0): add Ebbinghaus decay-based time-series awareness#12987
quinnmacro wants to merge 1 commit into
NousResearch:mainfrom
quinnmacro:feat/mem0-ebbinghaus-decay

Conversation

@quinnmacro

@quinnmacro quinnmacro commented Apr 20, 2026

Copy link
Copy Markdown

What does this PR do?

Adds Ebbinghaus decay-based time-series awareness to the Mem0 memory plugin, enabling agents to reason about memory freshness and prioritize recent/actively-used memories over stale ones.

Problem

Current Mem0 plugin returns memories without any time-series context:

  • No indication of how old a memory is
  • No differentiation between volatile market data vs stable preferences
  • No prioritization based on usage frequency
  • No lifecycle state (active/stale/archived)

Industry Context

Implements techniques from CortexGraph and SuperLocalMemory V3.3:

  • Continuous Ebbinghaus decay formula: score(t) = (n_use)^β · e^(-λ·Δt) · s
  • Domain-aware half-life tuning
  • Power-law use count reinforcement
  • Trust-weighted acceleration for low-confidence memories
  • Lifecycle quantization downgrade suggestions

Changes Made

Core Logic (plugins/memory/mem0/__init__.py, +213/-3)

Pure functions (no class modifications, no config changes):

  • _detect_domain(text) — keyword-based domain classification (volatile/normal/stable)
  • _calculate_age_seconds(created_at) — ISO-8601 age calculation (future timestamps → 0)
  • _calculate_ebbinghaus_score(age, n_use, domain, strength, trust) — core decay formula
  • _determine_lifecycle(score, domain) — score → lifecycle state mapping
  • _add_time_aware_fields(memory) — enrich a memory dict with all time-series fields

Constants (module-private, _ prefixed):

  • _EBBINGHAUS_PARAMS — domain profiles with half-life/beta/threshold
  • _DOMAIN_KEYWORDS — English-only keyword patterns for domain detection
  • _LIFECYCLE_STATES — retention→bit-width→tag mapping

Integration (additive, no existing code modified):

  • mem0_profile — enriched results include memories array with time-series fields
  • mem0_search — each result includes age_days, domain, retention_score, lifecycle_state, etc.

Tests (tests/plugins/memory/test_ebbinghaus_decay.py, 84 tests)

Test Class Count What's Tested
TestDetectDomain 16 Empty, volatile, stable, priority, case insensitivity
TestCalculateAgeSeconds 7 Valid ISO, Z suffix, invalid, future→0, near-zero
TestCalculateEbbinghausScore 14 Half-life invariant, fresh≈1.0, n_use reinforcement, trust acceleration, clamping, monotonicity, custom strength
TestDetermineLifecycle 12 All 5 states, domain-specific thresholds, boundaries, output keys
TestAddTimeAwareFields 18 Non-dict passthrough, all fields added, domain detection, n_use fallbacks, promotion eligibility, trust from metadata, immutability
TestParameterConsistency 7 Required keys, monotonicity, positive half-lives, beta ranges, regex validity

All 84 tests pass ✅ (plus 15 existing test_mem0_v2.py = 99 total, 0 failures)

Documentation (plugins/memory/mem0/README.md)

Added "Time-Series Awareness" section with:

  • Domain profiles table (volatile/normal/stable half-lives)
  • Enriched fields table (8 new fields)
  • Formula explanation
  • Lifecycle thresholds table
  • Customization example

Verification

Half-Life Invariant (Core Correctness)

Normal domain, day 7:   score = 0.500  ✅ (matches half-life)
Volatile domain, day 3: score = 0.500  ✅
Stable domain, day 30:  score = 0.650  ✅ (strength=1.3 boosts above 0.5)

Use Count Reinforcement

n_use=5, beta=0.6 (normal):   use_factor = 5^0.6 ≈ 2.63
n_use=5, beta=0.8 (volatile): use_factor = 5^0.8 ≈ 3.62

Trust Acceleration

trust=1.0, day 7, normal:  score = 0.500
trust=0.5, day 7, normal:  score < 0.500  (faster decay)
trust=0.0, day 7, normal:  score = 0.125  (3× effective λ)

Lifecycle Transitions

score > 0.8  → active  (32-bit, no tag)
0.5 < score ≤ 0.8 → warm   (8-bit, ⏳)
0.2 < score ≤ 0.5 → cold   (4-bit, ⚠️)
threshold < score ≤ 0.2 → archive (2-bit, 🔴)
score ≤ threshold → forgotten

Impact

  • No breaking changes — additive fields only, all existing tool contracts preserved
  • No upstream code modified — only additions to __init__.py, README, and new test file
  • Self-hosted compatible — uses created_at only, no extra DB fields needed
  • Zero performance impact — O(1) calculations, no API calls
  • English-only keywords — all domain detection patterns use English
  • Future timestamps handled — clamped to age=0

Type of Change

  • New feature (non-breaking change that adds functionality)

Checklist

  • Follows Conventional Commits
  • Self-hosted compatible
  • No unrelated commits
  • Tests added and passing (84 new tests)
  • Documentation updated (README.md)

References

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins tool/memory Memory tool and memory providers labels Apr 22, 2026
@quinnmacro quinnmacro force-pushed the feat/mem0-ebbinghaus-decay branch from 005bab0 to 89e3036 Compare April 26, 2026 01:48
@quinnmacro

Copy link
Copy Markdown
Author

Major Rewrite: Clean Rebase + Minimal Diff

This PR has been completely rewritten to be a minimal, surgical addition on top of the current upstream main.

What Changed

Metric Before After
Diff size +365/-118 +193/-3
Files changed 1 1
Upstream code modified Yes (class structure, config loading, tool schemas, imports) No — only additive changes

Problems Fixed

The original PR was based on an older upstream and had drifted significantly. Git's auto-merge succeeded without conflict markers, but silently reverted upstream improvements:

  1. _load_config() — lost keyword_search field, Path objects, robust merge logic
  2. Mem0MemoryProvider class — lost name, is_available, save_config, get_config_schema methods
  3. _get_client() — wrong import path (mem0.client.MemoryClient vs mem0.MemoryClient)
  4. _is_breaker_open()time.time() vs upstream's time.monotonic()
  5. Tool schemas moved from class-level to file-end
  6. Double-escaped newlines (\\n\\\\n)

All of these are now fixed. The new version starts from upstream's current code and only adds the Ebbinghaus functionality — nothing else is touched.

Code Quality Improvements (vs original PR)

  • Constants prefixed with _ (module-private convention)
  • float | None union syntax instead of Optional[float]
  • Simplified _detect_domain with early return on empty input
  • Cleaner _calculate_ebbinghaus_score docstring
  • _determine_lifecycle renamed from _determine_lifecycle_state (less redundant)

How to Verify

git diff upstream/main -- plugins/memory/mem0/__init__.py
# Should show ONLY additions (Ebbinghaus helpers + time-aware field injection)
# Zero modifications to upstream code

Ready for review.

@quinnmacro quinnmacro force-pushed the feat/mem0-ebbinghaus-decay branch from 89e3036 to 5b64638 Compare April 26, 2026 05:36
@quinnmacro

Copy link
Copy Markdown
Author

Update: Tests + Docs + Code Quality

Pushed a comprehensive update to this PR:

What's New

84 unit tests (tests/plugins/memory/test_ebbinghaus_decay.py):

Class Tests Coverage
TestDetectDomain 16 Empty, volatile, stable, priority, case insensitivity, all keywords
TestCalculateAgeSeconds 7 Valid ISO, Z suffix, invalid, future timestamps → 0
TestCalculateEbbinghausScore 14 Half-life invariant, fresh≈1.0, n_use reinforcement, trust acceleration, clamping, monotonicity
TestDetermineLifecycle 12 All 5 states, domain-specific thresholds, boundaries
TestAddTimeAwareFields 18 Passthrough, all fields, domain detection, n_use fallbacks, promotion, immutability
TestParameterConsistency 7 Required keys, monotonicity, ranges, regex validity

99/99 tests pass (84 new + 15 existing test_mem0_v2.py)

Code Quality Fixes

  1. English-only keywords — replaced all Chinese regex patterns (非农, 利差, etc.) with English (nonfarm, spread, etc.)
  2. Lowercase keyword bug fixr"GDP"r"gdp" since _detect_domain lowercases input before regex matching
  3. Future timestamp clamping_calculate_age_seconds now returns 0.0 for timestamps in the future (negative age is meaningless for decay)
  4. Docstring accuracy — half-life examples now correctly show stable domain at day 30 = 0.65 (not 0.5, due to strength_default=1.3)

Documentation

Added Time-Series Awareness section to plugins/memory/mem0/README.md:

  • Domain profiles table
  • 8 enriched fields table
  • Formula explanation
  • Lifecycle thresholds
  • Customization example

Diff Summary

plugins/memory/mem0/README.md                   | +58
plugins/memory/mem0/__init__.py                 | +29 -6
tests/plugins/memory/test_ebbinghaus_decay.py   | +558 (new file)

All changes are additive only — no upstream code modified.

@quinnmacro

Copy link
Copy Markdown
Author

Rebased onto latest upstream/main (v2026.5.29)

Clean single commit: +808/-3 across 3 files. All 84 tests passing.

TL;DR

Adds time-series awareness to the Mem0 memory plugin so agents can distinguish fresh memories from stale ones — without any breaking changes or performance cost.

Why this matters

Right now, the Mem0 plugin treats a memory from 2 minutes ago the same as one from 2 months ago. For users who run Hermes daily (cron jobs, long sessions, evolving projects), this means:

  • Stale market data looks as authoritative as fresh data
  • Old project decisions compete with current ones
  • Agents can't say "this fact is 90 days old, I should verify it"

Design decisions

Pure functions, zero side effects — all decay logic is in standalone functions (_detect_domain, _calculate_ebbinghaus_score, _determine_lifecycle, etc.) that don't touch the plugin class or config. This makes the diff minimal and the logic independently testable.

Additive-only integrationmem0_profile and mem0_search results get extra fields (age_days, domain, retention_score, lifecycle_state). Existing consumers see no change. Downstream code can opt into using the new fields.

Domain-aware half-lives — not all memories decay at the same rate:

Domain Half-life Example
Volatile 2 days Market data, breaking news
Normal 7 days Project status, tool configs
Stable 30 days User preferences, infra facts

Self-hosted compatible — uses the existing created_at field from Mem0. No schema changes, no extra DB columns, no Neo4j modifications.

Test coverage

84 tests across 6 classes:

  • TestDetectDomain — keyword classification (volatile/normal/stable)
  • TestCalculateAgeSeconds — ISO-8601 age calculation, future timestamp clamping
  • TestCalculateEbbinghausScore — decay formula invariants, half-life correctness
  • TestDetermineLifecycle — state transitions and thresholds
  • TestAddTimeAwareFields — enrichment logic, original field preservation
  • TestParameterConsistency — configuration sanity checks
84 passed in 1.13s

What this does NOT do

  • Does not modify upstream Mem0 — all changes are in the Hermes plugin layer
  • Does not add API calls or network overhead — O(1) math per memory
  • Does not change existing behavior — purely additive fields
  • Does not require a Mem0 version upgrade

This is a building block for the broader time-awareness work tracked in #17459/#17476. Happy to address any review feedback.

Adds Ebbinghaus decay-based time-series awareness to the Mem0 memory
plugin, enabling agents to reason about memory freshness and prioritize
recent/actively-used memories over stale ones.

Core implementation:
- Continuous Ebbinghaus decay formula: score(t) = (n_use)^b * e^(-l*dt) * s
- Domain-aware half-life tuning (volatile/normal/stable)
- Power-law use count reinforcement with trust-weighted acceleration
- Lifecycle states: active -> warm -> cold -> archive -> forgotten

Key changes:
- plugins/memory/mem0/__init__.py: Pure functions for domain detection,
  age calculation, Ebbinghaus scoring, lifecycle determination, and
  time-aware field enrichment. Additive integration to mem0_profile and
  mem0_search.
- plugins/memory/mem0/README.md: Time-Series Awareness section.
- tests/plugins/memory/test_ebbinghaus_decay.py: 84 new tests across
  6 test classes.

Zero breaking changes. Zero performance impact. Self-hosted compatible.
Rebased onto upstream/main (v2026.5.29).
@quinnmacro quinnmacro force-pushed the feat/mem0-ebbinghaus-decay branch from ffd053e to c3e6fab Compare June 11, 2026 08:11
@quinnmacro

Copy link
Copy Markdown
Author

Rebased onto v2026.6.5 (commit c94e93a)

Clean rebase, zero conflicts. 84 tests all passing. Diff: +808/-3 across 3 files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have tool/memory Memory tool and memory providers type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants