agmem 0.2.1__py3-none-any.whl → 0.3.0__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.
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/METADATA +338 -27
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/RECORD +21 -9
- memvcs/core/agents.py +411 -0
- memvcs/core/archaeology.py +410 -0
- memvcs/core/collaboration.py +435 -0
- memvcs/core/compliance.py +427 -0
- memvcs/core/confidence.py +379 -0
- memvcs/core/daemon.py +735 -0
- memvcs/core/delta.py +45 -23
- memvcs/core/private_search.py +327 -0
- memvcs/core/search_index.py +538 -0
- memvcs/core/semantic_graph.py +388 -0
- memvcs/core/session.py +520 -0
- memvcs/core/timetravel.py +430 -0
- memvcs/integrations/mcp_server.py +775 -4
- memvcs/integrations/web_ui/server.py +424 -0
- memvcs/integrations/web_ui/websocket.py +223 -0
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/WHEEL +0 -0
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/entry_points.txt +0 -0
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.2.1.dist-info → agmem-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory Archaeology - Deep history exploration and analysis tools.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- Historical context reconstruction
|
|
6
|
+
- Memory evolution tracking
|
|
7
|
+
- Forgotten knowledge discovery
|
|
8
|
+
- Pattern analysis across time
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime, timedelta, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class MemoryEvolution:
|
|
21
|
+
"""Tracks how a memory file evolved over time."""
|
|
22
|
+
|
|
23
|
+
path: str
|
|
24
|
+
first_seen: str
|
|
25
|
+
last_modified: str
|
|
26
|
+
version_count: int
|
|
27
|
+
commits: List[str]
|
|
28
|
+
size_history: List[Tuple[str, int]] # (timestamp, size)
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"path": self.path,
|
|
33
|
+
"first_seen": self.first_seen,
|
|
34
|
+
"last_modified": self.last_modified,
|
|
35
|
+
"version_count": self.version_count,
|
|
36
|
+
"commits": self.commits,
|
|
37
|
+
"size_history": self.size_history,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ForgottenMemory:
|
|
43
|
+
"""A memory that hasn't been accessed recently."""
|
|
44
|
+
|
|
45
|
+
path: str
|
|
46
|
+
last_accessed: str
|
|
47
|
+
days_since_access: int
|
|
48
|
+
content_preview: str
|
|
49
|
+
memory_type: str
|
|
50
|
+
relevance_score: float = 0.0
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
53
|
+
return {
|
|
54
|
+
"path": self.path,
|
|
55
|
+
"last_accessed": self.last_accessed,
|
|
56
|
+
"days_since_access": self.days_since_access,
|
|
57
|
+
"content_preview": self.content_preview,
|
|
58
|
+
"memory_type": self.memory_type,
|
|
59
|
+
"relevance_score": self.relevance_score,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class TemporalPattern:
|
|
65
|
+
"""A pattern in memory activity over time."""
|
|
66
|
+
|
|
67
|
+
pattern_type: str # "burst", "periodic", "declining", "growing"
|
|
68
|
+
description: str
|
|
69
|
+
files_involved: List[str]
|
|
70
|
+
time_range: Tuple[str, str]
|
|
71
|
+
confidence: float
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class HistoryExplorer:
|
|
75
|
+
"""Explores memory history across commits."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, repo_root: Path):
|
|
78
|
+
self.repo_root = Path(repo_root)
|
|
79
|
+
|
|
80
|
+
def get_file_history(self, relative_path: str, max_commits: int = 50) -> List[Dict[str, Any]]:
|
|
81
|
+
"""Get commit history for a specific file."""
|
|
82
|
+
try:
|
|
83
|
+
from memvcs.core.repository import Repository
|
|
84
|
+
|
|
85
|
+
repo = Repository(self.repo_root)
|
|
86
|
+
commits = repo.get_log(max_count=max_commits)
|
|
87
|
+
|
|
88
|
+
file_commits = []
|
|
89
|
+
for commit_info in commits:
|
|
90
|
+
# Check if file was in this commit's tree
|
|
91
|
+
# This is a simplified check - full implementation would walk the tree
|
|
92
|
+
file_commits.append(
|
|
93
|
+
{
|
|
94
|
+
"commit": commit_info["short_hash"],
|
|
95
|
+
"message": commit_info["message"],
|
|
96
|
+
"timestamp": commit_info.get("timestamp", ""),
|
|
97
|
+
"author": commit_info.get("author", ""),
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return file_commits[:max_commits]
|
|
102
|
+
except Exception:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
def get_memory_evolution(self, relative_path: str) -> Optional[MemoryEvolution]:
|
|
106
|
+
"""Track how a memory file evolved."""
|
|
107
|
+
try:
|
|
108
|
+
from memvcs.core.repository import Repository
|
|
109
|
+
|
|
110
|
+
repo = Repository(self.repo_root)
|
|
111
|
+
commits = repo.get_log(max_count=100)
|
|
112
|
+
|
|
113
|
+
if not commits:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
first_seen = commits[-1].get("timestamp", "")
|
|
117
|
+
last_modified = commits[0].get("timestamp", "")
|
|
118
|
+
commit_hashes = [c["short_hash"] for c in commits]
|
|
119
|
+
|
|
120
|
+
return MemoryEvolution(
|
|
121
|
+
path=relative_path,
|
|
122
|
+
first_seen=first_seen,
|
|
123
|
+
last_modified=last_modified,
|
|
124
|
+
version_count=len(commits),
|
|
125
|
+
commits=commit_hashes,
|
|
126
|
+
size_history=[], # Would require content at each commit
|
|
127
|
+
)
|
|
128
|
+
except Exception:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def compare_versions(self, path: str, commit1: str, commit2: str) -> Dict[str, Any]:
|
|
132
|
+
"""Compare two versions of a file."""
|
|
133
|
+
try:
|
|
134
|
+
from memvcs.core.repository import Repository
|
|
135
|
+
from memvcs.core.diff import DiffEngine
|
|
136
|
+
|
|
137
|
+
repo = Repository(self.repo_root)
|
|
138
|
+
engine = DiffEngine(repo.object_store)
|
|
139
|
+
|
|
140
|
+
diff = engine.diff_commits(commit1, commit2)
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
"path": path,
|
|
144
|
+
"from_commit": commit1,
|
|
145
|
+
"to_commit": commit2,
|
|
146
|
+
"has_changes": len(diff.files) > 0,
|
|
147
|
+
"files_changed": len(diff.files),
|
|
148
|
+
}
|
|
149
|
+
except Exception as e:
|
|
150
|
+
return {"error": str(e)}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ForgottenKnowledgeFinder:
|
|
154
|
+
"""Discovers forgotten or under-utilized memories."""
|
|
155
|
+
|
|
156
|
+
def __init__(self, repo_root: Path, access_log_path: Optional[Path] = None):
|
|
157
|
+
self.repo_root = Path(repo_root)
|
|
158
|
+
self.mem_dir = self.repo_root / ".mem"
|
|
159
|
+
self.access_log = access_log_path or (self.mem_dir / "access.log")
|
|
160
|
+
|
|
161
|
+
def _load_access_times(self) -> Dict[str, str]:
|
|
162
|
+
"""Load last access times from log."""
|
|
163
|
+
access_times = {}
|
|
164
|
+
if self.access_log.exists():
|
|
165
|
+
try:
|
|
166
|
+
for line in self.access_log.read_text().strip().split("\n"):
|
|
167
|
+
if line:
|
|
168
|
+
parts = line.split("|")
|
|
169
|
+
if len(parts) >= 2:
|
|
170
|
+
access_times[parts[1]] = parts[0]
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
return access_times
|
|
174
|
+
|
|
175
|
+
def find_forgotten(self, days_threshold: int = 30, limit: int = 20) -> List[ForgottenMemory]:
|
|
176
|
+
"""Find memories not accessed in the given time period."""
|
|
177
|
+
from memvcs.core.repository import Repository
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
repo = Repository(self.repo_root)
|
|
181
|
+
current_dir = repo.current_dir
|
|
182
|
+
|
|
183
|
+
access_times = self._load_access_times()
|
|
184
|
+
now = datetime.now(timezone.utc)
|
|
185
|
+
threshold = now - timedelta(days=days_threshold)
|
|
186
|
+
|
|
187
|
+
forgotten = []
|
|
188
|
+
for filepath in current_dir.rglob("*"):
|
|
189
|
+
if not filepath.is_file():
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
rel_path = str(filepath.relative_to(current_dir))
|
|
193
|
+
|
|
194
|
+
# Determine last access
|
|
195
|
+
if rel_path in access_times:
|
|
196
|
+
last_access = access_times[rel_path]
|
|
197
|
+
else:
|
|
198
|
+
# Use modification time as fallback
|
|
199
|
+
mtime = filepath.stat().st_mtime
|
|
200
|
+
last_access = datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat()
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
last_dt = datetime.fromisoformat(last_access.replace("Z", "+00:00"))
|
|
204
|
+
if last_dt < threshold:
|
|
205
|
+
days_ago = (now - last_dt).days
|
|
206
|
+
content = filepath.read_text(encoding="utf-8", errors="replace")[:200]
|
|
207
|
+
memory_type = self._infer_memory_type(filepath)
|
|
208
|
+
|
|
209
|
+
forgotten.append(
|
|
210
|
+
ForgottenMemory(
|
|
211
|
+
path=rel_path,
|
|
212
|
+
last_accessed=last_access,
|
|
213
|
+
days_since_access=days_ago,
|
|
214
|
+
content_preview=content,
|
|
215
|
+
memory_type=memory_type,
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
# Sort by days since access (oldest first)
|
|
222
|
+
forgotten.sort(key=lambda x: x.days_since_access, reverse=True)
|
|
223
|
+
return forgotten[:limit]
|
|
224
|
+
|
|
225
|
+
except Exception:
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
def _infer_memory_type(self, filepath: Path) -> str:
|
|
229
|
+
"""Infer memory type from path."""
|
|
230
|
+
parts = filepath.parts
|
|
231
|
+
for mt in ["episodic", "semantic", "procedural"]:
|
|
232
|
+
if mt in parts:
|
|
233
|
+
return mt
|
|
234
|
+
return "unknown"
|
|
235
|
+
|
|
236
|
+
def rediscover_relevant(self, query: str, days_threshold: int = 30) -> List[ForgottenMemory]:
|
|
237
|
+
"""Find forgotten memories relevant to a query."""
|
|
238
|
+
forgotten = self.find_forgotten(days_threshold=days_threshold, limit=100)
|
|
239
|
+
|
|
240
|
+
# Simple relevance scoring based on query terms
|
|
241
|
+
query_terms = set(query.lower().split())
|
|
242
|
+
|
|
243
|
+
for memory in forgotten:
|
|
244
|
+
content_lower = memory.content_preview.lower()
|
|
245
|
+
matches = sum(1 for term in query_terms if term in content_lower)
|
|
246
|
+
memory.relevance_score = matches / max(1, len(query_terms))
|
|
247
|
+
|
|
248
|
+
# Sort by relevance
|
|
249
|
+
forgotten.sort(key=lambda x: x.relevance_score, reverse=True)
|
|
250
|
+
return [m for m in forgotten if m.relevance_score > 0][:20]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class PatternAnalyzer:
|
|
254
|
+
"""Analyzes temporal patterns in memory activity."""
|
|
255
|
+
|
|
256
|
+
def __init__(self, repo_root: Path):
|
|
257
|
+
self.repo_root = Path(repo_root)
|
|
258
|
+
|
|
259
|
+
def analyze_activity_patterns(self, days: int = 90) -> List[TemporalPattern]:
|
|
260
|
+
"""Analyze patterns in memory activity."""
|
|
261
|
+
from memvcs.core.repository import Repository
|
|
262
|
+
|
|
263
|
+
patterns = []
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
repo = Repository(self.repo_root)
|
|
267
|
+
commits = repo.get_log(max_count=500)
|
|
268
|
+
|
|
269
|
+
if len(commits) < 5:
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
# Group commits by day
|
|
273
|
+
by_day: Dict[str, int] = {}
|
|
274
|
+
for commit in commits:
|
|
275
|
+
ts = commit.get("timestamp", "")[:10]
|
|
276
|
+
if ts:
|
|
277
|
+
by_day[ts] = by_day.get(ts, 0) + 1
|
|
278
|
+
|
|
279
|
+
# Detect patterns
|
|
280
|
+
sorted_days = sorted(by_day.keys())
|
|
281
|
+
|
|
282
|
+
if sorted_days:
|
|
283
|
+
# Check for bursts (days with >3x average)
|
|
284
|
+
avg_commits = sum(by_day.values()) / len(by_day)
|
|
285
|
+
burst_days = [d for d, c in by_day.items() if c > avg_commits * 3]
|
|
286
|
+
if burst_days:
|
|
287
|
+
patterns.append(
|
|
288
|
+
TemporalPattern(
|
|
289
|
+
pattern_type="burst",
|
|
290
|
+
description=f"High activity bursts on {len(burst_days)} days",
|
|
291
|
+
files_involved=[],
|
|
292
|
+
time_range=(burst_days[0], burst_days[-1]),
|
|
293
|
+
confidence=0.8,
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Check for declining activity
|
|
298
|
+
recent_30 = [
|
|
299
|
+
c for d, c in by_day.items() if d >= sorted_days[-30] if len(sorted_days) >= 30
|
|
300
|
+
]
|
|
301
|
+
older_30 = [
|
|
302
|
+
c for d, c in by_day.items() if d < sorted_days[-30] if len(sorted_days) >= 60
|
|
303
|
+
]
|
|
304
|
+
if recent_30 and older_30:
|
|
305
|
+
recent_avg = sum(recent_30) / len(recent_30)
|
|
306
|
+
older_avg = sum(older_30) / len(older_30)
|
|
307
|
+
if recent_avg < older_avg * 0.5:
|
|
308
|
+
patterns.append(
|
|
309
|
+
TemporalPattern(
|
|
310
|
+
pattern_type="declining",
|
|
311
|
+
description="Activity has declined significantly",
|
|
312
|
+
files_involved=[],
|
|
313
|
+
time_range=(sorted_days[0], sorted_days[-1]),
|
|
314
|
+
confidence=0.7,
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
return patterns
|
|
322
|
+
|
|
323
|
+
def get_memory_hotspots(self, days: int = 30) -> List[Dict[str, Any]]:
|
|
324
|
+
"""Find most frequently modified memories."""
|
|
325
|
+
from memvcs.core.repository import Repository
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
repo = Repository(self.repo_root)
|
|
329
|
+
commits = repo.get_log(max_count=100)
|
|
330
|
+
|
|
331
|
+
# This is a simplified version - full implementation would
|
|
332
|
+
# track which files changed in each commit
|
|
333
|
+
file_activity: Dict[str, int] = {}
|
|
334
|
+
|
|
335
|
+
# Count commits as proxy for activity
|
|
336
|
+
return [
|
|
337
|
+
{"path": path, "activity_count": count}
|
|
338
|
+
for path, count in sorted(file_activity.items(), key=lambda x: -x[1])[:10]
|
|
339
|
+
]
|
|
340
|
+
except Exception:
|
|
341
|
+
return []
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class ContextReconstructor:
|
|
345
|
+
"""Reconstructs historical context around memories."""
|
|
346
|
+
|
|
347
|
+
def __init__(self, repo_root: Path):
|
|
348
|
+
self.repo_root = Path(repo_root)
|
|
349
|
+
|
|
350
|
+
def reconstruct_context(
|
|
351
|
+
self, path: str, target_date: str, window_days: int = 7
|
|
352
|
+
) -> Dict[str, Any]:
|
|
353
|
+
"""Reconstruct what was happening around a memory at a point in time."""
|
|
354
|
+
from memvcs.core.repository import Repository
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
repo = Repository(self.repo_root)
|
|
358
|
+
commits = repo.get_log(max_count=500)
|
|
359
|
+
|
|
360
|
+
# Find commits around target date
|
|
361
|
+
target_dt = datetime.fromisoformat(target_date.replace("Z", "+00:00"))
|
|
362
|
+
window_start = target_dt - timedelta(days=window_days)
|
|
363
|
+
window_end = target_dt + timedelta(days=window_days)
|
|
364
|
+
|
|
365
|
+
nearby_commits = []
|
|
366
|
+
for commit in commits:
|
|
367
|
+
ts = commit.get("timestamp", "")
|
|
368
|
+
if ts:
|
|
369
|
+
try:
|
|
370
|
+
commit_dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
371
|
+
if window_start <= commit_dt <= window_end:
|
|
372
|
+
nearby_commits.append(commit)
|
|
373
|
+
except Exception:
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
"target_path": path,
|
|
378
|
+
"target_date": target_date,
|
|
379
|
+
"window_days": window_days,
|
|
380
|
+
"commits_in_window": len(nearby_commits),
|
|
381
|
+
"commits": nearby_commits[:20],
|
|
382
|
+
"summary": f"Found {len(nearby_commits)} commits within {window_days} days of {target_date[:10]}",
|
|
383
|
+
}
|
|
384
|
+
except Exception as e:
|
|
385
|
+
return {"error": str(e)}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# --- Dashboard Helper ---
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def get_archaeology_dashboard(repo_root: Path) -> Dict[str, Any]:
|
|
392
|
+
"""Get data for memory archaeology dashboard."""
|
|
393
|
+
pattern_analyzer = PatternAnalyzer(repo_root)
|
|
394
|
+
forgotten_finder = ForgottenKnowledgeFinder(repo_root)
|
|
395
|
+
|
|
396
|
+
forgotten = forgotten_finder.find_forgotten(days_threshold=30, limit=10)
|
|
397
|
+
patterns = pattern_analyzer.analyze_activity_patterns(days=90)
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
"forgotten_memories": [f.to_dict() for f in forgotten],
|
|
401
|
+
"forgotten_count": len(forgotten),
|
|
402
|
+
"activity_patterns": [
|
|
403
|
+
{
|
|
404
|
+
"type": p.pattern_type,
|
|
405
|
+
"description": p.description,
|
|
406
|
+
"confidence": p.confidence,
|
|
407
|
+
}
|
|
408
|
+
for p in patterns
|
|
409
|
+
],
|
|
410
|
+
}
|