superlocalmemory 3.0.3 → 3.0.7
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/README.md +24 -2
- package/package.json +1 -1
- package/scripts/postinstall.js +109 -11
- package/src/superlocalmemory/core/embeddings.py +22 -4
- package/src/superlocalmemory/core/engine.py +6 -1
- package/src/superlocalmemory/encoding/entropy_gate.py +14 -17
- package/src/superlocalmemory/storage/v2_migrator.py +67 -14
package/README.md
CHANGED
|
@@ -85,13 +85,35 @@ Query ──► Strategy Classifier ──► 4 Parallel Channels:
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
+
## Prerequisites
|
|
89
|
+
|
|
90
|
+
| Requirement | Version | Why |
|
|
91
|
+
|:-----------|:--------|:----|
|
|
92
|
+
| **Node.js** | 14+ | npm package manager |
|
|
93
|
+
| **Python** | 3.11+ | V3 engine runtime |
|
|
94
|
+
| **pip** | Latest | Python dependency installer |
|
|
95
|
+
|
|
96
|
+
> All Python dependencies are installed automatically during `npm install`. You don't need to run pip manually. If any dependency fails, the installer shows clear instructions.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
88
100
|
## Quick Start
|
|
89
101
|
|
|
90
|
-
### Install via npm (recommended)
|
|
102
|
+
### Install via npm (recommended — one command, everything included)
|
|
91
103
|
|
|
92
104
|
```bash
|
|
93
105
|
npm install -g superlocalmemory
|
|
94
|
-
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This single command:
|
|
109
|
+
- Installs the V3 engine and CLI
|
|
110
|
+
- Auto-installs all Python dependencies (numpy, scipy, networkx, sentence-transformers, etc.)
|
|
111
|
+
- Creates the data directory at `~/.superlocalmemory/`
|
|
112
|
+
- Detects and guides V2 migration if applicable
|
|
113
|
+
|
|
114
|
+
Then configure:
|
|
115
|
+
```bash
|
|
116
|
+
slm setup # Choose mode, configure provider
|
|
95
117
|
```
|
|
96
118
|
|
|
97
119
|
### Install via pip
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.7",
|
|
4
4
|
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/scripts/postinstall.js
CHANGED
|
@@ -2,26 +2,26 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* SuperLocalMemory V3 - NPM Postinstall Script
|
|
4
4
|
*
|
|
5
|
+
* ONE COMMAND INSTALL. Everything the user needs.
|
|
6
|
+
* Python deps auto-installed. Embeddings auto-downloaded.
|
|
7
|
+
*
|
|
5
8
|
* Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
6
9
|
* Licensed under MIT License
|
|
7
|
-
* Repository: https://github.com/qualixar/superlocalmemory
|
|
8
10
|
*/
|
|
9
11
|
|
|
12
|
+
const { spawnSync } = require('child_process');
|
|
10
13
|
const path = require('path');
|
|
11
14
|
const os = require('os');
|
|
12
15
|
const fs = require('fs');
|
|
13
16
|
|
|
14
17
|
console.log('\n════════════════════════════════════════════════════════════');
|
|
15
|
-
console.log(' SuperLocalMemory V3
|
|
18
|
+
console.log(' SuperLocalMemory V3 — Post-Installation');
|
|
16
19
|
console.log(' by Varun Pratap Bhardwaj / Qualixar');
|
|
17
20
|
console.log(' https://github.com/qualixar/superlocalmemory');
|
|
18
21
|
console.log('════════════════════════════════════════════════════════════\n');
|
|
19
22
|
|
|
20
|
-
//
|
|
23
|
+
// --- Step 1: Create data directory ---
|
|
21
24
|
const SLM_HOME = path.join(os.homedir(), '.superlocalmemory');
|
|
22
|
-
const V2_HOME = path.join(os.homedir(), '.claude-memory');
|
|
23
|
-
|
|
24
|
-
// Ensure V3 data directory exists
|
|
25
25
|
if (!fs.existsSync(SLM_HOME)) {
|
|
26
26
|
fs.mkdirSync(SLM_HOME, { recursive: true });
|
|
27
27
|
console.log('✓ Created data directory: ' + SLM_HOME);
|
|
@@ -29,7 +29,102 @@ if (!fs.existsSync(SLM_HOME)) {
|
|
|
29
29
|
console.log('✓ Data directory exists: ' + SLM_HOME);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// --- Step 2: Find Python 3 ---
|
|
33
|
+
function findPython() {
|
|
34
|
+
const candidates = [
|
|
35
|
+
'python3', 'python',
|
|
36
|
+
'/opt/homebrew/bin/python3', '/usr/local/bin/python3', '/usr/bin/python3',
|
|
37
|
+
];
|
|
38
|
+
if (os.platform() === 'win32') candidates.push('py -3');
|
|
39
|
+
for (const cmd of candidates) {
|
|
40
|
+
try {
|
|
41
|
+
const parts = cmd.split(' ');
|
|
42
|
+
const r = spawnSync(parts[0], [...parts.slice(1), '--version'], {
|
|
43
|
+
stdio: 'pipe', timeout: 5000,
|
|
44
|
+
env: { ...process.env, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:' + (process.env.PATH || '') },
|
|
45
|
+
});
|
|
46
|
+
if (r.status === 0 && (r.stdout || '').toString().includes('3.')) return parts;
|
|
47
|
+
} catch (e) { /* next */ }
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pythonParts = findPython();
|
|
53
|
+
if (!pythonParts) {
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
56
|
+
console.log('║ ⚠ Python 3.11+ Required ║');
|
|
57
|
+
console.log('╚══════════════════════════════════════════════════════════╝');
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(' SuperLocalMemory V3 requires Python 3.11+');
|
|
60
|
+
console.log(' Install from: https://python.org/downloads/');
|
|
61
|
+
console.log(' After installing Python, run: slm setup');
|
|
62
|
+
console.log('');
|
|
63
|
+
process.exit(0); // Don't fail npm install
|
|
64
|
+
}
|
|
65
|
+
console.log('✓ Found Python: ' + pythonParts.join(' '));
|
|
66
|
+
|
|
67
|
+
// --- Step 3: Install ALL Python dependencies ---
|
|
68
|
+
console.log('\nInstalling Python dependencies (this may take 1-2 minutes)...\n');
|
|
69
|
+
|
|
70
|
+
// Detect if --user or --break-system-packages is needed
|
|
71
|
+
function pipInstall(packages, label) {
|
|
72
|
+
// Try normal install first
|
|
73
|
+
let result = spawnSync(pythonParts[0], [
|
|
74
|
+
...pythonParts.slice(1), '-m', 'pip', 'install', '--quiet', '--disable-pip-version-check',
|
|
75
|
+
...packages,
|
|
76
|
+
], { stdio: 'pipe', timeout: 300000, env: { ...process.env, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:' + (process.env.PATH || '') } });
|
|
77
|
+
|
|
78
|
+
if (result.status === 0) return true;
|
|
79
|
+
|
|
80
|
+
// If PEP 668 blocks it, try --user
|
|
81
|
+
const stderr = (result.stderr || '').toString();
|
|
82
|
+
if (stderr.includes('externally-managed') || stderr.includes('PEP 668')) {
|
|
83
|
+
result = spawnSync(pythonParts[0], [
|
|
84
|
+
...pythonParts.slice(1), '-m', 'pip', 'install', '--quiet', '--disable-pip-version-check',
|
|
85
|
+
'--user', ...packages,
|
|
86
|
+
], { stdio: 'pipe', timeout: 300000, env: { ...process.env, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:' + (process.env.PATH || '') } });
|
|
87
|
+
if (result.status === 0) return true;
|
|
88
|
+
|
|
89
|
+
// Last resort: --break-system-packages
|
|
90
|
+
result = spawnSync(pythonParts[0], [
|
|
91
|
+
...pythonParts.slice(1), '-m', 'pip', 'install', '--quiet', '--disable-pip-version-check',
|
|
92
|
+
'--break-system-packages', ...packages,
|
|
93
|
+
], { stdio: 'pipe', timeout: 300000, env: { ...process.env, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:' + (process.env.PATH || '') } });
|
|
94
|
+
return result.status === 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Core dependencies (REQUIRED — product won't work without these)
|
|
101
|
+
const coreDeps = [
|
|
102
|
+
'numpy>=1.26.0', 'scipy>=1.12.0', 'networkx>=3.0',
|
|
103
|
+
'httpx>=0.24.0', 'python-dateutil>=2.9.0',
|
|
104
|
+
'rank-bm25>=0.2.2', 'vaderSentiment>=3.3.2',
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
if (pipInstall(coreDeps, 'core')) {
|
|
108
|
+
console.log('✓ Core dependencies installed (math, search, NLP)');
|
|
109
|
+
} else {
|
|
110
|
+
console.log('⚠ Core dependency installation failed.');
|
|
111
|
+
console.log(' Run manually: pip install ' + coreDeps.join(' '));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Search dependencies (IMPORTANT — enables semantic search, 4-channel retrieval)
|
|
115
|
+
const searchDeps = ['sentence-transformers>=2.5.0', 'geoopt>=0.5.0'];
|
|
116
|
+
|
|
117
|
+
console.log('\nInstalling semantic search engine (downloads ~500MB on first use)...');
|
|
118
|
+
if (pipInstall(searchDeps, 'search')) {
|
|
119
|
+
console.log('✓ Semantic search engine installed (sentence-transformers + Fisher-Rao)');
|
|
120
|
+
} else {
|
|
121
|
+
console.log('⚠ Semantic search installation failed (BM25 keyword search still works).');
|
|
122
|
+
console.log(' For full 4-channel retrieval, run:');
|
|
123
|
+
console.log(' pip install sentence-transformers geoopt');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- Step 4: Detect V2 installation ---
|
|
127
|
+
const V2_HOME = path.join(os.homedir(), '.claude-memory');
|
|
33
128
|
if (fs.existsSync(V2_HOME) && fs.existsSync(path.join(V2_HOME, 'memory.db'))) {
|
|
34
129
|
console.log('');
|
|
35
130
|
console.log('╔══════════════════════════════════════════════════════════╗');
|
|
@@ -42,11 +137,9 @@ if (fs.existsSync(V2_HOME) && fs.existsSync(path.join(V2_HOME, 'memory.db'))) {
|
|
|
42
137
|
console.log(' To migrate V2 data to V3, run:');
|
|
43
138
|
console.log(' slm migrate');
|
|
44
139
|
console.log('');
|
|
45
|
-
console.log(' Read the migration guide:');
|
|
46
|
-
console.log(' https://github.com/qualixar/superlocalmemory/wiki/Migration-from-V2');
|
|
47
|
-
console.log('');
|
|
48
140
|
}
|
|
49
141
|
|
|
142
|
+
// --- Done ---
|
|
50
143
|
console.log('════════════════════════════════════════════════════════════');
|
|
51
144
|
console.log(' ✓ SuperLocalMemory V3 installed successfully!');
|
|
52
145
|
console.log('');
|
|
@@ -56,5 +149,10 @@ console.log(' slm status # Check system status');
|
|
|
56
149
|
console.log(' slm remember "..." # Store a memory');
|
|
57
150
|
console.log(' slm recall "..." # Search memories');
|
|
58
151
|
console.log('');
|
|
59
|
-
console.log('
|
|
152
|
+
console.log(' Prerequisites satisfied:');
|
|
153
|
+
console.log(' ✓ Python 3.11+');
|
|
154
|
+
console.log(' ✓ Core math & search libraries');
|
|
155
|
+
console.log(' ✓ Data directory (~/.superlocalmemory/)');
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(' Docs: https://github.com/qualixar/superlocalmemory/wiki');
|
|
60
158
|
console.log('════════════════════════════════════════════════════════════\n');
|
|
@@ -56,6 +56,14 @@ class EmbeddingService:
|
|
|
56
56
|
self._model: object | None = None
|
|
57
57
|
self._lock = threading.Lock()
|
|
58
58
|
self._loaded = False
|
|
59
|
+
self._available = True # Set False if model can't load
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def is_available(self) -> bool:
|
|
63
|
+
"""Check if embedding service has a usable model."""
|
|
64
|
+
if not self._loaded:
|
|
65
|
+
self._ensure_loaded()
|
|
66
|
+
return self._available and self._model is not None
|
|
59
67
|
|
|
60
68
|
# ------------------------------------------------------------------
|
|
61
69
|
# Public API
|
|
@@ -78,6 +86,9 @@ class EmbeddingService:
|
|
|
78
86
|
"""
|
|
79
87
|
if not text or not text.strip():
|
|
80
88
|
raise ValueError("Cannot embed empty text")
|
|
89
|
+
self._ensure_loaded()
|
|
90
|
+
if self._model is None:
|
|
91
|
+
return None
|
|
81
92
|
vec = self._encode_single(text)
|
|
82
93
|
self._validate_dimension(vec)
|
|
83
94
|
return vec.tolist()
|
|
@@ -98,6 +109,9 @@ class EmbeddingService:
|
|
|
98
109
|
if not t or not t.strip():
|
|
99
110
|
raise ValueError(f"Text at index {i} is empty")
|
|
100
111
|
|
|
112
|
+
self._ensure_loaded()
|
|
113
|
+
if self._model is None:
|
|
114
|
+
return [None] * len(texts)
|
|
101
115
|
vectors = self._encode_batch(texts)
|
|
102
116
|
for vec in vectors:
|
|
103
117
|
self._validate_dimension(vec)
|
|
@@ -175,12 +189,16 @@ class EmbeddingService:
|
|
|
175
189
|
try:
|
|
176
190
|
from sentence_transformers import SentenceTransformer
|
|
177
191
|
except ImportError:
|
|
178
|
-
|
|
179
|
-
"sentence-transformers
|
|
180
|
-
"pip install sentence-transformers"
|
|
192
|
+
logger.warning(
|
|
193
|
+
"sentence-transformers not installed. Embeddings disabled. "
|
|
194
|
+
"Install with: pip install sentence-transformers"
|
|
181
195
|
)
|
|
196
|
+
self._model = None
|
|
197
|
+
self._loaded = True
|
|
198
|
+
self._available = False
|
|
199
|
+
return
|
|
182
200
|
model = SentenceTransformer(
|
|
183
|
-
self._config.model_name, trust_remote_code=
|
|
201
|
+
self._config.model_name, trust_remote_code=True,
|
|
184
202
|
)
|
|
185
203
|
actual_dim = model.get_sentence_embedding_dimension()
|
|
186
204
|
if actual_dim != self._config.dimension:
|
|
@@ -81,7 +81,12 @@ class MemoryEngine:
|
|
|
81
81
|
|
|
82
82
|
self._db = DatabaseManager(self._config.db_path)
|
|
83
83
|
self._db.initialize(schema)
|
|
84
|
-
|
|
84
|
+
try:
|
|
85
|
+
emb = EmbeddingService(self._config.embedding)
|
|
86
|
+
self._embedder = emb if emb.is_available else None
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
logger.warning("Embeddings unavailable (%s). BM25-only mode.", exc)
|
|
89
|
+
self._embedder = None
|
|
85
90
|
|
|
86
91
|
if self._caps.llm_fact_extraction:
|
|
87
92
|
self._llm = LLMBackbone(self._config.llm)
|
|
@@ -68,24 +68,21 @@ class EntropyGate:
|
|
|
68
68
|
logger.debug("Entropy gate: blocked (low-info pattern: '%s')", normalized)
|
|
69
69
|
return False
|
|
70
70
|
|
|
71
|
-
# Stage 2: Similarity-based deduplication
|
|
72
|
-
if self._embedder is not None
|
|
71
|
+
# Stage 2: Similarity-based deduplication (requires embeddings)
|
|
72
|
+
if self._embedder is not None:
|
|
73
73
|
emb = self._embedder.embed(content)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
self._recent_embeddings.
|
|
85
|
-
|
|
86
|
-
# First content — add to window, always pass
|
|
87
|
-
emb = self._embedder.embed(content)
|
|
88
|
-
self._recent_embeddings.append(emb)
|
|
74
|
+
if emb is not None:
|
|
75
|
+
if self._recent_embeddings:
|
|
76
|
+
for recent in self._recent_embeddings:
|
|
77
|
+
sim = _cosine(emb, recent)
|
|
78
|
+
if sim > self._threshold:
|
|
79
|
+
logger.debug(
|
|
80
|
+
"Entropy gate: blocked (near-duplicate, sim=%.3f)", sim
|
|
81
|
+
)
|
|
82
|
+
return False
|
|
83
|
+
self._recent_embeddings.append(emb)
|
|
84
|
+
if len(self._recent_embeddings) > self._window_size:
|
|
85
|
+
self._recent_embeddings.pop(0)
|
|
89
86
|
|
|
90
87
|
return True
|
|
91
88
|
|
|
@@ -260,24 +260,77 @@ class V2Migrator:
|
|
|
260
260
|
# Backfill V3 columns from V2 data
|
|
261
261
|
conn.execute('UPDATE memories SET profile_id = COALESCE(profile, "default") WHERE profile_id IS NULL')
|
|
262
262
|
conn.execute("UPDATE memories SET memory_id = 'v2_' || CAST(id AS TEXT) WHERE memory_id IS NULL")
|
|
263
|
+
# Create unique index on memory_id so V3 FKs work
|
|
264
|
+
try:
|
|
265
|
+
conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_memory_id ON memories (memory_id)")
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
# Disable FK enforcement for migrated DBs (V2 schema is incompatible)
|
|
269
|
+
conn.execute("PRAGMA foreign_keys=OFF")
|
|
270
|
+
|
|
271
|
+
# Rename ALL tables with incompatible schemas (V2 + old alpha)
|
|
272
|
+
# User data is in 'memories' table (already upgraded above)
|
|
273
|
+
# Everything else is computed/derived and will be recreated by V3
|
|
274
|
+
all_existing = {r[0] for r in conn.execute(
|
|
275
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_v2_bak_%'"
|
|
276
|
+
).fetchall()}
|
|
277
|
+
# Keep only: memories (upgraded), profiles, schema_version, v3_config, sqlite_sequence
|
|
278
|
+
keep_tables = {"memories", "profiles", "schema_version", "v3_config", "sqlite_sequence"}
|
|
279
|
+
v2_conflicting = [t for t in all_existing if t not in keep_tables and not t.startswith("_")]
|
|
280
|
+
for table in v2_conflicting:
|
|
281
|
+
try:
|
|
282
|
+
conn.execute(f'ALTER TABLE "{table}" RENAME TO "_v2_bak_{table}"')
|
|
283
|
+
except Exception:
|
|
284
|
+
pass # Table may not exist
|
|
285
|
+
|
|
263
286
|
conn.commit()
|
|
264
287
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
conn.execute(sql)
|
|
269
|
-
# Mark migration
|
|
270
|
-
conn.execute(
|
|
271
|
-
"INSERT OR REPLACE INTO v3_config (key, value, updated_at) VALUES (?, ?, ?)",
|
|
272
|
-
("migration_date", datetime.now(UTC).isoformat(), datetime.now(UTC).isoformat()),
|
|
273
|
-
)
|
|
274
|
-
conn.execute(
|
|
275
|
-
"INSERT OR REPLACE INTO v3_config (key, value, updated_at) VALUES (?, ?, ?)",
|
|
276
|
-
("migration_version", "3.0.0", datetime.now(UTC).isoformat()),
|
|
277
|
-
)
|
|
288
|
+
# Use the FULL V3 schema (not the partial V3_TABLES_SQL)
|
|
289
|
+
from superlocalmemory.storage import schema
|
|
290
|
+
schema.create_all_tables(conn)
|
|
278
291
|
conn.commit()
|
|
292
|
+
# Mark migration in config table
|
|
293
|
+
try:
|
|
294
|
+
conn.execute(
|
|
295
|
+
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
|
|
296
|
+
("migration_date", datetime.now(UTC).isoformat(), datetime.now(UTC).isoformat()),
|
|
297
|
+
)
|
|
298
|
+
conn.execute(
|
|
299
|
+
"INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
|
|
300
|
+
("migration_version", "3.0.0", datetime.now(UTC).isoformat()),
|
|
301
|
+
)
|
|
302
|
+
conn.commit()
|
|
303
|
+
except Exception:
|
|
304
|
+
pass # Schema handles this on engine init
|
|
305
|
+
# Step 4b: Convert V2 memories → V3 atomic_facts
|
|
306
|
+
try:
|
|
307
|
+
now = datetime.now(UTC).isoformat()
|
|
308
|
+
rows = conn.execute("SELECT memory_id, profile_id, content, created_at FROM memories").fetchall()
|
|
309
|
+
converted = 0
|
|
310
|
+
for row in rows:
|
|
311
|
+
mid, pid, content, created = row[0], row[1], row[2], row[3]
|
|
312
|
+
if not content or not content.strip():
|
|
313
|
+
continue
|
|
314
|
+
fid = f"v2_fact_{mid}"
|
|
315
|
+
conn.execute(
|
|
316
|
+
"INSERT OR IGNORE INTO atomic_facts "
|
|
317
|
+
"(fact_id, memory_id, profile_id, content, fact_type, "
|
|
318
|
+
" entities_json, canonical_entities_json, confidence, importance, "
|
|
319
|
+
" evidence_count, access_count, source_turn_ids_json, session_id, "
|
|
320
|
+
" lifecycle, emotional_valence, emotional_arousal, signal_type, created_at) "
|
|
321
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
322
|
+
(fid, mid, pid or "default", content, "factual",
|
|
323
|
+
"[]", "[]", 0.8, 0.5, 1, 0, "[]", "",
|
|
324
|
+
"active", 0.0, 0.0, "factual", created or now),
|
|
325
|
+
)
|
|
326
|
+
converted += 1
|
|
327
|
+
conn.commit()
|
|
328
|
+
stats["steps"].append(f"Converted {converted} V2 memories to V3 facts")
|
|
329
|
+
except Exception as exc:
|
|
330
|
+
stats["steps"].append(f"V2 conversion partial: {exc}")
|
|
331
|
+
|
|
279
332
|
conn.close()
|
|
280
|
-
stats["steps"].append(
|
|
333
|
+
stats["steps"].append("Created V3 schema")
|
|
281
334
|
|
|
282
335
|
# Step 5: Symlink (only if .claude-memory is not already a symlink)
|
|
283
336
|
if not self._v2_base.is_symlink():
|