@trac3er/oh-my-god 2.0.0-beta.2 → 2.1.0-alpha
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/OMG-setup.sh +33 -0
- package/README.md +43 -0
- package/claude_experimental/__init__.py +27 -0
- package/claude_experimental/_compat.py +12 -0
- package/claude_experimental/_degradation.py +210 -0
- package/claude_experimental/_flags.py +35 -0
- package/claude_experimental/_lifecycle.py +122 -0
- package/claude_experimental/integration/__init__.py +16 -0
- package/claude_experimental/integration/_placeholder.py +7 -0
- package/claude_experimental/integration/autotuner.py +182 -0
- package/claude_experimental/integration/checkpoints.py +327 -0
- package/claude_experimental/integration/experiments.py +302 -0
- package/claude_experimental/integration/openapi_gen.py +428 -0
- package/claude_experimental/integration/streaming.py +131 -0
- package/claude_experimental/integration/telemetry.py +287 -0
- package/claude_experimental/memory/__init__.py +16 -0
- package/claude_experimental/memory/_placeholder.py +7 -0
- package/claude_experimental/memory/api.py +434 -0
- package/claude_experimental/memory/augmented_generation.py +142 -0
- package/claude_experimental/memory/episodic.py +190 -0
- package/claude_experimental/memory/failure_learning.py +183 -0
- package/claude_experimental/memory/migrate.py +169 -0
- package/claude_experimental/memory/procedural.py +212 -0
- package/claude_experimental/memory/semantic.py +376 -0
- package/claude_experimental/memory/store.py +310 -0
- package/claude_experimental/parallel/__init__.py +17 -0
- package/claude_experimental/parallel/_placeholder.py +7 -0
- package/claude_experimental/parallel/aggregation.py +131 -0
- package/claude_experimental/parallel/api.py +244 -0
- package/claude_experimental/parallel/executor.py +110 -0
- package/claude_experimental/parallel/ralph_bridge.py +164 -0
- package/claude_experimental/parallel/sandbox.py +314 -0
- package/claude_experimental/parallel/scaling.py +171 -0
- package/claude_experimental/parallel/ultraworker.py +285 -0
- package/claude_experimental/patterns/__init__.py +16 -0
- package/claude_experimental/patterns/_placeholder.py +7 -0
- package/claude_experimental/patterns/antipatterns.py +454 -0
- package/claude_experimental/patterns/api.py +196 -0
- package/claude_experimental/patterns/extractor.py +163 -0
- package/claude_experimental/patterns/mining.py +219 -0
- package/claude_experimental/patterns/refactoring.py +234 -0
- package/hooks/_common.py +3 -1
- package/package.json +1 -1
- package/runtime/subagent_dispatcher.py +109 -9
- package/settings.json +6 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
import json
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from claude_experimental.memory.store import MemoryStore
|
|
8
|
+
|
|
9
|
+
_EVENT_TYPES = {"success", "failure", "decision", "discovery"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EpisodicMemory:
|
|
13
|
+
def __init__(self, store: MemoryStore | None = None):
|
|
14
|
+
self.store: MemoryStore = store or MemoryStore()
|
|
15
|
+
|
|
16
|
+
def record(
|
|
17
|
+
self,
|
|
18
|
+
event_type: str,
|
|
19
|
+
context: object,
|
|
20
|
+
outcome: object,
|
|
21
|
+
session_id: str | None = None,
|
|
22
|
+
metadata: dict[str, object] | None = None,
|
|
23
|
+
) -> int:
|
|
24
|
+
_require_memory_enabled()
|
|
25
|
+
|
|
26
|
+
normalized_event_type = event_type.strip().lower()
|
|
27
|
+
if normalized_event_type not in _EVENT_TYPES:
|
|
28
|
+
allowed = ", ".join(sorted(_EVENT_TYPES))
|
|
29
|
+
raise ValueError(f"Invalid event_type '{event_type}'. Allowed values: {allowed}")
|
|
30
|
+
|
|
31
|
+
importance = self._importance_for(normalized_event_type, outcome, metadata)
|
|
32
|
+
payload = {
|
|
33
|
+
"event_type": normalized_event_type,
|
|
34
|
+
"session_id": session_id,
|
|
35
|
+
"context": context,
|
|
36
|
+
"outcome": outcome,
|
|
37
|
+
}
|
|
38
|
+
content = self._to_text(payload)
|
|
39
|
+
|
|
40
|
+
store_metadata: dict[str, object] = {
|
|
41
|
+
"event_type": normalized_event_type,
|
|
42
|
+
"session_id": session_id,
|
|
43
|
+
"context": context,
|
|
44
|
+
"outcome": outcome,
|
|
45
|
+
}
|
|
46
|
+
if metadata:
|
|
47
|
+
store_metadata.update(metadata)
|
|
48
|
+
|
|
49
|
+
return self.store.save(
|
|
50
|
+
content=content,
|
|
51
|
+
memory_type="episodic",
|
|
52
|
+
importance=importance,
|
|
53
|
+
metadata=store_metadata,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def recall(
|
|
57
|
+
self,
|
|
58
|
+
query: str,
|
|
59
|
+
limit: int = 5,
|
|
60
|
+
temperature: float = 0.3,
|
|
61
|
+
event_type_filter: str | None = None,
|
|
62
|
+
) -> list[dict[str, object]]:
|
|
63
|
+
_require_memory_enabled()
|
|
64
|
+
|
|
65
|
+
clamped_temperature = max(0.0, min(1.0, temperature))
|
|
66
|
+
min_score = 1.0 - clamped_temperature
|
|
67
|
+
normalized_filter = event_type_filter.strip().lower() if event_type_filter else None
|
|
68
|
+
if normalized_filter and normalized_filter not in _EVENT_TYPES:
|
|
69
|
+
allowed = ", ".join(sorted(_EVENT_TYPES))
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Invalid event_type_filter '{event_type_filter}'. Allowed values: {allowed}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
base_results = self.store.search(
|
|
75
|
+
query=query,
|
|
76
|
+
limit=max(limit, 1),
|
|
77
|
+
memory_type="episodic",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
filtered: list[dict[str, object]] = []
|
|
81
|
+
total = len(base_results)
|
|
82
|
+
for index, item in enumerate(base_results):
|
|
83
|
+
memory = dict(item)
|
|
84
|
+
metadata_raw = memory.get("metadata")
|
|
85
|
+
if isinstance(metadata_raw, str):
|
|
86
|
+
parsed_json: object
|
|
87
|
+
try:
|
|
88
|
+
parsed_json = cast(object, json.loads(metadata_raw))
|
|
89
|
+
except json.JSONDecodeError:
|
|
90
|
+
parsed_json = cast(object, {})
|
|
91
|
+
metadata = _coerce_object_dict(parsed_json)
|
|
92
|
+
else:
|
|
93
|
+
metadata = _coerce_object_dict(metadata_raw)
|
|
94
|
+
|
|
95
|
+
event_type = str(metadata.get("event_type", "")).strip().lower()
|
|
96
|
+
if normalized_filter and event_type != normalized_filter:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
relevance = self._relevance(memory, index, total)
|
|
100
|
+
memory["score"] = relevance
|
|
101
|
+
if relevance >= min_score:
|
|
102
|
+
filtered.append(memory)
|
|
103
|
+
if len(filtered) >= limit:
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
return filtered
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _importance_for(
|
|
110
|
+
event_type: str,
|
|
111
|
+
outcome: object,
|
|
112
|
+
metadata: dict[str, object] | None,
|
|
113
|
+
) -> float:
|
|
114
|
+
if event_type == "success":
|
|
115
|
+
return 0.7
|
|
116
|
+
if event_type == "failure" and EpisodicMemory._has_lessons(outcome, metadata):
|
|
117
|
+
return 0.8
|
|
118
|
+
return 0.3
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _has_lessons(outcome: object, metadata: dict[str, object] | None) -> bool:
|
|
122
|
+
if metadata:
|
|
123
|
+
if _contains_lesson_signal(metadata.get("lessons")):
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
outcome_dict = _coerce_object_dict(outcome)
|
|
127
|
+
if _contains_lesson_signal(outcome_dict.get("lessons")):
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
outcome_text = EpisodicMemory._to_text(outcome).lower()
|
|
131
|
+
return "lesson" in outcome_text or "learned" in outcome_text
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _to_text(value: object) -> str:
|
|
135
|
+
if isinstance(value, str):
|
|
136
|
+
return value
|
|
137
|
+
return json.dumps(value, sort_keys=True, default=str)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _relevance(memory: dict[str, object], index: int, total: int) -> float:
|
|
141
|
+
if total <= 1:
|
|
142
|
+
rank_relevance = 1.0
|
|
143
|
+
else:
|
|
144
|
+
rank_relevance = max(0.0, min(1.0, 1.0 - (index / (total - 1))))
|
|
145
|
+
|
|
146
|
+
score = memory.get("score")
|
|
147
|
+
if isinstance(score, (int, float)):
|
|
148
|
+
return max(0.0, min(1.0, float(score)))
|
|
149
|
+
|
|
150
|
+
return rank_relevance
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _require_memory_enabled() -> None:
|
|
154
|
+
memory_module = import_module("claude_experimental.memory")
|
|
155
|
+
require_enabled = getattr(memory_module, "_require_enabled", None)
|
|
156
|
+
if callable(require_enabled):
|
|
157
|
+
_ = require_enabled()
|
|
158
|
+
return
|
|
159
|
+
raise RuntimeError("claude_experimental.memory._require_enabled is unavailable")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _coerce_object_dict(value: object) -> dict[str, object]:
|
|
163
|
+
if not isinstance(value, dict):
|
|
164
|
+
return {}
|
|
165
|
+
value_dict = cast(dict[object, object], value)
|
|
166
|
+
return {str(key): item for key, item in value_dict.items()}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _contains_lesson_signal(value: object) -> bool:
|
|
170
|
+
if isinstance(value, str):
|
|
171
|
+
return bool(value.strip())
|
|
172
|
+
if isinstance(value, list):
|
|
173
|
+
for item in cast(list[object], value):
|
|
174
|
+
if _contains_lesson_signal(item):
|
|
175
|
+
return True
|
|
176
|
+
return False
|
|
177
|
+
if isinstance(value, tuple):
|
|
178
|
+
for item in cast(tuple[object, ...], value):
|
|
179
|
+
if _contains_lesson_signal(item):
|
|
180
|
+
return True
|
|
181
|
+
return False
|
|
182
|
+
if isinstance(value, set):
|
|
183
|
+
for item in cast(set[object], value):
|
|
184
|
+
if _contains_lesson_signal(item):
|
|
185
|
+
return True
|
|
186
|
+
return False
|
|
187
|
+
if isinstance(value, dict):
|
|
188
|
+
value_dict = cast(dict[object, object], value)
|
|
189
|
+
return _contains_lesson_signal(list(value_dict.values()))
|
|
190
|
+
return False
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""FailureLearner — records agent failures and learns patterns to warn on similar future contexts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from claude_experimental.memory.episodic import EpisodicMemory
|
|
11
|
+
from claude_experimental.memory.store import MemoryStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FailureLearner:
|
|
15
|
+
"""Records agent failures and learns patterns to warn on similar future contexts.
|
|
16
|
+
|
|
17
|
+
Uses episodic memory to store failure records and FTS5 search to find
|
|
18
|
+
similar past failures when new errors occur. Warn-only: does not
|
|
19
|
+
prevent dispatch or automatic avoidance.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, db_path: str | None = None) -> None:
|
|
23
|
+
self.store: MemoryStore = MemoryStore(db_path=db_path) if db_path else MemoryStore()
|
|
24
|
+
self.episodic: EpisodicMemory = EpisodicMemory(self.store)
|
|
25
|
+
|
|
26
|
+
# ------------------------------------------------------------------
|
|
27
|
+
# Public API
|
|
28
|
+
# ------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def record_failure(
|
|
31
|
+
self,
|
|
32
|
+
context: str,
|
|
33
|
+
error_type: str,
|
|
34
|
+
error_message: str,
|
|
35
|
+
stack_trace: str | None = None,
|
|
36
|
+
) -> int:
|
|
37
|
+
"""Record a failure for future pattern matching.
|
|
38
|
+
|
|
39
|
+
Returns the memory ID of the stored failure record.
|
|
40
|
+
"""
|
|
41
|
+
_require_memory_enabled()
|
|
42
|
+
|
|
43
|
+
sanitized_trace: str | None = (
|
|
44
|
+
self._sanitize_trace(stack_trace) if stack_trace else None
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
content = f"FAILURE: {error_type} | Context: {context} | Error: {error_message}"
|
|
48
|
+
|
|
49
|
+
metadata: dict[str, object] = {
|
|
50
|
+
"error_type": error_type,
|
|
51
|
+
"context": context,
|
|
52
|
+
"error_message": error_message,
|
|
53
|
+
"sanitized_trace": sanitized_trace,
|
|
54
|
+
"event_type": "failure",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return self.store.save(
|
|
58
|
+
content=content,
|
|
59
|
+
memory_type="episodic",
|
|
60
|
+
importance=0.9,
|
|
61
|
+
metadata=metadata,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def suggest_fix(
|
|
65
|
+
self,
|
|
66
|
+
error_type: str,
|
|
67
|
+
context: str,
|
|
68
|
+
limit: int = 3,
|
|
69
|
+
) -> list[dict[str, object]]:
|
|
70
|
+
"""Find past failures matching this error type and context.
|
|
71
|
+
|
|
72
|
+
Returns list of dicts with context, error_type, error_message,
|
|
73
|
+
memory_id, and relevance score.
|
|
74
|
+
"""
|
|
75
|
+
_require_memory_enabled()
|
|
76
|
+
|
|
77
|
+
# FTS5 uses implicit AND; use OR for broader failure matching
|
|
78
|
+
raw_terms = f"{error_type} {context}".split()
|
|
79
|
+
query = " OR ".join(raw_terms)
|
|
80
|
+
results = self.episodic.recall(
|
|
81
|
+
query=query,
|
|
82
|
+
limit=limit,
|
|
83
|
+
event_type_filter="failure",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
suggestions: list[dict[str, object]] = []
|
|
87
|
+
for result in results:
|
|
88
|
+
meta = _parse_metadata(result)
|
|
89
|
+
suggestions.append({
|
|
90
|
+
"context": str(meta.get("context", "")),
|
|
91
|
+
"error_type": str(meta.get("error_type", "")),
|
|
92
|
+
"error_message": str(meta.get("error_message", "")),
|
|
93
|
+
"memory_id": result.get("id", 0),
|
|
94
|
+
"relevance": float(cast(float, result.get("score", 0.0))),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return suggestions
|
|
98
|
+
|
|
99
|
+
def get_failure_patterns(
|
|
100
|
+
self,
|
|
101
|
+
min_occurrences: int = 2,
|
|
102
|
+
) -> list[dict[str, object]]:
|
|
103
|
+
"""Find recurring failure patterns grouped by error type.
|
|
104
|
+
|
|
105
|
+
Returns error types that have occurred at least *min_occurrences* times.
|
|
106
|
+
"""
|
|
107
|
+
_require_memory_enabled()
|
|
108
|
+
|
|
109
|
+
conn = self.store.connect()
|
|
110
|
+
try:
|
|
111
|
+
rows = conn.execute(
|
|
112
|
+
"SELECT metadata FROM memories WHERE memory_type = 'episodic'"
|
|
113
|
+
).fetchall()
|
|
114
|
+
|
|
115
|
+
counts: dict[str, int] = {}
|
|
116
|
+
for row in rows:
|
|
117
|
+
meta = _safe_parse_json(row["metadata"])
|
|
118
|
+
if meta.get("event_type") != "failure":
|
|
119
|
+
continue
|
|
120
|
+
error_type = str(meta.get("error_type", "unknown"))
|
|
121
|
+
counts[error_type] = counts.get(error_type, 0) + 1
|
|
122
|
+
|
|
123
|
+
return [
|
|
124
|
+
{"error_type": error_type, "count": count}
|
|
125
|
+
for error_type, count in sorted(counts.items())
|
|
126
|
+
if count >= min_occurrences
|
|
127
|
+
]
|
|
128
|
+
finally:
|
|
129
|
+
conn.close()
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
# Internal helpers
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _sanitize_trace(trace: str) -> str:
|
|
137
|
+
"""Remove full file paths and line numbers from stack traces."""
|
|
138
|
+
sanitized = re.sub(
|
|
139
|
+
r'File "([^"]+)"',
|
|
140
|
+
lambda m: f'File "{os.path.basename(m.group(1))}"',
|
|
141
|
+
trace,
|
|
142
|
+
)
|
|
143
|
+
return re.sub(r", line \d+", "", sanitized)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
# Module-level helpers
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _require_memory_enabled() -> None:
|
|
152
|
+
"""Gate all public methods behind the experimental memory feature flag."""
|
|
153
|
+
memory_module = import_module("claude_experimental.memory")
|
|
154
|
+
require_enabled = getattr(memory_module, "_require_enabled", None)
|
|
155
|
+
if callable(require_enabled):
|
|
156
|
+
_ = require_enabled()
|
|
157
|
+
return
|
|
158
|
+
raise RuntimeError("claude_experimental.memory._require_enabled is unavailable")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _parse_metadata(result: dict[str, object]) -> dict[str, object]:
|
|
162
|
+
"""Parse metadata from a recall result dict."""
|
|
163
|
+
return _safe_parse_json(result.get("metadata"))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _safe_parse_json(value: object) -> dict[str, object]:
|
|
167
|
+
"""Safely parse a JSON string or coerce a dict; returns empty dict on failure."""
|
|
168
|
+
if isinstance(value, str):
|
|
169
|
+
try:
|
|
170
|
+
parsed = json.loads(value)
|
|
171
|
+
if isinstance(parsed, dict):
|
|
172
|
+
parsed_dict = cast(dict[object, object], parsed)
|
|
173
|
+
return {str(k): v for k, v in parsed_dict.items()}
|
|
174
|
+
except json.JSONDecodeError:
|
|
175
|
+
pass
|
|
176
|
+
return {}
|
|
177
|
+
if isinstance(value, dict):
|
|
178
|
+
raw_dict = cast(dict[object, object], value)
|
|
179
|
+
return {str(k): v for k, v in raw_dict.items()}
|
|
180
|
+
return {}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
__all__ = ["FailureLearner"]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Legacy Markdown Memory Migration Utility.
|
|
2
|
+
|
|
3
|
+
Migrates existing .omg/state/memory/*.md files to SQLite MemoryStore.
|
|
4
|
+
Preserves original files (copy, don't move) for safe rollback.
|
|
5
|
+
Idempotent: running twice doesn't create duplicates (dedup via content hash).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import cast
|
|
16
|
+
|
|
17
|
+
from claude_experimental.memory.store import MemoryStore
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def migrate_markdown_memories(
|
|
21
|
+
project_dir: str,
|
|
22
|
+
target_store: MemoryStore | None = None,
|
|
23
|
+
) -> dict[str, int]:
|
|
24
|
+
"""Migrate markdown memories from .omg/state/memory/ to SQLite store.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
project_dir: Root project directory containing .omg/state/memory/
|
|
28
|
+
target_store: MemoryStore instance (default: project-scoped store)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dict with keys:
|
|
32
|
+
- files_found: Number of .md files discovered
|
|
33
|
+
- memories_migrated: Number successfully inserted
|
|
34
|
+
- errors: Number of files that failed to process
|
|
35
|
+
- skipped_duplicates: Number skipped due to content hash collision
|
|
36
|
+
"""
|
|
37
|
+
store = target_store or MemoryStore(scope="project")
|
|
38
|
+
memory_dir = os.path.join(project_dir, ".omg", "state", "memory")
|
|
39
|
+
|
|
40
|
+
result = {
|
|
41
|
+
"files_found": 0,
|
|
42
|
+
"memories_migrated": 0,
|
|
43
|
+
"errors": 0,
|
|
44
|
+
"skipped_duplicates": 0,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Handle missing directory gracefully
|
|
48
|
+
if not os.path.isdir(memory_dir):
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
# Collect all .md files
|
|
52
|
+
md_files = sorted(
|
|
53
|
+
[f for f in os.listdir(memory_dir) if f.endswith(".md")],
|
|
54
|
+
reverse=True, # Process newest first
|
|
55
|
+
)
|
|
56
|
+
result["files_found"] = len(md_files)
|
|
57
|
+
|
|
58
|
+
for filename in md_files:
|
|
59
|
+
filepath = os.path.join(memory_dir, filename)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# Read file content
|
|
63
|
+
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
|
64
|
+
content = f.read()
|
|
65
|
+
|
|
66
|
+
if not content.strip():
|
|
67
|
+
result["errors"] += 1
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
# Extract date from filename (format: YYYY-MM-DD-*.md)
|
|
71
|
+
date_match = re.match(r"(\d{4}-\d{2}-\d{2})", filename)
|
|
72
|
+
if date_match:
|
|
73
|
+
date_str = date_match.group(1)
|
|
74
|
+
try:
|
|
75
|
+
created_at = datetime.strptime(date_str, "%Y-%m-%d").timestamp()
|
|
76
|
+
except ValueError:
|
|
77
|
+
created_at = datetime.now().timestamp()
|
|
78
|
+
else:
|
|
79
|
+
created_at = datetime.now().timestamp()
|
|
80
|
+
|
|
81
|
+
# Compute content hash for deduplication
|
|
82
|
+
content_hash = hashlib.sha256(content.encode()).hexdigest()
|
|
83
|
+
|
|
84
|
+
# Check if memory with same content hash already exists
|
|
85
|
+
existing = _find_memory_by_content_hash(store, content_hash)
|
|
86
|
+
if existing is not None:
|
|
87
|
+
result["skipped_duplicates"] += 1
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
# Calculate importance: normalized to 500 chars
|
|
91
|
+
importance = min(len(content) / 500.0, 1.0)
|
|
92
|
+
|
|
93
|
+
# Build metadata
|
|
94
|
+
metadata: dict[str, object] = {
|
|
95
|
+
"content_hash": content_hash,
|
|
96
|
+
"source_file": filename,
|
|
97
|
+
"migrated_from": "markdown",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Insert into store
|
|
101
|
+
memory_id = store.save(
|
|
102
|
+
content=content,
|
|
103
|
+
memory_type="semantic",
|
|
104
|
+
importance=importance,
|
|
105
|
+
scope="project",
|
|
106
|
+
metadata=metadata,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if memory_id > 0:
|
|
110
|
+
result["memories_migrated"] += 1
|
|
111
|
+
else:
|
|
112
|
+
result["errors"] += 1
|
|
113
|
+
|
|
114
|
+
except (OSError, IOError) as e:
|
|
115
|
+
result["errors"] += 1
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _find_memory_by_content_hash(
|
|
122
|
+
store: MemoryStore, content_hash: str
|
|
123
|
+
) -> dict[str, object] | None:
|
|
124
|
+
"""Search for existing memory with matching content hash in metadata.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
store: MemoryStore instance
|
|
128
|
+
content_hash: SHA256 hex digest to search for
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Memory dict if found, None otherwise
|
|
132
|
+
"""
|
|
133
|
+
conn = store.connect()
|
|
134
|
+
try:
|
|
135
|
+
# Search metadata JSON for content_hash field
|
|
136
|
+
rows = cast(
|
|
137
|
+
list[tuple[object, ...]],
|
|
138
|
+
conn.execute(
|
|
139
|
+
"""
|
|
140
|
+
SELECT id, content, memory_type, importance, scope, metadata,
|
|
141
|
+
created_at, accessed_at, access_count, schema_version
|
|
142
|
+
FROM memories
|
|
143
|
+
WHERE scope = ? AND json_extract(metadata, '$.content_hash') = ?
|
|
144
|
+
LIMIT 1
|
|
145
|
+
""",
|
|
146
|
+
("project", content_hash),
|
|
147
|
+
).fetchall(),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if rows:
|
|
151
|
+
row = rows[0]
|
|
152
|
+
return {
|
|
153
|
+
"id": row[0],
|
|
154
|
+
"content": row[1],
|
|
155
|
+
"memory_type": row[2],
|
|
156
|
+
"importance": row[3],
|
|
157
|
+
"scope": row[4],
|
|
158
|
+
"metadata": json.loads(row[5]) if isinstance(row[5], str) else row[5],
|
|
159
|
+
"created_at": row[6],
|
|
160
|
+
"accessed_at": row[7],
|
|
161
|
+
"access_count": row[8],
|
|
162
|
+
"schema_version": row[9],
|
|
163
|
+
}
|
|
164
|
+
return None
|
|
165
|
+
except Exception:
|
|
166
|
+
# If query fails (e.g., json_extract not available), return None
|
|
167
|
+
return None
|
|
168
|
+
finally:
|
|
169
|
+
conn.close()
|