ultra-memory 3.0.5 → 3.2.0
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.
- package/SKILL.md +141 -0
- package/integrations/__init__.py +1 -0
- package/integrations/langchain_memory.py +118 -0
- package/integrations/langgraph_checkpointer.py +76 -0
- package/integrations/n8n_nodes.py +150 -0
- package/package.json +121 -108
- package/scripts/auto_decay.py +351 -0
- package/scripts/cleanup.py +21 -0
- package/scripts/detect_contradictions.py +537 -0
- package/scripts/evolve_profile.py +414 -0
- package/scripts/extract_facts.py +471 -0
- package/scripts/log_op.py +42 -0
- package/scripts/multimodal/__init__.py +2 -0
- package/scripts/multimodal/extract_from_image.py +138 -0
- package/scripts/multimodal/extract_from_pdf.py +182 -0
- package/scripts/multimodal/transcribe_video.py +157 -0
package/package.json
CHANGED
|
@@ -1,108 +1,121 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ultra-memory",
|
|
3
|
-
"version": "3.0
|
|
4
|
-
"description": "超长会话记忆系统 — 5
|
|
5
|
-
"keywords": [
|
|
6
|
-
"ai",
|
|
7
|
-
"memory",
|
|
8
|
-
"long-context",
|
|
9
|
-
"claude",
|
|
10
|
-
"openclaw",
|
|
11
|
-
"mcp",
|
|
12
|
-
"llm",
|
|
13
|
-
"session-memory",
|
|
14
|
-
"semantic-search",
|
|
15
|
-
"ultra-memory",
|
|
16
|
-
"agent-memory",
|
|
17
|
-
"context-window",
|
|
18
|
-
"mem0-alternative",
|
|
19
|
-
"openai",
|
|
20
|
-
"gemini",
|
|
21
|
-
"qwen"
|
|
22
|
-
],
|
|
23
|
-
"homepage": "https://github.com/nanjingya/ultra-memory",
|
|
24
|
-
"bugs": {
|
|
25
|
-
"url": "https://github.com/nanjingya/ultra-memory/issues"
|
|
26
|
-
},
|
|
27
|
-
"license": "MIT",
|
|
28
|
-
"author": {
|
|
29
|
-
"name": "NanJingYa",
|
|
30
|
-
"url": "https://github.com/nanjingya"
|
|
31
|
-
},
|
|
32
|
-
"contributors": [
|
|
33
|
-
{
|
|
34
|
-
"name": "NanJingYa",
|
|
35
|
-
"url": "https://github.com/nanjingya"
|
|
36
|
-
}
|
|
37
|
-
],
|
|
38
|
-
"repository": {
|
|
39
|
-
"type": "git",
|
|
40
|
-
"url": "git+https://github.com/nanjingya/ultra-memory.git"
|
|
41
|
-
},
|
|
42
|
-
"bin": {
|
|
43
|
-
"ultra-memory": "scripts/mcp-server.js",
|
|
44
|
-
"ultra-memory-init": "scripts/init.py",
|
|
45
|
-
"ultra-memory-log": "scripts/log_op.py",
|
|
46
|
-
"ultra-memory-recall": "scripts/recall.py",
|
|
47
|
-
"ultra-memory-summarize": "scripts/summarize.py",
|
|
48
|
-
"ultra-memory-restore": "scripts/restore.py",
|
|
49
|
-
"ultra-memory-knowledge": "scripts/log_knowledge.py"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "ultra-memory",
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "超长会话记忆系统 — 5层记忆架构+自我进化引擎,支持所有LLM平台(Claude/GPT/Gemini/Qwen等)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"memory",
|
|
8
|
+
"long-context",
|
|
9
|
+
"claude",
|
|
10
|
+
"openclaw",
|
|
11
|
+
"mcp",
|
|
12
|
+
"llm",
|
|
13
|
+
"session-memory",
|
|
14
|
+
"semantic-search",
|
|
15
|
+
"ultra-memory",
|
|
16
|
+
"agent-memory",
|
|
17
|
+
"context-window",
|
|
18
|
+
"mem0-alternative",
|
|
19
|
+
"openai",
|
|
20
|
+
"gemini",
|
|
21
|
+
"qwen"
|
|
22
|
+
],
|
|
23
|
+
"homepage": "https://github.com/nanjingya/ultra-memory",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/nanjingya/ultra-memory/issues"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": {
|
|
29
|
+
"name": "NanJingYa",
|
|
30
|
+
"url": "https://github.com/nanjingya"
|
|
31
|
+
},
|
|
32
|
+
"contributors": [
|
|
33
|
+
{
|
|
34
|
+
"name": "NanJingYa",
|
|
35
|
+
"url": "https://github.com/nanjingya"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/nanjingya/ultra-memory.git"
|
|
41
|
+
},
|
|
42
|
+
"bin": {
|
|
43
|
+
"ultra-memory": "scripts/mcp-server.js",
|
|
44
|
+
"ultra-memory-init": "scripts/init.py",
|
|
45
|
+
"ultra-memory-log": "scripts/log_op.py",
|
|
46
|
+
"ultra-memory-recall": "scripts/recall.py",
|
|
47
|
+
"ultra-memory-summarize": "scripts/summarize.py",
|
|
48
|
+
"ultra-memory-restore": "scripts/restore.py",
|
|
49
|
+
"ultra-memory-knowledge": "scripts/log_knowledge.py",
|
|
50
|
+
"ultra-memory-extract-facts": "scripts/extract_facts.py",
|
|
51
|
+
"ultra-memory-evolve": "scripts/evolve_profile.py"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"test": "python3 test_e2e.py",
|
|
55
|
+
"start": "node scripts/mcp-server.js",
|
|
56
|
+
"server": "python3 platform/server.py"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18.0.0",
|
|
60
|
+
"python": ">=3.8.0"
|
|
61
|
+
},
|
|
62
|
+
"optionalDependencies": {
|
|
63
|
+
"pdfminer.six": "*",
|
|
64
|
+
"pytesseract": "*",
|
|
65
|
+
"whisper": "*"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"langchain": ">=0.1.0"
|
|
69
|
+
},
|
|
70
|
+
"os": [
|
|
71
|
+
"darwin",
|
|
72
|
+
"linux",
|
|
73
|
+
"win32"
|
|
74
|
+
],
|
|
75
|
+
"files": [
|
|
76
|
+
"scripts/",
|
|
77
|
+
"scripts/multimodal/",
|
|
78
|
+
"platform/",
|
|
79
|
+
"integrations/",
|
|
80
|
+
"SKILL.md",
|
|
81
|
+
"README.md",
|
|
82
|
+
"CLAWHUB.md"
|
|
83
|
+
],
|
|
84
|
+
"publishConfig": {
|
|
85
|
+
"registry": "https://registry.npmjs.org/"
|
|
86
|
+
},
|
|
87
|
+
"claudeskills": {
|
|
88
|
+
"format": "1.0",
|
|
89
|
+
"trigger": {
|
|
90
|
+
"zh": [
|
|
91
|
+
"记住",
|
|
92
|
+
"别忘了",
|
|
93
|
+
"上次",
|
|
94
|
+
"回忆",
|
|
95
|
+
"记忆",
|
|
96
|
+
"不要忘记",
|
|
97
|
+
"记录",
|
|
98
|
+
"跨会话",
|
|
99
|
+
"继续昨天"
|
|
100
|
+
],
|
|
101
|
+
"en": [
|
|
102
|
+
"remember",
|
|
103
|
+
"don't forget",
|
|
104
|
+
"what did we",
|
|
105
|
+
"recall",
|
|
106
|
+
"memory",
|
|
107
|
+
"log this",
|
|
108
|
+
"context lost",
|
|
109
|
+
"keep track"
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
"layer": {
|
|
113
|
+
"1": "ops.jsonl — 操作日志层(append-only)",
|
|
114
|
+
"2": "summary.md — 会话摘要层(里程碑压缩)",
|
|
115
|
+
"3": "semantic/ — 跨会话语义层(知识库+实体索引)",
|
|
116
|
+
"4": "entities.jsonl — 结构化实体索引(7类实体)",
|
|
117
|
+
"5": "tfidf_cache.json — 向量语义层(TF-IDF/sentence-transformers)",
|
|
118
|
+
"6": "evolution/ — 自我进化层(事实提取+矛盾检测+遗忘)"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ultra-memory: 自动遗忘引擎 (Evolution Engine Phase 3)
|
|
4
|
+
基于时间衰减 + 访问频率 + 重要性评分计算每条事实的 decay_score。
|
|
5
|
+
decay_score < 0.05 的事实标记为遗忘(soft-delete,不删除原始记录)。
|
|
6
|
+
|
|
7
|
+
衰减公式:
|
|
8
|
+
decay_score = importance_score × recency_weight × access_weight
|
|
9
|
+
|
|
10
|
+
recency_weight = 0.5 ^ (age_days / half_life_days) # half_life=30天
|
|
11
|
+
access_weight = min(1.0, log2(access_count + 1) / log2(11))
|
|
12
|
+
# access_count=0 → ~0, access_count=10 → ~0.95
|
|
13
|
+
|
|
14
|
+
衰减等级:
|
|
15
|
+
≥ 0.6 → "none" (健康)
|
|
16
|
+
0.4–0.6 → "mild" (轻度衰减)
|
|
17
|
+
0.2–0.4 → "moderate" (中度衰减)
|
|
18
|
+
0.05–0.2 → "severe" (即将遗忘)
|
|
19
|
+
< 0.05 → "forgotten" (触发遗忘)
|
|
20
|
+
|
|
21
|
+
被 cleanup.py --run-decay 调用,或每日定时触发。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import json
|
|
27
|
+
import argparse
|
|
28
|
+
import math
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
if sys.stdout.encoding != "utf-8":
|
|
33
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
34
|
+
if sys.stderr.encoding != "utf-8":
|
|
35
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
36
|
+
|
|
37
|
+
ULTRA_MEMORY_HOME = Path(os.environ.get("ULTRA_MEMORY_HOME", Path.home() / ".ultra-memory"))
|
|
38
|
+
|
|
39
|
+
# 默认衰减参数(可通过 config.json 覆盖)
|
|
40
|
+
DEFAULT_HALF_LIFE_DAYS = 30
|
|
41
|
+
DEFAULT_FORGET_THRESHOLD = 0.05
|
|
42
|
+
|
|
43
|
+
# 衰减等级边界
|
|
44
|
+
DECAY_LEVELS = [
|
|
45
|
+
(0.6, "none"),
|
|
46
|
+
(0.4, "mild"),
|
|
47
|
+
(0.2, "moderate"),
|
|
48
|
+
(0.05, "severe"),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── 工具函数 ───────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _now_iso() -> str:
|
|
56
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parse_ts(ts_str: str) -> datetime:
|
|
60
|
+
"""解析 ISO 时间字符串"""
|
|
61
|
+
if not ts_str:
|
|
62
|
+
return datetime.now(timezone.utc)
|
|
63
|
+
try:
|
|
64
|
+
# 移除 Z 后缀
|
|
65
|
+
ts_str = ts_str.replace("Z", "+00:00")
|
|
66
|
+
return datetime.fromisoformat(ts_str)
|
|
67
|
+
except ValueError:
|
|
68
|
+
return datetime.now(timezone.utc)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _load_config() -> dict:
|
|
72
|
+
"""加载配置(如果有 decay 相关配置)"""
|
|
73
|
+
config_file = ULTRA_MEMORY_HOME / "config.json"
|
|
74
|
+
if config_file.exists():
|
|
75
|
+
try:
|
|
76
|
+
with open(config_file, encoding="utf-8") as f:
|
|
77
|
+
return json.load(f)
|
|
78
|
+
except (json.JSONDecodeError, IOError):
|
|
79
|
+
pass
|
|
80
|
+
return {}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ── 衰减计算 ───────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def compute_decay_score(
|
|
87
|
+
fact_metadata: dict,
|
|
88
|
+
now: datetime,
|
|
89
|
+
) -> float:
|
|
90
|
+
"""
|
|
91
|
+
计算单条事实的衰减评分。
|
|
92
|
+
|
|
93
|
+
decay_score = importance_score × recency_weight × access_weight
|
|
94
|
+
"""
|
|
95
|
+
age_days = (now - _parse_ts(fact_metadata.get("last_updated", ""))).days
|
|
96
|
+
half_life_days = fact_metadata.get("ttl_days", DEFAULT_HALF_LIFE_DAYS)
|
|
97
|
+
|
|
98
|
+
# 时间衰减权重
|
|
99
|
+
recency_weight = math.pow(0.5, age_days / half_life_days) if half_life_days > 0 else 0.0
|
|
100
|
+
|
|
101
|
+
# 访问频率权重
|
|
102
|
+
access_count = fact_metadata.get("access_count", 0)
|
|
103
|
+
access_weight = min(1.0, math.log2(access_count + 1) / math.log2(11))
|
|
104
|
+
|
|
105
|
+
# 重要性评分
|
|
106
|
+
importance_score = fact_metadata.get("importance_score", 0.5)
|
|
107
|
+
|
|
108
|
+
score = importance_score * recency_weight * access_weight
|
|
109
|
+
|
|
110
|
+
# 从未访问且较老的事实额外惩罚
|
|
111
|
+
if access_count == 0 and age_days > half_life_days:
|
|
112
|
+
score *= 0.5
|
|
113
|
+
|
|
114
|
+
return max(0.0, min(1.0, score))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def compute_decay_level(score: float) -> str:
|
|
118
|
+
"""根据衰减评分确定衰减等级"""
|
|
119
|
+
for threshold, level in DECAY_LEVELS:
|
|
120
|
+
if score >= threshold:
|
|
121
|
+
return level
|
|
122
|
+
return "forgotten"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def compute_importance_score(fact: dict, meta: dict) -> float:
|
|
126
|
+
"""
|
|
127
|
+
计算事实的重要性评分 (0.0–1.0)。
|
|
128
|
+
来源类型 × 矛盾抵抗 × 用户确认度 的加权组合。
|
|
129
|
+
"""
|
|
130
|
+
base = 0.5
|
|
131
|
+
|
|
132
|
+
# 来源类型权重
|
|
133
|
+
source_weights = {
|
|
134
|
+
"milestone": 0.9,
|
|
135
|
+
"decision": 0.85,
|
|
136
|
+
"user_instruction": 0.8,
|
|
137
|
+
"file_write": 0.7,
|
|
138
|
+
"tool_call": 0.6,
|
|
139
|
+
"reasoning": 0.6,
|
|
140
|
+
"bash_exec": 0.55,
|
|
141
|
+
"error": 0.5,
|
|
142
|
+
"file_read": 0.4,
|
|
143
|
+
}
|
|
144
|
+
source_type = fact.get("source_type", "")
|
|
145
|
+
base = source_weights.get(source_type, 0.6)
|
|
146
|
+
|
|
147
|
+
# 矛盾抵抗:矛盾越少越稳定
|
|
148
|
+
contradiction_count = meta.get("contradiction_count", 0)
|
|
149
|
+
contradiction_penalty = min(0.3, contradiction_count * 0.1)
|
|
150
|
+
base = max(0.1, base - contradiction_penalty)
|
|
151
|
+
|
|
152
|
+
# 用户显式确认权重(如果 correction_history 有 manual 条目)
|
|
153
|
+
manual_corrections = [
|
|
154
|
+
c for c in meta.get("correction_history", [])
|
|
155
|
+
if c.get("source") == "manual"
|
|
156
|
+
]
|
|
157
|
+
if manual_corrections:
|
|
158
|
+
base = min(1.0, base + 0.15)
|
|
159
|
+
|
|
160
|
+
return max(0.1, min(1.0, base))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ── 遗忘处理 ───────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _load_facts() -> list[dict]:
|
|
167
|
+
"""加载所有 facts"""
|
|
168
|
+
facts_file = ULTRA_MEMORY_HOME / "evolution" / "facts.jsonl"
|
|
169
|
+
if not facts_file.exists():
|
|
170
|
+
return []
|
|
171
|
+
facts = []
|
|
172
|
+
with open(facts_file, encoding="utf-8") as f:
|
|
173
|
+
for line in f:
|
|
174
|
+
line = line.strip()
|
|
175
|
+
if not line:
|
|
176
|
+
continue
|
|
177
|
+
try:
|
|
178
|
+
facts.append(json.loads(line))
|
|
179
|
+
except json.JSONDecodeError:
|
|
180
|
+
continue
|
|
181
|
+
return facts
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _load_metadata() -> dict:
|
|
185
|
+
"""加载 fact_metadata.json"""
|
|
186
|
+
meta_file = ULTRA_MEMORY_HOME / "evolution" / "fact_metadata.json"
|
|
187
|
+
if not meta_file.exists():
|
|
188
|
+
return {"version": 1, "updated_at": _now_iso(), "facts": {}}
|
|
189
|
+
try:
|
|
190
|
+
with open(meta_file, encoding="utf-8") as f:
|
|
191
|
+
return json.load(f)
|
|
192
|
+
except (json.JSONDecodeError, IOError):
|
|
193
|
+
return {"version": 1, "updated_at": _now_iso(), "facts": {}}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _save_metadata(meta: dict):
|
|
197
|
+
"""原子写入 metadata"""
|
|
198
|
+
evolution_dir = ULTRA_MEMORY_HOME / "evolution"
|
|
199
|
+
evolution_dir.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
meta_file = evolution_dir / "fact_metadata.json"
|
|
201
|
+
meta["updated_at"] = _now_iso()
|
|
202
|
+
|
|
203
|
+
tmp_file = meta_file.with_suffix(".tmp")
|
|
204
|
+
with open(tmp_file, "w", encoding="utf-8") as f:
|
|
205
|
+
json.dump(meta, f, ensure_ascii=False, indent=2)
|
|
206
|
+
tmp_file.replace(meta_file)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def append_decay_log(entry: dict):
|
|
210
|
+
"""追加写入 decay_log.jsonl"""
|
|
211
|
+
evolution_dir = ULTRA_MEMORY_HOME / "evolution"
|
|
212
|
+
evolution_dir.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
log_file = evolution_dir / "decay_log.jsonl"
|
|
214
|
+
|
|
215
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
|
216
|
+
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── 全量衰减扫描 ───────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def run_decay_pass(session_id: str | None = None):
|
|
223
|
+
"""
|
|
224
|
+
执行全量衰减扫描。
|
|
225
|
+
session_id 不为 None 时只扫描指定 session 的事实。
|
|
226
|
+
"""
|
|
227
|
+
now = datetime.now(timezone.utc)
|
|
228
|
+
facts = _load_facts()
|
|
229
|
+
meta = _load_metadata()
|
|
230
|
+
|
|
231
|
+
config = _load_config()
|
|
232
|
+
forget_threshold = (
|
|
233
|
+
config.get("decay", {}).get("forget_threshold", DEFAULT_FORGET_THRESHOLD)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# 初始化所有 fact 的 metadata(如果不存在)
|
|
237
|
+
for fact in facts:
|
|
238
|
+
fid = fact.get("fact_id")
|
|
239
|
+
if not fid:
|
|
240
|
+
continue
|
|
241
|
+
if fid not in meta["facts"]:
|
|
242
|
+
meta["facts"][fid] = {
|
|
243
|
+
"confidence": fact.get("confidence", 0.7),
|
|
244
|
+
"access_count": fact.get("access_count", 1),
|
|
245
|
+
"last_accessed": fact.get("last_accessed", _now_iso()),
|
|
246
|
+
"last_updated": fact.get("ts", _now_iso()),
|
|
247
|
+
"importance_score": compute_importance_score(fact, {}),
|
|
248
|
+
"decay_level": "none",
|
|
249
|
+
"ttl_days": DEFAULT_HALF_LIFE_DAYS,
|
|
250
|
+
"expires_at": None,
|
|
251
|
+
"status": "active",
|
|
252
|
+
"contradiction_count": fact.get("contradiction_count", 0),
|
|
253
|
+
"correction_history": [],
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# 计算每条事实的衰减
|
|
257
|
+
forgotten_ids = []
|
|
258
|
+
severe_ids = []
|
|
259
|
+
updated_ids = []
|
|
260
|
+
|
|
261
|
+
for fid, fact_meta in meta["facts"].items():
|
|
262
|
+
if fact_meta.get("status") in ("forgotten", "superseded"):
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
# session_id 过滤
|
|
266
|
+
if session_id:
|
|
267
|
+
# 找到对应 fact
|
|
268
|
+
matching_fact = next(
|
|
269
|
+
(f for f in facts if f.get("fact_id") == fid), None
|
|
270
|
+
)
|
|
271
|
+
if not matching_fact:
|
|
272
|
+
continue
|
|
273
|
+
if matching_fact.get("session_id") != session_id:
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
# TTL 过期检测
|
|
277
|
+
if fact_meta.get("expires_at"):
|
|
278
|
+
expires_at = _parse_ts(fact_meta["expires_at"])
|
|
279
|
+
if now >= expires_at:
|
|
280
|
+
fact_meta["decay_level"] = "forgotten"
|
|
281
|
+
|
|
282
|
+
# 重新计算重要性评分(基于最新事实数据)
|
|
283
|
+
matching_fact = next(
|
|
284
|
+
(f for f in facts if f.get("fact_id") == fid), None
|
|
285
|
+
)
|
|
286
|
+
if matching_fact:
|
|
287
|
+
fact_meta["importance_score"] = compute_importance_score(
|
|
288
|
+
matching_fact, fact_meta
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# 计算衰减评分
|
|
292
|
+
decay_score = compute_decay_score(fact_meta, now)
|
|
293
|
+
old_level = fact_meta.get("decay_level", "none")
|
|
294
|
+
new_level = compute_decay_level(decay_score)
|
|
295
|
+
fact_meta["decay_level"] = new_level
|
|
296
|
+
|
|
297
|
+
# 更新 ttl_days 配置
|
|
298
|
+
ttl_days = fact_meta.get("ttl_days", DEFAULT_HALF_LIFE_DAYS)
|
|
299
|
+
|
|
300
|
+
# 触发遗忘
|
|
301
|
+
if new_level == "forgotten" and old_level != "forgotten":
|
|
302
|
+
fact_meta["status"] = "forgotten"
|
|
303
|
+
fact_meta["forgotten_at"] = _now_iso()
|
|
304
|
+
forgotten_ids.append(fid)
|
|
305
|
+
|
|
306
|
+
append_decay_log({
|
|
307
|
+
"ts": _now_iso(),
|
|
308
|
+
"fact_id": fid,
|
|
309
|
+
"reason": "ttl_expired" if fact_meta.get("expires_at") else "importance_decay",
|
|
310
|
+
"decay_level_before": old_level,
|
|
311
|
+
"action": "marked_forgotten",
|
|
312
|
+
"decay_score": round(decay_score, 3),
|
|
313
|
+
"session_id": session_id or "system",
|
|
314
|
+
})
|
|
315
|
+
elif new_level == "severe" and old_level in ("none", "mild", "moderate"):
|
|
316
|
+
severe_ids.append(fid)
|
|
317
|
+
|
|
318
|
+
if new_level != old_level:
|
|
319
|
+
updated_ids.append(fid)
|
|
320
|
+
|
|
321
|
+
_save_metadata(meta)
|
|
322
|
+
|
|
323
|
+
print(f"[ultra-memory] ✅ 衰减扫描完成 (session: {session_id or 'all'})")
|
|
324
|
+
print(f" 遗忘: {len(forgotten_ids)} 条")
|
|
325
|
+
print(f" 严重衰减: {len(severe_ids)} 条")
|
|
326
|
+
print(f" 等级变化: {len(updated_ids)} 条")
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
"forgotten": forgotten_ids,
|
|
330
|
+
"severe": severe_ids,
|
|
331
|
+
"updated": updated_ids,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ── CLI ─────────────────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
if __name__ == "__main__":
|
|
339
|
+
parser = argparse.ArgumentParser(description="执行事实衰减扫描")
|
|
340
|
+
parser.add_argument(
|
|
341
|
+
"--session", default=None,
|
|
342
|
+
help="会话 ID(省略则扫描所有事实)"
|
|
343
|
+
)
|
|
344
|
+
parser.add_argument(
|
|
345
|
+
"--run", action="store_true",
|
|
346
|
+
help="执行衰减扫描(配合 cleanup.py 使用)"
|
|
347
|
+
)
|
|
348
|
+
args = parser.parse_args()
|
|
349
|
+
|
|
350
|
+
result = run_decay_pass(args.session)
|
|
351
|
+
sys.exit(0)
|
package/scripts/cleanup.py
CHANGED
|
@@ -152,5 +152,26 @@ if __name__ == "__main__":
|
|
|
152
152
|
parser.add_argument("--archive-only", action="store_true", help="只归档到 archive/ 目录,不删除")
|
|
153
153
|
parser.add_argument("--dry-run", action="store_true", help="演习模式,只打印不执行")
|
|
154
154
|
parser.add_argument("--project", default=None, help="只清理指定项目(默认所有项目)")
|
|
155
|
+
parser.add_argument(
|
|
156
|
+
"--run-decay", action="store_true",
|
|
157
|
+
help="执行事实衰减扫描(auto_decay.py),在清理前运行"
|
|
158
|
+
)
|
|
155
159
|
args = parser.parse_args()
|
|
160
|
+
|
|
161
|
+
if args.run_decay:
|
|
162
|
+
import subprocess
|
|
163
|
+
scripts_dir = Path(__file__).parent
|
|
164
|
+
python = sys.executable
|
|
165
|
+
startupinfo = subprocess.STARTUPINFO()
|
|
166
|
+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
167
|
+
print("[ultra-memory] 运行事实衰减扫描...")
|
|
168
|
+
result = subprocess.run(
|
|
169
|
+
[python, str(scripts_dir / "auto_decay.py"), "--session", args.project or ""],
|
|
170
|
+
capture_output=True, text=True, startupinfo=startupinfo,
|
|
171
|
+
)
|
|
172
|
+
if result.stdout:
|
|
173
|
+
print(result.stdout)
|
|
174
|
+
if result.stderr:
|
|
175
|
+
print(result.stderr, file=sys.stderr)
|
|
176
|
+
|
|
156
177
|
cleanup(args.days, args.archive_only, args.dry_run, args.project)
|