mindforge-memory 6.4.1__py3-none-any.whl
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.
- MindForge.py +20 -0
- mindforge/__init__.py +111 -0
- mindforge/adapters/__init__.py +18 -0
- mindforge/adapters/claude_adapter.py +84 -0
- mindforge/adapters/crewai_memory.py +76 -0
- mindforge/adapters/generic_api.py +183 -0
- mindforge/adapters/langgraph_store.py +463 -0
- mindforge/adapters/openclaw_adapter.py +91 -0
- mindforge/api/__init__.py +1 -0
- mindforge/api/server.py +1567 -0
- mindforge/cli/__init__.py +1 -0
- mindforge/cli/main.py +13607 -0
- mindforge/core/__init__.py +37 -0
- mindforge/core/embedding.py +847 -0
- mindforge/core/encryption.py +597 -0
- mindforge/core/indexer.py +363 -0
- mindforge/core/mindforge.py +6298 -0
- mindforge/core/paths.py +43 -0
- mindforge/core/query.py +501 -0
- mindforge/core/storage.py +18656 -0
- mindforge/core/types.py +314 -0
- mindforge/core/version.py +56 -0
- mindforge/daemon/__init__.py +0 -0
- mindforge/daemon/server.py +277 -0
- mindforge/mcp/__init__.py +4 -0
- mindforge/mcp/server.py +2559 -0
- mindforge/modules/__init__.py +138 -0
- mindforge/modules/categorizer.py +151 -0
- mindforge/modules/conflict_detector.py +356 -0
- mindforge/modules/connectors.py +331 -0
- mindforge/modules/context_window.py +337 -0
- mindforge/modules/event_bus.py +572 -0
- mindforge/modules/evolution.py +210 -0
- mindforge/modules/experience.py +402 -0
- mindforge/modules/federated.py +722 -0
- mindforge/modules/federated_acl.py +393 -0
- mindforge/modules/follow_up.py +315 -0
- mindforge/modules/hybrid_search.py +565 -0
- mindforge/modules/importer.py +374 -0
- mindforge/modules/integrator.py +196 -0
- mindforge/modules/intent_router.py +359 -0
- mindforge/modules/knowledge_graph.py +395 -0
- mindforge/modules/memory_compressor.py +592 -0
- mindforge/modules/memory_decay.py +474 -0
- mindforge/modules/memory_passport.py +250 -0
- mindforge/modules/multi_agent.py +534 -0
- mindforge/modules/multimodal.py +304 -0
- mindforge/modules/outcome_learning.py +186 -0
- mindforge/modules/personality.py +275 -0
- mindforge/modules/principled_forgetting.py +258 -0
- mindforge/modules/privacy.py +736 -0
- mindforge/modules/recall.py +263 -0
- mindforge/modules/session_focus.py +305 -0
- mindforge/modules/share_conflict.py +521 -0
- mindforge/modules/skill_extractor.py +340 -0
- mindforge/modules/skill_pack.py +205 -0
- mindforge/modules/timeline_viz.py +206 -0
- mindforge/modules/v6.py +931 -0
- mindforge_memory-6.4.1.dist-info/METADATA +281 -0
- mindforge_memory-6.4.1.dist-info/RECORD +64 -0
- mindforge_memory-6.4.1.dist-info/WHEEL +5 -0
- mindforge_memory-6.4.1.dist-info/entry_points.txt +3 -0
- mindforge_memory-6.4.1.dist-info/licenses/LICENSE +21 -0
- mindforge_memory-6.4.1.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MindForge 记忆衰减引擎
|
|
3
|
+
|
|
4
|
+
基于艾宾浩斯遗忘曲线的记忆生命周期管理:
|
|
5
|
+
- 衰减评分:根据时间、重要性、访问频率计算记忆强度
|
|
6
|
+
- 自动归档:强度低于阈值的记忆移入归档区
|
|
7
|
+
- 过期清除:归档超过保留期的记忆永久删除
|
|
8
|
+
- GC 周期:update → archive → purge 的完整编排
|
|
9
|
+
|
|
10
|
+
设计理念:
|
|
11
|
+
真实记忆系统和"日志数据库"的分水岭就是会忘。
|
|
12
|
+
长期不召回的记忆降权/归档,冲突记忆被新事实覆盖。
|
|
13
|
+
旧上下文不污染新决策——Agent 长期运行的核心痛点。
|
|
14
|
+
|
|
15
|
+
和现有模块的关系:
|
|
16
|
+
- evolution.py: ForgettingCurve 提供 Ebbinghaus 数学模型
|
|
17
|
+
- storage.py: archived_memories 表 + archive_memories_by_ids
|
|
18
|
+
- recall.py: 召回时更新 last_accessed_at,间接触发衰减重置
|
|
19
|
+
- memory_decay.py: 编排层,将上述组件串联为 GC 周期
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
import time
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
26
|
+
from enum import Enum
|
|
27
|
+
|
|
28
|
+
from mindforge.core.types import MemoryLayer, Importance
|
|
29
|
+
from mindforge.core.storage import StorageEngine, MemoryEntry
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DecayPolicy(Enum):
|
|
33
|
+
"""衰减策略"""
|
|
34
|
+
CONSERVATIVE = "conservative" # 少记多存,适合重要库
|
|
35
|
+
BALANCED = "balanced" # 默认平衡
|
|
36
|
+
AGGRESSIVE = "aggressive" # 快速遗忘,适合高频临时记忆
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class DecayConfig:
|
|
41
|
+
"""衰减配置(v5.6.0 新增)
|
|
42
|
+
|
|
43
|
+
三套预设策略 + 可微调参数。
|
|
44
|
+
"""
|
|
45
|
+
policy: DecayPolicy = DecayPolicy.BALANCED
|
|
46
|
+
|
|
47
|
+
# 衰减数学参数
|
|
48
|
+
decay_rate: float = 0.05 # 每小时衰减率(越小越慢忘)
|
|
49
|
+
review_boost: float = 0.3 # 每次召回的强度增益
|
|
50
|
+
importance_multiplier: Dict[str, float] = field(default_factory=lambda: {
|
|
51
|
+
"LOW": 0.7,
|
|
52
|
+
"MEDIUM": 1.0,
|
|
53
|
+
"HIGH": 1.5,
|
|
54
|
+
"CRITICAL": 2.5, # CRITICAL 几乎不衰减
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
# 归档阈值:强度低于此值 → 归档
|
|
58
|
+
archive_threshold: float = 0.15
|
|
59
|
+
|
|
60
|
+
# 永久删除:归档超过此天数 → 删除
|
|
61
|
+
purge_after_days: int = 90
|
|
62
|
+
|
|
63
|
+
# 保护规则
|
|
64
|
+
protect_permanent: bool = True # PERMANENT 层不衰减
|
|
65
|
+
protect_critical: bool = True # CRITICAL 重要性不衰减
|
|
66
|
+
protect_recent_hours: float = 6.0 # 最近 N 小时内创建的不归档
|
|
67
|
+
protect_starred: bool = True # 收藏/starred 记忆不衰减
|
|
68
|
+
protect_pinned: bool = True # 置顶/pinned 记忆不衰减
|
|
69
|
+
|
|
70
|
+
# 临时层额外清理
|
|
71
|
+
sensory_max_age_hours: int = 48 # 感官层超过 N 小时 → 归档
|
|
72
|
+
short_term_max_age_hours: int = 168 # 短期层超过 7 天 → 评估
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_policy(cls, policy: DecayPolicy) -> "DecayConfig":
|
|
76
|
+
"""从预设策略创建配置"""
|
|
77
|
+
if policy == DecayPolicy.CONSERVATIVE:
|
|
78
|
+
return cls(
|
|
79
|
+
policy=policy,
|
|
80
|
+
decay_rate=0.02,
|
|
81
|
+
review_boost=0.4,
|
|
82
|
+
archive_threshold=0.08,
|
|
83
|
+
purge_after_days=365,
|
|
84
|
+
sensory_max_age_hours=168,
|
|
85
|
+
short_term_max_age_hours=720,
|
|
86
|
+
)
|
|
87
|
+
elif policy == DecayPolicy.AGGRESSIVE:
|
|
88
|
+
return cls(
|
|
89
|
+
policy=policy,
|
|
90
|
+
decay_rate=0.15,
|
|
91
|
+
review_boost=0.2,
|
|
92
|
+
archive_threshold=0.25,
|
|
93
|
+
purge_after_days=30,
|
|
94
|
+
sensory_max_age_hours=12,
|
|
95
|
+
short_term_max_age_hours=48,
|
|
96
|
+
)
|
|
97
|
+
else: # BALANCED
|
|
98
|
+
return cls(policy=policy)
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
101
|
+
return {
|
|
102
|
+
"policy": self.policy.value,
|
|
103
|
+
"decay_rate": self.decay_rate,
|
|
104
|
+
"review_boost": self.review_boost,
|
|
105
|
+
"importance_multiplier": self.importance_multiplier,
|
|
106
|
+
"archive_threshold": self.archive_threshold,
|
|
107
|
+
"purge_after_days": self.purge_after_days,
|
|
108
|
+
"protect_permanent": self.protect_permanent,
|
|
109
|
+
"protect_critical": self.protect_critical,
|
|
110
|
+
"protect_recent_hours": self.protect_recent_hours,
|
|
111
|
+
"protect_starred": self.protect_starred,
|
|
112
|
+
"protect_pinned": self.protect_pinned,
|
|
113
|
+
"sensory_max_age_hours": self.sensory_max_age_hours,
|
|
114
|
+
"short_term_max_age_hours": self.short_term_max_age_hours,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class MemoryDecayEngine:
|
|
119
|
+
"""记忆衰减引擎
|
|
120
|
+
|
|
121
|
+
编排完整的 GC 周期:
|
|
122
|
+
1. compute_decay_scores() — 计算所有活跃记忆的当前强度
|
|
123
|
+
2. archive_decayed() — 归档强度低于阈值的记忆
|
|
124
|
+
3. purge_old_archives() — 永久删除过期的归档记忆
|
|
125
|
+
4. cleanup_sensory() — 按时间清理超期感官/短期层
|
|
126
|
+
5. run_gc() — 一键执行以上全部步骤
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, storage: StorageEngine, config: Optional[DecayConfig] = None):
|
|
130
|
+
self.storage = storage
|
|
131
|
+
self.config = config or DecayConfig.from_policy(DecayPolicy.BALANCED)
|
|
132
|
+
|
|
133
|
+
def compute_strength(self, entry: MemoryEntry) -> float:
|
|
134
|
+
"""计算单条记忆的当前强度
|
|
135
|
+
|
|
136
|
+
基于 Ebbinghaus 指数衰减:
|
|
137
|
+
strength = initial * exp(-adjusted_decay * elapsed_hours)
|
|
138
|
+
|
|
139
|
+
修正因子:
|
|
140
|
+
- 召回增强:每次召回提升 review_boost,访问频率越高衰减越慢
|
|
141
|
+
- 重要性加权:CRITICAL 几乎不衰减,LOW 快速衰减
|
|
142
|
+
- 巩固次数:每次巩固(层级提升)减慢后续衰减
|
|
143
|
+
"""
|
|
144
|
+
now = time.time()
|
|
145
|
+
elapsed_hours = max(0, (now - entry.last_accessed_at) / 3600)
|
|
146
|
+
|
|
147
|
+
if elapsed_hours < 0.01:
|
|
148
|
+
return entry.strength
|
|
149
|
+
|
|
150
|
+
multiplier = self.config.importance_multiplier.get(
|
|
151
|
+
entry.importance.value, 1.0
|
|
152
|
+
)
|
|
153
|
+
adjusted_decay = self.config.decay_rate / multiplier
|
|
154
|
+
|
|
155
|
+
# 巩固系数:每巩固一次,衰减速率降 10%(最多降 50%)
|
|
156
|
+
consolidation_factor = max(0.5, 1.0 - 0.1 * entry.consolidation_count)
|
|
157
|
+
|
|
158
|
+
raw_strength = entry.strength * math.exp(
|
|
159
|
+
-adjusted_decay * elapsed_hours * consolidation_factor
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return max(0.01, min(1.0, raw_strength))
|
|
163
|
+
|
|
164
|
+
def _is_protected(self, entry: MemoryEntry, now: float) -> bool:
|
|
165
|
+
"""判断记忆是否受保护(不参与归档)"""
|
|
166
|
+
# PERMANENT 层保护
|
|
167
|
+
if self.config.protect_permanent and entry.layer == MemoryLayer.PERMANENT:
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
# CRITICAL 重要性保护
|
|
171
|
+
if self.config.protect_critical and entry.importance == Importance.CRITICAL:
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
# 收藏/置顶保护
|
|
175
|
+
if self.config.protect_starred and getattr(entry, "starred", False):
|
|
176
|
+
return True
|
|
177
|
+
if self.config.protect_pinned and getattr(entry, "pinned", False):
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
# 最近创建的记忆保护
|
|
181
|
+
age_hours = (now - entry.created_at) / 3600
|
|
182
|
+
if age_hours < self.config.protect_recent_hours:
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
# 已在回收站的跳过
|
|
186
|
+
if entry.category == "trash":
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def compute_decay_scores(self, limit: int = 10000) -> Dict[str, Any]:
|
|
192
|
+
"""扫描所有活跃记忆,计算衰减分数
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
{
|
|
196
|
+
"scanned": 总扫描数,
|
|
197
|
+
"updated": 更新元数据数,
|
|
198
|
+
"below_threshold": 低于归档阈值的记忆数,
|
|
199
|
+
"by_layer": {sensory: N, short_term: N, ...},
|
|
200
|
+
"avg_strength": 平均强度,
|
|
201
|
+
}
|
|
202
|
+
"""
|
|
203
|
+
now = time.time()
|
|
204
|
+
entries = self.storage.list_memories(limit=limit)
|
|
205
|
+
|
|
206
|
+
scanned = 0
|
|
207
|
+
updated = 0
|
|
208
|
+
below_threshold = 0
|
|
209
|
+
total_strength = 0.0
|
|
210
|
+
by_layer: Dict[str, int] = {}
|
|
211
|
+
|
|
212
|
+
# v5.6.1 性能优化:批量更新替代逐条 update_memory
|
|
213
|
+
bulk_updates = []
|
|
214
|
+
for entry in entries:
|
|
215
|
+
scanned += 1
|
|
216
|
+
layer_name = entry.layer.value if hasattr(entry.layer, 'value') else str(entry.layer)
|
|
217
|
+
by_layer[layer_name] = by_layer.get(layer_name, 0) + 1
|
|
218
|
+
|
|
219
|
+
strength = self.compute_strength(entry)
|
|
220
|
+
total_strength += strength
|
|
221
|
+
|
|
222
|
+
if strength < self.config.archive_threshold:
|
|
223
|
+
below_threshold += 1
|
|
224
|
+
|
|
225
|
+
# 更新元数据中的衰减信息
|
|
226
|
+
if abs(strength - entry.strength) > 0.001:
|
|
227
|
+
new_metadata = {
|
|
228
|
+
**entry.metadata,
|
|
229
|
+
"current_strength": round(strength, 4),
|
|
230
|
+
"forgetting_score": round(1.0 - strength, 4),
|
|
231
|
+
"decay_updated_at": now,
|
|
232
|
+
}
|
|
233
|
+
bulk_updates.append({
|
|
234
|
+
"id": entry.id,
|
|
235
|
+
"strength": round(strength, 4),
|
|
236
|
+
"forgetting_score": round(1.0 - strength, 4),
|
|
237
|
+
"metadata": new_metadata,
|
|
238
|
+
})
|
|
239
|
+
updated += 1
|
|
240
|
+
|
|
241
|
+
if bulk_updates:
|
|
242
|
+
self.storage.bulk_update_memory_fields(
|
|
243
|
+
bulk_updates,
|
|
244
|
+
["strength", "forgetting_score", "metadata"],
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
"scanned": scanned,
|
|
249
|
+
"updated": updated,
|
|
250
|
+
"below_threshold": below_threshold,
|
|
251
|
+
"by_layer": by_layer,
|
|
252
|
+
"avg_strength": round(total_strength / max(1, scanned), 4),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
def archive_decayed(self, dry_run: bool = False,
|
|
256
|
+
actor: str = "gc") -> Dict[str, Any]:
|
|
257
|
+
"""归档强度低于阈值的记忆
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
dry_run: 只预览不执行
|
|
261
|
+
actor: 操作者标识
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
{
|
|
265
|
+
"candidates": 候选归档数,
|
|
266
|
+
"archived": 实际归档数,
|
|
267
|
+
"protected": 受保护跳过数,
|
|
268
|
+
"by_layer": {...},
|
|
269
|
+
"sample_ids": [...], # 前 10 个候选 ID
|
|
270
|
+
}
|
|
271
|
+
"""
|
|
272
|
+
now = time.time()
|
|
273
|
+
entries = self.storage.list_memories(limit=10000)
|
|
274
|
+
|
|
275
|
+
candidates: List[str] = []
|
|
276
|
+
protected = 0
|
|
277
|
+
by_layer: Dict[str, int] = {}
|
|
278
|
+
|
|
279
|
+
for entry in entries:
|
|
280
|
+
if self._is_protected(entry, now):
|
|
281
|
+
protected += 1
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
strength = self.compute_strength(entry)
|
|
285
|
+
if strength < self.config.archive_threshold:
|
|
286
|
+
candidates.append(entry.id)
|
|
287
|
+
layer_name = entry.layer.value if hasattr(entry.layer, 'value') else str(entry.layer)
|
|
288
|
+
by_layer[layer_name] = by_layer.get(layer_name, 0) + 1
|
|
289
|
+
|
|
290
|
+
if dry_run:
|
|
291
|
+
return {
|
|
292
|
+
"candidates": len(candidates),
|
|
293
|
+
"archived": 0,
|
|
294
|
+
"protected": protected,
|
|
295
|
+
"by_layer": by_layer,
|
|
296
|
+
"sample_ids": candidates[:10],
|
|
297
|
+
"dry_run": True,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
archived = self.storage.archive_memories_by_ids(
|
|
301
|
+
candidates, reason="decayed", actor=actor
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
"candidates": len(candidates),
|
|
306
|
+
"archived": archived,
|
|
307
|
+
"protected": protected,
|
|
308
|
+
"by_layer": by_layer,
|
|
309
|
+
"sample_ids": candidates[:10],
|
|
310
|
+
"dry_run": False,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
def purge_old_archives(self, dry_run: bool = False,
|
|
314
|
+
actor: str = "gc") -> Dict[str, Any]:
|
|
315
|
+
"""永久删除过期的归档记忆
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
dry_run: 只预览不执行
|
|
319
|
+
actor: 操作者标识
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
{"to_purge": 候选数, "purged": 实际删除数, "dry_run": bool}
|
|
323
|
+
"""
|
|
324
|
+
if dry_run:
|
|
325
|
+
archived = self.storage.list_archived(limit=10000)
|
|
326
|
+
now = time.time()
|
|
327
|
+
cutoff = now - (self.config.purge_after_days * 86400)
|
|
328
|
+
count = sum(
|
|
329
|
+
1 for a in archived
|
|
330
|
+
if a.get("archived_at", 0) < cutoff
|
|
331
|
+
)
|
|
332
|
+
return {
|
|
333
|
+
"to_purge": count,
|
|
334
|
+
"purged": 0,
|
|
335
|
+
"dry_run": True,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
purged = self.storage.purge_archived(
|
|
339
|
+
older_than_days=self.config.purge_after_days,
|
|
340
|
+
actor=actor,
|
|
341
|
+
)
|
|
342
|
+
return {
|
|
343
|
+
"to_purge": purged,
|
|
344
|
+
"purged": purged,
|
|
345
|
+
"dry_run": False,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
def cleanup_temporary_layers(self, dry_run: bool = False,
|
|
349
|
+
actor: str = "gc") -> Dict[str, Any]:
|
|
350
|
+
"""按时间清理超期的感官层/短期层记忆
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
dry_run: 只预览不执行
|
|
354
|
+
actor: 操作者标识
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
{"sensory_archived": N, "short_term_archived": N, "dry_run": bool}
|
|
358
|
+
"""
|
|
359
|
+
results: Dict[str, Any] = {"dry_run": dry_run}
|
|
360
|
+
|
|
361
|
+
for layer_name, max_hours in [
|
|
362
|
+
("sensory", self.config.sensory_max_age_hours),
|
|
363
|
+
("short_term", self.config.short_term_max_age_hours),
|
|
364
|
+
]:
|
|
365
|
+
if dry_run:
|
|
366
|
+
conn = self.storage._get_conn()
|
|
367
|
+
cutoff = time.time() - (max_hours * 3600)
|
|
368
|
+
rows = conn.execute(
|
|
369
|
+
"SELECT COUNT(*) as cnt FROM memories"
|
|
370
|
+
" WHERE layer = ? AND category != 'trash' AND created_at < ?",
|
|
371
|
+
(layer_name, cutoff)
|
|
372
|
+
).fetchone()
|
|
373
|
+
count = rows["cnt"] if rows else 0
|
|
374
|
+
results[f"{layer_name}_candidates"] = count
|
|
375
|
+
results[f"{layer_name}_archived"] = 0
|
|
376
|
+
else:
|
|
377
|
+
result = self.storage.auto_archive(
|
|
378
|
+
max_age_hours=max_hours,
|
|
379
|
+
layer=layer_name,
|
|
380
|
+
actor=actor,
|
|
381
|
+
)
|
|
382
|
+
results[f"{layer_name}_archived"] = result.get("archived", 0)
|
|
383
|
+
|
|
384
|
+
return results
|
|
385
|
+
|
|
386
|
+
def run_gc(self, dry_run: bool = False,
|
|
387
|
+
skip_purge: bool = False,
|
|
388
|
+
actor: str = "gc") -> Dict[str, Any]:
|
|
389
|
+
"""执行完整 GC 周期
|
|
390
|
+
|
|
391
|
+
顺序:
|
|
392
|
+
1. compute_decay_scores — 更新衰减分数
|
|
393
|
+
2. cleanup_temporary_layers — 按时间清理感官/短期层
|
|
394
|
+
3. archive_decayed — 按强度归档
|
|
395
|
+
4. purge_old_archives — 清除过期归档(可跳过)
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
dry_run: 只预览不执行
|
|
399
|
+
skip_purge: 跳过永久删除步骤(保守模式)
|
|
400
|
+
actor: 操作者标识
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
完整 GC 报告
|
|
404
|
+
"""
|
|
405
|
+
report: Dict[str, Any] = {
|
|
406
|
+
"dry_run": dry_run,
|
|
407
|
+
"started_at": time.time(),
|
|
408
|
+
"policy": self.config.policy.value,
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# Step 1: 衰减评分
|
|
412
|
+
report["decay_scores"] = self.compute_decay_scores()
|
|
413
|
+
|
|
414
|
+
# Step 2: 时间清理临时层
|
|
415
|
+
report["temporary_cleanup"] = self.cleanup_temporary_layers(
|
|
416
|
+
dry_run=dry_run, actor=actor
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# Step 3: 强度归档
|
|
420
|
+
report["archive_decayed"] = self.archive_decayed(
|
|
421
|
+
dry_run=dry_run, actor=actor
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Step 4: 永久删除(可跳过)
|
|
425
|
+
if skip_purge:
|
|
426
|
+
report["purge_old_archives"] = {"skipped": True}
|
|
427
|
+
else:
|
|
428
|
+
report["purge_old_archives"] = self.purge_old_archives(
|
|
429
|
+
dry_run=dry_run, actor=actor
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
report["finished_at"] = time.time()
|
|
433
|
+
report["duration_ms"] = round(
|
|
434
|
+
(report["finished_at"] - report["started_at"]) * 1000, 2
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
return report
|
|
438
|
+
|
|
439
|
+
def gc_stats(self) -> Dict[str, Any]:
|
|
440
|
+
"""获取衰减/GC 统计概览"""
|
|
441
|
+
stats = self.storage.get_stats()
|
|
442
|
+
archived_count = len(self.storage.list_archived(limit=10000))
|
|
443
|
+
|
|
444
|
+
# 计算当前衰减概况
|
|
445
|
+
entries = self.storage.list_memories(limit=10000)
|
|
446
|
+
now = time.time()
|
|
447
|
+
|
|
448
|
+
strengths: List[float] = []
|
|
449
|
+
below = 0
|
|
450
|
+
protected = 0
|
|
451
|
+
|
|
452
|
+
for entry in entries:
|
|
453
|
+
if self._is_protected(entry, now):
|
|
454
|
+
protected += 1
|
|
455
|
+
continue
|
|
456
|
+
s = self.compute_strength(entry)
|
|
457
|
+
strengths.append(s)
|
|
458
|
+
if s < self.config.archive_threshold:
|
|
459
|
+
below += 1
|
|
460
|
+
|
|
461
|
+
avg = sum(strengths) / max(1, len(strengths))
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
"total_memories": stats.get("total", 0),
|
|
465
|
+
"archived_memories": archived_count,
|
|
466
|
+
"active_memories": len(strengths),
|
|
467
|
+
"protected_memories": protected,
|
|
468
|
+
"below_threshold": below,
|
|
469
|
+
"avg_strength": round(avg, 4),
|
|
470
|
+
"archive_threshold": self.config.archive_threshold,
|
|
471
|
+
"purge_after_days": self.config.purge_after_days,
|
|
472
|
+
"policy": self.config.policy.value,
|
|
473
|
+
"by_layer": stats.get("by_layer", {}),
|
|
474
|
+
}
|