get-claudia 1.34.2 → 1.35.1
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/memory-daemon/claudia_memory/__main__.py +401 -3
- package/memory-daemon/claudia_memory/config.py +42 -0
- package/memory-daemon/claudia_memory/daemon/health.py +19 -0
- package/memory-daemon/claudia_memory/database.py +97 -26
- package/memory-daemon/claudia_memory/embeddings.py +114 -1
- package/memory-daemon/claudia_memory/mcp/server.py +122 -0
- package/memory-daemon/claudia_memory/schema.sql +5 -28
- package/memory-daemon/claudia_memory/services/consolidate.py +146 -24
- package/memory-daemon/claudia_memory/services/recall.py +6 -0
- package/memory-daemon/scripts/install.sh +73 -8
- package/memory-daemon/tests/test_backup.py +72 -0
- package/memory-daemon/tests/test_embedding_cache.py +210 -0
- package/memory-daemon/tests/test_embedding_migration.py +415 -0
- package/memory-daemon/tests/test_invalidated_filter.py +180 -0
- package/memory-daemon/tests/test_retention.py +312 -0
- package/package.json +1 -1
- package/template-v2/.claude/hooks/hooks.json +20 -0
- package/template-v2/.claude/hooks/session-health-check.py +43 -1
- package/template-v2/.claude/hooks/session-health-check.sh +40 -3
- package/template-v2/.claude/skills/README.md +50 -0
- package/template-v2/.claude/skills/capability-suggester.md +18 -0
- package/template-v2/.claude/skills/commitment-detector.md +18 -0
- package/template-v2/.claude/skills/ingest-sources/SKILL.md +1 -0
- package/template-v2/.claude/skills/morning-brief/SKILL.md +1 -0
- package/template-v2/.claude/skills/pattern-recognizer.md +18 -0
- package/template-v2/.claude/skills/relationship-tracker.md +18 -0
- package/template-v2/.claude/skills/risk-surfacer.md +18 -0
- package/template-v2/.claude/skills/skill-index.json +350 -0
- package/template-v2/.claude/skills/structure-evolution.md +18 -0
|
@@ -13,6 +13,7 @@ import hashlib
|
|
|
13
13
|
import logging
|
|
14
14
|
import os
|
|
15
15
|
import signal
|
|
16
|
+
import sqlite3
|
|
16
17
|
import sys
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
|
|
@@ -182,6 +183,16 @@ def main():
|
|
|
182
183
|
action="store_true",
|
|
183
184
|
help="Generate embeddings for all memories that don't have them yet, then exit",
|
|
184
185
|
)
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--migrate-embeddings",
|
|
188
|
+
action="store_true",
|
|
189
|
+
help="Migrate embeddings to a new model/dimensions (drop and recreate vec0 tables, re-embed all data)",
|
|
190
|
+
)
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
"--backup",
|
|
193
|
+
action="store_true",
|
|
194
|
+
help="Create a database backup and exit",
|
|
195
|
+
)
|
|
185
196
|
|
|
186
197
|
args = parser.parse_args()
|
|
187
198
|
|
|
@@ -232,6 +243,21 @@ def main():
|
|
|
232
243
|
|
|
233
244
|
db = get_db()
|
|
234
245
|
db.initialize()
|
|
246
|
+
config = get_config()
|
|
247
|
+
|
|
248
|
+
# Fail fast if dimensions mismatch (user needs --migrate-embeddings instead)
|
|
249
|
+
stored_dims = db.execute(
|
|
250
|
+
"SELECT value FROM _meta WHERE key = 'embedding_dimensions'",
|
|
251
|
+
fetch=True,
|
|
252
|
+
)
|
|
253
|
+
if stored_dims and int(stored_dims[0]["value"]) != config.embedding_dimensions:
|
|
254
|
+
print(
|
|
255
|
+
f"Error: Dimension mismatch detected. "
|
|
256
|
+
f"Database has {stored_dims[0]['value']}D embeddings, "
|
|
257
|
+
f"config specifies {config.embedding_dimensions}D. "
|
|
258
|
+
f"Run --migrate-embeddings first."
|
|
259
|
+
)
|
|
260
|
+
sys.exit(1)
|
|
235
261
|
|
|
236
262
|
# Find memories not in the memory_embeddings table
|
|
237
263
|
missing = db.execute(
|
|
@@ -256,11 +282,10 @@ def main():
|
|
|
256
282
|
for i, row in enumerate(missing, 1):
|
|
257
283
|
embedding = svc.embed_sync(row["content"])
|
|
258
284
|
if embedding:
|
|
259
|
-
import
|
|
260
|
-
blob = struct.pack(f"{len(embedding)}f", *embedding)
|
|
285
|
+
import json as _json
|
|
261
286
|
db.execute(
|
|
262
287
|
"INSERT OR REPLACE INTO memory_embeddings (memory_id, embedding) VALUES (?, ?)",
|
|
263
|
-
(row["id"],
|
|
288
|
+
(row["id"], _json.dumps(embedding)),
|
|
264
289
|
)
|
|
265
290
|
success += 1
|
|
266
291
|
else:
|
|
@@ -268,9 +293,382 @@ def main():
|
|
|
268
293
|
if i % 10 == 0 or i == len(missing):
|
|
269
294
|
print(f" Progress: {i}/{len(missing)} (success={success}, failed={failed})")
|
|
270
295
|
|
|
296
|
+
# Update stored embedding model to match current config (clears mismatch warning)
|
|
297
|
+
db.execute(
|
|
298
|
+
"INSERT OR REPLACE INTO _meta (key, value) VALUES ('embedding_model', ?)",
|
|
299
|
+
(svc.model,),
|
|
300
|
+
)
|
|
301
|
+
|
|
271
302
|
print(f"Backfill complete: {success} embedded, {failed} failed, {len(missing)} total.")
|
|
272
303
|
return
|
|
273
304
|
|
|
305
|
+
if args.migrate_embeddings:
|
|
306
|
+
# Full embedding migration: change model and/or dimensions
|
|
307
|
+
setup_logging(debug=args.debug)
|
|
308
|
+
import json as _json
|
|
309
|
+
|
|
310
|
+
from .database import Database
|
|
311
|
+
from .embeddings import get_embedding_service
|
|
312
|
+
|
|
313
|
+
db = get_db()
|
|
314
|
+
db.initialize()
|
|
315
|
+
config = get_config()
|
|
316
|
+
svc = get_embedding_service()
|
|
317
|
+
|
|
318
|
+
new_model = config.embedding_model
|
|
319
|
+
new_dim = config.embedding_dimensions
|
|
320
|
+
|
|
321
|
+
# Read current state from _meta
|
|
322
|
+
old_model_row = db.execute(
|
|
323
|
+
"SELECT value FROM _meta WHERE key = 'embedding_model'",
|
|
324
|
+
fetch=True,
|
|
325
|
+
)
|
|
326
|
+
old_dims_row = db.execute(
|
|
327
|
+
"SELECT value FROM _meta WHERE key = 'embedding_dimensions'",
|
|
328
|
+
fetch=True,
|
|
329
|
+
)
|
|
330
|
+
old_model = old_model_row[0]["value"] if old_model_row else "unknown"
|
|
331
|
+
old_dim = int(old_dims_row[0]["value"]) if old_dims_row else 384
|
|
332
|
+
|
|
333
|
+
if old_model == new_model and old_dim == new_dim:
|
|
334
|
+
# No mismatch -- offer interactive model selection
|
|
335
|
+
print(f"\nCurrent embedding model: {old_model} ({old_dim}D)")
|
|
336
|
+
print()
|
|
337
|
+
print("Available models:")
|
|
338
|
+
models_info = [
|
|
339
|
+
("1", "all-minilm:l6-v2", 384, " 23MB", "Fast, good baseline"),
|
|
340
|
+
("2", "nomic-embed-text", 768, " 274MB", "Better retrieval (+6%)"),
|
|
341
|
+
("3", "mxbai-embed-large", 1024, " 669MB", "Best accuracy, larger"),
|
|
342
|
+
]
|
|
343
|
+
for num, name, dim, size, desc in models_info:
|
|
344
|
+
current = " (current)" if name == old_model else ""
|
|
345
|
+
print(f" {num}) {name:<20s} {dim}D {size} {desc}{current}")
|
|
346
|
+
print(" 4) Cancel")
|
|
347
|
+
print()
|
|
348
|
+
choice = input("Switch to [1-4, default=4]: ").strip()
|
|
349
|
+
|
|
350
|
+
model_map = {
|
|
351
|
+
"1": ("all-minilm:l6-v2", 384),
|
|
352
|
+
"2": ("nomic-embed-text", 768),
|
|
353
|
+
"3": ("mxbai-embed-large", 1024),
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if choice not in model_map:
|
|
357
|
+
print("No changes made.")
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
new_model, new_dim = model_map[choice]
|
|
361
|
+
|
|
362
|
+
if new_model == old_model and new_dim == old_dim:
|
|
363
|
+
print(f"Already using {new_model}. No changes needed.")
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
# Update config.json with the user's choice
|
|
367
|
+
config_path = Path.home() / ".claudia" / "config.json"
|
|
368
|
+
try:
|
|
369
|
+
if config_path.exists():
|
|
370
|
+
with open(config_path) as f:
|
|
371
|
+
cfg_data = _json.load(f)
|
|
372
|
+
else:
|
|
373
|
+
cfg_data = {}
|
|
374
|
+
cfg_data["embedding_model"] = new_model
|
|
375
|
+
cfg_data["embedding_dimensions"] = new_dim
|
|
376
|
+
with open(config_path, "w") as f:
|
|
377
|
+
_json.dump(cfg_data, f, indent=2)
|
|
378
|
+
print(f"\nConfig updated: {new_model} ({new_dim}D)")
|
|
379
|
+
except Exception as e:
|
|
380
|
+
print(f"Warning: Could not update config.json: {e}")
|
|
381
|
+
|
|
382
|
+
# Reinitialize embedding service with new model
|
|
383
|
+
svc.model = new_model
|
|
384
|
+
svc.dimensions = new_dim
|
|
385
|
+
svc._available = None # Force re-check
|
|
386
|
+
|
|
387
|
+
# Pre-flight: verify Ollama is running and model is available
|
|
388
|
+
if not svc.is_available_sync():
|
|
389
|
+
# Distinguish: Ollama not running vs model not pulled
|
|
390
|
+
import subprocess
|
|
391
|
+
import httpx
|
|
392
|
+
|
|
393
|
+
ollama_running = False
|
|
394
|
+
try:
|
|
395
|
+
resp = httpx.get(f"{svc.host}/api/tags", timeout=5)
|
|
396
|
+
ollama_running = resp.status_code == 200
|
|
397
|
+
except Exception:
|
|
398
|
+
pass
|
|
399
|
+
|
|
400
|
+
if not ollama_running:
|
|
401
|
+
print(f"Error: Ollama is not running.")
|
|
402
|
+
print(f"Please start Ollama and try again.")
|
|
403
|
+
sys.exit(1)
|
|
404
|
+
|
|
405
|
+
# Ollama is running but model is missing -- offer to pull it
|
|
406
|
+
print(f"\nThe model '{new_model}' is not installed in Ollama.")
|
|
407
|
+
pull_choice = input(f"Download it now? (Y/n): ").strip().lower()
|
|
408
|
+
if pull_choice in ("", "y", "yes"):
|
|
409
|
+
print(f"Downloading {new_model}... (this may take a minute)")
|
|
410
|
+
try:
|
|
411
|
+
result = subprocess.run(
|
|
412
|
+
["ollama", "pull", new_model],
|
|
413
|
+
capture_output=False,
|
|
414
|
+
text=True,
|
|
415
|
+
)
|
|
416
|
+
if result.returncode != 0:
|
|
417
|
+
print(f"Error: Failed to pull {new_model}.")
|
|
418
|
+
sys.exit(1)
|
|
419
|
+
except FileNotFoundError:
|
|
420
|
+
print("Error: 'ollama' command not found. Please install Ollama.")
|
|
421
|
+
sys.exit(1)
|
|
422
|
+
|
|
423
|
+
# Re-check availability after pull
|
|
424
|
+
svc._available = None
|
|
425
|
+
if not svc.is_available_sync():
|
|
426
|
+
print(f"Error: Model still not available after pull.")
|
|
427
|
+
sys.exit(1)
|
|
428
|
+
print(f"Model '{new_model}' ready.")
|
|
429
|
+
else:
|
|
430
|
+
print("Migration cancelled.")
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
# Count embeddings across all tables
|
|
434
|
+
embedding_counts = {}
|
|
435
|
+
for table, pk in Database.VEC0_TABLES:
|
|
436
|
+
try:
|
|
437
|
+
rows = db.execute(f"SELECT COUNT(*) as cnt FROM {table}", fetch=True)
|
|
438
|
+
embedding_counts[table] = rows[0]["cnt"] if rows else 0
|
|
439
|
+
except Exception:
|
|
440
|
+
embedding_counts[table] = 0
|
|
441
|
+
total_embeddings = sum(embedding_counts.values())
|
|
442
|
+
|
|
443
|
+
# Show migration summary
|
|
444
|
+
print(f"\nEmbedding Migration")
|
|
445
|
+
print(f" Current: {old_model} ({old_dim}D)")
|
|
446
|
+
print(f" Target: {new_model} ({new_dim}D)")
|
|
447
|
+
print(f" Embeddings to regenerate: {total_embeddings}")
|
|
448
|
+
print()
|
|
449
|
+
|
|
450
|
+
# Count source data to re-embed
|
|
451
|
+
mem_count_rows = db.execute(
|
|
452
|
+
"SELECT COUNT(*) as cnt FROM memories WHERE deleted_at IS NULL",
|
|
453
|
+
fetch=True,
|
|
454
|
+
)
|
|
455
|
+
ent_count_rows = db.execute(
|
|
456
|
+
"SELECT COUNT(*) as cnt FROM entities WHERE deleted_at IS NULL",
|
|
457
|
+
fetch=True,
|
|
458
|
+
)
|
|
459
|
+
ep_count_rows = db.execute(
|
|
460
|
+
"SELECT COUNT(*) as cnt FROM episodes WHERE summary IS NOT NULL AND summary != ''",
|
|
461
|
+
fetch=True,
|
|
462
|
+
)
|
|
463
|
+
msg_count_rows = db.execute(
|
|
464
|
+
"SELECT COUNT(*) as cnt FROM messages",
|
|
465
|
+
fetch=True,
|
|
466
|
+
)
|
|
467
|
+
ref_count_rows = db.execute(
|
|
468
|
+
"SELECT COUNT(*) as cnt FROM reflections",
|
|
469
|
+
fetch=True,
|
|
470
|
+
)
|
|
471
|
+
mem_count = mem_count_rows[0]["cnt"] if mem_count_rows else 0
|
|
472
|
+
ent_count = ent_count_rows[0]["cnt"] if ent_count_rows else 0
|
|
473
|
+
ep_count = ep_count_rows[0]["cnt"] if ep_count_rows else 0
|
|
474
|
+
msg_count = msg_count_rows[0]["cnt"] if msg_count_rows else 0
|
|
475
|
+
ref_count = ref_count_rows[0]["cnt"] if ref_count_rows else 0
|
|
476
|
+
total_to_embed = mem_count + ent_count + ep_count + msg_count + ref_count
|
|
477
|
+
|
|
478
|
+
print(f" Source data to re-embed:")
|
|
479
|
+
print(f" Memories: {mem_count}")
|
|
480
|
+
print(f" Entities: {ent_count}")
|
|
481
|
+
print(f" Episodes: {ep_count}")
|
|
482
|
+
print(f" Messages: {msg_count}")
|
|
483
|
+
print(f" Reflections: {ref_count}")
|
|
484
|
+
print(f" Total: {total_to_embed}")
|
|
485
|
+
print()
|
|
486
|
+
|
|
487
|
+
# Pre-flight: verify sqlite-vec is available
|
|
488
|
+
try:
|
|
489
|
+
db.execute("SELECT vec_version()", fetch=True)
|
|
490
|
+
except Exception:
|
|
491
|
+
print("Error: sqlite-vec extension not available. Cannot migrate embeddings.")
|
|
492
|
+
print("Install with: pip install sqlite-vec")
|
|
493
|
+
sys.exit(1)
|
|
494
|
+
|
|
495
|
+
# Confirmation
|
|
496
|
+
confirm = input("Proceed with migration? (y/N): ").strip().lower()
|
|
497
|
+
if confirm != "y":
|
|
498
|
+
print("Migration cancelled.")
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
# Step 1: Backup
|
|
502
|
+
print("\nStep 1/4: Creating backup...")
|
|
503
|
+
backup_path = db.backup()
|
|
504
|
+
print(f" Backup at: {backup_path}")
|
|
505
|
+
|
|
506
|
+
# Step 2: Drop and recreate vec0 tables with new dimensions
|
|
507
|
+
print("\nStep 2/4: Recreating vector tables...")
|
|
508
|
+
with db.transaction() as conn:
|
|
509
|
+
for table, pk in Database.VEC0_TABLES:
|
|
510
|
+
try:
|
|
511
|
+
conn.execute(f"DROP TABLE IF EXISTS {table}")
|
|
512
|
+
conn.execute(f"""
|
|
513
|
+
CREATE VIRTUAL TABLE {table} USING vec0(
|
|
514
|
+
{pk} INTEGER PRIMARY KEY,
|
|
515
|
+
embedding FLOAT[{new_dim}]
|
|
516
|
+
)
|
|
517
|
+
""")
|
|
518
|
+
print(f" Recreated {table} ({new_dim}D)")
|
|
519
|
+
except sqlite3.OperationalError as e:
|
|
520
|
+
if "no such module: vec0" in str(e):
|
|
521
|
+
print(f" Warning: sqlite-vec not available, skipping {table}")
|
|
522
|
+
else:
|
|
523
|
+
print(f" Error recreating {table}: {e}")
|
|
524
|
+
print("Aborting. Restore from backup to recover.")
|
|
525
|
+
sys.exit(1)
|
|
526
|
+
|
|
527
|
+
# Step 3: Re-embed everything
|
|
528
|
+
print("\nStep 3/4: Re-embedding all data...")
|
|
529
|
+
results = {}
|
|
530
|
+
|
|
531
|
+
# 3a. Memory embeddings (largest, most important)
|
|
532
|
+
if mem_count > 0:
|
|
533
|
+
memories = db.execute(
|
|
534
|
+
"SELECT id, content FROM memories WHERE deleted_at IS NULL",
|
|
535
|
+
fetch=True,
|
|
536
|
+
)
|
|
537
|
+
success = 0
|
|
538
|
+
for i, row in enumerate(memories or [], 1):
|
|
539
|
+
embedding = svc.embed_sync(row["content"])
|
|
540
|
+
if embedding:
|
|
541
|
+
db.execute(
|
|
542
|
+
"INSERT INTO memory_embeddings (memory_id, embedding) VALUES (?, ?)",
|
|
543
|
+
(row["id"], _json.dumps(embedding)),
|
|
544
|
+
)
|
|
545
|
+
success += 1
|
|
546
|
+
if i % 25 == 0 or i == mem_count:
|
|
547
|
+
print(f" Memories: {i}/{mem_count}")
|
|
548
|
+
results["memories"] = success
|
|
549
|
+
else:
|
|
550
|
+
results["memories"] = 0
|
|
551
|
+
|
|
552
|
+
# 3b. Entity embeddings
|
|
553
|
+
if ent_count > 0:
|
|
554
|
+
entities = db.execute(
|
|
555
|
+
"SELECT id, name, description FROM entities WHERE deleted_at IS NULL",
|
|
556
|
+
fetch=True,
|
|
557
|
+
)
|
|
558
|
+
success = 0
|
|
559
|
+
for i, row in enumerate(entities or [], 1):
|
|
560
|
+
text = f"{row['name']}: {row['description'] or ''}"
|
|
561
|
+
embedding = svc.embed_sync(text)
|
|
562
|
+
if embedding:
|
|
563
|
+
db.execute(
|
|
564
|
+
"INSERT INTO entity_embeddings (entity_id, embedding) VALUES (?, ?)",
|
|
565
|
+
(row["id"], _json.dumps(embedding)),
|
|
566
|
+
)
|
|
567
|
+
success += 1
|
|
568
|
+
if i % 25 == 0 or i == ent_count:
|
|
569
|
+
print(f" Entities: {i}/{ent_count}")
|
|
570
|
+
results["entities"] = success
|
|
571
|
+
else:
|
|
572
|
+
results["entities"] = 0
|
|
573
|
+
|
|
574
|
+
# 3c. Episode embeddings (from summaries)
|
|
575
|
+
if ep_count > 0:
|
|
576
|
+
episodes = db.execute(
|
|
577
|
+
"SELECT id, summary FROM episodes WHERE summary IS NOT NULL AND summary != ''",
|
|
578
|
+
fetch=True,
|
|
579
|
+
)
|
|
580
|
+
success = 0
|
|
581
|
+
for i, row in enumerate(episodes or [], 1):
|
|
582
|
+
embedding = svc.embed_sync(row["summary"])
|
|
583
|
+
if embedding:
|
|
584
|
+
db.execute(
|
|
585
|
+
"INSERT INTO episode_embeddings (episode_id, embedding) VALUES (?, ?)",
|
|
586
|
+
(row["id"], _json.dumps(embedding)),
|
|
587
|
+
)
|
|
588
|
+
success += 1
|
|
589
|
+
if i % 25 == 0 or i == ep_count:
|
|
590
|
+
print(f" Episodes: {i}/{ep_count}")
|
|
591
|
+
results["episodes"] = success
|
|
592
|
+
else:
|
|
593
|
+
results["episodes"] = 0
|
|
594
|
+
|
|
595
|
+
# 3d. Message embeddings
|
|
596
|
+
if msg_count > 0:
|
|
597
|
+
messages = db.execute(
|
|
598
|
+
"SELECT id, content FROM messages",
|
|
599
|
+
fetch=True,
|
|
600
|
+
)
|
|
601
|
+
success = 0
|
|
602
|
+
for i, row in enumerate(messages or [], 1):
|
|
603
|
+
embedding = svc.embed_sync(row["content"])
|
|
604
|
+
if embedding:
|
|
605
|
+
db.execute(
|
|
606
|
+
"INSERT INTO message_embeddings (message_id, embedding) VALUES (?, ?)",
|
|
607
|
+
(row["id"], _json.dumps(embedding)),
|
|
608
|
+
)
|
|
609
|
+
success += 1
|
|
610
|
+
if i % 25 == 0 or i == msg_count:
|
|
611
|
+
print(f" Messages: {i}/{msg_count}")
|
|
612
|
+
results["messages"] = success
|
|
613
|
+
else:
|
|
614
|
+
results["messages"] = 0
|
|
615
|
+
|
|
616
|
+
# 3e. Reflection embeddings
|
|
617
|
+
if ref_count > 0:
|
|
618
|
+
reflections = db.execute(
|
|
619
|
+
"SELECT id, content FROM reflections",
|
|
620
|
+
fetch=True,
|
|
621
|
+
)
|
|
622
|
+
success = 0
|
|
623
|
+
for i, row in enumerate(reflections or [], 1):
|
|
624
|
+
embedding = svc.embed_sync(row["content"])
|
|
625
|
+
if embedding:
|
|
626
|
+
db.execute(
|
|
627
|
+
"INSERT INTO reflection_embeddings (reflection_id, embedding) VALUES (?, ?)",
|
|
628
|
+
(row["id"], _json.dumps(embedding)),
|
|
629
|
+
)
|
|
630
|
+
success += 1
|
|
631
|
+
if i % 25 == 0 or i == ref_count:
|
|
632
|
+
print(f" Reflections: {i}/{ref_count}")
|
|
633
|
+
results["reflections"] = success
|
|
634
|
+
else:
|
|
635
|
+
results["reflections"] = 0
|
|
636
|
+
|
|
637
|
+
# Step 4: Update _meta
|
|
638
|
+
print("\nStep 4/4: Updating metadata...")
|
|
639
|
+
db.execute(
|
|
640
|
+
"INSERT OR REPLACE INTO _meta (key, value) VALUES ('embedding_model', ?)",
|
|
641
|
+
(new_model,),
|
|
642
|
+
)
|
|
643
|
+
db.execute(
|
|
644
|
+
"INSERT OR REPLACE INTO _meta (key, value) VALUES ('embedding_dimensions', ?)",
|
|
645
|
+
(str(new_dim),),
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
# Clear embedding cache (old-dimension entries)
|
|
649
|
+
svc._cache.clear()
|
|
650
|
+
svc._model_mismatch = False
|
|
651
|
+
|
|
652
|
+
# Summary
|
|
653
|
+
print(f"\nMigration complete:")
|
|
654
|
+
print(f" Model: {new_model} ({new_dim}D)")
|
|
655
|
+
print(f" Memories re-embedded: {results['memories']}/{mem_count}")
|
|
656
|
+
print(f" Entities re-embedded: {results['entities']}/{ent_count}")
|
|
657
|
+
print(f" Episodes re-embedded: {results['episodes']}/{ep_count}")
|
|
658
|
+
print(f" Messages re-embedded: {results['messages']}/{msg_count}")
|
|
659
|
+
print(f" Reflections re-embedded: {results['reflections']}/{ref_count}")
|
|
660
|
+
print(f" Backup at: {backup_path}")
|
|
661
|
+
print(f"\n To rollback: restore the backup file.")
|
|
662
|
+
return
|
|
663
|
+
|
|
664
|
+
if args.backup:
|
|
665
|
+
setup_logging(debug=args.debug)
|
|
666
|
+
db = get_db()
|
|
667
|
+
db.initialize()
|
|
668
|
+
backup_path = db.backup()
|
|
669
|
+
print(f"Backup created: {backup_path}")
|
|
670
|
+
return
|
|
671
|
+
|
|
274
672
|
# Run the daemon
|
|
275
673
|
run_daemon(mcp_mode=not args.standalone, debug=args.debug, project_id=project_id)
|
|
276
674
|
|
|
@@ -68,6 +68,16 @@ class MemoryConfig:
|
|
|
68
68
|
# Health check
|
|
69
69
|
health_port: int = 3848
|
|
70
70
|
|
|
71
|
+
# Backup settings
|
|
72
|
+
backup_retention_count: int = 3 # Number of rolling backups to keep
|
|
73
|
+
enable_pre_consolidation_backup: bool = True # Auto-backup before consolidation
|
|
74
|
+
|
|
75
|
+
# Retention settings (data cleanup during consolidation)
|
|
76
|
+
audit_log_retention_days: int = 90
|
|
77
|
+
prediction_retention_days: int = 30
|
|
78
|
+
turn_buffer_retention_days: int = 60
|
|
79
|
+
metrics_retention_days: int = 90
|
|
80
|
+
|
|
71
81
|
# Daemon settings
|
|
72
82
|
log_path: Path = field(default_factory=lambda: Path.home() / ".claudia" / "daemon.log")
|
|
73
83
|
|
|
@@ -119,6 +129,18 @@ class MemoryConfig:
|
|
|
119
129
|
config.fts_weight = data["fts_weight"]
|
|
120
130
|
if "health_port" in data:
|
|
121
131
|
config.health_port = data["health_port"]
|
|
132
|
+
if "backup_retention_count" in data:
|
|
133
|
+
config.backup_retention_count = data["backup_retention_count"]
|
|
134
|
+
if "enable_pre_consolidation_backup" in data:
|
|
135
|
+
config.enable_pre_consolidation_backup = data["enable_pre_consolidation_backup"]
|
|
136
|
+
if "audit_log_retention_days" in data:
|
|
137
|
+
config.audit_log_retention_days = data["audit_log_retention_days"]
|
|
138
|
+
if "prediction_retention_days" in data:
|
|
139
|
+
config.prediction_retention_days = data["prediction_retention_days"]
|
|
140
|
+
if "turn_buffer_retention_days" in data:
|
|
141
|
+
config.turn_buffer_retention_days = data["turn_buffer_retention_days"]
|
|
142
|
+
if "metrics_retention_days" in data:
|
|
143
|
+
config.metrics_retention_days = data["metrics_retention_days"]
|
|
122
144
|
if "log_path" in data:
|
|
123
145
|
config.log_path = Path(data["log_path"])
|
|
124
146
|
|
|
@@ -171,6 +193,20 @@ class MemoryConfig:
|
|
|
171
193
|
weights = self.vector_weight + self.importance_weight + self.recency_weight + self.fts_weight
|
|
172
194
|
if abs(weights - 1.0) > 0.01:
|
|
173
195
|
logger.warning(f"Ranking weights sum to {weights:.3f}, not 1.0. Results may be skewed.")
|
|
196
|
+
if self.backup_retention_count < 1:
|
|
197
|
+
logger.warning(f"backup_retention_count={self.backup_retention_count} below minimum, using 1")
|
|
198
|
+
self.backup_retention_count = 1
|
|
199
|
+
for attr in ("audit_log_retention_days", "prediction_retention_days", "turn_buffer_retention_days", "metrics_retention_days"):
|
|
200
|
+
val = getattr(self, attr)
|
|
201
|
+
if val < 1:
|
|
202
|
+
logger.warning(f"{attr}={val} below minimum, using 1")
|
|
203
|
+
setattr(self, attr, 1)
|
|
204
|
+
common_dims = {384, 512, 768, 1024, 1536}
|
|
205
|
+
if self.embedding_dimensions not in common_dims:
|
|
206
|
+
logger.warning(
|
|
207
|
+
f"embedding_dimensions={self.embedding_dimensions} is not a common value "
|
|
208
|
+
f"({sorted(common_dims)}). Verify this matches your embedding model's output."
|
|
209
|
+
)
|
|
174
210
|
|
|
175
211
|
def save(self) -> None:
|
|
176
212
|
"""Save current configuration to ~/.claudia/config.json"""
|
|
@@ -193,6 +229,12 @@ class MemoryConfig:
|
|
|
193
229
|
"recency_weight": self.recency_weight,
|
|
194
230
|
"fts_weight": self.fts_weight,
|
|
195
231
|
"health_port": self.health_port,
|
|
232
|
+
"backup_retention_count": self.backup_retention_count,
|
|
233
|
+
"enable_pre_consolidation_backup": self.enable_pre_consolidation_backup,
|
|
234
|
+
"audit_log_retention_days": self.audit_log_retention_days,
|
|
235
|
+
"prediction_retention_days": self.prediction_retention_days,
|
|
236
|
+
"turn_buffer_retention_days": self.turn_buffer_retention_days,
|
|
237
|
+
"metrics_retention_days": self.metrics_retention_days,
|
|
196
238
|
"log_path": str(self.log_path),
|
|
197
239
|
}
|
|
198
240
|
|
|
@@ -10,6 +10,7 @@ import logging
|
|
|
10
10
|
import threading
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
13
|
+
from pathlib import Path
|
|
13
14
|
from typing import Any, Callable, Dict, Optional
|
|
14
15
|
|
|
15
16
|
from ..config import get_config
|
|
@@ -68,6 +69,24 @@ def build_status_report(*, db=None) -> dict:
|
|
|
68
69
|
except Exception:
|
|
69
70
|
report["counts"][table] = -1
|
|
70
71
|
|
|
72
|
+
# Backup status
|
|
73
|
+
try:
|
|
74
|
+
import glob
|
|
75
|
+
db_path = str(get_config().db_path)
|
|
76
|
+
pattern = f"{db_path}.backup-*.db"
|
|
77
|
+
backups = sorted(glob.glob(pattern))
|
|
78
|
+
if backups:
|
|
79
|
+
latest = Path(backups[-1])
|
|
80
|
+
report["backup"] = {
|
|
81
|
+
"count": len(backups),
|
|
82
|
+
"latest_path": str(latest),
|
|
83
|
+
"latest_size_bytes": latest.stat().st_size if latest.exists() else 0,
|
|
84
|
+
}
|
|
85
|
+
else:
|
|
86
|
+
report["backup"] = {"count": 0}
|
|
87
|
+
except Exception:
|
|
88
|
+
report["backup"] = {"count": -1, "error": "unable to check"}
|
|
89
|
+
|
|
71
90
|
except Exception:
|
|
72
91
|
report["components"]["database"] = "error"
|
|
73
92
|
report["status"] = "degraded"
|