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.
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/CHANGELOG.md +29 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/PKG-INFO +4 -2
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/README.md +1 -1
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/__init__.py +1 -1
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/beam.py +263 -11
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/local_llm.py +108 -58
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/PKG-INFO +4 -2
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/requires.txt +2 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/pyproject.toml +2 -2
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_beam.py +495 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/CONTRIBUTING.md +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/LICENSE +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/MANIFEST.in +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/UPDATING.md +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/assets/mnemosyne.jpg +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/README.md +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/api-reference.md +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/architecture.md +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/changelog.md +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/comparison.md +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/configuration.md +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/getting-started.md +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/hermes-integration.md +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/docs/llm-installation-guide.md +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/hermes_memory_provider/__init__.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/hermes_memory_provider/cli.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/hermes_plugin/__init__.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/hermes_plugin/tools.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/cli.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/__init__.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/aaak.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/banks.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/cost_log.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/embeddings.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/entities.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/extraction.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/__init__.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/agentic.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/base.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/cognee.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/honcho.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/letta.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/mem0.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/supermemory.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/importers/zep.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/memory.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/patterns.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/plugins.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/streaming.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/token_counter.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/core/triples.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/diagnose.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/dr/__init__.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/dr/recovery.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/install.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/mcp_server.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne/mcp_tools.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/SOURCES.txt +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/dependency_links.txt +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/entry_points.txt +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/mnemosyne_memory.egg-info/top_level.txt +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/setup.cfg +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/setup.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_configurable_scoring.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_entities.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_entity_integration.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_extraction.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_extraction_integration.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_local_llm.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_mcp_server.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_memory_banks.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_mnemosyne_stats.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_multi_agent_identity.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_patterns.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_plugins.py +0 -0
- {mnemosyne_memory-2.2 → mnemosyne_memory-2.3}/tests/test_streaming.py +0 -0
- {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.
|
|
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
|
[](https://python.org)
|
|
53
|
-
[](https://pypi.org/project/mnemosyne-memory/)
|
|
54
56
|
[](https://sqlite.org/codeofethics.html)
|
|
55
57
|
[](LICENSE)
|
|
56
58
|
[](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
|
[](https://python.org)
|
|
8
|
-
[](https://pypi.org/project/mnemosyne-memory/)
|
|
9
9
|
[](https://sqlite.org/codeofethics.html)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
[](https://github.com/AxDSan/mnemosyne/actions/workflows/ci.yml)
|
|
@@ -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
|
|
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]:
|