ultra-memory 3.0.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/CLAWHUB.md +190 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/SKILL.md +383 -0
- package/package.json +107 -0
- package/platform/SYSTEM_PROMPT.md +184 -0
- package/platform/__pycache__/server.cpython-313.pyc +0 -0
- package/platform/openapi.yaml +305 -0
- package/platform/server.py +454 -0
- package/platform/tools_gemini.json +176 -0
- package/platform/tools_openai.json +207 -0
- package/scripts/__pycache__/cleanup.cpython-313.pyc +0 -0
- package/scripts/__pycache__/export.cpython-313.pyc +0 -0
- package/scripts/__pycache__/extract_entities.cpython-313.pyc +0 -0
- package/scripts/__pycache__/init.cpython-313.pyc +0 -0
- package/scripts/__pycache__/log_op.cpython-313.pyc +0 -0
- package/scripts/__pycache__/recall.cpython-313.pyc +0 -0
- package/scripts/__pycache__/restore.cpython-313.pyc +0 -0
- package/scripts/__pycache__/summarize.cpython-313.pyc +0 -0
- package/scripts/cleanup.py +156 -0
- package/scripts/export.py +158 -0
- package/scripts/extract_entities.py +289 -0
- package/scripts/init.py +243 -0
- package/scripts/log_op.py +328 -0
- package/scripts/mcp-server.js +341 -0
- package/scripts/recall.py +683 -0
- package/scripts/restore.py +267 -0
- package/scripts/summarize.py +389 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ultra-memory: 结构化实体提取
|
|
4
|
+
从操作日志中提取函数名、文件路径、依赖包、决策、错误等结构化实体,
|
|
5
|
+
写入 semantic/entities.jsonl,作为 recall 的第4检索层。
|
|
6
|
+
|
|
7
|
+
解决的问题:
|
|
8
|
+
"我们在 auth 模块动过哪些文件?" → 实体检索精确命中
|
|
9
|
+
"添加了哪些依赖?" → 依赖实体列表
|
|
10
|
+
"有哪些关键决策?" → 决策实体列表
|
|
11
|
+
以上查询用 bigram 关键词匹配完全无法回答。
|
|
12
|
+
|
|
13
|
+
被 log_op.py 在每次写入时自动调用,无需手动触发。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import re
|
|
19
|
+
import json
|
|
20
|
+
import argparse
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
if sys.stdout.encoding != "utf-8":
|
|
25
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
26
|
+
if sys.stderr.encoding != "utf-8":
|
|
27
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
ULTRA_MEMORY_HOME = Path(os.environ.get("ULTRA_MEMORY_HOME", Path.home() / ".ultra-memory"))
|
|
30
|
+
|
|
31
|
+
# ── 提取规则 ──────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
# 函数名:匹配 funcname() 或 def funcname
|
|
34
|
+
FUNC_PATTERN = re.compile(r'\b([a-zA-Z_][a-zA-Z0-9_]{2,})\s*\(\)', re.MULTILINE)
|
|
35
|
+
DEF_PATTERN = re.compile(r'\bdef\s+([a-zA-Z_][a-zA-Z0-9_]+)', re.MULTILINE)
|
|
36
|
+
|
|
37
|
+
# 文件路径:匹配含 / 或 \ 的带扩展名路径,以及 detail.path 字段
|
|
38
|
+
FILE_EXT = r'\.(py|js|ts|jsx|tsx|vue|html|css|scss|json|yaml|yml|toml|md|sql|sh|go|rs|java|rb|php|cpp|c|h)'
|
|
39
|
+
FILE_PATTERN = re.compile(
|
|
40
|
+
r'(?:[a-zA-Z0-9_\-\.]+/)+[a-zA-Z0-9_\-\.]+' + FILE_EXT + r'\b',
|
|
41
|
+
re.IGNORECASE
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# 依赖包:从 pip install / npm install 等命令提取
|
|
45
|
+
PIP_PATTERN = re.compile(r'pip3?\s+install\s+([\w\-]+(?:\s+[\w\-]+)*)', re.IGNORECASE)
|
|
46
|
+
NPM_PATTERN = re.compile(r'(?:npm|yarn|pnpm)\s+(?:install|add)\s+([\w\-@/]+)', re.IGNORECASE)
|
|
47
|
+
|
|
48
|
+
# 错误类型:ExceptionName 或 Error: message
|
|
49
|
+
ERROR_PATTERN = re.compile(r'\b([A-Z][a-zA-Z]+(?:Error|Exception|Warning|Fault))\b')
|
|
50
|
+
|
|
51
|
+
# 类名:class ClassName
|
|
52
|
+
CLASS_PATTERN = re.compile(r'\bclass\s+([A-Z][a-zA-Z0-9_]+)', re.MULTILINE)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def extract_from_op(op: dict) -> list[dict]:
|
|
56
|
+
"""
|
|
57
|
+
从单条操作记录中提取结构化实体。
|
|
58
|
+
|
|
59
|
+
返回实体列表,每条实体格式:
|
|
60
|
+
{
|
|
61
|
+
"entity_type": "function|file|dependency|decision|error|class",
|
|
62
|
+
"name": "clean_df",
|
|
63
|
+
"context": "实现数据清洗函数", # 原始 summary 片段
|
|
64
|
+
"session_id": "sess_xxx",
|
|
65
|
+
"op_seq": 42,
|
|
66
|
+
"ts": "2026-04-02T...",
|
|
67
|
+
"tags": ["code", "data"]
|
|
68
|
+
}
|
|
69
|
+
"""
|
|
70
|
+
entities = []
|
|
71
|
+
summary = op.get("summary", "")
|
|
72
|
+
detail = op.get("detail", {})
|
|
73
|
+
op_type = op.get("type", "")
|
|
74
|
+
ts = op.get("ts", "")
|
|
75
|
+
seq = op.get("seq", 0)
|
|
76
|
+
tags = op.get("tags", [])
|
|
77
|
+
session = op.get("_session_id", "")
|
|
78
|
+
|
|
79
|
+
base = {
|
|
80
|
+
"op_seq": seq,
|
|
81
|
+
"ts": ts,
|
|
82
|
+
"tags": tags,
|
|
83
|
+
"context": summary[:80],
|
|
84
|
+
"session_id": session,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
combined_text = summary + " " + json.dumps(detail, ensure_ascii=False)
|
|
88
|
+
|
|
89
|
+
# 1. 文件实体(优先从 detail.path 提取,最精确)
|
|
90
|
+
file_path = detail.get("path", "")
|
|
91
|
+
if file_path and op_type == "file_write":
|
|
92
|
+
entities.append({**base,
|
|
93
|
+
"entity_type": "file",
|
|
94
|
+
"name": file_path,
|
|
95
|
+
"op_type": op_type,
|
|
96
|
+
})
|
|
97
|
+
else:
|
|
98
|
+
# 从文本中提取文件路径
|
|
99
|
+
for m in FILE_PATTERN.finditer(combined_text):
|
|
100
|
+
fpath = m.group(0)
|
|
101
|
+
if len(fpath) > 3: # 过滤太短的误匹配
|
|
102
|
+
entities.append({**base,
|
|
103
|
+
"entity_type": "file",
|
|
104
|
+
"name": fpath,
|
|
105
|
+
"op_type": op_type,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
# 2. 函数/方法实体
|
|
109
|
+
for m in FUNC_PATTERN.finditer(combined_text):
|
|
110
|
+
name = m.group(1)
|
|
111
|
+
# 过滤:排除常见英文单词和太短的名字
|
|
112
|
+
if len(name) > 3 and name not in {
|
|
113
|
+
"print", "open", "read", "write", "list", "dict", "type",
|
|
114
|
+
"True", "False", "None", "self", "args", "kwargs"
|
|
115
|
+
}:
|
|
116
|
+
entities.append({**base,
|
|
117
|
+
"entity_type": "function",
|
|
118
|
+
"name": name,
|
|
119
|
+
})
|
|
120
|
+
for m in DEF_PATTERN.finditer(combined_text):
|
|
121
|
+
name = m.group(1)
|
|
122
|
+
if len(name) > 3:
|
|
123
|
+
entities.append({**base,
|
|
124
|
+
"entity_type": "function",
|
|
125
|
+
"name": name,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
# 3. 类实体
|
|
129
|
+
for m in CLASS_PATTERN.finditer(combined_text):
|
|
130
|
+
entities.append({**base,
|
|
131
|
+
"entity_type": "class",
|
|
132
|
+
"name": m.group(1),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
# 4. 依赖包实体(从 bash_exec 命令提取)
|
|
136
|
+
if op_type == "bash_exec":
|
|
137
|
+
cmd = detail.get("cmd", summary)
|
|
138
|
+
for m in PIP_PATTERN.finditer(cmd):
|
|
139
|
+
for pkg in m.group(1).split():
|
|
140
|
+
pkg = pkg.strip()
|
|
141
|
+
if pkg and not pkg.startswith("-"):
|
|
142
|
+
entities.append({**base,
|
|
143
|
+
"entity_type": "dependency",
|
|
144
|
+
"name": pkg,
|
|
145
|
+
"manager": "pip",
|
|
146
|
+
})
|
|
147
|
+
for m in NPM_PATTERN.finditer(cmd):
|
|
148
|
+
pkg = m.group(1).strip()
|
|
149
|
+
if pkg:
|
|
150
|
+
entities.append({**base,
|
|
151
|
+
"entity_type": "dependency",
|
|
152
|
+
"name": pkg,
|
|
153
|
+
"manager": "npm",
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
# 5. 决策实体(type == decision 全部保留,字段完整)
|
|
157
|
+
if op_type == "decision":
|
|
158
|
+
entities.append({**base,
|
|
159
|
+
"entity_type": "decision",
|
|
160
|
+
"name": summary[:60],
|
|
161
|
+
"rationale": detail.get("rationale", ""),
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
# 6. 错误实体(记录错误类型)
|
|
165
|
+
if op_type == "error":
|
|
166
|
+
for m in ERROR_PATTERN.finditer(combined_text):
|
|
167
|
+
entities.append({**base,
|
|
168
|
+
"entity_type": "error",
|
|
169
|
+
"name": m.group(1),
|
|
170
|
+
"message": summary[:60],
|
|
171
|
+
})
|
|
172
|
+
if not ERROR_PATTERN.search(combined_text):
|
|
173
|
+
# 没有识别到具体错误类型,记录通用错误
|
|
174
|
+
entities.append({**base,
|
|
175
|
+
"entity_type": "error",
|
|
176
|
+
"name": "UnknownError",
|
|
177
|
+
"message": summary[:60],
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return entities
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def deduplicate_entities(existing: list[dict], new_entities: list[dict]) -> list[dict]:
|
|
184
|
+
"""
|
|
185
|
+
实体去重:相同 (entity_type, name) 在同一 session 中只保留最新一条。
|
|
186
|
+
跨 session 的同类实体保留(用于追踪变化)。
|
|
187
|
+
"""
|
|
188
|
+
# 构建现有实体的 key 集合(session + type + name)
|
|
189
|
+
existing_keys = {
|
|
190
|
+
(e.get("session_id"), e.get("entity_type"), e.get("name"))
|
|
191
|
+
for e in existing
|
|
192
|
+
}
|
|
193
|
+
added = []
|
|
194
|
+
for ent in new_entities:
|
|
195
|
+
key = (ent.get("session_id"), ent.get("entity_type"), ent.get("name"))
|
|
196
|
+
if key not in existing_keys:
|
|
197
|
+
existing_keys.add(key)
|
|
198
|
+
added.append(ent)
|
|
199
|
+
else:
|
|
200
|
+
# 同 session 内相同实体:更新 ts 和 context(追加写入,不修改旧记录)
|
|
201
|
+
added.append(ent) # 允许多次出现,recall 时取最新
|
|
202
|
+
return added
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def append_entities(entities: list[dict], session_id: str):
|
|
206
|
+
"""将提取的实体追加写入 semantic/entities.jsonl"""
|
|
207
|
+
if not entities:
|
|
208
|
+
return
|
|
209
|
+
semantic_dir = ULTRA_MEMORY_HOME / "semantic"
|
|
210
|
+
semantic_dir.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
entities_file = semantic_dir / "entities.jsonl"
|
|
212
|
+
|
|
213
|
+
# 为每个实体附上 session_id(如果尚未有)
|
|
214
|
+
for ent in entities:
|
|
215
|
+
if not ent.get("session_id"):
|
|
216
|
+
ent["session_id"] = session_id
|
|
217
|
+
|
|
218
|
+
with open(entities_file, "a", encoding="utf-8") as f:
|
|
219
|
+
for ent in entities:
|
|
220
|
+
f.write(json.dumps(ent, ensure_ascii=False) + "\n")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def extract_and_store(session_id: str, op: dict):
|
|
224
|
+
"""对单条操作提取实体并写入 entities.jsonl(供 log_op.py 调用)"""
|
|
225
|
+
op["_session_id"] = session_id
|
|
226
|
+
entities = extract_from_op(op)
|
|
227
|
+
if entities:
|
|
228
|
+
append_entities(entities, session_id)
|
|
229
|
+
return entities
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def extract_all(session_id: str):
|
|
233
|
+
"""全量提取:对整个 ops.jsonl 重新提取实体(适合初次运行或修复)"""
|
|
234
|
+
session_dir = ULTRA_MEMORY_HOME / "sessions" / session_id
|
|
235
|
+
ops_file = session_dir / "ops.jsonl"
|
|
236
|
+
if not ops_file.exists():
|
|
237
|
+
print(f"[ultra-memory] ⚠️ ops.jsonl 不存在: {session_id}")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
# 清除该 session 的旧实体(重新提取)
|
|
241
|
+
semantic_dir = ULTRA_MEMORY_HOME / "semantic"
|
|
242
|
+
entities_file = semantic_dir / "entities.jsonl"
|
|
243
|
+
if entities_file.exists():
|
|
244
|
+
with open(entities_file, encoding="utf-8") as f:
|
|
245
|
+
existing = [json.loads(l) for l in f if l.strip()]
|
|
246
|
+
# 保留其他 session 的实体
|
|
247
|
+
kept = [e for e in existing if e.get("session_id") != session_id]
|
|
248
|
+
else:
|
|
249
|
+
kept = []
|
|
250
|
+
|
|
251
|
+
# 重新提取
|
|
252
|
+
new_entities = []
|
|
253
|
+
with open(ops_file, encoding="utf-8") as f:
|
|
254
|
+
for line in f:
|
|
255
|
+
line = line.strip()
|
|
256
|
+
if not line:
|
|
257
|
+
continue
|
|
258
|
+
try:
|
|
259
|
+
op = json.loads(line)
|
|
260
|
+
op["_session_id"] = session_id
|
|
261
|
+
new_entities.extend(extract_from_op(op))
|
|
262
|
+
except json.JSONDecodeError:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
all_entities = kept + new_entities
|
|
266
|
+
semantic_dir.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
with open(entities_file, "w", encoding="utf-8") as f:
|
|
268
|
+
for ent in all_entities:
|
|
269
|
+
f.write(json.dumps(ent, ensure_ascii=False) + "\n")
|
|
270
|
+
|
|
271
|
+
# 统计
|
|
272
|
+
type_counts: dict[str, int] = {}
|
|
273
|
+
for e in new_entities:
|
|
274
|
+
t = e.get("entity_type", "unknown")
|
|
275
|
+
type_counts[t] = type_counts.get(t, 0) + 1
|
|
276
|
+
|
|
277
|
+
print(f"[ultra-memory] ✅ 实体提取完成 (session: {session_id})")
|
|
278
|
+
for t, c in sorted(type_counts.items()):
|
|
279
|
+
print(f" {t}: {c} 个")
|
|
280
|
+
print(f" 总计: {len(new_entities)} 个实体")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
parser = argparse.ArgumentParser(description="从操作日志提取结构化实体")
|
|
285
|
+
parser.add_argument("--session", required=True, help="会话 ID")
|
|
286
|
+
parser.add_argument("--all", action="store_true",
|
|
287
|
+
help="全量重新提取(默认只提取未提取的)")
|
|
288
|
+
args = parser.parse_args()
|
|
289
|
+
extract_all(args.session)
|
package/scripts/init.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ultra-memory: 会话初始化脚本
|
|
4
|
+
在每次新会话开始时调用,创建三层记忆结构并注入历史上下文
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import json
|
|
10
|
+
import hashlib
|
|
11
|
+
import argparse
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# Windows 兼容:强制 stdout/stderr 使用 UTF-8
|
|
16
|
+
if sys.stdout.encoding != "utf-8":
|
|
17
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
18
|
+
if sys.stderr.encoding != "utf-8":
|
|
19
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
ULTRA_MEMORY_HOME = Path(os.environ.get("ULTRA_MEMORY_HOME", Path.home() / ".ultra-memory"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_session_id(project: str = "default") -> str:
|
|
26
|
+
ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
27
|
+
h = hashlib.md5(f"{project}{ts}".encode()).hexdigest()[:6]
|
|
28
|
+
return f"sess_{ts}_{h}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def init_session(project: str = "default", resume: bool = False) -> dict:
|
|
32
|
+
sessions_dir = ULTRA_MEMORY_HOME / "sessions"
|
|
33
|
+
semantic_dir = ULTRA_MEMORY_HOME / "semantic"
|
|
34
|
+
sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
semantic_dir.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
# 尝试恢复最近会话
|
|
38
|
+
if resume:
|
|
39
|
+
last = find_last_session(project, sessions_dir)
|
|
40
|
+
if last:
|
|
41
|
+
print(f"[ultra-memory] 恢复会话: {last['session_id']}")
|
|
42
|
+
inject_context(last, semantic_dir)
|
|
43
|
+
return last
|
|
44
|
+
|
|
45
|
+
# 新建会话
|
|
46
|
+
session_id = get_session_id(project)
|
|
47
|
+
session_dir = sessions_dir / session_id
|
|
48
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
meta = {
|
|
51
|
+
"session_id": session_id,
|
|
52
|
+
"project": project,
|
|
53
|
+
"started_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
54
|
+
"op_count": 0,
|
|
55
|
+
"last_summary_at": None,
|
|
56
|
+
"mode": detect_mode(),
|
|
57
|
+
}
|
|
58
|
+
with open(session_dir / "meta.json", "w", encoding="utf-8") as f:
|
|
59
|
+
json.dump(meta, f, ensure_ascii=False, indent=2)
|
|
60
|
+
|
|
61
|
+
# 初始化空 ops 日志
|
|
62
|
+
open(session_dir / "ops.jsonl", "w", encoding="utf-8").close()
|
|
63
|
+
|
|
64
|
+
# 注入历史上下文
|
|
65
|
+
inject_context(meta, semantic_dir)
|
|
66
|
+
|
|
67
|
+
# 更新会话索引
|
|
68
|
+
update_session_index(session_id, project, semantic_dir)
|
|
69
|
+
|
|
70
|
+
print(f"[ultra-memory] ✅ 会话初始化完成")
|
|
71
|
+
print(f"[ultra-memory] session_id: {session_id}")
|
|
72
|
+
print(f"[ultra-memory] 存储路径: {session_dir}")
|
|
73
|
+
print(f"[ultra-memory] 运行模式: {meta['mode']}")
|
|
74
|
+
print("MEMORY_READY")
|
|
75
|
+
|
|
76
|
+
return meta
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def find_last_session(project: str, sessions_dir: Path) -> dict | None:
|
|
80
|
+
"""找到同项目最近的会话"""
|
|
81
|
+
candidates = []
|
|
82
|
+
for d in sessions_dir.iterdir():
|
|
83
|
+
if not d.is_dir():
|
|
84
|
+
continue
|
|
85
|
+
meta_file = d / "meta.json"
|
|
86
|
+
if not meta_file.exists():
|
|
87
|
+
continue
|
|
88
|
+
with open(meta_file, encoding="utf-8") as f:
|
|
89
|
+
meta = json.load(f)
|
|
90
|
+
if meta.get("project") == project:
|
|
91
|
+
candidates.append(meta)
|
|
92
|
+
if not candidates:
|
|
93
|
+
return None
|
|
94
|
+
candidates.sort(key=lambda x: x["started_at"], reverse=True)
|
|
95
|
+
return candidates[0]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def inject_context(meta: dict, semantic_dir: Path):
|
|
99
|
+
"""将历史上下文注入到 stdout,供 Claude 读取"""
|
|
100
|
+
profile_file = semantic_dir / "user_profile.json"
|
|
101
|
+
index_file = semantic_dir / "session_index.json"
|
|
102
|
+
|
|
103
|
+
profile = {}
|
|
104
|
+
if profile_file.exists():
|
|
105
|
+
with open(profile_file, encoding="utf-8") as f:
|
|
106
|
+
profile = json.load(f)
|
|
107
|
+
|
|
108
|
+
recent_sessions = []
|
|
109
|
+
if index_file.exists():
|
|
110
|
+
with open(index_file, encoding="utf-8") as f:
|
|
111
|
+
index = json.load(f)
|
|
112
|
+
recent_sessions = index.get("sessions", [])[-3:] # 最近3个会话
|
|
113
|
+
|
|
114
|
+
if not profile and not recent_sessions:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
print("\n<!-- ULTRA-MEMORY CONTEXT INJECTION -->")
|
|
118
|
+
if profile:
|
|
119
|
+
stack = ", ".join(profile.get("tech_stack", []))
|
|
120
|
+
projects = ", ".join(profile.get("projects", []))
|
|
121
|
+
lang = profile.get("language", "zh-CN")
|
|
122
|
+
print(f"**已知背景(来自记忆层):**")
|
|
123
|
+
if stack:
|
|
124
|
+
print(f"- 技术栈: {stack}")
|
|
125
|
+
if projects:
|
|
126
|
+
print(f"- 活跃项目: {projects}")
|
|
127
|
+
if lang:
|
|
128
|
+
print(f"- 语言偏好: {lang}")
|
|
129
|
+
patterns = profile.get("observed_patterns", [])
|
|
130
|
+
for p in patterns[:2]:
|
|
131
|
+
print(f"- 偏好: {p}")
|
|
132
|
+
|
|
133
|
+
if recent_sessions:
|
|
134
|
+
print(f"\n**近期会话记录:**")
|
|
135
|
+
for s in reversed(recent_sessions):
|
|
136
|
+
ts = s.get("started_at", "")[:10]
|
|
137
|
+
proj = s.get("project", "")
|
|
138
|
+
summary = s.get("last_milestone", "")
|
|
139
|
+
if summary:
|
|
140
|
+
print(f"- [{ts}] 项目 {proj}: {summary}")
|
|
141
|
+
|
|
142
|
+
print("<!-- END INJECTION -->\n")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def detect_mode() -> str:
|
|
146
|
+
"""检测运行环境,决定使用哪种存储模式"""
|
|
147
|
+
try:
|
|
148
|
+
import lancedb # noqa
|
|
149
|
+
return "enhanced" # 向量检索模式
|
|
150
|
+
except ImportError:
|
|
151
|
+
return "lightweight" # 纯文件模式
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def check_context_pressure(session_id: str) -> str:
|
|
155
|
+
"""
|
|
156
|
+
检查当前会话的 context 压力,基于未压缩操作数量估算。
|
|
157
|
+
输出结构化状态供 Claude 解析。
|
|
158
|
+
|
|
159
|
+
压力级别(每隔10次操作调用一次):
|
|
160
|
+
low — 未压缩操作 < 20
|
|
161
|
+
medium — 20 ≤ 未压缩操作 < 40(留意增长)
|
|
162
|
+
high — 40 ≤ 未压缩操作 < 60(建议压缩)
|
|
163
|
+
critical — 未压缩操作 ≥ 60(立即压缩)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
压力级别字符串(low/medium/high/critical)
|
|
167
|
+
"""
|
|
168
|
+
session_dir = ULTRA_MEMORY_HOME / "sessions" / session_id
|
|
169
|
+
meta_file = session_dir / "meta.json"
|
|
170
|
+
|
|
171
|
+
op_count = 0
|
|
172
|
+
if meta_file.exists():
|
|
173
|
+
with open(meta_file, encoding="utf-8") as f:
|
|
174
|
+
meta = json.load(f)
|
|
175
|
+
op_count = meta.get("op_count", 0)
|
|
176
|
+
|
|
177
|
+
# 统计自上次压缩以来的未压缩操作数
|
|
178
|
+
uncompressed = 0
|
|
179
|
+
ops_file = session_dir / "ops.jsonl"
|
|
180
|
+
if ops_file.exists():
|
|
181
|
+
with open(ops_file, encoding="utf-8") as f:
|
|
182
|
+
for line in f:
|
|
183
|
+
line = line.strip()
|
|
184
|
+
if not line:
|
|
185
|
+
continue
|
|
186
|
+
try:
|
|
187
|
+
op = json.loads(line)
|
|
188
|
+
if not op.get("compressed"):
|
|
189
|
+
uncompressed += 1
|
|
190
|
+
except json.JSONDecodeError:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
if uncompressed >= 60:
|
|
194
|
+
level = "critical"
|
|
195
|
+
advice = "立即触发摘要压缩,保留最近 20 条操作"
|
|
196
|
+
elif uncompressed >= 40:
|
|
197
|
+
level = "high"
|
|
198
|
+
advice = "建议触发 summarize.py 压缩"
|
|
199
|
+
elif uncompressed >= 20:
|
|
200
|
+
level = "medium"
|
|
201
|
+
advice = "操作数适中,可继续但留意增长"
|
|
202
|
+
else:
|
|
203
|
+
level = "low"
|
|
204
|
+
advice = "无需操作"
|
|
205
|
+
|
|
206
|
+
print(f"CONTEXT_PRESSURE: {level}")
|
|
207
|
+
print(f"[ultra-memory] 未压缩操作数: {uncompressed} | 总操作数: {op_count} | 建议: {advice}")
|
|
208
|
+
|
|
209
|
+
if level in ("high", "critical"):
|
|
210
|
+
print(f"[ultra-memory] ⚡ 建议运行: python3 scripts/summarize.py --session {session_id}")
|
|
211
|
+
|
|
212
|
+
return level
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def update_session_index(session_id: str, project: str, semantic_dir: Path):
|
|
216
|
+
index_file = semantic_dir / "session_index.json"
|
|
217
|
+
index = {"sessions": []}
|
|
218
|
+
if index_file.exists():
|
|
219
|
+
with open(index_file, encoding="utf-8") as f:
|
|
220
|
+
index = json.load(f)
|
|
221
|
+
index["sessions"].append({
|
|
222
|
+
"session_id": session_id,
|
|
223
|
+
"project": project,
|
|
224
|
+
"started_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
225
|
+
"last_milestone": None,
|
|
226
|
+
})
|
|
227
|
+
# 只保留最近 100 个会话记录
|
|
228
|
+
index["sessions"] = index["sessions"][-100:]
|
|
229
|
+
with open(index_file, "w", encoding="utf-8") as f:
|
|
230
|
+
json.dump(index, f, ensure_ascii=False, indent=2)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if __name__ == "__main__":
|
|
234
|
+
parser = argparse.ArgumentParser(description="ultra-memory 会话初始化")
|
|
235
|
+
parser.add_argument("--project", default="default", help="项目名称")
|
|
236
|
+
parser.add_argument("--resume", action="store_true", help="尝试恢复最近会话")
|
|
237
|
+
parser.add_argument("--check-pressure", metavar="SESSION_ID", help="检查指定会话的 context 压力")
|
|
238
|
+
args = parser.parse_args()
|
|
239
|
+
|
|
240
|
+
if args.check_pressure:
|
|
241
|
+
check_context_pressure(args.check_pressure)
|
|
242
|
+
else:
|
|
243
|
+
init_session(args.project, args.resume)
|