mnemosyne-memory 2.2__tar.gz → 2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/CHANGELOG.md +29 -0
  2. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/PKG-INFO +4 -2
  3. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/README.md +1 -1
  4. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/__init__.py +1 -1
  5. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/beam.py +263 -11
  6. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/local_llm.py +108 -58
  7. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/PKG-INFO +4 -2
  8. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/requires.txt +2 -0
  9. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/pyproject.toml +2 -2
  10. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_beam.py +495 -0
  11. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/CONTRIBUTING.md +0 -0
  12. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/LICENSE +0 -0
  13. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/MANIFEST.in +0 -0
  14. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/UPDATING.md +0 -0
  15. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/assets/mnemosyne.jpg +0 -0
  16. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/README.md +0 -0
  17. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/api-reference.md +0 -0
  18. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/architecture.md +0 -0
  19. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/changelog.md +0 -0
  20. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/comparison.md +0 -0
  21. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/configuration.md +0 -0
  22. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/getting-started.md +0 -0
  23. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/hermes-integration.md +0 -0
  24. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/llm-installation-guide.md +0 -0
  25. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/hermes_memory_provider/__init__.py +0 -0
  26. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/hermes_memory_provider/cli.py +0 -0
  27. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/hermes_plugin/__init__.py +0 -0
  28. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/hermes_plugin/tools.py +0 -0
  29. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/cli.py +0 -0
  30. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/__init__.py +0 -0
  31. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/aaak.py +0 -0
  32. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/banks.py +0 -0
  33. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/cost_log.py +0 -0
  34. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/embeddings.py +0 -0
  35. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/entities.py +0 -0
  36. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/extraction.py +0 -0
  37. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/__init__.py +0 -0
  38. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/agentic.py +0 -0
  39. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/base.py +0 -0
  40. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/cognee.py +0 -0
  41. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/honcho.py +0 -0
  42. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/letta.py +0 -0
  43. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/mem0.py +0 -0
  44. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/supermemory.py +0 -0
  45. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/zep.py +0 -0
  46. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/memory.py +0 -0
  47. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/patterns.py +0 -0
  48. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/plugins.py +0 -0
  49. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/streaming.py +0 -0
  50. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/token_counter.py +0 -0
  51. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/triples.py +0 -0
  52. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/diagnose.py +0 -0
  53. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/dr/__init__.py +0 -0
  54. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/dr/recovery.py +0 -0
  55. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/install.py +0 -0
  56. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/mcp_server.py +0 -0
  57. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/mcp_tools.py +0 -0
  58. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/SOURCES.txt +0 -0
  59. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/dependency_links.txt +0 -0
  60. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/entry_points.txt +0 -0
  61. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/top_level.txt +0 -0
  62. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/setup.cfg +0 -0
  63. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/setup.py +0 -0
  64. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_configurable_scoring.py +0 -0
  65. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_entities.py +0 -0
  66. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_entity_integration.py +0 -0
  67. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_extraction.py +0 -0
  68. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_extraction_integration.py +0 -0
  69. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_local_llm.py +0 -0
  70. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_mcp_server.py +0 -0
  71. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_memory_banks.py +0 -0
  72. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_mnemosyne_stats.py +0 -0
  73. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_multi_agent_identity.py +0 -0
  74. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_patterns.py +0 -0
  75. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_plugins.py +0 -0
  76. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_streaming.py +0 -0
  77. {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_temporal_recall.py +0 -0
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Simple Versioning](https://github.com/AxDSan/mnemosyne) (MAJOR.MINOR).
7
7
 
8
+ ## [2.3] — 2026-05-05
9
+
10
+ ### Added
11
+
12
+ **Tiered Episodic Degradation — long-term recall without unbounded growth**
13
+ - Three degradation tiers: Tier 1 (0-30d, full detail), Tier 2 (30-180d, LLM-compressed), Tier 3 (180d+, entity-extracted signal)
14
+ - Automatic tier promotion during `sleep()` — no manual maintenance
15
+ - Tier multipliers in recall scoring: cold memories need 4x stronger semantic match
16
+ - Configurable via `MNEMOSYNE_TIER2_DAYS`, `MNEMOSYNE_TIER3_DAYS`, `MNEMOSYNE_TIER*_WEIGHT`
17
+ - Mnemonics can now truthfully claim "remembers what you told it a year ago"
18
+
19
+ **Smart Compression — entity-aware tier 2→3 extraction**
20
+ - `_extract_key_signal()` scores sentences by entity density (proper nouns, acronyms, security terms, tech stack, urgency)
21
+ - Preserves facts buried anywhere in a long memory, not just the first sentence
22
+ - Configurable: `MNEMOSYNE_SMART_COMPRESS=1` (default on), `MNEMOSYNE_TIER3_MAX_CHARS=300`
23
+
24
+ **Memory Confidence — veracity signal for every memory**
25
+ - New `veracity` field: `stated`, `inferred`, `tool`, `imported`, `unknown`
26
+ - `remember(veracity="stated")` — set confidence at write time
27
+ - `recall(veracity="stated")` — filter by confidence level
28
+ - Recall applies veracity multiplier to scores (stated=1.0x, inferred=0.7x, tool=0.5x)
29
+ - `get_contaminated()` — surface non-stated memories for review
30
+ - Configurable weights via `MNEMOSYNE_*_WEIGHT` env vars
31
+
32
+ ### Fixed
33
+ - `local_llm.summarize()` → `summarize_memories()` — would crash on LLM degradation path
34
+ - SQLite connection conflicts in batch degradation tests
35
+ - Removed hallucinated Phase 2 from roadmap
36
+
8
37
  ## [2.2] — 2026-05-02
9
38
 
10
39
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mnemosyne-memory
3
- Version: 2.2
3
+ Version: 2.3
4
4
  Summary: The Zero-Dependency, Sub-Millisecond AI Memory System
5
5
  Home-page: https://github.com/AxDSan/mnemosyne
6
6
  Author: Abdias J
@@ -27,11 +27,13 @@ Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
28
  Provides-Extra: llm
29
29
  Requires-Dist: ctransformers>=0.2.27; extra == "llm"
30
+ Requires-Dist: llama-cpp-python>=0.2.0; extra == "llm"
30
31
  Requires-Dist: huggingface-hub>=0.20; extra == "llm"
31
32
  Provides-Extra: embeddings
32
33
  Requires-Dist: fastembed>=0.3.0; extra == "embeddings"
33
34
  Provides-Extra: all
34
35
  Requires-Dist: ctransformers>=0.2.27; extra == "all"
36
+ Requires-Dist: llama-cpp-python>=0.2.0; extra == "all"
35
37
  Requires-Dist: huggingface-hub>=0.20; extra == "all"
36
38
  Requires-Dist: fastembed>=0.3.0; extra == "all"
37
39
  Provides-Extra: dev
@@ -50,7 +52,7 @@ Dynamic: requires-python
50
52
  > Native, zero-cloud memory for AI agents. SQLite-backed. Sub-millisecond. Fully private.
51
53
 
52
54
  [![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](https://python.org)
53
- [![PyPI](https://img.shields.io/pypi/v/mnemosyne-memory.svg?v=2.2)](https://pypi.org/project/mnemosyne-memory/)
55
+ [![PyPI](https://img.shields.io/pypi/v/mnemosyne-memory.svg?v=2.3)](https://pypi.org/project/mnemosyne-memory/)
54
56
  [![SQLite](https://img.shields.io/badge/SQLite-3.35+-green.svg)](https://sqlite.org/codeofethics.html)
55
57
  [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
56
58
  [![CI](https://github.com/AxDSan/mnemosyne/actions/workflows/ci.yml/badge.svg)](https://github.com/AxDSan/mnemosyne/actions/workflows/ci.yml)
@@ -5,7 +5,7 @@
5
5
  > Native, zero-cloud memory for AI agents. SQLite-backed. Sub-millisecond. Fully private.
6
6
 
7
7
  [![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](https://python.org)
8
- [![PyPI](https://img.shields.io/pypi/v/mnemosyne-memory.svg?v=2.2)](https://pypi.org/project/mnemosyne-memory/)
8
+ [![PyPI](https://img.shields.io/pypi/v/mnemosyne-memory.svg?v=2.3)](https://pypi.org/project/mnemosyne-memory/)
9
9
  [![SQLite](https://img.shields.io/badge/SQLite-3.35+-green.svg)](https://sqlite.org/codeofethics.html)
10
10
  [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
11
  [![CI](https://github.com/AxDSan/mnemosyne/actions/workflows/ci.yml/badge.svg)](https://github.com/AxDSan/mnemosyne/actions/workflows/ci.yml)
@@ -10,7 +10,7 @@ Example:
10
10
  >>> results = recall("user preferences")
11
11
  """
12
12
 
13
- __version__ = "2.2"
13
+ __version__ = "2.3"
14
14
  __author__ = "Abdias J"
15
15
  __license__ = "MIT"
16
16
 
@@ -54,6 +54,23 @@ SLEEP_BATCH_SIZE = int(os.environ.get("MNEMOSYNE_SLEEP_BATCH", "5000"))
54
54
  SCRATCHPAD_MAX_ITEMS = int(os.environ.get("MNEMOSYNE_SP_MAX", "1000"))
55
55
  RECENCY_HALFLIFE_HOURS = float(os.environ.get("MNEMOSYNE_RECENCY_HALFLIFE", "168")) # 1 week default
56
56
 
57
+ # Tiered episodic degradation
58
+ TIER2_DAYS = int(os.environ.get("MNEMOSYNE_TIER2_DAYS", "30"))
59
+ TIER3_DAYS = int(os.environ.get("MNEMOSYNE_TIER3_DAYS", "180"))
60
+ TIER1_WEIGHT = float(os.environ.get("MNEMOSYNE_TIER1_WEIGHT", "1.0"))
61
+ TIER2_WEIGHT = float(os.environ.get("MNEMOSYNE_TIER2_WEIGHT", "0.5"))
62
+ TIER3_WEIGHT = float(os.environ.get("MNEMOSYNE_TIER3_WEIGHT", "0.25"))
63
+ DEGRADE_BATCH_SIZE = int(os.environ.get("MNEMOSYNE_DEGRADE_BATCH", "100"))
64
+ SMART_COMPRESS = os.environ.get("MNEMOSYNE_SMART_COMPRESS", "1") not in ("0", "false", "no")
65
+ TIER3_MAX_CHARS = int(os.environ.get("MNEMOSYNE_TIER3_MAX_CHARS", "300"))
66
+
67
+ # Veracity weighting (memory confidence)
68
+ STATED_WEIGHT = float(os.environ.get("MNEMOSYNE_STATED_WEIGHT", "1.0"))
69
+ INFERRED_WEIGHT = float(os.environ.get("MNEMOSYNE_INFERRED_WEIGHT", "0.7"))
70
+ TOOL_WEIGHT = float(os.environ.get("MNEMOSYNE_TOOL_WEIGHT", "0.5"))
71
+ IMPORTED_WEIGHT = float(os.environ.get("MNEMOSYNE_IMPORTED_WEIGHT", "0.6"))
72
+ UNKNOWN_WEIGHT = float(os.environ.get("MNEMOSYNE_UNKNOWN_WEIGHT", "0.8"))
73
+
57
74
  # Vector compression: float32 | int8 | bit
58
75
  VEC_TYPE = os.environ.get("MNEMOSYNE_VEC_TYPE", "int8").lower()
59
76
  if VEC_TYPE not in ("float32", "int8", "bit"):
@@ -125,6 +142,7 @@ def init_beam(db_path: Path = None):
125
142
  session_id TEXT DEFAULT 'default',
126
143
  importance REAL DEFAULT 0.5,
127
144
  metadata_json TEXT,
145
+ veracity TEXT DEFAULT 'unknown',
128
146
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
129
147
  )
130
148
  """)
@@ -144,6 +162,7 @@ def init_beam(db_path: Path = None):
144
162
  importance REAL DEFAULT 0.5,
145
163
  metadata_json TEXT,
146
164
  summary_of TEXT DEFAULT '',
165
+ veracity TEXT DEFAULT 'unknown',
147
166
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
148
167
  )
149
168
  """)
@@ -151,6 +170,27 @@ def init_beam(db_path: Path = None):
151
170
  cursor.execute("CREATE INDEX IF NOT EXISTS idx_em_timestamp ON episodic_memory(timestamp)")
152
171
  cursor.execute("CREATE INDEX IF NOT EXISTS idx_em_source ON episodic_memory(source)")
153
172
 
173
+ # --- Tiered degradation migration (v2.3) ---
174
+ try:
175
+ cursor.execute("ALTER TABLE episodic_memory ADD COLUMN tier INTEGER DEFAULT 1")
176
+ except sqlite3.OperationalError:
177
+ pass # Column already exists
178
+ try:
179
+ cursor.execute("ALTER TABLE episodic_memory ADD COLUMN degraded_at TEXT")
180
+ except sqlite3.OperationalError:
181
+ pass
182
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_em_tier ON episodic_memory(tier)")
183
+
184
+ # --- Veracity migration (v2.4) ---
185
+ try:
186
+ cursor.execute("ALTER TABLE working_memory ADD COLUMN veracity TEXT DEFAULT 'unknown'")
187
+ except sqlite3.OperationalError:
188
+ pass
189
+ try:
190
+ cursor.execute("ALTER TABLE episodic_memory ADD COLUMN veracity TEXT DEFAULT 'unknown'")
191
+ except sqlite3.OperationalError:
192
+ pass
193
+
154
194
  # --- SCRATCHPAD ---
155
195
  cursor.execute("""
156
196
  CREATE TABLE IF NOT EXISTS scratchpad (
@@ -691,7 +731,8 @@ class BeamMemory:
691
731
  valid_until: str = None, scope: str = "session",
692
732
  memory_id: str = None,
693
733
  extract_entities: bool = False,
694
- extract: bool = False) -> str:
734
+ extract: bool = False,
735
+ veracity: str = "unknown") -> str:
695
736
  """Store into working_memory. Deduplicates exact content matches.
696
737
 
697
738
  When called from the legacy-compatible Mnemosyne.remember() path,
@@ -710,6 +751,7 @@ class BeamMemory:
710
751
  extract_entities: If True, extract and store entity mentions as triples
711
752
  extract: If True, extract structured facts from content using LLM
712
753
  and store as triples. Default False.
754
+ veracity: Confidence level — 'stated', 'inferred', 'tool', 'imported', 'unknown'
713
755
  """
714
756
  # --- Deduplication: exact match ---
715
757
  existing_id = self._find_duplicate(content)
@@ -737,11 +779,11 @@ class BeamMemory:
737
779
  cursor.execute("""
738
780
  INSERT INTO working_memory
739
781
  (id, content, source, timestamp, session_id, importance, metadata_json, valid_until, scope,
740
- author_id, author_type, channel_id)
741
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
782
+ author_id, author_type, channel_id, veracity)
783
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
742
784
  """, (memory_id, content, source, timestamp, self.session_id, importance,
743
785
  json.dumps(metadata or {}), valid_until, scope,
744
- self.author_id, self.author_type, self.channel_id))
786
+ self.author_id, self.author_type, self.channel_id, veracity))
745
787
  self.conn.commit()
746
788
  self._trim_working_memory()
747
789
 
@@ -978,6 +1020,7 @@ class BeamMemory:
978
1020
  author_id: Optional[str] = None,
979
1021
  author_type: Optional[str] = None,
980
1022
  channel_id: Optional[str] = None,
1023
+ veracity: Optional[str] = None,
981
1024
  temporal_weight: float = 0.0,
982
1025
  query_time: Optional[Any] = None,
983
1026
  temporal_halflife: Optional[float] = None,
@@ -1077,6 +1120,9 @@ class BeamMemory:
1077
1120
  # Topic stored in source field for now (pending dedicated topic column)
1078
1121
  wm_where_clauses.append("source = ?")
1079
1122
  wm_params.append(topic)
1123
+ if veracity:
1124
+ wm_where_clauses.append("veracity = ?")
1125
+ wm_params.append(veracity)
1080
1126
  if author_id:
1081
1127
  wm_where_clauses.append("author_id = ?")
1082
1128
  wm_params.append(author_id)
@@ -1093,7 +1139,7 @@ class BeamMemory:
1093
1139
  placeholders = ",".join("?" * len(wm_ids))
1094
1140
  cursor = self.conn.cursor()
1095
1141
  cursor.execute(f"""
1096
- SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id
1142
+ SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id, veracity
1097
1143
  FROM working_memory
1098
1144
  WHERE id IN ({placeholders})
1099
1145
  AND {wm_where}
@@ -1103,7 +1149,7 @@ class BeamMemory:
1103
1149
  # Fallback: fetch recent items and score in Python (old path)
1104
1150
  cursor = self.conn.cursor()
1105
1151
  cursor.execute(f"""
1106
- SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id
1152
+ SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id, veracity
1107
1153
  FROM working_memory
1108
1154
  WHERE {wm_where}
1109
1155
  ORDER BY timestamp DESC
@@ -1169,6 +1215,7 @@ class BeamMemory:
1169
1215
  "author_id": row["author_id"] if "author_id" in row.keys() else None,
1170
1216
  "author_type": row["author_type"] if "author_type" in row.keys() else None,
1171
1217
  "channel_id": row["channel_id"] if "channel_id" in row.keys() else None,
1218
+ "veracity": row["veracity"] if "veracity" in row.keys() else "unknown",
1172
1219
  "valid_until": row["valid_until"] if "valid_until" in row.keys() else None,
1173
1220
  "superseded_by": row["superseded_by"] if "superseded_by" in row.keys() else None
1174
1221
  })
@@ -1180,7 +1227,7 @@ class BeamMemory:
1180
1227
  placeholders = ",".join("?" * len(entity_memory_ids))
1181
1228
  cursor = self.conn.cursor()
1182
1229
  cursor.execute(f"""
1183
- SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id
1230
+ SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id, veracity
1184
1231
  FROM working_memory
1185
1232
  WHERE id IN ({placeholders})
1186
1233
  AND {wm_where}
@@ -1222,6 +1269,7 @@ class BeamMemory:
1222
1269
  "author_id": row["author_id"] if "author_id" in row.keys() else None,
1223
1270
  "author_type": row["author_type"] if "author_type" in row.keys() else None,
1224
1271
  "channel_id": row["channel_id"] if "channel_id" in row.keys() else None,
1272
+ "veracity": row["veracity"] if "veracity" in row.keys() else "unknown",
1225
1273
  "valid_until": row["valid_until"] if "valid_until" in row.keys() else None,
1226
1274
  "superseded_by": row["superseded_by"] if "superseded_by" in row.keys() else None,
1227
1275
  "entity_match": True
@@ -1240,7 +1288,7 @@ class BeamMemory:
1240
1288
  em_entity_params = [*tuple(entity_memory_ids), self.session_id]
1241
1289
  em_entity_params.extend([datetime.now().isoformat()])
1242
1290
  cursor.execute(f"""
1243
- SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id
1291
+ SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id, veracity
1244
1292
  FROM episodic_memory
1245
1293
  WHERE id IN ({em_placeholders})
1246
1294
  AND {em_entity_scope}
@@ -1282,6 +1330,7 @@ class BeamMemory:
1282
1330
  "author_id": row["author_id"] if "author_id" in row.keys() else None,
1283
1331
  "author_type": row["author_type"] if "author_type" in row.keys() else None,
1284
1332
  "channel_id": row["channel_id"] if "channel_id" in row.keys() else None,
1333
+ "veracity": row["veracity"] if "veracity" in row.keys() else "unknown",
1285
1334
  "valid_until": row["valid_until"] if "valid_until" in row.keys() else None,
1286
1335
  "superseded_by": row["superseded_by"] if "superseded_by" in row.keys() else None,
1287
1336
  "entity_match": True
@@ -1294,7 +1343,7 @@ class BeamMemory:
1294
1343
  cursor = self.conn.cursor()
1295
1344
  # Check working_memory for fact matches
1296
1345
  cursor.execute(f"""
1297
- SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id
1346
+ SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id, veracity
1298
1347
  FROM working_memory
1299
1348
  WHERE id IN ({placeholders})
1300
1349
  AND {wm_where}
@@ -1334,6 +1383,7 @@ class BeamMemory:
1334
1383
  "author_id": row["author_id"] if "author_id" in row.keys() else None,
1335
1384
  "author_type": row["author_type"] if "author_type" in row.keys() else None,
1336
1385
  "channel_id": row["channel_id"] if "channel_id" in row.keys() else None,
1386
+ "veracity": row["veracity"] if "veracity" in row.keys() else "unknown",
1337
1387
  "valid_until": row["valid_until"] if "valid_until" in row.keys() else None,
1338
1388
  "superseded_by": row["superseded_by"] if "superseded_by" in row.keys() else None,
1339
1389
  "fact_match": True
@@ -1351,7 +1401,7 @@ class BeamMemory:
1351
1401
  fact_em_params = [*tuple(fact_memory_ids), self.session_id]
1352
1402
  fact_em_params.extend([datetime.now().isoformat()])
1353
1403
  cursor.execute(f"""
1354
- SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id
1404
+ SELECT id, content, source, timestamp, importance, recall_count, last_recalled, valid_until, superseded_by, scope, author_id, author_type, channel_id, veracity
1355
1405
  FROM episodic_memory
1356
1406
  WHERE id IN ({placeholders})
1357
1407
  AND {fact_em_scope}
@@ -1393,6 +1443,7 @@ class BeamMemory:
1393
1443
  "author_id": row["author_id"] if "author_id" in row.keys() else None,
1394
1444
  "author_type": row["author_type"] if "author_type" in row.keys() else None,
1395
1445
  "channel_id": row["channel_id"] if "channel_id" in row.keys() else None,
1446
+ "veracity": row["veracity"] if "veracity" in row.keys() else "unknown",
1396
1447
  "valid_until": row["valid_until"] if "valid_until" in row.keys() else None,
1397
1448
  "superseded_by": row["superseded_by"] if "superseded_by" in row.keys() else None,
1398
1449
  "fact_match": True
@@ -1457,6 +1508,9 @@ class BeamMemory:
1457
1508
  if topic:
1458
1509
  em_where_clauses.append("source = ?")
1459
1510
  em_params.append(topic)
1511
+ if veracity:
1512
+ em_where_clauses.append("veracity = ?")
1513
+ em_params.append(veracity)
1460
1514
  if author_id:
1461
1515
  em_where_clauses.append("author_id = ?")
1462
1516
  em_params.append(author_id)
@@ -1565,10 +1619,34 @@ class BeamMemory:
1565
1619
  "author_id": row["author_id"] if "author_id" in row.keys() else None,
1566
1620
  "author_type": row["author_type"] if "author_type" in row.keys() else None,
1567
1621
  "channel_id": row["channel_id"] if "channel_id" in row.keys() else None,
1622
+ "veracity": row["veracity"] if "veracity" in row.keys() else "unknown",
1568
1623
  "valid_until": row["valid_until"] if "valid_until" in row.keys() else None,
1569
1624
  "superseded_by": row["superseded_by"] if "superseded_by" in row.keys() else None
1570
1625
  })
1571
1626
 
1627
+ # --- Tiered degradation weighting: apply tier multiplier to episodic scores ---
1628
+ weight_map = {1: TIER1_WEIGHT, 2: TIER2_WEIGHT, 3: TIER3_WEIGHT}
1629
+ veracity_map = {"stated": STATED_WEIGHT, "inferred": INFERRED_WEIGHT,
1630
+ "tool": TOOL_WEIGHT, "imported": IMPORTED_WEIGHT,
1631
+ "unknown": UNKNOWN_WEIGHT}
1632
+ em_ids_for_tier = [r["id"] for r in results if r.get("tier") == "episodic"]
1633
+ if em_ids_for_tier:
1634
+ placeholders = ",".join("?" * len(em_ids_for_tier))
1635
+ tier_rows = cursor.execute(
1636
+ f"SELECT id, tier, veracity FROM episodic_memory WHERE id IN ({placeholders})",
1637
+ em_ids_for_tier
1638
+ ).fetchall()
1639
+ tier_lookup = {r["id"]: (r["tier"] or 1) for r in tier_rows}
1640
+ veracity_lookup = {r["id"]: (r["veracity"] or "unknown") for r in tier_rows}
1641
+ for r in results:
1642
+ if r.get("tier") == "episodic":
1643
+ ep_tier = tier_lookup.get(r["id"], 1)
1644
+ ep_veracity = veracity_lookup.get(r["id"], "unknown")
1645
+ r["degradation_tier"] = ep_tier
1646
+ r["veracity"] = ep_veracity
1647
+ r["score"] *= weight_map.get(ep_tier, 1.0)
1648
+ r["score"] *= veracity_map.get(ep_veracity, UNKNOWN_WEIGHT)
1649
+
1572
1650
  results.sort(key=lambda x: x["score"], reverse=True)
1573
1651
  final_results = results[:top_k]
1574
1652
 
@@ -1670,6 +1748,172 @@ class BeamMemory:
1670
1748
  self.conn.execute("DELETE FROM scratchpad WHERE session_id = ?", (self.session_id,))
1671
1749
  self.conn.commit()
1672
1750
 
1751
+ # ------------------------------------------------------------------
1752
+ # Tiered Episodic Degradation
1753
+ # ------------------------------------------------------------------
1754
+ def _extract_key_signal(self, content: str, max_chars: int = 300) -> str:
1755
+ """Extract the highest-signal sentences from content for tier 3 compression.
1756
+
1757
+ Scores each sentence by entity/keyword density (proper nouns, technical
1758
+ terms, preference indicators) and keeps top-scoring sentences until the
1759
+ character budget is reached. Falls back to first-N-chars if content has
1760
+ no clear sentence boundaries.
1761
+ """
1762
+ import re
1763
+ if len(content) <= max_chars:
1764
+ return content
1765
+
1766
+ # Split into sentences
1767
+ sentences = re.split(r'(?<=[.!?])\s+', content)
1768
+ if len(sentences) <= 1:
1769
+ # No sentence boundaries — take first max_chars
1770
+ return content[:max_chars] + " [...]"
1771
+
1772
+ # Scoring patterns
1773
+ signal_patterns = [
1774
+ (r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b', 3), # Proper nouns: "GitHub Actions", "Docker Compose"
1775
+ (r'\b[A-Z]{2,}\b', 3), # Acronyms: "XKCD", "CI/CD", "API", "AWS"
1776
+ (r'\b(Docker|Kubernetes|AWS|GCP|Azure|Terraform|Python|Rust|Go|TypeScript|React|Next\.?js|Node\.?js|SQLite|Postgres|Redis|nginx|systemd|Linux|macOS|Windows)\b', 4),
1777
+ (r'\b(prefers?|uses?|likes?|loves?|hates?|dislikes?|wants?|needs?)\b', 2), # Preference indicators
1778
+ (r'\b(password|token|secret|key|credential|auth|encrypt|decrypt|private)\b', 3), # Security terms
1779
+ (r'\b(production|staging|deploy|database|backup|migration)\b', 2), # Infra terms
1780
+ (r'\b(critical|urgent|important|breaking|incident|outage|down)\b', 3), # Urgency
1781
+ (r'\b(always|never|every|must|should)\b', 1), # Emphasis words
1782
+ (r'\b(\d{1,3}\.\d{1,3}\.\d{1,3})\b', 3), # Version numbers
1783
+ (r'\b(https?://|www\.|[a-z]+\.[a-z]{2,})\b', 2), # URLs / domains
1784
+ (r'["\'].*?["\']', 1), # Quoted strings
1785
+ ]
1786
+
1787
+ scored = []
1788
+ for sentence in sentences:
1789
+ if not sentence.strip():
1790
+ continue
1791
+ score = 0
1792
+ # Bonus for shorter sentences (signal density)
1793
+ if len(sentence) < 120:
1794
+ score += 1
1795
+ for pattern, weight in signal_patterns:
1796
+ score += len(re.findall(pattern, sentence)) * weight
1797
+ scored.append((score, sentence))
1798
+
1799
+ # Sort by score descending, keep top sentences up to max_chars
1800
+ scored.sort(key=lambda x: x[0], reverse=True)
1801
+ result = []
1802
+ used = 0
1803
+ for _, sentence in scored:
1804
+ if used + len(sentence) + 1 > max_chars:
1805
+ break
1806
+ result.append(sentence)
1807
+ used += len(sentence) + 1 # +1 for space
1808
+
1809
+ if not result:
1810
+ return content[:max_chars] + " [...]"
1811
+
1812
+ compressed = " ".join(result)
1813
+ if len(content) > len(compressed):
1814
+ compressed += " [...]"
1815
+ return compressed
1816
+
1817
+ def degrade_episodic(self, dry_run: bool = False) -> Dict:
1818
+ """Degrade old episodic memories through tier 1→2→3 compression.
1819
+
1820
+ Tier 1 (0-TIER2_DAYS): Full detail, 1.0x recall weight
1821
+ Tier 2 (TIER2_DAYS-TIER3_DAYS): LLM-summarized, 0.5x weight
1822
+ Tier 3 (TIER3_DAYS+): Text extraction compressed, 0.25x weight
1823
+
1824
+ Returns summary of tier transitions performed.
1825
+ """
1826
+ cursor = self.conn.cursor()
1827
+ now = datetime.now()
1828
+ results = {"status": "dry_run" if dry_run else "degraded",
1829
+ "tier1_to_tier2": 0, "tier2_to_tier3": 0}
1830
+
1831
+ # --- Find candidates for degradation ---
1832
+ tier2_cutoff = (now - timedelta(days=TIER2_DAYS)).isoformat()
1833
+ tier3_cutoff = (now - timedelta(days=TIER3_DAYS)).isoformat()
1834
+
1835
+ # Tier 1 → Tier 2: old enough, still at tier 1
1836
+ cursor.execute("""
1837
+ SELECT id, content, importance FROM episodic_memory
1838
+ WHERE tier = 1 AND created_at < ?
1839
+ ORDER BY created_at ASC LIMIT ?
1840
+ """, (tier2_cutoff, DEGRADE_BATCH_SIZE))
1841
+ tier1_rows = cursor.fetchall()
1842
+
1843
+ # Tier 2 → Tier 3: very old, at tier 2
1844
+ cursor.execute("""
1845
+ SELECT id, content FROM episodic_memory
1846
+ WHERE tier = 2 AND created_at < ?
1847
+ ORDER BY created_at ASC LIMIT ?
1848
+ """, (tier3_cutoff, DEGRADE_BATCH_SIZE // 2))
1849
+ tier2_rows = cursor.fetchall()
1850
+
1851
+ if dry_run:
1852
+ results["tier1_to_tier2"] = len(tier1_rows)
1853
+ results["tier2_to_tier3"] = len(tier2_rows)
1854
+ return results
1855
+
1856
+ # --- Degrade tier 1 → tier 2: LLM summarization ---
1857
+ from mnemosyne.core import local_llm
1858
+ for row in tier1_rows:
1859
+ try:
1860
+ compressed = row["content"]
1861
+ if local_llm.llm_available() and len(row["content"]) > 300:
1862
+ summary = local_llm.summarize_memories([row["content"]])
1863
+ if summary:
1864
+ compressed = summary[:400]
1865
+ cursor.execute(
1866
+ "UPDATE episodic_memory SET content = ?, tier = 2, degraded_at = ? WHERE id = ?",
1867
+ (compressed[:800], now.isoformat(), row["id"])
1868
+ )
1869
+ results["tier1_to_tier2"] += 1
1870
+ except Exception:
1871
+ pass
1872
+
1873
+ # --- Degrade tier 2 → tier 3: smart extraction (keep key entities) ---
1874
+ for row in tier2_rows:
1875
+ try:
1876
+ content = row["content"]
1877
+ if SMART_COMPRESS and len(content) > TIER3_MAX_CHARS:
1878
+ compressed = self._extract_key_signal(content, max_chars=TIER3_MAX_CHARS)
1879
+ else:
1880
+ compressed = content[:TIER3_MAX_CHARS]
1881
+ if len(content) > TIER3_MAX_CHARS:
1882
+ compressed += " [...]"
1883
+ cursor.execute(
1884
+ "UPDATE episodic_memory SET content = ?, tier = 3, degraded_at = ? WHERE id = ?",
1885
+ (compressed, now.isoformat(), row["id"])
1886
+ )
1887
+ results["tier2_to_tier3"] += 1
1888
+ except Exception:
1889
+ pass
1890
+
1891
+ self.conn.commit()
1892
+ return results
1893
+
1894
+ def get_contaminated(self, limit: int = 50, min_importance: float = 0.0) -> List[Dict]:
1895
+ """Return potentially contaminated memories for review.
1896
+
1897
+ Contaminated = veracity in ('inferred', 'tool', 'imported', 'unknown')
1898
+ — i.e., anything not explicitly stated by the user. Sorted by
1899
+ importance descending so the highest-stakes items surface first.
1900
+
1901
+ Args:
1902
+ limit: Max memories to return
1903
+ min_importance: Only return memories with importance >= this
1904
+ """
1905
+ cursor = self.conn.cursor()
1906
+ cursor.execute("""
1907
+ SELECT id, content, source, veracity, tier, importance,
1908
+ created_at, degraded_at, session_id
1909
+ FROM episodic_memory
1910
+ WHERE veracity IN ('inferred', 'tool', 'imported', 'unknown')
1911
+ AND importance >= ?
1912
+ ORDER BY importance DESC, created_at DESC
1913
+ LIMIT ?
1914
+ """, (min_importance, limit))
1915
+ return [dict(row) for row in cursor.fetchall()]
1916
+
1673
1917
  # ------------------------------------------------------------------
1674
1918
  # Consolidation / Sleep
1675
1919
  # ------------------------------------------------------------------
@@ -1785,13 +2029,17 @@ class BeamMemory:
1785
2029
  """, (self.session_id, len(consolidated_ids), f"{summaries_created} summaries ({method}) from {len(consolidated_ids)} items"))
1786
2030
  self.conn.commit()
1787
2031
 
2032
+ # Run tiered degradation after consolidation
2033
+ degrade_result = self.degrade_episodic(dry_run=dry_run)
2034
+
1788
2035
  return {
1789
2036
  "status": "dry_run" if dry_run else "consolidated",
1790
2037
  "items_consolidated": len(consolidated_ids),
1791
2038
  "summaries_created": summaries_created,
1792
2039
  "llm_used": llm_used_count,
1793
2040
  "method": method,
1794
- "consolidated_ids": consolidated_ids
2041
+ "consolidated_ids": consolidated_ids,
2042
+ "degradation": degrade_result
1795
2043
  }
1796
2044
 
1797
2045
  def sleep_all_sessions(self, dry_run: bool = False) -> Dict:
@@ -1855,6 +2103,9 @@ class BeamMemory:
1855
2103
  except Exception as exc:
1856
2104
  errors.append({"session_id": session_id, "error": repr(exc)})
1857
2105
 
2106
+ # Run tiered degradation after all-sessions consolidation
2107
+ degrade_result = self.degrade_episodic(dry_run=dry_run)
2108
+
1858
2109
  return {
1859
2110
  "status": "dry_run" if dry_run else ("consolidated" if items_consolidated else "no_op"),
1860
2111
  "sessions_scanned": len(session_rows),
@@ -1865,6 +2116,7 @@ class BeamMemory:
1865
2116
  "errors": len(errors),
1866
2117
  "error_details": errors,
1867
2118
  "session_results": session_results,
2119
+ "degradation": degrade_result
1868
2120
  }
1869
2121
 
1870
2122
  def get_consolidation_log(self, limit: int = 10) -> List[Dict]: